Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6/5]: invoice+rpc: add exit hop InvoiceAcceptor sub-systems and RPC calls #8771

Merged
merged 12 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contractcourt/htlc_incoming_contest_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(

resolution, err := h.Registry.NotifyExitHopHtlc(
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
circuitKey, hodlQueue.ChanIn(), payload,
circuitKey, hodlQueue.ChanIn(), nil, payload,
)
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions contractcourt/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Registry interface {
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error)

// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
Expand Down
1 change: 1 addition & 0 deletions contractcourt/mock_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type mockRegistry struct {
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error) {

r.notifyChan <- notifyExitHopData{
Expand Down
1 change: 1 addition & 0 deletions htlcswitch/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type InvoiceDatabase interface {
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error)

// CancelInvoice attempts to cancel the invoice corresponding to the
Expand Down
25 changes: 18 additions & 7 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -3530,13 +3530,24 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
return nil
}

// As we're the exit hop, we'll double check the hop-payload included in
// the HTLC to ensure that it was crafted correctly by the sender and
// is compatible with the HTLC we were extended.
if pd.Amount < fwdInfo.AmountToForward {
// As we're the exit hop, we'll double-check the hop-payload included
// in the HTLC to ensure that it was crafted correctly by the sender
// and is compatible with the HTLC we were extended.
//
// For a special case, if the fwdInfo doesn't have any blinded path
// information, and the incoming HTLC had special extra data, then
// we'll skip this amount check. The invoice acceptor will make sure we
// reject the HTLC if it's not containing the correct amount after
// examining the custom data.
hasBlindedPath := fwdInfo.NextBlinding.IsSome()
customHTLC := len(pd.CustomRecords) > 0 && !hasBlindedPath
log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
hasBlindedPath, customHTLC)

if !customHTLC && pd.Amount < fwdInfo.AmountToForward {
l.log.Errorf("onion payload of incoming htlc(%x) has "+
"incompatible value: expected <=%v, got %v", pd.RHash,
pd.Amount, fwdInfo.AmountToForward)
"incompatible value: expected >=%v, got %v", pd.RHash,
fwdInfo.AmountToForward, pd.Amount)

failure := NewLinkError(
lnwire.NewFinalIncorrectHtlcAmount(pd.Amount),
Expand Down Expand Up @@ -3573,7 +3584,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,

event, err := l.cfg.Registry.NotifyExitHopHtlc(
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
circuitKey, l.hodlQueue.ChanIn(), payload,
circuitKey, l.hodlQueue.ChanIn(), pd.CustomRecords, payload,
)
if err != nil {
return err
Expand Down
7 changes: 5 additions & 2 deletions htlcswitch/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
panic(err)
}

modifierMock := &invoices.MockHtlcModifier{}
registry := invoices.NewRegistry(
cdb,
invoices.NewInvoiceExpiryWatcher(
Expand All @@ -1019,6 +1020,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
),
&invoices.RegistryConfig{
FinalCltvRejectDelta: 5,
HtlcModifier: modifierMock,
},
)
registry.Start()
Expand All @@ -1044,11 +1046,12 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error) {

event, err := i.registry.NotifyExitHopHtlc(
rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
payload,
rhash, amt, expiry, currentHeight, circuitKey,
hodlChan, wireCustomRecords, payload,
)
if err != nil {
return nil, err
Expand Down
62 changes: 62 additions & 0 deletions invoices/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
Expand Down Expand Up @@ -198,3 +199,64 @@ type InvoiceUpdater interface {
// Finalize finalizes the update before it is written to the database.
Finalize(updateType UpdateType) error
}

// HtlcModifyRequest is the request that is passed to the client via callback
// during a HTLC interceptor session. The request contains the invoice that the
// given HTLC is attempting to settle.
type HtlcModifyRequest struct {
// WireCustomRecords are the custom records that were parsed from the
// HTLC wire message. These are the records of the current HTLC to be
// accepted/settled. All previously accepted/settled HTLCs for the same
// invoice are present in the Invoice field below.
WireCustomRecords lnwire.CustomRecords

// ExitHtlcCircuitKey is the circuit key that identifies the HTLC which
// is involved in the invoice settlement.
ExitHtlcCircuitKey CircuitKey

// ExitHtlcAmt is the amount of the HTLC which is involved in the
// invoice settlement.
ExitHtlcAmt lnwire.MilliSatoshi

// ExitHtlcExpiry is the absolute expiry height of the HTLC which is
// involved in the invoice settlement.
ExitHtlcExpiry uint32

// CurrentHeight is the current block height.
CurrentHeight uint32

// Invoice is the invoice that is being intercepted. The HTLCs within
// the invoice are only those previously accepted/settled for the same
// invoice.
Invoice Invoice
}

// HtlcModifyResponse is the response that the client should send back to the
// interceptor after processing the HTLC modify request.
type HtlcModifyResponse struct {
// AmountPaid is the amount that the client has decided the HTLC is
// actually worth. This might be different from the amount that the
// HTLC was originally sent with, in case additional value is carried
// along with it (which might be the case in custom channels).
AmountPaid lnwire.MilliSatoshi
}

// HtlcModifyCallback is a function that is called when an invoice is
// intercepted by the invoice interceptor.
type HtlcModifyCallback func(HtlcModifyRequest) error

// HtlcModifier is an interface that allows the caller to intercept and modify
// aspects of HTLCs that are settling an invoice.
type HtlcModifier interface {
// Intercept generates a new intercept session for the given invoice.
// The session is returned to the caller so that they can block until
// the client resolution is received.
Intercept(HtlcModifyRequest) fn.Option[InterceptSession]

// SetClientCallback sets the client callback function that is called
// when an invoice is intercepted.
SetClientCallback(HtlcModifyCallback)

// Modify changes parts of the HTLC based on the client's response.
Modify(htlc CircuitKey, amountPaid lnwire.MilliSatoshi) error
}
59 changes: 58 additions & 1 deletion invoices/invoiceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -887,6 +892,7 @@ func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error {
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload Payload) (HtlcResolution, error) {

// Create the update context containing the relevant details of the
Expand All @@ -898,6 +904,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
expiry: expiry,
currentHeight: currentHeight,
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
wireCustomRecords: wireCustomRecords,
customRecords: payload.CustomRecords(),
mpp: payload.MultiPath(),
amp: payload.AMPRecord(),
Expand Down Expand Up @@ -998,6 +1005,54 @@ 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{
WireCustomRecords: ctx.wireCustomRecords,
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
Expand Down Expand Up @@ -1051,6 +1106,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,
Expand Down Expand Up @@ -1183,7 +1240,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
Expand Down
Loading
Loading