mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/loopin: wait for risk decisions
Subscribe to static loop-in confirmation-risk notifications before starting the payment deadline. Start that deadline only after server acceptance or the legacy confirmation fallback, and cancel the swap invoice when the server rejects the risk wait. Refresh selected deposits before the legacy fallback so recovered monitors use current confirmation heights.
This commit is contained in:
parent
dad817a3b2
commit
d045d5ecd2
6 changed files with 1721 additions and 34 deletions
|
|
@ -26,6 +26,14 @@ const (
|
|||
// static loop in sweep requests.
|
||||
NotificationTypeStaticLoopInSweepRequest
|
||||
|
||||
// NotificationTypeStaticLoopInRiskAccepted is the notification type for
|
||||
// static loop in confirmation risk acceptance.
|
||||
NotificationTypeStaticLoopInRiskAccepted
|
||||
|
||||
// NotificationTypeStaticLoopInRiskRejected is the notification type for
|
||||
// static loop in confirmation risk rejection.
|
||||
NotificationTypeStaticLoopInRiskRejected
|
||||
|
||||
// NotificationTypeUnfinishedSwap is the notification type for unfinished
|
||||
// swap notifications.
|
||||
NotificationTypeUnfinishedSwap
|
||||
|
|
@ -92,6 +100,12 @@ type Manager struct {
|
|||
hasL402 bool
|
||||
|
||||
subscribers map[NotificationType][]subscriber
|
||||
|
||||
staticLoopInRiskAccepted map[lntypes.Hash]*swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification
|
||||
|
||||
staticLoopInRiskRejected map[lntypes.Hash]*swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification
|
||||
}
|
||||
|
||||
// NewManager creates a new notification manager.
|
||||
|
|
@ -107,6 +121,14 @@ func NewManager(cfg *Config) *Manager {
|
|||
return &Manager{
|
||||
cfg: cfg,
|
||||
subscribers: make(map[NotificationType][]subscriber),
|
||||
staticLoopInRiskAccepted: make(
|
||||
map[lntypes.Hash]*swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification,
|
||||
),
|
||||
staticLoopInRiskRejected: make(
|
||||
map[lntypes.Hash]*swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +136,7 @@ type subscriber struct {
|
|||
subCtx context.Context
|
||||
recvChan any
|
||||
enqueue func(any)
|
||||
swapHash *lntypes.Hash
|
||||
}
|
||||
|
||||
// newNotificationQueue creates a per-subscriber FIFO delivery function.
|
||||
|
|
@ -271,6 +294,80 @@ func (m *Manager) SubscribeStaticLoopInSweepRequests(ctx context.Context,
|
|||
return notifChan
|
||||
}
|
||||
|
||||
// SubscribeStaticLoopInRiskAccepted subscribes to static loop in risk accepted
|
||||
// notifications.
|
||||
func (m *Manager) SubscribeStaticLoopInRiskAccepted(ctx context.Context,
|
||||
swapHash lntypes.Hash,
|
||||
) <-chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification {
|
||||
|
||||
notifChan := make(
|
||||
chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification, 1,
|
||||
)
|
||||
|
||||
sub := subscriber{
|
||||
subCtx: ctx,
|
||||
recvChan: notifChan,
|
||||
swapHash: &swapHash,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
m.subscribers[NotificationTypeStaticLoopInRiskAccepted] = append(
|
||||
m.subscribers[NotificationTypeStaticLoopInRiskAccepted], sub,
|
||||
)
|
||||
if ntfn, ok := m.staticLoopInRiskAccepted[swapHash]; ok {
|
||||
notifChan <- ntfn
|
||||
delete(m.staticLoopInRiskAccepted, swapHash)
|
||||
}
|
||||
m.Unlock()
|
||||
|
||||
context.AfterFunc(ctx, func() {
|
||||
m.removeSubscriber(NotificationTypeStaticLoopInRiskAccepted, sub)
|
||||
m.Lock()
|
||||
delete(m.staticLoopInRiskAccepted, swapHash)
|
||||
m.Unlock()
|
||||
close(notifChan)
|
||||
})
|
||||
|
||||
return notifChan
|
||||
}
|
||||
|
||||
// SubscribeStaticLoopInRiskRejected subscribes to static loop in risk rejected
|
||||
// notifications.
|
||||
func (m *Manager) SubscribeStaticLoopInRiskRejected(ctx context.Context,
|
||||
swapHash lntypes.Hash,
|
||||
) <-chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification {
|
||||
|
||||
notifChan := make(
|
||||
chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification, 1,
|
||||
)
|
||||
|
||||
sub := subscriber{
|
||||
subCtx: ctx,
|
||||
recvChan: notifChan,
|
||||
swapHash: &swapHash,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
m.subscribers[NotificationTypeStaticLoopInRiskRejected] = append(
|
||||
m.subscribers[NotificationTypeStaticLoopInRiskRejected], sub,
|
||||
)
|
||||
if ntfn, ok := m.staticLoopInRiskRejected[swapHash]; ok {
|
||||
notifChan <- ntfn
|
||||
delete(m.staticLoopInRiskRejected, swapHash)
|
||||
}
|
||||
m.Unlock()
|
||||
|
||||
context.AfterFunc(ctx, func() {
|
||||
m.removeSubscriber(NotificationTypeStaticLoopInRiskRejected, sub)
|
||||
m.Lock()
|
||||
delete(m.staticLoopInRiskRejected, swapHash)
|
||||
m.Unlock()
|
||||
close(notifChan)
|
||||
})
|
||||
|
||||
return notifChan
|
||||
}
|
||||
|
||||
// SubscribeUnfinishedSwaps subscribes to the unfinished swap notifications.
|
||||
func (m *Manager) SubscribeUnfinishedSwaps(ctx context.Context,
|
||||
) <-chan *swapserverrpc.ServerUnfinishedSwapNotification {
|
||||
|
|
@ -476,6 +573,94 @@ func (m *Manager) handleNotification(ntfn *swapserverrpc.
|
|||
queueNotification(sub, recvChan, staticLoopInSweepRequestNtfn)
|
||||
}
|
||||
|
||||
case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInRiskAccepted: // nolint: lll
|
||||
// We'll forward the static loop in risk accepted notification to the
|
||||
// subscriber for the matching swap.
|
||||
riskAcceptedNtfn := ntfn.GetStaticLoopInRiskAccepted()
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
var (
|
||||
swapHash lntypes.Hash
|
||||
hasSwapHash bool
|
||||
)
|
||||
if riskAcceptedNtfn != nil {
|
||||
hash, err := lntypes.MakeHash(riskAcceptedNtfn.SwapHash)
|
||||
if err != nil {
|
||||
log.Warnf("Received invalid static loop in risk "+
|
||||
"accepted notification: %v", err)
|
||||
} else {
|
||||
swapHash = hash
|
||||
hasSwapHash = true
|
||||
m.staticLoopInRiskAccepted[hash] =
|
||||
riskAcceptedNtfn
|
||||
delete(m.staticLoopInRiskRejected, hash)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sub := range m.subscribers[NotificationTypeStaticLoopInRiskAccepted] { // nolint: lll
|
||||
if !hasSwapHash || sub.swapHash == nil ||
|
||||
*sub.swapHash != swapHash {
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
recvChan := sub.recvChan.(chan *swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification)
|
||||
|
||||
select {
|
||||
case recvChan <- riskAcceptedNtfn:
|
||||
case <-sub.subCtx.Done():
|
||||
default:
|
||||
log.Debugf("Dropping static loop in risk " +
|
||||
"accepted notification for slow subscriber")
|
||||
}
|
||||
}
|
||||
|
||||
case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInRiskRejected: // nolint: lll
|
||||
// We'll forward the static loop in risk rejected notification to the
|
||||
// subscriber for the matching swap.
|
||||
riskRejectedNtfn := ntfn.GetStaticLoopInRiskRejected()
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
var (
|
||||
swapHash lntypes.Hash
|
||||
hasSwapHash bool
|
||||
)
|
||||
if riskRejectedNtfn != nil {
|
||||
hash, err := lntypes.MakeHash(riskRejectedNtfn.SwapHash)
|
||||
if err != nil {
|
||||
log.Warnf("Received invalid static loop in risk "+
|
||||
"rejected notification: %v", err)
|
||||
} else {
|
||||
swapHash = hash
|
||||
hasSwapHash = true
|
||||
m.staticLoopInRiskRejected[hash] =
|
||||
riskRejectedNtfn
|
||||
delete(m.staticLoopInRiskAccepted, hash)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sub := range m.subscribers[NotificationTypeStaticLoopInRiskRejected] { // nolint: lll
|
||||
if !hasSwapHash || sub.swapHash == nil ||
|
||||
*sub.swapHash != swapHash {
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
recvChan := sub.recvChan.(chan *swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification)
|
||||
|
||||
select {
|
||||
case recvChan <- riskRejectedNtfn:
|
||||
case <-sub.subCtx.Done():
|
||||
default:
|
||||
log.Debugf("Dropping static loop in risk " +
|
||||
"rejected notification for slow subscriber")
|
||||
}
|
||||
}
|
||||
|
||||
case *swapserverrpc.SubscribeNotificationsResponse_UnfinishedSwap: // nolint: lll
|
||||
// We'll forward the unfinished swap notification to all
|
||||
// subscribers.
|
||||
|
|
|
|||
|
|
@ -220,6 +220,88 @@ func staticLoopInSweepNotification(
|
|||
}
|
||||
}
|
||||
|
||||
func staticLoopInRiskAcceptedNotification(
|
||||
swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse {
|
||||
|
||||
return &swapserverrpc.SubscribeNotificationsResponse{
|
||||
Notification: &swapserverrpc.
|
||||
SubscribeNotificationsResponse_StaticLoopInRiskAccepted{
|
||||
StaticLoopInRiskAccepted: &swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification{
|
||||
SwapHash: swapHash[:],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// staticLoopInRiskRejectedNotification builds a risk rejected notification.
|
||||
func staticLoopInRiskRejectedNotification(
|
||||
swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse {
|
||||
|
||||
return &swapserverrpc.SubscribeNotificationsResponse{
|
||||
Notification: &swapserverrpc.
|
||||
SubscribeNotificationsResponse_StaticLoopInRiskRejected{
|
||||
StaticLoopInRiskRejected: &swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification{
|
||||
SwapHash: swapHash[:],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type staticLoopInRiskNotification interface {
|
||||
GetSwapHash() []byte
|
||||
}
|
||||
|
||||
// assertStaticLoopInRiskNotificationSwapScoped checks swap-scoped fanout.
|
||||
func assertStaticLoopInRiskNotificationSwapScoped[
|
||||
T staticLoopInRiskNotification](t *testing.T,
|
||||
subscribe func(*Manager, context.Context, lntypes.Hash) <-chan T,
|
||||
notification func(lntypes.Hash) *swapserverrpc.
|
||||
SubscribeNotificationsResponse, label string,
|
||||
swapHashA, swapHashB lntypes.Hash) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
mgr := NewManager(&Config{})
|
||||
|
||||
subCtx, subCancel := context.WithCancel(t.Context())
|
||||
defer subCancel()
|
||||
|
||||
subChanA := subscribe(mgr, subCtx, swapHashA)
|
||||
subChanB := subscribe(mgr, subCtx, swapHashB)
|
||||
|
||||
mgr.handleNotification(notification(swapHashA))
|
||||
|
||||
select {
|
||||
case received := <-subChanA:
|
||||
require.Equal(t, swapHashA[:], received.GetSwapHash())
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("did not receive first swap risk %s notification",
|
||||
label)
|
||||
}
|
||||
|
||||
select {
|
||||
case received := <-subChanB:
|
||||
t.Fatalf("second swap received wrong notification: %x",
|
||||
received.GetSwapHash())
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
mgr.handleNotification(notification(swapHashB))
|
||||
|
||||
select {
|
||||
case received := <-subChanB:
|
||||
require.Equal(t, swapHashB[:], received.GetSwapHash())
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("did not receive second swap risk %s notification",
|
||||
label)
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager_SlowReservationSubscriberDoesNotBlock tests that a reservation
|
||||
// subscriber with a full notification channel does not block delivery to other
|
||||
// subscribers. Reservation notifications are best-effort, so slow subscribers
|
||||
|
|
@ -460,6 +542,178 @@ func assertQueuedSwapHashNotifications[T any](t *testing.T,
|
|||
}
|
||||
}
|
||||
|
||||
// TestManager_StaticLoopInRiskAcceptedNotification tests that the Manager
|
||||
// forwards static loop in risk accepted notifications to subscribers.
|
||||
func TestManager_StaticLoopInRiskAcceptedNotification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr := NewManager(&Config{})
|
||||
|
||||
subCtx, subCancel := context.WithCancel(t.Context())
|
||||
defer subCancel()
|
||||
|
||||
swapHash := lntypes.Hash{0x04, 0x05}
|
||||
|
||||
subChan := mgr.SubscribeStaticLoopInRiskAccepted(subCtx, swapHash)
|
||||
|
||||
mgr.handleNotification(
|
||||
&swapserverrpc.SubscribeNotificationsResponse{
|
||||
Notification: &swapserverrpc.
|
||||
SubscribeNotificationsResponse_StaticLoopInRiskAccepted{
|
||||
StaticLoopInRiskAccepted: &swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification{
|
||||
SwapHash: swapHash[:],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
select {
|
||||
case received := <-subChan:
|
||||
require.Equal(t, swapHash[:], received.SwapHash)
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("did not receive risk accepted notification")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager_StaticLoopInRiskAcceptedNotificationSwapScoped verifies that a
|
||||
// notification for one swap does not occupy another swap's subscriber channel.
|
||||
func TestManager_StaticLoopInRiskAcceptedNotificationSwapScoped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assertStaticLoopInRiskNotificationSwapScoped(
|
||||
t, func(m *Manager, ctx context.Context,
|
||||
swapHash lntypes.Hash) <-chan *swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification {
|
||||
|
||||
return m.SubscribeStaticLoopInRiskAccepted(ctx, swapHash)
|
||||
}, staticLoopInRiskAcceptedNotification, "accepted",
|
||||
lntypes.Hash{0x04, 0x05}, lntypes.Hash{0x06, 0x07},
|
||||
)
|
||||
}
|
||||
|
||||
// TestManager_StaticLoopInRiskAcceptedNotificationReplay tests that the Manager
|
||||
// replays a risk accepted notification that arrives before the swap-specific
|
||||
// subscriber is registered.
|
||||
func TestManager_StaticLoopInRiskAcceptedNotificationReplay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr := NewManager(&Config{})
|
||||
|
||||
swapHash := lntypes.Hash{0x06, 0x07}
|
||||
mgr.handleNotification(
|
||||
&swapserverrpc.SubscribeNotificationsResponse{
|
||||
Notification: &swapserverrpc.
|
||||
SubscribeNotificationsResponse_StaticLoopInRiskAccepted{
|
||||
StaticLoopInRiskAccepted: &swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification{
|
||||
SwapHash: swapHash[:],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
subCtx, subCancel := context.WithCancel(t.Context())
|
||||
defer subCancel()
|
||||
|
||||
subChan := mgr.SubscribeStaticLoopInRiskAccepted(subCtx, swapHash)
|
||||
|
||||
select {
|
||||
case received := <-subChan:
|
||||
require.Equal(t, swapHash[:], received.SwapHash)
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("did not replay risk accepted notification")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager_StaticLoopInRiskRejectedNotification tests that the Manager
|
||||
// forwards static loop in risk rejected notifications to subscribers.
|
||||
func TestManager_StaticLoopInRiskRejectedNotification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr := NewManager(&Config{})
|
||||
|
||||
subCtx, subCancel := context.WithCancel(t.Context())
|
||||
defer subCancel()
|
||||
|
||||
swapHash := lntypes.Hash{0x08, 0x09}
|
||||
|
||||
subChan := mgr.SubscribeStaticLoopInRiskRejected(subCtx, swapHash)
|
||||
|
||||
mgr.handleNotification(
|
||||
&swapserverrpc.SubscribeNotificationsResponse{
|
||||
Notification: &swapserverrpc.
|
||||
SubscribeNotificationsResponse_StaticLoopInRiskRejected{
|
||||
StaticLoopInRiskRejected: &swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification{
|
||||
SwapHash: swapHash[:],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
select {
|
||||
case received := <-subChan:
|
||||
require.Equal(t, swapHash[:], received.SwapHash)
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("did not receive risk rejected notification")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager_StaticLoopInRiskRejectedNotificationSwapScoped verifies that a
|
||||
// notification for one swap does not occupy another swap's subscriber channel.
|
||||
func TestManager_StaticLoopInRiskRejectedNotificationSwapScoped(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assertStaticLoopInRiskNotificationSwapScoped(
|
||||
t, func(m *Manager, ctx context.Context,
|
||||
swapHash lntypes.Hash) <-chan *swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification {
|
||||
|
||||
return m.SubscribeStaticLoopInRiskRejected(ctx, swapHash)
|
||||
}, staticLoopInRiskRejectedNotification, "rejected",
|
||||
lntypes.Hash{0x08, 0x09}, lntypes.Hash{0x0a, 0x0b},
|
||||
)
|
||||
}
|
||||
|
||||
// TestManager_StaticLoopInRiskRejectedNotificationReplay tests that the Manager
|
||||
// replays a risk rejected notification that arrives before the swap-specific
|
||||
// subscriber is registered.
|
||||
func TestManager_StaticLoopInRiskRejectedNotificationReplay(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mgr := NewManager(&Config{})
|
||||
|
||||
swapHash := lntypes.Hash{0x0a, 0x0b}
|
||||
mgr.handleNotification(
|
||||
&swapserverrpc.SubscribeNotificationsResponse{
|
||||
Notification: &swapserverrpc.
|
||||
SubscribeNotificationsResponse_StaticLoopInRiskRejected{
|
||||
StaticLoopInRiskRejected: &swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification{
|
||||
SwapHash: swapHash[:],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
subCtx, subCancel := context.WithCancel(t.Context())
|
||||
defer subCancel()
|
||||
|
||||
subChan := mgr.SubscribeStaticLoopInRiskRejected(subCtx, swapHash)
|
||||
|
||||
select {
|
||||
case received := <-subChan:
|
||||
require.Equal(t, swapHash[:], received.SwapHash)
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("did not replay risk rejected notification")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManager_Backoff verifies that repeated failures in
|
||||
// subscribeNotifications cause the Manager to space out subscription attempts
|
||||
// via a predictable incremental backoff.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
|
|
@ -389,6 +390,114 @@ func (f *FSM) handleInvoiceUpdate(update lndclient.InvoiceUpdate) (
|
|||
}
|
||||
}
|
||||
|
||||
// selectedDepositConfirmationHeights returns current confirmation heights for
|
||||
// the original deposit outpoints selected by this loop-in.
|
||||
func selectedDepositConfirmationHeights(
|
||||
loopIn *StaticAddressLoopIn) map[string]int64 {
|
||||
|
||||
confirmations := make(map[string]int64, len(loopIn.Deposits))
|
||||
outpoints := make(map[string]struct{}, len(loopIn.DepositOutpoints))
|
||||
for _, outpoint := range loopIn.DepositOutpoints {
|
||||
outpoints[outpoint] = struct{}{}
|
||||
}
|
||||
|
||||
for _, d := range loopIn.Deposits {
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
outpoint := d.OutPoint.String()
|
||||
confirmationHeight := d.GetConfirmationHeight()
|
||||
|
||||
if _, ok := outpoints[outpoint]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
confirmations[outpoint] = confirmationHeight
|
||||
}
|
||||
|
||||
return confirmations
|
||||
}
|
||||
|
||||
// refreshSelectedDeposits reloads the loop-in's selected deposits from the
|
||||
// deposit manager/store so recovery does not rely on stale deposit snapshots.
|
||||
func (f *FSM) refreshSelectedDeposits(ctx context.Context) error {
|
||||
if f.cfg.DepositManager == nil || len(f.loopIn.DepositOutpoints) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
const ignoreUnknownOutpoints = false
|
||||
deposits, err := f.cfg.DepositManager.DepositsForOutpoints(
|
||||
ctx, f.loopIn.DepositOutpoints, ignoreUnknownOutpoints,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deposits) != len(f.loopIn.DepositOutpoints) {
|
||||
return fmt.Errorf("expected %d selected deposits, got %d",
|
||||
len(f.loopIn.DepositOutpoints), len(deposits))
|
||||
}
|
||||
|
||||
f.loopIn.Deposits = deposits
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// legacyMinConfsReached returns true once every original deposit is confirmed
|
||||
// and the youngest original deposit has reached the legacy confirmation target.
|
||||
func legacyMinConfsReached(outpoints []string,
|
||||
confirmationHeights map[string]int64, currentHeight int32) bool {
|
||||
|
||||
if currentHeight <= 0 || len(outpoints) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
youngestConfirmation := int64(0)
|
||||
for _, outpoint := range outpoints {
|
||||
confirmationHeight, ok := confirmationHeights[outpoint]
|
||||
if !ok || confirmationHeight <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if confirmationHeight > youngestConfirmation {
|
||||
youngestConfirmation = confirmationHeight
|
||||
}
|
||||
}
|
||||
|
||||
return int64(currentHeight) >= youngestConfirmation+deposit.MinConfs-1
|
||||
}
|
||||
|
||||
// shouldStartLegacyConfirmationFallback reports whether the local MinConfs
|
||||
// payment deadline fallback should be armed at the current block height.
|
||||
//
|
||||
// The primary path starts the deadline from a server risk-accepted notification.
|
||||
// This fallback preserves the legacy client-side MinConfs behavior when no risk
|
||||
// decision has been observed locally: once every original deposit reaches
|
||||
// MinConfs, the client treats that as enough confirmation-risk clearance to
|
||||
// start the payment window. The selected deposits are refreshed first so
|
||||
// recovered swaps do not depend on stale in-memory deposit snapshots.
|
||||
func (f *FSM) shouldStartLegacyConfirmationFallback(ctx context.Context,
|
||||
currentHeight int32) bool {
|
||||
|
||||
err := f.refreshSelectedDeposits(ctx)
|
||||
if err != nil {
|
||||
f.Warnf("unable to refresh selected deposits for legacy "+
|
||||
"confirmation fallback: %v", err)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
depositConfirmationHeights := selectedDepositConfirmationHeights(
|
||||
f.loopIn,
|
||||
)
|
||||
|
||||
return legacyMinConfsReached(
|
||||
f.loopIn.DepositOutpoints, depositConfirmationHeights,
|
||||
currentHeight,
|
||||
)
|
||||
}
|
||||
|
||||
// originalDepositOutpointUnavailable checks the original selected deposit
|
||||
// outpoints against the chain backend's UTXO view.
|
||||
func (f *FSM) originalDepositOutpointUnavailable(ctx context.Context) (
|
||||
|
|
@ -653,7 +762,28 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
var (
|
||||
riskAcceptedChan <-chan *swapserverrpc.
|
||||
ServerStaticLoopInRiskAcceptedNotification
|
||||
riskRejectedChan <-chan *swapserverrpc.
|
||||
ServerStaticLoopInRiskRejectedNotification
|
||||
cancelRiskNotificationSubscriptions = func() {}
|
||||
)
|
||||
if f.cfg.NotificationManager != nil {
|
||||
notificationCtx, cancel := context.WithCancel(ctx)
|
||||
cancelRiskNotificationSubscriptions = cancel
|
||||
riskAcceptedChan = f.cfg.NotificationManager.
|
||||
SubscribeStaticLoopInRiskAccepted(
|
||||
notificationCtx, f.loopIn.SwapHash,
|
||||
)
|
||||
riskRejectedChan = f.cfg.NotificationManager.
|
||||
SubscribeStaticLoopInRiskRejected(
|
||||
notificationCtx, f.loopIn.SwapHash,
|
||||
)
|
||||
}
|
||||
defer cancelRiskNotificationSubscriptions()
|
||||
htlcConfirmed := false
|
||||
depositsUnlocked := false
|
||||
|
||||
invoice, err := f.cfg.LndClient.LookupInvoice(ctx, f.loopIn.SwapHash)
|
||||
if err != nil {
|
||||
|
|
@ -663,30 +793,34 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
// Create the swap payment timeout timer. If it runs out we cancel the
|
||||
// invoice, but keep monitoring the htlc confirmation.
|
||||
// If the invoice was canceled, e.g. before a restart, we don't need to
|
||||
// set a new deadline.
|
||||
var deadlineChan <-chan time.Time
|
||||
if invoice.State != invoices.ContractCanceled {
|
||||
// If the invoice is still live we set the timeout to the
|
||||
// remaining payment time. If too much time has elapsed, e.g.
|
||||
// after a restart, we set the timeout to 0 to cancel the
|
||||
// invoice and unlock the deposits immediately.
|
||||
remainingTimeSeconds := f.loopIn.RemainingPaymentTimeSeconds()
|
||||
// Create the swap payment timeout timer after the server confirms
|
||||
// confirmation risk was accepted. If a server does not support risk
|
||||
// notifications, fall back after the legacy deposit confirmation depth.
|
||||
var (
|
||||
deadlineChan <-chan time.Time
|
||||
deadlineTimer *time.Timer
|
||||
deadlineStarted bool
|
||||
)
|
||||
defer func() {
|
||||
if deadlineTimer != nil {
|
||||
deadlineTimer.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
// If the invoice isn't cancelled yet and the payment timeout
|
||||
// elapsed, we set the timeout to 0 to cancel the invoice and
|
||||
// unlock the deposits immediately. Otherwise, we start the
|
||||
// timer with the remaining seconds to timeout.
|
||||
timeout := time.Duration(0) * time.Second
|
||||
if remainingTimeSeconds > 0 {
|
||||
timeout = time.Duration(remainingTimeSeconds) *
|
||||
time.Second
|
||||
startPaymentDeadline := func(reason string) {
|
||||
if deadlineStarted || invoice.State == invoices.ContractCanceled {
|
||||
return
|
||||
}
|
||||
|
||||
deadlineChan = time.NewTimer(timeout).C
|
||||
} else {
|
||||
timeout := f.loopIn.PaymentTimeoutDuration()
|
||||
|
||||
f.Infof("starting payment deadline after %s", reason)
|
||||
deadlineTimer = time.NewTimer(timeout)
|
||||
deadlineChan = deadlineTimer.C
|
||||
deadlineStarted = true
|
||||
}
|
||||
|
||||
if invoice.State == invoices.ContractCanceled {
|
||||
// If the invoice was canceled previously we end our
|
||||
// subscription to invoice updates.
|
||||
cancelInvoiceSubscription()
|
||||
|
|
@ -743,6 +877,8 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
}
|
||||
|
||||
case <-deadlineChan:
|
||||
deadlineChan = nil
|
||||
|
||||
// If the server didn't pay the invoice on time, we
|
||||
// cancel the invoice and keep monitoring the htlc tx
|
||||
// confirmation. We also need to unlock the deposits to
|
||||
|
|
@ -753,9 +889,80 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
if err != nil {
|
||||
f.Errorf("unable to unlock deposits after "+
|
||||
"payment deadline: %v", err)
|
||||
continue
|
||||
}
|
||||
depositsUnlocked = true
|
||||
|
||||
case riskAccepted, ok := <-riskAcceptedChan:
|
||||
if !ok {
|
||||
riskAcceptedChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Equal(
|
||||
riskAccepted.SwapHash, f.loopIn.SwapHash[:],
|
||||
) {
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
startPaymentDeadline("risk accepted notification")
|
||||
|
||||
case riskRejected, ok := <-riskRejectedChan:
|
||||
if !ok {
|
||||
riskRejectedChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Equal(
|
||||
riskRejected.SwapHash, f.loopIn.SwapHash[:],
|
||||
) {
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
cancelInvoiceSubscription()
|
||||
f.cancelSwapInvoice()
|
||||
|
||||
return f.HandleError(errors.New(
|
||||
"server rejected confirmation risk wait",
|
||||
))
|
||||
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the htlc is confirmed but blockChan fires before
|
||||
// htlcConfChan, we would wrongfully assume that the
|
||||
// htlc tx was not confirmed which would lead to
|
||||
|
|
@ -781,13 +988,13 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
if !htlcConfirmed {
|
||||
f.Infof("swap timed out, htlc not confirmed")
|
||||
|
||||
// If the htlc hasn't confirmed but the timeout
|
||||
// path opened up, and we didn't receive the
|
||||
// swap payment, we consider the swap attempt to
|
||||
// be failed. We cancelled the invoice, but
|
||||
// don't need to unlock the deposits because
|
||||
// that happened when the payment deadline was
|
||||
// reached.
|
||||
if !depositsUnlocked {
|
||||
err = f.unlockDeposits(ctx)
|
||||
if err != nil {
|
||||
return f.HandleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
return OnSwapTimedOut
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -125,4 +125,18 @@ type NotificationManager interface {
|
|||
// a sweep of a static loop in that has been finished.
|
||||
SubscribeStaticLoopInSweepRequests(ctx context.Context,
|
||||
) <-chan *swapserverrpc.ServerStaticLoopInSweepNotification
|
||||
|
||||
// SubscribeStaticLoopInRiskAccepted subscribes to static loop in risk
|
||||
// accepted notifications. These are sent by the server after the selected
|
||||
// deposits are accepted by confirmation risk tracking.
|
||||
SubscribeStaticLoopInRiskAccepted(
|
||||
ctx context.Context, swapHash lntypes.Hash,
|
||||
) <-chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification
|
||||
|
||||
// SubscribeStaticLoopInRiskRejected subscribes to static loop in risk
|
||||
// rejected notifications. These are sent by the server if it aborts the
|
||||
// confirmation risk wait before payment.
|
||||
SubscribeStaticLoopInRiskRejected(
|
||||
ctx context.Context, swapHash lntypes.Hash,
|
||||
) <-chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification
|
||||
}
|
||||
|
|
|
|||
|
|
@ -467,12 +467,28 @@ func (l *StaticAddressLoopIn) TotalDepositAmount() btcutil.Amount {
|
|||
|
||||
// RemainingPaymentTimeSeconds returns the remaining time in seconds until the
|
||||
// payment timeout is reached. The remaining time is calculated from the
|
||||
// initiation time of the swap. If more than the swaps configured payment
|
||||
// initiation time of the swap. If more than the swap's configured payment
|
||||
// timeout has passed, the remaining time will be negative.
|
||||
func (l *StaticAddressLoopIn) RemainingPaymentTimeSeconds() int64 {
|
||||
elapsedSinceInitiation := time.Since(l.InitiationTime).Seconds()
|
||||
|
||||
return int64(l.PaymentTimeoutSeconds) - int64(elapsedSinceInitiation)
|
||||
return l.paymentTimeoutSeconds() - int64(elapsedSinceInitiation)
|
||||
}
|
||||
|
||||
// PaymentTimeoutDuration returns the configured payment timeout duration,
|
||||
// falling back to the default if the swap predates the persisted timeout field.
|
||||
func (l *StaticAddressLoopIn) PaymentTimeoutDuration() time.Duration {
|
||||
return time.Duration(l.paymentTimeoutSeconds()) * time.Second
|
||||
}
|
||||
|
||||
// paymentTimeoutSeconds returns the configured timeout in seconds.
|
||||
func (l *StaticAddressLoopIn) paymentTimeoutSeconds() int64 {
|
||||
timeoutSeconds := int64(l.PaymentTimeoutSeconds)
|
||||
if timeoutSeconds == 0 {
|
||||
timeoutSeconds = int64(DefaultPaymentTimeoutSeconds)
|
||||
}
|
||||
|
||||
return timeoutSeconds
|
||||
}
|
||||
|
||||
// Outpoints returns the wire outpoints of the deposits.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue