diff --git a/lnwallet/chancloser/rbf_coop_states.go b/lnwallet/chancloser/rbf_coop_states.go index 5b8ba8bcc..3850abfc9 100644 --- a/lnwallet/chancloser/rbf_coop_states.go +++ b/lnwallet/chancloser/rbf_coop_states.go @@ -49,6 +49,11 @@ var ( // ErrCloserAndClosee is returned when we expect a sig covering both // outputs, it isn't present. ErrCloserAndClosee = fmt.Errorf("expected CloserAndClosee sig") + + // ErrWrongLocalScript is returned when the remote party sends a + // ClosingComplete message that doesn't carry our last local script + // sent. + ErrWrongLocalScript = fmt.Errorf("wrong local script") ) // ProtocolEvent is a special interface used to create the equivalent of a @@ -382,7 +387,7 @@ type AsymmetricPeerState interface { type ProtocolStates interface { ChannelActive | ShutdownPending | ChannelFlushing | ClosingNegotiation | LocalCloseStart | LocalOfferSent | RemoteCloseStart | - ClosePending | CloseFin + ClosePending | CloseFin | CloseErr } // ChannelActive is the base state for the channel closer state machine. In @@ -523,6 +528,13 @@ type ClosingNegotiation struct { // the ShouldRouteTo method to determine which state route incoming // events to. PeerState lntypes.Dual[AsymmetricPeerState] + + // CloseChannelTerms is the terms we'll use to close the channel. We + // hold a value here which is pointed to by the various + // AsymmetricPeerState instances. This allows us to update this value if + // the remote peer sends a new address, with each of the state noting + // the new value via a pointer. + *CloseChannelTerms } // String returns the name of the state for ClosingNegotiation. @@ -542,6 +554,56 @@ func (c *ClosingNegotiation) IsTerminal() bool { // protocolSealed indicates that this struct is a ProtocolEvent instance. func (c *ClosingNegotiation) protocolStateSealed() {} +// ErrState can be used to introspect into a benign error related to a state +// transition. +type ErrState interface { + sealed() + + error + + // Err returns an error for the ErrState. + Err() error +} + +// ErrStateCantPayForFee is sent when the local party attempts a fee update +// that they can't actually party for. +type ErrStateCantPayForFee struct { + localBalance btcutil.Amount + + attemptedFee btcutil.Amount +} + +// NewErrStateCantPayForFee returns a new NewErrStateCantPayForFee error. +func NewErrStateCantPayForFee(localBalance, attemptedFee btcutil.Amount, +) *ErrStateCantPayForFee { + + return &ErrStateCantPayForFee{ + localBalance: localBalance, + attemptedFee: attemptedFee, + } +} + +// sealed makes this a sealed interface. +func (e *ErrStateCantPayForFee) sealed() { +} + +// Err returns an error for the ErrState. +func (e *ErrStateCantPayForFee) Err() error { + return fmt.Errorf("cannot pay for fee of %v, only have %v local "+ + "balance", e.attemptedFee, e.localBalance) +} + +// Error returns the error string for the ErrState. +func (e *ErrStateCantPayForFee) Error() string { + return e.Err().Error() +} + +// String returns the string for the ErrStateCantPayForFee. +func (e *ErrStateCantPayForFee) String() string { + return fmt.Sprintf("ErrStateCantPayForFee(local_balance=%v, "+ + "attempted_fee=%v)", e.localBalance, e.attemptedFee) +} + // CloseChannelTerms is a set of terms that we'll use to close the channel. This // includes the balances of the channel, and the scripts we'll use to send each // party's funds to. @@ -553,11 +615,11 @@ type CloseChannelTerms struct { // DeriveCloseTxOuts takes the close terms, and returns the local and remote tx // out for the close transaction. If an output is dust, then it'll be nil. -// -// TODO(roasbeef): add func for w/e heuristic to not manifest own output? func (c *CloseChannelTerms) DeriveCloseTxOuts() (*wire.TxOut, *wire.TxOut) { //nolint:ll deriveTxOut := func(balance btcutil.Amount, pkScript []byte) *wire.TxOut { + // We'll base the existence of the output on our normal dust + // check. dustLimit := lnwallet.DustLimitForSize(len(pkScript)) if balance >= dustLimit { return &wire.TxOut{ @@ -618,7 +680,7 @@ func (c *CloseChannelTerms) RemoteCanPayFees(absoluteFee btcutil.Amount) bool { // input events: // - SendOfferEvent type LocalCloseStart struct { - CloseChannelTerms + *CloseChannelTerms } // String returns the name of the state for LocalCloseStart, including proposed @@ -658,7 +720,7 @@ func (l *LocalCloseStart) protocolStateSealed() {} // input events: // - LocalSigReceived type LocalOfferSent struct { - CloseChannelTerms + *CloseChannelTerms // ProposedFee is the fee we proposed to the remote party. ProposedFee btcutil.Amount @@ -710,13 +772,26 @@ type ClosePending struct { // CloseTx is the pending close transaction. CloseTx *wire.MsgTx + *CloseChannelTerms + // FeeRate is the fee rate of the closing transaction. FeeRate chainfee.SatPerVByte + + // Party indicates which party is at this state. This is used to + // implement the state transition properly, based on ShouldRouteTo. + Party lntypes.ChannelParty } // String returns the name of the state for ClosePending. func (c *ClosePending) String() string { - return fmt.Sprintf("ClosePending(txid=%v)", c.CloseTx.TxHash()) + return fmt.Sprintf("ClosePending(txid=%v, party=%v, fee_rate=%v)", + c.CloseTx.TxHash(), c.Party, c.FeeRate) +} + +// isType returns true if the value is of type T. +func isType[T any](value any) bool { + _, ok := value.(T) + return ok } // ShouldRouteTo returns true if the target state should process the target @@ -726,6 +801,17 @@ func (c *ClosePending) ShouldRouteTo(event ProtocolEvent) bool { case *SpendEvent: return true default: + switch { + case c.Party == lntypes.Local && isType[*SendOfferEvent](event): + return true + + case c.Party == lntypes.Remote && isType[*OfferReceivedEvent]( + event, + ): + + return true + } + return false } } @@ -765,7 +851,7 @@ func (c *CloseFin) IsTerminal() bool { // - fromState: ChannelFlushing // - toState: ClosePending type RemoteCloseStart struct { - CloseChannelTerms + *CloseChannelTerms } // String returns the name of the state for RemoteCloseStart. @@ -792,6 +878,54 @@ func (l *RemoteCloseStart) IsTerminal() bool { return false } +// CloseErr is an error state in the protocol. We enter this state when a +// protocol constraint is violated, or an upfront sanity check fails. +type CloseErr struct { + ErrState + + *CloseChannelTerms + + // Party indicates which party is at this state. This is used to + // implement the state transition properly, based on ShouldRouteTo. + Party lntypes.ChannelParty +} + +// String returns the name of the state for CloseErr, including error and party +// details. +func (c *CloseErr) String() string { + return fmt.Sprintf("CloseErr(Party: %v, Error: %v)", c.Party, c.Err()) +} + +// ShouldRouteTo returns true if the target state should process the target +// event. +func (c *CloseErr) ShouldRouteTo(event ProtocolEvent) bool { + switch event.(type) { + case *SpendEvent: + return true + default: + switch { + case c.Party == lntypes.Local && isType[*SendOfferEvent](event): + return true + + case c.Party == lntypes.Remote && isType[*OfferReceivedEvent]( + event, + ): + + return true + } + + return false + } +} + +// protocolStateSealed indicates that this struct is a ProtocolEvent instance. +func (c *CloseErr) protocolStateSealed() {} + +// IsTerminal returns true if the target state is a terminal state. +func (c *CloseErr) IsTerminal() bool { + return true +} + // RbfChanCloser is a state machine that handles the RBF-enabled cooperative // channel close protocol. type RbfChanCloser = protofsm.StateMachine[ProtocolEvent, *Environment] diff --git a/lnwallet/chancloser/rbf_coop_test.go b/lnwallet/chancloser/rbf_coop_test.go index d0bd76472..7456025c3 100644 --- a/lnwallet/chancloser/rbf_coop_test.go +++ b/lnwallet/chancloser/rbf_coop_test.go @@ -110,7 +110,10 @@ func assertStateTransitions[Event any, Env protofsm.Environment]( t.Helper() for _, expectedState := range expectedStates { - newState := <-stateSub.NewItemCreated.ChanOut() + newState, err := fn.RecvOrTimeout( + stateSub.NewItemCreated.ChanOut(), 10*time.Millisecond, + ) + require.NoError(t, err, "expected state: %T", expectedState) require.IsType(t, expectedState, newState) } @@ -598,6 +601,8 @@ func (r *rbfCloserTestHarness) assertSingleRbfIteration( // response of the remote party, which completes one iteration localSigEvent := &LocalSigReceived{ SigMsg: lnwire.ClosingSig{ + CloserScript: localAddr, + CloseeScript: remoteAddr, ClosingSigs: lnwire.ClosingSigs{ CloserAndClosee: newSigTlv[tlv.TlvType3]( remoteWireSig, @@ -620,26 +625,16 @@ func (r *rbfCloserTestHarness) assertSingleRbfIteration( } func (r *rbfCloserTestHarness) assertSingleRemoteRbfIteration( - initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount, - sequence uint32, iteration bool) { + initEvent *OfferReceivedEvent, balanceAfterClose, + absoluteFee btcutil.Amount, sequence uint32, iteration bool) { ctx := context.Background() - // 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, + &localSig, &remoteSig, initEvent.SigMsg.CloseeScript, + initEvent.SigMsg.CloserScript, absoluteFee, balanceAfterClose, false, ) @@ -648,6 +643,13 @@ func (r *rbfCloserTestHarness) assertSingleRemoteRbfIteration( // Our outer state should transition to ClosingNegotiation state. r.assertStateTransitions(&ClosingNegotiation{}) + // If this is an iteration, then we'll go from ClosePending -> + // RemoteCloseStart -> ClosePending. So we'll assert an extra transition + // here. + if iteration { + 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) @@ -1184,9 +1186,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { startingState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: *closeTerms, + CloseChannelTerms: closeTerms, }, }, + CloseChannelTerms: closeTerms, } sendOfferEvent := &SendOfferEvent{ @@ -1195,6 +1198,8 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { balanceAfterClose := localBalance.ToSatoshis() - absoluteFee + // TODO(roasbeef): add test case for error state validation, then resume + // 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) { @@ -1231,6 +1236,8 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { // we'll specify 2 signature fields. localSigEvent := &LocalSigReceived{ SigMsg: lnwire.ClosingSig{ + CloserScript: localAddr, + CloseeScript: remoteAddr, ClosingSigs: lnwire.ClosingSigs{ CloserNoClosee: newSigTlv[tlv.TlvType1]( remoteWireSig, @@ -1261,9 +1268,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: newCloseTerms, + CloseChannelTerms: &newCloseTerms, }, }, + CloseChannelTerms: &newCloseTerms, } closeHarness := newCloser(t, &harnessCfg{ @@ -1286,9 +1294,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: *closeTerms, + CloseChannelTerms: closeTerms, }, }, + CloseChannelTerms: closeTerms, } closeHarness := newCloser(t, &harnessCfg{ @@ -1306,15 +1315,55 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { ) }) + // 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_wrong_local_script", func(t *testing.T) { + firstState := &ClosingNegotiation{ + PeerState: lntypes.Dual[AsymmetricPeerState]{ + Local: &LocalCloseStart{ + CloseChannelTerms: closeTerms, + }, + }, + CloseChannelTerms: closeTerms, + } + + closeHarness := newCloser(t, &harnessCfg{ + initialState: fn.Some[ProtocolState](firstState), + localUpfrontAddr: fn.Some(localAddr), + }) + defer closeHarness.stopAndAssert() + + // The remote party will send a ClosingSig message, but with the + // wrong local script. We should expect an error. + closeHarness.expectFailure(ErrWrongLocalScript) + + // We'll send this message in directly, as we shouldn't get any + // further in the process. + // assuming we start in this negotiation state. + localSigEvent := &LocalSigReceived{ + SigMsg: lnwire.ClosingSig{ + CloserScript: remoteAddr, + CloseeScript: remoteAddr, + ClosingSigs: lnwire.ClosingSigs{ + CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll + remoteWireSig, + ), + }, + }, + } + closeHarness.chanCloser.SendEvent(ctx, localSigEvent) + }) + // 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, + CloseChannelTerms: closeTerms, }, }, + CloseChannelTerms: closeTerms, } closeHarness := newCloser(t, &harnessCfg{ @@ -1330,44 +1379,18 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) { 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. + // Next, we'll send in a new SendOfferEvent 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, + localOffer := &SendOfferEvent{ + TargetFeeRate: 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(ctx, 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). + // Now we expect that another full RBF iteration takes place (we + // initiate a new local sig). closeHarness.assertSingleRbfIteration( - shutdownEvent, balanceAfterClose, absoluteFee, + localOffer, balanceAfterClose, absoluteFee, noDustExpect, ) @@ -1403,12 +1426,13 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { startingState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: *closeTerms, + CloseChannelTerms: closeTerms, }, Remote: &RemoteCloseStart{ - CloseChannelTerms: *closeTerms, + CloseChannelTerms: closeTerms, }, }, + CloseChannelTerms: closeTerms, } balanceAfterClose := remoteBalance.ToSatoshis() - absoluteFee @@ -1430,7 +1454,9 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { // be higher than the remote party's balance. feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ - FeeSatoshis: absoluteFee * 10, + CloserScript: remoteAddr, + CloseeScript: localAddr, + FeeSatoshis: absoluteFee * 10, }, } closeHarness.chanCloser.SendEvent(ctx, feeOffer) @@ -1449,12 +1475,13 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: closingTerms, + CloseChannelTerms: &closingTerms, }, Remote: &RemoteCloseStart{ - CloseChannelTerms: closingTerms, + CloseChannelTerms: &closingTerms, }, }, + CloseChannelTerms: &closingTerms, } closeHarness := newCloser(t, &harnessCfg{ @@ -1469,7 +1496,9 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { // includes our output. feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ - FeeSatoshis: absoluteFee, + FeeSatoshis: absoluteFee, + CloserScript: remoteAddr, + CloseeScript: localAddr, ClosingSigs: lnwire.ClosingSigs{ CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll remoteWireSig, @@ -1498,7 +1527,9 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { // signature as it excludes an output. feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ - FeeSatoshis: absoluteFee, + FeeSatoshis: absoluteFee, + CloserScript: remoteAddr, + CloseeScript: localAddr, ClosingSigs: lnwire.ClosingSigs{ CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll remoteWireSig, @@ -1516,8 +1547,8 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { // 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. + // We'll modify our balance s.t we're unable to pay for fees, + // but aren't yet dust. closingTerms := *closeTerms closingTerms.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll 9000, @@ -1526,12 +1557,13 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { firstState := &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: closingTerms, + CloseChannelTerms: &closingTerms, }, Remote: &RemoteCloseStart{ - CloseChannelTerms: closingTerms, + CloseChannelTerms: &closingTerms, }, }, + CloseChannelTerms: &closingTerms, } closeHarness := newCloser(t, &harnessCfg{ @@ -1542,8 +1574,10 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { feeOffer := &OfferReceivedEvent{ SigMsg: lnwire.ClosingComplete{ - FeeSatoshis: absoluteFee, - LockTime: 1, + CloserScript: remoteAddr, + CloseeScript: localAddr, + FeeSatoshis: absoluteFee, + LockTime: 1, ClosingSigs: lnwire.ClosingSigs{ CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll remoteWireSig, @@ -1560,31 +1594,9 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { 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( - ctx, &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. + // 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( @@ -1595,5 +1607,85 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) { closeHarness.assertNoStateTransitions() }) - // TODO(roasbeef): cross sig case? tested isolation, so wolog? + t.Run("recv_offer_wrong_local_script", func(t *testing.T) { + closeHarness := newCloser(t, &harnessCfg{ + initialState: fn.Some[ProtocolState](startingState), + }) + defer closeHarness.stopAndAssert() + + // The remote party will send a ClosingComplete message, but + // with the wrong local script. We should expect an error. + closeHarness.expectFailure(ErrWrongLocalScript) + + // We'll send our remote addr as the Closee script, which should + // trigger an error. + feeOffer := &OfferReceivedEvent{ + SigMsg: lnwire.ClosingComplete{ + FeeSatoshis: absoluteFee, + CloserScript: remoteAddr, + CloseeScript: remoteAddr, + ClosingSigs: lnwire.ClosingSigs{ + CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll + remoteWireSig, + ), + }, + }, + } + closeHarness.chanCloser.SendEvent(ctx, feeOffer) + + // We shouldn't have transitioned to a new state. + closeHarness.assertNoStateTransitions() + }) + + t.Run("recv_offer_remote_addr_change", func(t *testing.T) { + closingTerms := *closeTerms + + firstState := &ClosingNegotiation{ + PeerState: lntypes.Dual[AsymmetricPeerState]{ + Local: &LocalCloseStart{ + CloseChannelTerms: &closingTerms, + }, + Remote: &RemoteCloseStart{ + CloseChannelTerms: &closingTerms, + }, + }, + CloseChannelTerms: &closingTerms, + } + + closeHarness := newCloser(t, &harnessCfg{ + initialState: fn.Some[ProtocolState](firstState), + localUpfrontAddr: fn.Some(localAddr), + }) + defer closeHarness.stopAndAssert() + + // This time, the close request sent by the remote party will + // modify their normal remote address. This should cause us to + // recognize this, and counter sign the proper co-op close + // transaction. + newRemoteAddr := lnwire.DeliveryAddress(append( + []byte{txscript.OP_1, txscript.OP_DATA_32}, + bytes.Repeat([]byte{0x03}, 32)..., + )) + feeOffer := &OfferReceivedEvent{ + SigMsg: lnwire.ClosingComplete{ + CloserScript: newRemoteAddr, + CloseeScript: localAddr, + 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, + ) + }) } diff --git a/lnwallet/chancloser/rbf_coop_transitions.go b/lnwallet/chancloser/rbf_coop_transitions.go index 01ab5c32f..452386825 100644 --- a/lnwallet/chancloser/rbf_coop_transitions.go +++ b/lnwallet/chancloser/rbf_coop_transitions.go @@ -1,6 +1,7 @@ package chancloser import ( + "bytes" "fmt" "github.com/btcsuite/btcd/btcec/v2" @@ -426,9 +427,6 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment, c.EarlyRemoteOffer = fn.Some(*msg) - // TODO(roasbeef): unit test! - // * actually do this ^ - // We'll perform a noop update so we can wait for the actual // channel flushed event. return &CloseStateTransition{ @@ -469,8 +467,6 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment, // We'll then use that fee rate to determine the absolute fee // we'd propose. - // - // TODO(roasbeef): need to sign the 3 diff versions of this? localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts() absoluteFee := env.FeeEstimator.EstimateFee( env.ChanType, localTxOut, remoteTxOut, @@ -522,12 +518,13 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment, NextState: &ClosingNegotiation{ PeerState: lntypes.Dual[AsymmetricPeerState]{ Local: &LocalCloseStart{ - CloseChannelTerms: closeTerms, + CloseChannelTerms: &closeTerms, }, Remote: &RemoteCloseStart{ - CloseChannelTerms: closeTerms, + CloseChannelTerms: &closeTerms, }, }, + CloseChannelTerms: &closeTerms, }, NewEvents: newEvents, }, nil @@ -571,6 +568,63 @@ func processNegotiateEvent(c *ClosingNegotiation, event ProtocolEvent, }, nil } +// updateAndValidateCloseTerms is a helper function that validates examines the +// incoming event, and decide if we need to update the remote party's address, +// or reject it if it doesn't include our latest address. +func (c *ClosingNegotiation) updateAndValidateCloseTerms(event ProtocolEvent, +) error { + + assertLocalScriptMatches := func(localScriptInMsg []byte) error { + if !bytes.Equal( + c.LocalDeliveryScript, localScriptInMsg, + ) { + + return fmt.Errorf("%w: remote party sent wrong "+ + "script, expected %x, got %x", + ErrWrongLocalScript, c.LocalDeliveryScript, + localScriptInMsg, + ) + } + + return nil + } + + switch msg := event.(type) { + // The remote party is sending us a new request to counter sign their + // version of the commitment transaction. + case *OfferReceivedEvent: + // Make sure that they're sending our local script, and not + // something else. + err := assertLocalScriptMatches(msg.SigMsg.CloseeScript) + if err != nil { + return err + } + + oldRemoteAddr := c.RemoteDeliveryScript + newRemoteAddr := msg.SigMsg.CloserScript + + // If they're sending a new script, then we'll update to the new + // one. + if !bytes.Equal(oldRemoteAddr, newRemoteAddr) { + c.RemoteDeliveryScript = newRemoteAddr + } + + // The remote party responded to our sig request with a signature for + // our version of the commitment transaction. + case *LocalSigReceived: + // Make sure that they're sending our local script, and not + // something else. + err := assertLocalScriptMatches(msg.SigMsg.CloserScript) + if err != nil { + return err + } + + return nil + } + + return nil +} + // ProcessEvent drives forward the composite states for the local and remote // party in response to new events. From this state, we'll continue to drive // forward the local and remote states until we arrive at the StateFin stage, @@ -597,22 +651,13 @@ func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment, ConfirmedTx: msg.Tx, }, }, nil + } - // Otherwise, if we receive a shutdown, or receive an event to send a - // shutdown, then we'll go back up to the ChannelActive state, and have - // it handle this event by emitting an internal event. - // - // TODO(roasbeef): both will have fee rate specified, so ok? - case *ShutdownReceived, *SendShutdown: - chancloserLog.Infof("ChannelPoint(%v): RBF case triggered, "+ - "restarting negotiation", env.ChanPoint) - - return &CloseStateTransition{ - NextState: &ChannelActive{}, - NewEvents: fn.Some(RbfEvent{ - InternalEvent: []ProtocolEvent{event}, - }), - }, nil + // At this point, we know its a new signature message. We'll validate, + // and maybe update the set of close terms based on what we receive. We + // might update the remote party's address for example. + if err := c.updateAndValidateCloseTerms(event); err != nil { + return nil, fmt.Errorf("event violates close terms: %w", err) } // If we get to this point, then we have an event that'll drive forward @@ -628,14 +673,14 @@ func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment, case c.PeerState.GetForParty(lntypes.Remote).ShouldRouteTo(event): chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+ - "chan state", env.ChanPoint, event) + "chan state", env.ChanPoint, event) // Drive forward the remote state based on the next event. return processNegotiateEvent(c, event, env, lntypes.Remote) } - return nil, fmt.Errorf("%w: received %T while in ClosingNegotiation", - ErrInvalidStateTransition, event) + return nil, fmt.Errorf("%w: received %T while in %v", + ErrInvalidStateTransition, event, c) } // newSigTlv is a helper function that returns a new optional TLV sig field for @@ -654,14 +699,34 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment, // rate to generate for the closing transaction with our ideal fee // rate. case *SendOfferEvent: - // First, we'll figure out the absolute fee rate we should pay // given the state of the local/remote outputs. + // First, we'll figure out the absolute fee rate we should pay localTxOut, remoteTxOut := l.DeriveCloseTxOuts() absoluteFee := env.FeeEstimator.EstimateFee( env.ChanType, localTxOut, remoteTxOut, msg.TargetFeeRate.FeePerKWeight(), ) + // If we can't actually pay for fees here, then we'll just do a + // noop back to the same state to await a new fee rate. + if !l.LocalCanPayFees(absoluteFee) { + chancloserLog.Infof("ChannelPoint(%v): unable to pay "+ + "fee=%v with local balance %v, skipping "+ + "closing_complete", env.ChanPoint, absoluteFee, + l.LocalBalance) + + return &CloseStateTransition{ + NextState: &CloseErr{ + CloseChannelTerms: l.CloseChannelTerms, + Party: lntypes.Local, + ErrState: NewErrStateCantPayForFee( + l.LocalBalance.ToSatoshis(), + absoluteFee, + ), + }, + }, nil + } + // Now that we know what fee we want to pay, we'll create a new // signature over our co-op close transaction. For our // proposals, we'll just always use the known RBF sequence @@ -848,8 +913,10 @@ func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment, return &CloseStateTransition{ NextState: &ClosePending{ - CloseTx: closeTx, - FeeRate: l.ProposedFeeRate, + CloseTx: closeTx, + FeeRate: l.ProposedFeeRate, + CloseChannelTerms: l.CloseChannelTerms, + Party: lntypes.Local, }, NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{ ExternalEvents: broadcastEvent, @@ -1022,8 +1089,10 @@ func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment, // the next state where we'll sign+broadcast the sig. return &CloseStateTransition{ NextState: &ClosePending{ - CloseTx: closeTx, - FeeRate: feeRate, + CloseTx: closeTx, + FeeRate: feeRate, + CloseChannelTerms: l.CloseChannelTerms, + Party: lntypes.Remote, }, NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{ ExternalEvents: daemonEvents, @@ -1051,6 +1120,32 @@ func (c *ClosePending) ProcessEvent(event ProtocolEvent, env *Environment, }, }, nil + // If we get a send offer event in this state, then we're doing a state + // transition to the LocalCloseStart state, so we can sign a new closing + // tx. + case *SendOfferEvent: + return &CloseStateTransition{ + NextState: &LocalCloseStart{ + CloseChannelTerms: c.CloseChannelTerms, + }, + NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{ + InternalEvent: []ProtocolEvent{msg}, + }), + }, nil + + // If we get an offer received event, then we're doing a state + // transition to the RemoteCloseStart, as the remote peer wants to sign + // a new closing tx. + case *OfferReceivedEvent: + return &CloseStateTransition{ + NextState: &RemoteCloseStart{ + CloseChannelTerms: c.CloseChannelTerms, + }, + NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{ + InternalEvent: []ProtocolEvent{msg}, + }), + }, nil + default: return &CloseStateTransition{ @@ -1068,3 +1163,44 @@ func (c *CloseFin) ProcessEvent(event ProtocolEvent, env *Environment, NextState: c, }, nil } + +// ProcessEvent is a semi-terminal state in the rbf-coop close state machine. +// In this state, we hit a validation error in an earlier state, so we'll remain +// in this state for the user to examine. We may also process new requests to +// continue the state machine. +func (c *CloseErr) ProcessEvent(event ProtocolEvent, env *Environment, +) (*CloseStateTransition, error) { + + switch msg := event.(type) { + // If we get a send offer event in this state, then we're doing a state + // transition to the LocalCloseStart state, so we can sign a new closing + // tx. + case *SendOfferEvent: + return &CloseStateTransition{ + NextState: &LocalCloseStart{ + CloseChannelTerms: c.CloseChannelTerms, + }, + NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{ + InternalEvent: []ProtocolEvent{msg}, + }), + }, nil + + // If we get an offer received event, then we're doing a state + // transition to the RemoteCloseStart, as the remote peer wants to sign + // a new closing tx. + case *OfferReceivedEvent: + return &CloseStateTransition{ + NextState: &RemoteCloseStart{ + CloseChannelTerms: c.CloseChannelTerms, + }, + NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{ + InternalEvent: []ProtocolEvent{msg}, + }), + }, nil + + default: + return &CloseStateTransition{ + NextState: c, + }, nil + } +}