Skip to content

Commit 26c8b27

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 4cb5c2c commit 26c8b27

File tree

7 files changed

+893
-104
lines changed

7 files changed

+893
-104
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"
@@ -1239,10 +1240,14 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
12391240
}
12401241

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

12471252
// Sanity check, there should be at least 1 sweep in this batch.
12481253
if len(sweeps) == 0 {
@@ -1255,6 +1260,13 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
12551260
LockTime: uint32(currentHeight),
12561261
}
12571262

1263+
var changeOutputs []*wire.TxOut
1264+
for _, sweep := range sweeps {
1265+
if sweep.change != nil {
1266+
changeOutputs = append(changeOutputs, sweep.change)
1267+
}
1268+
}
1269+
12581270
// Add transaction inputs and estimate its weight.
12591271
var weightEstimate input.TxWeightEstimator
12601272
for _, sweep := range sweeps {
@@ -1300,6 +1312,11 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13001312
"failed: %w", err)
13011313
}
13021314

1315+
// Add the optional change outputs to weight estimates.
1316+
for _, o := range changeOutputs {
1317+
weightEstimate.AddOutput(o.PkScript)
1318+
}
1319+
13031320
// Keep track of the total amount this batch is sweeping back.
13041321
batchAmt := btcutil.Amount(0)
13051322
for _, sweep := range sweeps {
@@ -1317,15 +1334,78 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13171334
feeForWeight++
13181335
}
13191336

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

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

13301410
return batchTx, weight, feeForWeight, fee, nil
13311411
}

0 commit comments

Comments
 (0)