Skip to content

Commit f19f9f5

Browse files
committed
sweepbatcher/presigned: minRelayFee edge cases
Make sure that broadcasted tx has feeRate >= minRelayFee. Make sure that feeRate of broadcasted tx doesn't decrease.
1 parent 7735bdf commit f19f9f5

File tree

2 files changed

+132
-8
lines changed

2 files changed

+132
-8
lines changed

sweepbatcher/presigned.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,16 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
407407
}
408408
}
409409

410+
// Determine the current minimum relay fee based on our chain backend.
411+
minRelayFee, err := b.wallet.MinRelayFee(ctx)
412+
if err != nil {
413+
return 0, fmt.Errorf("failed to get minRelayFee: %w", err),
414+
false
415+
}
416+
410417
// Cache current height and desired feerate of the batch.
411418
currentHeight := b.currentHeight
412-
feeRate := b.rbfCache.FeeRate
419+
feeRate := max(b.rbfCache.FeeRate, minRelayFee)
413420

414421
// Append this sweep to an array of sweeps. This is needed to keep the
415422
// order of sweeps stored, as iterating the sweeps map does not
@@ -447,13 +454,6 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
447454
batchAmt += sweep.value
448455
}
449456

450-
// Determine the current minimum relay fee based on our chain backend.
451-
minRelayFee, err := b.wallet.MinRelayFee(ctx)
452-
if err != nil {
453-
return 0, fmt.Errorf("failed to get minRelayFee: %w", err),
454-
false
455-
}
456-
457457
// Get a pre-signed transaction.
458458
const loadOnly = false
459459
signedTx, err := b.cfg.presignedHelper.SignTx(
@@ -508,6 +508,9 @@ func (b *batch) publishPresigned(ctx context.Context) (btcutil.Amount, error,
508508
b.batchTxid = &txHash
509509
b.batchPkScript = tx.TxOut[0].PkScript
510510

511+
// Update cached FeeRate not to broadcast a tx with lower feeRate.
512+
b.rbfCache.FeeRate = max(b.rbfCache.FeeRate, signedFeeRate)
513+
511514
return fee, nil, true
512515
}
513516

sweepbatcher/sweep_batcher_presigned_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ func (h *mockPresignedHelper) SignTx(ctx context.Context,
159159
h.mu.Lock()
160160
defer h.mu.Unlock()
161161

162+
if feeRate < minRelayFee {
163+
return nil, fmt.Errorf("feeRate (%v) is below minRelayFee (%v)",
164+
feeRate, minRelayFee)
165+
}
166+
162167
// If all the inputs are online and loadOnly is not set, sign this exact
163168
// transaction.
164169
if offline := h.offlineInputs(tx); len(offline) == 0 && !loadOnly {
@@ -492,6 +497,118 @@ func testPresigned_input1_offline_then_input2(t *testing.T,
492497
require.NoError(t, err)
493498
}
494499

500+
// testPresigned_min_relay_fee tests that online and presigned transactions
501+
// comply with min_relay_fee.
502+
func testPresigned_min_relay_fee(t *testing.T,
503+
batcherStore testBatcherStore) {
504+
505+
defer test.Guard(t)()
506+
507+
lnd := test.NewMockLnd()
508+
ctx, cancel := context.WithCancel(context.Background())
509+
defer cancel()
510+
511+
const inputAmt = 1_000_000
512+
513+
customFeeRate := func(_ context.Context, _ lntypes.Hash,
514+
_ wire.OutPoint) (chainfee.SatPerKWeight, error) {
515+
516+
return chainfee.FeePerKwFloor, nil
517+
}
518+
519+
presignedHelper := newMockPresignedHelper()
520+
521+
batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
522+
testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
523+
batcherStore, presignedHelper,
524+
WithCustomFeeRate(customFeeRate),
525+
WithPresignedHelper(presignedHelper))
526+
go func() {
527+
err := batcher.Run(ctx)
528+
checkBatcherError(t, err)
529+
}()
530+
531+
// Set high min_relay_fee.
532+
lnd.SetMinRelayFee(400)
533+
534+
// Create the first sweep.
535+
swapHash1 := lntypes.Hash{1, 1, 1}
536+
op1 := wire.OutPoint{
537+
Hash: chainhash.Hash{1, 1},
538+
Index: 1,
539+
}
540+
sweepReq1 := SweepRequest{
541+
SwapHash: swapHash1,
542+
Inputs: []Input{{
543+
Value: inputAmt,
544+
Outpoint: op1,
545+
}},
546+
Notifier: &dummyNotifier,
547+
}
548+
549+
// Enable the input and presign.
550+
presignedHelper.SetOutpointOnline(op1, true)
551+
err := batcher.PresignSweepsGroup(
552+
ctx, []Input{{Outpoint: op1, Value: inputAmt}},
553+
sweepTimeout, destAddr,
554+
)
555+
require.NoError(t, err)
556+
557+
// Deliver sweep request to batcher.
558+
require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
559+
560+
// Since a batch was created we check that it registered for its primary
561+
// sweep's spend.
562+
<-lnd.RegisterSpendChannel
563+
564+
// Wait for a transactions to be published.
565+
tx := <-lnd.TxPublishChannel
566+
gotFeeRate := presignedHelper.getTxFeerate(tx, inputAmt)
567+
require.Equal(t, chainfee.SatPerKWeight(402), gotFeeRate)
568+
569+
// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
570+
// The only difference of tx2 is a higher lock_time.
571+
lnd.SetMinRelayFee(300)
572+
require.NoError(t, lnd.NotifyHeight(601))
573+
tx2 := <-lnd.TxPublishChannel
574+
require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value)
575+
gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
576+
require.Equal(t, chainfee.SatPerKWeight(402), gotFeeRate)
577+
require.Equal(t, uint32(601), tx2.LockTime)
578+
579+
// Set a higher min_relay_fee, turn off the client and try presigned tx.
580+
lnd.SetMinRelayFee(500)
581+
presignedHelper.SetOutpointOnline(op1, false)
582+
583+
// Check fee rate of the presigned tx broadcasted.
584+
require.NoError(t, lnd.NotifyHeight(602))
585+
tx = <-lnd.TxPublishChannel
586+
gotFeeRate = presignedHelper.getTxFeerate(tx, inputAmt)
587+
require.Equal(t, chainfee.SatPerKWeight(523), gotFeeRate)
588+
// LockTime of a presigned tx is 0.
589+
require.Equal(t, uint32(0), tx.LockTime)
590+
591+
// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
592+
// It should re-broadcast the same presigned tx.
593+
lnd.SetMinRelayFee(450)
594+
require.NoError(t, lnd.NotifyHeight(603))
595+
tx2 = <-lnd.TxPublishChannel
596+
require.Equal(t, tx.TxHash(), tx2.TxHash())
597+
gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
598+
require.Equal(t, chainfee.SatPerKWeight(523), gotFeeRate)
599+
// LockTime of a presigned tx is 0.
600+
require.Equal(t, uint32(0), tx2.LockTime)
601+
602+
// Even if the client is back online, fee rate doesn't decrease.
603+
presignedHelper.SetOutpointOnline(op1, true)
604+
require.NoError(t, lnd.NotifyHeight(604))
605+
tx3 := <-lnd.TxPublishChannel
606+
require.Equal(t, tx2.TxOut[0].Value, tx3.TxOut[0].Value)
607+
gotFeeRate = presignedHelper.getTxFeerate(tx3, inputAmt)
608+
require.Equal(t, chainfee.SatPerKWeight(523), gotFeeRate)
609+
require.Equal(t, uint32(604), tx3.LockTime)
610+
}
611+
495612
// testPresigned_two_inputs_one_goes_offline tests presigned mode for the
496613
// following scenario: two online inputs are added, then one of them goes
497614
// offline, then feerate grows and a presigned transaction is used.
@@ -1692,6 +1809,10 @@ func TestPresigned(t *testing.T) {
16921809
testPresigned_input1_offline_then_input2(t, NewStoreMock())
16931810
})
16941811

1812+
t.Run("min_relay_fee", func(t *testing.T) {
1813+
testPresigned_min_relay_fee(t, NewStoreMock())
1814+
})
1815+
16951816
t.Run("two_inputs_one_goes_offline", func(t *testing.T) {
16961817
testPresigned_two_inputs_one_goes_offline(t, NewStoreMock())
16971818
})

0 commit comments

Comments
 (0)