-
Notifications
You must be signed in to change notification settings - Fork 119
sweepbatcher: add mode with presigned transactions #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
3cbc2e6
to
418bd2d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work @starius. I've left a couple of questions in my first pass.
sweepbatcher/sweep_batch.go
Outdated
for _, s := range b.sweeps { | ||
if !s.presigned { | ||
b.log().Infof("failed to add sweep %x to the "+ | ||
"batch, because the batch has "+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe clarify "failed to add presigned sweep %x..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
sweepbatcher/sweep_batch.go
Outdated
// Ensure that all the sweeps in the batch don't use presigned. | ||
for _, s := range b.sweeps { | ||
if s.presigned { | ||
b.log().Infof("failed to add sweep %x to the "+ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe clarify "failed to add non-presigned sweep %x..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
sweepbatcher/sweep_batch.go
Outdated
|
||
for _, sweep := range b.sweeps { | ||
if sweep.presigned { | ||
return true, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if all sweeps are presigned we return true
, false otherwise. So we should return false
here if !sweep.presigned
, otherwise a presigned batch could still have non-presigned sweeps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I rewrote the function to ensure that all the sweeps have the same presigned status. If there is a mix, the function returns an error. This would be a bug.
sweepbatcher/sweep_batcher.go
Outdated
@@ -564,8 +628,40 @@ func (b *Batcher) Run(ctx context.Context) error { | |||
} | |||
} | |||
|
|||
// PresignSweep creates and stores presigned 1:1 transactions for the sweep. | |||
// This method must be called prior to AddSweep if presigned mode is enabled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this is ensured in the batch by b.ensurePresigned
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated ensurePresigned
and SignTx interface (added argument presignedOnly
), so ensurePresigned
actually ensured that it is presigned, not tried to sign (become read-only). Also added a test making sure that the batcher crashes if AddSweep is called before calling PresignSweep (for a presigned sweep). (AddSweep can't return that error, since it only pushes a sweep to the event loop.)
sweepbatcher/sweep_batcher.go
Outdated
} | ||
|
||
// Find the feerate needed to get into next block. Use conf_target=2, | ||
nextBlockFeerate, err := b.wallet.EstimateFeeRate(ctx, 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: camel case nextBlockFeeRate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
sweepbatcher/sweep_batch.go
Outdated
return false, nil | ||
} | ||
} else { | ||
if err := b.ensurePresigned(ctx, sweep); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if err == nil
here, don't we then have to call b.presign
for the sweep here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ensurePresigned
returned no error, this means, it has been presigned already. No need to presign again. If not presigned, then crash the batcher (this is only possible because of a bug).
sweepbatcher/sweep_batch.go
Outdated
// use the same mode (presigned or regular). | ||
if sweep.presigned { | ||
// Ensure that all the sweeps in the batch use presigned mode. | ||
for _, s := range b.sweeps { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of iterating, do you think it makes sense to add a presigned
flag to the batch struct that we can check before adding a sweep to a batch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I plan to allow half-presigned batches in the future to use a regular sweep an an anchor and such a flag would be removed. So I decided to keep this calculated not to forget to remove it in the future :)
sweepbatcher/presigned.go
Outdated
batchAmt += sweep.value | ||
} | ||
|
||
// Find when at least one sweep expires. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: "find the sweep with the earliest expiry"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
sweepbatcher/presigned.go
Outdated
} | ||
|
||
// Find the feerate needed to get into next block. Use conf_target=2, | ||
nextBlockFeerate, err := b.wallet.EstimateFeeRate(ctx, 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it 2? Maybe create a constant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated this place:
// priorityConfTarget defines the confirmation target for quick
// inclusion in a block. A value of 2, rather than 1, is used to prevent
// overestimation caused by temporary anomalies, such as periods without
// blocks.
const priorityConfTarget = 2
// Find the feerate needed to get into next block.
nextBlockFeeRate, err := b.wallet.EstimateFeeRate(
ctx, priorityConfTarget,
)
This fee rate is only used to define a high fee rate (10x of this value and at least 100 sats/vbyte).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW the fee estimator fails with conf_target=1: #898
Maybe it worth making this a global constant in Loop and use it in all such places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the comment:
// priorityConfTarget defines the confirmation target for quick
// inclusion in a block. A value of 2, rather than 1, is used to prevent
// fee estimator from failing.
// See https://github.com/lightninglabs/loop/issues/898
const priorityConfTarget = 2
613903a
to
b115f41
Compare
6abbc68
to
0006a7e
Compare
@hieblmi: review reminder |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's great work @starius, I've left a couple of more comments, looks almost good to me.
sweepbatcher/sweep_batcher.go
Outdated
|
||
// DestPkScript returns destination pkScript used in a presigned | ||
// transaction sweeping the inputs. Returns an error, if such tx | ||
// doesn't exist. If there are many such transactions, returns any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its not possible that if there are many such transactions, that they'd have different pkScripts, I assume.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, all of the transactions should have the same destination address, by construction. However this is not enforced by PresignedHelper
. In theory someone can call Presign
on transactions with the same list of inputs and different destination addresses and all of the addresses would be considered valid. We can demand PresignedHelper
to check that address is always the same, but this would make queries heavier. (Checking in Presign
that destination address is the same in all presigned transactions with such inputs.)
sweepbatcher/sweep_batch.go
Outdated
return false, nil | ||
} | ||
|
||
// If this is new batch being formed, make sure we already have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if this is a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
sweepbatcher/sweep_batch.go
Outdated
@@ -1960,7 +2116,21 @@ func (b *batch) persist(ctx context.Context) error { | |||
|
|||
// getBatchDestAddr returns the batch's destination address. If the batch | |||
// has already generated an address then the same one will be returned. | |||
// The method must not be used in presigned mode. Use getSweepsDestAddr instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we call this method in the comment getPresignedSweepsDestAddr
to clarify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed. Thanks!
sweepbatcher/presigned.go
Outdated
|
||
// getSweepsDestAddr returns the destination address used by a group of sweeps. | ||
// The method must be used in presigned mode only. | ||
func (b *batch) getSweepsDestAddr(ctx context.Context, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could call it getPresignedSweepsDestAddr
to make it clearer that this only makes sense in the context of presigned batches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed
sweepbatcher/presigned.go
Outdated
return fmt.Errorf("presignedHelper is not installed") | ||
} | ||
if len(b.sweeps) != 0 { | ||
return fmt.Errorf("ensurePresigned is done when adding to an " + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: shoud be done...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
sweepbatcher/presigned.go
Outdated
for _, sweep := range newSweeps { | ||
batchAmt += sweep.value | ||
} | ||
const presignedOnly = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we could call this flag loadOnly
or something similar?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to loadOnly
.
sweepbatcher/presigned.go
Outdated
) | ||
|
||
// ensurePresigned checks that we can sign a 1:1 transaction sweeping the input. | ||
func (b *batch) ensurePresigned(ctx context.Context, newSweeps []*sweep) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does ensurePresigned
imply that there should be already be a transaction with given inputs that has a populated witness? I don't see a check anywhere in this method or sub-method that checks for a witness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a check to CheckSignedTx
which is called by this method:
// Make sure all inputs of signedTx have a non-empty witness.
for _, txIn := range signedTx.TxIn {
if len(txIn.Witness) == 0 {
return fmt.Errorf("input %s of signed tx is not signed",
txIn.PreviousOutPoint)
}
}
sweepbatcher/presigned.go
Outdated
} | ||
|
||
// Try to presign this transaction. | ||
err = presigner.Presign(ctx, tx, batchAmt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a side question: Currently the result is not yet used. Is it intended to then pass this pre-signed transaction into our store? I guess once the batch is confirmed we could clean up all the presigned fee versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, would Presigner
handle persisting the txns? Otherwise wouldn't that require another client change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PresignHelper
is responsible for storing the transactions. SweepBatcher relies on helper.SignTx
to be able to load transactions stored by previous helper.Presign
and helper.SignTx
calls.
I guess once the batch is confirmed we could clean up all the presigned fee versions.
This is done. There is the method CleanupTransactions
which is called by batch.handleConf
method.
ctx, tx, batchAmt, minRelayFee, feeRate, presignedOnly, | ||
) | ||
if err != nil { | ||
return 0, fmt.Errorf("failed to sign tx: %w", err), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it trying to sign when presignedOnly
is passed in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
presignedOnly = false
in this call, so it tries to sign interactively and if it fails, goes to the store of presigned transactions.
} | ||
|
||
// CheckSignedTx makes sure that signedTx matches the unsignedTx. It checks | ||
// according to criteria specified in the description of PresignedHelper.SignTx. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like the input sequence numbers aren't checked as described in the godoc of SignTx
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sequence numbers are compared using unsignedMap
. Also there is a test case to verify this.
Found a couple of edge cases. Converting to a draft until the fix is ready. |
55dfa9f
to
5c68867
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments and questions for the latest set of changes.
@hieblmi Thanks for comments! 🎉 Also found a small edge case for function |
930d193
to
065ff70
Compare
Updated the last commit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really amazing work on this @starius. LGTM!
@hieblmi I expanded the purging test to verify that if the sweeps are online at the time of purging (i.e. confirmed tx doesn't have them and they have to be re-added to the batcher) they can be successfully added to an existing batch. I did it in a temp commit to make it easier to re-review the change. |
@hieblmi One more change. I replaces |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really good! My only complaint is the possible timeout overflow, otherwise LGTM!
sweepbatcher/presigned.go
Outdated
) | ||
|
||
// Calculate the locktime value to use for high feerate transactions. | ||
highFeeRateLockTime := uint32(timeout - timeoutThreshold) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can overflow if timeout
is less than 50.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this to fix this edge case. Thanks! Added a test case.
// Calculate the locktime value to use for high feerate transactions.
// If timeout <= timeoutThreshold, don't set LockTime (keep value 0).
var highFeeRateLockTime uint32
if timeout > timeoutThreshold {
highFeeRateLockTime = uint32(timeout - timeoutThreshold)
}
@bhandras I pushed two commits to squash them later. They have the fixes for your comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice additions @starius 🥇 LGTM! 🎉
Also added a unit test for it.
Previously it may round one satoshi towards zero resulting in fee-rate slightly lower than requested.
This is needed to run additional checks in AddSweep in the next commit.
In this mode sweepbatcher uses transactions provided by the Presigned helper. Transactions are signed upon adding an input to a batch. A single Batcher instance can handle both presigned and regular batches. Currently presigned and non-presigned sweeps never appear in the same batch.
In this mode sweepbatcher uses transactions provided by the Presigned helper.
Transactions are signed upon adding an input to a batch.
A single Batcher instance can handle both presigned and regular batches.
Currently presigned and non-presigned sweeps never appear in the same batch.
Pull Request Checklist
release_notes.md
if your PR contains major features, breaking changes or bugfixes