From c58b6a25a24c19f5f1639e44adc464e99eb6d77f Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 23 Apr 2024 14:33:31 +0100 Subject: [PATCH] invoices: integrate settlement interceptor with invoice registry This commit updates the invoice registry to utilize the settlement interceptor during the invoice settlement routine. It allows the interceptor to capture the invoice, providing interception clients an opportunity to determine the settlement outcome. --- htlcswitch/mock.go | 2 ++ invoices/invoiceregistry.go | 60 +++++++++++++++++++++++++++++--- invoices/invoiceregistry_test.go | 10 ++++++ invoices/test_utils_test.go | 1 + 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index 6123c9010..f43fc7a94 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -1014,6 +1014,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry { panic(err) } + modifierMock := &invoices.MockHtlcModifier{} registry := invoices.NewRegistry( cdb, invoices.NewInvoiceExpiryWatcher( @@ -1022,6 +1023,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry { ), &invoices.RegistryConfig{ FinalCltvRejectDelta: 5, + HtlcInterceptor: modifierMock, }, ) registry.Start() diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 7b3caa34a..c7f4cdb7b 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -74,6 +74,10 @@ type RegistryConfig struct { // KeysendHoldTime indicates for how long we want to accept and hold // spontaneous keysend payments. KeysendHoldTime time.Duration + + // HtlcInterceptor is an interface that allows the invoice registry to + // let clients intercept invoices before they are settled. + HtlcInterceptor HtlcInterceptor } // htlcReleaseEvent describes an htlc auto-release event. It is used to release @@ -1019,13 +1023,61 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( ctx *invoiceUpdateCtx, hodlChan chan<- interface{}) ( HtlcResolution, invoiceExpiry, error) { + invoiceRef := ctx.invoiceRef() + setID := (*SetID)(ctx.setID()) + + // We need to look up the current state of the invoice in order to send + // the previously accepted/settled HTLCs to the interceptor. + existingInvoice, err := i.idb.LookupInvoice( + context.Background(), invoiceRef, + ) + switch { + case errors.Is(err, ErrInvoiceNotFound) || + errors.Is(err, ErrNoInvoicesCreated): + + // If the invoice was not found, return a failure resolution + // with an invoice not found result. + return NewFailResolution( + ctx.circuitKey, ctx.currentHeight, + ResultInvoiceNotFound, + ), nil, nil + + case err != nil: + ctx.log(err.Error()) + return nil, nil, err + } + + // Provide the invoice to the settlement interceptor to allow + // the interceptor's client an opportunity to manipulate the + // settlement process. + err = i.cfg.HtlcInterceptor.Intercept(HtlcModifyRequest{ + ExitHtlcCircuitKey: ctx.circuitKey, + ExitHtlcAmt: ctx.amtPaid, + ExitHtlcExpiry: ctx.expiry, + CurrentHeight: uint32(ctx.currentHeight), + Invoice: existingInvoice, + }, func(resp HtlcModifyResponse) { + log.Debugf("Received invoice HTLC interceptor response: %v", + resp) + + if resp.AmountPaid != 0 { + ctx.amtPaid = resp.AmountPaid + } + }) + if err != nil { + err := fmt.Errorf("error during invoice HTLC interception: %w", + err) + ctx.log(err.Error()) + + return nil, nil, err + } + // We'll attempt to settle an invoice matching this rHash on disk (if // one exists). The callback will update the invoice state and/or htlcs. var ( resolution HtlcResolution updateSubscribers bool ) - callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) { updateDesc, res, err := updateInvoice(ctx, inv) if err != nil { @@ -1042,8 +1094,6 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( return updateDesc, nil } - invoiceRef := ctx.invoiceRef() - setID := (*SetID)(ctx.setID()) invoice, err := i.idb.UpdateInvoice( context.Background(), invoiceRef, setID, callback, ) @@ -1080,6 +1130,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( var invoiceToExpire invoiceExpiry + log.Tracef("Settlement resolution: %T %v", resolution, resolution) + switch res := resolution.(type) { case *HtlcFailResolution: // Inspect latest htlc state on the invoice. If it is found, @@ -1212,7 +1264,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( } // Now that the links have been notified of any state changes to their - // HTLCs, we'll go ahead and notify any clients wiaiting on the invoice + // HTLCs, we'll go ahead and notify any clients waiting on the invoice // state changes. if updateSubscribers { // We'll add a setID onto the notification, but only if this is diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go index b0c019522..b7b3d574a 100644 --- a/invoices/invoiceregistry_test.go +++ b/invoices/invoiceregistry_test.go @@ -23,6 +23,12 @@ import ( "github.com/stretchr/testify/require" ) +var ( + // htlcModifierMock is a mock implementation of the invoice HtlcModifier + // interface. + htlcModifierMock = &invpkg.MockHtlcModifier{} +) + // TestInvoiceRegistry is a master test which encompasses all tests using an // InvoiceDB instance. The purpose of this test is to be able to run all tests // with a custom DB instance, so that we can test the same logic with different @@ -517,6 +523,7 @@ func testSettleHoldInvoice(t *testing.T, cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, Clock: clock, + HtlcInterceptor: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( @@ -683,6 +690,7 @@ func testCancelHoldInvoice(t *testing.T, cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, Clock: testClock, + HtlcInterceptor: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( cfg.Clock, 0, uint32(testCurrentHeight), nil, newMockNotifier(), @@ -1200,6 +1208,7 @@ func testInvoiceExpiryWithRegistry(t *testing.T, cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, Clock: testClock, + HtlcInterceptor: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( @@ -1310,6 +1319,7 @@ func testOldInvoiceRemovalOnStart(t *testing.T, FinalCltvRejectDelta: testFinalCltvRejectDelta, Clock: testClock, GcCanceledInvoicesOnStartup: true, + HtlcInterceptor: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( diff --git a/invoices/test_utils_test.go b/invoices/test_utils_test.go index ed7bfccdd..fe69cad6f 100644 --- a/invoices/test_utils_test.go +++ b/invoices/test_utils_test.go @@ -153,6 +153,7 @@ func defaultRegistryConfig() invpkg.RegistryConfig { return invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, HtlcHoldDuration: 30 * time.Second, + HtlcInterceptor: &invpkg.MockHtlcModifier{}, } }