multi: extract path ID and total amt from received payment

We've covered all the logic for building a blinded path to ourselves and
putting that into an invoice - so now we start preparing to actually be
able to recognise the incoming payment as one from a blinded path we
created.

The incoming update_add_htlc will have an `encrypted_recipient_data`
blob for us that we would have put in the original invoice. From this we
extract the PathID which we wrote. We consider this the payment address
and we use this to derive the associated invoice location.

Blinded path payments will not include MPP records, so the payment
address and total payment amount must be gleaned from the pathID and new
totalAmtMsat onion field respectively.

This commit only covers the final hop payload of a hop in a blinded
path. Dummy hops will be handled in the following commit.
This commit is contained in:
Elle Mouton
2024-05-05 14:48:50 +02:00
parent 3d9c77d1fc
commit b0d3e4dc0d
8 changed files with 94 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
package hop package hop
import ( import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/lnwire" "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 // node in UpdateAddHtlc. This field is set if the htlc is part of a
// blinded route. // blinded route.
NextBlinding lnwire.BlindingPointRecord 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
} }

View File

@@ -8,6 +8,7 @@ import (
"sync" "sync"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/record"
@@ -230,11 +231,11 @@ func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload,
return nil, routeRole, err return nil, routeRole, err
} }
// Exit early if this onion is for the exit hop of the route since // This is the final node in the blinded route.
// route blinding receives are not yet supported.
if isFinal { if isFinal {
return nil, routeRole, fmt.Errorf("being the final hop in a " + return deriveBlindedRouteFinalHopForwardingInfo(
"blinded path is not yet supported") routeData, payload, routeRole,
)
} }
// Else, we are a forwarding node in this blinded path. // 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 // deriveBlindedRouteForwardingInfo uses the parsed BlindedRouteData from the
// recipient to derive the ForwardingInfo for the payment. // recipient to derive the ForwardingInfo for the payment.
func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator, func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator,

View File

@@ -6,6 +6,7 @@ import (
"io" "io"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/record"
@@ -408,6 +409,12 @@ func (h *Payload) BlindingPoint() *btcec.PublicKey {
return h.blindingPoint 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 // Metadata returns the additional data that is sent along with the
// payment to the payee. // payment to the payee.
func (h *Payload) Metadata() []byte { 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 // 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 // to probe the blinded route and compare it to updated channel policies in
// the network. // 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, func ValidateBlindedRouteData(blindedData *record.BlindedRouteData,
incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error { incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error {

View File

@@ -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 // 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 // used if we are the introduction node and need to present an error as if
// we're the failing party. // 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, func (l *channelLink) sendIncomingHTLCFailureMsg(htlcIndex uint64,
e hop.ErrorEncrypter, e hop.ErrorEncrypter,
originalFailure lnwire.OpaqueReason) error { originalFailure lnwire.OpaqueReason) error {

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"time" "time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -105,6 +106,14 @@ type Payload interface {
// Metadata returns the additional data that is sent along with the // Metadata returns the additional data that is sent along with the
// payment to the payee. // payment to the payee.
Metadata() []byte 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 // InvoiceQuery represents a query to the invoice database. The query allows a

View File

@@ -902,6 +902,8 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
mpp: payload.MultiPath(), mpp: payload.MultiPath(),
amp: payload.AMPRecord(), amp: payload.AMPRecord(),
metadata: payload.Metadata(), metadata: payload.Metadata(),
pathID: payload.PathID(),
totalAmtMsat: payload.TotalAmtMsat(),
} }
switch { switch {

View File

@@ -30,6 +30,8 @@ type mockPayload struct {
amp *record.AMP amp *record.AMP
customRecords record.CustomSet customRecords record.CustomSet
metadata []byte metadata []byte
pathID *chainhash.Hash
totalAmtMsat lnwire.MilliSatoshi
} }
func (p *mockPayload) MultiPath() *record.MPP { func (p *mockPayload) MultiPath() *record.MPP {
@@ -40,6 +42,14 @@ func (p *mockPayload) AMPRecord() *record.AMP {
return p.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 { func (p *mockPayload) CustomRecords() record.CustomSet {
// This function should always return a map instance, but for mock // This function should always return a map instance, but for mock
// configuration we do accept nil. // configuration we do accept nil.

View File

@@ -1,9 +1,11 @@
package invoices package invoices
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"errors" "errors"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/amp" "github.com/lightningnetwork/lnd/amp"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -23,12 +25,17 @@ type invoiceUpdateCtx struct {
mpp *record.MPP mpp *record.MPP
amp *record.AMP amp *record.AMP
metadata []byte metadata []byte
pathID *chainhash.Hash
totalAmtMsat lnwire.MilliSatoshi
} }
// invoiceRef returns an identifier that can be used to lookup or update the // invoiceRef returns an identifier that can be used to lookup or update the
// invoice this HTLC is targeting. // invoice this HTLC is targeting.
func (i *invoiceUpdateCtx) invoiceRef() InvoiceRef { func (i *invoiceUpdateCtx) invoiceRef() InvoiceRef {
switch { switch {
case i.pathID != nil:
return InvoiceRefByHashAndAddr(i.hash, *i.pathID)
case i.amp != nil && i.mpp != nil: case i.amp != nil && i.mpp != nil:
payAddr := i.mpp.PaymentAddr() payAddr := i.mpp.PaymentAddr()
return InvoiceRefByAddr(payAddr) 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, // 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 // or a payment to an invoice created before we started to require the
// MPP payload. // MPP payload.
if ctx.mpp == nil { if ctx.mpp == nil && ctx.pathID == nil {
return updateLegacy(ctx, inv) return updateLegacy(ctx, inv)
} }
@@ -158,12 +165,27 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
setID := ctx.setID() 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. // Start building the accept descriptor.
acceptDesc := &HtlcAcceptDesc{ acceptDesc := &HtlcAcceptDesc{
Amt: ctx.amtPaid, Amt: ctx.amtPaid,
Expiry: ctx.expiry, Expiry: ctx.expiry,
AcceptHeight: ctx.currentHeight, AcceptHeight: ctx.currentHeight,
MppTotalAmt: ctx.mpp.TotalMsat(), MppTotalAmt: totalAmt,
CustomRecords: ctx.customRecords, CustomRecords: ctx.customRecords,
} }
@@ -184,18 +206,18 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
} }
// Check the payment address that authorizes the payment. // 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 return nil, ctx.failRes(ResultAddressMismatch), nil
} }
// Don't accept zero-valued sets. // Don't accept zero-valued sets.
if ctx.mpp.TotalMsat() == 0 { if totalAmt == 0 {
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
} }
// Check that the total amt of the htlc set is high enough. In case this // 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. // 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 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. // Check whether total amt matches other htlcs in the set.
var newSetTotal lnwire.MilliSatoshi var newSetTotal lnwire.MilliSatoshi
for _, htlc := range htlcSet { for _, htlc := range htlcSet {
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt { if totalAmt != htlc.MppTotalAmt {
return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil 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. // If the invoice cannot be settled yet, only record the htlc.
setComplete := newSetTotal >= ctx.mpp.TotalMsat() setComplete := newSetTotal >= totalAmt
if !setComplete { if !setComplete {
return &update, ctx.acceptRes(resultPartialAccepted), nil return &update, ctx.acceptRes(resultPartialAccepted), nil
} }