Skip to content

Commit 576270f

Browse files
committed
sweepbatcher: consider change in presigning and batch tx
Presigning sweeps takes change outputs into account. Each sweep belonging to the same sweep group points to the same change output, if existent. sweepbatcher.presign scans all passed sweeps for change outputs and passes them to constructUnsignedTx. Optional change of a swap is encoded in its sweeps as a pointer to the same change output. This change is taken into account when constructing the unsigned batch transaction when it comes to tx weight and outputs.
1 parent 26b38cf commit 576270f

File tree

7 files changed

+256
-55
lines changed

7 files changed

+256
-55
lines changed

sweepbatcher/greedy_batch_selection.go

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

213+
// Add change output weights.
214+
for _, s := range batch.sweeps {
215+
if s.change != nil {
216+
weight.AddP2TROutput()
217+
}
218+
}
219+
213220
// Add inputs.
214221
for _, sweep := range batch.sweeps {
215222
if sweep.nonCoopHint || sweep.coopFailed {

sweepbatcher/presigned.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep,
5151
outpoint: s.outpoint,
5252
value: s.value,
5353
presigned: s.presigned,
54+
change: s.change,
5455
}
5556
}
5657

@@ -493,10 +494,12 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
493494
signedFeeRate := chainfee.NewSatPerKWeight(fee, realWeight)
494495

495496
numSweeps := len(tx.TxIn)
497+
numChange := len(tx.TxOut) - 1
496498
b.Infof("attempting to publish custom signed tx=%v, desiredFeerate=%v,"+
497-
" signedFeeRate=%v, weight=%v, fee=%v, sweeps=%d, destAddr=%s",
499+
" signedFeeRate=%v, weight=%v, fee=%v, sweeps=%d, "+
500+
"changeOutputs=%d, destAddr=%s",
498501
txHash, feeRate, signedFeeRate, realWeight, fee, numSweeps,
499-
address)
502+
numChange, address)
500503
b.debugLogTx("serialized batch", tx)
501504

502505
// Publish the transaction.
@@ -593,23 +596,30 @@ func CheckSignedTx(unsignedTx, signedTx *wire.MsgTx, inputAmt btcutil.Amount,
593596
}
594597

