Skip to content

Commit e8cf638

Browse files
committed
sweepbatcher: allow adding groups of inputs
A group of inputs can be added by passing it in SweepRequest.Inputs field. All the inputs belong to the same swap and are added to the same batch.
1 parent dc7261b commit e8cf638

6 files changed

+762
-396
lines changed

loopout.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1159,8 +1159,12 @@ func (s *loopOutSwap) waitForHtlcSpendConfirmedV2(globalCtx context.Context,
11591159

11601160
sweepReq := sweepbatcher.SweepRequest{
11611161
SwapHash: s.hash,
1162-
Outpoint: htlcOutpoint,
1163-
Value: htlcValue,
1162+
Inputs: []sweepbatcher.Input{
1163+
{
1164+
Outpoint: htlcOutpoint,
1165+
Value: htlcValue,
1166+
},
1167+
},
11641168
Notifier: &notifier,
11651169
}
11661170

sweepbatcher/greedy_batch_selection.go

+64-44
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,34 @@ import (
1515
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
1616
)
1717

18-
// greedyAddSweep selects a batch for the sweep using the greedy algorithm,
19-
// which minimizes costs, and adds the sweep to the batch. To accomplish this,
20-
// it first collects fee details about the sweep being added, about a potential
21-
// new batch composed of this sweep only, and about all existing batches. It
18+
// greedyAddSweeps selects a batch for the sweeps using the greedy algorithm,
19+
// which minimizes costs, and adds the sweeps to the batch. To accomplish this,
20+
// it first collects fee details about the sweeps being added, about a potential
21+
// new batch composed of these sweeps only, and about all existing batches. It
2222
// skips batches with at least MaxSweepsPerBatch swaps to keep tx standard. Then
2323
// it passes the data to selectBatches() function, which emulates adding the
24-
// sweep to each batch and creating new batch for the sweep, and calculates the
24+
// sweep to each batch and creating new batch for the sweeps, and calculates the
2525
// costs of each alternative. Based on the estimates of selectBatches(), this
26-
// method adds the sweep to the batch that results in the least overall fee
27-
// increase, or creates new batch for it. If the sweep is not accepted by an
26+
// method adds the sweeps to the batch that results in the least overall fee
27+
// increase, or creates new batch for it. If the sweeps are not accepted by an
2828
// existing batch (may happen because of too distant timeouts), next batch is
2929
// tried in the list returned by selectBatches(). If adding fails or new batch
3030
// creation fails, this method returns an error. If this method fails for any
3131
// reason, the caller falls back to the simple algorithm (method handleSweep).
32-
func (b *Batcher) greedyAddSweep(ctx context.Context, sweep *sweep) error {
32+
func (b *Batcher) greedyAddSweeps(ctx context.Context, sweeps []*sweep) error {
33+
if len(sweeps) == 0 {
34+
return fmt.Errorf("trying to greedy add an empty sweeps group")
35+
}
36+
37+
swap := sweeps[0].swapHash
38+
3339
// Collect weight and fee rate info about the sweep and new batch.
3440
sweepFeeDetails, newBatchFeeDetails, err := estimateSweepFeeIncrement(
35-
sweep,
41+
sweeps,
3642
)
3743
if err != nil {
3844
return fmt.Errorf("failed to estimate tx weight for "+
39-
"sweep %x: %w", sweep.swapHash[:6], err)
45+
"sweep %x: %w", swap[:6], err)
4046
}
4147

4248
// Collect weight and fee rate info about existing batches.
@@ -64,54 +70,65 @@ func (b *Batcher) greedyAddSweep(ctx context.Context, sweep *sweep) error {
6470
)
6571
if err != nil {
6672
return fmt.Errorf("batch selection algorithm failed for sweep "+
67-
"%x: %w", sweep.swapHash[:6], err)
73+
"%x: %w", swap[:6], err)
6874
}
6975

7076
// Try batches, starting with the best.
7177
for _, batchId := range batchesIds {
7278
// If the best option is to start new batch, do it.
7379
if batchId == newBatchSignal {
74-
return b.spinUpNewBatch(ctx, sweep)
80+
return b.spinUpNewBatch(ctx, sweeps)
7581
}
7682

77-
// Locate the batch to add the sweep to.
83+
// Locate the batch to add the sweeps to.
7884
bestBatch, has := b.batches[batchId]
7985
if !has {
8086
return fmt.Errorf("batch selection algorithm returned "+
8187
"batch id %d which doesn't exist, for sweep %x",
82-
batchId, sweep.swapHash[:6])
88+
batchId, swap[:6])
8389
}
8490

85-
// Add the sweep to the batch.
86-
accepted, err := bestBatch.addSweep(ctx, sweep)
91+
// Add the sweeps to the batch.
92+
accepted, err := bestBatch.addSweeps(ctx, sweeps)
8793
if err != nil {
8894
return fmt.Errorf("batch selection algorithm returned "+
8995
"batch id %d for sweep %x, but adding failed: "+
90-
"%w", batchId, sweep.swapHash[:6], err)
96+
"%w", batchId, swap[:6], err)
9197
}
9298
if accepted {
9399
return nil
94100
}
95101

