mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-18 05:42:09 +01:00
Merge pull request #9568 from Roasbeef/rbf-staging-state-machine
lnwallet: update RBF state machine w/ latest spec guidelines
This commit is contained in:
commit
ac53f55e49
@ -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
|
||||
@ -449,6 +454,11 @@ type ShutdownPending struct {
|
||||
// IdealFeeRate is the ideal fee rate we'd like to use for the closing
|
||||
// attempt.
|
||||
IdealFeeRate fn.Option[chainfee.SatPerVByte]
|
||||
|
||||
// EarlyRemoteOffer is the offer we received from the remote party
|
||||
// before we received their shutdown message. We'll stash it to process
|
||||
// later.
|
||||
EarlyRemoteOffer fn.Option[OfferReceivedEvent]
|
||||
}
|
||||
|
||||
// String returns the name of the state for ShutdownPending.
|
||||
@ -523,6 +533,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 +559,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 +620,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 +685,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,11 +725,14 @@ 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
|
||||
|
||||
// ProposedFeeRate is the fee rate we proposed to the remote party.
|
||||
ProposedFeeRate chainfee.SatPerVByte
|
||||
|
||||
// LocalSig is the signature we sent to the remote party.
|
||||
LocalSig lnwire.Sig
|
||||
}
|
||||
@ -706,11 +776,27 @@ func (l *LocalOfferSent) IsTerminal() bool {
|
||||
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
|
||||
@ -720,6 +806,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
|
||||
}
|
||||
}
|
||||
@ -759,7 +856,7 @@ func (c *CloseFin) IsTerminal() bool {
|
||||
// - fromState: ChannelFlushing
|
||||
// - toState: ClosePending
|
||||
type RemoteCloseStart struct {
|
||||
CloseChannelTerms
|
||||
*CloseChannelTerms
|
||||
}
|
||||
|
||||
// String returns the name of the state for RemoteCloseStart.
|
||||
@ -786,6 +883,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)
|
||||
}
|
||||
@ -154,6 +157,27 @@ func assertUnknownEventFail(t *testing.T, startingState ProtocolState) {
|
||||
})
|
||||
}
|
||||
|
||||
// assertSpendEventCloseFin asserts that the state machine transitions to the
|
||||
// CloseFin state when a spend event is received.
|
||||
func assertSpendEventCloseFin(t *testing.T, startingState ProtocolState) {
|
||||
t.Helper()
|
||||
|
||||
// If a spend event is received, the state machine should transition to
|
||||
// the CloseFin state.
|
||||
t.Run("spend_event", func(t *testing.T) {
|
||||
closeHarness := newCloser(t, &harnessCfg{
|
||||
initialState: fn.Some(startingState),
|
||||
})
|
||||
defer closeHarness.stopAndAssert()
|
||||
|
||||
closeHarness.chanCloser.SendEvent(
|
||||
context.Background(), &SpendEvent{},
|
||||
)
|
||||
|
||||
closeHarness.assertStateTransitions(&CloseFin{})
|
||||
})
|
||||
}
|
||||
|
||||
type harnessCfg struct {
|
||||
initialState fn.Option[ProtocolState]
|
||||
|
||||
@ -248,6 +272,8 @@ func (r *rbfCloserTestHarness) assertNoStateTransitions() {
|
||||
}
|
||||
|
||||
func (r *rbfCloserTestHarness) assertStateTransitions(states ...RbfState) {
|
||||
r.T.Helper()
|
||||
|
||||
assertStateTransitions(r.T, r.stateSub, states)
|
||||
}
|
||||
|
||||
@ -598,6 +624,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,34 +648,34 @@ 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,
|
||||
sendInit 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,
|
||||
)
|
||||
|
||||
r.chanCloser.SendEvent(ctx, initEvent)
|
||||
if sendInit {
|
||||
r.chanCloser.SendEvent(ctx, initEvent)
|
||||
}
|
||||
|
||||
// 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)
|
||||
@ -858,7 +886,28 @@ func TestRbfChannelActiveTransitions(t *testing.T) {
|
||||
closeHarness.waitForMsgSent()
|
||||
})
|
||||
|
||||
// TODO(roasbeef): thaw height fail
|
||||
// If the remote party attempts to close, and a thaw height is active,
|
||||
// but not yet met, then we should fail.
|
||||
t.Run("remote_initiated_thaw_height_close_fail", func(t *testing.T) {
|
||||
closeHarness := newCloser(t, &harnessCfg{
|
||||
localUpfrontAddr: fn.Some(localAddr),
|
||||
thawHeight: fn.Some(uint32(100000)),
|
||||
})
|
||||
defer closeHarness.stopAndAssert()
|
||||
|
||||
// Next, we'll emit the recv event, with the addr of the remote
|
||||
// party.
|
||||
closeHarness.chanCloser.SendEvent(
|
||||
ctx, &ShutdownReceived{
|
||||
ShutdownScript: remoteAddr,
|
||||
BlockHeight: 1,
|
||||
},
|
||||
)
|
||||
|
||||
// We expect a failure as the block height is less than the
|
||||
// start height.
|
||||
closeHarness.expectFailure(ErrThawHeightNotReached)
|
||||
})
|
||||
|
||||
// When we receive a shutdown, we should transition to the shutdown
|
||||
// pending state, with the local+remote shutdown addrs known.
|
||||
@ -902,6 +951,9 @@ func TestRbfChannelActiveTransitions(t *testing.T) {
|
||||
|
||||
// Any other event should be ignored.
|
||||
assertUnknownEventFail(t, &ChannelActive{})
|
||||
|
||||
// Sending a Spend event should transition to CloseFin.
|
||||
assertSpendEventCloseFin(t, &ChannelActive{})
|
||||
}
|
||||
|
||||
// TestRbfShutdownPendingTransitions tests the transitions of the RBF closer
|
||||
@ -1033,8 +1085,106 @@ func TestRbfShutdownPendingTransitions(t *testing.T) {
|
||||
closeHarness.assertStateTransitions(&ChannelFlushing{})
|
||||
})
|
||||
|
||||
// If we an early offer from the remote party, then we should stash
|
||||
// that, transition to the channel flushing state. Once there, another
|
||||
// self transition should emit the stashed offer.
|
||||
t.Run("early_remote_offer_shutdown_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]())
|
||||
|
||||
// Before we send the shutdown complete event, we'll send in an
|
||||
// early offer from the remote party.
|
||||
closeHarness.chanCloser.SendEvent(ctx, &OfferReceivedEvent{})
|
||||
|
||||
// This will cause a self transition back to ShutdownPending.
|
||||
closeHarness.assertStateTransitions(&ShutdownPending{})
|
||||
|
||||
// Next, we'll send in a shutdown complete event.
|
||||
closeHarness.chanCloser.SendEvent(ctx, &ShutdownComplete{})
|
||||
|
||||
// We should transition to the channel flushing state, then the
|
||||
// self event to have this state cache he early offer should
|
||||
// follow.
|
||||
closeHarness.assertStateTransitions(
|
||||
&ChannelFlushing{}, &ChannelFlushing{},
|
||||
)
|
||||
|
||||
// If we get the current state, we should see that the offer is
|
||||
// cached.
|
||||
currentState := assertStateT[*ChannelFlushing](closeHarness)
|
||||
require.NotNil(t, currentState.EarlyRemoteOffer)
|
||||
})
|
||||
|
||||
// If we an early offer from the remote party, then we should stash
|
||||
// that, transition to the channel flushing state. Once there, another
|
||||
// self transition should emit the stashed offer.
|
||||
t.Run("early_remote_offer_shutdown_received", 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]())
|
||||
closeHarness.expectIncomingAddsDisabled()
|
||||
|
||||
// Before we send the shutdown complete event, we'll send in an
|
||||
// early offer from the remote party.
|
||||
closeHarness.chanCloser.SendEvent(ctx, &OfferReceivedEvent{})
|
||||
|
||||
// This will cause a self transition back to ShutdownPending.
|
||||
closeHarness.assertStateTransitions(&ShutdownPending{})
|
||||
|
||||
// Next, we'll send in a shutdown complete event.
|
||||
closeHarness.chanCloser.SendEvent(ctx, &ShutdownReceived{})
|
||||
|
||||
// We should transition to the channel flushing state, then the
|
||||
// self event to have this state cache he early offer should
|
||||
// follow.
|
||||
closeHarness.assertStateTransitions(
|
||||
&ChannelFlushing{}, &ChannelFlushing{},
|
||||
)
|
||||
|
||||
// If we get the current state, we should see that the offer is
|
||||
// cached.
|
||||
currentState := assertStateT[*ChannelFlushing](closeHarness)
|
||||
require.NotNil(t, currentState.EarlyRemoteOffer)
|
||||
})
|
||||
|
||||
// Any other event should be ignored.
|
||||
assertUnknownEventFail(t, startingState)
|
||||
|
||||
// Sending a Spend event should transition to CloseFin.
|
||||
assertSpendEventCloseFin(t, startingState)
|
||||
}
|
||||
|
||||
// TestRbfChannelFlushingTransitions tests the transitions of the RBF closer
|
||||
@ -1142,7 +1292,7 @@ func TestRbfChannelFlushingTransitions(t *testing.T) {
|
||||
closeHarness.expectChanPendingClose()
|
||||
}
|
||||
|
||||
// From where, we expect the state transition to go
|
||||
// From here, 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
|
||||
@ -1154,8 +1304,65 @@ func TestRbfChannelFlushingTransitions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// This tests that if we receive an `OfferReceivedEvent` while in the
|
||||
// flushing state, then we'll cache that, and once we receive
|
||||
// ChannelFlushed, we'll emit an internal `OfferReceivedEvent` in the
|
||||
// negotiation state.
|
||||
t.Run("early_offer", func(t *testing.T) {
|
||||
firstState := *startingState
|
||||
|
||||
closeHarness := newCloser(t, &harnessCfg{
|
||||
initialState: fn.Some[ProtocolState](
|
||||
&firstState,
|
||||
),
|
||||
})
|
||||
defer closeHarness.stopAndAssert()
|
||||
|
||||
flushEvent := *flushTemplate
|
||||
|
||||
// Set up the fee estimate s.t the local party doesn't have
|
||||
// balance to close.
|
||||
closeHarness.expectFeeEstimate(absoluteFee, 1)
|
||||
|
||||
// First, we'll emit an `OfferReceivedEvent` to simulate an
|
||||
// early offer (async network, they determine the channel is
|
||||
// "flushed" before we do, and send their offer over).
|
||||
remoteOffer := &OfferReceivedEvent{
|
||||
SigMsg: lnwire.ClosingComplete{
|
||||
FeeSatoshis: absoluteFee,
|
||||
CloserScript: remoteAddr,
|
||||
CloseeScript: localAddr,
|
||||
ClosingSigs: lnwire.ClosingSigs{
|
||||
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||
remoteWireSig,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
closeHarness.chanCloser.SendEvent(ctx, remoteOffer)
|
||||
|
||||
// We should do a self transition, and still be in the
|
||||
// ChannelFlushing state.
|
||||
closeHarness.assertStateTransitions(&ChannelFlushing{})
|
||||
|
||||
sequence := uint32(mempool.MaxRBFSequence)
|
||||
|
||||
// Now we'll send in the channel flushed event, and assert that
|
||||
// this triggers a remote RBF iteration (we process their early
|
||||
// offer and send our sig).
|
||||
closeHarness.chanCloser.SendEvent(ctx, &flushEvent)
|
||||
closeHarness.assertSingleRemoteRbfIteration(
|
||||
remoteOffer, absoluteFee, absoluteFee, sequence, false,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
// Any other event should be ignored.
|
||||
assertUnknownEventFail(t, startingState)
|
||||
|
||||
// Sending a Spend event should transition to CloseFin.
|
||||
assertSpendEventCloseFin(t, startingState)
|
||||
}
|
||||
|
||||
// TestRbfCloseClosingNegotiationLocal tests the local portion of the primary
|
||||
@ -1184,9 +1391,10 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
||||
startingState := &ClosingNegotiation{
|
||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||
Local: &LocalCloseStart{
|
||||
CloseChannelTerms: *closeTerms,
|
||||
CloseChannelTerms: closeTerms,
|
||||
},
|
||||
},
|
||||
CloseChannelTerms: closeTerms,
|
||||
}
|
||||
|
||||
sendOfferEvent := &SendOfferEvent{
|
||||
@ -1195,6 +1403,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 +1441,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 +1473,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 +1499,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 +1520,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 +1584,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,
|
||||
)
|
||||
|
||||
@ -1376,6 +1604,59 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
||||
&ClosingNegotiation{},
|
||||
)
|
||||
})
|
||||
|
||||
// Make sure that we'll go to the error state if we try to try a close
|
||||
// that we can't pay for.
|
||||
t.Run("send_offer_cannot_pay_for_fees", 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()
|
||||
|
||||
// We'll prep to return an absolute fee that's much higher than
|
||||
// the amount we have in the channel.
|
||||
closeHarness.expectFeeEstimate(btcutil.SatoshiPerBitcoin, 1)
|
||||
|
||||
rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte()
|
||||
localOffer := &SendOfferEvent{
|
||||
TargetFeeRate: rbfFeeBump,
|
||||
}
|
||||
|
||||
// Next, we'll send in this event, which should fail as we can't
|
||||
// actually pay for fees.
|
||||
closeHarness.chanCloser.SendEvent(ctx, localOffer)
|
||||
|
||||
// We should transition to the CloseErr (within
|
||||
// ClosingNegotiation) state.
|
||||
closeHarness.assertStateTransitions(&ClosingNegotiation{})
|
||||
|
||||
// If we get the state, we should see the expected ErrState.
|
||||
currentState := assertStateT[*ClosingNegotiation](closeHarness)
|
||||
|
||||
closeErrState, ok := currentState.PeerState.GetForParty(
|
||||
lntypes.Local,
|
||||
).(*CloseErr)
|
||||
require.True(t, ok)
|
||||
require.IsType(
|
||||
t, &ErrStateCantPayForFee{}, closeErrState.ErrState,
|
||||
)
|
||||
})
|
||||
|
||||
// Any other event should be ignored.
|
||||
assertUnknownEventFail(t, startingState)
|
||||
|
||||
// Sending a Spend event should transition to CloseFin.
|
||||
assertSpendEventCloseFin(t, startingState)
|
||||
}
|
||||
|
||||
// TestRbfCloseClosingNegotiationRemote tests that state machine is able to
|
||||
@ -1383,6 +1664,7 @@ func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
|
||||
// party.
|
||||
func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
localBalance := lnwire.NewMSatFromSatoshis(40_000)
|
||||
@ -1403,16 +1685,16 @@ 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
|
||||
|
||||
sequence := uint32(mempool.MaxRBFSequence)
|
||||
|
||||
// This case tests that if we receive a signature from the remote
|
||||
@ -1430,7 +1712,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 +1733,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 +1754,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 +1785,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 +1805,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 +1815,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 +1832,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,
|
||||
@ -1557,43 +1849,214 @@ func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
|
||||
// sig.
|
||||
closeHarness.assertSingleRemoteRbfIteration(
|
||||
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
||||
false,
|
||||
false, true,
|
||||
)
|
||||
|
||||
// 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(
|
||||
feeOffer, balanceAfterClose, absoluteFee, sequence,
|
||||
true,
|
||||
true, true,
|
||||
)
|
||||
|
||||
closeHarness.assertNoStateTransitions()
|
||||
})
|
||||
|
||||
// TODO(roasbeef): cross sig case? tested isolation, so wolog?
|
||||
// This tests that if we get an offer that has the wrong local script,
|
||||
// then we'll emit a hard error.
|
||||
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()
|
||||
})
|
||||
|
||||
// If we receive an offer from the remote party with a different remote
|
||||
// script, then this ensures that we'll process that and use that create
|
||||
// the next offer.
|
||||
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, true,
|
||||
)
|
||||
})
|
||||
|
||||
// Any other event should be ignored.
|
||||
assertUnknownEventFail(t, startingState)
|
||||
|
||||
// Sending a Spend event should transition to CloseFin.
|
||||
assertSpendEventCloseFin(t, startingState)
|
||||
}
|
||||
|
||||
// TestRbfCloseErr tests that the state machine is able to properly restart
|
||||
// the state machine if we encounter an error.
|
||||
func TestRbfCloseErr(t *testing.T) {
|
||||
localBalance := lnwire.NewMSatFromSatoshis(40_000)
|
||||
remoteBalance := lnwire.NewMSatFromSatoshis(50_000)
|
||||
|
||||
closeTerms := &CloseChannelTerms{
|
||||
ShutdownBalances: ShutdownBalances{
|
||||
LocalBalance: localBalance,
|
||||
RemoteBalance: remoteBalance,
|
||||
},
|
||||
ShutdownScripts: ShutdownScripts{
|
||||
LocalDeliveryScript: localAddr,
|
||||
RemoteDeliveryScript: remoteAddr,
|
||||
},
|
||||
}
|
||||
startingState := &ClosingNegotiation{
|
||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||
Local: &CloseErr{
|
||||
CloseChannelTerms: closeTerms,
|
||||
},
|
||||
},
|
||||
CloseChannelTerms: closeTerms,
|
||||
}
|
||||
|
||||
absoluteFee := btcutil.Amount(10_100)
|
||||
balanceAfterClose := localBalance.ToSatoshis() - absoluteFee
|
||||
|
||||
// From the error state, we should be able to kick off a new iteration
|
||||
// for a local fee bump.
|
||||
t.Run("send_offer_restart", func(t *testing.T) {
|
||||
closeHarness := newCloser(t, &harnessCfg{
|
||||
initialState: fn.Some[ProtocolState](startingState),
|
||||
})
|
||||
defer closeHarness.stopAndAssert()
|
||||
|
||||
rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte()
|
||||
localOffer := &SendOfferEvent{
|
||||
TargetFeeRate: rbfFeeBump,
|
||||
}
|
||||
|
||||
// Now we expect that another full RBF iteration takes place (we
|
||||
// initiate a new local sig).
|
||||
closeHarness.assertSingleRbfIteration(
|
||||
localOffer, balanceAfterClose, absoluteFee,
|
||||
noDustExpect,
|
||||
)
|
||||
|
||||
// We should terminate in the negotiation state.
|
||||
closeHarness.assertStateTransitions(
|
||||
&ClosingNegotiation{},
|
||||
)
|
||||
})
|
||||
|
||||
// From the error state, we should be able to handle the remote party
|
||||
// kicking off a new iteration for a fee bump.
|
||||
t.Run("recv_offer_restart", func(t *testing.T) {
|
||||
startingState := &ClosingNegotiation{
|
||||
PeerState: lntypes.Dual[AsymmetricPeerState]{
|
||||
Remote: &CloseErr{
|
||||
CloseChannelTerms: closeTerms,
|
||||
Party: lntypes.Remote,
|
||||
},
|
||||
},
|
||||
CloseChannelTerms: closeTerms,
|
||||
}
|
||||
|
||||
closeHarness := newCloser(t, &harnessCfg{
|
||||
initialState: fn.Some[ProtocolState](startingState),
|
||||
localUpfrontAddr: fn.Some(localAddr),
|
||||
})
|
||||
defer closeHarness.stopAndAssert()
|
||||
|
||||
feeOffer := &OfferReceivedEvent{
|
||||
SigMsg: lnwire.ClosingComplete{
|
||||
CloserScript: remoteAddr,
|
||||
CloseeScript: localAddr,
|
||||
FeeSatoshis: absoluteFee,
|
||||
LockTime: 1,
|
||||
ClosingSigs: lnwire.ClosingSigs{
|
||||
CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
|
||||
remoteWireSig,
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sequence := uint32(mempool.MaxRBFSequence)
|
||||
|
||||
// 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, true,
|
||||
)
|
||||
})
|
||||
|
||||
// Sending a Spend event should transition to CloseFin.
|
||||
assertSpendEventCloseFin(t, startingState)
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package chancloser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/mempool"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
@ -14,11 +16,18 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnutils"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/protofsm"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidStateTransition is returned if the remote party tries to
|
||||
// close, but the thaw height hasn't been matched yet.
|
||||
ErrThawHeightNotReached = fmt.Errorf("thaw height not reached")
|
||||
)
|
||||
|
||||
// sendShutdownEvents is a helper function that returns a set of daemon events
|
||||
// we need to emit when we decide that we should send a shutdown message. We'll
|
||||
// also mark the channel as borked as well, as at this point, we no longer want
|
||||
@ -104,11 +113,11 @@ func validateShutdown(chanThawHeight fn.Option[uint32],
|
||||
// reject the shutdown message as we can't yet co-op close the
|
||||
// channel.
|
||||
if msg.BlockHeight < thawHeight {
|
||||
return fmt.Errorf("initiator attempting to "+
|
||||
return fmt.Errorf("%w: initiator attempting to "+
|
||||
"co-op close frozen ChannelPoint(%v) "+
|
||||
"(current_height=%v, thaw_height=%v)",
|
||||
chanPoint, msg.BlockHeight,
|
||||
thawHeight)
|
||||
ErrThawHeightNotReached, chanPoint,
|
||||
msg.BlockHeight, thawHeight)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -171,7 +180,7 @@ func (c *ChannelActive) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
}
|
||||
|
||||
chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg, "+
|
||||
"delivery_script=%v", env.ChanPoint, shutdownScript)
|
||||
"delivery_script=%x", env.ChanPoint, shutdownScript)
|
||||
|
||||
// From here, we'll transition to the shutdown pending state. In
|
||||
// this state we await their shutdown message (self loop), then
|
||||
@ -288,6 +297,22 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
},
|
||||
}, nil
|
||||
|
||||
// The remote party sent an offer early. We'll go to the ChannelFlushing
|
||||
// case, and then emit the offer as a internal event, which'll be
|
||||
// handled as an early offer.
|
||||
case *OfferReceivedEvent:
|
||||
chancloserLog.Infof("ChannelPoint(%v): got an early offer "+
|
||||
"in ShutdownPending, emitting as external event",
|
||||
env.ChanPoint)
|
||||
|
||||
s.EarlyRemoteOffer = fn.Some(*msg)
|
||||
|
||||
// We'll perform a noop update so we can wait for the actual
|
||||
// channel flushed event.
|
||||
return &CloseStateTransition{
|
||||
NextState: s,
|
||||
}, nil
|
||||
|
||||
// When we receive a shutdown from the remote party, we'll validate the
|
||||
// shutdown message, then transition to the ChannelFlushing state.
|
||||
case *ShutdownReceived:
|
||||
@ -311,7 +336,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
// If the channel is *already* flushed, and the close is
|
||||
// go straight into negotiation, as this is the RBF loop.
|
||||
// already in progress, then we can skip the flushing state and
|
||||
var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
|
||||
var eventsToEmit []ProtocolEvent
|
||||
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
||||
unknownBalance,
|
||||
)
|
||||
@ -319,11 +344,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
||||
ShutdownBalances: finalBalances,
|
||||
})
|
||||
eventsToEmit = fn.Some(RbfEvent{
|
||||
InternalEvent: []ProtocolEvent{
|
||||
channelFlushed,
|
||||
},
|
||||
})
|
||||
eventsToEmit = append(eventsToEmit, channelFlushed)
|
||||
}
|
||||
|
||||
chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
|
||||
@ -339,6 +360,19 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
|
||||
"be flushed...", env.ChanPoint)
|
||||
|
||||
// If we received a remote offer early from the remote party,
|
||||
// then we'll add that to the set of internal events to emit.
|
||||
s.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
|
||||
eventsToEmit = append(eventsToEmit, &offer)
|
||||
})
|
||||
|
||||
var newEvents fn.Option[RbfEvent]
|
||||
if len(eventsToEmit) > 0 {
|
||||
newEvents = fn.Some(RbfEvent{
|
||||
InternalEvent: eventsToEmit,
|
||||
})
|
||||
}
|
||||
|
||||
// We transition to the ChannelFlushing state, where we await
|
||||
// the ChannelFlushed event.
|
||||
return &CloseStateTransition{
|
||||
@ -349,7 +383,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
RemoteDeliveryScript: msg.ShutdownScript, //nolint:ll
|
||||
},
|
||||
},
|
||||
NewEvents: eventsToEmit,
|
||||
NewEvents: newEvents,
|
||||
}, nil
|
||||
|
||||
// If we get this message, then this means that we were finally able to
|
||||
@ -362,7 +396,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
// If the channel is *already* flushed, and the close is
|
||||
// already in progress, then we can skip the flushing state and
|
||||
// go straight into negotiation, as this is the RBF loop.
|
||||
var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
|
||||
var eventsToEmit []ProtocolEvent
|
||||
finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
|
||||
unknownBalance,
|
||||
)
|
||||
@ -370,10 +404,19 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
channelFlushed := ProtocolEvent(&ChannelFlushed{
|
||||
ShutdownBalances: finalBalances,
|
||||
})
|
||||
eventsToEmit = fn.Some(RbfEvent{
|
||||
InternalEvent: []ProtocolEvent{
|
||||
channelFlushed,
|
||||
},
|
||||
eventsToEmit = append(eventsToEmit, channelFlushed)
|
||||
}
|
||||
|
||||
// If we received a remote offer early from the remote party,
|
||||
// then we'll add that to the set of internal events to emit.
|
||||
s.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
|
||||
eventsToEmit = append(eventsToEmit, &offer)
|
||||
})
|
||||
|
||||
var newEvents fn.Option[RbfEvent]
|
||||
if len(eventsToEmit) > 0 {
|
||||
newEvents = fn.Some(RbfEvent{
|
||||
InternalEvent: eventsToEmit,
|
||||
})
|
||||
}
|
||||
|
||||
@ -384,7 +427,7 @@ func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
IdealFeeRate: s.IdealFeeRate,
|
||||
ShutdownScripts: s.ShutdownScripts,
|
||||
},
|
||||
NewEvents: eventsToEmit,
|
||||
NewEvents: newEvents,
|
||||
}, nil
|
||||
|
||||
// Any other messages in this state will result in an error, as this is
|
||||
@ -424,9 +467,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{
|
||||
@ -467,8 +507,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,
|
||||
@ -520,12 +558,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
|
||||
@ -569,6 +608,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,
|
||||
@ -580,6 +676,12 @@ func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
// we receive a confirmation event, or we receive a signal to restart
|
||||
// the co-op close process.
|
||||
switch msg := event.(type) {
|
||||
// Ignore any potential duplicate channel flushed events.
|
||||
case *ChannelFlushed:
|
||||
return &CloseStateTransition{
|
||||
NextState: c,
|
||||
}, nil
|
||||
|
||||
// If we get a confirmation, then the spend request we issued when we
|
||||
// were leaving the ChannelFlushing state has been confirmed. We'll
|
||||
// now transition to the StateFin state.
|
||||
@ -589,45 +691,45 @@ 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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
return &CloseStateTransition{
|
||||
NextState: &ChannelActive{},
|
||||
NewEvents: fn.Some(RbfEvent{
|
||||
InternalEvent: []ProtocolEvent{event},
|
||||
}),
|
||||
}, nil
|
||||
shouldRouteTo := func(party lntypes.ChannelParty) bool {
|
||||
state := c.PeerState.GetForParty(party)
|
||||
if state == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return state.ShouldRouteTo(event)
|
||||
}
|
||||
|
||||
// If we get to this point, then we have an event that'll drive forward
|
||||
// the negotiation process. Based on the event, we'll figure out which
|
||||
// state we'll be modifying.
|
||||
switch {
|
||||
case c.PeerState.GetForParty(lntypes.Local).ShouldRouteTo(event):
|
||||
case shouldRouteTo(lntypes.Local):
|
||||
chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
|
||||
"chan state", env.ChanPoint, event)
|
||||
|
||||
// Drive forward the local state based on the next event.
|
||||
return processNegotiateEvent(c, event, env, lntypes.Local)
|
||||
|
||||
case c.PeerState.GetForParty(lntypes.Remote).ShouldRouteTo(event):
|
||||
case shouldRouteTo(lntypes.Remote):
|
||||
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
|
||||
@ -646,14 +748,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
|
||||
@ -714,12 +836,13 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
// TODO(roasbeef): type alias for protocol event
|
||||
sendEvent := protofsm.DaemonEventSet{&protofsm.SendMsgEvent[ProtocolEvent]{ //nolint:ll
|
||||
TargetPeer: env.ChanPeer,
|
||||
// TODO(roasbeef): mew new func
|
||||
Msgs: []lnwire.Message{&lnwire.ClosingComplete{
|
||||
ChannelID: env.ChanID,
|
||||
FeeSatoshis: absoluteFee,
|
||||
LockTime: env.BlockHeight,
|
||||
ClosingSigs: closingSigs,
|
||||
ChannelID: env.ChanID,
|
||||
CloserScript: l.LocalDeliveryScript,
|
||||
CloseeScript: l.RemoteDeliveryScript,
|
||||
FeeSatoshis: absoluteFee,
|
||||
LockTime: env.BlockHeight,
|
||||
ClosingSigs: closingSigs,
|
||||
}},
|
||||
}}
|
||||
|
||||
@ -730,6 +853,7 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
return &CloseStateTransition{
|
||||
NextState: &LocalOfferSent{
|
||||
ProposedFee: absoluteFee,
|
||||
ProposedFeeRate: msg.TargetFeeRate,
|
||||
LocalSig: wireSig,
|
||||
CloseChannelTerms: l.CloseChannelTerms,
|
||||
},
|
||||
@ -838,7 +962,10 @@ func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
|
||||
return &CloseStateTransition{
|
||||
NextState: &ClosePending{
|
||||
CloseTx: closeTx,
|
||||
CloseTx: closeTx,
|
||||
FeeRate: l.ProposedFeeRate,
|
||||
CloseChannelTerms: l.CloseChannelTerms,
|
||||
Party: lntypes.Local,
|
||||
},
|
||||
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||
ExternalEvents: broadcastEvent,
|
||||
@ -981,8 +1108,12 @@ func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
sendEvent := &protofsm.SendMsgEvent[ProtocolEvent]{
|
||||
TargetPeer: env.ChanPeer,
|
||||
Msgs: []lnwire.Message{&lnwire.ClosingSig{
|
||||
ChannelID: env.ChanID,
|
||||
ClosingSigs: closingSigs,
|
||||
ChannelID: env.ChanID,
|
||||
CloserScript: l.RemoteDeliveryScript,
|
||||
CloseeScript: l.LocalDeliveryScript,
|
||||
FeeSatoshis: msg.SigMsg.FeeSatoshis,
|
||||
LockTime: msg.SigMsg.LockTime,
|
||||
ClosingSigs: closingSigs,
|
||||
}},
|
||||
}
|
||||
broadcastEvent := &protofsm.BroadcastTxn{
|
||||
@ -995,11 +1126,22 @@ func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
|
||||
sendEvent, broadcastEvent,
|
||||
}
|
||||
|
||||
// We'll also compute the final fee rate that the remote party
|
||||
// paid based off the absolute fee and the size of the closing
|
||||
// transaction.
|
||||
vSize := mempool.GetTxVirtualSize(btcutil.NewTx(closeTx))
|
||||
feeRate := chainfee.SatPerVByte(
|
||||
int64(msg.SigMsg.FeeSatoshis) / vSize,
|
||||
)
|
||||
|
||||
// Now that we've extracted the signature, we'll transition to
|
||||
// the next state where we'll sign+broadcast the sig.
|
||||
return &CloseStateTransition{
|
||||
NextState: &ClosePending{
|
||||
CloseTx: closeTx,
|
||||
CloseTx: closeTx,
|
||||
FeeRate: feeRate,
|
||||
CloseChannelTerms: l.CloseChannelTerms,
|
||||
Party: lntypes.Remote,
|
||||
},
|
||||
NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
|
||||
ExternalEvents: daemonEvents,
|
||||
@ -1027,6 +1169,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{
|
||||
@ -1044,3 +1212,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
|
||||
}
|
||||
}
|
||||
|
@ -9269,13 +9269,19 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
||||
closeTx.LockTime = lockTime
|
||||
})
|
||||
|
||||
// TODO(roasbeef): needs support for dropping inputs
|
||||
|
||||
// Create both cooperative closure outputs, properly respecting the
|
||||
// dust limits of both parties.
|
||||
// Create both cooperative closure outputs, properly respecting the dust
|
||||
// limits of both parties.
|
||||
var localOutputIdx fn.Option[int]
|
||||
haveLocalOutput := ourBalance >= localDust
|
||||
if haveLocalOutput {
|
||||
// If our script is an OP_RETURN, then we set our balance to
|
||||
// zero.
|
||||
if opts.customSequence.IsSome() &&
|
||||
input.ScriptIsOpReturn(ourDeliveryScript) {
|
||||
|
||||
ourBalance = 0
|
||||
}
|
||||
|
||||
closeTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: ourDeliveryScript,
|
||||
Value: int64(ourBalance),
|
||||
@ -9287,6 +9293,14 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
|
||||
var remoteOutputIdx fn.Option[int]
|
||||
haveRemoteOutput := theirBalance >= remoteDust
|
||||
if haveRemoteOutput {
|
||||
// If a party's script is an OP_RETURN, then we set their
|
||||
// balance to zero.
|
||||
if opts.customSequence.IsSome() &&
|
||||
input.ScriptIsOpReturn(theirDeliveryScript) {
|
||||
|
||||
theirBalance = 0
|
||||
}
|
||||
|
||||
closeTx.AddTxOut(&wire.TxOut{
|
||||
PkScript: theirDeliveryScript,
|
||||
Value: int64(theirBalance),
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/txsort"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/mempool"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
@ -2450,6 +2451,72 @@ func TestCooperativeCloseDustAdherence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCooperativeCloseOpReturn tests that if either party's script is an
|
||||
// OP_RETURN script, then we'll set their output value as zero on the closing
|
||||
// transaction.
|
||||
func TestCooperativeCloseOpReturn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test channel which will be used for the duration of this
|
||||
// unittest. The channel will be funded evenly with Alice having 5 BTC,
|
||||
// and Bob having 5 BTC.
|
||||
aliceChannel, bobChannel, err := CreateTestChannels(
|
||||
t, channeldb.SingleFunderTweaklessBit,
|
||||
)
|
||||
require.NoError(t, err, "unable to create test channels")
|
||||
|
||||
// Alice will have a "normal" looking script, while Bob will have a
|
||||
// script that's just an OP_RETURN.
|
||||
aliceDeliveryScript := bobsPrivKey
|
||||
bobDeliveryScript := []byte{txscript.OP_RETURN}
|
||||
|
||||
aliceFeeRate := chainfee.SatPerKWeight(
|
||||
aliceChannel.channelState.LocalCommitment.FeePerKw,
|
||||
)
|
||||
aliceFee := aliceChannel.CalcFee(aliceFeeRate) + 1000
|
||||
|
||||
assertBobOpReturn := func(tx *wire.MsgTx) {
|
||||
// We should still have two outputs on the commitment
|
||||
// transaction, as Alice's is non-dust.
|
||||
require.Len(t, tx.TxOut, 2)
|
||||
|
||||
// We should find that Bob's output has a zero value.
|
||||
bobTxOut := fn.Filter(tx.TxOut, func(txOut *wire.TxOut) bool {
|
||||
return bytes.Equal(txOut.PkScript, bobDeliveryScript)
|
||||
})
|
||||
require.Len(t, bobTxOut, 1)
|
||||
|
||||
require.True(t, bobTxOut[0].Value == 0)
|
||||
}
|
||||
|
||||
// Next, we'll make a new co-op close proposal, initiated by Alice.
|
||||
aliceSig, closeTxAlice, _, err := aliceChannel.CreateCloseProposal(
|
||||
aliceFee, aliceDeliveryScript, bobDeliveryScript,
|
||||
// We use a custom sequence as this rule only applies to the RBF
|
||||
// coop channel type.
|
||||
WithCustomSequence(mempool.MaxRBFSequence),
|
||||
)
|
||||
require.NoError(t, err, "unable to close channel")
|
||||
|
||||
assertBobOpReturn(closeTxAlice)
|
||||
|
||||
bobSig, _, _, err := bobChannel.CreateCloseProposal(
|
||||
aliceFee, bobDeliveryScript, aliceDeliveryScript,
|
||||
WithCustomSequence(mempool.MaxRBFSequence),
|
||||
)
|
||||
require.NoError(t, err, "unable to close channel")
|
||||
|
||||
// We should now be able to complete the cooperative channel closure,
|
||||
// finding that the close tx still only has a single output.
|
||||
closeTx, _, err := bobChannel.CompleteCooperativeClose(
|
||||
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
|
||||
aliceFee, WithCustomSequence(mempool.MaxRBFSequence),
|
||||
)
|
||||
require.NoError(t, err, "unable to accept channel close")
|
||||
|
||||
assertBobOpReturn(closeTx)
|
||||
}
|
||||
|
||||
// TestUpdateFeeAdjustments tests that the state machine is able to properly
|
||||
// accept valid fee changes, as well as reject any invalid fee updates.
|
||||
func TestUpdateFeeAdjustments(t *testing.T) {
|
||||
|
@ -30,6 +30,14 @@ type ClosingComplete struct {
|
||||
// ChannelID serves to identify which channel is to be closed.
|
||||
ChannelID ChannelID
|
||||
|
||||
// CloserScript is the script to which the channel funds will be paid
|
||||
// for the closer (the person sending the ClosingComplete) message.
|
||||
CloserScript DeliveryAddress
|
||||
|
||||
// CloseeScript is the script to which the channel funds will be paid
|
||||
// (the person receiving the ClosingComplete message).
|
||||
CloseeScript DeliveryAddress
|
||||
|
||||
// FeeSatoshis is the total fee in satoshis that the party to the
|
||||
// channel would like to propose for the close transaction.
|
||||
FeeSatoshis btcutil.Amount
|
||||
@ -79,7 +87,10 @@ func decodeClosingSigs(c *ClosingSigs, tlvRecords ExtraOpaqueData) error {
|
||||
// passed io.Reader.
|
||||
func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
|
||||
// First, read out all the fields that are hard coded into the message.
|
||||
err := ReadElements(r, &c.ChannelID, &c.FeeSatoshis, &c.LockTime)
|
||||
err := ReadElements(
|
||||
r, &c.ChannelID, &c.CloserScript, &c.CloseeScript,
|
||||
&c.FeeSatoshis, &c.LockTime,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -125,6 +136,13 @@ func (c *ClosingComplete) Encode(w *bytes.Buffer, _ uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteDeliveryAddress(w, c.CloserScript); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := WriteDeliveryAddress(w, c.CloseeScript); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteSatoshi(w, c.FeeSatoshis); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package lnwire
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
)
|
||||
|
||||
// ClosingSig is sent in response to a ClosingComplete message. It carries the
|
||||
@ -11,6 +13,22 @@ type ClosingSig struct {
|
||||
// ChannelID serves to identify which channel is to be closed.
|
||||
ChannelID ChannelID
|
||||
|
||||
// CloserScript is the script to which the channel funds will be paid
|
||||
// for the closer (the person sending the ClosingComplete) message.
|
||||
CloserScript DeliveryAddress
|
||||
|
||||
// CloseeScript is the script to which the channel funds will be paid
|
||||
// (the person receiving the ClosingComplete message).
|
||||
CloseeScript DeliveryAddress
|
||||
|
||||
// FeeSatoshis is the total fee in satoshis that the party to the
|
||||
// channel proposed for the close transaction.
|
||||
FeeSatoshis btcutil.Amount
|
||||
|
||||
// LockTime is the locktime number to be used in the input spending the
|
||||
// funding transaction.
|
||||
LockTime uint32
|
||||
|
||||
// ClosingSigs houses the 3 possible signatures that can be sent.
|
||||
ClosingSigs
|
||||
|
||||
@ -24,7 +42,10 @@ type ClosingSig struct {
|
||||
// io.Reader.
|
||||
func (c *ClosingSig) Decode(r io.Reader, _ uint32) error {
|
||||
// First, read out all the fields that are hard coded into the message.
|
||||
err := ReadElements(r, &c.ChannelID)
|
||||
err := ReadElements(
|
||||
r, &c.ChannelID, &c.CloserScript, &c.CloseeScript,
|
||||
&c.FeeSatoshis, &c.LockTime,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -53,6 +74,21 @@ func (c *ClosingSig) Encode(w *bytes.Buffer, _ uint32) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteDeliveryAddress(w, c.CloserScript); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := WriteDeliveryAddress(w, c.CloseeScript); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteSatoshi(w, c.FeeSatoshis); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WriteUint32(w, c.LockTime); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recordProducers := closingSigRecords(&c.ClosingSigs)
|
||||
|
||||
err := EncodeMessageExtraData(&c.ExtraData, recordProducers...)
|
||||
|
@ -1355,6 +1355,18 @@ func TestLightningWireProtocol(t *testing.T) {
|
||||
LockTime: uint32(r.Int63()),
|
||||
ClosingSigs: ClosingSigs{},
|
||||
}
|
||||
req.CloserScript, err = randDeliveryAddress(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate delivery "+
|
||||
"address: %v", err)
|
||||
return
|
||||
}
|
||||
req.CloseeScript, err = randDeliveryAddress(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate delivery "+
|
||||
"address: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Intn(2) == 0 {
|
||||
sig := req.CloserNoClosee.Zero()
|
||||
@ -1403,6 +1415,20 @@ func TestLightningWireProtocol(t *testing.T) {
|
||||
req := ClosingSig{
|
||||
ChannelID: ChannelID(c),
|
||||
ClosingSigs: ClosingSigs{},
|
||||
FeeSatoshis: btcutil.Amount(r.Int63()),
|
||||
LockTime: uint32(r.Int63()),
|
||||
}
|
||||
req.CloserScript, err = randDeliveryAddress(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate delivery "+
|
||||
"address: %v", err)
|
||||
return
|
||||
}
|
||||
req.CloseeScript, err = randDeliveryAddress(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate delivery "+
|
||||
"address: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Intn(2) == 0 {
|
||||
|
@ -377,9 +377,18 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
||||
})
|
||||
}
|
||||
|
||||
// If this doesn't have a SendWhen predicate, then we can just
|
||||
// send it off right away.
|
||||
if !daemonEvent.SendWhen.IsSome() {
|
||||
canSend := func() bool {
|
||||
return fn.MapOptionZ(
|
||||
daemonEvent.SendWhen,
|
||||
func(pred SendPredicate) bool {
|
||||
return pred()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// If this doesn't have a SendWhen predicate, or if it's already
|
||||
// true, then we can just send it off right away.
|
||||
if !daemonEvent.SendWhen.IsSome() || canSend() {
|
||||
return sendAndCleanUp()
|
||||
}
|
||||
|
||||
@ -397,14 +406,7 @@ func (s *StateMachine[Event, Env]) executeDaemonEvent(ctx context.Context,
|
||||
for {
|
||||
select {
|
||||
case <-predicateTicker.C:
|
||||
canSend := fn.MapOptionZ(
|
||||
daemonEvent.SendWhen,
|
||||
func(pred SendPredicate) bool {
|
||||
return pred()
|
||||
},
|
||||
)
|
||||
|
||||
if canSend {
|
||||
if canSend() {
|
||||
s.log.InfoS(ctx, "Send active predicate")
|
||||
|
||||
err := sendAndCleanUp()
|
||||
|
Loading…
x
Reference in New Issue
Block a user