Skip to content

Commit 1b31b88

Browse files
committed
sweepbatcher: consider min relay fee when constructing batch tx
if constructUnsignedTx constructs a batch transaction that is below the minimum relay fee, an error is returned.
1 parent 75e4704 commit 1b31b88

File tree

7 files changed

+184
-44
lines changed

7 files changed

+184
-44
lines changed

loopout_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ func testCustomSweepConfTarget(t *testing.T) {
280280
// yields a much higher fee rate.
281281
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, 250)
282282
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000)
283+
ctx.Lnd.SetMinRelayFee(250)
283284

284285
cfg := newSwapConfig(
285286
&lnd.LndServices, loopdb.NewStoreMock(t), server, nil,

sweepbatcher/presigned.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,14 @@ func (b *batch) ensurePresigned(ctx context.Context, newSweeps []*sweep,
2828
"adding to an empty batch")
2929
}
3030

31+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
32+
if err != nil {
33+
return fmt.Errorf("failed to get minRelayFee: %w", err)
34+
}
35+
3136
return ensurePresigned(
32-
ctx, newSweeps, b.cfg.presignedHelper, b.cfg.chainParams,
37+
ctx, newSweeps, b.cfg.presignedHelper, minRelayFeeRate,
38+
b.cfg.chainParams,
3339
)
3440
}
3541