595598
// Compare outputs.
596-
if len(unsignedTx.TxOut) != 1 {
597-
return fmt.Errorf("unsigned tx has %d outputs, want 1",
598-
len(unsignedTx.TxOut))
599-
}
600-
if len(signedTx.TxOut) != 1 {
601-
return fmt.Errorf("the signed tx has %d outputs, want 1",
599+
if len(unsignedTx.TxOut) != len(signedTx.TxOut) {
600+
return fmt.Errorf("unsigned tx has %d outputs, signed tx has "+
601+
"%d outputs, should be equal", len(unsignedTx.TxOut),
602602
len(signedTx.TxOut))
603603
}
604-
unsignedOut := unsignedTx.TxOut[0]
605-
signedOut := signedTx.TxOut[0]
606-
if !bytes.Equal(unsignedOut.PkScript, signedOut.PkScript) {
607-
return fmt.Errorf("mismatch of output pkScript: %x, %x",
608-
unsignedOut.PkScript, signedOut.PkScript)
604+
for i, o := range unsignedTx.TxOut {
605+
if !bytes.Equal(o.PkScript, signedTx.TxOut[i].PkScript) {
606+
return fmt.Errorf("mismatch of output pkScript: %x, %x",
607+
o.PkScript, signedTx.TxOut[i].PkScript)
608+
}
609+
}
610+
611+
// The first output is always the batch output.
612+
batchOutput := signedTx.TxOut[0]
613+
614+
// Calculate the total value of change outputs to help determine the
615+
// transaction fee.
616+
totalChangeValue := btcutil.Amount(0)
617+
for i := 1; i < len(signedTx.TxOut); i++ {
618+
totalChangeValue += btcutil.Amount(signedTx.TxOut[i].Value)
609619
}
610620

611621
// Find the feerate of signedTx.
612-
fee := inputAmt - btcutil.Amount(signedOut.Value)
622+
fee := inputAmt - btcutil.Amount(batchOutput.Value) - totalChangeValue
613623
weight := lntypes.WeightUnit(
614624
blockchain.GetTransactionWeight(btcutil.NewTx(signedTx)),
615625
)

sweepbatcher/presigned_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@ func TestPresign(t *testing.T) {
10111011
ctx, tc.presigner, tc.destAddr,
10121012
tc.primarySweepID, tc.sweeps,
10131013
tc.nextBlockFeeRate,
1014+
&chaincfg.RegressionNetParams,
10141015
)
10151016
if tc.wantErr != "" {
10161017
require.Error(t, err)
@@ -1460,7 +1461,8 @@ func TestCheckSignedTx(t *testing.T) {
14601461
},
14611462
inputAmt: 3_000_000,
14621463
minRelayFee: 253,
1463-
wantErr: "unsigned tx has 2 outputs, want 1",
1464+
wantErr: "unsigned tx has 2 outputs, signed tx " +
1465+
"has 1 outputs, should be equal",
14641466
},
14651467

14661468
{
@@ -1517,7 +1519,8 @@ func TestCheckSignedTx(t *testing.T) {
15171519
},
15181520
inputAmt: 3_000_000,
15191521
minRelayFee: 253,
1520-
wantErr: "the signed tx has 2 outputs, want 1",
1522+
wantErr: "unsigned tx has 1 outputs, signed tx " +
1523+
"has 2 outputs, should be equal",
15211524
},
15221525

15231526
{

sweepbatcher/sweep_batch.go

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ type sweep struct {
125125

126126
// presigned is set, if the sweep should be handled in presigned mode.
127127
presigned bool
128+
129+
// change is the optional change output of the sweep.
130+
change *wire.TxOut
128131
}
129132

130133
// batchState is the state of the batch.
@@ -1236,10 +1239,13 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
12361239
}
12371240

12381241
// constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr.
1239-
// It also returns absolute fee (from weight and clamped).
1242+
// It also returns absolute fee (from weight and clamped). The main output is
1243+
// the first output of the transaction, followed by an optional list of change
1244+
// outputs.
12401245
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
1241-
currentHeight int32, feeRate chainfee.SatPerKWeight) (*wire.MsgTx,
1242-
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
1246+
currentHeight int32, feeRate chainfee.SatPerKWeight) (
1247+
*wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount,
1248+
error) {
12431249

12441250
// Sanity check, there should be at least 1 sweep in this batch.
12451251
if len(sweeps) == 0 {
@@ -1252,6 +1258,13 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
12521258
LockTime: uint32(currentHeight),
12531259
}
12541260

1261+
var changeOutputs []*wire.TxOut
1262+
for _, sweep := range sweeps {
1263+
if sweep.change != nil {
1264+
changeOutputs = append(changeOutputs, sweep.change)
1265+
}
1266+
}
1267+
12551268
// Add transaction inputs and estimate its weight.
12561269
var weightEstimate input.TxWeightEstimator
12571270
for _, sweep := range sweeps {
@@ -1297,6 +1310,11 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
12971310
"failed: %w", err)
12981311
}
12991312

1313+
// Add the optional change outputs to weight estimates.
1314+
for _, _ = range changeOutputs {
1315+
weightEstimate.AddP2TROutput()
1316+
}
1317+
13001318
// Keep track of the total amount this batch is sweeping back.
13011319
batchAmt := btcutil.Amount(0)
13021320
for _, sweep := range sweeps {
@@ -1318,11 +1336,50 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13181336
fee := clampBatchFee(feeForWeight, batchAmt)
13191337

13201338
// Add the batch transaction output, which excludes the fees paid to
1321-
// miners.
1322-
batchTx.AddTxOut(&wire.TxOut{
1323-
PkScript: batchPkScript,
1324-
Value: int64(batchAmt - fee),
1325-
})
1339+
// miners. Reduce the amount by the sum of change outputs, if any.
1340+
if len(changeOutputs) == 0 {
1341+
batchTx.AddTxOut(&wire.TxOut{
1342+
PkScript: batchPkScript,
1343+
Value: int64(batchAmt - fee),
1344+
})
1345+
} else {
1346+
// Reduce the batch output by the sum of change outputs.
1347+
var sumChange int64
1348+
for _, change := range changeOutputs {
1349+
sumChange += change.Value
1350+
}
1351+
// Add the main output first.
1352+
batchTx.AddTxOut(&wire.TxOut{
1353+
PkScript: batchPkScript,
1354+
Value: int64(batchAmt-fee) - sumChange,
1355+
})
1356+
// Then add change outputs.
1357+
for _, txOut := range changeOutputs {
1358+
batchTx.AddTxOut(&wire.TxOut{
1359+
PkScript: txOut.PkScript,
1360+
Value: txOut.Value,
1361+
})
1362+
}
1363+
}
1364+
1365+
// Check that for each swap, inputs exceed the change outputs.
1366+
swap2Inputs := make(map[lntypes.Hash]btcutil.Amount)
1367+
swap2Change := make(map[lntypes.Hash]btcutil.Amount)
1368+
for _, sweep := range sweeps {
1369+
swap2Inputs[sweep.swapHash] += sweep.value
1370+
if sweep.change != nil {
1371+
swap2Change[sweep.swapHash] +=
1372+
btcutil.Amount(sweep.change.Value)
1373+
}
1374+
}
1375+
1376+
for swapHash, inputs := range swap2Inputs {
1377+
change := swap2Change[swapHash]
1378+
if inputs <= change {
1379+
return nil, 0, 0, 0, fmt.Errorf("inputs %v <= change "+
1380+
"%v for swap %x", inputs, change, swapHash[:6])
1381+
}
1382+
}
13261383

13271384
return batchTx, weight, feeForWeight, fee, nil
13281385
}
@@ -1396,7 +1453,9 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
13961453
attempt)
13971454

13981455
// Construct unsigned batch transaction.
1399-
var err error
1456+
var (
1457+
err error
1458+
)
14001459
tx, weight, feeForWeight, fee, err = constructUnsignedTx(
14011460
sweeps, address, b.currentHeight, b.rbfCache.FeeRate,
14021461
)

sweepbatcher/sweep_batch_test.go

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func TestConstructUnsignedTx(t *testing.T) {
2929
Hash: chainhash.Hash{2, 2, 2},
3030
Index: 2,
3131
}
32+
op3 := wire.OutPoint{
33+
Hash: chainhash.Hash{3, 3, 3},
34+
Index: 3,
35+
}
3236

3337
batchPkScript, err := txscript.PayToAddrScript(destAddr)
3438
require.NoError(t, err)
@@ -40,6 +44,15 @@ func TestConstructUnsignedTx(t *testing.T) {
4044
p2trPkScript, err := txscript.PayToAddrScript(p2trAddress)
4145
require.NoError(t, err)
4246

47+
change1 := &wire.TxOut{
48+
Value: 100_000,
49+
PkScript: p2trPkScript,
50+
}
51+
change2 := &wire.TxOut{
52+
Value: 200_000,
53+
PkScript: p2trPkScript,
54+
}
55+
4356
serializedPubKey := []byte{
4457
0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95,
4558
0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03,
@@ -223,7 +236,7 @@ func TestConstructUnsignedTx(t *testing.T) {
223236
},
224237
TxOut: []*wire.TxOut{
225238
{
226-
Value: 2400000,
239+
Value: 2_400_000,
227240
PkScript: batchPkScript,
228241
},
229242
},
@@ -265,7 +278,7 @@ func TestConstructUnsignedTx(t *testing.T) {
265278
},
266279
TxOut: []*wire.TxOut{
267280
{
268-
Value: 2999211,
281+
Value: 2_999_211,
269282
PkScript: batchPkScript,
270283
},
271284
},
@@ -275,6 +288,97 @@ func TestConstructUnsignedTx(t *testing.T) {
275288
wantFee: 789,
276289
},
277290

291+
{
292+
name: "all sweeps same change output",
293+
sweeps: []sweep{
294+
{
295+
outpoint: op1,
296+
value: 1_000_000,
297+
change: change1,
298+
},
299+
},
300+
address: p2trAddress,
301+
currentHeight: 800_000,
302+
feeRate: 1000,
303+
wantTx: &wire.MsgTx{
304+
Version: 2,
305+
LockTime: 800_000,
306+
TxIn: []*wire.TxIn{
307+
{
308+
PreviousOutPoint: op1,
309+
},
310+
},
311+
TxOut: []*wire.TxOut{
312+
{
313+
Value: 899_384,
314+
PkScript: p2trPkScript,
315+
},
316+
{
317+
Value: change1.Value,
318+
PkScript: change1.PkScript,
319+
},
320+
},
321+
},
322+
wantWeight: 616,
323+
wantFeeForWeight: 616,
324+
wantFee: 616,
325+
},
326+
327+
{
328+
name: "all sweeps different change outputs",
329+
sweeps: []sweep{
330+
{
331+
outpoint: op1,
332+
value: 1_000_000,
333+
},
334+
{
335+
outpoint: op2,
336+
value: 2_000_000,
337+
change: change1,
338+
},
339+
{
340+
outpoint: op3,
341+
value: 3_000_000,
342+
change: change2,
343+
},
344+
},
345+
address: p2trAddress,
346+
currentHeight: 800_000,
347+
feeRate: 1000,
348+
wantTx: &wire.MsgTx{
349+
Version: 2,
350+
LockTime: 800_000,
351+
TxIn: []*wire.TxIn{
352+
{
353+
PreviousOutPoint: op1,
354+
},
355+
{
356+
PreviousOutPoint: op2,
357+
},
358+
{
359+
PreviousOutPoint: op3,
360+
},
361+
},
362+
TxOut: []*wire.TxOut{
363+
{
364+
Value: 5_698_752,
365+
PkScript: p2trPkScript,
366+
},
367+
{
368+
Value: change1.Value,
369+
PkScript: change1.PkScript,
370+
},
371+
{
372+
Value: change2.Value,
373+
PkScript: change2.PkScript,
374+
},
375+
},
376+
},
377+
wantWeight: 1248,
378+
wantFeeForWeight: 1248,
379+
wantFee: 1248,
380+
},
381+
278382
{
279383
name: "weight estimator fails",
280384
sweeps: []sweep{

0 commit comments

Comments
 (0)