diff --git a/htlcswitch/hop/forwarding_info.go b/htlcswitch/hop/forwarding_info.go index 5a1463c48..92ea541cc 100644 --- a/htlcswitch/hop/forwarding_info.go +++ b/htlcswitch/hop/forwarding_info.go @@ -1,6 +1,7 @@ package hop import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lnwire" ) @@ -27,4 +28,9 @@ type ForwardingInfo struct { // node in UpdateAddHtlc. This field is set if the htlc is part of a // blinded route. NextBlinding lnwire.BlindingPointRecord + + // PathID is a secret identifier that the creator of a blinded path + // sets for itself to ensure that the blinded path has been used in the + // correct context. + PathID *chainhash.Hash } diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index 01a6a2655..bb3fb12d7 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" @@ -230,11 +231,11 @@ func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload, return nil, routeRole, err } - // Exit early if this onion is for the exit hop of the route since - // route blinding receives are not yet supported. + // This is the final node in the blinded route. if isFinal { - return nil, routeRole, fmt.Errorf("being the final hop in a " + - "blinded path is not yet supported") + return deriveBlindedRouteFinalHopForwardingInfo( + routeData, payload, routeRole, + ) } // Else, we are a forwarding node in this blinded path. @@ -243,6 +244,32 @@ func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload, ) } +// deriveBlindedRouteFinalHopForwardingInfo extracts the PathID from the +// routeData and constructs the ForwardingInfo accordingly. +func deriveBlindedRouteFinalHopForwardingInfo( + routeData *record.BlindedRouteData, payload *Payload, + routeRole RouteRole) (*Payload, RouteRole, error) { + + var pathID *chainhash.Hash + routeData.PathID.WhenSome(func(r tlv.RecordT[tlv.TlvType6, []byte]) { + var id chainhash.Hash + copy(id[:], r.Val) + pathID = &id + }) + if pathID == nil { + return nil, routeRole, ErrInvalidPayload{ + Type: tlv.Type(6), + Violation: InsufficientViolation, + } + } + + payload.FwdInfo = ForwardingInfo{ + PathID: pathID, + } + + return payload, routeRole, nil +} + // deriveBlindedRouteForwardingInfo uses the parsed BlindedRouteData from the // recipient to derive the ForwardingInfo for the payment. func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator, diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 9e717bbd2..fc456828a 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -6,6 +6,7 @@ import ( "io" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" @@ -408,6 +409,12 @@ func (h *Payload) BlindingPoint() *btcec.PublicKey { return h.blindingPoint } +// PathID returns the path ID that was encoded in the final hop payload of a +// blinded payment. +func (h *Payload) PathID() *chainhash.Hash { + return h.FwdInfo.PathID +} + // Metadata returns the additional data that is sent along with the // payment to the payee. func (h *Payload) Metadata() []byte { @@ -460,10 +467,6 @@ func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type { // the route "expires" and a malicious party does not have endless opportunity // to probe the blinded route and compare it to updated channel policies in // the network. -// -// Note that this function only validates blinded route data for forwarding -// nodes, as LND does not yet support receiving via a blinded route (which has -// different validation rules). func ValidateBlindedRouteData(blindedData *record.BlindedRouteData, incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error { diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 712bbda9e..f39a12b2b 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -3774,9 +3774,6 @@ func (l *channelLink) sendHTLCError(pd *lnwallet.PaymentDescriptor, // that we're not part of a blinded route and an error encrypter that'll be // used if we are the introduction node and need to present an error as if // we're the failing party. -// -// Note: this function does not yet handle special error cases for receiving -// nodes in blinded paths, as LND does not support blinded receives. func (l *channelLink) sendIncomingHTLCFailureMsg(htlcIndex uint64, e hop.ErrorEncrypter, originalFailure lnwire.OpaqueReason) error { diff --git a/invoices/interface.go b/invoices/interface.go index 490db1be5..f48aa37b6 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -105,6 +106,14 @@ type Payload interface { // Metadata returns the additional data that is sent along with the // payment to the payee. Metadata() []byte + + // PathID returns the path ID encoded in the payload of a blinded + // payment. + PathID() *chainhash.Hash + + // TotalAmtMsat returns the total amount sent to the final hop, as set + // by the payee. + TotalAmtMsat() lnwire.MilliSatoshi } // InvoiceQuery represents a query to the invoice database. The query allows a diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index de731b474..4e2748a0f 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -902,6 +902,8 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, mpp: payload.MultiPath(), amp: payload.AMPRecord(), metadata: payload.Metadata(), + pathID: payload.PathID(), + totalAmtMsat: payload.TotalAmtMsat(), } switch { diff --git a/invoices/test_utils_test.go b/invoices/test_utils_test.go index a0adf7dc8..ed7bfccdd 100644 --- a/invoices/test_utils_test.go +++ b/invoices/test_utils_test.go @@ -30,6 +30,8 @@ type mockPayload struct { amp *record.AMP customRecords record.CustomSet metadata []byte + pathID *chainhash.Hash + totalAmtMsat lnwire.MilliSatoshi } func (p *mockPayload) MultiPath() *record.MPP { @@ -40,6 +42,14 @@ func (p *mockPayload) AMPRecord() *record.AMP { return p.amp } +func (p *mockPayload) PathID() *chainhash.Hash { + return p.pathID +} + +func (p *mockPayload) TotalAmtMsat() lnwire.MilliSatoshi { + return p.totalAmtMsat +} + func (p *mockPayload) CustomRecords() record.CustomSet { // This function should always return a map instance, but for mock // configuration we do accept nil. diff --git a/invoices/update.go b/invoices/update.go index ed60c278c..d14bafee0 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -1,9 +1,11 @@ package invoices import ( + "bytes" "encoding/hex" "errors" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/amp" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" @@ -23,12 +25,17 @@ type invoiceUpdateCtx struct { mpp *record.MPP amp *record.AMP metadata []byte + pathID *chainhash.Hash + totalAmtMsat lnwire.MilliSatoshi } // invoiceRef returns an identifier that can be used to lookup or update the // invoice this HTLC is targeting. func (i *invoiceUpdateCtx) invoiceRef() InvoiceRef { switch { + case i.pathID != nil: + return InvoiceRefByHashAndAddr(i.hash, *i.pathID) + case i.amp != nil && i.mpp != nil: payAddr := i.mpp.PaymentAddr() return InvoiceRefByAddr(payAddr) @@ -130,7 +137,7 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *Invoice) ( // If no MPP payload was provided, then we expect this to be a keysend, // or a payment to an invoice created before we started to require the // MPP payload. - if ctx.mpp == nil { + if ctx.mpp == nil && ctx.pathID == nil { return updateLegacy(ctx, inv) } @@ -158,12 +165,27 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, setID := ctx.setID() + var ( + totalAmt = ctx.totalAmtMsat + paymentAddr []byte + ) + // If an MPP record is present, then the payment address and total + // payment amount is extracted from it. Otherwise, the pathID is used + // to extract the payment address. + if ctx.mpp != nil { + totalAmt = ctx.mpp.TotalMsat() + payAddr := ctx.mpp.PaymentAddr() + paymentAddr = payAddr[:] + } else { + paymentAddr = ctx.pathID[:] + } + // Start building the accept descriptor. acceptDesc := &HtlcAcceptDesc{ Amt: ctx.amtPaid, Expiry: ctx.expiry, AcceptHeight: ctx.currentHeight, - MppTotalAmt: ctx.mpp.TotalMsat(), + MppTotalAmt: totalAmt, CustomRecords: ctx.customRecords, } @@ -184,18 +206,18 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, } // Check the payment address that authorizes the payment. - if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr { + if !bytes.Equal(paymentAddr, inv.Terms.PaymentAddr[:]) { return nil, ctx.failRes(ResultAddressMismatch), nil } // Don't accept zero-valued sets. - if ctx.mpp.TotalMsat() == 0 { + if totalAmt == 0 { return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil } // Check that the total amt of the htlc set is high enough. In case this // is a zero-valued invoice, it will always be enough. - if ctx.mpp.TotalMsat() < inv.Terms.Value { + if totalAmt < inv.Terms.Value { return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil } @@ -204,7 +226,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, // Check whether total amt matches other htlcs in the set. var newSetTotal lnwire.MilliSatoshi for _, htlc := range htlcSet { - if ctx.mpp.TotalMsat() != htlc.MppTotalAmt { + if totalAmt != htlc.MppTotalAmt { return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil } @@ -238,7 +260,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, } // If the invoice cannot be settled yet, only record the htlc. - setComplete := newSetTotal >= ctx.mpp.TotalMsat() + setComplete := newSetTotal >= totalAmt if !setComplete { return &update, ctx.acceptRes(resultPartialAccepted), nil }