From f468f24e6f2cbf376062410f34b88cb1d405044a Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Wed, 1 Jul 2026 13:16:49 +0200 Subject: [PATCH] staticaddr/deposit: ignore queued expiry in final states A block notification can queue OnExpiry before a deposit reaches a final state. If the final transition wins that race first, the stale expiry event must not overwrite the terminal outcome. Keep LoopedIn and Withdrawn as self-loops on OnExpiry, matching the other final states. Add a focused FSM test that sends OnExpiry directly to each final state and verifies the state is preserved. --- staticaddr/deposit/fsm.go | 4 +-- staticaddr/deposit/fsm_test.go | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/staticaddr/deposit/fsm.go b/staticaddr/deposit/fsm.go index b4c89ec8..0833be92 100644 --- a/staticaddr/deposit/fsm.go +++ b/staticaddr/deposit/fsm.go @@ -393,7 +393,7 @@ func (f *FSM) DepositStatesV0() fsm.States { }, LoopedIn: fsm.State{ Transitions: fsm.Transitions{ - OnExpiry: Expired, + OnExpiry: LoopedIn, }, Action: f.FinalizeDepositAction, }, @@ -412,7 +412,7 @@ func (f *FSM) DepositStatesV0() fsm.States { }, Withdrawn: fsm.State{ Transitions: fsm.Transitions{ - OnExpiry: Expired, + OnExpiry: Withdrawn, OnWithdrawn: Withdrawn, }, Action: f.FinalizeDepositAction, diff --git a/staticaddr/deposit/fsm_test.go b/staticaddr/deposit/fsm_test.go index d7a70bb2..f174ad1d 100644 --- a/staticaddr/deposit/fsm_test.go +++ b/staticaddr/deposit/fsm_test.go @@ -71,6 +71,53 @@ func TestHandleBlockNotificationIgnoresFinalStates(t *testing.T) { } } +// TestFinalStatesIgnoreQueuedExpiry verifies that a queued OnExpiry event cannot +// overwrite a deposit that already reached a final state. +func TestFinalStatesIgnoreQueuedExpiry(t *testing.T) { + t.Parallel() + + finalStates := []fsm.StateType{ + Expired, + Withdrawn, + LoopedIn, + HtlcTimeoutSwept, + ChannelPublished, + } + + for i, state := range finalStates { + t.Run(string(state), func(t *testing.T) { + t.Parallel() + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{byte(i + 1)}, + Index: uint32(i), + } + deposit := &Deposit{ + OutPoint: outpoint, + } + deposit.SetState(state) + + depositFSM := &FSM{ + cfg: &ManagerConfig{ + Store: new(mockStore), + }, + deposit: deposit, + quitChan: make(chan struct{}), + finalizedDepositChan: make(chan wire.OutPoint, 1), + } + depositFSM.StateMachine = fsm.NewStateMachineWithState( + depositFSM.DepositStatesV0(), state, + DefaultObserverSize, + ) + depositFSM.ActionEntryFunc = depositFSM.updateDeposit + + err := depositFSM.SendEvent(t.Context(), OnExpiry, nil) + require.NoError(t, err) + require.Equal(t, state, deposit.GetState()) + }) + } +} + // TestLoopingInTransitionsToSweepHtlcTimeout verifies that a deposit selected // by a loop-in can be moved into the timeout sweep state if the server confirms // the HTLC without paying the invoice.