Skip to content

Commit 4458577

Browse files
authored
Merge pull request #1020 from starius/sweepbatcher-change-outputs-fees-fix
sweepbatcher: fix change fee accounting, add test
2 parents 7193552 + 1f15c60 commit 4458577

File tree

3 files changed

+161
-3
lines changed

3 files changed

+161
-3
lines changed

sweepbatcher/presigned.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,10 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
506506

507507
// Find actual fee rate of the signed transaction. It may differ from
508508
// the desired fee rate, because SignTx may return a presigned tx.
509-
output := btcutil.Amount(tx.TxOut[0].Value)
509+
var output btcutil.Amount
510+
for _, txOut := range tx.TxOut {
511+
output += btcutil.Amount(txOut.Value)
512+
}
510513
fee = batchAmt - output
511514
signedFeeRate := chainfee.NewSatPerKWeight(fee, realWeight)
512515

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

2510+
t.Run("fee_portion_change", func(t *testing.T) {
2511+
testPresigned_fee_portion_with_change(t, NewStoreMock())
2512+
})
2513+
23592514
t.Run("identical change pkscript", func(t *testing.T) {
23602515
testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
23612516
})

0 commit comments

Comments
 (0)