1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063 |
- package sweep
- import (
- "errors"
- "testing"
- "time"
- "github.com/btcsuite/btcd/btcec/v2"
- "github.com/btcsuite/btcd/btcutil"
- "github.com/btcsuite/btcd/chaincfg/chainhash"
- "github.com/btcsuite/btcd/wire"
- "github.com/lightningnetwork/lnd/chainntnfs"
- "github.com/lightningnetwork/lnd/fn"
- "github.com/lightningnetwork/lnd/input"
- "github.com/lightningnetwork/lnd/lnwallet/chainfee"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
- )
- var (
- errDummy = errors.New("dummy error")
- testPubKey, _ = btcec.ParsePubKey([]byte{
- 0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a,
- 0x01, 0x6b, 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e,
- 0xb6, 0x8a, 0x38, 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca,
- 0xd7, 0xb1, 0x48, 0xa6, 0x90, 0x9a, 0x5c, 0xb2, 0xe0,
- 0xea, 0xdd, 0xfb, 0x84, 0xcc, 0xf9, 0x74, 0x44, 0x64,
- 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, 0x8b, 0x64, 0xf9,
- 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, 0xf6, 0x56,
- 0xb4, 0x12, 0xa3,
- })
- )
- // TestMarkInputsPendingPublish checks that given a list of inputs with
- // different states, only the non-terminal state will be marked as `Published`.
- func TestMarkInputsPendingPublish(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{})
- // Create a mock input set.
- set := &MockInputSet{}
- defer set.AssertExpectations(t)
- // Create three testing inputs.
- //
- // inputNotExist specifies an input that's not found in the sweeper's
- // `pendingInputs` map.
- inputNotExist := &input.MockInput{}
- defer inputNotExist.AssertExpectations(t)
- inputNotExist.On("OutPoint").Return(wire.OutPoint{Index: 0})
- // inputInit specifies a newly created input.
- inputInit := &input.MockInput{}
- defer inputInit.AssertExpectations(t)
- inputInit.On("OutPoint").Return(wire.OutPoint{Index: 1})
- s.inputs[inputInit.OutPoint()] = &SweeperInput{
- state: Init,
- }
- // inputPendingPublish specifies an input that's about to be published.
- inputPendingPublish := &input.MockInput{}
- defer inputPendingPublish.AssertExpectations(t)
- inputPendingPublish.On("OutPoint").Return(wire.OutPoint{Index: 2})
- s.inputs[inputPendingPublish.OutPoint()] = &SweeperInput{
- state: PendingPublish,
- }
- // inputTerminated specifies an input that's terminated.
- inputTerminated := &input.MockInput{}
- defer inputTerminated.AssertExpectations(t)
- inputTerminated.On("OutPoint").Return(wire.OutPoint{Index: 3})
- s.inputs[inputTerminated.OutPoint()] = &SweeperInput{
- state: Excluded,
- }
- // Mark the test inputs. We expect the non-exist input and the
- // inputTerminated to be skipped, and the rest to be marked as pending
- // publish.
- set.On("Inputs").Return([]input.Input{
- inputNotExist, inputInit, inputPendingPublish, inputTerminated,
- })
- s.markInputsPendingPublish(set)
- // We expect unchanged number of pending inputs.
- require.Len(s.inputs, 3)
- // We expect the init input's state to become pending publish.
- require.Equal(PendingPublish, s.inputs[inputInit.OutPoint()].state)
- // We expect the pending-publish to stay unchanged.
- require.Equal(PendingPublish,
- s.inputs[inputPendingPublish.OutPoint()].state)
- // We expect the terminated to stay unchanged.
- require.Equal(Excluded, s.inputs[inputTerminated.OutPoint()].state)
- }
- // TestMarkInputsPublished checks that given a list of inputs with different
- // states, only the state `PendingPublish` will be marked as `Published`.
- func TestMarkInputsPublished(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a mock sweeper store.
- mockStore := NewMockSweeperStore()
- // Create a test TxRecord and a dummy error.
- dummyTR := &TxRecord{}
- dummyErr := errors.New("dummy error")
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Store: mockStore,
- })
- // Create three testing inputs.
- //
- // inputNotExist specifies an input that's not found in the sweeper's
- // `inputs` map.
- inputNotExist := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 1},
- }
- // inputInit specifies a newly created input. When marking this as
- // published, we should see an error log as this input hasn't been
- // published yet.
- inputInit := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 2},
- }
- s.inputs[inputInit.PreviousOutPoint] = &SweeperInput{
- state: Init,
- }
- // inputPendingPublish specifies an input that's about to be published.
- inputPendingPublish := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 3},
- }
- s.inputs[inputPendingPublish.PreviousOutPoint] = &SweeperInput{
- state: PendingPublish,
- }
- // First, check that when an error is returned from db, it's properly
- // returned here.
- mockStore.On("StoreTx", dummyTR).Return(dummyErr).Once()
- err := s.markInputsPublished(dummyTR, nil)
- require.ErrorIs(err, dummyErr)
- // We also expect the record has been marked as published.
- require.True(dummyTR.Published)
- // Then, check that the target input has will be correctly marked as
- // published.
- //
- // Mock the store to return nil
- mockStore.On("StoreTx", dummyTR).Return(nil).Once()
- // Mark the test inputs. We expect the non-exist input and the
- // inputInit to be skipped, and the final input to be marked as
- // published.
- err = s.markInputsPublished(dummyTR, []*wire.TxIn{
- inputNotExist, inputInit, inputPendingPublish,
- })
- require.NoError(err)
- // We expect unchanged number of pending inputs.
- require.Len(s.inputs, 2)
- // We expect the init input's state to stay unchanged.
- require.Equal(Init,
- s.inputs[inputInit.PreviousOutPoint].state)
- // We expect the pending-publish input's is now marked as published.
- require.Equal(Published,
- s.inputs[inputPendingPublish.PreviousOutPoint].state)
- // Assert mocked statements are executed as expected.
- mockStore.AssertExpectations(t)
- }
- // TestMarkInputsPublishFailed checks that given a list of inputs with
- // different states, only the state `PendingPublish` and `Published` will be
- // marked as `PublishFailed`.
- func TestMarkInputsPublishFailed(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a mock sweeper store.
- mockStore := NewMockSweeperStore()
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Store: mockStore,
- })
- // Create three testing inputs.
- //
- // inputNotExist specifies an input that's not found in the sweeper's
- // `inputs` map.
- inputNotExist := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 1},
- }
- // inputInit specifies a newly created input. When marking this as
- // published, we should see an error log as this input hasn't been
- // published yet.
- inputInit := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 2},
- }
- s.inputs[inputInit.PreviousOutPoint] = &SweeperInput{
- state: Init,
- }
- // inputPendingPublish specifies an input that's about to be published.
- inputPendingPublish := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 3},
- }
- s.inputs[inputPendingPublish.PreviousOutPoint] = &SweeperInput{
- state: PendingPublish,
- }
- // inputPublished specifies an input that's published.
- inputPublished := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 4},
- }
- s.inputs[inputPublished.PreviousOutPoint] = &SweeperInput{
- state: Published,
- }
- // Mark the test inputs. We expect the non-exist input and the
- // inputInit to be skipped, and the final input to be marked as
- // published.
- s.markInputsPublishFailed([]wire.OutPoint{
- inputNotExist.PreviousOutPoint,
- inputInit.PreviousOutPoint,
- inputPendingPublish.PreviousOutPoint,
- inputPublished.PreviousOutPoint,
- })
- // We expect unchanged number of pending inputs.
- require.Len(s.inputs, 3)
- // We expect the init input's state to stay unchanged.
- require.Equal(Init,
- s.inputs[inputInit.PreviousOutPoint].state)
- // We expect the pending-publish input's is now marked as publish
- // failed.
- require.Equal(PublishFailed,
- s.inputs[inputPendingPublish.PreviousOutPoint].state)
- // We expect the published input's is now marked as publish failed.
- require.Equal(PublishFailed,
- s.inputs[inputPublished.PreviousOutPoint].state)
- // Assert mocked statements are executed as expected.
- mockStore.AssertExpectations(t)
- }
- // TestMarkInputsSwept checks that given a list of inputs with different
- // states, only the non-terminal state will be marked as `Swept`.
- func TestMarkInputsSwept(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a mock input.
- mockInput := &input.MockInput{}
- defer mockInput.AssertExpectations(t)
- // Mock the `OutPoint` to return a dummy outpoint.
- mockInput.On("OutPoint").Return(wire.OutPoint{Hash: chainhash.Hash{1}})
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{})
- // Create three testing inputs.
- //
- // inputNotExist specifies an input that's not found in the sweeper's
- // `inputs` map.
- inputNotExist := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 1},
- }
- // inputInit specifies a newly created input.
- inputInit := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 2},
- }
- s.inputs[inputInit.PreviousOutPoint] = &SweeperInput{
- state: Init,
- Input: mockInput,
- }
- // inputPendingPublish specifies an input that's about to be published.
- inputPendingPublish := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 3},
- }
- s.inputs[inputPendingPublish.PreviousOutPoint] = &SweeperInput{
- state: PendingPublish,
- Input: mockInput,
- }
- // inputTerminated specifies an input that's terminated.
- inputTerminated := &wire.TxIn{
- PreviousOutPoint: wire.OutPoint{Index: 4},
- }
- s.inputs[inputTerminated.PreviousOutPoint] = &SweeperInput{
- state: Excluded,
- Input: mockInput,
- }
- tx := &wire.MsgTx{
- TxIn: []*wire.TxIn{
- inputNotExist, inputInit,
- inputPendingPublish, inputTerminated,
- },
- }
- // Mark the test inputs. We expect the inputTerminated to be skipped,
- // and the rest to be marked as swept.
- s.markInputsSwept(tx, true)
- // We expect unchanged number of pending inputs.
- require.Len(s.inputs, 3)
- // We expect the init input's state to become swept.
- require.Equal(Swept,
- s.inputs[inputInit.PreviousOutPoint].state)
- // We expect the pending-publish becomes swept.
- require.Equal(Swept,
- s.inputs[inputPendingPublish.PreviousOutPoint].state)
- // We expect the terminated to stay unchanged.
- require.Equal(Excluded,
- s.inputs[inputTerminated.PreviousOutPoint].state)
- }
- // TestMempoolLookup checks that the method `mempoolLookup` works as expected.
- func TestMempoolLookup(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a test outpoint.
- op := wire.OutPoint{Index: 1}
- // Create a mock mempool watcher.
- mockMempool := chainntnfs.NewMockMempoolWatcher()
- defer mockMempool.AssertExpectations(t)
- // Create a test sweeper without a mempool.
- s := New(&UtxoSweeperConfig{})
- // Since we don't have a mempool, we expect the call to return a
- // fn.None indicating it's not found.
- tx := s.mempoolLookup(op)
- require.True(tx.IsNone())
- // Re-create the sweeper with the mocked mempool watcher.
- s = New(&UtxoSweeperConfig{
- Mempool: mockMempool,
- })
- // Mock the mempool watcher to return not found.
- mockMempool.On("LookupInputMempoolSpend", op).Return(
- fn.None[wire.MsgTx]()).Once()
- // We expect a fn.None tx to be returned.
- tx = s.mempoolLookup(op)
- require.True(tx.IsNone())
- // Mock the mempool to return a spending tx.
- dummyTx := wire.MsgTx{}
- mockMempool.On("LookupInputMempoolSpend", op).Return(
- fn.Some(dummyTx)).Once()
- // Calling the loopup again, we expect the dummyTx to be returned.
- tx = s.mempoolLookup(op)
- require.False(tx.IsNone())
- require.Equal(dummyTx, tx.UnsafeFromSome())
- }
- // TestUpdateSweeperInputs checks that the method `updateSweeperInputs` will
- // properly update the inputs based on their states.
- func TestUpdateSweeperInputs(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a test sweeper.
- s := New(nil)
- // Create mock inputs.
- inp1 := &input.MockInput{}
- defer inp1.AssertExpectations(t)
- inp2 := &input.MockInput{}
- defer inp2.AssertExpectations(t)
- inp3 := &input.MockInput{}
- defer inp3.AssertExpectations(t)
- // Create a list of inputs using all the states.
- //
- // Mock the input to have a locktime that's matured so it will be
- // returned.
- inp1.On("RequiredLockTime").Return(
- uint32(s.currentHeight), false).Once()
- inp1.On("BlocksToMaturity").Return(uint32(0)).Once()
- inp1.On("HeightHint").Return(uint32(s.currentHeight)).Once()
- input0 := &SweeperInput{state: Init, Input: inp1}
- // These inputs won't hit RequiredLockTime so we won't mock.
- input1 := &SweeperInput{state: PendingPublish, Input: inp1}
- input2 := &SweeperInput{state: Published, Input: inp1}
- // Mock the input to have a locktime that's matured so it will be
- // returned.
- inp1.On("RequiredLockTime").Return(
- uint32(s.currentHeight), false).Once()
- inp1.On("BlocksToMaturity").Return(uint32(0)).Once()
- inp1.On("HeightHint").Return(uint32(s.currentHeight)).Once()
- input3 := &SweeperInput{state: PublishFailed, Input: inp1}
- // These inputs won't hit RequiredLockTime so we won't mock.
- input4 := &SweeperInput{state: Swept, Input: inp1}
- input5 := &SweeperInput{state: Excluded, Input: inp1}
- input6 := &SweeperInput{state: Failed, Input: inp1}
- // Mock the input to have a locktime in the future so it will NOT be
- // returned.
- inp2.On("RequiredLockTime").Return(
- uint32(s.currentHeight+1), true).Once()
- input7 := &SweeperInput{state: Init, Input: inp2}
- // Mock the input to have a CSV expiry in the future so it will NOT be
- // returned.
- inp3.On("RequiredLockTime").Return(
- uint32(s.currentHeight), false).Once()
- inp3.On("BlocksToMaturity").Return(uint32(2)).Once()
- inp3.On("HeightHint").Return(uint32(s.currentHeight)).Once()
- input8 := &SweeperInput{state: Init, Input: inp3}
- // Add the inputs to the sweeper. After the update, we should see the
- // terminated inputs being removed.
- s.inputs = map[wire.OutPoint]*SweeperInput{
- {Index: 0}: input0,
- {Index: 1}: input1,
- {Index: 2}: input2,
- {Index: 3}: input3,
- {Index: 4}: input4,
- {Index: 5}: input5,
- {Index: 6}: input6,
- {Index: 7}: input7,
- {Index: 8}: input8,
- }
- // We expect the inputs with `Swept`, `Excluded`, and `Failed` to be
- // removed.
- expectedInputs := map[wire.OutPoint]*SweeperInput{
- {Index: 0}: input0,
- {Index: 1}: input1,
- {Index: 2}: input2,
- {Index: 3}: input3,
- {Index: 7}: input7,
- {Index: 8}: input8,
- }
- // We expect only the inputs with `Init` and `PublishFailed` to be
- // returned.
- expectedReturn := map[wire.OutPoint]*SweeperInput{
- {Index: 0}: input0,
- {Index: 3}: input3,
- }
- // Update the sweeper inputs.
- inputs := s.updateSweeperInputs()
- // Assert the returned inputs are as expected.
- require.Equal(expectedReturn, inputs)
- // Assert the sweeper inputs are as expected.
- require.Equal(expectedInputs, s.inputs)
- }
- // TestDecideStateAndRBFInfo checks that the expected state and RBFInfo are
- // returned based on whether this input can be found both in mempool and the
- // sweeper store.
- func TestDecideStateAndRBFInfo(t *testing.T) {
- t.Parallel()
- require := require.New(t)
- // Create a test outpoint.
- op := wire.OutPoint{Index: 1}
- // Create a mock mempool watcher and a mock sweeper store.
- mockMempool := chainntnfs.NewMockMempoolWatcher()
- defer mockMempool.AssertExpectations(t)
- mockStore := NewMockSweeperStore()
- defer mockStore.AssertExpectations(t)
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Store: mockStore,
- Mempool: mockMempool,
- })
- // First, mock the mempool to return false.
- mockMempool.On("LookupInputMempoolSpend", op).Return(
- fn.None[wire.MsgTx]()).Once()
- // Since the mempool lookup failed, we exepect state Init and no
- // RBFInfo.
- state, rbf := s.decideStateAndRBFInfo(op)
- require.True(rbf.IsNone())
- require.Equal(Init, state)
- // Mock the mempool lookup to return a tx three times as we are calling
- // attachAvailableRBFInfo three times.
- tx := wire.MsgTx{}
- mockMempool.On("LookupInputMempoolSpend", op).Return(
- fn.Some(tx)).Times(3)
- // Mock the store to return an error saying the tx cannot be found.
- mockStore.On("GetTx", tx.TxHash()).Return(nil, ErrTxNotFound).Once()
- // Although the db lookup failed, we expect the state to be Published.
- state, rbf = s.decideStateAndRBFInfo(op)
- require.True(rbf.IsNone())
- require.Equal(Published, state)
- // Mock the store to return a db error.
- dummyErr := errors.New("dummy error")
- mockStore.On("GetTx", tx.TxHash()).Return(nil, dummyErr).Once()
- // Although the db lookup failed, we expect the state to be Published.
- state, rbf = s.decideStateAndRBFInfo(op)
- require.True(rbf.IsNone())
- require.Equal(Published, state)
- // Mock the store to return a record.
- tr := &TxRecord{
- Fee: 100,
- FeeRate: 100,
- }
- mockStore.On("GetTx", tx.TxHash()).Return(tr, nil).Once()
- // Call the method again.
- state, rbf = s.decideStateAndRBFInfo(op)
- // Assert that the RBF info is returned.
- rbfInfo := fn.Some(RBFInfo{
- Txid: tx.TxHash(),
- Fee: btcutil.Amount(tr.Fee),
- FeeRate: chainfee.SatPerKWeight(tr.FeeRate),
- })
- require.Equal(rbfInfo, rbf)
- // Assert the state is updated.
- require.Equal(Published, state)
- }
- // TestMarkInputFailed checks that the input is marked as failed as expected.
- func TestMarkInputFailed(t *testing.T) {
- t.Parallel()
- // Create a mock input.
- mockInput := &input.MockInput{}
- defer mockInput.AssertExpectations(t)
- // Mock the `OutPoint` to return a dummy outpoint.
- mockInput.On("OutPoint").Return(wire.OutPoint{Hash: chainhash.Hash{1}})
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{})
- // Create a testing pending input.
- pi := &SweeperInput{
- state: Init,
- Input: mockInput,
- }
- // Call the method under test.
- s.markInputFailed(pi, errors.New("dummy error"))
- // Assert the state is updated.
- require.Equal(t, Failed, pi.state)
- }
- // TestSweepPendingInputs checks that `sweepPendingInputs` correctly executes
- // its workflow based on the returned values from the interfaces.
- func TestSweepPendingInputs(t *testing.T) {
- t.Parallel()
- // Create a mock wallet and aggregator.
- wallet := &MockWallet{}
- defer wallet.AssertExpectations(t)
- aggregator := &mockUtxoAggregator{}
- defer aggregator.AssertExpectations(t)
- publisher := &MockBumper{}
- defer publisher.AssertExpectations(t)
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Wallet: wallet,
- Aggregator: aggregator,
- Publisher: publisher,
- GenSweepScript: func() ([]byte, error) {
- return testPubKey.SerializeCompressed(), nil
- },
- NoDeadlineConfTarget: uint32(DefaultDeadlineDelta),
- })
- // Set a current height to test the deadline override.
- s.currentHeight = testHeight
- // Create an input set that needs wallet inputs.
- setNeedWallet := &MockInputSet{}
- defer setNeedWallet.AssertExpectations(t)
- // Mock this set to ask for wallet input.
- setNeedWallet.On("NeedWalletInput").Return(true).Once()
- setNeedWallet.On("AddWalletInputs", wallet).Return(nil).Once()
- // Mock the wallet to require the lock once.
- wallet.On("WithCoinSelectLock", mock.Anything).Return(nil).Once()
- // Create an input set that doesn't need wallet inputs.
- normalSet := &MockInputSet{}
- defer normalSet.AssertExpectations(t)
- normalSet.On("NeedWalletInput").Return(false).Once()
- // Mock the methods used in `sweep`. This is not important for this
- // unit test.
- setNeedWallet.On("Inputs").Return(nil).Times(4)
- setNeedWallet.On("DeadlineHeight").Return(testHeight).Once()
- setNeedWallet.On("Budget").Return(btcutil.Amount(1)).Once()
- setNeedWallet.On("StartingFeeRate").Return(
- fn.None[chainfee.SatPerKWeight]()).Once()
- normalSet.On("Inputs").Return(nil).Times(4)
- normalSet.On("DeadlineHeight").Return(testHeight).Once()
- normalSet.On("Budget").Return(btcutil.Amount(1)).Once()
- normalSet.On("StartingFeeRate").Return(
- fn.None[chainfee.SatPerKWeight]()).Once()
- // Make pending inputs for testing. We don't need real values here as
- // the returned clusters are mocked.
- pis := make(InputsMap)
- // Mock the aggregator to return the mocked input sets.
- aggregator.On("ClusterInputs", pis).Return([]InputSet{
- setNeedWallet, normalSet,
- })
- // Mock `Broadcast` to return an error. This should cause the
- // `createSweepTx` inside `sweep` to fail. This is done so we can
- // terminate the method early as we are only interested in testing the
- // workflow in `sweepPendingInputs`. We don't need to test `sweep` here
- // as it should be tested in its own unit test.
- dummyErr := errors.New("dummy error")
- publisher.On("Broadcast", mock.Anything).Return(nil, dummyErr).Twice()
- // Call the method under test.
- s.sweepPendingInputs(pis)
- }
- // TestHandleBumpEventTxFailed checks that the sweeper correctly handles the
- // case where the bump event tx fails to be published.
- func TestHandleBumpEventTxFailed(t *testing.T) {
- t.Parallel()
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{})
- var (
- // Create four testing outpoints.
- op1 = wire.OutPoint{Hash: chainhash.Hash{1}}
- op2 = wire.OutPoint{Hash: chainhash.Hash{2}}
- op3 = wire.OutPoint{Hash: chainhash.Hash{3}}
- opNotExist = wire.OutPoint{Hash: chainhash.Hash{4}}
- )
- // Create three mock inputs.
- input1 := &input.MockInput{}
- defer input1.AssertExpectations(t)
- input2 := &input.MockInput{}
- defer input2.AssertExpectations(t)
- input3 := &input.MockInput{}
- defer input3.AssertExpectations(t)
- // Construct the initial state for the sweeper.
- s.inputs = InputsMap{
- op1: &SweeperInput{Input: input1, state: PendingPublish},
- op2: &SweeperInput{Input: input2, state: PendingPublish},
- op3: &SweeperInput{Input: input3, state: PendingPublish},
- }
- // Create a testing tx that spends the first two inputs.
- tx := &wire.MsgTx{
- TxIn: []*wire.TxIn{
- {PreviousOutPoint: op1},
- {PreviousOutPoint: op2},
- {PreviousOutPoint: opNotExist},
- },
- }
- // Create a testing bump result.
- br := &BumpResult{
- Tx: tx,
- Event: TxFailed,
- Err: errDummy,
- }
- // Call the method under test.
- err := s.handleBumpEvent(br)
- require.ErrorIs(t, err, errDummy)
- // Assert the states of the first two inputs are updated.
- require.Equal(t, PublishFailed, s.inputs[op1].state)
- require.Equal(t, PublishFailed, s.inputs[op2].state)
- // Assert the state of the third input is not updated.
- require.Equal(t, PendingPublish, s.inputs[op3].state)
- // Assert the non-existing input is not added to the pending inputs.
- require.NotContains(t, s.inputs, opNotExist)
- }
- // TestHandleBumpEventTxReplaced checks that the sweeper correctly handles the
- // case where the bump event tx is replaced.
- func TestHandleBumpEventTxReplaced(t *testing.T) {
- t.Parallel()
- // Create a mock store.
- store := &MockSweeperStore{}
- defer store.AssertExpectations(t)
- // Create a mock wallet.
- wallet := &MockWallet{}
- defer wallet.AssertExpectations(t)
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Store: store,
- Wallet: wallet,
- })
- // Create a testing outpoint.
- op := wire.OutPoint{Hash: chainhash.Hash{1}}
- // Create a mock input.
- inp := &input.MockInput{}
- defer inp.AssertExpectations(t)
- // Construct the initial state for the sweeper.
- s.inputs = InputsMap{
- op: &SweeperInput{Input: inp, state: PendingPublish},
- }
- // Create a testing tx that spends the input.
- tx := &wire.MsgTx{
- LockTime: 1,
- TxIn: []*wire.TxIn{
- {PreviousOutPoint: op},
- },
- }
- // Create a replacement tx.
- replacementTx := &wire.MsgTx{
- LockTime: 2,
- TxIn: []*wire.TxIn{
- {PreviousOutPoint: op},
- },
- }
- // Create a testing bump result.
- br := &BumpResult{
- Tx: replacementTx,
- ReplacedTx: tx,
- Event: TxReplaced,
- }
- // Mock the store to return an error.
- dummyErr := errors.New("dummy error")
- store.On("GetTx", tx.TxHash()).Return(nil, dummyErr).Once()
- // Call the method under test and assert the error is returned.
- err := s.handleBumpEventTxReplaced(br)
- require.ErrorIs(t, err, dummyErr)
- // Mock the store to return the old tx record.
- store.On("GetTx", tx.TxHash()).Return(&TxRecord{
- Txid: tx.TxHash(),
- }, nil).Once()
- // We expect to cancel rebroadcasting the replaced tx.
- wallet.On("CancelRebroadcast", tx.TxHash()).Once()
- // Mock an error returned when deleting the old tx record.
- store.On("DeleteTx", tx.TxHash()).Return(dummyErr).Once()
- // Call the method under test and assert the error is returned.
- err = s.handleBumpEventTxReplaced(br)
- require.ErrorIs(t, err, dummyErr)
- // Mock the store to return the old tx record and delete it without
- // error.
- store.On("GetTx", tx.TxHash()).Return(&TxRecord{
- Txid: tx.TxHash(),
- }, nil).Once()
- store.On("DeleteTx", tx.TxHash()).Return(nil).Once()
- // Mock the store to save the new tx record.
- store.On("StoreTx", &TxRecord{
- Txid: replacementTx.TxHash(),
- Published: true,
- }).Return(nil).Once()
- // We expect to cancel rebroadcasting the replaced tx.
- wallet.On("CancelRebroadcast", tx.TxHash()).Once()
- // Call the method under test.
- err = s.handleBumpEventTxReplaced(br)
- require.NoError(t, err)
- // Assert the state of the input is updated.
- require.Equal(t, Published, s.inputs[op].state)
- }
- // TestHandleBumpEventTxPublished checks that the sweeper correctly handles the
- // case where the bump event tx is published.
- func TestHandleBumpEventTxPublished(t *testing.T) {
- t.Parallel()
- // Create a mock store.
- store := &MockSweeperStore{}
- defer store.AssertExpectations(t)
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Store: store,
- })
- // Create a testing outpoint.
- op := wire.OutPoint{Hash: chainhash.Hash{1}}
- // Create a mock input.
- inp := &input.MockInput{}
- defer inp.AssertExpectations(t)
- // Construct the initial state for the sweeper.
- s.inputs = InputsMap{
- op: &SweeperInput{Input: inp, state: PendingPublish},
- }
- // Create a testing tx that spends the input.
- tx := &wire.MsgTx{
- LockTime: 1,
- TxIn: []*wire.TxIn{
- {PreviousOutPoint: op},
- },
- }
- // Create a testing bump result.
- br := &BumpResult{
- Tx: tx,
- Event: TxPublished,
- }
- // Mock the store to save the new tx record.
- store.On("StoreTx", &TxRecord{
- Txid: tx.TxHash(),
- Published: true,
- }).Return(nil).Once()
- // Call the method under test.
- err := s.handleBumpEventTxPublished(br)
- require.NoError(t, err)
- // Assert the state of the input is updated.
- require.Equal(t, Published, s.inputs[op].state)
- }
- // TestMonitorFeeBumpResult checks that the fee bump monitor loop correctly
- // exits when the sweeper is stopped, the tx is confirmed or failed.
- func TestMonitorFeeBumpResult(t *testing.T) {
- // Create a mock store.
- store := &MockSweeperStore{}
- defer store.AssertExpectations(t)
- // Create a mock wallet.
- wallet := &MockWallet{}
- defer wallet.AssertExpectations(t)
- // Create a test sweeper.
- s := New(&UtxoSweeperConfig{
- Store: store,
- Wallet: wallet,
- })
- // Create a testing outpoint.
- op := wire.OutPoint{Hash: chainhash.Hash{1}}
- // Create a mock input.
- inp := &input.MockInput{}
- defer inp.AssertExpectations(t)
- // Construct the initial state for the sweeper.
- s.inputs = InputsMap{
- op: &SweeperInput{Input: inp, state: PendingPublish},
- }
- // Create a testing tx that spends the input.
- tx := &wire.MsgTx{
- LockTime: 1,
- TxIn: []*wire.TxIn{
- {PreviousOutPoint: op},
- },
- }
- testCases := []struct {
- name string
- setupResultChan func() <-chan *BumpResult
- shouldExit bool
- }{
- {
- // When a tx confirmed event is received, we expect to
- // exit the monitor loop.
- name: "tx confirmed",
- // We send a result with TxConfirmed event to the
- // result channel.
- setupResultChan: func() <-chan *BumpResult {
- // Create a result chan.
- resultChan := make(chan *BumpResult, 1)
- resultChan <- &BumpResult{
- Tx: tx,
- Event: TxConfirmed,
- Fee: 10000,
- FeeRate: 100,
- }
- // We expect to cancel rebroadcasting the tx
- // once confirmed.
- wallet.On("CancelRebroadcast",
- tx.TxHash()).Once()
- return resultChan
- },
- shouldExit: true,
- },
- {
- // When a tx failed event is received, we expect to
- // exit the monitor loop.
- name: "tx failed",
- // We send a result with TxConfirmed event to the
- // result channel.
- setupResultChan: func() <-chan *BumpResult {
- // Create a result chan.
- resultChan := make(chan *BumpResult, 1)
- resultChan <- &BumpResult{
- Tx: tx,
- Event: TxFailed,
- Err: errDummy,
- }
- // We expect to cancel rebroadcasting the tx
- // once failed.
- wallet.On("CancelRebroadcast",
- tx.TxHash()).Once()
- return resultChan
- },
- shouldExit: true,
- },
- {
- // When processing non-confirmed events, the monitor
- // should not exit.
- name: "no exit on normal event",
- // We send a result with TxPublished and mock the
- // method `StoreTx` to return nil.
- setupResultChan: func() <-chan *BumpResult {
- // Create a result chan.
- resultChan := make(chan *BumpResult, 1)
- resultChan <- &BumpResult{
- Tx: tx,
- Event: TxPublished,
- }
- return resultChan
- },
- shouldExit: false,
- }, {
- // When the sweeper is shutting down, the monitor loop
- // should exit.
- name: "exit on sweeper shutdown",
- // We don't send anything but quit the sweeper.
- setupResultChan: func() <-chan *BumpResult {
- close(s.quit)
- return nil
- },
- shouldExit: true,
- },
- }
- for _, tc := range testCases {
- tc := tc
- t.Run(tc.name, func(t *testing.T) {
- // Setup the testing result channel.
- resultChan := tc.setupResultChan()
- // Create a done chan that's used to signal the monitor
- // has exited.
- done := make(chan struct{})
- s.wg.Add(1)
- go func() {
- s.monitorFeeBumpResult(resultChan)
- close(done)
- }()
- // The monitor is expected to exit, we check it's done
- // in one second or fail.
- if tc.shouldExit {
- select {
- case <-done:
- case <-time.After(1 * time.Second):
- require.Fail(t, "monitor not exited")
- }
- return
- }
- // The monitor should not exit, check it doesn't close
- // the `done` channel within one second.
- select {
- case <-done:
- require.Fail(t, "monitor exited")
- case <-time.After(1 * time.Second):
- }
- })
- }
- }
|