loop/staticaddr/loopin/sql_store_test.go
Slyghtning 325545143c
staticaddr/loopin: persist risk decisions
Store the server's static loop-in confirmation-risk decision and the time it
was received. This lets recovered swaps reconstruct whether payment waiting had
already started and how much of the payment timeout remains.

Wire notification handling to persist accepted and rejected decisions before
caching and forwarding them. If the swap row is not present yet, the
notification is still cached so the per-swap waiter can replay and store the
decision later.

Recover accepted decisions by starting the payment deadline from the persisted
decision time, and recover rejected decisions by canceling the invoice and
failing the swap instead of waiting forever.
2026-06-01 10:46:52 +02:00

425 lines
13 KiB
Go

package loopin
import (
"context"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// TestGetStaticAddressLoopInSwapsByStates tests that we can retrieve
// StaticAddressLoopIn swaps by their states and that the deposits
// associated with the swaps are correctly populated.
func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) {
// Set up test context objects.
ctxb := context.Background()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
swapStore := NewSqlStore(
loopdb.NewTypedStore[Querier](testDb), testClock,
&chaincfg.RegressionNetParams,
)
newID := func() deposit.ID {
did, err := deposit.GetRandomDepositID()
require.NoError(t, err)
return did
}
loopingDepositID := newID()
loopedInDepositID := newID()
failedDepositID := newID()
d1, d2, d3 := &deposit.Deposit{
ID: loopingDepositID,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
Index: 0,
},
Value: btcutil.Amount(100_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
},
},
&deposit.Deposit{
ID: loopedInDepositID,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e},
Index: 1,
},
Value: btcutil.Amount(200_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d,
},
},
&deposit.Deposit{
ID: failedDepositID,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x3a, 0x2b, 0x3c, 0x4e},
Index: 2,
},
Value: btcutil.Amount(300_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4f,
},
}
err := depositStore.CreateDeposit(ctxb, d1)
require.NoError(t, err)
err = depositStore.CreateDeposit(ctxb, d2)
require.NoError(t, err)
err = depositStore.CreateDeposit(ctxb, d3)
require.NoError(t, err)
// Add two updates per deposit, expect the last to be retrieved.
d1.SetState(deposit.Deposited)
d2.SetState(deposit.Deposited)
d3.SetState(deposit.Deposited)
err = depositStore.UpdateDeposit(ctxb, d1)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d2)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d3)
require.NoError(t, err)
d1.SetState(deposit.LoopingIn)
d2.SetState(deposit.LoopedIn)
d3.SetState(deposit.Deposited)
err = depositStore.UpdateDeposit(ctxb, d1)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d2)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctxb, d3)
require.NoError(t, err)
_, clientPubKey := test.CreateKey(1)
_, serverPubKey := test.CreateKey(2)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
// Create pending swap.
swapHashPending := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
swapPending := StaticAddressLoopIn{
SwapHash: swapHashPending,
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
DepositOutpoints: []string{d1.OutPoint.String()},
Deposits: []*deposit.Deposit{d1},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapPending.SetState(SignHtlcTx)
err = swapStore.CreateLoopIn(ctxb, &swapPending)
require.NoError(t, err)
// Create succeeded swap.
swapHashSucceeded := lntypes.Hash{0x2, 0x2, 0x3, 0x5}
swapSucceeded := StaticAddressLoopIn{
SwapHash: swapHashSucceeded,
SwapPreimage: lntypes.Preimage{0x2, 0x2, 0x3, 0x5},
DepositOutpoints: []string{d2.OutPoint.String()},
Deposits: []*deposit.Deposit{d2},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapSucceeded.SetState(Succeeded)
err = swapStore.CreateLoopIn(ctxb, &swapSucceeded)
require.NoError(t, err)
// Create failed swap. Failed is the last final state, so this
// exercises the state-list query boundary.
swapHashFailed := lntypes.Hash{0x3, 0x2, 0x3, 0x5}
swapFailed := StaticAddressLoopIn{
SwapHash: swapHashFailed,
SwapPreimage: lntypes.Preimage{0x3, 0x2, 0x3, 0x5},
DepositOutpoints: []string{d3.OutPoint.String()},
Deposits: []*deposit.Deposit{d3},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapFailed.SetState(Failed)
err = swapStore.CreateLoopIn(ctxb, &swapFailed)
require.NoError(t, err)
pendingSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, PendingStates)
require.NoError(t, err)
require.Len(t, pendingSwaps, 1)
require.Equal(t, swapHashPending, pendingSwaps[0].SwapHash)
require.Equal(t, []string{d1.OutPoint.String()}, pendingSwaps[0].DepositOutpoints)
require.Equal(t, SignHtlcTx, pendingSwaps[0].GetState())
pendingDeposits := pendingSwaps[0].Deposits
require.Len(t, pendingDeposits, 1)
require.Equal(t, d1.ID, pendingDeposits[0].ID)
require.Equal(t, d1.OutPoint, pendingDeposits[0].OutPoint)
require.Equal(t, d1.Value, pendingDeposits[0].Value)
require.Equal(t, deposit.LoopingIn, pendingDeposits[0].GetState())
finalizedSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, FinalStates)
require.NoError(t, err)
require.Len(t, finalizedSwaps, 2)
require.Equal(t, swapHashSucceeded, finalizedSwaps[0].SwapHash)
require.Equal(t, []string{d2.OutPoint.String()}, finalizedSwaps[0].DepositOutpoints)
require.Equal(t, Succeeded, finalizedSwaps[0].GetState())
require.Equal(t, swapHashFailed, finalizedSwaps[1].SwapHash)
require.Equal(t, Failed, finalizedSwaps[1].GetState())
finalizedDeposits := finalizedSwaps[0].Deposits
require.Len(t, finalizedDeposits, 1)
require.Equal(t, d2.ID, finalizedDeposits[0].ID)
require.Equal(t, d2.OutPoint, finalizedDeposits[0].OutPoint)
require.Equal(t, d2.Value, finalizedDeposits[0].Value)
require.Equal(t, deposit.LoopedIn, finalizedDeposits[0].GetState())
}
// TestCreateLoopIn tests that CreateLoopIn correctly creates a new
// StaticAddressLoopIn swap and associates it with the provided deposits.
func TestCreateLoopIn(t *testing.T) {
// Set up test context objects.
ctx := t.Context()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
swapStore := NewSqlStore(
loopdb.NewTypedStore[Querier](testDb), testClock,
&chaincfg.RegressionNetParams,
)
newID := func() deposit.ID {
did, err := deposit.GetRandomDepositID()
require.NoError(t, err)
return did
}
d1, d2 := &deposit.Deposit{
ID: newID(),
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
Index: 0,
},
Value: btcutil.Amount(100_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
},
},
&deposit.Deposit{
ID: newID(),
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e},
Index: 1,
},
Value: btcutil.Amount(200_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d,
},
}
err := depositStore.CreateDeposit(ctx, d1)
require.NoError(t, err)
err = depositStore.CreateDeposit(ctx, d2)
require.NoError(t, err)
d1.SetState(deposit.LoopingIn)
d2.SetState(deposit.LoopingIn)
err = depositStore.UpdateDeposit(ctx, d1)
require.NoError(t, err)
err = depositStore.UpdateDeposit(ctx, d2)
require.NoError(t, err)
_, clientPubKey := test.CreateKey(1)
_, serverPubKey := test.CreateKey(2)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
// Create pending swap.
swapHashPending := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
swapPending := StaticAddressLoopIn{
SwapHash: swapHashPending,
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
DepositOutpoints: []string{d1.OutPoint.String(),
d2.OutPoint.String()},
Deposits: []*deposit.Deposit{d1, d2},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swapPending.SetState(SignHtlcTx)
err = swapStore.CreateLoopIn(ctx, &swapPending)
require.NoError(t, err)
depositIDs, err := swapStore.DepositIDsForSwapHash(
ctx, swapHashPending,
)
require.NoError(t, err)
require.Len(t, depositIDs, 2)
require.Contains(t, depositIDs, d1.ID)
require.Contains(t, depositIDs, d2.ID)
swapHashes, err := swapStore.SwapHashesForDepositIDs(
ctx, []deposit.ID{depositIDs[0], depositIDs[1]},
)
require.NoError(t, err)
require.Len(t, swapHashes, 1)
require.Len(t, swapHashes[swapHashPending], 2)
require.Contains(t, swapHashes[swapHashPending], depositIDs[0])
require.Contains(t, swapHashes[swapHashPending], depositIDs[1])
swap, err := swapStore.GetLoopInByHash(ctx, swapHashPending)
require.NoError(t, err)
require.Equal(t, swapHashPending, swap.SwapHash)
require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()},
swap.DepositOutpoints)
require.Equal(t, SignHtlcTx, swap.GetState())
require.Equal(
t, ConfirmationRiskDecisionNone,
swap.ConfirmationRiskDecision,
)
decisionTime := time.Unix(123, 0).UTC()
testClock.SetTime(decisionTime)
err = swapStore.RecordStaticAddressRiskDecision(
ctx, swapHashPending, ConfirmationRiskDecisionAccepted,
)
require.NoError(t, err)
swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending)
require.NoError(t, err)
require.Equal(
t, ConfirmationRiskDecisionAccepted,
swap.ConfirmationRiskDecision,
)
require.True(t, swap.ConfirmationRiskDecisionTime.Equal(decisionTime))
err = swapStore.RecordStaticAddressRiskDecision(
ctx, lntypes.Hash{0x9, 0x9, 0x9},
ConfirmationRiskDecisionRejected,
)
require.ErrorIs(t, err, ErrLoopInNotFound)
require.Len(t, swap.Deposits, 2)
require.Equal(t, d1.ID, swap.Deposits[0].ID)
require.Equal(t, d1.OutPoint, swap.Deposits[0].OutPoint)
require.Equal(t, d1.Value, swap.Deposits[0].Value)
require.Equal(t, deposit.LoopingIn, swap.Deposits[0].GetState())
require.Equal(t, d2.ID, swap.Deposits[1].ID)
require.Equal(t, d2.OutPoint, swap.Deposits[1].OutPoint)
require.Equal(t, d2.Value, swap.Deposits[1].Value)
require.Equal(t, deposit.LoopingIn, swap.Deposits[1].GetState())
updateTime := testClock.Now().Add(time.Minute)
testClock.SetTime(updateTime)
swapPending.SetState(Succeeded)
err = swapStore.UpdateLoopIn(ctx, &swapPending)
require.NoError(t, err)
swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending)
require.NoError(t, err)
require.Equal(t, Succeeded, swap.GetState())
require.WithinDuration(
t, updateTime.UTC(), swap.LastUpdateTime.UTC(),
time.Microsecond,
)
}
// TestGetLoopInByHashPreservesStoredDepositOutpoints ensures recovered loop-ins
// keep the original outpoint snapshot stored when the swap was created.
func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) {
ctxb := context.Background()
testDb := loopdb.NewTestDB(t)
testClock := clock.NewTestClock(time.Now())
defer testDb.Close()
depositStore := deposit.NewSqlStore(testDb.BaseDB)
swapStore := NewSqlStore(
loopdb.NewTypedStore[Querier](testDb), testClock,
&chaincfg.RegressionNetParams,
)
depositID, err := deposit.GetRandomDepositID()
require.NoError(t, err)
oldOutpoint := wire.OutPoint{
Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d},
Index: 0,
}
currentOutpoint := wire.OutPoint{
Hash: chainhash.Hash{0x5a, 0x6b, 0x7c, 0x8d},
Index: 1,
}
d := &deposit.Deposit{
ID: depositID,
OutPoint: oldOutpoint,
Value: btcutil.Amount(100_000),
TimeOutSweepPkScript: []byte{
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
},
}
require.NoError(t, depositStore.CreateDeposit(ctxb, d))
d.SetState(deposit.LoopingIn)
require.NoError(t, depositStore.UpdateDeposit(ctxb, d))
_, clientPubKey := test.CreateKey(1)
_, serverPubKey := test.CreateKey(2)
addr, err := btcutil.DecodeAddress(P2wkhAddr, nil)
require.NoError(t, err)
swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4}
swap := StaticAddressLoopIn{
SwapHash: swapHash,
SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4},
DepositOutpoints: []string{oldOutpoint.String()},
Deposits: []*deposit.Deposit{d},
ClientPubkey: clientPubKey,
ServerPubkey: serverPubKey,
HtlcTimeoutSweepAddress: addr,
}
swap.SetState(SignHtlcTx)
require.NoError(t, swapStore.CreateLoopIn(ctxb, &swap))
d.OutPoint = currentOutpoint
d.ConfirmationHeight = 42
require.NoError(t, depositStore.UpdateDeposit(ctxb, d))
storedSwap, err := swapStore.GetLoopInByHash(ctxb, swapHash)
require.NoError(t, err)
require.Equal(
t, []string{oldOutpoint.String()},
storedSwap.DepositOutpoints,
)
require.Len(t, storedSwap.Deposits, 1)
require.Equal(t, currentOutpoint, storedSwap.Deposits[0].OutPoint)
require.Equal(t, int64(42), storedSwap.Deposits[0].ConfirmationHeight)
}