staticaddr/loopin: recover deposits by current outpoint

Recovered loop-ins carry two outpoint views. DepositOutpoints is the
immutable swap input snapshot sent to the server and used to validate
sweep requests. Deposits comes from the store's swap_hash/deposit-id
join and reflects the current deposit rows.

The active-deposit lookup takes a detour through the reconstructed
deposit rows before asking the deposit manager for active deposits.
That keeps recovery from depending on the historical input snapshot.

A future replacement path can RBF a deposit from its original funding
outpoint to a replacement outpoint while the swap still needs to retain
the original input list. Looking up active deposits by DepositOutpoints
would then fail recovery even though the store still maps the correct
deposit IDs to the swap hash.

Keep list responses on the store reconstruction too, so they do not
re-resolve deposits through historical outpoints.
This commit is contained in:
Slyghtning 2026-07-02 10:46:02 +02:00
parent 67252a84de
commit 3fdd9e2250
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 85 additions and 33 deletions

View file

@ -533,17 +533,15 @@ func (m *Manager) recoverLoopIns(ctx context.Context) error {
for _, loopIn := range pendingLoopIns {
log.Debugf("Recovering loopIn %x", loopIn.SwapHash[:])
// Retrieve all deposits regardless of deposit state. If any of
// the deposits is not active in the in-mem map of the deposits
// manager we log it, but continue to recover the loop-in.
var allActive bool
loopIn.Deposits, allActive =
m.cfg.DepositManager.AllStringOutpointsActiveDeposits(
loopIn.DepositOutpoints, fsm.EmptyState,
)
// Retrieve all deposits regardless of deposit state. If all
// deposits are active in the in-mem map of the deposits manager,
// use those active instances. Otherwise, keep the store's
// swap_hash/deposit-id reconstruction and continue recovery.
activeDeposits, allActive := m.activeDepositsForLoopIn(loopIn)
if !allActive {
log.Errorf("one or more deposits are not active")
} else {
loopIn.Deposits = activeDeposits
}
loopIn.AddressParams, err =
@ -818,35 +816,29 @@ func (m *Manager) startLoopInFsm(ctx context.Context,
func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn,
error) {
swaps, err := m.cfg.Store.GetStaticAddressLoopInSwapsByStates(
return m.cfg.Store.GetStaticAddressLoopInSwapsByStates(
ctx, AllStates,
)
if err != nil {
return nil, err
}
}
allDeposits, err := m.cfg.DepositManager.GetAllDeposits(ctx)
if err != nil {
return nil, err
}
// activeDepositsForLoopIn returns the active deposit instances for a loop-in
// using the current deposit outpoints reconstructed by the store. The stored
// deposit outpoint snapshots remain the original swap inputs and are not the
// source of truth for current deposit rows.
func (m *Manager) activeDepositsForLoopIn(loopIn *StaticAddressLoopIn) (
[]*deposit.Deposit, bool) {
var depositLookup = make(map[string]*deposit.Deposit)
for i, d := range allDeposits {
depositLookup[d.OutPoint.String()] = allDeposits[i]
}
for i, s := range swaps {
var deposits []*deposit.Deposit
for _, outpoint := range s.DepositOutpoints {
if d, ok := depositLookup[outpoint]; ok {
deposits = append(deposits, d)
}
outpoints := loopIn.DepositOutpoints
if len(loopIn.Deposits) > 0 {
outpoints = make([]string, 0, len(loopIn.Deposits))
for _, d := range loopIn.Deposits {
outpoints = append(outpoints, d.OutPoint.String())
}
swaps[i].Deposits = deposits
}
return swaps, nil
return m.cfg.DepositManager.AllStringOutpointsActiveDeposits(
outpoints, fsm.EmptyState,
)
}
// SelectDeposits sorts the deposits by amount in descending order, then by

View file

@ -298,6 +298,65 @@ func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) {
require.ErrorContains(t, err, depOutpoint)
}
// TestActiveDepositsForLoopInUsesCurrentDepositOutpoints verifies that
// recovery checks the current deposit outpoints reconstructed by the store
// rather than the original outpoint snapshot persisted on the swap.
func TestActiveDepositsForLoopInUsesCurrentDepositOutpoints(t *testing.T) {
oldOutpoint := wire.OutPoint{
Hash: chainhash.Hash{0xaa},
Index: 0,
}
currentDeposit := makeDeposit(0xbb, 1, 10_000, 42)
manager := &Manager{
cfg: &Config{
DepositManager: &mockDepositManager{
byOutpoint: map[string]*deposit.Deposit{
currentDeposit.OutPoint.String(): currentDeposit,
},
},
},
}
deposits, allActive := manager.activeDepositsForLoopIn(
&StaticAddressLoopIn{
DepositOutpoints: []string{oldOutpoint.String()},
Deposits: []*deposit.Deposit{currentDeposit},
},
)
require.True(t, allActive)
require.Equal(t, []*deposit.Deposit{currentDeposit}, deposits)
}
// TestGetAllSwapsPreservesStoreDeposits verifies that list responses keep the
// store's swap_hash/deposit-id reconstruction even when DepositOutpoints is an
// original input snapshot and the deposit's current outpoint has changed.
func TestGetAllSwapsPreservesStoreDeposits(t *testing.T) {
oldOutpoint := wire.OutPoint{
Hash: chainhash.Hash{0xcc},
Index: 0,
}
currentDeposit := makeDeposit(0xdd, 1, 10_000, 42)
swap := &StaticAddressLoopIn{
DepositOutpoints: []string{oldOutpoint.String()},
Deposits: []*deposit.Deposit{currentDeposit},
}
manager := &Manager{
cfg: &Config{
Store: &mockStore{
swaps: []*StaticAddressLoopIn{swap},
},
},
}
swaps, err := manager.GetAllSwaps(t.Context())
require.NoError(t, err)
require.Len(t, swaps, 1)
require.Equal(t, []string{oldOutpoint.String()}, swaps[0].DepositOutpoints)
require.Equal(t, []*deposit.Deposit{currentDeposit}, swaps[0].Deposits)
}
// mockDepositManager implements DepositManager for tests.
type mockDepositManager struct {
// activeDeposits is the set returned by GetActiveDepositsInState.
@ -316,7 +375,7 @@ func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
func (m *mockDepositManager) AllStringOutpointsActiveDeposits(outpoints []string,
state fsm.StateType) ([]*deposit.Deposit, bool) {
if state != deposit.Deposited {
if state != deposit.Deposited && state != fsm.EmptyState {
return nil, false
}
@ -408,6 +467,7 @@ func (m *mockQuoteGetter) GetLoopInQuote(_ context.Context,
// mockStore implements StaticAddressLoopInStore for tests.
type mockStore struct {
swaps []*StaticAddressLoopIn
loopIns map[lntypes.Hash]*StaticAddressLoopIn
mapIDs map[lntypes.Hash][]deposit.ID
}
@ -427,7 +487,7 @@ func (s *mockStore) UpdateLoopIn(_ context.Context,
func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context,
_ []fsm.StateType) ([]*StaticAddressLoopIn, error) {
return nil, nil
return s.swaps, nil
}
func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
return false, nil