Skip to content

Commit 16ebc11

Browse files
committed
sweepbatcher: consolidate identical change pkscripts
batch separate change outputs with identical pkscripts are tallied up and consolidated to a single change output.
1 parent 6ace0b1 commit 16ebc11

File tree

5 files changed

+305
-8
lines changed

5 files changed

+305
-8
lines changed

sweepbatcher/greedy_batch_selection.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,21 @@ func estimateBatchWeight(batch *batch) (feeDetails, error) {
210210
err)
211211
}
212212

213-
// Add change output weights.
213+
// Add change output weights. Change outputs with identical pkscript
214+
// will be consolidated into a single output.
215+
changeOutputs := make(map[string]struct{})
214216
for _, s := range batch.sweeps {
215-
if s.change != nil {
216-
weight.AddOutput(s.change.PkScript)
217+
if s.change == nil {
218+
continue
217219
}
220+
221+
pkScriptString := string(s.change.PkScript)
222+
if _, has := changeOutputs[pkScriptString]; has {
223+
continue
224+
}
225+
226+
weight.AddOutput(s.change.PkScript)
227+
changeOutputs[pkScriptString] = struct{}{}
218228
}
219229

220230
// Add inputs.

sweepbatcher/greedy_batch_selection_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636

3737
coopTwoSweepBatchWeight = coopNewBatchWeight + coopInputWeight
3838
coopSingleSweepChangeBatchWeight = coopInputWeight + batchOutputWeight + changeOutputWeight
39+
coopDoubleSweepChangeBatchWeight = 2*coopInputWeight + batchOutputWeight + changeOutputWeight
3940
nonCoopTwoSweepBatchWeight = coopTwoSweepBatchWeight + 2*nonCoopPenalty
4041
v2v3BatchWeight = nonCoopTwoSweepBatchWeight - 25
4142
mixedTwoSweepBatchWeight = coopTwoSweepBatchWeight + nonCoopPenalty
@@ -325,6 +326,35 @@ func TestEstimateBatchWeight(t *testing.T) {
325326
},
326327
},
327328

