loopdb: persist static loop-in risk decisions

Add schema, sqlc queries, store fields, and SqlStore support for
recording server confirmation-risk decisions with static loop-in swaps.
Store the decision timestamp so payment-deadline recovery can
reconstruct elapsed time after restart.
This commit is contained in:
Slyghtning 2026-07-08 13:57:01 +02:00
parent b0f43bbe1e
commit 491fddbc34
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
9 changed files with 302 additions and 78 deletions

View file

@ -0,0 +1,3 @@
-- Drop confirmation-risk decision fields from static address loop-ins.
ALTER TABLE static_address_swaps DROP COLUMN confirmation_risk_decision;
ALTER TABLE static_address_swaps DROP COLUMN confirmation_risk_decision_time;

View file

@ -0,0 +1,15 @@
-- confirmation_risk_decision records the server's confirmation-risk decision
-- for a static address loop-in. Possible values are:
-- - '': no decision has been received yet;
-- - 'accepted': the server accepted waiting for the low-confirmation
-- deposits, which starts or reconstructs the payment deadline;
-- - 'rejected': the server stopped waiting for the low-confirmation deposits
-- before paying the invoice.
-- Once rejected, a later accepted update is ignored.
ALTER TABLE static_address_swaps ADD COLUMN confirmation_risk_decision TEXT NOT NULL DEFAULT '';
-- confirmation_risk_decision_time records when loopd received and persisted
-- the server's decision, so payment deadlines can be reconstructed after
-- restart. Same-decision replays preserve the original timestamp; changing
-- from accepted to rejected updates it.
ALTER TABLE static_address_swaps ADD COLUMN confirmation_risk_decision_time TIMESTAMP;

View file

@ -137,18 +137,20 @@ type StaticAddress struct {
} }
type StaticAddressSwap struct { type StaticAddressSwap struct {
ID int32 ID int32
SwapHash []byte SwapHash []byte
SwapInvoice string SwapInvoice string
LastHop []byte LastHop []byte
PaymentTimeoutSeconds int32 PaymentTimeoutSeconds int32
QuotedSwapFeeSatoshis int64 QuotedSwapFeeSatoshis int64
DepositOutpoints string DepositOutpoints string
HtlcTxFeeRateSatKw int64 HtlcTxFeeRateSatKw int64
HtlcTimeoutSweepTxID sql.NullString HtlcTimeoutSweepTxID sql.NullString
HtlcTimeoutSweepAddress string HtlcTimeoutSweepAddress string
SelectedAmount int64 SelectedAmount int64
Fast bool Fast bool
ConfirmationRiskDecision string
ConfirmationRiskDecisionTime sql.NullTime
} }
type StaticAddressSwapUpdate struct { type StaticAddressSwapUpdate struct {

View file

@ -67,6 +67,7 @@ type Querier interface {
MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error
OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSelectedSwapAmountParams) error OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSelectedSwapAmountParams) error
OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error
RecordStaticAddressRiskDecision(ctx context.Context, arg RecordStaticAddressRiskDecisionParams) error
SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error)
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error

View file

