staticaddr/loopin: recover risk-decision deadlines

Record replayed server risk decisions through the loop-in store,
recover accepted payment-deadline timers using the persisted decision
time, and handle persisted rejections on restart. This lets recovered
static loop-ins keep pending confirmation-risk state instead of
restarting payment timing from scratch.
This commit is contained in:
Slyghtning 2026-07-08 14:00:00 +02:00
parent f01a1f02d9
commit 6593afc8bc
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
5 changed files with 656 additions and 55 deletions

View file

@ -536,6 +536,14 @@ func (s *mockStaticAddressLoopInStore) IsStored(_ context.Context,
return false, nil
}
// RecordStaticAddressRiskDecision satisfies the static loop-in store interface.
func (s *mockStaticAddressLoopInStore) RecordStaticAddressRiskDecision(
_ context.Context, _ lntypes.Hash,
_ loopin.ConfirmationRiskDecision) error {
return nil
}
// GetLoopInByHash returns the configured loop-in with the given hash.
func (s *mockStaticAddressLoopInStore) GetLoopInByHash(_ context.Context,
swapHash lntypes.Hash) (*loopin.StaticAddressLoopIn, error) {

View file

@ -926,12 +926,18 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
deposit.SweepHtlcTimeout,
)
startPaymentDeadline := func(reason string) {
startPaymentDeadline := func(reason string, startedAt time.Time) {
if deadlineStarted || invoice.State == invoices.ContractCanceled {
return
}
timeout := f.loopIn.PaymentTimeoutDuration()
if !startedAt.IsZero() {
timeout -= time.Since(startedAt)
if timeout < 0 {
timeout = 0
}
}
f.Infof("starting payment deadline after %s", reason)
deadlineTimer = time.NewTimer(timeout)
@ -963,6 +969,16 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
depositsLockedForHtlcTimeout = true
}
startLegacyFallback := func(reason string, currentHeight int32) {
if deadlineStarted || invoice.State == invoices.ContractCanceled {
return
}
if f.shouldStartLegacyConfirmationFallback(ctx, currentHeight) {
startPaymentDeadline(reason, time.Time{})
}
}
if invoice.State == invoices.ContractCanceled {
// If the invoice was canceled previously we end our
// subscription to invoice updates.
@ -983,6 +999,108 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
invoiceCanceledForNonPayment = true
}
riskDecisionTime := func(decision ConfirmationRiskDecision) time.Time {
now := time.Now()
if f.cfg.Store == nil {
return now
}
storedLoopIn, err := f.cfg.Store.GetLoopInByHash(
ctx, f.loopIn.SwapHash,
)
if err != nil {
f.Warnf("unable to reload persisted risk decision for "+
"swap %v: %v", f.loopIn.SwapHash, err)
return now
}
if storedLoopIn == nil {
return now
}
hasPersistedDecision :=
storedLoopIn.ConfirmationRiskDecision == decision &&
!storedLoopIn.ConfirmationRiskDecisionTime.IsZero()
if !hasPersistedDecision {
err = f.cfg.Store.RecordStaticAddressRiskDecision(
ctx, f.loopIn.SwapHash, decision,
)
if err != nil {
f.Warnf("unable to persist replayed risk "+
"decision for swap %v: %v",
f.loopIn.SwapHash, err)
return now
}
storedLoopIn, err = f.cfg.Store.GetLoopInByHash(
ctx, f.loopIn.SwapHash,
)
if err != nil {
f.Warnf("unable to reload persisted risk "+
"decision for swap %v: %v",
f.loopIn.SwapHash, err)
return now
}
if storedLoopIn == nil ||
storedLoopIn.ConfirmationRiskDecision != decision ||
storedLoopIn.ConfirmationRiskDecisionTime.IsZero() {
return now
}
}
f.loopIn.ConfirmationRiskDecision =
storedLoopIn.ConfirmationRiskDecision
f.loopIn.ConfirmationRiskDecisionTime =
storedLoopIn.ConfirmationRiskDecisionTime
return storedLoopIn.ConfirmationRiskDecisionTime
}
handleRiskRejected := func(reason string) fsm.EventType {
cancelInvoiceSubscription()
f.cancelSwapInvoice()
invoice.State = invoices.ContractCanceled
invoiceCanceledForNonPayment = true
decisionTime := riskDecisionTime(
ConfirmationRiskDecisionRejected,
)
f.loopIn.ConfirmationRiskDecision =
ConfirmationRiskDecisionRejected
f.loopIn.ConfirmationRiskDecisionTime = decisionTime
riskAcceptedChan = nil
riskRejectedChan = nil
return f.HandleError(fmt.Errorf(
"server rejected confirmation risk wait after %s", reason,
))
}
switch f.loopIn.ConfirmationRiskDecision {
case ConfirmationRiskDecisionAccepted:
startPaymentDeadline(
"recovered risk accepted notification",
f.loopIn.ConfirmationRiskDecisionTime,
)
case ConfirmationRiskDecisionRejected:
return handleRiskRejected("recovered risk rejection")
}
info, err := f.cfg.LndClient.GetInfo(ctx)
if err != nil {
f.Warnf("unable to query current height for legacy confirmation "+
"fallback: %v", err)
} else {
startLegacyFallback(
"legacy confirmation fallback", int32(info.BlockHeight),
)
}
htlcConfirmed := false
for {
select {
@ -1070,7 +1188,16 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
continue
}
startPaymentDeadline("risk accepted notification")
startedAt := riskDecisionTime(
ConfirmationRiskDecisionAccepted,
)
f.loopIn.ConfirmationRiskDecision =
ConfirmationRiskDecisionAccepted
f.loopIn.ConfirmationRiskDecisionTime = startedAt
startPaymentDeadline(
"risk accepted notification",
f.loopIn.ConfirmationRiskDecisionTime,
)
case riskRejected, ok := <-riskRejectedChan:
if !ok {
@ -1085,47 +1212,12 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
continue
}
cancelInvoiceSubscription()
f.cancelSwapInvoice()
return f.HandleError(errors.New(
"server rejected confirmation risk wait",
))
return handleRiskRejected("risk rejection")
case currentHeight := <-blockChan:
if !deadlineStarted &&
invoice.State != invoices.ContractCanceled {
err = f.refreshSelectedDeposits(ctx)
if err != nil {
f.Warnf("unable to refresh selected "+
"deposits for legacy confirmation "+
"fallback: %v", err)
} else {
depositConfirmationHeights :=
selectedDepositConfirmationHeights(
f.loopIn,
)
if legacyMinConfsReached(
f.loopIn.DepositOutpoints,
depositConfirmationHeights,
currentHeight,
) {
// This fallback is a compatibility path for
// servers that do not send confirmation-risk
// notifications. Reaching legacy MinConfs is
// treated as synthetic risk acceptance, so the
// payment window starts here just as it would
// when a modern server sends an acceptance
// notification.
startPaymentDeadline(
"legacy confirmation fallback",
)
}
}
}
startLegacyFallback(
"legacy confirmation fallback", currentHeight,
)
// If the htlc is confirmed but blockChan fires before
// htlcConfChan, we would wrongfully assume that the

View file

@ -124,7 +124,7 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) {
SwapHash: swapHash,
HtlcCltvExpiry: 2_000,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
InitiationTime: time.Now().Add(-time.Hour),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
@ -806,9 +806,10 @@ func TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline(t *testing.T) {
}
}
// TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected verifies that a server-side
// confirmation risk rejection is terminal for the client monitor action.
func TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected(t *testing.T) {
// TestMonitorInvoiceAndHtlcTxUsesPersistedAcceptedRiskTime verifies that live
// risk notifications use the durable receipt time, not the local channel
// receive time, when reconstructing the payment deadline.
func TestMonitorInvoiceAndHtlcTxUsesPersistedAcceptedRiskTime(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
@ -819,9 +820,124 @@ func TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected(t *testing.T) {
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
swapHash := lntypes.Hash{5, 6, 7}
swapHash := lntypes.Hash{4, 5, 7}
depositOutpoint := wire.OutPoint{
Hash: chainhash.Hash{9},
Hash: chainhash.Hash{8},
Index: 0,
}
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
HtlcCltvExpiry: 2_000,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PaymentTimeoutSeconds: 1,
DepositOutpoints: []string{
depositOutpoint.String(),
},
Deposits: []*deposit.Deposit{{
OutPoint: depositOutpoint,
}},
}
loopIn.SetState(MonitorInvoiceAndHtlcTx)
mockLnd.SetInvoice(&lndclient.Invoice{
Hash: swapHash,
State: invoices.ContractOpen,
})
notificationMgr := &mockNotificationManager{
riskAccepted: make(
chan *swapserverrpc.
ServerStaticLoopInRiskAcceptedNotification, 1,
),
}
cfg := &Config{
AddressManager: &mockAddressManager{
params: &script.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
ProtocolVersion: version.ProtocolVersion_V0,
},
},
ChainNotifier: mockLnd.ChainNotifier,
DepositManager: &noopDepositManager{},
InvoicesClient: mockLnd.LndServices.Invoices,
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
NotificationManager: notificationMgr,
Store: &mockStore{
loopIns: map[lntypes.Hash]*StaticAddressLoopIn{
swapHash: {
ConfirmationRiskDecision: ConfirmationRiskDecisionAccepted,
ConfirmationRiskDecisionTime: time.Now().Add(
-time.Minute,
),
},
},
},
}
f, err := NewFSM(ctx, loopIn, cfg, false)
require.NoError(t, err)
resultChan := make(chan fsm.EventType, 1)
go func() {
resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil)
}()
waitForMonitorSubscriptions(t, ctx, mockLnd)
select {
case hash := <-mockLnd.FailInvoiceChannel:
t.Fatalf("invoice canceled before risk acceptance: %v", hash)
case <-time.After(200 * time.Millisecond):
}
notificationMgr.riskAccepted <- &swapserverrpc.
ServerStaticLoopInRiskAcceptedNotification{
SwapHash: swapHash[:],
}
select {
case hash := <-mockLnd.FailInvoiceChannel:
require.Equal(t, swapHash, hash)
case <-ctx.Done():
t.Fatalf("invoice was not canceled: %v", ctx.Err())
}
cancel()
select {
case event := <-resultChan:
require.Equal(t, fsm.NoOp, event)
case <-time.After(time.Second):
t.Fatal("monitor action did not exit")
}
}
// TestMonitorInvoiceAndHtlcTxPersistsReplayedRiskAccepted verifies that a risk
// notification replayed after the swap row exists is written back to the store.
func TestMonitorInvoiceAndHtlcTxPersistsReplayedRiskAccepted(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
mockLnd := test.NewMockLnd()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
swapHash := lntypes.Hash{5, 6, 10}
depositOutpoint := wire.OutPoint{
Hash: chainhash.Hash{14},
Index: 0,
}
@ -849,11 +965,19 @@ func TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected(t *testing.T) {
})
notificationMgr := &mockNotificationManager{
riskRejected: make(
riskAccepted: make(
chan *swapserverrpc.
ServerStaticLoopInRiskRejectedNotification, 1,
ServerStaticLoopInRiskAcceptedNotification, 1,
),
}
store := &recordingRiskStore{
mockStore: &mockStore{
loopIns: map[lntypes.Hash]*StaticAddressLoopIn{
swapHash: {},
},
},
decisions: make(chan ConfirmationRiskDecision, 1),
}
cfg := &Config{
AddressManager: &mockAddressManager{
@ -869,6 +993,121 @@ func TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected(t *testing.T) {
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
NotificationManager: notificationMgr,
Store: store,
}
f, err := NewFSM(ctx, loopIn, cfg, false)
require.NoError(t, err)
resultChan := make(chan fsm.EventType, 1)
go func() {
resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil)
}()
waitForMonitorSubscriptions(t, ctx, mockLnd)
notificationMgr.riskAccepted <- &swapserverrpc.
ServerStaticLoopInRiskAcceptedNotification{
SwapHash: swapHash[:],
}
select {
case decision := <-store.decisions:
require.Equal(t, ConfirmationRiskDecisionAccepted, decision)
case <-ctx.Done():
t.Fatalf("risk decision was not persisted: %v", ctx.Err())
}
stored := store.loopIns[swapHash]
require.Equal(t, ConfirmationRiskDecisionAccepted,
stored.ConfirmationRiskDecision)
require.False(t, stored.ConfirmationRiskDecisionTime.IsZero())
cancel()
select {
case event := <-resultChan:
require.Equal(t, fsm.NoOp, event)
case <-time.After(time.Second):
t.Fatal("monitor action did not exit")
}
}
// TestMonitorInvoiceAndHtlcTxPersistsRiskRejected verifies that a server-side
// confirmation risk rejection is persisted and exits through the generic error
// path so the FSM unlocks deposits.
func TestMonitorInvoiceAndHtlcTxPersistsRiskRejected(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
mockLnd := test.NewMockLnd()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
swapHash := lntypes.Hash{5, 6, 7}
depositOutpoint := wire.OutPoint{
Hash: chainhash.Hash{9},
Index: 0,
}
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
HtlcCltvExpiry: mockLnd.Height,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PaymentTimeoutSeconds: 3_600,
DepositOutpoints: []string{
depositOutpoint.String(),
},
Deposits: []*deposit.Deposit{{
OutPoint: depositOutpoint,
}},
}
loopIn.SetState(MonitorInvoiceAndHtlcTx)
mockLnd.SetInvoice(&lndclient.Invoice{
Hash: swapHash,
State: invoices.ContractOpen,
})
notificationMgr := &mockNotificationManager{
riskRejected: make(
chan *swapserverrpc.
ServerStaticLoopInRiskRejectedNotification, 1,
),
}
store := &recordingRiskStore{
mockStore: &mockStore{
loopIns: map[lntypes.Hash]*StaticAddressLoopIn{
swapHash: {},
},
},
decisions: make(chan ConfirmationRiskDecision, 1),
}
cfg := &Config{
AddressManager: &mockAddressManager{
params: &script.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
ProtocolVersion: version.ProtocolVersion_V0,
},
},
ChainNotifier: mockLnd.ChainNotifier,
DepositManager: &noopDepositManager{},
InvoicesClient: mockLnd.LndServices.Invoices,
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
NotificationManager: notificationMgr,
Store: store,
}
f, err := NewFSM(ctx, loopIn, cfg, false)
@ -893,6 +1132,19 @@ func TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected(t *testing.T) {
t.Fatalf("invoice was not canceled: %v", ctx.Err())
}
select {
case decision := <-store.decisions:
require.Equal(t, ConfirmationRiskDecisionRejected, decision)
case <-ctx.Done():
t.Fatalf("risk decision was not persisted: %v", ctx.Err())
}
stored := store.loopIns[swapHash]
require.Equal(t, ConfirmationRiskDecisionRejected,
stored.ConfirmationRiskDecision)
require.False(t, stored.ConfirmationRiskDecisionTime.IsZero())
select {
case event := <-resultChan:
require.Equal(t, fsm.OnError, event)
@ -900,7 +1152,201 @@ func TestMonitorInvoiceAndHtlcTxCancelsOnRiskRejected(t *testing.T) {
t, f.LastActionError,
"server rejected confirmation risk wait",
)
case <-time.After(time.Second):
t.Fatal("monitor action did not exit")
}
}
// TestMonitorInvoiceAndHtlcTxRecoversAcceptedRiskDecision verifies that a
// persisted risk acceptance restarts the payment deadline with elapsed time
// preserved after restart.
func TestMonitorInvoiceAndHtlcTxRecoversAcceptedRiskDecision(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
mockLnd := test.NewMockLnd()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
swapHash := lntypes.Hash{5, 6, 8}
depositOutpoint := wire.OutPoint{
Hash: chainhash.Hash{12},
Index: 0,
}
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
HtlcCltvExpiry: 2_000,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PaymentTimeoutSeconds: 1,
ConfirmationRiskDecision: ConfirmationRiskDecisionAccepted,
ConfirmationRiskDecisionTime: time.Now().Add(-time.Minute),
DepositOutpoints: []string{
depositOutpoint.String(),
},
Deposits: []*deposit.Deposit{{
OutPoint: depositOutpoint,
}},
}
loopIn.SetState(MonitorInvoiceAndHtlcTx)
mockLnd.SetInvoice(&lndclient.Invoice{
Hash: swapHash,
State: invoices.ContractOpen,
})
depositMgr := &recordingDepositManager{
transitionChan: make(chan depositTransition, 1),
}
cfg := &Config{
AddressManager: &mockAddressManager{
params: &script.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
ProtocolVersion: version.ProtocolVersion_V0,
},
},
ChainNotifier: mockLnd.ChainNotifier,
DepositManager: depositMgr,
InvoicesClient: mockLnd.LndServices.Invoices,
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
}
f, err := NewFSM(ctx, loopIn, cfg, false)
require.NoError(t, err)
resultChan := make(chan fsm.EventType, 1)
go func() {
resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil)
}()
waitForMonitorSubscriptions(t, ctx, mockLnd)
select {
case hash := <-mockLnd.FailInvoiceChannel:
require.Equal(t, swapHash, hash)
case <-ctx.Done():
t.Fatalf("invoice was not canceled: %v", ctx.Err())
}
select {
case transition := <-depositMgr.transitionChan:
require.Equal(t, fsm.OnError, transition.event)
require.Equal(t, deposit.Deposited, transition.state)
case <-ctx.Done():
t.Fatalf("deposits were not unlocked: %v", ctx.Err())
}
cancel()
select {
case event := <-resultChan:
require.Equal(t, fsm.NoOp, event)
case <-time.After(time.Second):
t.Fatal("monitor action did not exit")
}
}
// TestMonitorInvoiceAndHtlcTxRecoversRejectedRiskDecision verifies that a
// persisted risk rejection still cancels after restart and exits through the
// generic error path so the FSM unlocks deposits.
func TestMonitorInvoiceAndHtlcTxRecoversRejectedRiskDecision(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
mockLnd := test.NewMockLnd()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
swapHash := lntypes.Hash{5, 6, 9}
depositOutpoint := wire.OutPoint{
Hash: chainhash.Hash{13},
Index: 0,
}
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
HtlcCltvExpiry: mockLnd.Height,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PaymentTimeoutSeconds: 3_600,
ConfirmationRiskDecision: ConfirmationRiskDecisionRejected,
ConfirmationRiskDecisionTime: time.Now(),
DepositOutpoints: []string{
depositOutpoint.String(),
},
Deposits: []*deposit.Deposit{{
OutPoint: depositOutpoint,
}},
}
loopIn.SetState(MonitorInvoiceAndHtlcTx)
mockLnd.SetInvoice(&lndclient.Invoice{
Hash: swapHash,
State: invoices.ContractOpen,
})
depositMgr := &recordingDepositManager{
transitionChan: make(chan depositTransition, 1),
}
cfg := &Config{
AddressManager: &mockAddressManager{
params: &script.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
ProtocolVersion: version.ProtocolVersion_V0,
},
},
ChainNotifier: mockLnd.ChainNotifier,
DepositManager: depositMgr,
InvoicesClient: mockLnd.LndServices.Invoices,
LndClient: mockLnd.Client,
ChainParams: mockLnd.ChainParams,
}
f, err := NewFSM(ctx, loopIn, cfg, false)
require.NoError(t, err)
resultChan := make(chan fsm.EventType, 1)
go func() {
resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil)
}()
waitForMonitorSubscriptions(t, ctx, mockLnd)
select {
case hash := <-mockLnd.FailInvoiceChannel:
require.Equal(t, swapHash, hash)
case <-ctx.Done():
t.Fatalf("invoice was not canceled: %v", ctx.Err())
}
select {
case event := <-resultChan:
require.Equal(t, fsm.OnError, event)
require.ErrorContains(
t, f.LastActionError,
"server rejected confirmation risk wait",
)
case <-time.After(time.Second):
t.Fatal("monitor action did not exit")
}
@ -1171,6 +1617,14 @@ func TestMonitorInvoiceAndHtlcTxStartsDeadlineAtLegacyMinConfs(t *testing.T) {
require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height))
select {
case hash := <-mockLnd.FailInvoiceChannel:
t.Fatalf("invoice canceled immediately after deposit "+
"confirmation: %v", hash)
case <-time.After(200 * time.Millisecond):
}
select {
case hash := <-mockLnd.FailInvoiceChannel:
require.Equal(t, swapHash, hash)
@ -1217,7 +1671,7 @@ func TestMonitorInvoiceAndHtlcTxStartsLegacyFallbackWithNotificationManager(
SwapHash: swapHash,
HtlcCltvExpiry: 2_000,
InitiationHeight: uint32(mockLnd.Height),
InitiationTime: time.Now(),
InitiationTime: time.Now().Add(-time.Hour),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
@ -2081,12 +2535,13 @@ type depositTransition struct {
type recordingDepositManager struct {
noopDepositManager
err error
transitions []depositTransition
transitionChan chan depositTransition
err error
errs []error
transitions []depositTransition
events []fsm.EventType
states []fsm.StateType
transitionChan chan depositTransition
events []fsm.EventType
states []fsm.StateType
}
// TransitionDeposits records the transition and returns the configured error.
@ -2108,9 +2563,43 @@ func (r *recordingDepositManager) TransitionDeposits(_ context.Context,
r.transitionChan <- transition
}
if len(r.errs) > 0 {
err := r.errs[0]
r.errs = r.errs[1:]
return err
}
return r.err
}
type recordingRiskStore struct {
*mockStore
decisions chan ConfirmationRiskDecision
}
// RecordStaticAddressRiskDecision records a risk decision in the mock store.
func (s *recordingRiskStore) RecordStaticAddressRiskDecision(
_ context.Context, swapHash lntypes.Hash,
decision ConfirmationRiskDecision) error {
loopIn, ok := s.loopIns[swapHash]
if !ok {
return ErrLoopInNotFound
}
loopIn.ConfirmationRiskDecision = decision
loopIn.ConfirmationRiskDecisionTime = time.Now()
select {
case s.decisions <- decision:
default:
}
return nil
}
// mockNotificationManager allows tests to push server notifications directly to
// monitor actions.
type mockNotificationManager struct {

View file

@ -91,6 +91,11 @@ type StaticAddressLoopInStore interface {
// IsStored checks if the loop-in is already stored in the database.
IsStored(ctx context.Context, swapHash lntypes.Hash) (bool, error)
// RecordStaticAddressRiskDecision persists the server's
// confirmation-risk decision for the loop-in identified by swapHash.
RecordStaticAddressRiskDecision(ctx context.Context,
swapHash lntypes.Hash, decision ConfirmationRiskDecision) error
// GetLoopInByHash returns the loop-in swap with the given hash.
GetLoopInByHash(ctx context.Context, swapHash lntypes.Hash) (
*StaticAddressLoopIn, error)

View file

@ -524,6 +524,13 @@ func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
return false, nil
}
// RecordStaticAddressRiskDecision implements Store for manager tests.
func (s *mockStore) RecordStaticAddressRiskDecision(context.Context,
lntypes.Hash, ConfirmationRiskDecision) error {
return nil
}
func (s *mockStore) GetLoopInByHash(_ context.Context,
swapHash lntypes.Hash) (*StaticAddressLoopIn, error) {