Skip to content

Commit bc7d155

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 8399a63 commit bc7d155

File tree

7 files changed

+900
-105
lines changed

7 files changed

+900
-105
lines changed

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: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/lightninglabs/loop/loopdb"
2727
"github.com/lightninglabs/loop/swap"
2828
sweeppkg "github.com/lightninglabs/loop/sweep"
29+
"github.com/lightninglabs/loop/utils"
2930
"github.com/lightningnetwork/lnd/chainntnfs"
3031
"github.com/lightningnetwork/lnd/clock"
3132
"github.com/lightningnetwork/lnd/input"
@@ -1290,10 +1291,14 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
12901291
}
12911292

12921293
// constructUnsignedTx creates unsigned tx from the sweeps, paying to the addr.
1293-
// It also returns absolute fee (from weight and clamped).
1294+
// It also returns absolute fee (from weight and clamped). The main output is
1295+
// the first output of the transaction, followed by an optional list of change
1296+
// outputs. If the main output value is below dust limit this function will
1297+
// return an error.
12941298
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
1295-
currentHeight int32, feeRate chainfee.SatPerKWeight) (*wire.MsgTx,
1296-
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
1299+
currentHeight int32, feeRate chainfee.SatPerKWeight) (
1300+
*wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount,
1301+
error) {
12971302

12981303
// Sanity check, there should be at least 1 sweep in this batch.
12991304
if len(sweeps) == 0 {
@@ -1306,6 +1311,13 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13061311
LockTime: uint32(currentHeight),
13071312
}
13081313

1314+
var changeOutputs []*wire.TxOut
1315+
for _, sweep := range sweeps {
1316+
if sweep.change != nil {
1317+
changeOutputs = append(changeOutputs, sweep.change)
1318+
}
1319+
}
1320+
13091321
// Add transaction inputs and estimate its weight.
13101322
var weightEstimate input.TxWeightEstimator
13111323
for _, sweep := range sweeps {
@@ -1351,6 +1363,11 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13511363
"failed: %w", err)
13521364
}
13531365

1366+
// Add the optional change outputs to weight estimates.
1367+
for _, o := range changeOutputs {
1368+
weightEstimate.AddOutput(o.PkScript)
1369+
}
1370+
13541371
// Keep track of the total amount this batch is sweeping back.
13551372
batchAmt := btcutil.Amount(0)
13561373
for _, sweep := range sweeps {
@@ -1368,15 +1385,78 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13681385
feeForWeight++
13691386
}
13701387

1388+
// Add the batch transaction output, which excludes the fees paid to
1389+
// miners. Reduce the amount by the sum of change outputs, if any.
1390+
var sumChange int64
1391+
for _, change := range changeOutputs {
1392+
sumChange += change.Value
1393+
}
1394+
1395+
// Ensure that the batch amount is greater than the sum of change.
1396+
if batchAmt <= btcutil.Amount(sumChange) {
1397+
return nil, 0, 0, 0, fmt.Errorf("batch amount %v is <= the "+
1398+
"sum of change outputs %v", batchAmt,
1399+
btcutil.Amount(sumChange))
1400+
}
1401+
13711402
// Clamp the calculated fee to the max allowed fee amount for the batch.
1372-
fee := clampBatchFee(feeForWeight, batchAmt)
1403+
fee := clampBatchFee(feeForWeight, batchAmt-btcutil.Amount(sumChange))
13731404

1374-
// Add the batch transaction output, which excludes the fees paid to
1375-
// miners.
1405+
// Ensure that batch amount exceeds the sum of change outputs and the
1406+
// fee, and that it is also greater than dust limit for the main
1407+
// output.
1408+
dustLimit := utils.DustLimitForPkScript(batchPkScript)
1409+
if fee+btcutil.Amount(sumChange)+dustLimit > batchAmt {
1410+
return nil, 0, 0, 0, fmt.Errorf("batch amount %v is <= the "+
1411+
"sum of change outputs %v plus fee %v and dust "+
1412+
"limit %v", batchAmt, btcutil.Amount(sumChange),
1413+
fee, dustLimit)
1414+
}
1415+
1416+
// Add the main output first.
13761417
batchTx.AddTxOut(&wire.TxOut{
13771418
PkScript: batchPkScript,
1378-
Value: int64(batchAmt - fee),
1419+
Value: int64(batchAmt-fee) - sumChange,
13791420
})
1421+
// Then add change outputs.
1422+
for _, txOut := range changeOutputs {
1423+
batchTx.AddTxOut(&wire.TxOut{
1424+
PkScript: txOut.PkScript,
1425+
Value: txOut.Value,
1426+
})
1427+
}
1428+
1429+
// Check that for each swap, inputs exceed the change outputs.
1430+
if len(changeOutputs) != 0 {
1431+
swap2Inputs := make(map[lntypes.Hash]btcutil.Amount)
1432+
swap2Change := make(map[lntypes.Hash]btcutil.Amount)
1433+
for _, sweep := range sweeps {
1434+
swap2Inputs[sweep.swapHash] += sweep.value
1435+
if sweep.change != nil {
1436+
swap2Change[sweep.swapHash] +=
1437+
btcutil.Amount(sweep.change.Value)
1438+
}
1439+
}
1440+
1441+
for swapHash, inputs := range swap2Inputs {
1442+
change := swap2Change[swapHash]
1443+
if inputs <= change {
1444+
return nil, 0, 0, 0, fmt.Errorf(""+
1445+
"inputs %v <= change %v for swap %x",
1446+
inputs, change, swapHash[:6])
1447+
}
1448+
}
1449+
}
1450+
1451+
// Ensure that each output is above dust limit.
1452+
for _, txOut := range batchTx.TxOut {
1453+
dustLimit = utils.DustLimitForPkScript(txOut.PkScript)
1454+
if btcutil.Amount(txOut.Value) < dustLimit {
1455+
return nil, 0, 0, 0, fmt.Errorf("output %v is below "+
1456+
"dust limit %v", btcutil.Amount(txOut.Value),
1457+
dustLimit)
1458+
}
1459+
}
13801460

13811461
return batchTx, weight, feeForWeight, fee, nil
13821462
}

0 commit comments

Comments
 (0)