sweep: retry sweeping inputs upon TxUnknownSpend
We now start handling `TxUnknownSpend` in our sweeper to make sure the failed inputs are retried when possible.
This commit is contained in:
2 files changed
+387
No files matched your search
@@ -1199,3 +1199,246 @@ func TestHandleBumpEventTxFatal(t *testing.T) {
|
||||
err = s.handleBumpEventTxFatal(resp)
|
||||
rt.NoError(err)
|
||||
}
|
||||
|
||||
// TestHandleUnknownSpendTxOurs checks that `handleUnknownSpendTx` correctly
|
||||
// marks an input as swept given the tx is ours.
|
||||
func TestHandleUnknownSpendTxOurs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a mock store.
|
||||
store := &MockSweeperStore{}
|
||||
defer store.AssertExpectations(t)
|
||||
|
||||
// Create a mock input set.
|
||||
set := &MockInputSet{}
|
||||
defer set.AssertExpectations(t)
|
||||
|
||||
// Create a test sweeper.
|
||||
s := New(&UtxoSweeperConfig{
|
||||
Store: store,
|
||||
})
|
||||
|
||||
// Create a mock input.
|
||||
inp := createMockInput(t, s, PublishFailed)
|
||||
op := inp.OutPoint()
|
||||
|
||||
si, ok := s.inputs[op]
|
||||
require.True(t, ok)
|
||||
|
||||
// Create a testing tx that spends the input.
|
||||
tx := &wire.MsgTx{
|
||||
LockTime: 1,
|
||||
TxIn: []*wire.TxIn{
|
||||
{PreviousOutPoint: op},
|
||||
},
|
||||
}
|
||||
txid := tx.TxHash()
|
||||
|
||||
// Mock the store to return true when calling IsOurTx.
|
||||
store.On("IsOurTx", txid).Return(true, nil).Once()
|
||||
|
||||
// Call the method under test.
|
||||
s.handleUnknownSpendTx(si, tx)
|
||||
|
||||
// Assert the state of the input is updated.
|
||||
require.Equal(t, Swept, s.inputs[op].state)
|
||||
}
|
||||
|
||||
// TestHandleUnknownSpendTxThirdParty checks that `handleUnknownSpendTx`
|
||||
// correctly marks an input as fatal given the tx is not ours.
|
||||
func TestHandleInputSpendTxThirdParty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a mock store.
|
||||
store := &MockSweeperStore{}
|
||||
defer store.AssertExpectations(t)
|
||||
|
||||
// Create a mock input set.
|
||||
set := &MockInputSet{}
|
||||
defer set.AssertExpectations(t)
|
||||
|
||||
// Create a test sweeper.
|
||||
s := New(&UtxoSweeperConfig{
|
||||
Store: store,
|
||||
})
|
||||
|
||||
// Create a mock input.
|
||||
inp := createMockInput(t, s, PublishFailed)
|
||||
op := inp.OutPoint()
|
||||
|
||||
si, ok := s.inputs[op]
|
||||
require.True(t, ok)
|
||||
|
||||
// Create a testing tx that spends the input.
|
||||
tx := &wire.MsgTx{
|
||||
LockTime: 1,
|
||||
TxIn: []*wire.TxIn{
|
||||
{PreviousOutPoint: op},
|
||||
},
|
||||
}
|
||||
txid := tx.TxHash()
|
||||
|
||||
// Mock the store to return false when calling IsOurTx.
|
||||
store.On("IsOurTx", txid).Return(false, nil).Once()
|
||||
|
||||
// Mock `ListSweeps` to return an empty slice as we are testing the
|
||||
// workflow here, not the method `removeConflictSweepDescendants`.
|
||||
store.On("ListSweeps").Return([]chainhash.Hash{}, nil).Once()
|
||||
|
||||
// Call the method under test.
|
||||
s.handleUnknownSpendTx(si, tx)
|
||||
|
||||
// Assert the state of the input is updated.
|
||||
require.Equal(t, Fatal, s.inputs[op].state)
|
||||
}
|
||||
|
||||
// TestHandleBumpEventTxUnknownSpendNoRetry checks the case when all the inputs
|
||||
// are failed due to them being spent by another party.
|
||||
func TestHandleBumpEventTxUnknownSpendNoRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a mock store.
|
||||
store := &MockSweeperStore{}
|
||||
defer store.AssertExpectations(t)
|
||||
|
||||
// Create a mock input set.
|
||||
set := &MockInputSet{}
|
||||
defer set.AssertExpectations(t)
|
||||
|
||||
// Create a test sweeper.
|
||||
s := New(&UtxoSweeperConfig{
|
||||
Store: store,
|
||||
})
|
||||
|
||||
// Create a mock input.
|
||||
inp := createMockInput(t, s, PendingPublish)
|
||||
set.On("Inputs").Return([]input.Input{inp})
|
||||
|
||||
op := inp.OutPoint()
|
||||
|
||||
// Create a testing tx that spends the input.
|
||||
tx := &wire.MsgTx{
|
||||
LockTime: 1,
|
||||
TxIn: []*wire.TxIn{
|
||||
{PreviousOutPoint: op},
|
||||
},
|
||||
}
|
||||
txid := tx.TxHash()
|
||||
|
||||
// Create a testing bump result.
|
||||
br := &BumpResult{
|
||||
Tx: tx,
|
||||
Event: TxUnknownSpend,
|
||||
SpentInputs: map[wire.OutPoint]*wire.MsgTx{
|
||||
op: tx,
|
||||
},
|
||||
}
|
||||
|
||||
// Create a testing bump response.
|
||||
resp := &bumpResp{
|
||||
result: br,
|
||||
set: set,
|
||||
}
|
||||
|
||||
// Mock the store to return true when calling IsOurTx.
|
||||
store.On("IsOurTx", txid).Return(true, nil).Once()
|
||||
|
||||
// Call the method under test.
|
||||
s.handleBumpEventTxUnknownSpend(resp)
|
||||
|
||||
// Assert the state of the input is updated.
|
||||
require.Equal(t, Swept, s.inputs[op].state)
|
||||
}
|
||||
|
||||
// TestHandleBumpEventTxUnknownSpendWithRetry checks the case when some the
|
||||
// inputs are retried after the bad inputs are filtered out.
|
||||
func TestHandleBumpEventTxUnknownSpendWithRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a mock store.
|
||||
store := &MockSweeperStore{}
|
||||
defer store.AssertExpectations(t)
|
||||
|
||||
// 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),
|
||||
Store: store,
|
||||
})
|
||||
|
||||
// Create a mock input set.
|
||||
set := &MockInputSet{}
|
||||
defer set.AssertExpectations(t)
|
||||
|
||||
// Create mock inputs - inp1 will be the bad input, and inp2 will be
|
||||
// retried.
|
||||
inp1 := createMockInput(t, s, PendingPublish)
|
||||
inp2 := createMockInput(t, s, PendingPublish)
|
||||
set.On("Inputs").Return([]input.Input{inp1, inp2})
|
||||
|
||||
op1 := inp1.OutPoint()
|
||||
op2 := inp2.OutPoint()
|
||||
|
||||
inp2.On("RequiredLockTime").Return(
|
||||
uint32(s.currentHeight), false).Once()
|
||||
inp2.On("BlocksToMaturity").Return(uint32(0)).Once()
|
||||
inp2.On("HeightHint").Return(uint32(s.currentHeight)).Once()
|
||||
|
||||
// Create a testing tx that spends inp1.
|
||||
tx := &wire.MsgTx{
|
||||
LockTime: 1,
|
||||
TxIn: []*wire.TxIn{
|
||||
{PreviousOutPoint: op1},
|
||||
},
|
||||
}
|
||||
txid := tx.TxHash()
|
||||
|
||||
// Create a testing bump result.
|
||||
br := &BumpResult{
|
||||
Tx: tx,
|
||||
Event: TxUnknownSpend,
|
||||
SpentInputs: map[wire.OutPoint]*wire.MsgTx{
|
||||
op1: tx,
|
||||
},
|
||||
}
|
||||
|
||||
// Create a testing bump response.
|
||||
resp := &bumpResp{
|
||||
result: br,
|
||||
set: set,
|
||||
}
|
||||
|
||||
// Mock the store to return true when calling IsOurTx.
|
||||
store.On("IsOurTx", txid).Return(true, nil).Once()
|
||||
|
||||
// Mock the aggregator to return an empty slice as we are not testing
|
||||
// the actual sweeping behavior.
|
||||
aggregator.On("ClusterInputs", mock.Anything).Return([]InputSet{})
|
||||
|
||||
// Call the method under test.
|
||||
s.handleBumpEventTxUnknownSpend(resp)
|
||||
|
||||
// Assert the first input is removed.
|
||||
require.NotContains(t, s.inputs, op1)
|
||||
|
||||
// Assert the state of the input is updated.
|
||||
require.Equal(t, PublishFailed, s.inputs[op2].state)
|
||||
}
|
||||
Reference in new issue
Block a user