Skip to content

Commit 4c58f85

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 4c58f85

File tree

7 files changed

+677
-48
lines changed

7 files changed

+677
-48
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/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
{

sweepbatcher/sweep_batch.go

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"encoding/hex"
77
"errors"
88
"fmt"
9+
"github.com/btcsuite/btcd/mempool"
10+
"github.com/lightningnetwork/lnd/lnwallet"
911
"math"
1012
"strings"
1113
"sync"
@@ -125,6 +127,9 @@ type sweep struct {
125127

126128
// presigned is set, if the sweep should be handled in presigned mode.
127129
presigned bool
130+
131+
// change is the optional change output of the sweep.
132+
change *wire.TxOut
128133
}
129134

130135
// batchState is the state of the batch.
@@ -1236,10 +1241,14 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
12361241
}
12371242

12381243
// constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr.
1239-
// It also returns absolute fee (from weight and clamped).
1244+
// It also returns absolute fee (from weight and clamped). The main output is
1245+
// the first output of the transaction, followed by an optional list of change
1246+
// outputs. If the main output value is below dust limit this function will
1247+
// return an error.
12401248
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
1241-
currentHeight int32, feeRate chainfee.SatPerKWeight) (*wire.MsgTx,
1242-
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
1249+
currentHeight int32, feeRate chainfee.SatPerKWeight) (
1250+
*wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount,
1251+
error) {
12431252

12441253
// Sanity check, there should be at least 1 sweep in this batch.
12451254
if len(sweeps) == 0 {
@@ -1252,6 +1261,13 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
12521261
LockTime: uint32(currentHeight),
12531262
}
12541263

1264+
var changeOutputs []*wire.TxOut
1265+
for _, sweep := range sweeps {
1266+
if sweep.change != nil {
1267+
changeOutputs = append(changeOutputs, sweep.change)
1268+
}
1269+
}
1270+
12551271
// Add transaction inputs and estimate its weight.
12561272
var weightEstimate input.TxWeightEstimator
12571273
for _, sweep := range sweeps {
@@ -1297,6 +1313,11 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
12971313
"failed: %w", err)
12981314
}
12991315

1316+
// Add the optional change outputs to weight estimates.
1317+
for _, o := range changeOutputs {
1318+
weightEstimate.AddOutput(o.PkScript)
1319+
}
1320+
13001321
// Keep track of the total amount this batch is sweeping back.
13011322
batchAmt := btcutil.Amount(0)
13021323
for _, sweep := range sweeps {
@@ -1314,15 +1335,80 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13141335
feeForWeight++
13151336
}
13161337

1338+
// Add the batch transaction output, which excludes the fees paid to
1339+
// miners. Reduce the amount by the sum of change outputs, if any.
1340+
var sumChange int64
1341+
for _, change := range changeOutputs {
1342+
sumChange += change.Value
1343+
}
1344+
1345+
// Ensure that the batch amount is greater than the sum of change.
1346+
if batchAmt <= btcutil.Amount(sumChange) {
1347+
return nil, 0, 0, 0, fmt.Errorf("batch amount %v is less than "+
1348+
"the sum of change outputs %v", batchAmt,
1349+
btcutil.Amount(sumChange))
1350+
}
1351+
13171352
// Clamp the calculated fee to the max allowed fee amount for the batch.
1318-
fee := clampBatchFee(feeForWeight, batchAmt)
1353+
fee := clampBatchFee(feeForWeight, batchAmt-btcutil.Amount(sumChange))
13191354

1320-
// Add the batch transaction output, which excludes the fees paid to
1321-
// miners.
1355+
// Ensure that batch amount exceeds the sum of change outputs and the
1356+
// fee, and that it is also greater than dust limit for the main
1357+
// output.
1358+
if fee+btcutil.Amount(sumChange)+
1359+
lnwallet.DustLimitForSize(len(batchPkScript)) > batchAmt {
1360+
1361+
return nil, 0, 0, 0, fmt.Errorf("batch amount %v is less than "+
1362+
"the sum of change outputs %v and fee %v",
1363+
batchAmt, btcutil.Amount(sumChange), fee)
1364+
}
1365+
1366+
// Add the main output first.
13221367
batchTx.AddTxOut(&wire.TxOut{
13231368
PkScript: batchPkScript,
1324-
Value: int64(batchAmt - fee),
1369+
Value: int64(batchAmt-fee) - sumChange,
13251370
})
1371+
// Then add change outputs.
1372+
for _, txOut := range changeOutputs {
1373+
batchTx.AddTxOut(&wire.TxOut{
1374+
PkScript: txOut.PkScript,
1375+
Value: txOut.Value,
1376+
})
1377+
}
1378+
1379+
// Check that for each swap, inputs exceed the change outputs.
1380+
if len(changeOutputs) != 0 {
1381+
swap2Inputs := make(map[lntypes.Hash]btcutil.Amount)
1382+
swap2Change := make(map[lntypes.Hash]btcutil.Amount)
1383+
for _, sweep := range sweeps {
1384+
swap2Inputs[sweep.swapHash] += sweep.value
1385+
if sweep.change != nil {
1386+
swap2Change[sweep.swapHash] +=
1387+
btcutil.Amount(sweep.change.Value)
1388+
}
1389+
}
1390+
1391+
for swapHash, inputs := range swap2Inputs {
1392+
change := swap2Change[swapHash]
1393+
if inputs <= change {
1394+
return nil, 0, 0, 0, fmt.Errorf(""+
1395+
"inputs %v <= change %v for swap %x",
1396+
inputs, change, swapHash[:6])
1397+
}
1398+
}
1399+
}
1400+
1401+
// Ensure that each output is above dust limit.
1402+
for _, txOut := range batchTx.TxOut {
1403+
// copied from lnwallet.DustLimitForSize
1404+
txout := &wire.TxOut{PkScript: txOut.PkScript}
1405+
dustlimit := btcutil.Amount(mempool.GetDustThreshold(txout))
1406+
if txOut.Value < int64(dustlimit) {
1407+
return nil, 0, 0, 0, fmt.Errorf("output %v is below "+
1408+
"dust limit %v", btcutil.Amount(txOut.Value),
1409+
dustlimit)
1410+
}
1411+
}
13261412

13271413
return batchTx, weight, feeForWeight, fee, nil
13281414
}

0 commit comments

Comments
 (0)