contractcourt: add tests for mempool rejection.

Add a test where the channel arbitrator starts up correctly
when a prior unilateral close of a channel did not broadcast
for specific reasons.
Also add a test which ensures that when a crib output is
rejected by the bitcoin backend the startup works correctly
for specific errors.
This commit is contained in:
ziggie 2023-06-07 10:00:13 +02:00
parent 8314f0a879
commit c88ff14477
No known key found for this signature in database
GPG Key ID: 1AFF9C4DCED6D666
2 changed files with 189 additions and 2 deletions

View File

@ -2686,6 +2686,91 @@ func TestChannelArbitratorAnchors(t *testing.T) {
)
}
// TestChannelArbitratorStartAfterCommitmentRejected tests that when we run into
// the case where our commitment tx is rejected by our bitcoin backend we still
// continue to startup the arbitrator for a specific set of errors.
func TestChannelArbitratorStartAfterCommitmentRejected(t *testing.T) {
t.Parallel()
tests := []struct {
name string
// The specific error during broadcasting the transaction.
broadcastErr error
// expected state when the startup of the arbitrator succeeds.
expectedState ArbitratorState
expectedStartup bool
}{
{
name: "Commitment is rejected because of low mempool " +
"fees",
broadcastErr: lnwallet.ErrMempoolFee,
expectedState: StateCommitmentBroadcasted,
expectedStartup: true,
},
{
// We map a rejected rbf transaction to ErrDoubleSpend
// in lnd.
name: "Commitment is rejected because of a " +
"rbf transaction not succeeding",
broadcastErr: lnwallet.ErrDoubleSpend,
expectedState: StateCommitmentBroadcasted,
expectedStartup: true,
},
{
name: "Commitment is rejected with an " +
"unmatched error",
broadcastErr: fmt.Errorf("Reject Commitment Tx"),
expectedState: StateBroadcastCommit,
expectedStartup: false,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// We'll create the arbitrator and its backing log
// to signal that it's already in the process of being
// force closed.
log := &mockArbitratorLog{
newStates: make(chan ArbitratorState, 5),
state: StateBroadcastCommit,
}
chanArbCtx, err := createTestChannelArbitrator(t, log)
require.NoError(t, err, "unable to create "+
"ChannelArbitrator")
chanArb := chanArbCtx.chanArb
// Customize the PublishTx function of the arbitrator.
chanArb.cfg.PublishTx = func(*wire.MsgTx,
string) error {
return test.broadcastErr
}
err = chanArb.Start(nil)
if !test.expectedStartup {
require.ErrorIs(t, err, test.broadcastErr)
return
}
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, chanArb.Stop())
})
// In case the startup succeeds we check that the state
// is as expected.
chanArbCtx.AssertStateTransitions(test.expectedState)
})
}
}
// putResolverReportInChannel returns a put report function which will pipe
// reports into the channel provided.
func putResolverReportInChannel(reports chan *channeldb.ResolverReport) func(

View File

@ -504,8 +504,7 @@ func createNurseryTestContext(t *testing.T,
/// Restart nursery.
nurseryCfg.SweepInput = ctx.sweeper.sweepInput
ctx.nursery = NewUtxoNursery(&nurseryCfg)
ctx.nursery.Start()
require.NoError(t, ctx.nursery.Start())
})
}
@ -646,6 +645,109 @@ func incubateTestOutput(t *testing.T, nursery *UtxoNursery,
return outgoingRes
}
// TestRejectedCribTransaction makes sure that our nursery does not fail to
// start up in case a Crib transaction (htlc-timeout) is rejected by the
// bitcoin backend for some excepted reasons.
func TestRejectedCribTransaction(t *testing.T) {
t.Parallel()
tests := []struct {
name string
// The specific error during broadcasting the transaction.
broadcastErr error
// expectErr specifies whether the rejection of the transaction
// fails the nursery engine.
expectErr bool
}{
{
name: "Crib tx is rejected because of low mempool " +
"fees",
broadcastErr: lnwallet.ErrMempoolFee,
},
{
// We map a rejected rbf transaction to ErrDoubleSpend
// in lnd.
name: "Crib tx is rejected because of a " +
"rbf transaction not succeeding",
broadcastErr: lnwallet.ErrDoubleSpend,
},
{
name: "Crib tx is rejected with an " +
"unmatched error",
broadcastErr: fmt.Errorf("Reject Commitment Tx"),
expectErr: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// The checkStartStop function just calls the callback
// here to make sure the restart routine works
// correctly.
ctx := createNurseryTestContext(t,
func(callback func()) bool {
callback()
return true
})
outgoingRes := createOutgoingRes(true)
ctx.nursery.cfg.PublishTransaction =
func(tx *wire.MsgTx, source string) error {
log.Tracef("Publishing tx %v "+
"by %v", tx.TxHash(), source)
return test.broadcastErr
}
ctx.notifyEpoch(125)
// Hand off to nursery.
err := ctx.nursery.IncubateOutputs(
testChanPoint,
[]lnwallet.OutgoingHtlcResolution{*outgoingRes},
nil, 0,
)
if test.expectErr {
require.ErrorIs(t, err, test.broadcastErr)
return
}
require.NoError(t, err)
// Make sure that a restart is not affected by the
// rejected Crib transaction.
ctx.restart()
// Confirm the timeout tx. This should promote the
// HTLC to KNDR state.
timeoutTxHash := outgoingRes.SignedTimeoutTx.TxHash()
err = ctx.notifier.ConfirmTx(&timeoutTxHash, 126)
require.NoError(t, err)
// Wait for output to be promoted in store to KNDR.
select {
case <-ctx.store.cribToKinderChan:
case <-time.After(defaultTestTimeout):
t.Fatalf("output not promoted to KNDR")
}
// Notify arrival of block where second level HTLC
// unlocks.
ctx.notifyEpoch(128)
// Check final sweep into wallet.
testSweepHtlc(t, ctx)
// Cleanup utxonursery.
ctx.finish()
})
}
}
func assertNurseryReport(t *testing.T, nursery *UtxoNursery,
expectedNofHtlcs int, expectedStage uint32,
expectedLimboBalance btcutil.Amount) {