Skip to content

Commit 29348a9

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 bc7d155 commit 29348a9

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
@@ -1296,9 +1296,9 @@ func (b *batch) createPsbt(unsignedTx *wire.MsgTx, sweeps []sweep) ([]byte,
12961296
// outputs. If the main output value is below dust limit this function will
12971297
// return an error.
12981298
func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
1299-
currentHeight int32, feeRate chainfee.SatPerKWeight) (
1300-
*wire.MsgTx, lntypes.WeightUnit, btcutil.Amount, btcutil.Amount,
1301-
error) {
1299+
currentHeight int32, feeRate chainfee.SatPerKWeight,
1300+
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx,
1301+
lntypes.WeightUnit, btcutil.Amount, btcutil.Amount, error) {
13021302

13031303
// Sanity check, there should be at least 1 sweep in this batch.
13041304
if len(sweeps) == 0 {
@@ -1400,7 +1400,14 @@ func constructUnsignedTx(sweeps []sweep, address btcutil.Address,
14001400
}
14011401

14021402
// Clamp the calculated fee to the max allowed fee amount for the batch.
1403-
fee := clampBatchFee(feeForWeight, batchAmt-btcutil.Amount(sumChange))
1403+
fee, err := clampBatchFee(
1404+
feeForWeight, batchAmt-btcutil.Amount(sumChange),
1405+
minRelayFeeRate, weight,
1406+
)
1407+
if err != nil {
1408+
return nil, 0, 0, 0, fmt.Errorf("failed to clamp batch "+
1409+
"fee: %w", err)
1410+
}
14041411

14051412
// Ensure that batch amount exceeds the sum of change outputs and the
14061413
// fee, and that it is also greater than dust limit for the main
@@ -1516,15 +1523,21 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
15161523
// known in advance to be non-cooperative (nonCoopHint) and not failed
15171524
// to sign cooperatively in previous rounds (coopFailed). If any of them
15181525
// fails, the sweep is excluded from all following rounds and another
1519-
// round is attempted. Otherwise the cycle completes and we sign the
1526+
// round is attempted. Otherwise, the cycle completes and we sign the
15201527
// remaining sweeps non-cooperatively.
15211528
var (
1522-
tx *wire.MsgTx
1523-
weight lntypes.WeightUnit
1524-
feeForWeight btcutil.Amount
1525-
fee btcutil.Amount
1526-
coopInputs int
1529+
tx *wire.MsgTx
1530+
weight lntypes.WeightUnit
1531+
feeForWeight btcutil.Amount
1532+
fee btcutil.Amount
1533+
minRelayFeeRate chainfee.SatPerKWeight
1534+
coopInputs int
15271535
)
1536+
minRelayFeeRate, err := b.wallet.MinRelayFee(ctx)
1537+
if err != nil {
1538+
return 0, fmt.Errorf("failed to get min relay fee: %w", err),
1539+
false
1540+
}
15281541
for attempt := 1; ; attempt++ {
15291542
b.Infof("Attempt %d of collecting cooperative signatures.",
15301543
attempt)
@@ -1533,6 +1546,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
15331546
var err error
15341547
tx, weight, feeForWeight, fee, err = constructUnsignedTx(
15351548
sweeps, address, b.currentHeight, b.rbfCache.FeeRate,
1549+
minRelayFeeRate,
15361550
)
15371551
if err != nil {
15381552
return 0, fmt.Errorf("failed to construct tx: %w", err),
@@ -1584,7 +1598,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
15841598
// If there was any failure of cooperative signing, we need to
15851599
// update weight estimates (since non-cooperative signing has
15861600
// larger witness) and hence update the whole transaction and
1587-
// all the signatures. Otherwise we complete cooperative part.
1601+
// all the signatures. Otherwise, we complete cooperative part.
15881602
if !newCoopFailures {
15891603
break
15901604
}
@@ -1720,7 +1734,7 @@ func (b *batch) publishMixedBatch(ctx context.Context) (btcutil.Amount, error,
17201734
}
17211735

17221736
// Publish the transaction.
1723-
err := b.wallet.PublishTransaction(
1737+
err = b.wallet.PublishTransaction(
17241738
ctx, tx, b.cfg.txLabeler(b.id),
17251739
)
17261740
if err != nil {
@@ -2583,16 +2597,25 @@ func (b *batch) persistSweep(ctx context.Context, sweep sweep,
25832597

25842598
// clampBatchFee takes the fee amount and total amount of the sweeps in the
25852599
// batch and makes sure the fee is not too high. If the fee is too high, it is
2586-
// clamped to the maximum allowed fee.
2587-
func clampBatchFee(fee btcutil.Amount,
2588-
totalAmount btcutil.Amount) btcutil.Amount {
2600+
// clamped to the maximum allowed fee. If the clamped fee results in a fee rate
2601+
// below the minimum relay fee, an error is returned.
2602+
func clampBatchFee(fee btcutil.Amount, totalAmount btcutil.Amount,
2603+
minRelayFeeRate chainfee.SatPerKWeight,
2604+
weight lntypes.WeightUnit) (btcutil.Amount, error) {
25892605

25902606
maxFeeAmount := btcutil.Amount(float64(totalAmount) *
25912607
maxFeeToSwapAmtRatio)
25922608

2609+
clampedFee := fee
25932610
if fee > maxFeeAmount {
2594-
return maxFeeAmount
2611+
clampedFee = maxFeeAmount
2612+
}
2613+
2614+
clampedFeeRate := chainfee.NewSatPerKWeight(clampedFee, weight)
2615+
if clampedFeeRate < minRelayFeeRate {
2616+
return 0, fmt.Errorf("clamped fee rate %v is less than "+
2617+
"minimum relay fee %v", clampedFeeRate, minRelayFeeRate)
25952618
}
25962619

2597-
return fee
2620+
return clampedFee, nil
25982621
}

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)