diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index c56b47548..fe4bbacde 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -1011,6 +1011,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry { panic(err) } + modifierMock := &invoices.MockHtlcModifier{} registry := invoices.NewRegistry( cdb, invoices.NewInvoiceExpiryWatcher( @@ -1019,6 +1020,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry { ), &invoices.RegistryConfig{ FinalCltvRejectDelta: 5, + HtlcModifier: modifierMock, }, ) registry.Start() diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index de731b474..2e28be366 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -74,6 +74,11 @@ type RegistryConfig struct { // KeysendHoldTime indicates for how long we want to accept and hold // spontaneous keysend payments. KeysendHoldTime time.Duration + + // HtlcModifier is a service that intercepts invoice HTLCs during the + // settlement phase, enabling a subscribed client to modify certain + // aspects of those HTLCs. + HtlcModifier HtlcModifier } // htlcReleaseEvent describes an htlc auto-release event. It is used to release @@ -998,6 +1003,53 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( ) callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) { + // Provide the invoice to the settlement interceptor to allow + // the interceptor's client an opportunity to manipulate the + // settlement process. + clientReq := HtlcModifyRequest{ + ExitHtlcCircuitKey: ctx.circuitKey, + ExitHtlcAmt: ctx.amtPaid, + ExitHtlcExpiry: ctx.expiry, + CurrentHeight: uint32(ctx.currentHeight), + Invoice: *inv, + } + interceptSession := i.cfg.HtlcModifier.Intercept( + clientReq, + ) + + // If the interceptor service has provided a response, we'll + // use the interceptor session to wait for the client to respond + // with a settlement resolution. + var interceptErr error + interceptSession.WhenSome(func(session InterceptSession) { + log.Debug("Waiting for client response from invoice " + + "HTLC interceptor session") + + select { + case resp := <-session.ClientResponseChannel: + log.Debugf("Received invoice HTLC interceptor "+ + "response: %v", resp) + + if resp.AmountPaid != 0 { + ctx.amtPaid = resp.AmountPaid + } + + case err := <-session.ClientErrChannel: + log.Errorf("Error from invoice HTLC "+ + "interceptor session: %v", err) + + interceptErr = err + + case <-session.Quit: + // At this point, the interceptor session has + // quit. + } + }) + if interceptErr != nil { + return nil, fmt.Errorf("error during invoice HTLC "+ + "interception: %w", interceptErr) + } + updateDesc, res, err := updateInvoice(ctx, inv) if err != nil { return nil, err @@ -1051,6 +1103,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, @@ -1183,7 +1237,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..78de71883 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, + HtlcModifier: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( @@ -683,6 +690,7 @@ func testCancelHoldInvoice(t *testing.T, cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, Clock: testClock, + HtlcModifier: 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, + HtlcModifier: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( @@ -1310,6 +1319,7 @@ func testOldInvoiceRemovalOnStart(t *testing.T, FinalCltvRejectDelta: testFinalCltvRejectDelta, Clock: testClock, GcCanceledInvoicesOnStartup: true, + HtlcModifier: htlcModifierMock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( diff --git a/invoices/test_utils_test.go b/invoices/test_utils_test.go index a0adf7dc8..aca5f3eac 100644 --- a/invoices/test_utils_test.go +++ b/invoices/test_utils_test.go @@ -143,6 +143,7 @@ func defaultRegistryConfig() invpkg.RegistryConfig { return invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, HtlcHoldDuration: 30 * time.Second, + HtlcModifier: &invpkg.MockHtlcModifier{}, } }