package chancloser import ( "bytes" "encoding/hex" "errors" "fmt" "math/rand" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/protofsm" "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) var ( localAddr = lnwire.DeliveryAddress(append( []byte{txscript.OP_1, txscript.OP_DATA_32}, bytes.Repeat([]byte{0x01}, 32)..., )) remoteAddr = lnwire.DeliveryAddress(append( []byte{txscript.OP_1, txscript.OP_DATA_32}, bytes.Repeat([]byte{0x02}, 32)..., )) localSigBytes = fromHex("3045022100cd496f2ab4fe124f977ffe3caa09f757" + "6d8a34156b4e55d326b4dffc0399a094022013500a0510b5094bff220c7" + "4656879b8ca0369d3da78004004c970790862fc03") localSig = sigMustParse(localSigBytes) localSigWire = mustWireSig(&localSig) remoteSigBytes = fromHex("304502210082235e21a2300022738dabb8e1bbd9d1" + "9cfb1e7ab8c30a23b0afbb8d178abcf3022024bf68e256c534ddfaf966b" + "f908deb944305596f7bdcc38d69acad7f9c868724") remoteSig = sigMustParse(remoteSigBytes) remoteWireSig = mustWireSig(&remoteSig) localTx = wire.MsgTx{Version: 2} closeTx = wire.NewMsgTx(2) ) func sigMustParse(sigBytes []byte) ecdsa.Signature { sig, err := ecdsa.ParseSignature(sigBytes) if err != nil { panic(err) } return *sig } func mustWireSig(e input.Signature) lnwire.Sig { wireSig, err := lnwire.NewSigFromSignature(e) if err != nil { panic(err) } return wireSig } func fromHex(s string) []byte { r, err := hex.DecodeString(s) if err != nil { panic("invalid hex in source file: " + s) } return r } func randOutPoint(t *testing.T) wire.OutPoint { var op wire.OutPoint if _, err := rand.Read(op.Hash[:]); err != nil { t.Fatalf("unable to generate random outpoint: %v", err) } op.Index = rand.Uint32() return op } func randPubKey(t *testing.T) *btcec.PublicKey { priv, err := btcec.NewPrivateKey() if err != nil { t.Fatalf("unable to generate private key: %v", err) } return priv.PubKey() } func assertStateTransitions[Event any, Env protofsm.Environment]( t *testing.T, stateSub protofsm.StateSubscriber[Event, Env], expectedStates []protofsm.State[Event, Env]) { t.Helper() for _, expectedState := range expectedStates { newState := <-stateSub.NewItemCreated.ChanOut() require.IsType(t, expectedState, newState) } // We should have no more states. select { case newState := <-stateSub.NewItemCreated.ChanOut(): t.Fatalf("unexpected state transition: %v", newState) default: } } // unknownEvent is a dummy event that is used to test that the state machine // transitions properly fail when an unknown event is received. type unknownEvent struct { } func (u *unknownEvent) protocolSealed() { } // assertUnknownEventFail asserts that the state machine fails as expected // given an unknown event. func assertUnknownEventFail(t *testing.T, startingState ProtocolState) { t.Helper() // Any other event should be ignored. t.Run("unknown_event", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some(startingState), }) defer closeHarness.stopAndAssert() closeHarness.expectFailure(ErrInvalidStateTransition) closeHarness.chanCloser.SendEvent(&unknownEvent{}) // There should be no further state transitions. closeHarness.assertNoStateTransitions() }) } type harnessCfg struct { initialState fn.Option[ProtocolState] thawHeight fn.Option[uint32] localUpfrontAddr fn.Option[lnwire.DeliveryAddress] remoteUpfrontAddr fn.Option[lnwire.DeliveryAddress] } // rbfCloserTestHarness is a test harness for the RBF closer. type rbfCloserTestHarness struct { *testing.T cfg *harnessCfg chanCloser *RbfChanCloser env Environment newAddrErr error pkScript []byte peerPub btcec.PublicKey startingState RbfState feeEstimator *mockFeeEstimator chanObserver *mockChanObserver signer *mockCloseSigner daemonAdapters *dummyAdapters errReporter *mockErrorReporter stateSub protofsm.StateSubscriber[ProtocolEvent, *Environment] } var errfailAddr = fmt.Errorf("fail") // failNewAddrFunc causes the newAddrFunc to fail. func (r *rbfCloserTestHarness) failNewAddrFunc() { r.T.Helper() r.newAddrErr = errfailAddr } func (r *rbfCloserTestHarness) newAddrFunc() ( lnwire.DeliveryAddress, error) { r.T.Helper() return lnwire.DeliveryAddress{}, r.newAddrErr } func (r *rbfCloserTestHarness) assertExpectations() { r.T.Helper() r.feeEstimator.AssertExpectations(r.T) r.chanObserver.AssertExpectations(r.T) r.daemonAdapters.AssertExpectations(r.T) r.errReporter.AssertExpectations(r.T) r.signer.AssertExpectations(r.T) } func (r *rbfCloserTestHarness) stopAndAssert() { r.T.Helper() defer r.chanCloser.RemoveStateSub(r.stateSub) r.chanCloser.Stop() r.assertExpectations() } func (r *rbfCloserTestHarness) assertStartupAssertions() { r.T.Helper() // When the state machine has started up, we recv a starting state // transition for the initial state. expectedStates := []RbfState{r.startingState} assertStateTransitions(r.T, r.stateSub, expectedStates) // Registering the spend transaction should have been called. r.daemonAdapters.AssertCalled( r.T, "RegisterSpendNtfn", &r.env.ChanPoint, r.pkScript, r.env.Scid.BlockHeight, ) } func (r *rbfCloserTestHarness) assertNoStateTransitions() { select { case newState := <-r.stateSub.NewItemCreated.ChanOut(): r.T.Fatalf("unexpected state transition: %T", newState) case <-time.After(10 * time.Millisecond): } } func (r *rbfCloserTestHarness) assertStateTransitions(states ...RbfState) { assertStateTransitions(r.T, r.stateSub, states) } func (r *rbfCloserTestHarness) currentState() RbfState { state, err := r.chanCloser.CurrentState() require.NoError(r.T, err) return state } type shutdownExpect struct { isInitiator bool allowSend bool recvShutdown bool finalBalances fn.Option[ShutdownBalances] } func (r *rbfCloserTestHarness) expectShutdownEvents(expect shutdownExpect) { r.T.Helper() r.chanObserver.On("FinalBalances").Return(expect.finalBalances) if expect.isInitiator { r.chanObserver.On("NoDanglingUpdates").Return(expect.allowSend) } else { r.chanObserver.On("NoDanglingUpdates").Return( expect.allowSend, ).Maybe() } // When we're receiving a shutdown, we should also disable incoming // adds. if expect.recvShutdown { r.chanObserver.On("DisableIncomingAdds").Return(nil) } // If a close is in progress, this is an RBF iteration, the link has // already been cleaned up, so we don't expect any assertions, other // than to check the closing state. if expect.finalBalances.IsSome() { return } r.chanObserver.On("DisableOutgoingAdds").Return(nil) r.chanObserver.On( "MarkShutdownSent", mock.Anything, expect.isInitiator, ).Return(nil) r.chanObserver.On("DisableChannel").Return(nil) } func (r *rbfCloserTestHarness) expectFinalBalances( b fn.Option[ShutdownBalances]) { r.chanObserver.On("FinalBalances").Return(b) } func (r *rbfCloserTestHarness) expectIncomingAddsDisabled() { r.T.Helper() r.chanObserver.On("DisableIncomingAdds").Return(nil) } type msgMatcher func([]lnwire.Message) bool func singleMsgMatcher[M lnwire.Message](f func(M) bool) msgMatcher { return func(msgs []lnwire.Message) bool { if len(msgs) != 1 { return false } wireMsg := msgs[0] msg, ok := wireMsg.(M) if !ok { return false } if f == nil { return true } return f(msg) } } func (r *rbfCloserTestHarness) expectMsgSent(matcher msgMatcher) { r.T.Helper() if matcher == nil { r.daemonAdapters.On( "SendMessages", r.peerPub, mock.Anything, ).Return(nil) } else { r.daemonAdapters.On( "SendMessages", r.peerPub, mock.MatchedBy(matcher), ).Return(nil) } } func (r *rbfCloserTestHarness) expectFeeEstimate(absoluteFee btcutil.Amount, numTimes int) { r.T.Helper() // TODO(roasbeef): mo assertions for dust case r.feeEstimator.On( "EstimateFee", mock.Anything, mock.Anything, mock.Anything, mock.Anything, ).Return(absoluteFee, nil).Times(numTimes) } func (r *rbfCloserTestHarness) expectFailure(err error) { r.T.Helper() errorMatcher := func(e error) bool { return errors.Is(e, err) } r.errReporter.On("ReportError", mock.MatchedBy(errorMatcher)).Return() } func (r *rbfCloserTestHarness) expectNewCloseSig( localScript, remoteScript []byte, fee btcutil.Amount, closeBalance btcutil.Amount) { r.T.Helper() r.signer.On( "CreateCloseProposal", fee, localScript, remoteScript, mock.Anything, ).Return(&localSig, &localTx, closeBalance, nil) } func (r *rbfCloserTestHarness) waitForMsgSent() { r.T.Helper() err := wait.Predicate(func() bool { return r.daemonAdapters.msgSent.Load() }, time.Second*3) require.NoError(r.T, err) } func (r *rbfCloserTestHarness) expectRemoteCloseFinalized( localCoopSig, remoteCoopSig input.Signature, localScript, remoteScript []byte, fee btcutil.Amount, balanceAfterClose btcutil.Amount, isLocal bool) { r.expectNewCloseSig( localScript, remoteScript, fee, balanceAfterClose, ) r.expectCloseFinalized( localCoopSig, remoteCoopSig, localScript, remoteScript, fee, balanceAfterClose, isLocal, ) r.expectMsgSent(singleMsgMatcher[*lnwire.ClosingSig](nil)) } func (r *rbfCloserTestHarness) expectCloseFinalized( localCoopSig, remoteCoopSig input.Signature, localScript, remoteScript []byte, fee btcutil.Amount, balanceAfterClose btcutil.Amount, isLocal bool) { // The caller should obtain the final signature. r.signer.On("CompleteCooperativeClose", localCoopSig, remoteCoopSig, localScript, remoteScript, fee, mock.Anything, ).Return(closeTx, balanceAfterClose, nil) // The caller should also mark the transaction as broadcast on disk. r.chanObserver.On("MarkCoopBroadcasted", closeTx, isLocal).Return(nil) // Finally, we expect that the daemon executor should broadcast the // above transaction. r.daemonAdapters.On( "BroadcastTransaction", closeTx, mock.Anything, ).Return(nil) } func (r *rbfCloserTestHarness) expectChanPendingClose() { var nilTx *wire.MsgTx r.chanObserver.On("MarkCoopBroadcasted", nilTx, true).Return(nil) } func (r *rbfCloserTestHarness) assertLocalClosePending() { // We should then remain in the outer close negotiation state. r.assertStateTransitions(&ClosingNegotiation{}) // If we examine the final resting state, we should see that the we're // now in the negotiation state still for our local peer state. currentState := assertStateT[*ClosingNegotiation](r) // From this, we'll assert the resulting peer state, and that the co-op // close txn is known. closePendingState, ok := currentState.PeerState.GetForParty( lntypes.Local, ).(*ClosePending) require.True(r.T, ok) require.Equal(r.T, closeTx, closePendingState.CloseTx) } type dustExpectation uint const ( noDustExpect dustExpectation = iota localDustExpect remoteDustExpect ) func (d dustExpectation) String() string { switch d { case noDustExpect: return "no dust" case localDustExpect: return "local dust" case remoteDustExpect: return "remote dust" default: return "unknown" } } // expectHalfSignerIteration asserts that we carry out 1/2 of the locally // initiated signer iteration. This constitutes sending a ClosingComplete // message to the remote party, and all the other intermediate steps. func (r *rbfCloserTestHarness) expectHalfSignerIteration( initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount, dustExpect dustExpectation) { numFeeCalls := 2 // If we're using the SendOfferEvent as a trigger, we only need to call // the few estimation once. if _, ok := initEvent.(*SendOfferEvent); ok { numFeeCalls = 1 } // We'll now expect that fee estimation is called, and then // send in the flushed event. We expect two calls: one to // figure out if we can pay the fee, and then another when we // actually pay the fee. r.expectFeeEstimate(absoluteFee, numFeeCalls) // Next, we'll assert that we receive calls to generate a new // commitment signature, and then send out the commit // ClosingComplete message to the remote peer. r.expectNewCloseSig( localAddr, remoteAddr, absoluteFee, balanceAfterClose, ) // We expect that only the closer and closee sig is set as both // parties have a balance. msgExpect := singleMsgMatcher(func(m *lnwire.ClosingComplete) bool { r.T.Helper() switch { case m.CloserNoClosee.IsSome(): r.T.Logf("closer no closee field set, expected: %v", dustExpect) return dustExpect == remoteDustExpect case m.NoCloserClosee.IsSome(): r.T.Logf("no close closee field set, expected: %v", dustExpect) return dustExpect == localDustExpect default: r.T.Logf("no dust field set, expected: %v", dustExpect) return (m.CloserAndClosee.IsSome() && dustExpect == noDustExpect) } }) r.expectMsgSent(msgExpect) r.chanCloser.SendEvent(initEvent) // Based on the init event, we'll either just go to the closing // negotiation state, or go through the channel flushing state first. var expectedStates []RbfState switch initEvent.(type) { case *ShutdownReceived: expectedStates = []RbfState{ &ChannelFlushing{}, &ClosingNegotiation{}, } case *SendOfferEvent: expectedStates = []RbfState{&ClosingNegotiation{}} case *ChannelFlushed: // If we're sending a flush event here, then this means that we // also have enough balance to cover the fee so we'll have // another inner transition to the negotiation state. expectedStates = []RbfState{ &ClosingNegotiation{}, &ClosingNegotiation{}, } default: r.T.Fatalf("unknown event type: %T", initEvent) } // We should transition from the negotiation state back to // itself. // // TODO(roasbeef): take in expected set of transitions!!! // * or base off of event, if shutdown recv'd know we're doing a full // loop r.assertStateTransitions(expectedStates...) // If we examine the final resting state, we should see that // the we're now in the LocalOffersSent for our local peer // state. currentState := assertStateT[*ClosingNegotiation](r) // From this, we'll assert the resulting peer state. offerSentState, ok := currentState.PeerState.GetForParty( lntypes.Local, ).(*LocalOfferSent) require.True(r.T, ok) // The proposed fee, as well as our local signature should be // properly stashed in the state. require.Equal(r.T, absoluteFee, offerSentState.ProposedFee) require.Equal(r.T, localSigWire, offerSentState.LocalSig) } func (r *rbfCloserTestHarness) assertSingleRbfIteration( initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount, dustExpect dustExpectation) { // We'll now send in the send offer event, which should trigger 1/2 of // the RBF loop, ending us in the LocalOfferSent state. r.expectHalfSignerIteration( initEvent, balanceAfterClose, absoluteFee, noDustExpect, ) // Now that we're in the local offer sent state, we'll send the // response of the remote party, which completes one iteration localSigEvent := &LocalSigReceived{ SigMsg: lnwire.ClosingSig{ ClosingSigs: lnwire.ClosingSigs{ CloserAndClosee: newSigTlv[tlv.TlvType3]( remoteWireSig, ), }, }, } // Before we send the event, we expect the close the final signature to // be combined/obtained, and for the close to finalized on disk. r.expectCloseFinalized( &localSig, &remoteSig, localAddr, remoteAddr, absoluteFee, balanceAfterClose, true, ) r.chanCloser.SendEvent(localSigEvent) // We should transition to the pending closing state now. r.assertLocalClosePending() } func (r *rbfCloserTestHarness) assertSingleRemoteRbfIteration( initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount, sequence uint32, iteration bool) { // If this is an iteration, then we expect some intermediate states, // before we enter the main RBF/sign loop. if iteration { r.expectFeeEstimate(absoluteFee, 1) r.assertStateTransitions( &ChannelActive{}, &ShutdownPending{}, &ChannelFlushing{}, &ClosingNegotiation{}, ) } // When we receive the signature below, our local state machine should // move to finalize the close. r.expectRemoteCloseFinalized( &localSig, &remoteSig, localAddr, remoteAddr, absoluteFee, balanceAfterClose, false, ) r.chanCloser.SendEvent(initEvent) // Our outer state should transition to ClosingNegotiation state. r.assertStateTransitions(&ClosingNegotiation{}) // If we examine the final resting state, we should see that the we're // now in the ClosePending state for the remote peer. currentState := assertStateT[*ClosingNegotiation](r) // From this, we'll assert the resulting peer state. pendingState, ok := currentState.PeerState.GetForParty( lntypes.Remote, ).(*ClosePending) require.True(r.T, ok) // The proposed fee, as well as our local signature should be properly // stashed in the state. require.Equal(r.T, closeTx, pendingState.CloseTx) } func assertStateT[T ProtocolState](h *rbfCloserTestHarness) T { h.T.Helper() currentState, ok := h.currentState().(T) require.True(h.T, ok) return currentState } // newRbfCloserTestHarness creates a new test harness for the RBF closer. func newRbfCloserTestHarness(t *testing.T, cfg *harnessCfg) *rbfCloserTestHarness { startingHeight := 200 chanPoint := randOutPoint(t) chanID := lnwire.NewChanIDFromOutPoint(chanPoint) scid := lnwire.NewShortChanIDFromInt(rand.Uint64()) peerPub := randPubKey(t) msgMapper := NewRbfMsgMapper(uint32(startingHeight), chanID) initialState := cfg.initialState.UnwrapOr(&ChannelActive{}) defaultFeeRate := chainfee.FeePerKwFloor feeEstimator := &mockFeeEstimator{} mockObserver := &mockChanObserver{} errReporter := &mockErrorReporter{} mockSigner := &mockCloseSigner{} harness := &rbfCloserTestHarness{ T: t, cfg: cfg, startingState: initialState, feeEstimator: feeEstimator, signer: mockSigner, chanObserver: mockObserver, errReporter: errReporter, peerPub: *peerPub, } env := Environment{ ChainParams: chaincfg.RegressionNetParams, ChanPeer: *peerPub, ChanPoint: chanPoint, ChanID: chanID, Scid: scid, DefaultFeeRate: defaultFeeRate.FeePerVByte(), ThawHeight: cfg.thawHeight, RemoteUpfrontShutdown: cfg.remoteUpfrontAddr, LocalUpfrontShutdown: cfg.localUpfrontAddr, NewDeliveryScript: harness.newAddrFunc, FeeEstimator: feeEstimator, ChanObserver: mockObserver, CloseSigner: mockSigner, } harness.env = env var pkScript []byte harness.pkScript = pkScript spendEvent := protofsm.RegisterSpend[ProtocolEvent]{ OutPoint: chanPoint, HeightHint: scid.BlockHeight, PkScript: pkScript, PostSpendEvent: fn.Some[RbfSpendMapper](SpendMapper), } daemonAdapters := newDaemonAdapters() harness.daemonAdapters = daemonAdapters protoCfg := RbfChanCloserCfg{ ErrorReporter: errReporter, Daemon: daemonAdapters, InitialState: initialState, Env: &env, InitEvent: fn.Some[protofsm.DaemonEvent](&spendEvent), MsgMapper: fn.Some[protofsm.MsgMapper[ProtocolEvent]]( msgMapper, ), CustomPollInterval: fn.Some(time.Nanosecond), } // Before we start we always expect an initial spend event. daemonAdapters.On( "RegisterSpendNtfn", &chanPoint, pkScript, scid.BlockHeight, ).Return(nil) chanCloser := protofsm.NewStateMachine(protoCfg) chanCloser.Start() harness.stateSub = chanCloser.RegisterStateEvents() harness.chanCloser = &chanCloser return harness } func newCloser(t *testing.T, cfg *harnessCfg) *rbfCloserTestHarness { chanCloser := newRbfCloserTestHarness(t, cfg) // We should start in the active state, and have our spend req // daemon event handled. chanCloser.assertStartupAssertions() return chanCloser } // TestRbfChannelActiveTransitions tests the transitions of from the // ChannelActive state. func TestRbfChannelActiveTransitions(t *testing.T) { localAddr := lnwire.DeliveryAddress(bytes.Repeat([]byte{0x01}, 20)) remoteAddr := lnwire.DeliveryAddress(bytes.Repeat([]byte{0x02}, 20)) feeRate := chainfee.SatPerVByte(1000) // Test that if a spend event is received, the FSM transitions to the // CloseFin terminal state. t.Run("spend_event", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ localUpfrontAddr: fn.Some(localAddr), }) defer closeHarness.stopAndAssert() closeHarness.chanCloser.SendEvent(&SpendEvent{}) closeHarness.assertStateTransitions(&CloseFin{}) }) // If we send in a local shutdown event, but fail to get an addr, the // state machine should terminate. t.Run("local_initiated_close_addr_fail", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{}) defer closeHarness.stopAndAssert() closeHarness.failNewAddrFunc() closeHarness.expectFailure(errfailAddr) // We don't specify an upfront shutdown addr, and don't specify // on here in the vent, so we should call new addr, but then // fail. closeHarness.chanCloser.SendEvent(&SendShutdown{}) // We shouldn't have transitioned to a new state. closeHarness.assertNoStateTransitions() }) // Initiating the shutdown should have us transition to the shutdown // pending state. We should also emit events to disable the channel, // and also send a message to our target peer. t.Run("local_initiated_close_ok", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ localUpfrontAddr: fn.Some(localAddr), }) defer closeHarness.stopAndAssert() // Once we send the event below, we should get calls to the // chan observer, the msg sender, and the link control. closeHarness.expectShutdownEvents(shutdownExpect{ isInitiator: true, allowSend: true, }) closeHarness.expectMsgSent(nil) // If we send the shutdown event, we should transition to the // shutdown pending state. closeHarness.chanCloser.SendEvent(&SendShutdown{ IdealFeeRate: feeRate, }) closeHarness.assertStateTransitions(&ShutdownPending{}) // If we examine the internal state, it should be consistent // with the fee+addr we sent in. currentState := assertStateT[*ShutdownPending](closeHarness) require.Equal( t, feeRate, currentState.IdealFeeRate.UnsafeFromSome(), ) require.Equal( t, localAddr, currentState.ShutdownScripts.LocalDeliveryScript, ) // Wait till the msg has been sent to assert our expectations. // // TODO(roasbeef): can use call.WaitFor here? closeHarness.waitForMsgSent() }) // TODO(roasbeef): thaw height fail // When we receive a shutdown, we should transition to the shutdown // pending state, with the local+remote shutdown addrs known. t.Run("remote_initiated_close_ok", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ localUpfrontAddr: fn.Some(localAddr), }) defer closeHarness.stopAndAssert() // We assert our shutdown events, and also that we eventually // send a shutdown to the remote party. We'll hold back the // send in this case though, as we should only send once the no // updates are dangling. closeHarness.expectShutdownEvents(shutdownExpect{ isInitiator: false, allowSend: false, recvShutdown: true, }) // Next, we'll emit the recv event, with the addr of the remote // party. closeHarness.chanCloser.SendEvent(&ShutdownReceived{ ShutdownScript: remoteAddr, }) // We should transition to the shutdown pending state. closeHarness.assertStateTransitions(&ShutdownPending{}) currentState := assertStateT[*ShutdownPending](closeHarness) // Both the local and remote shutdown scripts should be set. require.Equal( t, localAddr, currentState.ShutdownScripts.LocalDeliveryScript, ) require.Equal( t, remoteAddr, currentState.ShutdownScripts.RemoteDeliveryScript, ) }) // Any other event should be ignored. assertUnknownEventFail(t, &ChannelActive{}) } // TestRbfShutdownPendingTransitions tests the transitions of the RBF closer // once we get to the shutdown pending state. In this state, we wait for either // a shutdown to be received, or a notification that we're able to send a // shutdown ourselves. func TestRbfShutdownPendingTransitions(t *testing.T) { t.Parallel() startingState := &ShutdownPending{} // Test that if a spend event is received, the FSM transitions to the // CloseFin terminal state. t.Run("spend_event", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState]( startingState, ), localUpfrontAddr: fn.Some(localAddr), }) defer closeHarness.stopAndAssert() closeHarness.chanCloser.SendEvent(&SpendEvent{}) closeHarness.assertStateTransitions(&CloseFin{}) }) // If the remote party sends us a diff shutdown addr than we expected, // then we'll fail. t.Run("initiator_shutdown_recv_validate_fail", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState]( startingState, ), remoteUpfrontAddr: fn.Some(remoteAddr), }) defer closeHarness.stopAndAssert() // We should fail as the shutdown script isn't what we // expected. closeHarness.expectFailure(ErrUpfrontShutdownScriptMismatch) // We'll now send in a ShutdownReceived event, but with a // different address provided in the shutdown message. This // should result in an error. closeHarness.chanCloser.SendEvent(&ShutdownReceived{ ShutdownScript: localAddr, }) // We shouldn't have transitioned to a new state. closeHarness.assertNoStateTransitions() }) // Otherwise, if the shutdown is well composed, then we should // transition to the ChannelFlushing state. t.Run("initiator_shutdown_recv_ok", func(t *testing.T) { firstState := *startingState firstState.IdealFeeRate = fn.Some( chainfee.FeePerKwFloor.FeePerVByte(), ) firstState.ShutdownScripts = ShutdownScripts{ LocalDeliveryScript: localAddr, RemoteDeliveryScript: remoteAddr, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState]( &firstState, ), localUpfrontAddr: fn.Some(localAddr), remoteUpfrontAddr: fn.Some(remoteAddr), }) defer closeHarness.stopAndAssert() // We should disable the outgoing adds for the channel at this // point as well. closeHarness.expectFinalBalances(fn.None[ShutdownBalances]()) closeHarness.expectIncomingAddsDisabled() // We'll send in a shutdown received event, with the expected // co-op close addr. closeHarness.chanCloser.SendEvent(&ShutdownReceived{ ShutdownScript: remoteAddr, }) // We should transition to the channel flushing state. closeHarness.assertStateTransitions(&ChannelFlushing{}) // Now we'll ensure that the flushing state has the proper // co-op close state. currentState := assertStateT[*ChannelFlushing](closeHarness) require.Equal(t, localAddr, currentState.LocalDeliveryScript) require.Equal(t, remoteAddr, currentState.RemoteDeliveryScript) require.Equal( t, firstState.IdealFeeRate, currentState.IdealFeeRate, ) }) // If we received the shutdown event, then we'll rely on the external // signal that we were able to send the message once there were no // updates dangling. t.Run("responder_complete", func(t *testing.T) { firstState := *startingState firstState.IdealFeeRate = fn.Some( chainfee.FeePerKwFloor.FeePerVByte(), ) firstState.ShutdownScripts = ShutdownScripts{ LocalDeliveryScript: localAddr, RemoteDeliveryScript: remoteAddr, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState]( &firstState, ), }) defer closeHarness.stopAndAssert() // In this case we're doing the shutdown dance for the first // time, so we'll mark the channel as not being flushed. closeHarness.expectFinalBalances(fn.None[ShutdownBalances]()) // We'll send in a shutdown received event. closeHarness.chanCloser.SendEvent(&ShutdownComplete{}) // We should transition to the channel flushing state. closeHarness.assertStateTransitions(&ChannelFlushing{}) }) // Any other event should be ignored. assertUnknownEventFail(t, startingState) } // TestRbfChannelFlushingTransitions tests the transitions of the RBF closer // for the channel flushing state. Once the coast is clear, we should // transition to the negotiation state. func TestRbfChannelFlushingTransitions(t *testing.T) { t.Parallel() localBalance := lnwire.NewMSatFromSatoshis(10_000) remoteBalance := lnwire.NewMSatFromSatoshis(50_000) absoluteFee := btcutil.Amount(10_100) startingState := &ChannelFlushing{ ShutdownScripts: ShutdownScripts{ LocalDeliveryScript: localAddr, RemoteDeliveryScript: remoteAddr, }, } flushTemplate := &ChannelFlushed{ ShutdownBalances: ShutdownBalances{ LocalBalance: localBalance, RemoteBalance: remoteBalance, }, } // If send in the channel flushed event, but the local party can't pay // for fees, then we should just head to the negotiation state. for _, isFreshFlush := range []bool{true, false} { chanFlushedEvent := *flushTemplate chanFlushedEvent.FreshFlush = isFreshFlush testName := fmt.Sprintf("local_cannot_pay_for_fee/"+ "fresh_flush=%v", isFreshFlush) t.Run(testName, func(t *testing.T) { firstState := *startingState closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState]( &firstState, ), }) defer closeHarness.stopAndAssert() // As part of the set up for this state, we'll have the // final absolute fee required be greater than the // balance of the local party. closeHarness.expectFeeEstimate(absoluteFee, 1) // If this is a fresh flush, then we expect the state // to be marked on disk. if isFreshFlush { closeHarness.expectChanPendingClose() } // We'll now send in the event which should trigger // this code path. closeHarness.chanCloser.SendEvent(&chanFlushedEvent) // With the event sent, we should now transition // straight to the ClosingNegotiation state, with no // further state transitions. closeHarness.assertStateTransitions( &ClosingNegotiation{}, ) }) } for _, isFreshFlush := range []bool{true, false} { flushEvent := *flushTemplate flushEvent.FreshFlush = isFreshFlush // We'll modify the starting balance to be 3x the required fee // to ensure that we can pay for the fee. flushEvent.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll absoluteFee * 3, ) testName := fmt.Sprintf("local_can_pay_for_fee/"+ "fresh_flush=%v", isFreshFlush) // This scenario, we'll have the local party be able to pay for // the fees, which will trigger additional state transitions. t.Run(testName, func(t *testing.T) { firstState := *startingState closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState]( &firstState, ), }) defer closeHarness.stopAndAssert() localBalance := flushEvent.ShutdownBalances.LocalBalance balanceAfterClose := localBalance.ToSatoshis() - absoluteFee //nolint:ll // If this is a fresh flush, then we expect the state // to be marked on disk. if isFreshFlush { closeHarness.expectChanPendingClose() } // From where, we expect the state transition to go // back to closing negotiated, for a ClosingComplete // message to be sent and then for us to terminate at // that state. This is 1/2 of the normal RBF signer // flow. closeHarness.expectHalfSignerIteration( &flushEvent, balanceAfterClose, absoluteFee, noDustExpect, ) }) } // Any other event should be ignored. assertUnknownEventFail(t, startingState) } // TestRbfCloseClosingNegotiationLocal tests the local portion of the primary // RBF close loop. We should be able to transition to a close state, get a sig, // then restart all over again to re-request a signature of at new higher fee // rate. func TestRbfCloseClosingNegotiationLocal(t *testing.T) { t.Parallel() localBalance := lnwire.NewMSatFromSatoshis(40_000) remoteBalance := lnwire.NewMSatFromSatoshis(50_000) absoluteFee := btcutil.Amount(10_100) closeTerms := &CloseChannelTerms{ ShutdownBalances: ShutdownBalances{ LocalBalance: localBalance, RemoteBalance: remoteBalance, }, ShutdownScripts: ShutdownScripts{ LocalDeliveryScript: localAddr, RemoteDeliveryScript: remoteAddr, }, } startingState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: *closeTerms, }, }, } sendOfferEvent := &SendOfferEvent{ TargetFeeRate: chainfee.FeePerKwFloor.FeePerVByte(), } balanceAfterClose := localBalance.ToSatoshis() - absoluteFee // In this state, we'll simulate deciding that we need to send a new // offer to the remote party. t.Run("send_offer_iteration_no_dust", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](startingState), }) defer closeHarness.stopAndAssert() // We'll now send in the initial sender offer event, which // should then trigger a single RBF iteration, ending at the // pending state. closeHarness.assertSingleRbfIteration( sendOfferEvent, balanceAfterClose, absoluteFee, noDustExpect, ) }) // We'll run a similar test as above, but verify that if more than one // sig field is set, we error out. t.Run("send_offer_too_many_sigs_received", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](startingState), }) defer closeHarness.stopAndAssert() // We'll kick off the test as normal, triggering a send offer // event to advance the state machine. closeHarness.expectHalfSignerIteration( sendOfferEvent, balanceAfterClose, absoluteFee, noDustExpect, ) // We'll now craft the local sig received event, but this time // we'll specify 2 signature fields. localSigEvent := &LocalSigReceived{ SigMsg: lnwire.ClosingSig{ ClosingSigs: lnwire.ClosingSigs{ CloserNoClosee: newSigTlv[tlv.TlvType1]( remoteWireSig, ), CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll remoteWireSig, ), }, }, } // We expect that the state machine fails as we received more // than one error. closeHarness.expectFailure(ErrTooManySigs) // We should fail as the remote party sent us more than one // signature. closeHarness.chanCloser.SendEvent(localSigEvent) }) // Next, we'll verify that if the balance of the remote party is dust, // then the proper sig field is set. t.Run("send_offer_iteration_remote_dust", func(t *testing.T) { // We'll modify the starting state to reduce the balance of the // remote party to something that'll be dust. newCloseTerms := *closeTerms newCloseTerms.ShutdownBalances.RemoteBalance = 100 firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: newCloseTerms, }, }, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](firstState), }) defer closeHarness.stopAndAssert() // We'll kick off the half iteration as normal, but this time // we expect that the remote party's output is dust, so the // proper field is set. closeHarness.expectHalfSignerIteration( sendOfferEvent, balanceAfterClose, absoluteFee, remoteDustExpect, ) }) // Similarly, we'll verify that if our final closing balance is dust, // then we send the sig that omits our output. t.Run("send_offer_iteration_local_dust", func(t *testing.T) { firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: *closeTerms, }, }, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](firstState), }) defer closeHarness.stopAndAssert() // We'll kick off the half iteration as normal, but this time // we'll have our balance after close be dust, so we expect // that the local output is dust in the sig we send. dustBalance := btcutil.Amount(100) closeHarness.expectHalfSignerIteration( sendOfferEvent, dustBalance, absoluteFee, localDustExpect, ) }) // In this test, we'll assert that we're able to restart the RBF loop // to trigger additional signature iterations. t.Run("send_offer_rbf_iteration_loop", func(t *testing.T) { firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: *closeTerms, }, }, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](firstState), localUpfrontAddr: fn.Some(localAddr), }) defer closeHarness.stopAndAssert() // We'll start out by first triggering a routine iteration, // assuming we start in this negotiation state. closeHarness.assertSingleRbfIteration( sendOfferEvent, balanceAfterClose, absoluteFee, noDustExpect, ) // Next, we'll send in a new SendShutdown event which simulates // the user requesting a RBF fee bump. We'll use 10x the fee we // used in the last iteration. rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte() * 10 sendShutdown := &SendShutdown{ IdealFeeRate: rbfFeeBump, } // We should send shutdown as normal, but skip some other // checks as we know the close is in progress. closeHarness.expectShutdownEvents(shutdownExpect{ allowSend: true, finalBalances: fn.Some(closeTerms.ShutdownBalances), recvShutdown: true, }) closeHarness.expectMsgSent( singleMsgMatcher[*lnwire.Shutdown](nil), ) closeHarness.chanCloser.SendEvent(sendShutdown) // We should first transition to the Channel Active state // momentarily, before transitioning to the shutdown pending // state. closeHarness.assertStateTransitions( &ChannelActive{}, &ShutdownPending{}, ) // Next, we'll send in the shutdown received event, which // should transition us to the channel flushing state. shutdownEvent := &ShutdownReceived{ ShutdownScript: remoteAddr, } // Now we expect that aanother full RBF iteration takes place // (we initiatea a new local sig). closeHarness.assertSingleRbfIteration( shutdownEvent, balanceAfterClose, absoluteFee, noDustExpect, ) // We should terminate in the negotiation state. closeHarness.assertStateTransitions( &ClosingNegotiation{}, ) }) } // TestRbfCloseClosingNegotiationRemote tests that state machine is able to // handle RBF iterations to sign for the closing transaction of the remote // party. func TestRbfCloseClosingNegotiationRemote(t *testing.T) { t.Parallel() localBalance := lnwire.NewMSatFromSatoshis(40_000) remoteBalance := lnwire.NewMSatFromSatoshis(50_000) absoluteFee := btcutil.Amount(10_100) closeTerms := &CloseChannelTerms{ ShutdownBalances: ShutdownBalances{ LocalBalance: localBalance, RemoteBalance: remoteBalance, }, ShutdownScripts: ShutdownScripts{ LocalDeliveryScript: localAddr, RemoteDeliveryScript: remoteAddr, }, } startingState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: *closeTerms, }, Remote: &RemoteCloseStart{ CloseChannelTerms: *closeTerms, }, }, } balanceAfterClose := remoteBalance.ToSatoshis() - absoluteFee sequence := uint32(mempool.MaxRBFSequence) // This case tests that if we receive a signature from the remote // party, where they can't pay for the fees, we exit. t.Run("recv_offer_cannot_pay_for_fees", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](startingState), }) defer closeHarness.stopAndAssert() // We should fail as they sent a sig, but can't pay for fees. closeHarness.expectFailure(ErrRemoteCannotPay) // We'll send in a new fee proposal, but the proposed fee will // be higher than the remote party's balance. feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ FeeSatoshis: absoluteFee * 10, }, } closeHarness.chanCloser.SendEvent(feeOffer) // We shouldn't have transitioned to a new state. closeHarness.assertNoStateTransitions() }) // If our balance, is dust, then the remote party should send a // signature that doesn't include our output. t.Run("recv_offer_err_closer_no_closee", func(t *testing.T) { // We'll modify our local balance to be dust. closingTerms := *closeTerms closingTerms.ShutdownBalances.LocalBalance = 100 firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: closingTerms, }, Remote: &RemoteCloseStart{ CloseChannelTerms: closingTerms, }, }, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](firstState), }) defer closeHarness.stopAndAssert() // We should fail as they included the wrong sig. closeHarness.expectFailure(ErrCloserNoClosee) // Our balance is dust, so we should reject this signature that // includes our output. feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ FeeSatoshis: absoluteFee, ClosingSigs: lnwire.ClosingSigs{ CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll remoteWireSig, ), }, }, } closeHarness.chanCloser.SendEvent(feeOffer) // We shouldn't have transitioned to a new state. closeHarness.assertNoStateTransitions() }) // If no balances are dust, then they should send a sig covering both // outputs. t.Run("recv_offer_err_closer_and_closee", func(t *testing.T) { closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](startingState), }) defer closeHarness.stopAndAssert() // We should fail as they included the wrong sig. closeHarness.expectFailure(ErrCloserAndClosee) // Both balances are above dust, we should reject this // signature as it excludes an output. feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ FeeSatoshis: absoluteFee, ClosingSigs: lnwire.ClosingSigs{ CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll remoteWireSig, ), }, }, } closeHarness.chanCloser.SendEvent(feeOffer) // We shouldn't have transitioned to a new state. closeHarness.assertNoStateTransitions() }) // If everything lines up, then we should be able to do multiple RBF // loops to enable the remote party to sign.new versions of the co-op // close transaction. t.Run("recv_offer_rbf_loop_iterations", func(t *testing.T) { // We'll modify our s.t we're unable to pay for fees, but // aren't yet dust. closingTerms := *closeTerms closingTerms.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll 9000, ) firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ CloseChannelTerms: closingTerms, }, Remote: &RemoteCloseStart{ CloseChannelTerms: closingTerms, }, }, } closeHarness := newCloser(t, &harnessCfg{ initialState: fn.Some[ProtocolState](firstState), localUpfrontAddr: fn.Some(localAddr), }) defer closeHarness.stopAndAssert() feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ FeeSatoshis: absoluteFee, LockTime: 1, ClosingSigs: lnwire.ClosingSigs{ CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll remoteWireSig, ), }, }, } // As we're already in the negotiation phase, we'll now trigger // a new iteration by having the remote party send a new offer // sig. closeHarness.assertSingleRemoteRbfIteration( feeOffer, balanceAfterClose, absoluteFee, sequence, false, ) // At this point, we've completed a single RBF iteration, and // want to test further iterations, so we'll use a shutdown // even tot kick it all off. // // Before we send the shutdown messages below, we'll mark the // balances as so we fast track to the negotiation state. closeHarness.expectShutdownEvents(shutdownExpect{ allowSend: true, finalBalances: fn.Some(closingTerms.ShutdownBalances), recvShutdown: true, }) closeHarness.expectMsgSent( singleMsgMatcher[*lnwire.Shutdown](nil), ) // We'll now simulate the start of the RBF loop, by receiving a // new Shutdown message from the remote party. This signals // that they want to obtain a new commit sig. closeHarness.chanCloser.SendEvent(&ShutdownReceived{ ShutdownScript: remoteAddr, }) // Next, we'll receive an offer from the remote party, and // drive another RBF iteration. This time, we'll increase the // absolute fee by 1k sats. feeOffer.SigMsg.FeeSatoshis += 1000 absoluteFee = feeOffer.SigMsg.FeeSatoshis closeHarness.assertSingleRemoteRbfIteration( feeOffer, balanceAfterClose, absoluteFee, sequence, true, ) closeHarness.assertNoStateTransitions() }) // TODO(roasbeef): cross sig case? tested isolation, so wolog? }