lnrpc/invoicesrpc: prep AddInvoice for blinded routes

Here we add a new `Blind` option to the `AddInvoiceData` which will
signal that the new invoice should encode a blinded route.

Certain other changes are also made in the case that this invoice
contains a blinded route:
1) the payment address/secret no longer needs to be in the invoice
   itself since it will be put in the `PathID` recored of the encrypted
   recipient record for our hop.
2) When we sign the invoice, we now use an ephemeral key since we dont
   want the sender to be able to derive our real node pub key from the
   invoice signature.
3) The invoice's FinalCLTV field should be zero for blinded invoices
   since the CLTV delta info will be communicated in the accumulated
   route policy values.
This commit is contained in:
Elle Mouton 2024-05-04 11:26:27 +02:00
parent 4b5327f057
commit 9787ae9c89
No known key found for this signature in database
GPG Key ID: D7D916376026F177

View File

@ -13,8 +13,10 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
@ -97,6 +99,31 @@ type AddInvoiceConfig struct {
// GetAlias allows the peer's alias SCID to be retrieved for private
// option_scid_alias channels.
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
// BestHeight returns the current best block height that this node is
// aware of.
BestHeight func() (uint32, error)
// QueryBlindedRoutes can be used to generate a few routes to this node
// that can then be used in the construction of a blinded payment path.
QueryBlindedRoutes func(lnwire.MilliSatoshi) ([]*route.Route, error)
// BlindedRoutePolicyIncrMultiplier is the amount by which policy values
// for hops in a blinded route will be bumped to avoid easy probing. For
// example, a multiplier of 1.1 will bump all appropriate the values
// (base fee, fee rate, CLTV delta and min HLTC) by 10%.
BlindedRoutePolicyIncrMultiplier float64
// BlindedRoutePolicyDecrMultiplier is the amount by which appropriate
// policy values for hops in a blinded route will be decreased to avoid
// easy probing. For example, a multiplier of 0.9 will reduce
// appropriate values (like maximum HTLC) by 10%.
BlindedRoutePolicyDecrMultiplier float64
// MinNumHops is the minimum number of hops that a blinded path should
// be. Dummy hops will be used to pad any route with a length less than
// this.
MinNumHops uint8
}
// AddInvoiceData contains the required data to create a new invoice.
@ -147,6 +174,11 @@ type AddInvoiceData struct {
// NOTE: Preimage should always be set to nil when this value is true.
Amp bool
// Blind signals that this invoice should disguise the location of the
// recipient by adding blinded payment paths to the invoice instead of
// revealing the destination node's real pub key.
Blind bool
// RouteHints are optional route hints that can each be individually
// used to assist in reaching the invoice's destination.
RouteHints [][]zpay32.HopHint
@ -251,6 +283,11 @@ func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage,
func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
invoice *AddInvoiceData) (*lntypes.Hash, *invoices.Invoice, error) {
if invoice.Amp && invoice.Blind {
return nil, nil, fmt.Errorf("AMP invoices with blinded paths " +
"are not yet supported")
}
paymentPreimage, paymentHash, err := invoice.paymentHashAndPreimage()
if err != nil {
return nil, nil, err
@ -322,10 +359,9 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
options = append(options, zpay32.FallbackAddr(addr))
}
var expiry time.Duration
switch {
// If expiry is set, specify it. If it is not provided, no expiry time
// will be explicitly added to this payment request, which will imply
// the default 3600 seconds.
// An invoice expiry has been provided by the caller.
case invoice.Expiry > 0:
// We'll ensure that the specified expiry is restricted to sane
@ -340,19 +376,19 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
float64(expSeconds), maxExpiry.Seconds())
}
expiry := time.Duration(invoice.Expiry) * time.Second
options = append(options, zpay32.Expiry(expiry))
expiry = time.Duration(invoice.Expiry) * time.Second
// If no custom expiry is provided, use the default MPP expiry.
case !invoice.Amp:
options = append(options, zpay32.Expiry(DefaultInvoiceExpiry))
expiry = DefaultInvoiceExpiry
// Otherwise, use the default AMP expiry.
default:
defaultExpiry := zpay32.Expiry(DefaultAMPInvoiceExpiry)
options = append(options, defaultExpiry)
expiry = DefaultAMPInvoiceExpiry
}
options = append(options, zpay32.Expiry(expiry))
// If the description hash is set, then we add it do the list of
// options. If not, use the memo field as the payment request
// description.
@ -366,15 +402,16 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
options = append(options, zpay32.Description(invoice.Memo))
}
// We'll use our current default CLTV value unless one was specified as
// an option on the command line when creating an invoice.
switch {
case invoice.CltvExpiry > routing.MaxCLTVDelta:
if invoice.CltvExpiry > routing.MaxCLTVDelta {
return nil, nil, fmt.Errorf("CLTV delta of %v is too large, "+
"max accepted is: %v", invoice.CltvExpiry,
math.MaxUint16)
}
case invoice.CltvExpiry != 0:
// We'll use our current default CLTV value unless one was specified as
// an option on the command line when creating an invoice.
cltvExpiryDelta := uint64(cfg.DefaultCLTVExpiry)
if invoice.CltvExpiry != 0 {
// Disallow user-chosen final CLTV deltas below the required
// minimum.
if invoice.CltvExpiry < routing.MinCLTVDelta {
@ -383,13 +420,14 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
invoice.CltvExpiry, routing.MinCLTVDelta)
}
options = append(options,
zpay32.CLTVExpiry(invoice.CltvExpiry))
cltvExpiryDelta = invoice.CltvExpiry
}
default:
// TODO(roasbeef): assumes set delta between versions
defaultCLTVExpiry := uint64(cfg.DefaultCLTVExpiry)
options = append(options, zpay32.CLTVExpiry(defaultCLTVExpiry))
// Only include a final CLTV expiry delta if this is not a blinded
// invoice. In a blinded invoice, this value will be added to the total
// blinded route CLTV delta value
if !invoice.Blind {
options = append(options, zpay32.CLTVExpiry(cltvExpiryDelta))
}
// We make sure that the given invoice routing hints number is within
@ -401,6 +439,11 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
// Include route hints if needed.
if len(invoice.RouteHints) > 0 || invoice.Private {
if invoice.Blind {
return nil, nil, fmt.Errorf("can't set both hop " +
"hints and add blinded payment paths")
}
// Validate provided hop hints.
for _, hint := range invoice.RouteHints {
if len(hint) == 0 {
@ -443,14 +486,73 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
}
options = append(options, zpay32.Features(invoiceFeatures))
// Generate and set a random payment address for this invoice. If the
// Generate and set a random payment address for this payment. If the
// sender understands payment addresses, this can be used to avoid
// intermediaries probing the receiver.
// intermediaries probing the receiver. If the invoice does not have
// blinded paths, then this will be encoded in the invoice itself.
// Otherwise, it will instead be embedded in the encrypted recipient
// data of blinded paths. In the blinded path case, this will be used
// for the PathID.
var paymentAddr [32]byte
if _, err := rand.Read(paymentAddr[:]); err != nil {
return nil, nil, err
}
options = append(options, zpay32.PaymentAddr(paymentAddr))
if invoice.Blind {
// Use the 10-min-per-block assumption to get a rough estimate
// of the number of blocks until the invoice expires. We want
// to make sure that the blinded path definitely does not expire
// before the invoice does, and so we add a healthy buffer.
invoiceExpiry := uint32(expiry.Minutes() / 10)
blindedPathExpiry := invoiceExpiry * 2
// Add BlockPadding to the finalCltvDelta so that the receiving
// node does not reject the HTLC if some blocks are mined while
// the payment is in-flight. Note that unlike vanilla invoices,
// with blinded paths, the recipient is responsible for adding
// this block padding instead of the sender.
finalCLTVDelta := uint32(cltvExpiryDelta)
finalCLTVDelta += uint32(routing.BlockPadding)
//nolint:lll
paths, err := buildBlindedPaymentPaths(&buildBlindedPathCfg{
findRoutes: cfg.QueryBlindedRoutes,
fetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
pathID: paymentAddr[:],
valueMsat: invoice.Value,
bestHeight: cfg.BestHeight,
minFinalCLTVExpiryDelta: finalCLTVDelta,
blocksUntilExpiry: blindedPathExpiry,
addPolicyBuffer: func(p *blindedHopPolicy) (
*blindedHopPolicy, error) {
return addPolicyBuffer(
p, cfg.BlindedRoutePolicyIncrMultiplier,
cfg.BlindedRoutePolicyDecrMultiplier,
)
},
minNumHops: cfg.MinNumHops,
// TODO: make configurable
dummyHopPolicy: &blindedHopPolicy{
cltvExpiryDelta: 80,
feeRate: 100,
baseFee: 100,
minHTLCMsat: 0,
maxHTLCMsat: lnwire.MaxMilliSatoshi,
},
})
if err != nil {
return nil, nil, err
}
for _, path := range paths {
options = append(options, zpay32.WithBlindedPaymentPath(
path,
))
}
} else {
options = append(options, zpay32.PaymentAddr(paymentAddr))
}
// Create and encode the payment request as a bech32 (zpay32) string.
creationDate := time.Now()
@ -463,7 +565,27 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
payReqString, err := payReq.Encode(zpay32.MessageSigner{
SignCompact: func(msg []byte) ([]byte, error) {
return cfg.NodeSigner.SignMessageCompact(msg, false)
// For an invoice without a blinded path, the main node
// key is used to sign the invoice so that the sender
// can derive the true pub key of the recipient.
if !invoice.Blind {
return cfg.NodeSigner.SignMessageCompact(
msg, false,
)
}
// For an invoice with a blinded path, we use an
// ephemeral key to sign the invoice since we don't want
// the sender to be able to know the real pub key of
// the recipient.
ephemKey, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
return ecdsa.SignCompact(
ephemKey, chainhash.HashB(msg), true,
)
},
})
if err != nil {