mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-26 01:33:02 +01:00
protofsm: update state machine w/ new spec flow
In this commit, we implement the latest version of the RBF loop as described in the spec. We remove the self loop back based on sending or receiving shutdown. Instead, from the ClosePending state, we can trigger a new loop by sending SendOfferEvent (we bump), or OfferReceivedEvent (they bump). We also update the rbf state machine w/ the new close addr logic. This log ensures that the remote party always sends our current address, and that if they send a new address, we'll update our view of it, and counter sign the correct transaction. We also add a CloseErr state. With this new state, we can ensure that we're able to properly report errors back to the RPC client, and also optionally force a reconnection or send a warning to the remote party.
This commit is contained in:
parent
14eca4406e
commit
e6d7a1a2ec
@ -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]
|
||||
|
@ -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,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user