@@ -43,6 +49,7 @@ type presignedTxChecker interface {
4349
// inputs of this group only.
4450
func ensurePresigned(ctx context.Context, newSweeps []*sweep,
4551
presignedTxChecker presignedTxChecker,
52+
minRelayFeeRate chainfee.SatPerKWeight,
4653
chainParams *chaincfg.Params) error {
4754

4855
sweeps := make([]sweep, len(newSweeps))
@@ -74,7 +81,7 @@ func ensurePresigned(ctx context.Context, newSweeps []*sweep,
7481
const feeRate = chainfee.FeePerKwFloor
7582

7683
tx, _, _, _, err := constructUnsignedTx(
77-
sweeps, destAddr, currentHeight, feeRate,
84+
sweeps, destAddr, currentHeight, feeRate, minRelayFeeRate,
7885
)
7986
if err != nil {
8087
return fmt.Errorf("failed to construct unsigned tx "+
@@ -218,6 +225,14 @@ func (b *batch) presign(ctx context.Context, newSweeps []*sweep) error {
218225

219226
b.Infof("nextBlockFeeRate is %v", nextBlockFeeRate)
220227

228+
// Find the minRelayFeeRate.
229+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
230+
if err != nil {
231+
return fmt.Errorf("failed to get minRelayFeeRate: %w", err)
232+
}
233+
234+
b.Infof("minRelayFeeRate is %v", minRelayFeeRate)
235+
221236
// We need to restore previously added groups. We can do it by reading
222237
// all the sweeps from DB (they must be ordered) and grouping by swap.
223238
groups, err := b.getSweepsGroups(ctx)
@@ -258,7 +273,7 @@ func (b *batch) presign(ctx context.Context, newSweeps []*sweep) error {
258273

259274
err = presign(
260275
ctx, b.cfg.presignedHelper, destAddr, primarySweepID,
261-
sweeps, nextBlockFeeRate,
276+
sweeps, nextBlockFeeRate, minRelayFeeRate,
262277
)
263278
if err != nil {
264279
return fmt.Errorf("failed to presign a transaction "+
@@ -300,7 +315,8 @@ type presigner interface {
300315
// 10x of the current next block feerate.
301316
func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
302317
primarySweepID wire.OutPoint, sweeps []sweep,
303-
nextBlockFeeRate chainfee.SatPerKWeight) error {
318+
nextBlockFeeRate chainfee.SatPerKWeight,
319+
minRelayFeeRate chainfee.SatPerKWeight) error {
304320

305321
if presigner == nil {
306322
return fmt.Errorf("presigner is not installed")
@@ -354,7 +370,7 @@ func presign(ctx context.Context, presigner presigner, destAddr btcutil.Address,
354370
for fr := start; fr <= stop; fr = (fr * factorPPM) / 1_000_000 {
355371
// Construct an unsigned transaction for this fee rate.
356372
tx, _, feeForWeight, fee, err := constructUnsignedTx(
357-
sweeps, destAddr, currentHeight, fr,
373+
sweeps, destAddr, currentHeight, fr, minRelayFeeRate,
358374
)
359375
if err != nil {
360376
return fmt.Errorf("failed to construct unsigned tx "+
@@ -411,15 +427,15 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
411427
}
412428

413429
// Determine the current minimum relay fee based on our chain backend.
414-
minRelayFee, err := b.wallet.MinRelayFee(ctx)
430+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
415431
if err != nil {
416-
return 0, fmt.Errorf("failed to get minRelayFee: %w", err),
432+
return 0, fmt.Errorf("failed to get minRelayFeeRate: %w", err),
417433
false
418434
}
419435

420436
// Cache current height and desired feerate of the batch.
421437
currentHeight := b.currentHeight
422-
feeRate := max(b.rbfCache.FeeRate, minRelayFee)
438+
feeRate := max(b.rbfCache.FeeRate, minRelayFeeRate)
423439

424440
// Append this sweep to an array of sweeps. This is needed to keep the
425441
// order of sweeps stored, as iterating the sweeps map does not
@@ -441,7 +457,7 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
441457

442458
// Construct unsigned batch transaction.
443459
tx, weight, _, fee, err := constructUnsignedTx(
444-
sweeps, address, currentHeight, feeRate,
460+
sweeps, address, currentHeight, feeRate, minRelayFeeRate,
445461
)
446462
if err != nil {
447463
return 0, fmt.Errorf("failed to construct tx: %w", err),
@@ -460,7 +476,7 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
460476
// Get a pre-signed transaction.
461477
const loadOnly = false
462478
signedTx, err := b.cfg.presignedHelper.SignTx(
463-
ctx, b.primarySweepID, tx, batchAmt, minRelayFee, feeRate,
479+
ctx, b.primarySweepID, tx, batchAmt, minRelayFeeRate, feeRate,
464480
loadOnly,
465481
)
466482
if err != nil {
@@ -470,7 +486,7 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
470486

471487
// Run sanity checks to make sure presignedHelper.SignTx complied with
472488
// all the invariants.
473-
err = CheckSignedTx(tx, signedTx, batchAmt, minRelayFee)
489+
err = CheckSignedTx(tx, signedTx, batchAmt, minRelayFeeRate)
474490
if err != nil {
475491
return 0, fmt.Errorf("signed tx doesn't correspond the "+
476492
"unsigned tx: %w", err), false

sweepbatcher/presigned_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515
"github.com/stretchr/testify/require"
1616
)
1717

18+
const (
19+
minRelayFeeRate = chainfee.FeePerKwFloor
20+
)
21+
1822
// TestOrderedSweeps checks that methods batch.getOrderedSweeps and
1923
// batch.getSweepsGroups works properly.
2024
func TestOrderedSweeps(t *testing.T) {
@@ -561,7 +565,7 @@ func TestEnsurePresigned(t *testing.T) {
561565
}
562566

563567
err := ensurePresigned(
564-
ctx, tc.sweeps, c,
568+
ctx, tc.sweeps, c, minRelayFeeRate,
565569
&chaincfg.RegressionNetParams,
566570
)
567571
switch {
@@ -1010,7 +1014,7 @@ func TestPresign(t *testing.T) {
10101014
err := presign(
10111015
ctx, tc.presigner, tc.destAddr,
10121016
tc.primarySweepID, tc.sweeps,
1013-
tc.nextBlockFeeRate,
1017+
tc.nextBlockFeeRate, minRelayFeeRate,
10141018
)
10151019
if tc.wantErr != "" {
10161020
require.Error(t, err)

sweepbatcher/sweep_batch.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,9 +1245,9 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
12451245
// outputs. If the main output value is below dust limit this function will
12461246
// return an error.
12471247
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
1248-
currentHeight int32, feeRate chainfee.SatPerKWeight) (
1249-
*wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount,
1250-
error) {
1248+
currentHeight int32, feeRate chainfee.SatPerKWeight,
1249+
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx,
1250+
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
12511251

12521252
// Sanity check, there should be at least 1 sweep in this batch.
12531253
if len(sweeps) == 0 {
@@ -1349,7 +1349,14 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
13491349
}
13501350

13511351
// Clamp the calculated fee to the max allowed fee amount for the batch.
1352-
fee := clampBatchFee(feeForWeight, batchAmt-btcutil.Amount(sumChange))
1352+
fee, err := clampBatchFee(
1353+
feeForWeight, batchAmt-btcutil.Amount(sumChange),
1354+
minRelayFeeRate, weight,
1355+
)
1356+
if err != nil {
1357+
return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
1358+
"fee: %w", err)
1359+
}
13531360

13541361
// Ensure that batch amount exceeds the sum of change outputs and the
13551362
// fee, and that it is also greater than dust limit for the main
@@ -1465,15 +1472,21 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
14651472
// known in advance to be non-cooperative (nonCoopHint) and not failed
14661473
// to sign cooperatively in previous rounds (coopFailed). If any of them
14671474
// fails, the sweep is excluded from all following rounds and another
1468-
// round is attempted. Otherwise the cycle completes and we sign the
1475+
// round is attempted. Otherwise, the cycle completes and we sign the
14691476
// remaining sweeps non-cooperatively.
14701477
var (
1471-
tx *wire.MsgTx
1472-
weight lntypes.WeightUnit
1473-
feeForWeight btcutil.Amount
1474-
fee btcutil.Amount
1475-
coopInputs int
1478+
tx *wire.MsgTx
1479+
weight lntypes.WeightUnit
1480+
feeForWeight btcutil.Amount
1481+
fee btcutil.Amount
1482+
minRelayFeeRate chainfee.SatPerKWeight
1483+
coopInputs int
14761484
)
1485+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
1486+
if err != nil {
1487+
return 0, fmt.Errorf("failed to get min relay fee: %w", err),
1488+
false
1489+
}
14771490
for attempt := 1; ; attempt++ {
14781491
b.Infof("Attempt %d of collecting cooperative signatures.",
14791492
attempt)
@@ -1482,6 +1495,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
14821495
var err error
14831496
tx, weight, feeForWeight, fee, err = constructUnsignedTx(
14841497
sweeps, address, b.currentHeight, b.rbfCache.FeeRate,
1498+
minRelayFeeRate,
14851499
)
14861500
if err != nil {
14871501
return 0, fmt.Errorf("failed to construct tx: %w", err),
@@ -1533,7 +1547,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
15331547
// If there was any failure of cooperative signing, we need to
15341548
// update weight estimates (since non-cooperative signing has
15351549
// larger witness) and hence update the whole transaction and
1536-
// all the signatures. Otherwise we complete cooperative part.
1550+
// all the signatures. Otherwise, we complete cooperative part.
15371551
if !newCoopFailures {
15381552
break
15391553
}
@@ -1669,7 +1683,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
16691683
}
16701684

16711685
// Publish the transaction.
1672-
err := b.wallet.PublishTransaction(
1686+
err = b.wallet.PublishTransaction(
16731687
ctx, tx, b.cfg.txLabeler(b.id),
16741688
)
16751689
if err != nil {
@@ -2565,16 +2579,25 @@ func (b *batch) persistSweep(ctx context.Context, sweep sweep,
25652579

25662580
// clampBatchFee takes the fee amount and total amount of the sweeps in the
25672581
// batch and makes sure the fee is not too high. If the fee is too high, it is
2568-
// clamped to the maximum allowed fee.
2569-
func clampBatchFee(fee btcutil.Amount,
2570-
totalAmount btcutil.Amount) btcutil.Amount {
2582+
// clamped to the maximum allowed fee. If the clamped fee results in a fee rate
2583+
// below the minimum relay fee, an error is returned.
2584+
func clampBatchFee(fee btcutil.Amount, totalAmount btcutil.Amount,
2585+
minRelayFeeRate chainfee.SatPerKWeight,
2586+
weight lntypes.WeightUnit) (btcutil.Amount, error) {
25712587

25722588
maxFeeAmount := btcutil.Amount(float64(totalAmount) *
25732589
maxFeeToSwapAmtRatio)
25742590

2591+
clampedFee := fee
25752592
if fee > maxFeeAmount {
2576-
return maxFeeAmount
2593+
clampedFee = maxFeeAmount
2594+
}
2595+
2596+
clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight)
2597+
if clampedFeeRate < minRelayFeeRate {
2598+
return 0, fmt.Errorf("clamped fee rate %v is less than "+
2599+
"minimum relay fee %v", clampedFeeRate, minRelayFeeRate)
25772600
}
25782601

2579-
return fee
2602+
return clampedFee, nil
25802603
}

sweepbatcher/sweep_batch_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,13 @@ func TestConstructUnsignedTx(t *testing.T) {
440440
change: change1,
441441
},
442442
},
443-
address: p2trAddress,
444-
currentHeight: 800_000,
445-
feeRate: 1,
443+
address: p2trAddress,
444+
currentHeight: 800_000,
445+
feeRate: 1_000,
446+
minRelayFeeRate: 50,
446447
wantErr: "batch amount 0.00100294 BTC is <= the sum " +
447448
"of change outputs 0.00100000 BTC plus fee " +
448-
"0.00000001 BTC and dust limit 0.00000330 BTC",
449+
"0.00000058 BTC and dust limit 0.00000330 BTC",
449450
},
450451

451452
{

sweepbatcher/sweep_batcher.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,10 @@ func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input,
736736
if err != nil {
737737
return fmt.Errorf("failed to get nextBlockFeeRate: %w", err)
738738
}
739+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
740+
if err != nil {
741+
return fmt.Errorf("failed to get minRelayFeeRate: %w", err)
742+
}
739743
destPkscript, err := txscript.PayToAddrScript(destAddress)
740744
if err != nil {
741745
return fmt.Errorf("txscript.PayToAddrScript failed: %w", err)
@@ -763,7 +767,7 @@ func (b *Batcher) PresignSweepsGroup(ctx context.Context, inputs []Input,
763767

764768
return presign(
765769
ctx, b.presignedHelper, destAddress, primarySweepID, sweeps,
766-
nextBlockFeeRate,
770+
nextBlockFeeRate, minRelayFeeRate,
767771
)
768772
}
769773

@@ -818,12 +822,18 @@ func (b *Batcher) AddSweep(ctx context.Context, sweepReq *SweepRequest) error {
818822
}
819823
}
820824

825+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
826+
if err != nil {
827+
return fmt.Errorf("failed to get min relay fee: %w", err)
828+
}
829+
821830
// If this is a presigned mode, make sure PresignSweepsGroup was called.
822831
// We skip the check for reorg-safely confirmed sweeps, because their
823832
// presigned transactions were already cleaned up from the store.
824833
if sweep.presigned && !fullyConfirmed {
825834
err := ensurePresigned(
826-
ctx, sweeps, b.presignedHelper, b.chainParams,
835+
ctx, sweeps, b.presignedHelper, minRelayFeeRate,
836+
b.chainParams,
827837
)
828838
if err != nil {
829839
return fmt.Errorf("inputs with primarySweep %v were "+

0 commit comments

Comments
 (0)