96102
debugf("Batch selection algorithm returned batch id %d "+
97103
"for sweep %x, but acceptance failed.", batchId,
98-
sweep.swapHash[:6])
104+
swap[:6])
99105
}
100106

101-
return fmt.Errorf("no batch accepted sweep %x", sweep.swapHash[:6])
107+
return fmt.Errorf("no batch accepted sweep group %x", swap[:6])
102108
}
103109

104-
// estimateSweepFeeIncrement returns fee details for adding the sweep to a batch
105-
// and for creating new batch with this sweep only.
106-
func estimateSweepFeeIncrement(s *sweep) (feeDetails, feeDetails, error) {
107-
// Create a fake batch with this sweep.
110+
// estimateSweepFeeIncrement returns fee details for adding the sweeps to
111+
// a batch and for creating new batch with these sweeps only.
112+
func estimateSweepFeeIncrement(
113+
sweeps []*sweep) (feeDetails, feeDetails, error) {
114+
115+
if len(sweeps) == 0 {
116+
return feeDetails{}, feeDetails{}, fmt.Errorf("estimating an " +
117+
"empty group of sweeps")
118+
}
119+
120+
// Create a fake batch with the sweeps.
108121
batch := &batch{
109122
rbfCache: rbfCache{
110-
FeeRate: s.minFeeRate,
111-
},
112-
sweeps: map[wire.OutPoint]sweep{
113-
s.outpoint: *s,
123+
FeeRate: sweeps[0].minFeeRate,
114124
},
125+
sweeps: make(map[wire.OutPoint]sweep, len(sweeps)),
126+
}
127+
for _, s := range sweeps {
128+
batch.sweeps[s.outpoint] = *s
129+
batch.rbfCache.FeeRate = max(
130+
batch.rbfCache.FeeRate, s.minFeeRate,
131+
)
115132
}
116133

117134
// Estimate new batch.
@@ -120,14 +137,17 @@ func estimateSweepFeeIncrement(s *sweep) (feeDetails, feeDetails, error) {
120137
return feeDetails{}, feeDetails{}, err
121138
}
122139

123-
// Add the same sweep again to measure weight increments.
124-
outpoint2 := s.outpoint
125-
outpoint2.Hash[0]++
126-
if _, has := batch.sweeps[outpoint2]; has {
127-
return feeDetails{}, feeDetails{}, fmt.Errorf("dummy outpoint "+
128-
"%s is present in the batch", outpoint2)
140+
// Add the same sweeps again with different outpoints to measure weight
141+
// increments.
142+
for _, s := range sweeps {
143+
dummy := s.outpoint
144+
dummy.Hash[0]++
145+
if _, has := batch.sweeps[dummy]; has {
146+
return feeDetails{}, feeDetails{}, fmt.Errorf("dummy "+
147+
"outpoint %s is present in the batch", dummy)
148+
}
149+
batch.sweeps[dummy] = *s
129150
}
130-
batch.sweeps[outpoint2] = *s
131151

132152
// Estimate weight of a batch with two sweeps.
133153
fd2, err := estimateBatchWeight(batch)
@@ -137,8 +157,8 @@ func estimateSweepFeeIncrement(s *sweep) (feeDetails, feeDetails, error) {
137157

138158
// Create feeDetails for sweep.
139159
sweepFeeDetails := feeDetails{
140-
FeeRate: s.minFeeRate,
141-
IsExternalAddr: s.isExternalAddr,
160+
FeeRate: batch.rbfCache.FeeRate,
161+
IsExternalAddr: sweeps[0].isExternalAddr,
142162

143163
// Calculate sweep weight as a difference.
144164
Weight: fd2.Weight - fd1.Weight,
@@ -252,10 +272,10 @@ func (e1 feeDetails) combine(e2 feeDetails) feeDetails {
252272
// rate and a weight is provided. Also, a hint is provided to signal which
253273
// spending path will be used by the batch.
254274
//
255-
// The same data is also provided for the sweep for which we are selecting a
256-
// batch to add. In case of the sweep weights are weight deltas resulted from
257-
// adding the sweep. Finally, the same data is provided for new batch having
258-
// this sweep only.
275+
// The same data is also provided for the sweep (or sweeps) for which we are
276+
// selecting a batch to add. In case of the sweep weights are weight deltas
277+
// resulted from adding the sweep. Finally, the same data is provided for new
278+
// batch having this sweep(s) only.
259279
//
260280
// The algorithm compares costs of adding the sweep to each existing batch, and
261281
// costs of new batch creation for this sweep and returns BatchId of the winning
@@ -265,11 +285,11 @@ func (e1 feeDetails) combine(e2 feeDetails) feeDetails {
265285
// having flag IsExternalAddr must go in individual batches. Cooperative
266286
// spending may only be available for some sweeps supporting it, not for all.
267287
func selectBatches(batches []feeDetails,
268-
sweep, oneSweepBatch feeDetails) ([]int32, error) {
288+
added, newBatch feeDetails) ([]int32, error) {
269289

270290
// If the sweep has IsExternalAddr flag, the sweep can't be added to
271291
// a batch, so create new batch for it.
272-
if sweep.IsExternalAddr {
292+
if added.IsExternalAddr {
273293
return []int32{newBatchSignal}, nil
274294
}
275295

@@ -286,7 +306,7 @@ func selectBatches(batches []feeDetails,
286306
// creation with this sweep only in it. The cost is its full fee.
287307
alternatives = append(alternatives, alternative{
288308
batchId: newBatchSignal,
289-
cost: oneSweepBatch.fee(),
309+
cost: newBatch.fee(),
290310
})
291311

292312
// Try to add the sweep to every batch, calculate the costs and
@@ -299,7 +319,7 @@ func selectBatches(batches []feeDetails,
299319
}
300320

301321
// Add the sweep to the batch virtually.
302-
combinedBatch := batch.combine(sweep)
322+
combinedBatch := batch.combine(added)
303323

304324
// The cost is the fee increase.
305325
cost := combinedBatch.fee() - batch.fee()

sweepbatcher/greedy_batch_selection_test.go

+71-26
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,28 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
7474
trAddr := (*btcutil.AddressTaproot)(nil)
7575
p2pkhAddr := (*btcutil.AddressPubKeyHash)(nil)
7676

77+
outpoint1 := wire.OutPoint{
78+
Hash: chainhash.Hash{1, 1, 1},
79+
Index: 1,
80+
}
81+
outpoint2 := wire.OutPoint{
82+
Hash: chainhash.Hash{2, 2, 2},
83+
Index: 2,
84+
}
85+
7786
cases := []struct {
7887
name string
79-
sweep *sweep
88+
sweeps []*sweep
8089
wantSweepFeeDetails feeDetails
8190
wantNewBatchFeeDetails feeDetails
8291
}{
8392
{
8493
name: "regular",
85-
sweep: &sweep{
86-
minFeeRate: lowFeeRate,
87-
htlcSuccessEstimator: se3,
94+
sweeps: []*sweep{
95+
{
96+
minFeeRate: lowFeeRate,
97+
htlcSuccessEstimator: se3,
98+
},
8899
},
89100
wantSweepFeeDetails: feeDetails{
90101
FeeRate: lowFeeRate,
@@ -98,9 +109,11 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
98109

99110
{
100111
name: "high fee rate",
101-
sweep: &sweep{
102-
minFeeRate: highFeeRate,
103-
htlcSuccessEstimator: se3,
112+
sweeps: []*sweep{
113+
{
114+
minFeeRate: highFeeRate,
115+
htlcSuccessEstimator: se3,
116+
},
104117
},
105118
wantSweepFeeDetails: feeDetails{
106119
FeeRate: highFeeRate,
@@ -114,11 +127,13 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
114127

115128
{
116129
name: "isExternalAddr taproot",
117-
sweep: &sweep{
118-
minFeeRate: lowFeeRate,
119-
htlcSuccessEstimator: se3,
120-
isExternalAddr: true,
121-
destAddr: trAddr,
130+
sweeps: []*sweep{
131+
{
132+
minFeeRate: lowFeeRate,
133+
htlcSuccessEstimator: se3,
134+
isExternalAddr: true,
135+
destAddr: trAddr,
136+
},
122137
},
123138
wantSweepFeeDetails: feeDetails{
124139
FeeRate: lowFeeRate,
@@ -134,11 +149,13 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
134149

135150
{
136151
name: "isExternalAddr P2PKH",
137-
sweep: &sweep{
138-
minFeeRate: lowFeeRate,
139-
htlcSuccessEstimator: se3,
140-
isExternalAddr: true,
141-
destAddr: p2pkhAddr,
152+
sweeps: []*sweep{
153+
{
154+
minFeeRate: lowFeeRate,
155+
htlcSuccessEstimator: se3,
156+
isExternalAddr: true,
157+
destAddr: p2pkhAddr,
158+
},
142159
},
143160
wantSweepFeeDetails: feeDetails{
144161
FeeRate: lowFeeRate,
@@ -155,10 +172,12 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
155172

156173
{
157174
name: "non-coop",
158-
sweep: &sweep{
159-
minFeeRate: lowFeeRate,
160-
htlcSuccessEstimator: se3,
161-
nonCoopHint: true,
175+
sweeps: []*sweep{
176+
{
177+
minFeeRate: lowFeeRate,
178+
htlcSuccessEstimator: se3,
179+
nonCoopHint: true,
180+
},
162181
},
163182
wantSweepFeeDetails: feeDetails{
164183
FeeRate: lowFeeRate,
@@ -172,10 +191,12 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
172191

173192
{
174193
name: "coop-failed",
175-
sweep: &sweep{
176-
minFeeRate: lowFeeRate,
177-
htlcSuccessEstimator: se3,
178-
coopFailed: true,
194+
sweeps: []*sweep{
195+
{
196+
minFeeRate: lowFeeRate,
197+
htlcSuccessEstimator: se3,
198+
coopFailed: true,
199+
},
179200
},
180201
wantSweepFeeDetails: feeDetails{
181202
FeeRate: lowFeeRate,
@@ -186,12 +207,36 @@ func TestEstimateSweepFeeIncrement(t *testing.T) {
186207
Weight: nonCoopNewBatchWeight,
187208
},
188209
},
210+
211+
{
212+
name: "two sweeps",
213+
sweeps: []*sweep{
214+
{
215+
outpoint: outpoint1,
216+
minFeeRate: lowFeeRate,
217+
htlcSuccessEstimator: se3,
218+
},
219+
{
220+
outpoint: outpoint2,
221+
minFeeRate: highFeeRate,
222+
htlcSuccessEstimator: se3,
223+
},
224+
},
225+
wantSweepFeeDetails: feeDetails{
226+
FeeRate: highFeeRate,
227+
Weight: coopInputWeight * 2,
228+
},
229+
wantNewBatchFeeDetails: feeDetails{
230+
FeeRate: highFeeRate,
231+
Weight: coopNewBatchWeight + coopInputWeight,
232+
},
233+
},
189234
}
190235

191236
for _, tc := range cases {
192237
t.Run(tc.name, func(t *testing.T) {
193238
gotSweepFeeDetails, gotNewBatchFeeDetails, err :=
194-
estimateSweepFeeIncrement(tc.sweep)
239+
estimateSweepFeeIncrement(tc.sweeps)
195240
require.NoError(t, err)
196241
require.Equal(
197242
t, tc.wantSweepFeeDetails, gotSweepFeeDetails,

0 commit comments

Comments
 (0)