329+
{
330+
name: "two sweeps regular batch with identical change",
331+
batch: &batch{
332+
id: 1,
333+
rbfCache: rbfCache{
334+
FeeRate: lowFeeRate,
335+
},
336+
sweeps: map[wire.OutPoint]sweep{
337+
outpoint1: {
338+
htlcSuccessEstimator: se3,
339+
change: &wire.TxOut{
340+
PkScript: changePkscript,
341+
},
342+
},
343+
outpoint2: {
344+
htlcSuccessEstimator: se3,
345+
change: &wire.TxOut{
346+
PkScript: changePkscript,
347+
},
348+
},
349+
},
350+
},
351+
wantBatchFeeDetails: feeDetails{
352+
BatchId: 1,
353+
FeeRate: lowFeeRate,
354+
Weight: coopDoubleSweepChangeBatchWeight,
355+
},
356+
},
357+
328358
{
329359
name: "two sweeps regular batch",
330360
batch: &batch{

sweepbatcher/sweep_batch.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,10 +1260,22 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
12601260
LockTime: uint32(currentHeight),
12611261
}
12621262

1263-
var changeOutputs []*wire.TxOut
1264-
for _, sweep := range sweeps {
1265-
if sweep.change != nil {
1266-
changeOutputs = append(changeOutputs, sweep.change)
1263+
// Consolidate change outputs with identical pkscript.
1264+
changeOutputs := make(map[string]*wire.TxOut)
1265+
for _, s := range sweeps {
1266+
if s.change == nil {
1267+
continue
1268+
}
1269+
1270+
stringPkScript := string(s.change.PkScript)
1271+
if _, has := changeOutputs[stringPkScript]; has {
1272+
changeOutputs[stringPkScript].Value += s.change.Value
1273+
continue
1274+
}
1275+
1276+
changeOutputs[stringPkScript] = &wire.TxOut{
1277+
Value: s.change.Value,
1278+
PkScript: s.change.PkScript,
12671279
}
12681280
}
12691281

sweepbatcher/sweep_batch_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ func TestConstructUnsignedTx(t *testing.T) {
5555
PkScript: change1Pkscript,
5656
}
5757

58+
change1PrimeAddr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" +
59+
"lwyyeq5ryn6r"
60+
change1PrimeAddress, err := btcutil.DecodeAddress(change1PrimeAddr, nil)
61+
require.NoError(t, err)
62+
change1PrimePkscript, err := txscript.PayToAddrScript(change1PrimeAddress)
63+
require.NoError(t, err)
64+
change1Prime := &wire.TxOut{
65+
Value: 200_000,
66+
PkScript: change1PrimePkscript,
67+
}
68+
5869
change2Addr := "bc1psw0nrrulq4pgyuyk09a3wsutygltys4gxjjw3zl2uz4ep8pa" +
5970
"r2vsvntfe0"
6071
change2Address, err := btcutil.DecodeAddress(change2Addr, nil)
@@ -395,6 +406,108 @@ func TestConstructUnsignedTx(t *testing.T) {
395406
wantFee: 1248,
396407
},
397408

409+
{
410+
name: "identical change pkscripts",
411+
sweeps: []sweep{
412+
{
413+
outpoint: op1,
414+
value: 1_000_000,
415+
},
416+
{
417+
outpoint: op2,
418+
value: 2_000_000,
419+
change: change1,
420+
},
421+
{
422+
outpoint: op3,
423+
value: 3_000_000,
424+
change: change1,
425+
},
426+
},
427+
address: p2trAddress,
428+
currentHeight: 800_000,
429+
feeRate: 1000,
430+
wantTx: &wire.MsgTx{
431+
Version: 2,
432+
LockTime: 800_000,
433+
TxIn: []*wire.TxIn{
434+
{
435+
PreviousOutPoint: op1,
436+
},
437+
{
438+
PreviousOutPoint: op2,
439+
},
440+
{
441+
PreviousOutPoint: op3,
442+
},
443+
},
444+
TxOut: []*wire.TxOut{
445+
{
446+
Value: 5_798_924,
447+
PkScript: p2trPkScript,
448+
},
449+
{
450+
Value: 2 * change1.Value,
451+
PkScript: change1.PkScript,
452+
},
453+
},
454+
},
455+
wantWeight: 1076,
456+
wantFeeForWeight: 1076,
457+
wantFee: 1076,
458+
},
459+
460+
{
461+
name: "identical change pkscripts different values",
462+
sweeps: []sweep{
463+
{
464+
outpoint: op1,
465+
value: 1_000_000,
466+
},
467+
{
468+
outpoint: op2,
469+
value: 2_000_000,
470+
change: change1,
471+
},
472+
{
473+
outpoint: op3,
474+
value: 3_000_000,
475+
change: change1Prime,
476+
},
477+
},
478+
address: p2trAddress,
479+
currentHeight: 800_000,
480+
feeRate: 1000,
481+
wantTx: &wire.MsgTx{
482+
Version: 2,
483+
LockTime: 800_000,
484+
TxIn: []*wire.TxIn{
485+
{
486+
PreviousOutPoint: op1,
487+
},
488+
{
489+
PreviousOutPoint: op2,
490+
},
491+
{
492+
PreviousOutPoint: op3,
493+
},
494+
},
495+
TxOut: []*wire.TxOut{
496+
{
497+
Value: 5_698_924,
498+
PkScript: p2trPkScript,
499+
},
500+
{
501+
Value: change1.Value + change1Prime.Value,
502+
PkScript: change1.PkScript,
503+
},
504+
},
505+
},
506+
wantWeight: 1076,
507+
wantFeeForWeight: 1076,
508+
wantFee: 1076,
509+
},
510+
398511
{
399512
name: "change exceeds input value",
400513
sweeps: []sweep{

sweepbatcher/sweep_batcher_presigned_test.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ func testPresigned_presigned_group_with_change(t *testing.T,
12301230
presignedHelper.SetOutpointOnline(op1, true)
12311231
presignedHelper.SetOutpointOnline(op2, true)
12321232

1233-
// An attempt to presign must fail.
1233+
// An attempt to presign shouldn't fail.
12341234
err = batcher.PresignSweepsGroup(
12351235
ctx, group1, sweepTimeout, destAddr, change,
12361236
)
@@ -1268,6 +1268,134 @@ func testPresigned_presigned_group_with_change(t *testing.T,
12681268
require.NoError(t, lnd.NotifyHeight(601))
12691269
}
12701270

1271+
// testPresigned_presigned_group_with_identical_change_pkscript tests passing multiple sweeps to
1272+
// the method PresignSweepsGroup. It tests that a change output of a primary
1273+
// deposit sweep is properly added to the presigned transaction.
1274+
func testPresigned_presigned_group_with_identical_change_pkscript(t *testing.T,
1275+
batcherStore testBatcherStore) {
1276+
1277+
defer test.Guard(t)()
1278+
1279+
batchPkScript, err := txscript.PayToAddrScript(destAddr)
1280+
require.NoError(t, err)
1281+
1282+
lnd := test.NewMockLnd()
1283+
1284+
ctx, cancel := context.WithCancel(context.Background())
1285+
defer cancel()
1286+
1287+
customFeeRate := func(_ context.Context, _ lntypes.Hash,
1288+
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
1289+
1290+
return chainfee.SatPerKWeight(10_000), nil
1291+
}
1292+
1293+
presignedHelper := newMockPresignedHelper()
1294+
1295+
batcher := NewBatcher(
1296+
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
1297+
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
1298+
batcherStore, presignedHelper,
1299+
WithCustomFeeRate(customFeeRate),
1300+
WithPresignedHelper(presignedHelper),
1301+
)
1302+
1303+
go func() {
1304+
err := batcher.Run(ctx)
1305+
checkBatcherError(t, err)
1306+
}()
1307+
1308+
// Create two swaps of a single sweep
1309+
swapHash1 := lntypes.Hash{1, 1, 1}
1310+
swapHash2 := lntypes.Hash{2, 2, 2}
1311+
op1 := wire.OutPoint{
1312+
Hash: chainhash.Hash{1, 1},
1313+
Index: 1,
1314+
}
1315+
op2 := wire.OutPoint{
1316+
Hash: chainhash.Hash{2, 2},
1317+
Index: 2,
1318+
}
1319+
group1 := []Input{
1320+
{
1321+
Outpoint: op1,
1322+
Value: 1_000_000,
1323+
},
1324+
}
1325+
change1 := &wire.TxOut{
1326+
Value: 500_000,
1327+
PkScript: []byte{0xaf, 0xfe},
1328+
}
1329+
group2 := []Input{
1330+
{
1331+
Outpoint: op2,
1332+
Value: 2_000_000,
1333+
},
1334+
}
1335+
change2 := &wire.TxOut{
1336+
Value: 600_000,
1337+
PkScript: []byte{0xaf, 0xfe},
1338+
}
1339+
1340+
presignedHelper.setChangeForPrimaryDeposit(op1, change1)
1341+
presignedHelper.setChangeForPrimaryDeposit(op2, change2)
1342+
1343+
// Enable only one of the sweeps.
1344+
presignedHelper.SetOutpointOnline(op1, true)
1345+
presignedHelper.SetOutpointOnline(op2, true)
1346+
1347+
// An attempt to presign group1 shouldn't fail.
1348+
err = batcher.PresignSweepsGroup(
1349+
ctx, group1, sweepTimeout, destAddr, change1,
1350+
)
1351+
require.NoError(t, err)
1352+
1353+
// Add the sweep, triggering the publishing attempt.
1354+
err = batcher.AddSweep(ctx, &SweepRequest{
1355+
SwapHash: swapHash1,
1356+
Inputs: group1,
1357+
Notifier: &dummyNotifier,
1358+
})
1359+
require.NoError(t, err)
1360+
1361+
// Since a batch was created we check that it registered for its primary
1362+
// sweep's spend.
1363+
<-lnd.RegisterSpendChannel
1364+
1365+
// An attempt to presign group2 shouldn't fail.
1366+
err = batcher.PresignSweepsGroup(
1367+
ctx, group2, sweepTimeout, destAddr, change2,
1368+
)
1369+
require.NoError(t, err)
1370+
1371+
// Add the sweep, triggering the publishing attempt.
1372+
err = batcher.AddSweep(ctx, &SweepRequest{
1373+
SwapHash: swapHash2,
1374+
Inputs: group2,
1375+
Notifier: &dummyNotifier,
1376+
})
1377+
require.NoError(t, err)
1378+
1379+
// Wait for a transactions to be published.
1380+
tx := <-lnd.TxPublishChannel
1381+
require.Len(t, tx.TxIn, 2)
1382+
require.Len(t, tx.TxOut, 2)
1383+
require.ElementsMatch(
1384+
t, []wire.OutPoint{op1, op2},
1385+
[]wire.OutPoint{
1386+
tx.TxIn[0].PreviousOutPoint,
1387+
tx.TxIn[1].PreviousOutPoint,
1388+
},
1389+
)
1390+
require.Equal(t, int64(1_893_300), tx.TxOut[0].Value)
1391+
require.Equal(t, change1.Value+change2.Value, tx.TxOut[1].Value)
1392+
require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
1393+
require.Equal(t, change1.PkScript, tx.TxOut[1].PkScript)
1394+
1395+
// Mine a blocks to trigger republishing.
1396+
require.NoError(t, lnd.NotifyHeight(601))
1397+
}
1398+
12711399
// testPresigned_presigned_group_with_dust_main_output passes a dust main output
12721400
// and a change output to PresignSweepsGroup. It will fail because of the dust
12731401
// main output. Note that the min relay fee is set low enough to pass the
@@ -2224,6 +2352,10 @@ func TestPresigned(t *testing.T) {
22242352
testPresigned_presigned_group_with_change(t, NewStoreMock())
22252353
})
22262354

2355+
t.Run("identical change pkscript", func(t *testing.T) {
2356+
testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
2357+
})
2358+
22272359
t.Run("dust_main_output", func(t *testing.T) {
22282360
testPresigned_presigned_group_with_dust_main_output(
22292361
t, NewStoreMock(),

0 commit comments

Comments
 (0)