staticaddr/deposit: ignore expiry blocks in final states

Return early when block notifications reach deposits that already
moved into a terminal state.

This prevents final deposits from retrying expiry handling after
recovery or while their FSM is still draining block updates.
This commit is contained in:
Slyghtning 2026-07-01 07:15:40 +02:00
parent ba1c37c0da
commit 0611832030
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 64 additions and 0 deletions

View file

@ -241,6 +241,10 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig,
func (f *FSM) handleBlockNotification(ctx context.Context,
currentHeight uint32) {
if f.deposit.IsInFinalState() {
return
}
// If the deposit is expired but not yet sufficiently confirmed, we
// republish the expiry sweep transaction.
if f.deposit.IsExpired(currentHeight, f.params.Expiry) {

View file

@ -2,6 +2,7 @@ package deposit
import (
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@ -11,6 +12,65 @@ import (
"github.com/stretchr/testify/require"
)
// TestHandleBlockNotificationIgnoresFinalStates verifies that a block-driven
// expiry notification cannot mutate deposits that already reached a final
// state but have not yet been removed from the manager's active set.
func TestHandleBlockNotificationIgnoresFinalStates(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,
ConfirmationHeight: 1,
}
deposit.SetState(state)
depositFSM := &FSM{
cfg: &ManagerConfig{
Store: new(mockStore),
},
deposit: deposit,
params: &script.Parameters{Expiry: 1},
quitChan: make(chan struct{}),
finalizedDepositChan: make(chan wire.OutPoint, 1),
}
depositFSM.StateMachine = fsm.NewStateMachineWithState(
depositFSM.DepositStatesV0(), state,
DefaultObserverSize,
)
depositFSM.ActionEntryFunc = depositFSM.updateDeposit
depositFSM.handleBlockNotification(t.Context(), 3)
require.Never(t, func() bool {
return deposit.GetState() != state
}, 100*time.Millisecond, 10*time.Millisecond)
select {
case finalized := <-depositFSM.finalizedDepositChan:
t.Fatalf("unexpected finalization for %v", finalized)
default:
}
})
}
}
// 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.