Skip to content

Commit a5871d6

Browse files
committed
sweepbatcher: fix change fee accounting, add test
Presigned sweeps that produce a change output misreported the on-chain fee. The fee portion was derived from the total swept amount minus only the first transaction output, so any change output was treated as additional fee. Update getFeePortionForSweep to subtract the value of every tx output so the fee portion reflects only the actual miner fee paid. Add regression coverage that sweeps a presigned input with change and asserts the spend and confirmation notifications report the corrected fee.
1 parent 7193552 commit a5871d6

File tree

2 files changed

+148
-2
lines changed

2 files changed

+148
-2
lines changed

sweepbatcher/sweep_batch.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,8 +2071,8 @@ func getFeePortionForSweep(spendTx *wire.MsgTx, numSweeps int,
20712071
totalSweptAmt btcutil.Amount) (btcutil.Amount, btcutil.Amount) {
20722072

20732073
totalFee := int64(totalSweptAmt)
2074-
if len(spendTx.TxOut) > 0 {
2075-
totalFee -= spendTx.TxOut[0].Value
2074+
for _, txOut := range spendTx.TxOut {
2075+
totalFee -= txOut.Value
20762076
}
20772077
feePortionPerSweep := totalFee / int64(numSweeps)
20782078
roundingDiff := totalFee - (int64(numSweeps) * feePortionPerSweep)

sweepbatcher/sweep_batcher_presigned_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sweepbatcher
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"os"
@@ -1268,6 +1269,147 @@ func testPresigned_presigned_group_with_change(t *testing.T,
12681269
require.NoError(t, lnd.NotifyHeight(601))
12691270
}
12701271

1272+
// testPresigned_fee_portion_with_change ensures that the fee portion reported
1273+
// to clients accounts for change outputs in the presigned transaction.
1274+
func testPresigned_fee_portion_with_change(t *testing.T,
1275+
batcherStore testBatcherStore) {
1276+
1277+
defer test.Guard(t)()
1278+
1279+
lnd := test.NewMockLnd()
1280+
1281+
ctx, cancel := context.WithCancel(context.Background())
1282+
defer cancel()
1283+
1284+
customFeeRate := func(_ context.Context, _ lntypes.Hash,
1285+
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
1286+
1287+
return chainfee.SatPerKWeight(10_000), nil
1288+
}
1289+
1290+
presignedHelper := newMockPresignedHelper()
1291+
1292+
batcher := NewBatcher(
1293+
lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
1294+
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
1295+
batcherStore, presignedHelper,
1296+
WithCustomFeeRate(customFeeRate),
1297+
WithPresignedHelper(presignedHelper),
1298+
)
1299+
1300+
go func() {
1301+
err := batcher.Run(ctx)
1302+
checkBatcherError(t, err)
1303+
}()
1304+
1305+
swapHash := lntypes.Hash{2, 2, 2}
1306+
op := wire.OutPoint{
1307+
Hash: chainhash.Hash{2, 2},
1308+
Index: 2,
1309+
}
1310+
group := []Input{
1311+
{
1312+
Outpoint: op,
1313+
Value: 1_000_000,
1314+
},
1315+
}
1316+
change := &wire.TxOut{
1317+
Value: 250_000,
1318+
PkScript: []byte{0xca, 0xfe},
1319+
}
1320+
1321+
presignedHelper.setChangeForPrimaryDeposit(op, change)
1322+
presignedHelper.SetOutpointOnline(op, true)
1323+
1324+
require.NoError(t, batcher.PresignSweepsGroup(
1325+
ctx, group, sweepTimeout, destAddr, change,
1326+
))
1327+
1328+
spendChan := make(chan *SpendDetail, 1)
1329+
confChan := make(chan *ConfDetail, 1)
1330+
notifier := &SpendNotifier{
1331+
SpendChan: spendChan,
1332+
SpendErrChan: make(chan error, 1),
1333+
ConfChan: confChan,
1334+
ConfErrChan: make(chan error, 1),
1335+
QuitChan: make(chan bool, 1),
1336+
}
1337+
1338+
require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
1339+
SwapHash: swapHash,
1340+
Inputs: group,
1341+
Notifier: notifier,
1342+
}))
1343+
1344+
spendReg := <-lnd.RegisterSpendChannel
1345+
require.NotNil(t, spendReg)
1346+
require.NotNil(t, spendReg.Outpoint)
1347+
require.Equal(t, op, *spendReg.Outpoint)
1348+
1349+
tx := <-lnd.TxPublishChannel
1350+
require.Len(t, tx.TxIn, 1)
1351+
require.Len(t, tx.TxOut, 2)
1352+
1353+
var (
1354+
outputSum int64
1355+
foundChange bool
1356+
)
1357+
for _, txOut := range tx.TxOut {
1358+
outputSum += txOut.Value
1359+
if txOut.Value != change.Value {
1360+
continue
1361+
}
1362+
1363+
if !bytes.Equal(txOut.PkScript, change.PkScript) {
1364+
continue
1365+
}
1366+
1367+
foundChange = true
1368+
}
1369+
1370+
require.True(t, foundChange)
1371+
1372+
totalInput := int64(group[0].Value)
1373+
require.LessOrEqual(t, outputSum, totalInput)
1374+
1375+
expectedFee := btcutil.Amount(totalInput - outputSum)
1376+
require.Greater(t, expectedFee, btcutil.Amount(0))
1377+
1378+
txHash := tx.TxHash()
1379+
spendDetail := &chainntnfs.SpendDetail{
1380+
SpentOutPoint: &op,
1381+
SpendingTx: tx,
1382+
SpenderTxHash: &txHash,
1383+
SpenderInputIndex: 0,
1384+
SpendingHeight: spendReg.HeightHint + 1,
1385+
}
1386+
lnd.SpendChannel <- spendDetail
1387+
1388+
spend := <-spendChan
1389+
require.Equal(t, expectedFee, spend.OnChainFeePortion)
1390+
1391+
confReg := <-lnd.RegisterConfChannel
1392+
require.True(t, bytes.Equal(tx.TxOut[0].PkScript, confReg.PkScript) ||
1393+
bytes.Equal(tx.TxOut[1].PkScript, confReg.PkScript))
1394+
1395+
require.NoError(
1396+
t, lnd.NotifyHeight(spendReg.HeightHint+batchConfHeight+1),
1397+
)
1398+
lnd.ConfChannel <- &chainntnfs.TxConfirmation{Tx: tx}
1399+
1400+
require.Eventually(t, func() bool {
1401+
select {
1402+
case <-presignedHelper.cleanupCalled:
1403+
return true
1404+
default:
1405+
return false
1406+
}
1407+
}, test.Timeout, eventuallyCheckFrequency)
1408+
1409+
conf := <-confChan
1410+
require.Equal(t, expectedFee, conf.OnChainFeePortion)
1411+
}
1412+
12711413
// testPresigned_presigned_group_with_identical_change_pkscript tests passing multiple sweeps to
12721414
// the method PresignSweepsGroup. It tests that a change output of a primary
12731415
// deposit sweep is properly added to the presigned transaction.
@@ -2356,6 +2498,10 @@ func TestPresigned(t *testing.T) {
23562498
testPresigned_presigned_group_with_change(t, NewStoreMock())
23572499
})
23582500

2501+
t.Run("fee_portion_change", func(t *testing.T) {
2502+
testPresigned_fee_portion_with_change(t, NewStoreMock())
2503+
})
2504+
23592505
t.Run("identical change pkscript", func(t *testing.T) {
23602506
testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
23612507
})

0 commit comments

Comments
 (0)