Skip to content

Commit 738ff64

Browse files
committed
sweepbatcher: consider change in presigning and batch tx
Presigning sweeps takes change outputs into account. Each primary deposit id of a sweep group points to an optional change output. 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 c87bd5d commit 738ff64

File tree

9 files changed

+985
-110
lines changed

9 files changed

+985
-110
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.AddOutput(s.change.PkScript)
217+
}
218+
}
219+
213220
// Add inputs.
214221
for _, sweep := range batch.sweeps {
215222
if sweep.nonCoopHint || sweep.coopFailed {

sweepbatcher/greedy_batch_selection_test.go

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/btcsuite/btcd/btcutil"
77
"github.com/btcsuite/btcd/chaincfg"
88
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/txscript"
910
"github.com/btcsuite/btcd/wire"
1011
"github.com/lightninglabs/loop/swap"
1112
"github.com/lightningnetwork/lnd/input"
@@ -16,24 +17,28 @@ import (
1617

1718
// Useful constants for tests.
1819
const (
19-
lowFeeRate = chainfee.FeePerKwFloor
20-
highFeeRate = chainfee.SatPerKWeight(30000)
20+
lowFeeRate = chainfee.FeePerKwFloor
21+
mediumFeeRate = lowFeeRate + 200
22+
highFeeRate = chainfee.SatPerKWeight(30000)
2123

2224
coopInputWeight = lntypes.WeightUnit(230)
25+
batchOutputWeight = lntypes.WeightUnit(343)
2326
nonCoopInputWeight = lntypes.WeightUnit(393)
2427
nonCoopPenalty = nonCoopInputWeight - coopInputWeight
2528
coopNewBatchWeight = lntypes.WeightUnit(444)
2629
nonCoopNewBatchWeight = coopNewBatchWeight + nonCoopPenalty
30+
changeOutputWeight = lntypes.WeightUnit(input.P2TROutputSize)
2731

2832
// p2pkhDiscount is weight discount P2PKH output has over P2TR output.
2933
p2pkhDiscount = lntypes.WeightUnit(
3034
input.P2TROutputSize-input.P2PKHOutputSize,
3135
) * 4
3236

33-
coopTwoSweepBatchWeight = coopNewBatchWeight + coopInputWeight
34-
nonCoopTwoSweepBatchWeight = coopTwoSweepBatchWeight + 2*nonCoopPenalty
35-
v2v3BatchWeight = nonCoopTwoSweepBatchWeight - 25
36-
mixedTwoSweepBatchWeight = coopTwoSweepBatchWeight + nonCoopPenalty
37+
coopTwoSweepBatchWeight = coopNewBatchWeight + coopInputWeight
38+
coopSingleSweepChangeBatchWeight = coopInputWeight + batchOutputWeight + changeOutputWeight
39+
nonCoopTwoSweepBatchWeight = coopTwoSweepBatchWeight + 2*nonCoopPenalty
40+
v2v3BatchWeight = nonCoopTwoSweepBatchWeight - 25
41+
mixedTwoSweepBatchWeight = coopTwoSweepBatchWeight + nonCoopPenalty
3742
)
3843

3944
// testHtlcV2SuccessEstimator adds weight of non-cooperative input to estimator
@@ -265,6 +270,13 @@ func TestEstimateBatchWeight(t *testing.T) {
265270
se3 := testHtlcV3SuccessEstimator
266271
trAddr := (*btcutil.AddressTaproot)(nil)
267272

273+
changeAddr := "bc1pdx9ggvtjjcpaqfqk375qhdmzx9xu8dcu7w94lqfcxhh0rj" +
274+
"lwyyeq5ryn6r"
275+
changeAddress, err := btcutil.DecodeAddress(changeAddr, nil)
276+
require.NoError(t, err)
277+
changePkscript, err := txscript.PayToAddrScript(changeAddress)
278+
require.NoError(t, err)
279+
268280
cases := []struct {
269281
name string
270282
batch *batch
@@ -290,6 +302,29 @@ func TestEstimateBatchWeight(t *testing.T) {
290302
},
291303
},
292304

305+
{
306+
name: "one sweep regular batch with change",
307+
batch: &batch{
308+
id: 1,
309+
rbfCache: rbfCache{
310+
FeeRate: lowFeeRate,
311+
},
312+
sweeps: map[wire.OutPoint]sweep{
313+
outpoint1: {
314+
htlcSuccessEstimator: se3,
315+
change: &wire.TxOut{
316+
PkScript: changePkscript,
317+
},
318+
},
319+
},
320+
},
321+
wantBatchFeeDetails: feeDetails{
322+
BatchId: 1,
323+
FeeRate: lowFeeRate,
324+
Weight: coopSingleSweepChangeBatchWeight,
325+
},
326+
},
327+
293328
{
294329
name: "two sweeps regular batch",
295330
batch: &batch{
@@ -778,6 +813,47 @@ func TestSelectBatches(t *testing.T) {
778813
},
779814
wantBestBatchesIds: []int32{1, newBatchSignal},
780815
},
816+
817+
{
818+
name: "low fee change sweep, placed in new batch",
819+
batches: []feeDetails{
820+
{
821+
BatchId: 1,
822+
FeeRate: mediumFeeRate,
823+
Weight: coopNewBatchWeight,
824+
},
825+
},
826+
sweep: feeDetails{
827+
FeeRate: lowFeeRate,
828+
Weight: coopInputWeight + changeOutputWeight,
829+
},
830+
oneSweepBatch: feeDetails{
831+
FeeRate: lowFeeRate,
832+
Weight: coopNewBatchWeight,
833+
},
834+
wantBestBatchesIds: []int32{newBatchSignal, 1},
835+
},
836+
837+
{
838+
name: "high fee change sweep, placed in existing " +
839+
"medium batch",
840+
batches: []feeDetails{
841+
{
842+
BatchId: 1,
843+
FeeRate: mediumFeeRate,
844+
Weight: coopNewBatchWeight,
845+
},
846+
},
847+
sweep: feeDetails{
848+
FeeRate: highFeeRate,
849+
Weight: coopInputWeight + changeOutputWeight,
850+
},
851+
oneSweepBatch: feeDetails{
852+
FeeRate: highFeeRate,
853+
Weight: coopNewBatchWeight,
854+
},
855+
wantBestBatchesIds: []int32{newBatchSignal, 1},
856+
},
781857
}
782858

783859
for _, tc := range cases {

sweepbatcher/presigned.go

Lines changed: 25 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,31 @@ 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+
if i != 0 && o.Value != signedTx.TxOut[i].Value {
610+
return fmt.Errorf("mismatch of output value: %d, %d",
611+
o.Value, signedTx.TxOut[i].Value)
612+
}
613+
}
614+
615+
// Calculate the total value of all outputs to help determine the
616+
// transaction fee.
617+
totalOutputValue := btcutil.Amount(0)
618+
for _, o := range signedTx.TxOut {
619+
totalOutputValue += btcutil.Amount(o.Value)
609620
}
610621

611622
// Find the feerate of signedTx.
612-
fee := inputAmt - btcutil.Amount(signedOut.Value)
623+
fee := inputAmt - totalOutputValue
613624
weight := lntypes.WeightUnit(
614625
blockchain.GetTransactionWeight(btcutil.NewTx(signedTx)),
615626
)

sweepbatcher/presigned_test.go

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,7 +1460,8 @@ func TestCheckSignedTx(t *testing.T) {
14601460
},
14611461
inputAmt: 3_000_000,
14621462
minRelayFee: 253,
1463-
wantErr: "unsigned tx has 2 outputs, want 1",
1463+
wantErr: "unsigned tx has 2 outputs, signed tx " +
1464+
"has 1 outputs, should be equal",
14641465
},
14651466

14661467
{
@@ -1517,7 +1518,153 @@ func TestCheckSignedTx(t *testing.T) {
15171518
},
15181519
inputAmt: 3_000_000,
15191520
minRelayFee: 253,
1520-
wantErr: "the signed tx has 2 outputs, want 1",
1521+
wantErr: "unsigned tx has 1 outputs, signed tx " +
1522+
"has 2 outputs, should be equal",
1523+
},
1524+
1525+
{
1526+
name: "pkscript mismatch",
1527+
unsignedTx: &wire.MsgTx{
1528+
Version: 2,
1529+
TxIn: []*wire.TxIn{
1530+
{
1531+
PreviousOutPoint: op2,
1532+
Sequence: 2,
1533+
},
1534+
},
1535+
TxOut: []*wire.TxOut{
1536+
{
1537+
Value: 2999374,
1538+
PkScript: batchPkScript,
1539+
},
1540+
},
1541+
LockTime: 800_000,
1542+
},
1543+
signedTx: &wire.MsgTx{
1544+
Version: 2,
1545+
TxIn: []*wire.TxIn{
1546+
{
1547+
PreviousOutPoint: op2,
1548+
Sequence: 2,
1549+
Witness: wire.TxWitness{
1550+
[]byte("test"),
1551+
},
1552+
},
1553+
},
1554+
TxOut: []*wire.TxOut{
1555+
{
1556+
Value: 2999374,
1557+
PkScript: []byte{0xaf, 0xfe}, // Just to make it different.
1558+
},
1559+
},
1560+
LockTime: 799_999,
1561+
},
1562+
inputAmt: 3_000_000,
1563+
minRelayFee: 253,
1564+
wantErr: "mismatch of output pkScript",
1565+
},
1566+
1567+
{
1568+
name: "value mismatch, first output",
1569+
unsignedTx: &wire.MsgTx{
1570+
Version: 2,
1571+
TxIn: []*wire.TxIn{
1572+
{
1573+
PreviousOutPoint: op2,
1574+
Sequence: 2,
1575+
},
1576+
},
1577+
TxOut: []*wire.TxOut{
1578+
{
1579+
Value: 2999374,
1580+
PkScript: batchPkScript,
1581+
},
1582+
},
1583+
LockTime: 800_000,
1584+
},
1585+
signedTx: &wire.MsgTx{
1586+
Version: 2,
1587+
TxIn: []*wire.TxIn{
1588+
{
1589+
PreviousOutPoint: op2,
1590+
Sequence: 2,
1591+
Witness: wire.TxWitness{
1592+
[]byte("test"),
1593+
},
1594+
},
1595+
},
1596+
TxOut: []*wire.TxOut{
1597+
{
1598+
Value: 1_337_000, // Just to make it different.
1599+
PkScript: batchPkScript,
1600+
},
1601+
},
1602+
LockTime: 799_999,
1603+
},
1604+
inputAmt: 3_000_000,
1605+
minRelayFee: 253,
1606+
wantErr: "",
1607+
},
1608+
1609+
{
1610+
name: "value mismatch, change output",
1611+
unsignedTx: &wire.MsgTx{
1612+
Version: 2,
1613+
TxIn: []*wire.TxIn{
1614+
{
1615+
PreviousOutPoint: op2,
1616+
Sequence: 2,
1617+
},
1618+
{
1619+
PreviousOutPoint: op1,
1620+
Sequence: 2,
1621+
},
1622+
},
1623+
TxOut: []*wire.TxOut{
1624+
{
1625+
Value: 2999374,
1626+
PkScript: batchPkScript,
1627+
},
1628+
{
1629+
Value: 1_337_000,
1630+
PkScript: batchPkScript,
1631+
},
1632+
},
1633+
LockTime: 800_000,
1634+
},
1635+
signedTx: &wire.MsgTx{
1636+
Version: 2,
1637+
TxIn: []*wire.TxIn{
1638+
{
1639+
PreviousOutPoint: op2,
1640+
Sequence: 2,
1641+
Witness: wire.TxWitness{
1642+
[]byte("test"),
1643+
},
1644+
},
1645+
{
1646+
PreviousOutPoint: op1,
1647+
Sequence: 2,
1648+
Witness: wire.TxWitness{
1649+
[]byte("test"),
1650+
},
1651+
},
1652+
},
1653+
TxOut: []*wire.TxOut{
1654+
{
1655+
Value: 2_493_300,
1656+
PkScript: batchPkScript,
1657+
},
1658+
{
1659+
Value: 1_338, // Just to make it different.
1660+
PkScript: batchPkScript,
1661+
},
1662+
},
1663+
LockTime: 799_999,
1664+
},
1665+
inputAmt: 3_000_000,
1666+
minRelayFee: 253,
1667+
wantErr: "mismatch of output value",
15211668
},
15221669

15231670
{

0 commit comments

Comments
 (0)