mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-18 13:27:56 +01:00
ab7aae0708
Find and replace all nolint instances refering to the `lll` linter and replace with `ll` which is the name of our custom version of the `lll` linter which can be used to ignore log lines during linting. The next commit will do the configuration of the custom linter and disable the default one.
1114 lines
31 KiB
Go
1114 lines
31 KiB
Go
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"
|
|
"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 testing inputs for each state.
|
|
//
|
|
// 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,
|
|
}
|
|
|
|
// inputPublishFailed specifies an input that's failed to be published.
|
|
inputPublishFailed := &wire.TxIn{
|
|
PreviousOutPoint: wire.OutPoint{Index: 5},
|
|
}
|
|
s.inputs[inputPublishFailed.PreviousOutPoint] = &SweeperInput{
|
|
state: PublishFailed,
|
|
}
|
|
|
|
// inputSwept specifies an input that's swept.
|
|
inputSwept := &wire.TxIn{
|
|
PreviousOutPoint: wire.OutPoint{Index: 6},
|
|
}
|
|
s.inputs[inputSwept.PreviousOutPoint] = &SweeperInput{
|
|
state: Swept,
|
|
}
|
|
|
|
// inputExcluded specifies an input that's excluded.
|
|
inputExcluded := &wire.TxIn{
|
|
PreviousOutPoint: wire.OutPoint{Index: 7},
|
|
}
|
|
s.inputs[inputExcluded.PreviousOutPoint] = &SweeperInput{
|
|
state: Excluded,
|
|
}
|
|
|
|
// inputFailed specifies an input that's failed.
|
|
inputFailed := &wire.TxIn{
|
|
PreviousOutPoint: wire.OutPoint{Index: 8},
|
|
}
|
|
s.inputs[inputFailed.PreviousOutPoint] = &SweeperInput{
|
|
state: Failed,
|
|
}
|
|
|
|
// Gather all inputs' outpoints.
|
|
pendingOps := make([]wire.OutPoint, 0, len(s.inputs)+1)
|
|
for op := range s.inputs {
|
|
pendingOps = append(pendingOps, op)
|
|
}
|
|
pendingOps = append(pendingOps, inputNotExist.PreviousOutPoint)
|
|
|
|
// 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(pendingOps)
|
|
|
|
// We expect unchanged number of pending inputs.
|
|
require.Len(s.inputs, 7)
|
|
|
|
// 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)
|
|
|
|
// We expect the publish failed input to stay unchanged.
|
|
require.Equal(PublishFailed,
|
|
s.inputs[inputPublishFailed.PreviousOutPoint].state)
|
|
|
|
// We expect the swept input to stay unchanged.
|
|
require.Equal(Swept, s.inputs[inputSwept.PreviousOutPoint].state)
|
|
|
|
// We expect the excluded input to stay unchanged.
|
|
require.Equal(Excluded, s.inputs[inputExcluded.PreviousOutPoint].state)
|
|
|
|
// We expect the failed input to stay unchanged.
|
|
require.Equal(Failed, s.inputs[inputFailed.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() fn.Result[lnwallet.AddrWithKey] {
|
|
//nolint:ll
|
|
return fn.Ok(lnwallet.AddrWithKey{
|
|
DeliveryAddress: testPubKey.SerializeCompressed(),
|
|
})
|
|
},
|
|
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).Maybe()
|
|
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).Maybe()
|
|
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):
|
|
}
|
|
})
|
|
}
|
|
}
|