-
Notifications
You must be signed in to change notification settings - Fork 116
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
sweepbatcher: add greedy batch selection algorithm #787
sweepbatcher: add greedy batch selection algorithm #787
Conversation
34c2c1c
to
5747074
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.
Looks good, just a few comments!
destAddr: s.DestAddr, | ||
minFeeRate: s.MinFeeRate, | ||
}, nil | ||
return b.loadSweep(ctx, dbSweep.SwapHash, dbSweep.Outpoint, |
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.
🎉
@@ -898,3 +886,12 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash, | |||
minFeeRate: s.MinFeeRate, | |||
}, nil | |||
} | |||
|
|||
// newBatchConfig creates new batch config. | |||
func (b *Batcher) newBatchConfig(maxTimeoutDistance int32) batchConfig { |
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.
👍
@@ -895,3 +875,18 @@ func (b *Batcher) newBatchConfig(maxTimeoutDistance int32) batchConfig { | |||
customMuSig2Signer: b.customMuSig2Signer, | |||
} | |||
} | |||
|
|||
// newBatchKit creates new batch kit. | |||
func (b *Batcher) newBatchKit() batchKit { |
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.
Awesome!
sweep/sweeper.go
Outdated
|
||
// Add output. | ||
if err := AddOutputEstimate(&weightEstimate, destAddr); err != nil { | ||
return 0, 0, 0, fmt.Errorf("estimate fee: %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.
nit: error message seems out of context, perhaps a bit more detail could be helpful.
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. Also added error message in case adding input fails.
@@ -848,6 +846,19 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash, | |||
swapHash[:6], err) | |||
} | |||
|
|||
var minFeeRate chainfee.SatPerKWeight |
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 this should actually be initialized to chainfee.AbsoluteFeePerKwFloor
so if the custom fee rate provider is unset then it still makes sense.
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.
minFeeRate is 0 be default. If customFeeRate is not provided, then rbfCache.FeeRate is also 0 and method batch.updateRbfRate() updates it to current fee rate according to batchConfTarget.
Added a comment to that code about this.
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.
SGTM
const specialId = -1 | ||
|
||
// entity is either batch of sweep and it holds data important for selection. | ||
type entity struct { |
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 entity
is not necessarily the best name as it sounds a bit too general. Maybe consider something like sweepSet
or sweepEntity
? Also given that the role of this type is to store weight and fee information perhaps we can be even more specific: sweepFeeDetails
?
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 renamed entity
to feeDetails
.
} | ||
|
||
// estimateSweepWeight returns sweep's input weight and one sweep batch weight. | ||
func estimateSweepWeight(s *sweep) (*entity, *entity, 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.
nit: i think returning values instead of pointers is cleaner.
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.
Done
} | ||
|
||
// estimateSweepWeight returns sweep's input weight and one sweep batch weight. | ||
func estimateSweepWeight(s *sweep) (*entity, *entity, 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.
nit: perhaps naming this functions as estimateSweepFeeDelta
or estimateSweepFeeIncrement
could be more readable. Maybe the godoc can also be made clearer slightly.
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 estimateSweepFeeIncrement
and added more godoc:
// estimateSweepFeeIncrement returns fee details for adding the sweep to a batch
// and for creating new batch with this sweep only.
func estimateSweepFeeIncrement(s *sweep) (feeDetails, feeDetails, error) {
6b0293a
to
bf645d0
Compare
@bhandras Thanks for the review! 💯 I fixed the comments. Please take another look! |
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.
LGTM 🎉
sweepbatcher/sweep_batcher.go
Outdated
@@ -848,6 +846,22 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash, | |||
swapHash[:6], err) | |||
} | |||
|
|||
// minFeeRate is 0 be default. If customFeeRate is not provided, then |
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: s/be/by
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.
Thanks! Fixed
@@ -848,6 +846,19 @@ func (b *Batcher) loadSweep(ctx context.Context, swapHash lntypes.Hash, | |||
swapHash[:6], err) | |||
} | |||
|
|||
var minFeeRate chainfee.SatPerKWeight |
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.
SGTM
} | ||
|
||
// Estimate new batch. | ||
newBatch, err := estimateBatchWeight(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.
nit: to avoid confusing names and improve readability, consider naming newBatch
=> feeDetails1
and batch2
=> feeDetails2
or something similar as these are not 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.
I renamed them to fd1
and fd2
.
// details has also IsExternalAddr flag. There is a rule that sweeps having flag | ||
// IsExternalAddr must go in individual batches. Cooperative spending is only | ||
// available if all the sweeps support cooperative spending path. | ||
func selectBatch(batches []feeDetails, sweep, oneSweepBatch feeDetails) (int32, 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.
nit: line too long
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
18ab904
to
64dc3f2
Compare
Updated commit "sweepbatcher: fix inaccuracies in fee estimations": function |
64dc3f2
to
64d9938
Compare
Rebased |
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.
Great effort to save fees for the users of loop out💰. I've left a few comments.
I will test this locally next.
sweepbatcher/sweep_batcher.go
Outdated
@@ -465,6 +470,19 @@ func (b *Batcher) handleSweep(ctx context.Context, sweep *sweep, | |||
} | |||
} | |||
|
|||
// If custom fee rate provider is used, run the greedy algorithm of | |||
// batch selection to minimize costs. | |||
if b.customFeeRate != 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.
Why do we try to minimize only if there's a custom fee provider active? Isn't it possible to optimize when multiple batches exist with the internal fee rate calculation?
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, how are you planning to enable the custom fee rates? Will this be a user configured parameter which will be passed in as option to batcher := sweepbatcher.NewBatcher(
?
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.
Great idea! We can always use it. I pushed two commits:
- sweepbatcher: always fill sweep.minFeeRate
- sweepbatcher: always try greedy batch selection
to implement this.
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 commit "sweepbatcher: always fill sweep.minFeeRate". Previous version of it caused an extra fee bump and failure of test TestCustomSweepConfTarget. Fixed.
Also pushed commit "loopout_test: enable logging in sweepbatcher", it helped debugging the issue.
ee100d7
to
fece888
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.
LGTM 💾 Frequent loop-out users will love this feature!
// of each alternative. Based on the estimates of selectBatch(), this method | ||
// adds the sweep to the batch that results in the least overall fee increase, | ||
// or creates new batch for it. If this method fails for whatever reason, the | ||
// caller falls back to the simple algorithm (method handleSweep). |
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.
👍 good idea to keep the existing mechanism as fall back
batchId, err := selectBatch( | ||
batches, sweepFeeDetails, newBatchFeeDetails, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("batch selection algorithm failed for sweep "+ | ||
"%x: %w", sweep.swapHash[:6], err) | ||
} | ||
|
||
// If the best option is to start new batch, do it. | ||
if batchId == newBatchSignal { | ||
return b.spinUpNewBatch(ctx, sweep) | ||
} | ||
|
||
// Locate the batch to add the sweep to. | ||
bestBatch, has := b.batches[batchId] | ||
if !has { | ||
return fmt.Errorf("batch selection algorithm returned "+ | ||
"batch id %d which doesn't exist, for sweep %x", | ||
batchId, sweep.swapHash[:6]) | ||
} | ||
|
||
// Add the sweep to the batch. | ||
accepted, err := bestBatch.addSweep(ctx, sweep) |
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 may need to be enhanced
Currently, addSweep
will also run checks that are related to the timeout window of the batch vs the sweep that is being added. See here https://github.com/lightninglabs/loop/blob/master/sweepbatcher/sweep_batch.go#L440-L447
This means that the single best solution that the greedy algorithm has calculated may be rejected because the timeout heights of the sweeps are too far away. I believe this is not the desired behavior?
I see 2 possible paths here:
a) Create a version of addSweep
that does not check for the timeout window limits, allowing swaps with very distant timeout heights to be in the same batch
b) Enhance algorithm to return an array of "best batches", sorted from best to worst. Then on the above line (76
) we would have to iterate over the results and try to addSweep
in order from the best to worst option
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.
Of course there's option "c) keep it as is"
which will probably cause high failure rates when calling greedyAddSweep
, which is OK as long as we fall back to legacy batch selection
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.
How often does it happen that a sweep is rejected based on the timeout? If that's not a common case then we can KISS and just try and add to the selected greedy batch, and if timeout prevents addition then fail the greedy algo and fall back to the default. Wdyt?
Didn't see your c)
, is a high failure rate expected?
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.
Didn't see your c), is a high failure rate expected?
That highly depends on the type of looper, frequent loopers will probably encounter this more often, as they are the ones having many batches running in parallel
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 like the option b) Enhance algorithm to return an array of "best batches", sorted from best to worst.
The selection algorithm already has a metric by which is compares all the alternatives, so it is easy to extend to return a sorted array.
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 implemented it and pushed a temporary commit with this change. Please take a look! I'll squash it before merging.
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.
awesome 🔥
LGTM
return nil | ||
} | ||
|
||
log.Infof("Batch selection algorithm returned batch id %d for "+ |
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: I think it's better to do Debugf
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.
Done 👍
// Sort the alternatives by cost. The lower the cost, the better. | ||
sort.Slice(alternatives, func(i, j int) bool { | ||
return alternatives[i].cost < alternatives[j].cost | ||
}) |
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.
beautiful 💯
The reason is because we want to know in advance if fee rates come from an external source or are determined by sweepbatcher internally. Also remove WithNoBumping option. It is now a part of WithCustomFeeRate: if WithCustomFeeRate is passed, automatic fee bumping by sweepbatcher is disabled.
It adds output to transaction weight estimator depending on address type.
If custom fee rates are used, the algorithm is tried. It selects a batch for the sweep using the greedy algorithm, which minimizes costs, and adds the sweep to the batch. If it fails, old algorithm is used, trying to add to any batch, or starting a new batch.
Use SigHashDefault instead of SigHashAll in weight estimations. Actual code uses SigHashDefault, not SigHashAll. SigHashAll is 1wu larger. Use the actual destination address in weight estimator, not taproot always. In the test the type of address is P2WPKH, not taproot. Updated testSweepFetcher to calculate fee, fee rate and weight accurately and to compare the data observed in the published transaction with the estimates.
This is useful to debug. It used to fail silently.
Use customFeeRate if it is provided, otherwise use wallet's EstimateFeeRate. Added flag SkipNextBump to rbfCache to avoid extra bumping upon updating fee rate externally. Fix test TestSweepBatcherCloseDuringAdding, it didn't have confTarget and failed in wallet.EstimateFeeRate call.
Now that sweep.minFeeRate is always set, greedy batch selection has all needed inputs to work even without customFeeRate provider.
60d06ab
to
7a61d5e
Compare
New algorithm is tried to select a batch to add a sweep to. It selects a batch for the sweep using the greedy algorithm, which minimizes costs, and adds the sweep to the batch or creates new batch, if it is the most cost efficient alternative. If the batch does not accept the sweep (too long timeout distance), next batch is tried. If the whole new approach fails, the old algorithm is used, trying to add to any batch, or starting a new batch.
The PR also includes some refactorings and fixes small inaccuracies in fee estimations.
Pull Request Checklist
release_notes.md
if your PR contains major features, breaking changes or bugfixes