mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
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:
parent
f01a1f02d9
commit
6593afc8bc
5 changed files with 656 additions and 55 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue