mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
e0ced8e629
This way we can re-use it. We also make it slightly more generalized.
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:lll
|
|
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):
|
|
}
|
|
})
|
|
}
|
|
}
|