@ -33,6 +33,22 @@ SET
WHERE WHERE
swap_hash = $1; swap_hash = $1;
-- name: RecordStaticAddressRiskDecision :exec
UPDATE static_address_swaps
SET
confirmation_risk_decision = $2,
confirmation_risk_decision_time = CASE
WHEN confirmation_risk_decision = $2 THEN
COALESCE(confirmation_risk_decision_time, $3)
ELSE $3
END
WHERE
swap_hash = $1
AND NOT (
confirmation_risk_decision = 'rejected'
AND $2 = 'accepted'
);
-- name: InsertStaticAddressMetaUpdate :exec -- name: InsertStaticAddressMetaUpdate :exec
INSERT INTO static_address_swap_updates ( INSERT INTO static_address_swap_updates (
swap_hash, swap_hash,
@ -147,7 +163,3 @@ WHERE
d.swap_hash = $1; d.swap_hash = $1;

View file

@ -153,7 +153,7 @@ func (q *Queries) GetLoopInSwapUpdates(ctx context.Context, swapHash []byte) ([]
const getStaticAddressLoopInSwap = `-- name: GetStaticAddressLoopInSwap :one const getStaticAddressLoopInSwap = `-- name: GetStaticAddressLoopInSwap :one
SELECT SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label, swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM FROM
swaps swaps
@ -166,36 +166,38 @@ WHERE
` `
type GetStaticAddressLoopInSwapRow struct { type GetStaticAddressLoopInSwapRow struct {
ID int32 ID int32
SwapHash []byte SwapHash []byte
Preimage []byte Preimage []byte
InitiationTime time.Time InitiationTime time.Time
AmountRequested int64 AmountRequested int64
CltvExpiry int32 CltvExpiry int32
MaxMinerFee int64 MaxMinerFee int64
MaxSwapFee int64 MaxSwapFee int64
InitiationHeight int32 InitiationHeight int32
ProtocolVersion int32 ProtocolVersion int32
Label string Label string
ID_2 int32 ID_2 int32
SwapHash_2 []byte SwapHash_2 []byte
SwapInvoice string SwapInvoice string
LastHop []byte LastHop []byte
PaymentTimeoutSeconds int32 PaymentTimeoutSeconds int32
QuotedSwapFeeSatoshis int64 QuotedSwapFeeSatoshis int64
DepositOutpoints string DepositOutpoints string
HtlcTxFeeRateSatKw int64 HtlcTxFeeRateSatKw int64
HtlcTimeoutSweepTxID sql.NullString HtlcTimeoutSweepTxID sql.NullString
HtlcTimeoutSweepAddress string HtlcTimeoutSweepAddress string
SelectedAmount int64 SelectedAmount int64
Fast bool Fast bool
SwapHash_3 []byte ConfirmationRiskDecision string
SenderScriptPubkey []byte ConfirmationRiskDecisionTime sql.NullTime
ReceiverScriptPubkey []byte SwapHash_3 []byte
SenderInternalPubkey []byte SenderScriptPubkey []byte
ReceiverInternalPubkey []byte ReceiverScriptPubkey []byte
ClientKeyFamily int32 SenderInternalPubkey []byte
ClientKeyIndex int32 ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
} }
func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error) { func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error) {
@ -225,6 +227,8 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
&i.HtlcTimeoutSweepAddress, &i.HtlcTimeoutSweepAddress,
&i.SelectedAmount, &i.SelectedAmount,
&i.Fast, &i.Fast,
&i.ConfirmationRiskDecision,
&i.ConfirmationRiskDecisionTime,
&i.SwapHash_3, &i.SwapHash_3,
&i.SenderScriptPubkey, &i.SenderScriptPubkey,
&i.ReceiverScriptPubkey, &i.ReceiverScriptPubkey,
@ -239,7 +243,7 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt
const getStaticAddressLoopInSwapsByStates = `-- name: GetStaticAddressLoopInSwapsByStates :many const getStaticAddressLoopInSwapsByStates = `-- name: GetStaticAddressLoopInSwapsByStates :many
SELECT SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label, swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM FROM
swaps swaps
@ -263,36 +267,38 @@ ORDER BY
` `
type GetStaticAddressLoopInSwapsByStatesRow struct { type GetStaticAddressLoopInSwapsByStatesRow struct {
ID int32 ID int32
SwapHash []byte SwapHash []byte
Preimage []byte Preimage []byte
InitiationTime time.Time InitiationTime time.Time
AmountRequested int64 AmountRequested int64
CltvExpiry int32 CltvExpiry int32
MaxMinerFee int64 MaxMinerFee int64
MaxSwapFee int64 MaxSwapFee int64
InitiationHeight int32 InitiationHeight int32
ProtocolVersion int32 ProtocolVersion int32
Label string Label string
ID_2 int32 ID_2 int32
SwapHash_2 []byte SwapHash_2 []byte
SwapInvoice string SwapInvoice string
LastHop []byte LastHop []byte
PaymentTimeoutSeconds int32 PaymentTimeoutSeconds int32
QuotedSwapFeeSatoshis int64 QuotedSwapFeeSatoshis int64
DepositOutpoints string DepositOutpoints string
HtlcTxFeeRateSatKw int64 HtlcTxFeeRateSatKw int64
HtlcTimeoutSweepTxID sql.NullString HtlcTimeoutSweepTxID sql.NullString
HtlcTimeoutSweepAddress string HtlcTimeoutSweepAddress string
SelectedAmount int64 SelectedAmount int64
Fast bool Fast bool
SwapHash_3 []byte ConfirmationRiskDecision string
SenderScriptPubkey []byte ConfirmationRiskDecisionTime sql.NullTime
ReceiverScriptPubkey []byte SwapHash_3 []byte
SenderInternalPubkey []byte SenderScriptPubkey []byte
ReceiverInternalPubkey []byte ReceiverScriptPubkey []byte
ClientKeyFamily int32 SenderInternalPubkey []byte
ClientKeyIndex int32 ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
} }
func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error) { func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error) {
@ -328,6 +334,8 @@ func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dolla
&i.HtlcTimeoutSweepAddress, &i.HtlcTimeoutSweepAddress,
&i.SelectedAmount, &i.SelectedAmount,
&i.Fast, &i.Fast,
&i.ConfirmationRiskDecision,
&i.ConfirmationRiskDecisionTime,
&i.SwapHash_3, &i.SwapHash_3,
&i.SenderScriptPubkey, &i.SenderScriptPubkey,
&i.ReceiverScriptPubkey, &i.ReceiverScriptPubkey,
@ -482,6 +490,34 @@ func (q *Queries) OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSe
return err return err
} }
const recordStaticAddressRiskDecision = `-- name: RecordStaticAddressRiskDecision :exec
UPDATE static_address_swaps
SET
confirmation_risk_decision = $2,
confirmation_risk_decision_time = CASE
WHEN confirmation_risk_decision = $2 THEN
COALESCE(confirmation_risk_decision_time, $3)
ELSE $3
END
WHERE
swap_hash = $1
AND NOT (
confirmation_risk_decision = 'rejected'
AND $2 = 'accepted'
)
`
type RecordStaticAddressRiskDecisionParams struct {
SwapHash []byte
ConfirmationRiskDecision string
ConfirmationRiskDecisionTime sql.NullTime
}
func (q *Queries) RecordStaticAddressRiskDecision(ctx context.Context, arg RecordStaticAddressRiskDecisionParams) error {
_, err := q.db.ExecContext(ctx, recordStaticAddressRiskDecision, arg.SwapHash, arg.ConfirmationRiskDecision, arg.ConfirmationRiskDecisionTime)
return err
}
const swapHashForDepositID = `-- name: SwapHashForDepositID :one const swapHashForDepositID = `-- name: SwapHashForDepositID :one
SELECT SELECT
swap_hash swap_hash

View file

@ -31,6 +31,23 @@ import (
"github.com/lightningnetwork/lnd/zpay32" "github.com/lightningnetwork/lnd/zpay32"
) )
// ConfirmationRiskDecision records the server's decision on whether it accepts
// waiting for low-confirmation deposits before paying a static loop-in invoice.
type ConfirmationRiskDecision string
const (
// ConfirmationRiskDecisionNone means no risk decision has been received.
ConfirmationRiskDecisionNone ConfirmationRiskDecision = ""
// ConfirmationRiskDecisionAccepted means the server accepted waiting for
// deposit confirmations and the payment deadline has started.
ConfirmationRiskDecisionAccepted ConfirmationRiskDecision = "accepted"
// ConfirmationRiskDecisionRejected means the server stopped waiting for
// deposit confirmations before paying the invoice.
ConfirmationRiskDecisionRejected ConfirmationRiskDecision = "rejected"
)
// StaticAddressLoopIn represents the in-memory loop-in information. // StaticAddressLoopIn represents the in-memory loop-in information.
type StaticAddressLoopIn struct { type StaticAddressLoopIn struct {
// SwapHash is the hashed preimage of the swap invoice. It represents // SwapHash is the hashed preimage of the swap invoice. It represents
@ -107,6 +124,15 @@ type StaticAddressLoopIn struct {
// LastUpdateTime is the timestamp of the latest persisted state update. // LastUpdateTime is the timestamp of the latest persisted state update.
LastUpdateTime time.Time LastUpdateTime time.Time
// ConfirmationRiskDecision records the server's persisted decision on
// low-confirmation deposit risk.
ConfirmationRiskDecision ConfirmationRiskDecision
// ConfirmationRiskDecisionTime is when loopd persisted the server risk
// decision. It is used to reconstruct payment-deadline timeouts after
// restart.
ConfirmationRiskDecisionTime time.Time
// state is the current state of the swap. // state is the current state of the swap.
state fsm.StateType state fsm.StateType

View file

@ -27,6 +27,9 @@ var (
// ErrInvalidOutpoint is returned when an outpoint contains the outpoint // ErrInvalidOutpoint is returned when an outpoint contains the outpoint
// separator. // separator.
ErrInvalidOutpoint = errors.New("outpoint contains outpoint separator") ErrInvalidOutpoint = errors.New("outpoint contains outpoint separator")
// ErrLoopInNotFound is returned when a loop-in swap is not stored.
ErrLoopInNotFound = errors.New("static address loop-in not found")
) )
// Querier is the interface that contains all the queries generated by sqlc for // Querier is the interface that contains all the queries generated by sqlc for
@ -51,6 +54,11 @@ type Querier interface {
UpdateStaticAddressLoopIn(ctx context.Context, UpdateStaticAddressLoopIn(ctx context.Context,
arg sqlc.UpdateStaticAddressLoopInParams) error arg sqlc.UpdateStaticAddressLoopInParams) error
// RecordStaticAddressRiskDecision stores the server's confirmation-risk
// decision for a loop-in swap.
RecordStaticAddressRiskDecision(ctx context.Context,
arg sqlc.RecordStaticAddressRiskDecisionParams) error
// GetStaticAddressLoopInSwap retrieves a loop-in swap by its swap hash. // GetStaticAddressLoopInSwap retrieves a loop-in swap by its swap hash.
GetStaticAddressLoopInSwap(ctx context.Context, GetStaticAddressLoopInSwap(ctx context.Context,
swapHash []byte) (sqlc.GetStaticAddressLoopInSwapRow, error) swapHash []byte) (sqlc.GetStaticAddressLoopInSwapRow, error)
@ -361,6 +369,43 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context,
) )
} }
// RecordStaticAddressRiskDecision stores the server's confirmation-risk
// decision for a static address loop-in. The timestamp is written by the store
// so recovery can reconstruct the remaining payment deadline from one durable
// clock source.
func (s *SqlStore) RecordStaticAddressRiskDecision(ctx context.Context,
swapHash lntypes.Hash, decision ConfirmationRiskDecision) error {
if decision != ConfirmationRiskDecisionAccepted &&
decision != ConfirmationRiskDecisionRejected {
return errors.New("unknown confirmation risk decision")
}
params := sqlc.RecordStaticAddressRiskDecisionParams{
SwapHash: swapHash[:],
ConfirmationRiskDecision: string(decision),
ConfirmationRiskDecisionTime: sql.NullTime{
Time: s.clock.Now(),
Valid: true,
},
}
return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(),
func(q Querier) error {
stored, err := q.IsStored(ctx, swapHash[:])
if err != nil {
return err
}
if !stored {
return ErrLoopInNotFound
}
return q.RecordStaticAddressRiskDecision(ctx, params)
},
)
}
func (s *SqlStore) BatchUpdateSelectedSwapAmounts(ctx context.Context, func (s *SqlStore) BatchUpdateSelectedSwapAmounts(ctx context.Context,
updateAmounts map[lntypes.Hash]btcutil.Amount) error { updateAmounts map[lntypes.Hash]btcutil.Amount) error {
@ -584,6 +629,9 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
DepositOutpoints: depositOutpoints, DepositOutpoints: depositOutpoints,
SelectedAmount: btcutil.Amount(swap.SelectedAmount), SelectedAmount: btcutil.Amount(swap.SelectedAmount),
Fast: swap.Fast, Fast: swap.Fast,
ConfirmationRiskDecision: ConfirmationRiskDecision(
swap.ConfirmationRiskDecision,
),
HtlcTxFeeRate: chainfee.SatPerKWeight( HtlcTxFeeRate: chainfee.SatPerKWeight(
swap.HtlcTxFeeRateSatKw, swap.HtlcTxFeeRateSatKw,
), ),
@ -591,6 +639,10 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params,
HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash, HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash,
Deposits: depositList, Deposits: depositList,
} }
if swap.ConfirmationRiskDecisionTime.Valid {
loopIn.ConfirmationRiskDecisionTime =
swap.ConfirmationRiskDecisionTime.Time
}
if len(updates) > 0 { if len(updates) > 0 {
lastUpdate := updates[len(updates)-1] lastUpdate := updates[len(updates)-1]

View file

@ -349,6 +349,83 @@ func TestCreateLoopIn(t *testing.T) {
require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()}, require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()},
swap.DepositOutpoints) swap.DepositOutpoints)
require.Equal(t, SignHtlcTx, swap.GetState()) 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))
// Replaying the same decision must retain its original deadline anchor.
laterDecisionTime := decisionTime.Add(time.Hour)
testClock.SetTime(laterDecisionTime)
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))
// A different decision is a new event and receives a new timestamp.
rejectedDecisionTime := laterDecisionTime.Add(time.Hour)
testClock.SetTime(rejectedDecisionTime)
err = swapStore.RecordStaticAddressRiskDecision(
ctx, swapHashPending, ConfirmationRiskDecisionRejected,
)
require.NoError(t, err)
swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending)
require.NoError(t, err)
require.Equal(
t, ConfirmationRiskDecisionRejected,
swap.ConfirmationRiskDecision,
)
require.True(t, swap.ConfirmationRiskDecisionTime.Equal(
rejectedDecisionTime,
))
// Rejected is terminal: a racing synthetic acceptance must not replace
// the server's rejection or move its deadline anchor.
testClock.SetTime(rejectedDecisionTime.Add(time.Hour))
err = swapStore.RecordStaticAddressRiskDecision(
ctx, swapHashPending, ConfirmationRiskDecisionAccepted,
)
require.NoError(t, err)
swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending)
require.NoError(t, err)
require.Equal(
t, ConfirmationRiskDecisionRejected,
swap.ConfirmationRiskDecision,
)
require.True(t, swap.ConfirmationRiskDecisionTime.Equal(
rejectedDecisionTime,
))
err = swapStore.RecordStaticAddressRiskDecision(
ctx, lntypes.Hash{0x9, 0x9, 0x9},
ConfirmationRiskDecisionRejected,
)
require.ErrorIs(t, err, ErrLoopInNotFound)
require.Len(t, swap.Deposits, 2) require.Len(t, swap.Deposits, 2)