lnd/sweep/fee_bumper_test.go
yyforyongyu 0f6fbc9a3b
sweep: refactor handleInitialTxError and createAndCheckTx
This commit refactors `handleInitialTxError` and `createAndCheckTx` to
take a `monitorRecord` param, which prepares for the following commit
where we start handling missing inputs.
2025-02-13 23:14:56 +08:00

2110 lines
56 KiB
Go

package sweep
import (
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/chain"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var (
// Create a taproot change script.
changePkScript = lnwallet.AddrWithKey{
DeliveryAddress: []byte{
0x51, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
}
testInputCount atomic.Uint64
)
func createTestInput(value int64,
witnessType input.WitnessType) input.BaseInput {
hash := chainhash.Hash{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
byte(testInputCount.Add(1))}
input := input.MakeBaseInput(
&wire.OutPoint{
Hash: hash,
},
witnessType,
&input.SignDescriptor{
Output: &wire.TxOut{
Value: value,
},
KeyDesc: keychain.KeyDescriptor{
PubKey: testPubKey,
},
},
1,
nil,
)
return input
}
// TestBumpResultValidate tests the validate method of the BumpResult struct.
func TestBumpResultValidate(t *testing.T) {
t.Parallel()
// An empty result will give an error.
b := BumpResult{}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)
// Unknown event type will give an error.
b = BumpResult{
Tx: &wire.MsgTx{},
Event: sentinalEvent,
}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)
// A replacing event without a new tx will give an error.
b = BumpResult{
Tx: &wire.MsgTx{},
Event: TxReplaced,
}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)
// A failed event without a failure reason will give an error.
b = BumpResult{
Tx: &wire.MsgTx{},
Event: TxFailed,
}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)
// A fatal event without a failure reason will give an error.
b = BumpResult{
Event: TxFailed,
}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)
// A confirmed event without fee info will give an error.
b = BumpResult{
Tx: &wire.MsgTx{},
Event: TxConfirmed,
}
require.ErrorIs(t, b.Validate(), ErrInvalidBumpResult)
// Test a valid result.
b = BumpResult{
Tx: &wire.MsgTx{},
Event: TxPublished,
}
require.NoError(t, b.Validate())
// Tx is allowed to be nil in a TxFailed event.
b = BumpResult{
Event: TxFailed,
Err: errDummy,
}
require.NoError(t, b.Validate())
// Tx is allowed to be nil in a TxFatal event.
b = BumpResult{
Event: TxFatal,
Err: errDummy,
}
require.NoError(t, b.Validate())
}
// TestCalcSweepTxWeight checks that the weight of the sweep tx is calculated
// correctly.
func TestCalcSweepTxWeight(t *testing.T) {
t.Parallel()
// Create an input.
inp := createTestInput(100, input.WitnessKeyHash)
// Use a wrong change script to test the error case.
weight, err := calcSweepTxWeight(
[]input.Input{&inp}, [][]byte{{0x00}},
)
require.Error(t, err)
require.Zero(t, weight)
// Use a correct change script to test the success case.
weight, err = calcSweepTxWeight(
[]input.Input{&inp}, [][]byte{changePkScript.DeliveryAddress},
)
require.NoError(t, err)
// BaseTxSize 8 bytes
// InputSize 1+41 bytes
// One P2TROutputSize 1+43 bytes
// One P2WKHWitnessSize 2+109 bytes
// Total weight = (8+42+44) * 4 + 111 = 487
require.EqualValuesf(t, 487, weight, "unexpected weight %v", weight)
}
// TestBumpRequestMaxFeeRateAllowed tests the max fee rate allowed for a bump
// request.
func TestBumpRequestMaxFeeRateAllowed(t *testing.T) {
t.Parallel()
// Create a test input.
inp := createTestInput(100, input.WitnessKeyHash)
// The weight is 487.
weight, err := calcSweepTxWeight(
[]input.Input{&inp}, [][]byte{changePkScript.DeliveryAddress},
)
require.NoError(t, err)
// Define a test budget and calculates its fee rate.
budget := btcutil.Amount(1000)
budgetFeeRate := chainfee.NewSatPerKWeight(budget, weight)
testCases := []struct {
name string
req *BumpRequest
expectedMaxFeeRate chainfee.SatPerKWeight
expectedErr bool
}{
{
// Use a wrong change script to test the error case.
name: "error calc weight",
req: &BumpRequest{
DeliveryAddress: lnwallet.AddrWithKey{
DeliveryAddress: []byte{1},
},
},
expectedMaxFeeRate: 0,
expectedErr: true,
},
{
// When the budget cannot give a fee rate that matches
// the supplied MaxFeeRate, the max allowed feerate is
// capped by the budget.
name: "use budget as max fee rate",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: budget,
MaxFeeRate: budgetFeeRate + 1,
},
expectedMaxFeeRate: budgetFeeRate,
},
{
// When the budget can give a fee rate that matches the
// supplied MaxFeeRate, the max allowed feerate is
// capped by the MaxFeeRate.
name: "use config as max fee rate",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: budget,
MaxFeeRate: budgetFeeRate - 1,
},
expectedMaxFeeRate: budgetFeeRate - 1,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Check the method under test.
maxFeeRate, err := tc.req.MaxFeeRateAllowed()
// If we expect an error, check the error is returned
// and the feerate is empty.
if tc.expectedErr {
require.Error(t, err)
require.Zero(t, maxFeeRate)
return
}
// Otherwise, check the max fee rate is as expected.
require.NoError(t, err)
require.Equal(t, tc.expectedMaxFeeRate, maxFeeRate)
})
}
}
// TestCalcCurrentConfTarget checks that the current confirmation target is
// calculated correctly.
func TestCalcCurrentConfTarget(t *testing.T) {
t.Parallel()
// When the current block height is 100 and deadline height is 200, the
// conf target should be 100.
conf := calcCurrentConfTarget(int32(100), int32(200))
require.EqualValues(t, 100, conf)
// When the current block height is 200 and deadline height is 100, the
// conf target should be 0 since the deadline has passed.
conf = calcCurrentConfTarget(int32(200), int32(100))
require.EqualValues(t, 0, conf)
}
// TestInitializeFeeFunction tests the initialization of the fee function.
func TestInitializeFeeFunction(t *testing.T) {
t.Parallel()
// Create a test input.
inp := createTestInput(100, input.WitnessKeyHash)
// Create a mock fee estimator.
estimator := &chainfee.MockEstimator{}
defer estimator.AssertExpectations(t)
// Create a publisher using the mocks.
tp := NewTxPublisher(TxPublisherConfig{
Estimator: estimator,
AuxSweeper: fn.Some[AuxSweeper](&MockAuxSweeper{}),
})
// Create a test feerate.
feerate := chainfee.SatPerKWeight(1000)
// Create a testing bump request.
req := &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
MaxFeeRate: feerate * 10,
DeadlineHeight: 10,
}
// Mock the fee estimator to return an error.
//
// We are not testing `NewLinearFeeFunction` here, so the actual params
// used are irrelevant.
dummyErr := fmt.Errorf("dummy error")
estimator.On("EstimateFeePerKW", mock.Anything).Return(
chainfee.SatPerKWeight(0), dummyErr).Once()
// Call the method under test and assert the error is returned.
f, err := tp.initializeFeeFunction(req)
require.ErrorIs(t, err, dummyErr)
require.Nil(t, f)
// Mock the fee estimator to return the testing fee rate.
//
// We are not testing `NewLinearFeeFunction` here, so the actual params
// used are irrelevant.
estimator.On("EstimateFeePerKW", mock.Anything).Return(
feerate, nil).Once()
estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Once()
// Call the method under test.
f, err = tp.initializeFeeFunction(req)
require.NoError(t, err)
require.Equal(t, feerate, f.FeeRate())
}
// TestUpdateRecord correctly updates the fields fee and tx, and saves the
// record.
func TestUpdateRecord(t *testing.T) {
t.Parallel()
// Create a test input.
inp := createTestInput(1000, input.WitnessKeyHash)
// Create a bump request.
req := &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
}
// Create a naive fee function.
feeFunc := &LinearFeeFunction{}
// Create a test fee and tx.
fee := btcutil.Amount(1000)
tx := &wire.MsgTx{}
// Create a publisher using the mocks.
tp := NewTxPublisher(TxPublisherConfig{
AuxSweeper: fn.Some[AuxSweeper](&MockAuxSweeper{}),
})
// Get the current counter and check it's increased later.
initialCounter := tp.requestCounter.Load()
op := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 0,
}
utxoIndex := map[wire.OutPoint]int{
op: 0,
}
// Create a sweepTxCtx.
sweepCtx := &sweepTxCtx{
tx: tx,
fee: fee,
outpointToTxIndex: utxoIndex,
}
// Create a test record.
record := &monitorRecord{
requestID: initialCounter,
req: req,
feeFunction: feeFunc,
}
// Call the method under test.
tp.updateRecord(record, sweepCtx)
// Read the saved record and compare.
record, ok := tp.records.Load(initialCounter)
require.True(t, ok)
require.Equal(t, tx, record.tx)
require.Equal(t, feeFunc, record.feeFunction)
require.Equal(t, fee, record.fee)
require.Equal(t, req, record.req)
require.Equal(t, utxoIndex, record.outpointToTxIndex)
}
// mockers wraps a list of mocked interfaces used inside tx publisher.
type mockers struct {
signer *input.MockInputSigner
wallet *MockWallet
estimator *chainfee.MockEstimator
notifier *chainntnfs.MockChainNotifier
feeFunc *MockFeeFunction
}
// createTestPublisher creates a new tx publisher using the provided mockers.
func createTestPublisher(t *testing.T) (*TxPublisher, *mockers) {
// Create a mock fee estimator.
estimator := &chainfee.MockEstimator{}
// Create a mock fee function.
feeFunc := &MockFeeFunction{}
// Create a mock signer.
signer := &input.MockInputSigner{}
// Create a mock wallet.
wallet := &MockWallet{}
// Create a mock chain notifier.
notifier := &chainntnfs.MockChainNotifier{}
t.Cleanup(func() {
estimator.AssertExpectations(t)
feeFunc.AssertExpectations(t)
signer.AssertExpectations(t)
wallet.AssertExpectations(t)
notifier.AssertExpectations(t)
})
m := &mockers{
signer: signer,
wallet: wallet,
estimator: estimator,
notifier: notifier,
feeFunc: feeFunc,
}
// Create a publisher using the mocks.
tp := NewTxPublisher(TxPublisherConfig{
Estimator: m.estimator,
Signer: m.signer,
Wallet: m.wallet,
Notifier: m.notifier,
AuxSweeper: fn.Some[AuxSweeper](&MockAuxSweeper{}),
})
return tp, m
}
// TestCreateAndCheckTx checks `createAndCheckTx` behaves as expected.
func TestCreateAndCheckTx(t *testing.T) {
t.Parallel()
// Create a test request.
inp := createTestInput(1000, input.WitnessKeyHash)
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Mock the wallet to fail on testmempoolaccept on the first call, and
// succeed on the second.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(errDummy).Once()
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once()
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
testCases := []struct {
name string
req *BumpRequest
expectedErr error
}{
{
// When the budget cannot cover the fee, an error
// should be returned.
name: "not enough budget",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
},
expectedErr: ErrNotEnoughBudget,
},
{
// When the mempool rejects the transaction, an error
// should be returned.
name: "testmempoolaccept fail",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
},
expectedErr: errDummy,
},
{
// When the mempool accepts the transaction, no error
// should be returned.
name: "testmempoolaccept pass",
req: &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
},
expectedErr: nil,
},
}
for _, tc := range testCases {
tc := tc
r := &monitorRecord{
req: tc.req,
feeFunction: m.feeFunc,
}
t.Run(tc.name, func(t *testing.T) {
// Call the method under test.
_, err := tp.createAndCheckTx(r)
// Check the result is as expected.
require.ErrorIs(t, err, tc.expectedErr)
})
}
}
// createTestBumpRequest creates a new bump request.
func createTestBumpRequest() *BumpRequest {
// Create a test input.
inp := createTestInput(1000, input.WitnessKeyHash)
return &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
}
}
// TestCreateRBFCompliantTx checks that `createRBFCompliantTx` behaves as
// expected.
func TestCreateRBFCompliantTx(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test bump request.
req := createTestBumpRequest()
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
testCases := []struct {
name string
setupMock func()
expectedErr error
}{
{
// When testmempoolaccept accepts the tx, no error
// should be returned.
name: "success case",
setupMock: func() {
// Mock the testmempoolaccept to pass.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(nil).Once()
},
expectedErr: nil,
},
{
// When testmempoolaccept fails due to a non-fee
// related error, an error should be returned.
name: "non-fee related testmempoolaccept fail",
setupMock: func() {
// Mock the testmempoolaccept to fail.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(errDummy).Once()
},
expectedErr: errDummy,
},
{
// When increase feerate gives an error, the error
// should be returned.
name: "fail on increase fee",
setupMock: func() {
// Mock the testmempoolaccept to fail on fee.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(
lnwallet.ErrMempoolFee).Once()
// Mock the fee function to return an error.
m.feeFunc.On("Increment").Return(
false, errDummy).Once()
},
expectedErr: errDummy,
},
{
// Test that after one round of increasing the feerate
// the tx passes testmempoolaccept.
name: "increase fee and success on min mempool fee",
setupMock: func() {
// Mock the testmempoolaccept to fail on fee
// for the first call.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(
lnwallet.ErrMempoolFee).Once()
// Mock the fee function to increase feerate.
m.feeFunc.On("Increment").Return(
true, nil).Once()
// Mock the testmempoolaccept to pass on the
// second call.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(nil).Once()
},
expectedErr: nil,
},
{
// Test that after one round of increasing the feerate
// the tx passes testmempoolaccept.
name: "increase fee and success on insufficient fee",
setupMock: func() {
// Mock the testmempoolaccept to fail on fee
// for the first call.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(
chain.ErrInsufficientFee).Once()
// Mock the fee function to increase feerate.
m.feeFunc.On("Increment").Return(
true, nil).Once()
// Mock the testmempoolaccept to pass on the
// second call.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(nil).Once()
},
expectedErr: nil,
},
{
// Test that the fee function increases the fee rate
// after one round.
name: "increase fee on second round",
setupMock: func() {
// Mock the testmempoolaccept to fail on fee
// for the first call.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(
chain.ErrInsufficientFee).Once()
// Mock the fee function to NOT increase
// feerate on the first round.
m.feeFunc.On("Increment").Return(
false, nil).Once()
// Mock the fee function to increase feerate.
m.feeFunc.On("Increment").Return(
true, nil).Once()
// Mock the testmempoolaccept to pass on the
// second call.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(nil).Once()
},
expectedErr: nil,
},
}
var requestCounter atomic.Uint64
for _, tc := range testCases {
tc := tc
rid := requestCounter.Add(1)
// Create a test record.
record := &monitorRecord{
requestID: rid,
req: req,
feeFunction: m.feeFunc,
}
t.Run(tc.name, func(t *testing.T) {
tc.setupMock()
// Call the method under test.
rec, err := tp.createRBFCompliantTx(record)
// Check the result is as expected.
require.ErrorIs(t, err, tc.expectedErr)
if tc.expectedErr != nil {
return
}
// Assert the returned record has the following fields
// populated.
require.NotEmpty(t, rec.tx)
require.NotEmpty(t, rec.fee)
})
}
}
// TestTxPublisherBroadcast checks the internal `broadcast` method behaves as
// expected.
func TestTxPublisherBroadcast(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test bump request.
req := createTestBumpRequest()
// Create a test tx.
tx := &wire.MsgTx{LockTime: 1}
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
op := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 0,
}
utxoIndex := map[wire.OutPoint]int{
op: 0,
}
// Create a testing record and put it in the map.
fee := btcutil.Amount(1000)
requestID := uint64(1)
// Create a sweepTxCtx.
sweepCtx := &sweepTxCtx{
tx: tx,
fee: fee,
outpointToTxIndex: utxoIndex,
}
// Create a test record.
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
}
rec := tp.updateRecord(record, sweepCtx)
testCases := []struct {
name string
setupMock func()
expectedErr error
expectedResult *BumpResult
}{
{
// When the wallet cannot publish this tx, the error
// should be put inside the result.
name: "fail to publish",
setupMock: func() {
// Mock the wallet to fail to publish.
m.wallet.On("PublishTransaction",
tx, mock.Anything).Return(
errDummy).Once()
},
expectedErr: nil,
expectedResult: &BumpResult{
Event: TxFailed,
Tx: tx,
Fee: fee,
FeeRate: feerate,
Err: errDummy,
requestID: requestID,
},
},
{
// When nothing goes wrong, the result is returned.
name: "publish success",
setupMock: func() {
// Mock the wallet to publish successfully.
m.wallet.On("PublishTransaction",
tx, mock.Anything).Return(nil).Once()
},
expectedErr: nil,
expectedResult: &BumpResult{
Event: TxPublished,
Tx: tx,
Fee: fee,
FeeRate: feerate,
Err: nil,
requestID: requestID,
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tc.setupMock()
// Call the method under test.
result, err := tp.broadcast(rec)
// Check the result is as expected.
require.ErrorIs(t, err, tc.expectedErr)
require.Equal(t, tc.expectedResult, result)
})
}
}
// TestRemoveResult checks the records and subscriptions are removed when a tx
// is confirmed or failed.
func TestRemoveResult(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test bump request.
req := createTestBumpRequest()
// Create a test tx.
tx := &wire.MsgTx{LockTime: 1}
// Create a testing record and put it in the map.
fee := btcutil.Amount(1000)
op := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 0,
}
utxoIndex := map[wire.OutPoint]int{
op: 0,
}
// Create a test request ID counter.
requestCounter := atomic.Uint64{}
// Create a sweepTxCtx.
sweepCtx := &sweepTxCtx{
tx: tx,
fee: fee,
outpointToTxIndex: utxoIndex,
}
testCases := []struct {
name string
setupRecord func() uint64
result *BumpResult
removed bool
}{
{
// When the tx is confirmed, the records will be
// removed.
name: "remove on TxConfirmed",
setupRecord: func() uint64 {
rid := requestCounter.Add(1)
// Create a test record.
record := &monitorRecord{
requestID: rid,
req: req,
feeFunction: m.feeFunc,
}
tp.updateRecord(record, sweepCtx)
tp.subscriberChans.Store(rid, nil)
return rid
},
result: &BumpResult{
Event: TxConfirmed,
Tx: tx,
},
removed: true,
},
{
// When the tx is failed, the records will be removed.
name: "remove on TxFailed",
setupRecord: func() uint64 {
rid := requestCounter.Add(1)
// Create a test record.
record := &monitorRecord{
requestID: rid,
req: req,
feeFunction: m.feeFunc,
}
tp.updateRecord(record, sweepCtx)
tp.subscriberChans.Store(rid, nil)
return rid
},
result: &BumpResult{
Event: TxFailed,
Err: errDummy,
Tx: tx,
},
removed: true,
},
{
// Noop when the tx is neither confirmed or failed.
name: "noop when tx is not confirmed or failed",
setupRecord: func() uint64 {
rid := requestCounter.Add(1)
// Create a test record.
record := &monitorRecord{
requestID: rid,
req: req,
feeFunction: m.feeFunc,
}
tp.updateRecord(record, sweepCtx)
tp.subscriberChans.Store(rid, nil)
return rid
},
result: &BumpResult{
Event: TxPublished,
Tx: tx,
},
removed: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
requestID := tc.setupRecord()
// Attach the requestID from the setup.
tc.result.requestID = requestID
// Remove the result.
tp.removeResult(tc.result)
// Check if the record is removed.
_, found := tp.records.Load(requestID)
require.Equal(t, !tc.removed, found)
_, found = tp.subscriberChans.Load(requestID)
require.Equal(t, !tc.removed, found)
})
}
}
// TestNotifyResult checks the subscribers are notified when a result is sent.
func TestNotifyResult(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test bump request.
req := createTestBumpRequest()
// Create a test tx.
tx := &wire.MsgTx{LockTime: 1}
op := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 0,
}
utxoIndex := map[wire.OutPoint]int{
op: 0,
}
// Create a testing record and put it in the map.
fee := btcutil.Amount(1000)
requestID := uint64(1)
// Create a sweepTxCtx.
sweepCtx := &sweepTxCtx{
tx: tx,
fee: fee,
outpointToTxIndex: utxoIndex,
}
// Create a test record.
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
}
tp.updateRecord(record, sweepCtx)
// Create a subscription to the event.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
// Create a test result.
result := &BumpResult{
requestID: requestID,
Tx: tx,
}
// Notify the result and expect the subscriber to receive it.
//
// NOTE: must be done inside a goroutine in case it blocks.
go tp.notifyResult(result)
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber to receive result")
case received := <-subscriber:
require.Equal(t, result, received)
}
// Notify two results. This time it should block because the channel is
// full. We then shutdown TxPublisher to test the quit behavior.
done := make(chan struct{})
go func() {
// Call notifyResult twice, which blocks at the second call.
tp.notifyResult(result)
tp.notifyResult(result)
close(done)
}()
// Shutdown the publisher and expect notifyResult to exit.
close(tp.quit)
// We expect to done chan.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for notifyResult to exit")
case <-done:
}
}
// TestBroadcast checks the public `Broadcast` method can successfully register
// a broadcast request.
func TestBroadcast(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, _ := createTestPublisher(t)
// Create a test feerate.
feerate := chainfee.SatPerKWeight(1000)
// Create a test request.
inp := createTestInput(1000, input.WitnessKeyHash)
// Create a testing bump request.
req := &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
MaxFeeRate: feerate * 10,
DeadlineHeight: 10,
}
// Send the req and expect no error.
resultChan := tp.Broadcast(req)
require.NotNil(t, resultChan)
// Validate the record was stored.
require.Equal(t, 1, tp.records.Len())
require.Equal(t, 1, tp.subscriberChans.Len())
// Validate the record.
rid := tp.requestCounter.Load()
record, found := tp.records.Load(rid)
require.True(t, found)
require.Equal(t, req, record.req)
}
// TestBroadcastImmediate checks the public `Broadcast` method can successfully
// register a broadcast request and publish the tx when `Immediate` flag is
// set.
func TestBroadcastImmediate(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test feerate.
feerate := chainfee.SatPerKWeight(1000)
// Create a test request.
inp := createTestInput(1000, input.WitnessKeyHash)
// Create a testing bump request.
req := &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
MaxFeeRate: feerate * 10,
DeadlineHeight: 10,
Immediate: true,
}
// Mock the fee estimator to return an error.
//
// NOTE: We are not testing `handleInitialBroadcast` here, but only
// interested in checking that this method is indeed called when
// `Immediate` is true. Thus we mock the method to return an error to
// quickly abort. As long as this mocked method is called, we know the
// `Immediate` flag works.
m.estimator.On("EstimateFeePerKW", mock.Anything).Return(
chainfee.SatPerKWeight(0), errDummy).Once()
// Send the req and expect no error.
resultChan := tp.Broadcast(req)
require.NotNil(t, resultChan)
// Validate the record was removed due to an error returned in initial
// broadcast.
require.Empty(t, tp.records.Len())
require.Empty(t, tp.subscriberChans.Len())
}
// TestCreateAnPublishFail checks all the error cases are handled properly in
// the method createAndPublish.
func TestCreateAnPublishFail(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test requestID.
requestID := uint64(1)
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Create a testing monitor record.
req := createTestBumpRequest()
// Overwrite the budget to make it smaller than the fee.
req.Budget = 100
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
tx: &wire.MsgTx{},
}
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
// Call the createAndPublish method.
resultOpt := tp.createAndPublishTx(record)
result := resultOpt.UnwrapOrFail(t)
// We expect the result to be TxFailed and the error is set in the
// result.
require.Equal(t, TxFailed, result.Event)
require.ErrorIs(t, result.Err, ErrNotEnoughBudget)
require.Equal(t, requestID, result.requestID)
// Increase the budget and call it again. This time we will mock an
// error to be returned from CheckMempoolAcceptance.
req.Budget = 1000
// Mock the testmempoolaccept to return a fee related error that should
// be ignored.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(lnwallet.ErrMempoolFee).Once()
// Call the createAndPublish method and expect a none option.
resultOpt = tp.createAndPublishTx(record)
require.True(t, resultOpt.IsNone())
// Mock the testmempoolaccept to return a fee related error that should
// be ignored.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(chain.ErrInsufficientFee).Once()
// Call the createAndPublish method and expect a none option.
resultOpt = tp.createAndPublishTx(record)
require.True(t, resultOpt.IsNone())
}
// TestCreateAnPublishSuccess checks the expected result is returned from the
// method createAndPublish.
func TestCreateAnPublishSuccess(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test requestID.
requestID := uint64(1)
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Create a testing monitor record.
req := createTestBumpRequest()
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
tx: &wire.MsgTx{},
}
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
// Mock the testmempoolaccept to return nil.
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil)
// Mock the wallet to publish and return an error.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(errDummy).Once()
// Call the createAndPublish method and expect a failure result.
resultOpt := tp.createAndPublishTx(record)
result := resultOpt.UnwrapOrFail(t)
// We expect the result to be TxFailed and the error is set.
require.Equal(t, TxFailed, result.Event)
require.ErrorIs(t, result.Err, errDummy)
// Although the replacement tx was failed to be published, the record
// should be stored.
require.NotNil(t, result.Tx)
require.NotNil(t, result.ReplacedTx)
_, found := tp.records.Load(requestID)
require.True(t, found)
// We now check a successful RBF.
//
// Mock the wallet to publish successfully.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(nil).Once()
// Call the createAndPublish method and expect a success result.
resultOpt = tp.createAndPublishTx(record)
result = resultOpt.UnwrapOrFail(t)
require.True(t, resultOpt.IsSome())
// We expect the result to be TxReplaced and the error is nil.
require.Equal(t, TxReplaced, result.Event)
require.Nil(t, result.Err)
// Check the Tx and ReplacedTx are set.
require.NotNil(t, result.Tx)
require.NotNil(t, result.ReplacedTx)
// Check the record is stored.
_, found = tp.records.Load(requestID)
require.True(t, found)
}
// TestHandleTxConfirmed checks the expected result is returned from the method
// handleTxConfirmed.
func TestHandleTxConfirmed(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test bump request.
req := createTestBumpRequest()
// Create a test tx.
tx := &wire.MsgTx{LockTime: 1}
op := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 0,
}
utxoIndex := map[wire.OutPoint]int{
op: 0,
}
// Create a testing record and put it in the map.
fee := btcutil.Amount(1000)
requestID := uint64(1)
// Create a sweepTxCtx.
sweepCtx := &sweepTxCtx{
tx: tx,
fee: fee,
outpointToTxIndex: utxoIndex,
}
// Create a test record.
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
}
tp.updateRecord(record, sweepCtx)
record, ok := tp.records.Load(requestID)
require.True(t, ok)
// Create a subscription to the event.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
// Mock the fee function to return a fee rate.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate).Once()
// Call the method and expect a result to be received.
//
// NOTE: must be called in a goroutine in case it blocks.
tp.wg.Add(1)
done := make(chan struct{})
go func() {
tp.handleTxConfirmed(record)
close(done)
}()
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber to receive result")
case result := <-subscriber:
// We expect the result to be TxConfirmed and the tx is set.
require.Equal(t, TxConfirmed, result.Event)
require.Equal(t, tx, result.Tx)
require.Nil(t, result.Err)
require.Equal(t, requestID, result.requestID)
require.Equal(t, record.fee, result.Fee)
require.Equal(t, feerate, result.FeeRate)
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("timeout waiting for handleTxConfirmed to return")
}
// We expect the record to be removed from the maps.
_, found := tp.records.Load(requestID)
require.False(t, found)
_, found = tp.subscriberChans.Load(requestID)
require.False(t, found)
}
// TestHandleFeeBumpTx validates handleFeeBumpTx behaves as expected.
func TestHandleFeeBumpTx(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test tx.
tx := &wire.MsgTx{LockTime: 1}
// Create a test current height.
testHeight := int32(800000)
// Create a testing monitor record.
req := createTestBumpRequest()
// Create a testing record and put it in the map.
requestID := uint64(1)
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
tx: tx,
}
op := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 0,
}
utxoIndex := map[wire.OutPoint]int{
op: 0,
}
fee := btcutil.Amount(1000)
// Create a sweepTxCtx.
sweepCtx := &sweepTxCtx{
tx: tx,
fee: fee,
outpointToTxIndex: utxoIndex,
}
tp.updateRecord(record, sweepCtx)
// Create a subscription to the event.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Mock the fee function to skip the bump due to error.
m.feeFunc.On("IncreaseFeeRate", mock.Anything).Return(
false, errDummy).Once()
// Call the method and expect no result received.
tp.wg.Add(1)
go tp.handleFeeBumpTx(record, testHeight)
// Check there's no result sent back.
select {
case <-time.After(time.Second):
case result := <-subscriber:
t.Fatalf("unexpected result received: %v", result)
}
// Mock the fee function to skip the bump.
m.feeFunc.On("IncreaseFeeRate", mock.Anything).Return(false, nil).Once()
// Call the method and expect no result received.
tp.wg.Add(1)
go tp.handleFeeBumpTx(record, testHeight)
// Check there's no result sent back.
select {
case <-time.After(time.Second):
case result := <-subscriber:
t.Fatalf("unexpected result received: %v", result)
}
// Mock the fee function to perform the fee bump.
m.feeFunc.On("IncreaseFeeRate", mock.Anything).Return(true, nil)
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
// Mock the testmempoolaccept to return nil.
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil)
// Mock the wallet to publish successfully.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(nil).Once()
// Call the method and expect a result to be received.
//
// NOTE: must be called in a goroutine in case it blocks.
tp.wg.Add(1)
go tp.handleFeeBumpTx(record, testHeight)
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber to receive result")
case result := <-subscriber:
// We expect the result to be TxReplaced.
require.Equal(t, TxReplaced, result.Event)
// The new tx and old tx should be properly set.
require.NotEqual(t, tx, result.Tx)
require.Equal(t, tx, result.ReplacedTx)
// No error should be set.
require.Nil(t, result.Err)
require.Equal(t, requestID, result.requestID)
}
// We expect the record to NOT be removed from the maps.
_, found := tp.records.Load(requestID)
require.True(t, found)
_, found = tp.subscriberChans.Load(requestID)
require.True(t, found)
}
// TestProcessRecordsInitial validates processRecords behaves as expected when
// processing the initial broadcast.
func TestProcessRecordsInitial(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create testing objects.
requestID := uint64(1)
req := createTestBumpRequest()
op := req.Inputs[0].OutPoint()
// Mock RegisterSpendNtfn.
//
// Create the spending event that doesn't send an event.
se := &chainntnfs.SpendEvent{
Cancel: func() {},
}
m.notifier.On("RegisterSpendNtfn",
&op, mock.Anything, mock.Anything).Return(se, nil).Once()
// Create a monitor record that's broadcast the first time.
record := &monitorRecord{
requestID: requestID,
req: req,
}
// Setup the initial publisher state by adding the records to the maps.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
tp.records.Store(requestID, record)
// The following methods should only be called once when creating the
// initial broadcast tx.
//
// Mock the signer to always return a valid script.
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(&input.Script{}, nil).Once()
// Mock the testmempoolaccept to return nil.
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once()
// Mock the wallet to publish successfully.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(nil).Once()
// Call processRecords and expect the results are notified back.
tp.processRecords()
// We expect the published tx to be notified back.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber")
case result := <-subscriber:
// We expect the result to be TxPublished.
require.Equal(t, TxPublished, result.Event)
// Expect the tx to be set but not the replaced tx.
require.NotNil(t, result.Tx)
require.Nil(t, result.ReplacedTx)
// No error should be set.
require.Nil(t, result.Err)
require.Equal(t, requestID, result.requestID)
}
}
// TestProcessRecordsInitialSpent validates processRecords behaves as expected
// when processing the initial broadcast when the input is spent.
func TestProcessRecordsInitialSpent(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create testing objects.
requestID := uint64(1)
req := createTestBumpRequest()
tx := &wire.MsgTx{LockTime: 1}
op := req.Inputs[0].OutPoint()
// Mock RegisterSpendNtfn.
se := createTestSpendEvent(tx)
m.notifier.On("RegisterSpendNtfn",
&op, mock.Anything, mock.Anything).Return(se, nil).Once()
// Create a monitor record that's broadcast the first time.
record := &monitorRecord{
requestID: requestID,
req: req,
}
// Setup the initial publisher state by adding the records to the maps.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
tp.records.Store(requestID, record)
// Call processRecords and expect the results are notified back.
tp.processRecords()
// We expect the published tx to be notified back.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber")
case result := <-subscriber:
// We expect the result to be TxUnknownSpend.
require.Equal(t, TxUnknownSpend, result.Event)
// Expect the tx and the replaced tx to be nil.
require.Nil(t, result.Tx)
require.Nil(t, result.ReplacedTx)
// The error should be set.
require.ErrorIs(t, result.Err, ErrUnknownSpent)
require.Equal(t, requestID, result.requestID)
}
}
// TestProcessRecordsFeeBump validates processRecords behaves as expected when
// processing fee bump records.
func TestProcessRecordsFeeBump(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create testing objects.
requestID := uint64(1)
req := createTestBumpRequest()
tx := &wire.MsgTx{LockTime: 1}
op := req.Inputs[0].OutPoint()
// Mock RegisterSpendNtfn.
//
// Create the spending event that doesn't send an event.
se := &chainntnfs.SpendEvent{
Cancel: func() {},
}
m.notifier.On("RegisterSpendNtfn",
&op, mock.Anything, mock.Anything).Return(se, nil).Once()
// Create a monitor record that's not confirmed. We know it's not
// confirmed because the `SpendEvent` is empty.
record := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
tx: tx,
}
// Setup the initial publisher state by adding the records to the maps.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
tp.records.Store(requestID, record)
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// The following methods should only be called once when creating the
// replacement tx.
//
// Mock the fee function to NOT skip the fee bump.
m.feeFunc.On("IncreaseFeeRate", mock.Anything).Return(true, nil).Once()
// Mock the signer to always return a valid script.
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(&input.Script{}, nil).Once()
// Mock the testmempoolaccept to return nil.
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once()
// Mock the wallet to publish successfully.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(nil).Once()
// Call processRecords and expect the results are notified back.
tp.processRecords()
// We expect the replaced tx to be notified back.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriberReplaced")
case result := <-subscriber:
// We expect the result to be TxReplaced.
require.Equal(t, TxReplaced, result.Event)
// The new tx and old tx should be properly set.
require.NotEqual(t, tx, result.Tx)
require.Equal(t, tx, result.ReplacedTx)
// No error should be set.
require.Nil(t, result.Err)
require.Equal(t, requestID, result.requestID)
}
}
// TestProcessRecordsConfirmed validates processRecords behaves as expected when
// processing confirmed records.
func TestProcessRecordsConfirmed(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create testing objects.
requestID := uint64(1)
req := createTestBumpRequest()
tx := &wire.MsgTx{LockTime: 1}
op := req.Inputs[0].OutPoint()
// Mock RegisterSpendNtfn.
se := createTestSpendEvent(tx)
m.notifier.On("RegisterSpendNtfn",
&op, mock.Anything, mock.Anything).Return(se, nil).Once()
// Create a monitor record that's confirmed.
recordConfirmed := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
tx: tx,
}
// Setup the initial publisher state by adding the records to the maps.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
tp.records.Store(requestID, recordConfirmed)
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Call processRecords and expect the results are notified back.
tp.processRecords()
// Check the confirmed tx result.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber")
case result := <-subscriber:
// We expect the result to be TxConfirmed.
require.Equal(t, TxConfirmed, result.Event)
require.Equal(t, tx, result.Tx)
// No error should be set.
require.Nil(t, result.Err)
require.Equal(t, requestID, result.requestID)
}
}
// TestProcessRecordsSpent validates processRecords behaves as expected when
// processing unknown spent records.
func TestProcessRecordsSpent(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create testing objects.
requestID := uint64(1)
req := createTestBumpRequest()
tx := &wire.MsgTx{LockTime: 1}
op := req.Inputs[0].OutPoint()
// Create a unknown tx.
txUnknown := &wire.MsgTx{LockTime: 2}
// Mock RegisterSpendNtfn.
se := createTestSpendEvent(txUnknown)
m.notifier.On("RegisterSpendNtfn",
&op, mock.Anything, mock.Anything).Return(se, nil).Once()
// Create a monitor record that's spent by txUnknown.
recordConfirmed := &monitorRecord{
requestID: requestID,
req: req,
feeFunction: m.feeFunc,
tx: tx,
}
// Setup the initial publisher state by adding the records to the maps.
subscriber := make(chan *BumpResult, 1)
tp.subscriberChans.Store(requestID, subscriber)
tp.records.Store(requestID, recordConfirmed)
// Mock the fee function to increase feerate.
m.feeFunc.On("Increment").Return(true, nil).Once()
// Create a test feerate and return it from the mock fee function.
feerate := chainfee.SatPerKWeight(1000)
m.feeFunc.On("FeeRate").Return(feerate)
// Call processRecords and expect the results are notified back.
tp.processRecords()
// Check the unknown tx result.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber")
case result := <-subscriber:
// We expect the result to be TxUnknownSpend.
require.Equal(t, TxUnknownSpend, result.Event)
require.Equal(t, tx, result.Tx)
// We expect the fee rate to be updated.
require.Equal(t, feerate, result.FeeRate)
// No error should be set.
require.ErrorIs(t, result.Err, ErrUnknownSpent)
require.Equal(t, requestID, result.requestID)
}
}
// TestHandleInitialBroadcastSuccess checks `handleInitialBroadcast` method can
// successfully broadcast a tx based on the request.
func TestHandleInitialBroadcastSuccess(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test feerate.
feerate := chainfee.SatPerKWeight(1000)
// Mock the fee estimator to return the testing fee rate.
//
// We are not testing `NewLinearFeeFunction` here, so the actual params
// used are irrelevant.
m.estimator.On("EstimateFeePerKW", mock.Anything).Return(
feerate, nil).Once()
m.estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Once()
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
// Mock the testmempoolaccept to pass.
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once()
// Mock the wallet to publish successfully.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(nil).Once()
// Create a test request.
inp := createTestInput(1000, input.WitnessKeyHash)
// Create a testing bump request.
req := &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
MaxFeeRate: feerate * 10,
DeadlineHeight: 10,
}
// Register the testing record use `Broadcast`.
resultChan := tp.Broadcast(req)
// Grab the monitor record from the map.
rid := tp.requestCounter.Load()
rec, ok := tp.records.Load(rid)
require.True(t, ok)
// Call the method under test.
tp.wg.Add(1)
tp.handleInitialBroadcast(rec)
// Check the result is sent back.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber to receive result")
case result := <-resultChan:
// We expect the first result to be TxPublished.
require.Equal(t, TxPublished, result.Event)
}
// Validate the record was stored.
require.Equal(t, 1, tp.records.Len())
require.Equal(t, 1, tp.subscriberChans.Len())
}
// TestHandleInitialBroadcastFail checks `handleInitialBroadcast` returns the
// error or a failed result when the broadcast fails.
func TestHandleInitialBroadcastFail(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create a test feerate.
feerate := chainfee.SatPerKWeight(1000)
// Create a test request.
inp := createTestInput(1000, input.WitnessKeyHash)
// Create a testing bump request.
req := &BumpRequest{
DeliveryAddress: changePkScript,
Inputs: []input.Input{&inp},
Budget: btcutil.Amount(1000),
MaxFeeRate: feerate * 10,
DeadlineHeight: 10,
}
// Mock the fee estimator to return the testing fee rate.
//
// We are not testing `NewLinearFeeFunction` here, so the actual params
// used are irrelevant.
m.estimator.On("EstimateFeePerKW", mock.Anything).Return(
feerate, nil).Twice()
m.estimator.On("RelayFeePerKW").Return(chainfee.FeePerKwFloor).Twice()
// Mock the signer to always return a valid script.
//
// NOTE: we are not testing the utility of creating valid txes here, so
// this is fine to be mocked. This behaves essentially as skipping the
// Signer check and alaways assume the tx has a valid sig.
script := &input.Script{}
m.signer.On("ComputeInputScript", mock.Anything,
mock.Anything).Return(script, nil)
// Mock the testmempoolaccept to return an error.
m.wallet.On("CheckMempoolAcceptance",
mock.Anything).Return(errDummy).Once()
// Register the testing record use `Broadcast`.
resultChan := tp.Broadcast(req)
// Grab the monitor record from the map.
rid := tp.requestCounter.Load()
rec, ok := tp.records.Load(rid)
require.True(t, ok)
// Call the method under test and expect an error returned.
tp.wg.Add(1)
tp.handleInitialBroadcast(rec)
// Check the result is sent back.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber to receive result")
case result := <-resultChan:
// We expect the first result to be TxFatal.
require.Equal(t, TxFatal, result.Event)
}
// Validate the record was NOT stored.
require.Equal(t, 0, tp.records.Len())
require.Equal(t, 0, tp.subscriberChans.Len())
// Mock the testmempoolaccept again, this time it passes.
m.wallet.On("CheckMempoolAcceptance", mock.Anything).Return(nil).Once()
// Mock the wallet to fail on publish.
m.wallet.On("PublishTransaction",
mock.Anything, mock.Anything).Return(errDummy).Once()
// Register the testing record use `Broadcast`.
resultChan = tp.Broadcast(req)
// Grab the monitor record from the map.
rid = tp.requestCounter.Load()
rec, ok = tp.records.Load(rid)
require.True(t, ok)
// Call the method under test.
tp.wg.Add(1)
tp.handleInitialBroadcast(rec)
// Check the result is sent back.
select {
case <-time.After(time.Second):
t.Fatal("timeout waiting for subscriber to receive result")
case result := <-resultChan:
// We expect the result to be TxFailed and the error is set in
// the result.
require.Equal(t, TxFailed, result.Event)
require.ErrorIs(t, result.Err, errDummy)
}
// Validate the record was removed.
require.Equal(t, 0, tp.records.Len())
require.Equal(t, 0, tp.subscriberChans.Len())
}
// TestHasInputsSpent checks the expected outpoint:tx map is returned.
func TestHasInputsSpent(t *testing.T) {
t.Parallel()
// Create a publisher using the mocks.
tp, m := createTestPublisher(t)
// Create mock inputs.
op1 := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 1,
}
inp1 := &input.MockInput{}
heightHint1 := uint32(1)
defer inp1.AssertExpectations(t)
op2 := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 2,
}
inp2 := &input.MockInput{}
heightHint2 := uint32(2)
defer inp2.AssertExpectations(t)
op3 := wire.OutPoint{
Hash: chainhash.Hash{1},
Index: 3,
}
walletInp := &input.MockInput{}
heightHint3 := uint32(0)
defer walletInp.AssertExpectations(t)
// We expect all the inputs to call OutPoint and HeightHint.
inp1.On("OutPoint").Return(op1).Once()
inp2.On("OutPoint").Return(op2).Once()
walletInp.On("OutPoint").Return(op3).Once()
inp1.On("HeightHint").Return(heightHint1).Once()
inp2.On("HeightHint").Return(heightHint2).Once()
walletInp.On("HeightHint").Return(heightHint3).Once()
// We expect the normal inputs to call SignDesc.
pkScript1 := []byte{1}
sd1 := &input.SignDescriptor{
Output: &wire.TxOut{
PkScript: pkScript1,
},
}
inp1.On("SignDesc").Return(sd1).Once()
pkScript2 := []byte{1}
sd2 := &input.SignDescriptor{
Output: &wire.TxOut{
PkScript: pkScript2,
},
}
inp2.On("SignDesc").Return(sd2).Once()
pkScript3 := []byte{3}
sd3 := &input.SignDescriptor{
Output: &wire.TxOut{
PkScript: pkScript3,
},
}
walletInp.On("SignDesc").Return(sd3).Once()
// Mock RegisterSpendNtfn.
//
// spendingTx1 is the tx spending op1.
spendingTx1 := &wire.MsgTx{}
se1 := createTestSpendEvent(spendingTx1)
m.notifier.On("RegisterSpendNtfn",
&op1, pkScript1, heightHint1).Return(se1, nil).Once()
// Create the spending event that doesn't send an event.
se2 := &chainntnfs.SpendEvent{
Cancel: func() {},
}
m.notifier.On("RegisterSpendNtfn",
&op2, pkScript2, heightHint2).Return(se2, nil).Once()
se3 := &chainntnfs.SpendEvent{
Cancel: func() {},
}
m.notifier.On("RegisterSpendNtfn",
&op3, pkScript3, heightHint3).Return(se3, nil).Once()
// Prepare the test inputs.
inputs := []input.Input{inp1, inp2, walletInp}
// Prepare the test record.
record := &monitorRecord{
req: &BumpRequest{
Inputs: inputs,
},
}
// Call the method under test.
result := tp.getSpentInputs(record)
// Assert the expected map is created.
expected := map[wire.OutPoint]*wire.MsgTx{
op1: spendingTx1,
}
require.Equal(t, expected, result)
}
// createTestSpendEvent creates a SpendEvent which places the specified tx in
// the channel, which can be read by a spending subscriber.
func createTestSpendEvent(tx *wire.MsgTx) *chainntnfs.SpendEvent {
// Create a monitor record that's confirmed.
spendDetails := chainntnfs.SpendDetail{
SpendingTx: tx,
}
spendChan1 := make(chan *chainntnfs.SpendDetail, 1)
spendChan1 <- &spendDetails
// Create the spend events.
return &chainntnfs.SpendEvent{
Spend: spendChan1,
Cancel: func() {},
}
}