2019-01-14 17:56:59 +01:00
|
|
|
package invoicesrpc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
2018-10-05 10:14:56 +02:00
|
|
|
"errors"
|
2019-01-14 17:56:59 +01:00
|
|
|
"fmt"
|
|
|
|
"math"
|
2022-09-13 09:23:05 -07:00
|
|
|
mathRand "math/rand"
|
2024-07-04 16:40:54 +02:00
|
|
|
"slices"
|
2022-09-13 09:23:05 -07:00
|
|
|
"sort"
|
2019-01-14 17:56:59 +01:00
|
|
|
"time"
|
|
|
|
|
2022-02-23 14:48:00 +01:00
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
2019-01-14 17:56:59 +01:00
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
2020-08-10 16:18:48 -07:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
2024-05-04 10:37:41 +02:00
|
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
2019-01-14 17:56:59 +01:00
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
2023-11-08 11:18:45 +02:00
|
|
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
2022-11-30 03:00:37 -08:00
|
|
|
"github.com/lightningnetwork/lnd/invoices"
|
2018-10-05 10:14:56 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2024-07-24 19:31:21 +08:00
|
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
2019-01-14 17:56:59 +01:00
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
2018-10-05 10:14:56 +02:00
|
|
|
"github.com/lightningnetwork/lnd/netann"
|
2024-05-04 10:37:41 +02:00
|
|
|
"github.com/lightningnetwork/lnd/record"
|
2020-07-24 13:13:56 -07:00
|
|
|
"github.com/lightningnetwork/lnd/routing"
|
2024-07-04 16:40:54 +02:00
|
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
2024-05-04 10:37:41 +02:00
|
|
|
"github.com/lightningnetwork/lnd/tlv"
|
2018-10-05 10:14:56 +02:00
|
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
2019-01-14 17:56:59 +01:00
|
|
|
)
|
|
|
|
|
2021-05-26 18:10:58 -07:00
|
|
|
const (
|
|
|
|
// DefaultInvoiceExpiry is the default invoice expiry for new MPP
|
|
|
|
// invoices.
|
|
|
|
DefaultInvoiceExpiry = 24 * time.Hour
|
|
|
|
|
|
|
|
// DefaultAMPInvoiceExpiry is the default invoice expiry for new AMP
|
|
|
|
// invoices.
|
|
|
|
DefaultAMPInvoiceExpiry = 30 * 24 * time.Hour
|
2022-01-27 15:09:21 +02:00
|
|
|
|
|
|
|
// hopHintFactor is factor by which we scale the total amount of
|
|
|
|
// inbound capacity we want our hop hints to represent, allowing us to
|
|
|
|
// have some leeway if peers go offline.
|
|
|
|
hopHintFactor = 2
|
2022-09-13 09:23:05 -07:00
|
|
|
|
|
|
|
// maxHopHints is the maximum number of hint paths that will be included
|
|
|
|
// in an invoice.
|
|
|
|
maxHopHints = 20
|
2024-05-04 10:43:03 +02:00
|
|
|
|
|
|
|
// oneMillion is a constant used frequently in fee rate calculations.
|
|
|
|
oneMillion = uint32(1_000_000)
|
2021-05-26 18:10:58 -07:00
|
|
|
)
|
|
|
|
|
2024-07-04 16:40:54 +02:00
|
|
|
// errInvalidBlindedPath indicates that the chosen real path is not usable as
|
|
|
|
// a blinded path.
|
|
|
|
var errInvalidBlindedPath = errors.New("the chosen path results in an " +
|
|
|
|
"unusable blinded path")
|
|
|
|
|
2019-01-14 17:56:59 +01:00
|
|
|
// AddInvoiceConfig contains dependencies for invoice creation.
|
|
|
|
type AddInvoiceConfig struct {
|
|
|
|
// AddInvoice is called to add the invoice to the registry.
|
2023-10-11 13:42:59 +02:00
|
|
|
AddInvoice func(ctx context.Context, invoice *invoices.Invoice,
|
|
|
|
paymentHash lntypes.Hash) (uint64, error)
|
2019-01-14 17:56:59 +01:00
|
|
|
|
|
|
|
// IsChannelActive is used to generate valid hop hints.
|
|
|
|
IsChannelActive func(chanID lnwire.ChannelID) bool
|
|
|
|
|
|
|
|
// ChainParams are required to properly decode invoice payment requests
|
|
|
|
// that are marshalled over rpc.
|
|
|
|
ChainParams *chaincfg.Params
|
|
|
|
|
|
|
|
// NodeSigner is an implementation of the MessageSigner implementation
|
|
|
|
// that's backed by the identity private key of the running lnd node.
|
|
|
|
NodeSigner *netann.NodeSigner
|
|
|
|
|
|
|
|
// DefaultCLTVExpiry is the default invoice expiry if no values is
|
|
|
|
// specified.
|
|
|
|
DefaultCLTVExpiry uint32
|
|
|
|
|
|
|
|
// ChanDB is a global boltdb instance which is needed to access the
|
|
|
|
// channel graph.
|
2021-09-21 19:18:17 +02:00
|
|
|
ChanDB *channeldb.ChannelStateDB
|
2019-12-10 13:09:52 -08:00
|
|
|
|
2020-10-26 14:14:15 +01:00
|
|
|
// Graph holds a reference to the ChannelGraph database.
|
|
|
|
Graph *channeldb.ChannelGraph
|
|
|
|
|
2019-12-10 13:09:52 -08:00
|
|
|
// GenInvoiceFeatures returns a feature containing feature bits that
|
|
|
|
// should be advertised on freshly generated invoices.
|
|
|
|
GenInvoiceFeatures func() *lnwire.FeatureVector
|
2021-05-06 09:15:01 -07:00
|
|
|
|
|
|
|
// GenAmpInvoiceFeatures returns a feature containing feature bits that
|
|
|
|
// should be advertised on freshly generated AMP invoices.
|
|
|
|
GenAmpInvoiceFeatures func() *lnwire.FeatureVector
|
2022-04-04 16:49:14 -04:00
|
|
|
|
|
|
|
// GetAlias allows the peer's alias SCID to be retrieved for private
|
|
|
|
// option_scid_alias channels.
|
|
|
|
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2019-01-15 10:06:48 +01:00
|
|
|
// AddInvoiceData contains the required data to create a new invoice.
|
|
|
|
type AddInvoiceData struct {
|
|
|
|
// An optional memo to attach along with the invoice. Used for record
|
|
|
|
// keeping purposes for the invoice's creator, and will also be set in
|
|
|
|
// the description field of the encoded payment request if the
|
|
|
|
// description_hash field is not being used.
|
|
|
|
Memo string
|
|
|
|
|
|
|
|
// The preimage which will allow settling an incoming HTLC payable to
|
2018-10-05 10:14:56 +02:00
|
|
|
// this preimage. If Preimage is set, Hash should be nil. If both
|
|
|
|
// Preimage and Hash are nil, a random preimage is generated.
|
2019-01-15 10:06:48 +01:00
|
|
|
Preimage *lntypes.Preimage
|
|
|
|
|
2018-10-05 10:14:56 +02:00
|
|
|
// The hash of the preimage. If Hash is set, Preimage should be nil.
|
|
|
|
// This condition indicates that we have a 'hold invoice' for which the
|
|
|
|
// htlc will be accepted and held until the preimage becomes known.
|
|
|
|
Hash *lntypes.Hash
|
|
|
|
|
2019-11-15 08:59:14 +01:00
|
|
|
// The value of this invoice in millisatoshis.
|
|
|
|
Value lnwire.MilliSatoshi
|
2019-01-15 10:06:48 +01:00
|
|
|
|
|
|
|
// Hash (SHA-256) of a description of the payment. Used if the
|
|
|
|
// description of payment (memo) is too long to naturally fit within the
|
|
|
|
// description field of an encoded payment request.
|
|
|
|
DescriptionHash []byte
|
|
|
|
|
|
|
|
// Payment request expiry time in seconds. Default is 3600 (1 hour).
|
|
|
|
Expiry int64
|
|
|
|
|
|
|
|
// Fallback on-chain address.
|
|
|
|
FallbackAddr string
|
|
|
|
|
|
|
|
// Delta to use for the time-lock of the CLTV extended to the final hop.
|
|
|
|
CltvExpiry uint64
|
|
|
|
|
|
|
|
// Whether this invoice should include routing hints for private
|
|
|
|
// channels.
|
|
|
|
Private bool
|
2020-04-08 13:47:10 +02:00
|
|
|
|
|
|
|
// HodlInvoice signals that this invoice shouldn't be settled
|
|
|
|
// immediately upon receiving the payment.
|
|
|
|
HodlInvoice bool
|
2019-11-03 15:00:27 +02:00
|
|
|
|
2021-05-06 09:15:15 -07:00
|
|
|
// Amp signals whether or not to create an AMP invoice.
|
|
|
|
//
|
|
|
|
// NOTE: Preimage should always be set to nil when this value is true.
|
|
|
|
Amp bool
|
|
|
|
|
2022-09-09 08:56:21 -07:00
|
|
|
// RouteHints are optional route hints that can each be individually
|
|
|
|
// used to assist in reaching the invoice's destination.
|
2019-11-03 15:00:27 +02:00
|
|
|
RouteHints [][]zpay32.HopHint
|
2019-01-15 10:06:48 +01:00
|
|
|
}
|
|
|
|
|
2021-05-06 09:13:54 -07:00
|
|
|
// paymentHashAndPreimage returns the payment hash and preimage for this invoice
|
|
|
|
// depending on the configuration.
|
|
|
|
//
|
2021-05-06 09:15:15 -07:00
|
|
|
// For AMP invoices (when Amp flag is true), this method always returns a nil
|
|
|
|
// preimage. The hash value can be set externally by the user using the Hash
|
|
|
|
// field, or one will be generated randomly. The payment hash here only serves
|
|
|
|
// as a unique identifier for insertion into the invoice index, as there is
|
|
|
|
// no universal preimage for an AMP payment.
|
|
|
|
//
|
2021-05-06 09:13:54 -07:00
|
|
|
// For MPP invoices (when Amp flag is false), this method may return nil
|
|
|
|
// preimage when create a hodl invoice, but otherwise will always return a
|
|
|
|
// non-nil preimage and the corresponding payment hash. The valid combinations
|
|
|
|
// are parsed as follows:
|
|
|
|
// - Preimage == nil && Hash == nil -> (random preimage, H(random preimage))
|
|
|
|
// - Preimage != nil && Hash == nil -> (Preimage, H(Preimage))
|
|
|
|
// - Preimage == nil && Hash != nil -> (nil, Hash)
|
|
|
|
func (d *AddInvoiceData) paymentHashAndPreimage() (
|
|
|
|
*lntypes.Preimage, lntypes.Hash, error) {
|
2019-01-14 17:56:59 +01:00
|
|
|
|
2021-05-06 09:15:15 -07:00
|
|
|
if d.Amp {
|
|
|
|
return d.ampPaymentHashAndPreimage()
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.mppPaymentHashAndPreimage()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ampPaymentHashAndPreimage returns the payment hash to use for an AMP invoice.
|
|
|
|
// The preimage will always be nil.
|
2022-09-09 08:56:21 -07:00
|
|
|
func (d *AddInvoiceData) ampPaymentHashAndPreimage() (*lntypes.Preimage,
|
|
|
|
lntypes.Hash, error) {
|
|
|
|
|
2021-05-06 09:15:15 -07:00
|
|
|
switch {
|
|
|
|
// Preimages cannot be set on AMP invoice.
|
|
|
|
case d.Preimage != nil:
|
|
|
|
return nil, lntypes.Hash{},
|
|
|
|
errors.New("preimage set on AMP invoice")
|
|
|
|
|
|
|
|
// If a specific hash was requested, use that.
|
|
|
|
case d.Hash != nil:
|
|
|
|
return nil, *d.Hash, nil
|
|
|
|
|
|
|
|
// Otherwise generate a random hash value, just needs to be unique to be
|
|
|
|
// added to the invoice index.
|
|
|
|
default:
|
|
|
|
var paymentHash lntypes.Hash
|
|
|
|
if _, err := rand.Read(paymentHash[:]); err != nil {
|
|
|
|
return nil, lntypes.Hash{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, paymentHash, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// mppPaymentHashAndPreimage returns the payment hash and preimage to use for an
|
|
|
|
// MPP invoice.
|
2022-09-09 08:56:21 -07:00
|
|
|
func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage,
|
|
|
|
lntypes.Hash, error) {
|
|
|
|
|
2018-10-05 10:14:56 +02:00
|
|
|
var (
|
2020-04-08 13:47:10 +02:00
|
|
|
paymentPreimage *lntypes.Preimage
|
2018-10-05 10:14:56 +02:00
|
|
|
paymentHash lntypes.Hash
|
|
|
|
)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
|
|
|
// Only either preimage or hash can be set.
|
2021-05-06 09:13:54 -07:00
|
|
|
case d.Preimage != nil && d.Hash != nil:
|
|
|
|
return nil, lntypes.Hash{},
|
2018-10-05 10:14:56 +02:00
|
|
|
errors.New("preimage and hash both set")
|
|
|
|
|
|
|
|
// If no hash or preimage is given, generate a random preimage.
|
2021-05-06 09:13:54 -07:00
|
|
|
case d.Preimage == nil && d.Hash == nil:
|
2020-04-08 13:47:10 +02:00
|
|
|
paymentPreimage = &lntypes.Preimage{}
|
2019-01-14 17:56:59 +01:00
|
|
|
if _, err := rand.Read(paymentPreimage[:]); err != nil {
|
2021-05-06 09:13:54 -07:00
|
|
|
return nil, lntypes.Hash{}, err
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
2018-10-05 10:14:56 +02:00
|
|
|
paymentHash = paymentPreimage.Hash()
|
|
|
|
|
|
|
|
// If just a hash is given, we create a hold invoice by setting the
|
|
|
|
// preimage to unknown.
|
2021-05-06 09:13:54 -07:00
|
|
|
case d.Preimage == nil && d.Hash != nil:
|
|
|
|
paymentHash = *d.Hash
|
2018-10-05 10:14:56 +02:00
|
|
|
|
|
|
|
// A specific preimage was supplied. Use that for the invoice.
|
2021-05-06 09:13:54 -07:00
|
|
|
case d.Preimage != nil && d.Hash == nil:
|
|
|
|
preimage := *d.Preimage
|
2020-04-08 13:47:10 +02:00
|
|
|
paymentPreimage = &preimage
|
2021-05-06 09:13:54 -07:00
|
|
|
paymentHash = d.Preimage.Hash()
|
|
|
|
}
|
|
|
|
|
|
|
|
return paymentPreimage, paymentHash, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddInvoice attempts to add a new invoice to the invoice database. Any
|
|
|
|
// duplicated invoices are rejected, therefore all invoices *must* have a
|
|
|
|
// unique payment preimage.
|
|
|
|
func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
2022-11-30 03:00:37 -08:00
|
|
|
invoice *AddInvoiceData) (*lntypes.Hash, *invoices.Invoice, error) {
|
2021-05-06 09:13:54 -07:00
|
|
|
|
|
|
|
paymentPreimage, paymentHash, err := invoice.paymentHashAndPreimage()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// The size of the memo, receipt and description hash attached must not
|
|
|
|
// exceed the maximum values for either of the fields.
|
2022-11-30 03:00:37 -08:00
|
|
|
if len(invoice.Memo) > invoices.MaxMemoSize {
|
2019-01-15 10:06:48 +01:00
|
|
|
return nil, nil, fmt.Errorf("memo too large: %v bytes "+
|
2022-09-09 08:56:21 -07:00
|
|
|
"(maxsize=%v)", len(invoice.Memo),
|
2022-11-30 03:00:37 -08:00
|
|
|
invoices.MaxMemoSize)
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
2022-09-09 08:56:21 -07:00
|
|
|
if len(invoice.DescriptionHash) > 0 &&
|
|
|
|
len(invoice.DescriptionHash) != 32 {
|
|
|
|
|
|
|
|
return nil, nil, fmt.Errorf("description hash is %v bytes, "+
|
|
|
|
"must be 32", len(invoice.DescriptionHash))
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2020-04-21 16:42:52 -07:00
|
|
|
// We set the max invoice amount to 100k BTC, which itself is several
|
|
|
|
// multiples off the current block reward.
|
|
|
|
maxInvoiceAmt := btcutil.Amount(btcutil.SatoshiPerBitcoin * 100000)
|
|
|
|
|
|
|
|
switch {
|
2019-01-14 17:56:59 +01:00
|
|
|
// The value of the invoice must not be negative.
|
2020-04-21 22:16:05 -07:00
|
|
|
case int64(invoice.Value) < 0:
|
2019-01-15 10:06:48 +01:00
|
|
|
return nil, nil, fmt.Errorf("payments of negative value "+
|
2020-04-21 22:16:05 -07:00
|
|
|
"are not allowed, value is %v", int64(invoice.Value))
|
2020-04-21 16:42:52 -07:00
|
|
|
|
|
|
|
// Also ensure that the invoice is actually realistic, while preventing
|
|
|
|
// any issues due to underflow.
|
|
|
|
case invoice.Value.ToSatoshis() > maxInvoiceAmt:
|
|
|
|
return nil, nil, fmt.Errorf("invoice amount %v is "+
|
|
|
|
"too large, max is %v", invoice.Value.ToSatoshis(),
|
|
|
|
maxInvoiceAmt)
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 08:59:14 +01:00
|
|
|
amtMSat := invoice.Value
|
2019-01-14 17:56:59 +01:00
|
|
|
|
|
|
|
// We also create an encoded payment request which allows the
|
|
|
|
// caller to compactly send the invoice to the payer. We'll create a
|
|
|
|
// list of options to be added to the encoded payment request. For now
|
|
|
|
// we only support the required fields description/description_hash,
|
|
|
|
// expiry, fallback address, and the amount field.
|
|
|
|
var options []func(*zpay32.Invoice)
|
|
|
|
|
|
|
|
// We only include the amount in the invoice if it is greater than 0.
|
|
|
|
// By not including the amount, we enable the creation of invoices that
|
2022-11-11 14:31:04 -08:00
|
|
|
// allow the payer to specify the amount of satoshis they wish to send.
|
2019-01-14 17:56:59 +01:00
|
|
|
if amtMSat > 0 {
|
|
|
|
options = append(options, zpay32.Amount(amtMSat))
|
|
|
|
}
|
|
|
|
|
|
|
|
// If specified, add a fallback address to the payment request.
|
|
|
|
if len(invoice.FallbackAddr) > 0 {
|
2022-04-22 22:03:08 -04:00
|
|
|
addr, err := btcutil.DecodeAddress(
|
|
|
|
invoice.FallbackAddr, cfg.ChainParams,
|
|
|
|
)
|
2019-01-14 17:56:59 +01:00
|
|
|
if err != nil {
|
2022-09-09 08:56:21 -07:00
|
|
|
return nil, nil, fmt.Errorf("invalid fallback "+
|
|
|
|
"address: %v", err)
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
2022-04-22 22:03:08 -04:00
|
|
|
|
|
|
|
if !addr.IsForNet(cfg.ChainParams) {
|
|
|
|
return nil, nil, fmt.Errorf("fallback address is not "+
|
|
|
|
"for %s", cfg.ChainParams.Name)
|
|
|
|
}
|
|
|
|
|
2019-01-14 17:56:59 +01:00
|
|
|
options = append(options, zpay32.FallbackAddr(addr))
|
|
|
|
}
|
|
|
|
|
2021-05-26 18:10:58 -07:00
|
|
|
switch {
|
2019-01-14 17:56:59 +01:00
|
|
|
// 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.
|
2021-05-26 18:10:58 -07:00
|
|
|
case invoice.Expiry > 0:
|
2019-01-14 17:56:59 +01:00
|
|
|
|
|
|
|
// We'll ensure that the specified expiry is restricted to sane
|
|
|
|
// number of seconds. As a result, we'll reject an invoice with
|
|
|
|
// an expiry greater than 1 year.
|
|
|
|
maxExpiry := time.Hour * 24 * 365
|
|
|
|
expSeconds := invoice.Expiry
|
|
|
|
|
|
|
|
if float64(expSeconds) > maxExpiry.Seconds() {
|
2019-01-15 10:06:48 +01:00
|
|
|
return nil, nil, fmt.Errorf("expiry of %v seconds "+
|
2019-01-14 17:56:59 +01:00
|
|
|
"greater than max expiry of %v seconds",
|
|
|
|
float64(expSeconds), maxExpiry.Seconds())
|
|
|
|
}
|
|
|
|
|
|
|
|
expiry := time.Duration(invoice.Expiry) * time.Second
|
|
|
|
options = append(options, zpay32.Expiry(expiry))
|
2021-05-26 18:10:58 -07:00
|
|
|
|
|
|
|
// If no custom expiry is provided, use the default MPP expiry.
|
|
|
|
case !invoice.Amp:
|
|
|
|
options = append(options, zpay32.Expiry(DefaultInvoiceExpiry))
|
|
|
|
|
|
|
|
// Otherwise, use the default AMP expiry.
|
|
|
|
default:
|
2022-09-09 08:56:21 -07:00
|
|
|
defaultExpiry := zpay32.Expiry(DefaultAMPInvoiceExpiry)
|
|
|
|
options = append(options, defaultExpiry)
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// 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.
|
2019-01-14 17:56:59 +01:00
|
|
|
if len(invoice.DescriptionHash) > 0 {
|
|
|
|
var descHash [32]byte
|
|
|
|
copy(descHash[:], invoice.DescriptionHash[:])
|
|
|
|
options = append(options, zpay32.DescriptionHash(descHash))
|
|
|
|
} else {
|
|
|
|
// Use the memo field as the description. If this is not set
|
|
|
|
// this will just be an empty string.
|
|
|
|
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 {
|
2023-02-18 11:53:27 +08:00
|
|
|
case invoice.CltvExpiry > routing.MaxCLTVDelta:
|
2022-09-09 08:56:21 -07:00
|
|
|
return nil, nil, fmt.Errorf("CLTV delta of %v is too large, "+
|
|
|
|
"max accepted is: %v", invoice.CltvExpiry,
|
|
|
|
math.MaxUint16)
|
|
|
|
|
2019-01-14 17:56:59 +01:00
|
|
|
case invoice.CltvExpiry != 0:
|
2020-07-24 13:13:56 -07:00
|
|
|
// Disallow user-chosen final CLTV deltas below the required
|
|
|
|
// minimum.
|
|
|
|
if invoice.CltvExpiry < routing.MinCLTVDelta {
|
|
|
|
return nil, nil, fmt.Errorf("CLTV delta of %v must be "+
|
|
|
|
"greater than minimum of %v",
|
2024-02-19 12:04:43 -05:00
|
|
|
invoice.CltvExpiry, routing.MinCLTVDelta)
|
2020-07-24 13:13:56 -07:00
|
|
|
}
|
|
|
|
|
2019-01-14 17:56:59 +01:00
|
|
|
options = append(options,
|
|
|
|
zpay32.CLTVExpiry(invoice.CltvExpiry))
|
2022-09-09 08:56:21 -07:00
|
|
|
|
2019-01-14 17:56:59 +01:00
|
|
|
default:
|
|
|
|
// TODO(roasbeef): assumes set delta between versions
|
2022-09-09 08:56:21 -07:00
|
|
|
defaultCLTVExpiry := uint64(cfg.DefaultCLTVExpiry)
|
|
|
|
options = append(options, zpay32.CLTVExpiry(defaultCLTVExpiry))
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2022-09-09 08:56:21 -07:00
|
|
|
// We make sure that the given invoice routing hints number is within
|
|
|
|
// the valid range
|
2022-09-13 09:23:05 -07:00
|
|
|
if len(invoice.RouteHints) > maxHopHints {
|
|
|
|
return nil, nil, fmt.Errorf("number of routing hints must "+
|
|
|
|
"not exceed maximum of %v", maxHopHints)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Include route hints if needed.
|
|
|
|
if len(invoice.RouteHints) > 0 || invoice.Private {
|
|
|
|
// Validate provided hop hints.
|
|
|
|
for _, hint := range invoice.RouteHints {
|
|
|
|
if len(hint) == 0 {
|
|
|
|
return nil, nil, fmt.Errorf("number of hop " +
|
|
|
|
"hint within a route must be positive")
|
|
|
|
}
|
2019-11-03 15:00:27 +02:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
totalHopHints := len(invoice.RouteHints)
|
|
|
|
if invoice.Private {
|
|
|
|
totalHopHints = maxHopHints
|
|
|
|
}
|
2019-11-03 15:00:27 +02:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
hopHintsCfg := newSelectHopHintsCfg(cfg, totalHopHints)
|
|
|
|
hopHints, err := PopulateHopHints(
|
|
|
|
hopHintsCfg, amtMSat, invoice.RouteHints,
|
|
|
|
)
|
2019-01-14 17:56:59 +01:00
|
|
|
if err != nil {
|
2022-09-13 09:23:05 -07:00
|
|
|
return nil, nil, fmt.Errorf("unable to populate hop "+
|
|
|
|
"hints: %v", err)
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// Convert our set of selected hop hints into route
|
|
|
|
// hints and add to our invoice options.
|
|
|
|
for _, hopHint := range hopHints {
|
|
|
|
routeHint := zpay32.RouteHint(hopHint)
|
2022-01-27 15:09:19 +02:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
options = append(
|
|
|
|
options, routeHint,
|
2020-08-11 17:48:14 -07:00
|
|
|
)
|
|
|
|
}
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2019-12-10 13:09:52 -08:00
|
|
|
// Set our desired invoice features and add them to our list of options.
|
2021-05-06 09:15:15 -07:00
|
|
|
var invoiceFeatures *lnwire.FeatureVector
|
|
|
|
if invoice.Amp {
|
|
|
|
invoiceFeatures = cfg.GenAmpInvoiceFeatures()
|
|
|
|
} else {
|
|
|
|
invoiceFeatures = cfg.GenInvoiceFeatures()
|
|
|
|
}
|
2019-11-22 02:24:28 -08:00
|
|
|
options = append(options, zpay32.Features(invoiceFeatures))
|
|
|
|
|
2019-12-05 07:59:31 -08:00
|
|
|
// Generate and set a random payment address for this invoice. If the
|
|
|
|
// sender understands payment addresses, this can be used to avoid
|
|
|
|
// intermediaries probing the receiver.
|
|
|
|
var paymentAddr [32]byte
|
|
|
|
if _, err := rand.Read(paymentAddr[:]); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
options = append(options, zpay32.PaymentAddr(paymentAddr))
|
|
|
|
|
2019-01-14 17:56:59 +01:00
|
|
|
// Create and encode the payment request as a bech32 (zpay32) string.
|
|
|
|
creationDate := time.Now()
|
|
|
|
payReq, err := zpay32.NewInvoice(
|
2018-10-05 10:14:56 +02:00
|
|
|
cfg.ChainParams, paymentHash, creationDate, options...,
|
2019-01-14 17:56:59 +01:00
|
|
|
)
|
|
|
|
if err != nil {
|
2019-01-15 10:06:48 +01:00
|
|
|
return nil, nil, err
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2021-09-23 16:54:27 +02:00
|
|
|
payReqString, err := payReq.Encode(zpay32.MessageSigner{
|
|
|
|
SignCompact: func(msg []byte) ([]byte, error) {
|
|
|
|
return cfg.NodeSigner.SignMessageCompact(msg, false)
|
2019-01-14 17:56:59 +01:00
|
|
|
},
|
2021-09-23 16:54:27 +02:00
|
|
|
})
|
2019-01-14 17:56:59 +01:00
|
|
|
if err != nil {
|
2019-01-15 10:06:48 +01:00
|
|
|
return nil, nil, err
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2022-11-30 03:00:37 -08:00
|
|
|
newInvoice := &invoices.Invoice{
|
2019-01-14 17:56:59 +01:00
|
|
|
CreationDate: creationDate,
|
|
|
|
Memo: []byte(invoice.Memo),
|
|
|
|
PaymentRequest: []byte(payReqString),
|
2022-11-30 03:00:37 -08:00
|
|
|
Terms: invoices.ContractTerm{
|
2019-11-22 02:25:02 -08:00
|
|
|
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
|
|
|
|
Expiry: payReq.Expiry(),
|
2019-01-14 17:56:59 +01:00
|
|
|
Value: amtMSat,
|
|
|
|
PaymentPreimage: paymentPreimage,
|
2019-12-05 07:59:31 -08:00
|
|
|
PaymentAddr: paymentAddr,
|
2019-11-22 02:24:28 -08:00
|
|
|
Features: invoiceFeatures,
|
2019-01-14 17:56:59 +01:00
|
|
|
},
|
2020-04-08 13:47:10 +02:00
|
|
|
HodlInvoice: invoice.HodlInvoice,
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
log.Tracef("[addinvoice] adding new invoice %v",
|
2024-07-25 22:18:00 +08:00
|
|
|
lnutils.SpewLogClosure(newInvoice))
|
2019-01-14 17:56:59 +01:00
|
|
|
|
|
|
|
// With all sanity checks passed, write the invoice to the database.
|
2023-10-11 13:42:59 +02:00
|
|
|
_, err = cfg.AddInvoice(ctx, newInvoice, paymentHash)
|
2019-01-14 17:56:59 +01:00
|
|
|
if err != nil {
|
2019-01-15 10:06:48 +01:00
|
|
|
return nil, nil, err
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
|
|
|
|
2018-10-05 10:14:56 +02:00
|
|
|
return &paymentHash, newInvoice, nil
|
2019-01-14 17:56:59 +01:00
|
|
|
}
|
2020-08-10 16:06:39 -07:00
|
|
|
|
2020-08-11 17:38:26 -07:00
|
|
|
// chanCanBeHopHint returns true if the target channel is eligible to be a hop
|
|
|
|
// hint.
|
2022-01-27 15:09:19 +02:00
|
|
|
func chanCanBeHopHint(channel *HopHintInfo, cfg *SelectHopHintsCfg) (
|
2023-11-08 11:18:45 +02:00
|
|
|
*models.ChannelEdgePolicy, bool) {
|
2020-08-11 17:38:26 -07:00
|
|
|
|
|
|
|
// Since we're only interested in our private channels, we'll skip
|
|
|
|
// public ones.
|
2022-01-27 15:09:18 +02:00
|
|
|
if channel.IsPublic {
|
2020-08-11 17:38:26 -07:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure the channel is active.
|
2022-01-27 15:09:18 +02:00
|
|
|
if !channel.IsActive {
|
2020-08-11 17:38:26 -07:00
|
|
|
log.Debugf("Skipping channel %v due to not "+
|
|
|
|
"being eligible to forward payments",
|
2022-01-27 15:09:18 +02:00
|
|
|
channel.ShortChannelID)
|
2020-08-11 17:38:26 -07:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// To ensure we don't leak unadvertised nodes, we'll make sure our
|
|
|
|
// counterparty is publicly advertised within the network. Otherwise,
|
|
|
|
// we'll end up leaking information about nodes that intend to stay
|
|
|
|
// unadvertised, like in the case of a node only having private
|
|
|
|
// channels.
|
|
|
|
var remotePub [33]byte
|
2022-01-27 15:09:18 +02:00
|
|
|
copy(remotePub[:], channel.RemotePubkey.SerializeCompressed())
|
2022-01-27 15:09:19 +02:00
|
|
|
isRemoteNodePublic, err := cfg.IsPublicNode(remotePub)
|
2020-08-11 17:38:26 -07:00
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Unable to determine if node %x "+
|
|
|
|
"is advertised: %v", remotePub, err)
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isRemoteNodePublic {
|
|
|
|
log.Debugf("Skipping channel %v due to "+
|
|
|
|
"counterparty %x being unadvertised",
|
2022-01-27 15:09:18 +02:00
|
|
|
channel.ShortChannelID, remotePub)
|
2020-08-11 17:38:26 -07:00
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the policies for each end of the channel.
|
2022-01-27 15:09:19 +02:00
|
|
|
info, p1, p2, err := cfg.FetchChannelEdgesByID(channel.ShortChannelID)
|
2020-08-11 17:38:26 -07:00
|
|
|
if err != nil {
|
2022-04-04 16:49:14 -04:00
|
|
|
// In the case of zero-conf channels, it may be the case that
|
|
|
|
// the alias SCID was deleted from the graph, and replaced by
|
|
|
|
// the confirmed SCID. Check the Graph for the confirmed SCID.
|
|
|
|
confirmedScid := channel.ConfirmedScidZC
|
|
|
|
info, p1, p2, err = cfg.FetchChannelEdgesByID(confirmedScid)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Unable to fetch the routing policies for "+
|
|
|
|
"the edges of the channel %v: %v",
|
|
|
|
channel.ShortChannelID, err)
|
|
|
|
return nil, false
|
|
|
|
}
|
2020-08-11 17:38:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Now, we'll need to determine which is the correct policy for HTLCs
|
|
|
|
// being sent from the remote node.
|
2023-11-08 11:18:45 +02:00
|
|
|
var remotePolicy *models.ChannelEdgePolicy
|
2020-08-11 17:38:26 -07:00
|
|
|
if bytes.Equal(remotePub[:], info.NodeKey1Bytes[:]) {
|
|
|
|
remotePolicy = p1
|
|
|
|
} else {
|
|
|
|
remotePolicy = p2
|
|
|
|
}
|
|
|
|
|
|
|
|
return remotePolicy, true
|
|
|
|
}
|
|
|
|
|
2022-01-27 15:09:18 +02:00
|
|
|
// HopHintInfo contains the channel information required to create a hop hint.
|
|
|
|
type HopHintInfo struct {
|
|
|
|
// IsPublic indicates whether a channel is advertised to the network.
|
|
|
|
IsPublic bool
|
|
|
|
|
|
|
|
// IsActive indicates whether the channel is online and available for
|
|
|
|
// use.
|
|
|
|
IsActive bool
|
|
|
|
|
|
|
|
// FundingOutpoint is the funding txid:index for the channel.
|
|
|
|
FundingOutpoint wire.OutPoint
|
|
|
|
|
|
|
|
// RemotePubkey is the public key of the remote party that this channel
|
|
|
|
// is in.
|
|
|
|
RemotePubkey *btcec.PublicKey
|
|
|
|
|
|
|
|
// RemoteBalance is the remote party's balance (our current incoming
|
|
|
|
// capacity).
|
|
|
|
RemoteBalance lnwire.MilliSatoshi
|
|
|
|
|
|
|
|
// ShortChannelID is the short channel ID of the channel.
|
|
|
|
ShortChannelID uint64
|
2022-04-04 16:49:14 -04:00
|
|
|
|
|
|
|
// ConfirmedScidZC is the confirmed SCID of a zero-conf channel. This
|
|
|
|
// may be used for looking up a channel in the graph.
|
|
|
|
ConfirmedScidZC uint64
|
|
|
|
|
|
|
|
// ScidAliasFeature denotes whether the channel has negotiated the
|
|
|
|
// option-scid-alias feature bit.
|
|
|
|
ScidAliasFeature bool
|
2022-01-27 15:09:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func newHopHintInfo(c *channeldb.OpenChannel, isActive bool) *HopHintInfo {
|
|
|
|
isPublic := c.ChannelFlags&lnwire.FFAnnounceChannel != 0
|
|
|
|
|
|
|
|
return &HopHintInfo{
|
2022-04-04 16:49:14 -04:00
|
|
|
IsPublic: isPublic,
|
|
|
|
IsActive: isActive,
|
|
|
|
FundingOutpoint: c.FundingOutpoint,
|
|
|
|
RemotePubkey: c.IdentityPub,
|
|
|
|
RemoteBalance: c.LocalCommitment.RemoteBalance,
|
|
|
|
ShortChannelID: c.ShortChannelID.ToUint64(),
|
|
|
|
ConfirmedScidZC: c.ZeroConfRealScid().ToUint64(),
|
|
|
|
ScidAliasFeature: c.ChanType.HasScidAliasFeature(),
|
2022-01-27 15:09:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// newHopHint returns a new hop hint using the relevant data from a hopHintInfo
|
|
|
|
// and a ChannelEdgePolicy.
|
|
|
|
func newHopHint(hopHintInfo *HopHintInfo,
|
2023-11-08 11:18:45 +02:00
|
|
|
chanPolicy *models.ChannelEdgePolicy) zpay32.HopHint {
|
2022-09-13 09:23:05 -07:00
|
|
|
|
|
|
|
return zpay32.HopHint{
|
|
|
|
NodeID: hopHintInfo.RemotePubkey,
|
|
|
|
ChannelID: hopHintInfo.ShortChannelID,
|
|
|
|
FeeBaseMSat: uint32(chanPolicy.FeeBaseMSat),
|
|
|
|
FeeProportionalMillionths: uint32(
|
|
|
|
chanPolicy.FeeProportionalMillionths,
|
|
|
|
),
|
|
|
|
CLTVExpiryDelta: chanPolicy.TimeLockDelta,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-27 15:09:19 +02:00
|
|
|
// SelectHopHintsCfg contains the dependencies required to obtain hop hints
|
|
|
|
// for an invoice.
|
|
|
|
type SelectHopHintsCfg struct {
|
|
|
|
// IsPublicNode is returns a bool indicating whether the node with the
|
|
|
|
// given public key is seen as a public node in the graph from the
|
|
|
|
// graph's source node's point of view.
|
|
|
|
IsPublicNode func(pubKey [33]byte) (bool, error)
|
|
|
|
|
|
|
|
// FetchChannelEdgesByID attempts to lookup the two directed edges for
|
|
|
|
// the channel identified by the channel ID.
|
2023-11-08 11:18:45 +02:00
|
|
|
FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
|
|
|
|
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy,
|
2022-01-27 15:09:19 +02:00
|
|
|
error)
|
2022-04-04 16:49:14 -04:00
|
|
|
|
|
|
|
// GetAlias allows the peer's alias SCID to be retrieved for private
|
|
|
|
// option_scid_alias channels.
|
|
|
|
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
|
2022-09-13 09:23:05 -07:00
|
|
|
|
|
|
|
// FetchAllChannels retrieves all open channels currently stored
|
|
|
|
// within the database.
|
|
|
|
FetchAllChannels func() ([]*channeldb.OpenChannel, error)
|
|
|
|
|
|
|
|
// IsChannelActive checks whether the channel identified by the provided
|
|
|
|
// ChannelID is considered active.
|
|
|
|
IsChannelActive func(chanID lnwire.ChannelID) bool
|
|
|
|
|
|
|
|
// MaxHopHints is the maximum number of hop hints we are interested in.
|
|
|
|
MaxHopHints int
|
2022-01-27 15:09:19 +02:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
func newSelectHopHintsCfg(invoicesCfg *AddInvoiceConfig,
|
|
|
|
maxHopHints int) *SelectHopHintsCfg {
|
|
|
|
|
2022-01-27 15:09:19 +02:00
|
|
|
return &SelectHopHintsCfg{
|
2022-09-13 09:23:05 -07:00
|
|
|
FetchAllChannels: invoicesCfg.ChanDB.FetchAllChannels,
|
|
|
|
IsChannelActive: invoicesCfg.IsChannelActive,
|
2022-01-27 15:09:19 +02:00
|
|
|
IsPublicNode: invoicesCfg.Graph.IsPublicNode,
|
|
|
|
FetchChannelEdgesByID: invoicesCfg.Graph.FetchChannelEdgesByID,
|
2022-04-04 16:49:14 -04:00
|
|
|
GetAlias: invoicesCfg.GetAlias,
|
2022-09-13 09:23:05 -07:00
|
|
|
MaxHopHints: maxHopHints,
|
2022-01-27 15:09:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-07 09:03:11 +02:00
|
|
|
// sufficientHints checks whether we have sufficient hop hints, based on the
|
2022-09-13 09:23:05 -07:00
|
|
|
// any of the following criteria:
|
|
|
|
// - Hop hint count: the number of hints have reach our max target.
|
2022-10-26 12:57:52 -05:00
|
|
|
// - Total incoming capacity (for non-zero invoice amounts): the sum of the
|
|
|
|
// remote balance amount in the hints is bigger of equal than our target
|
|
|
|
// (currently twice the invoice amount)
|
2022-02-07 09:03:11 +02:00
|
|
|
//
|
|
|
|
// We limit our number of hop hints like this to keep our invoice size down,
|
|
|
|
// and to avoid leaking all our private channels when we don't need to.
|
2022-09-13 09:23:05 -07:00
|
|
|
func sufficientHints(nHintsLeft int, currentAmount,
|
|
|
|
targetAmount lnwire.MilliSatoshi) bool {
|
2022-02-07 09:03:11 +02:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
if nHintsLeft <= 0 {
|
|
|
|
log.Debugf("Reached targeted number of hop hints")
|
2022-02-07 09:03:11 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-10-26 12:57:52 -05:00
|
|
|
if targetAmount != 0 && currentAmount >= targetAmount {
|
2022-02-07 09:03:11 +02:00
|
|
|
log.Debugf("Total hint amount: %v has reached target hint "+
|
2022-09-13 09:23:05 -07:00
|
|
|
"bandwidth: %v", currentAmount, targetAmount)
|
2022-02-07 09:03:11 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// getPotentialHints returns a slice of open channels that should be considered
|
|
|
|
// for the hopHint list in an invoice. The slice is sorted in descending order
|
|
|
|
// based on the remote balance.
|
|
|
|
func getPotentialHints(cfg *SelectHopHintsCfg) ([]*channeldb.OpenChannel,
|
|
|
|
error) {
|
2022-02-07 09:03:11 +02:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// TODO(positiveblue): get the channels slice already filtered by
|
|
|
|
// private == true and sorted by RemoteBalance?
|
|
|
|
openChannels, err := cfg.FetchAllChannels()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-02-07 09:03:11 +02:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
privateChannels := make([]*channeldb.OpenChannel, 0, len(openChannels))
|
|
|
|
for _, oc := range openChannels {
|
|
|
|
isPublic := oc.ChannelFlags&lnwire.FFAnnounceChannel != 0
|
|
|
|
if !isPublic {
|
|
|
|
privateChannels = append(privateChannels, oc)
|
2020-08-10 16:18:48 -07:00
|
|
|
}
|
2022-09-13 09:23:05 -07:00
|
|
|
}
|
2020-08-10 16:18:48 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// Sort the channels in descending remote balance.
|
|
|
|
compareRemoteBalance := func(i, j int) bool {
|
|
|
|
iBalance := privateChannels[i].LocalCommitment.RemoteBalance
|
|
|
|
jBalance := privateChannels[j].LocalCommitment.RemoteBalance
|
|
|
|
return iBalance > jBalance
|
|
|
|
}
|
|
|
|
sort.Slice(privateChannels, compareRemoteBalance)
|
2020-08-10 16:18:48 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
return privateChannels, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// shouldIncludeChannel returns true if the channel passes all the checks to
|
|
|
|
// be a hopHint in a given invoice.
|
|
|
|
func shouldIncludeChannel(cfg *SelectHopHintsCfg,
|
|
|
|
channel *channeldb.OpenChannel,
|
|
|
|
alreadyIncluded map[uint64]bool) (zpay32.HopHint, lnwire.MilliSatoshi,
|
|
|
|
bool) {
|
|
|
|
|
|
|
|
if _, ok := alreadyIncluded[channel.ShortChannelID.ToUint64()]; ok {
|
|
|
|
return zpay32.HopHint{}, 0, false
|
|
|
|
}
|
|
|
|
|
|
|
|
chanID := lnwire.NewChanIDFromOutPoint(
|
2024-01-29 16:19:15 -05:00
|
|
|
channel.FundingOutpoint,
|
2022-09-13 09:23:05 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
hopHintInfo := newHopHintInfo(channel, cfg.IsChannelActive(chanID))
|
|
|
|
|
|
|
|
// If this channel can't be a hop hint, then skip it.
|
|
|
|
edgePolicy, canBeHopHint := chanCanBeHopHint(hopHintInfo, cfg)
|
|
|
|
if edgePolicy == nil || !canBeHopHint {
|
|
|
|
return zpay32.HopHint{}, 0, false
|
|
|
|
}
|
|
|
|
|
|
|
|
if hopHintInfo.ScidAliasFeature {
|
|
|
|
alias, err := cfg.GetAlias(chanID)
|
|
|
|
if err != nil {
|
|
|
|
return zpay32.HopHint{}, 0, false
|
2022-04-04 16:49:14 -04:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
if alias.IsDefault() || alreadyIncluded[alias.ToUint64()] {
|
|
|
|
return zpay32.HopHint{}, 0, false
|
|
|
|
}
|
2020-08-10 16:18:48 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
hopHintInfo.ShortChannelID = alias.ToUint64()
|
2020-08-10 16:18:48 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// Now that we know this channel use usable, add it as a hop hint and
|
|
|
|
// the indexes we'll use later.
|
|
|
|
hopHint := newHopHint(hopHintInfo, edgePolicy)
|
|
|
|
return hopHint, hopHintInfo.RemoteBalance, true
|
|
|
|
}
|
|
|
|
|
|
|
|
// selectHopHints iterates a list of potential hints selecting the valid hop
|
|
|
|
// hints until we have enough hints or run out of channels.
|
|
|
|
//
|
|
|
|
// NOTE: selectHopHints expects potentialHints to be already sorted in
|
|
|
|
// descending priority.
|
|
|
|
func selectHopHints(cfg *SelectHopHintsCfg, nHintsLeft int,
|
|
|
|
targetBandwidth lnwire.MilliSatoshi,
|
|
|
|
potentialHints []*channeldb.OpenChannel,
|
|
|
|
alreadyIncluded map[uint64]bool) [][]zpay32.HopHint {
|
|
|
|
|
|
|
|
currentBandwidth := lnwire.MilliSatoshi(0)
|
|
|
|
hopHints := make([][]zpay32.HopHint, 0, nHintsLeft)
|
|
|
|
for _, channel := range potentialHints {
|
2022-02-07 09:03:11 +02:00
|
|
|
enoughHopHints := sufficientHints(
|
2022-09-13 09:23:05 -07:00
|
|
|
nHintsLeft, currentBandwidth, targetBandwidth,
|
2022-02-07 09:03:11 +02:00
|
|
|
)
|
|
|
|
if enoughHopHints {
|
|
|
|
return hopHints
|
2020-08-10 16:06:39 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
hopHint, remoteBalance, include := shouldIncludeChannel(
|
|
|
|
cfg, channel, alreadyIncluded,
|
|
|
|
)
|
2020-08-10 16:18:48 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
if include {
|
|
|
|
// Now that we now this channel use usable, add it as a hop
|
|
|
|
// hint and the indexes we'll use later.
|
|
|
|
hopHints = append(hopHints, []zpay32.HopHint{hopHint})
|
|
|
|
currentBandwidth += remoteBalance
|
|
|
|
nHintsLeft--
|
2020-08-10 16:18:48 -07:00
|
|
|
}
|
2022-09-13 09:23:05 -07:00
|
|
|
}
|
2020-08-10 16:18:48 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// We do not want to leak information about how our remote balance is
|
|
|
|
// distributed in our private channels. We shuffle the selected ones
|
|
|
|
// here so they do not appear in order in the invoice.
|
|
|
|
mathRand.Shuffle(
|
|
|
|
len(hopHints), func(i, j int) {
|
|
|
|
hopHints[i], hopHints[j] = hopHints[j], hopHints[i]
|
|
|
|
},
|
|
|
|
)
|
|
|
|
return hopHints
|
|
|
|
}
|
2020-08-10 16:06:39 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// PopulateHopHints will select up to cfg.MaxHophints from the current open
|
|
|
|
// channels. The set of hop hints will be returned as a slice of functional
|
|
|
|
// options that'll append the route hint to the set of all route hints.
|
|
|
|
//
|
|
|
|
// TODO(roasbeef): do proper sub-set sum max hints usually << numChans.
|
|
|
|
func PopulateHopHints(cfg *SelectHopHintsCfg, amtMSat lnwire.MilliSatoshi,
|
|
|
|
forcedHints [][]zpay32.HopHint) ([][]zpay32.HopHint, error) {
|
2022-04-04 16:49:14 -04:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
hopHints := forcedHints
|
2020-08-10 16:06:39 -07:00
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
// If we already have enough hints we don't need to add any more.
|
|
|
|
nHintsLeft := cfg.MaxHopHints - len(hopHints)
|
|
|
|
if nHintsLeft <= 0 {
|
|
|
|
return hopHints, nil
|
2020-08-10 16:06:39 -07:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:23:05 -07:00
|
|
|
alreadyIncluded := make(map[uint64]bool)
|
|
|
|
for _, hopHint := range hopHints {
|
|
|
|
alreadyIncluded[hopHint[0].ChannelID] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
potentialHints, err := getPotentialHints(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
targetBandwidth := amtMSat * hopHintFactor
|
|
|
|
selectedHints := selectHopHints(
|
|
|
|
cfg, nHintsLeft, targetBandwidth, potentialHints,
|
|
|
|
alreadyIncluded,
|
|
|
|
)
|
|
|
|
|
|
|
|
hopHints = append(hopHints, selectedHints...)
|
|
|
|
return hopHints, nil
|
2020-08-10 16:06:39 -07:00
|
|
|
}
|
2024-05-04 10:37:41 +02:00
|
|
|
|
2024-07-04 16:40:54 +02:00
|
|
|
// buildBlindedPathCfg defines the various resources and configuration values
|
|
|
|
// required to build a blinded payment path to this node.
|
|
|
|
type buildBlindedPathCfg struct {
|
|
|
|
// findRoutes returns a set of routes to us that can be used for the
|
|
|
|
// construction of blinded paths. These routes will consist of real
|
|
|
|
// nodes advertising the route blinding feature bit. They may be of
|
|
|
|
// various lengths and may even contain only a single hop. Any route
|
|
|
|
// shorter than minNumHops will be padded with dummy hops during route
|
|
|
|
// construction.
|
|
|
|
findRoutes func(value lnwire.MilliSatoshi) ([]*route.Route, error)
|
|
|
|
|
|
|
|
// fetchChannelEdgesByID attempts to look up the two directed edges for
|
|
|
|
// the channel identified by the channel ID.
|
|
|
|
fetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
|
|
|
|
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error)
|
|
|
|
|
|
|
|
// bestHeight can be used to fetch the best block height that this node
|
|
|
|
// is aware of.
|
|
|
|
bestHeight func() (uint32, error)
|
|
|
|
|
|
|
|
// addPolicyBuffer is a function that can be used to alter the policy
|
|
|
|
// values of the given channel edge. The main reason for doing this is
|
|
|
|
// to add a safety buffer so that if the node makes small policy changes
|
|
|
|
// during the lifetime of the blinded path, then the path remains valid
|
|
|
|
// and so probing is more difficult. Note that this will only be called
|
|
|
|
// for the policies of real nodes and won't be applied to
|
|
|
|
// dummyHopPolicy.
|
|
|
|
addPolicyBuffer func(policy *blindedHopPolicy) (*blindedHopPolicy,
|
|
|
|
error)
|
|
|
|
|
|
|
|
// pathID is the secret data to embed in the blinded path data that we
|
|
|
|
// will receive back as the recipient. This is the equivalent of the
|
|
|
|
// payment address used in normal payments. It lets the recipient check
|
|
|
|
// that the path is being used in the correct context.
|
|
|
|
pathID []byte
|
|
|
|
|
|
|
|
// valueMsat is the payment amount in milli-satoshis that must be
|
|
|
|
// routed. This will be used for selecting appropriate routes to use for
|
|
|
|
// the blinded path.
|
|
|
|
valueMsat lnwire.MilliSatoshi
|
|
|
|
|
|
|
|
// minFinalCLTVExpiryDelta is the minimum CLTV delta that the recipient
|
|
|
|
// requires for the final hop of the payment.
|
|
|
|
//
|
|
|
|
// NOTE that the caller is responsible for adding additional block
|
|
|
|
// padding to this value to account for blocks being mined while the
|
|
|
|
// payment is in-flight.
|
|
|
|
minFinalCLTVExpiryDelta uint32
|
|
|
|
|
|
|
|
// blocksUntilExpiry is the number of blocks that this blinded path
|
|
|
|
// should remain valid for.
|
|
|
|
blocksUntilExpiry uint32
|
|
|
|
|
|
|
|
// minNumHops is the minimum number of hops that each blinded path
|
|
|
|
// should be. If the number of hops in a path returned by findRoutes is
|
|
|
|
// less than this number, then dummy hops will be post-fixed to the
|
|
|
|
// route.
|
|
|
|
minNumHops uint8
|
|
|
|
|
|
|
|
// dummyHopPolicy holds the policy values that should be used for dummy
|
|
|
|
// hops. Note that these will _not_ be buffered via addPolicyBuffer.
|
|
|
|
dummyHopPolicy *blindedHopPolicy
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildBlindedPaymentPaths uses the passed config to construct a set of blinded
|
|
|
|
// payment paths that can be added to the invoice.
|
|
|
|
func buildBlindedPaymentPaths(cfg *buildBlindedPathCfg) (
|
|
|
|
[]*zpay32.BlindedPaymentPath, error) {
|
|
|
|
|
|
|
|
if cfg.minFinalCLTVExpiryDelta >= cfg.blocksUntilExpiry {
|
|
|
|
return nil, fmt.Errorf("blinded path CLTV expiry delta (%d) "+
|
|
|
|
"must be greater than the minimum final CLTV expiry "+
|
|
|
|
"delta (%d)", cfg.blocksUntilExpiry,
|
|
|
|
cfg.minFinalCLTVExpiryDelta)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find some appropriate routes for the value to be routed. This will
|
|
|
|
// return a set of routes made up of real nodes.
|
|
|
|
routes, err := cfg.findRoutes(cfg.valueMsat)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(routes) == 0 {
|
|
|
|
return nil, fmt.Errorf("could not find any routes to self to " +
|
|
|
|
"use for blinded route construction")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not every route returned will necessarily result in a usable blinded
|
|
|
|
// path and so the number of paths returned might be less than the
|
|
|
|
// number of real routes returned by findRoutes above.
|
|
|
|
paths := make([]*zpay32.BlindedPaymentPath, 0, len(routes))
|
|
|
|
|
|
|
|
// For each route returned, we will construct the associated blinded
|
|
|
|
// payment path.
|
|
|
|
for _, route := range routes {
|
|
|
|
path, err := buildBlindedPaymentPath(
|
|
|
|
cfg, extractCandidatePath(route),
|
|
|
|
)
|
|
|
|
if errors.Is(err, errInvalidBlindedPath) {
|
|
|
|
log.Debugf("Not using route (%s) as a blinded path "+
|
|
|
|
"since it resulted in an invalid blinded path",
|
|
|
|
route)
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
paths = append(paths, path)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(paths) == 0 {
|
|
|
|
return nil, fmt.Errorf("could not build any blinded paths")
|
|
|
|
}
|
|
|
|
|
|
|
|
return paths, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildBlindedPaymentPath takes a route from an introduction node to this node
|
|
|
|
// and uses the given config to convert it into a blinded payment path.
|
|
|
|
func buildBlindedPaymentPath(cfg *buildBlindedPathCfg, path *candidatePath) (
|
|
|
|
*zpay32.BlindedPaymentPath, error) {
|
|
|
|
|
|
|
|
// Pad the given route with dummy hops until the minimum number of hops
|
|
|
|
// is met.
|
|
|
|
err := path.padWithDummyHops(cfg.minNumHops)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not collect blinded path relay "+
|
|
|
|
"info: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
relayInfo := make([]*record.PaymentRelayInfo, len(hops))
|
|
|
|
for i, hop := range hops {
|
|
|
|
relayInfo[i] = hop.relayInfo
|
|
|
|
}
|
|
|
|
|
|
|
|
// Using the collected relay info, we can calculate the aggregated
|
|
|
|
// policy values for the route.
|
|
|
|
baseFee, feeRate, cltvDelta := calcBlindedPathPolicies(
|
|
|
|
relayInfo, uint16(cfg.minFinalCLTVExpiryDelta),
|
|
|
|
)
|
|
|
|
|
|
|
|
currentHeight, err := cfg.bestHeight()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// The next step is to calculate the payment constraints to communicate
|
|
|
|
// to each hop and to package up the hop info for each hop. We will
|
|
|
|
// handle the final hop first since its payload looks a bit different,
|
|
|
|
// and then we will iterate backwards through the remaining hops.
|
|
|
|
//
|
|
|
|
// Note that the +1 here is required because the route won't have the
|
|
|
|
// introduction node included in the "Hops". But since we want to create
|
|
|
|
// payloads for all the hops as well as the introduction node, we add 1
|
|
|
|
// here to get the full hop length along with the introduction node.
|
|
|
|
hopDataSet := make([]*hopData, 0, len(path.hops)+1)
|
|
|
|
|
|
|
|
// Determine the maximum CLTV expiry for the destination node.
|
|
|
|
cltvExpiry := currentHeight + cfg.blocksUntilExpiry +
|
|
|
|
cfg.minFinalCLTVExpiryDelta
|
|
|
|
|
|
|
|
constraints := &record.PaymentConstraints{
|
|
|
|
MaxCltvExpiry: cltvExpiry,
|
|
|
|
HtlcMinimumMsat: minHTLC,
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the blinded route has only a source node (introduction node) and
|
|
|
|
// no hops, then the destination node is also the source node.
|
|
|
|
finalHopPubKey := path.introNode
|
|
|
|
if len(path.hops) > 0 {
|
|
|
|
finalHopPubKey = path.hops[len(path.hops)-1].pubKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// For the final hop, we only send it the path ID and payment
|
|
|
|
// constraints.
|
|
|
|
info, err := buildFinalHopRouteData(
|
|
|
|
finalHopPubKey, cfg.pathID, constraints,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
hopDataSet = append(hopDataSet, info)
|
|
|
|
|
|
|
|
// Iterate through the remaining (non-final) hops, back to front.
|
|
|
|
for i := len(hops) - 1; i >= 0; i-- {
|
|
|
|
hop := hops[i]
|
|
|
|
|
|
|
|
cltvExpiry += uint32(hop.relayInfo.CltvExpiryDelta)
|
|
|
|
|
|
|
|
constraints = &record.PaymentConstraints{
|
|
|
|
MaxCltvExpiry: cltvExpiry,
|
|
|
|
HtlcMinimumMsat: minHTLC,
|
|
|
|
}
|
|
|
|
|
|
|
|
var info *hopData
|
|
|
|
if hop.nextHopIsDummy {
|
|
|
|
info, err = buildDummyRouteData(
|
|
|
|
hop.hopPubKey, hop.relayInfo, constraints,
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
info, err = buildHopRouteData(
|
|
|
|
hop.hopPubKey, hop.nextSCID, hop.relayInfo,
|
|
|
|
constraints,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
hopDataSet = append(hopDataSet, info)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort the hop info list in reverse order so that the data for the
|
|
|
|
// introduction node is first.
|
|
|
|
slices.Reverse(hopDataSet)
|
|
|
|
|
|
|
|
// Add padding to each route data instance until the encrypted data
|
|
|
|
// blobs are all the same size.
|
|
|
|
paymentPath, _, err := padHopInfo(hopDataSet, true)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Derive an ephemeral session key.
|
|
|
|
sessionKey, err := btcec.NewPrivateKey()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Encrypt the hop info.
|
|
|
|
blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(blindedPath.BlindedHops) < 1 {
|
|
|
|
return nil, fmt.Errorf("blinded path must have at least one " +
|
|
|
|
"hop")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Overwrite the introduction point's blinded pub key with the real
|
|
|
|
// pub key since then we can use this more compact format in the
|
|
|
|
// invoice without needing to encode the un-used blinded node pub key of
|
|
|
|
// the intro node.
|
|
|
|
blindedPath.BlindedHops[0].BlindedNodePub =
|
|
|
|
blindedPath.IntroductionPoint
|
|
|
|
|
|
|
|
// Now construct a z32 blinded path.
|
|
|
|
return &zpay32.BlindedPaymentPath{
|
|
|
|
FeeBaseMsat: uint32(baseFee),
|
|
|
|
FeeRate: feeRate,
|
|
|
|
CltvExpiryDelta: cltvDelta,
|
|
|
|
HTLCMinMsat: uint64(minHTLC),
|
|
|
|
HTLCMaxMsat: uint64(maxHTLC),
|
|
|
|
Features: lnwire.EmptyFeatureVector(),
|
|
|
|
FirstEphemeralBlindingPoint: blindedPath.BlindingPoint,
|
|
|
|
Hops: blindedPath.BlindedHops,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// hopRelayInfo packages together the relay info to send to hop on a blinded
|
|
|
|
// path along with the pub key of that hop and the SCID that the hop should
|
|
|
|
// forward the payment on to.
|
|
|
|
type hopRelayInfo struct {
|
|
|
|
hopPubKey route.Vertex
|
|
|
|
nextSCID lnwire.ShortChannelID
|
|
|
|
relayInfo *record.PaymentRelayInfo
|
|
|
|
nextHopIsDummy bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// collectRelayInfo collects the relay policy rules for each relay hop on the
|
|
|
|
// route and applies any policy buffers.
|
|
|
|
//
|
|
|
|
// For the blinded route:
|
|
|
|
//
|
|
|
|
// C --chan(CB)--> B --chan(BA)--> A
|
|
|
|
//
|
|
|
|
// where C is the introduction node, the route.Route struct we are given will
|
|
|
|
// have SourcePubKey set to C's pub key, and then it will have the following
|
|
|
|
// route.Hops:
|
|
|
|
//
|
|
|
|
// - PubKeyBytes: B, ChannelID: chan(CB)
|
|
|
|
// - PubKeyBytes: A, ChannelID: chan(BA)
|
|
|
|
//
|
|
|
|
// We, however, want to collect the channel policies for the following PubKey
|
|
|
|
// and ChannelID pairs:
|
|
|
|
//
|
|
|
|
// - PubKey: C, ChannelID: chan(CB)
|
|
|
|
// - PubKey: B, ChannelID: chan(BA)
|
|
|
|
//
|
|
|
|
// Therefore, when we go through the route and its hops to collect policies, our
|
|
|
|
// index for collecting public keys will be trailing that of the channel IDs by
|
|
|
|
// 1.
|
|
|
|
func collectRelayInfo(cfg *buildBlindedPathCfg, path *candidatePath) (
|
|
|
|
[]*hopRelayInfo, lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
|
|
|
|
|
|
|
|
var (
|
|
|
|
hops = make([]*hopRelayInfo, 0, len(path.hops))
|
|
|
|
minHTLC lnwire.MilliSatoshi
|
|
|
|
maxHTLC lnwire.MilliSatoshi
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// The first pub key is that of the introduction node.
|
|
|
|
hopSource = path.introNode
|
|
|
|
)
|
|
|
|
for _, hop := range path.hops {
|
|
|
|
var (
|
|
|
|
// For dummy hops, we use pre-configured policy values.
|
|
|
|
policy = cfg.dummyHopPolicy
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
if !hop.isDummy {
|
|
|
|
// For real hops, retrieve the channel policy for this
|
|
|
|
// hop's channel ID in the direction pointing away from
|
|
|
|
// the hopSource node.
|
|
|
|
policy, err = getNodeChannelPolicy(
|
|
|
|
cfg, hop.channelID, hopSource,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply any policy changes now before caching the
|
|
|
|
// policy.
|
|
|
|
policy, err = cfg.addPolicyBuffer(policy)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, 0, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is the first policy we are collecting, then use this
|
|
|
|
// policy to set the base values for min/max htlc.
|
|
|
|
if len(hops) == 0 {
|
|
|
|
minHTLC = policy.minHTLCMsat
|
|
|
|
maxHTLC = policy.maxHTLCMsat
|
|
|
|
} else {
|
|
|
|
if policy.minHTLCMsat > minHTLC {
|
|
|
|
minHTLC = policy.minHTLCMsat
|
|
|
|
}
|
|
|
|
|
|
|
|
if policy.maxHTLCMsat < maxHTLC {
|
|
|
|
maxHTLC = policy.maxHTLCMsat
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// From the policy values for this hop, we can collect the
|
|
|
|
// payment relay info that we will send to this hop.
|
|
|
|
hops = append(hops, &hopRelayInfo{
|
|
|
|
hopPubKey: hopSource,
|
|
|
|
nextSCID: lnwire.NewShortChanIDFromInt(hop.channelID),
|
|
|
|
relayInfo: &record.PaymentRelayInfo{
|
|
|
|
FeeRate: policy.feeRate,
|
|
|
|
BaseFee: policy.baseFee,
|
|
|
|
CltvExpiryDelta: policy.cltvExpiryDelta,
|
|
|
|
},
|
|
|
|
nextHopIsDummy: hop.isDummy,
|
|
|
|
})
|
|
|
|
|
|
|
|
// This hop's pub key will be the policy creator for the next
|
|
|
|
// hop.
|
|
|
|
hopSource = hop.pubKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// It can happen that there is no HTLC-range overlap between the various
|
|
|
|
// hops along the path. We return errInvalidBlindedPath to indicate that
|
|
|
|
// this route was not usable
|
|
|
|
if minHTLC > maxHTLC {
|
|
|
|
return nil, 0, 0, fmt.Errorf("%w: resulting blinded path min "+
|
|
|
|
"HTLC value is larger than the resulting max HTLC "+
|
|
|
|
"value", errInvalidBlindedPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
return hops, minHTLC, maxHTLC, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildDummyRouteData constructs the record.BlindedRouteData struct for the
|
|
|
|
// given a hop in a blinded route where the following hop is a dummy hop.
|
|
|
|
func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
|
|
|
|
constraints *record.PaymentConstraints) (*hopData, error) {
|
|
|
|
|
|
|
|
nodeID, err := btcec.ParsePubKey(node[:])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &hopData{
|
|
|
|
data: record.NewDummyHopRouteData(
|
|
|
|
nodeID, *relayInfo, *constraints,
|
|
|
|
),
|
|
|
|
nodeID: nodeID,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildHopRouteData constructs the record.BlindedRouteData struct for the given
|
|
|
|
// non-final hop on a blinded path and packages it with the node's ID.
|
|
|
|
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,
|
|
|
|
relayInfo *record.PaymentRelayInfo,
|
|
|
|
constraints *record.PaymentConstraints) (*hopData, error) {
|
|
|
|
|
|
|
|
// Wrap up the data we want to send to this hop.
|
|
|
|
blindedRouteHopData := record.NewNonFinalBlindedRouteData(
|
|
|
|
scid, nil, *relayInfo, constraints, nil,
|
|
|
|
)
|
|
|
|
|
|
|
|
nodeID, err := btcec.ParsePubKey(node[:])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &hopData{
|
|
|
|
data: blindedRouteHopData,
|
|
|
|
nodeID: nodeID,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// buildFinalHopRouteData constructs the record.BlindedRouteData struct for the
|
|
|
|
// final hop and packages it with the real node ID of the node it is intended
|
|
|
|
// for.
|
|
|
|
func buildFinalHopRouteData(node route.Vertex, pathID []byte,
|
|
|
|
constraints *record.PaymentConstraints) (*hopData, error) {
|
|
|
|
|
|
|
|
blindedRouteHopData := record.NewFinalHopBlindedRouteData(
|
|
|
|
constraints, pathID,
|
|
|
|
)
|
|
|
|
nodeID, err := btcec.ParsePubKey(node[:])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &hopData{
|
|
|
|
data: blindedRouteHopData,
|
|
|
|
nodeID: nodeID,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getNodeChanPolicy fetches the routing policy info for the given channel and
|
|
|
|
// node pair.
|
|
|
|
func getNodeChannelPolicy(cfg *buildBlindedPathCfg, chanID uint64,
|
|
|
|
nodeID route.Vertex) (*blindedHopPolicy, error) {
|
|
|
|
|
|
|
|
// Attempt to fetch channel updates for the given channel. We will have
|
|
|
|
// at most two updates for a given channel.
|
|
|
|
_, update1, update2, err := cfg.fetchChannelEdgesByID(chanID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now we need to determine which of the updates was created by the
|
|
|
|
// node in question. We know the update is the correct one if the
|
|
|
|
// "ToNode" for the fetched policy is _not_ equal to the node ID in
|
|
|
|
// question.
|
|
|
|
var policy *models.ChannelEdgePolicy
|
|
|
|
switch {
|
|
|
|
case update1 != nil && !bytes.Equal(update1.ToNode[:], nodeID[:]):
|
|
|
|
policy = update1
|
|
|
|
|
|
|
|
case update2 != nil && !bytes.Equal(update2.ToNode[:], nodeID[:]):
|
|
|
|
policy = update2
|
|
|
|
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("no channel updates found from node "+
|
|
|
|
"%s for channel %d", nodeID, chanID)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &blindedHopPolicy{
|
|
|
|
cltvExpiryDelta: policy.TimeLockDelta,
|
|
|
|
feeRate: uint32(policy.FeeProportionalMillionths),
|
|
|
|
baseFee: policy.FeeBaseMSat,
|
|
|
|
minHTLCMsat: policy.MinHTLC,
|
|
|
|
maxHTLCMsat: policy.MaxHTLC,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// candidatePath holds all the information about a route to this node that we
|
|
|
|
// need in order to build a blinded route.
|
|
|
|
type candidatePath struct {
|
|
|
|
introNode route.Vertex
|
|
|
|
finalNodeID route.Vertex
|
|
|
|
hops []*blindedPathHop
|
|
|
|
}
|
|
|
|
|
|
|
|
// padWithDummyHops will append n dummy hops to the candidatePath hop set. The
|
|
|
|
// pub key for the dummy hop will be the same as the pub key for the final hop
|
|
|
|
// of the path. That way, the final hop will be able to decrypt the data
|
|
|
|
// encrypted for each dummy hop.
|
|
|
|
func (c *candidatePath) padWithDummyHops(n uint8) error {
|
|
|
|
for len(c.hops) < int(n) {
|
|
|
|
c.hops = append(c.hops, &blindedPathHop{
|
|
|
|
pubKey: c.finalNodeID,
|
|
|
|
isDummy: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// blindedPathHop holds the information we need to know about a hop in a route
|
|
|
|
// in order to use it in the construction of a blinded path.
|
|
|
|
type blindedPathHop struct {
|
|
|
|
// pubKey is the real pub key of a node on a blinded path.
|
|
|
|
pubKey route.Vertex
|
|
|
|
|
|
|
|
// channelID is the channel along which the previous hop should forward
|
|
|
|
// their HTLC in order to reach this hop.
|
|
|
|
channelID uint64
|
|
|
|
|
|
|
|
// isDummy is true if this hop is an appended dummy hop.
|
|
|
|
isDummy bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// extractCandidatePath extracts the data it needs from the given route.Route in
|
|
|
|
// order to construct a candidatePath.
|
|
|
|
func extractCandidatePath(path *route.Route) *candidatePath {
|
|
|
|
var (
|
|
|
|
hops = make([]*blindedPathHop, len(path.Hops))
|
|
|
|
finalNode = path.SourcePubKey
|
|
|
|
)
|
|
|
|
for i, hop := range path.Hops {
|
|
|
|
hops[i] = &blindedPathHop{
|
|
|
|
pubKey: hop.PubKeyBytes,
|
|
|
|
channelID: hop.ChannelID,
|
|
|
|
}
|
|
|
|
|
|
|
|
if i == len(path.Hops)-1 {
|
|
|
|
finalNode = hop.PubKeyBytes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &candidatePath{
|
|
|
|
introNode: path.SourcePubKey,
|
|
|
|
finalNodeID: finalNode,
|
|
|
|
hops: hops,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-04 11:11:35 +02:00
|
|
|
// blindedHopPolicy holds the set of relay policy values to use for a channel
|
|
|
|
// in a blinded path.
|
|
|
|
type blindedHopPolicy struct {
|
|
|
|
cltvExpiryDelta uint16
|
|
|
|
feeRate uint32
|
|
|
|
baseFee lnwire.MilliSatoshi
|
|
|
|
minHTLCMsat lnwire.MilliSatoshi
|
|
|
|
maxHTLCMsat lnwire.MilliSatoshi
|
|
|
|
}
|
|
|
|
|
|
|
|
// addPolicyBuffer constructs the bufferedChanPolicies for a path hop by taking
|
|
|
|
// its actual policy values and multiplying them by the given multipliers.
|
|
|
|
// The base fee, fee rate and minimum HTLC msat values are adjusted via the
|
|
|
|
// incMultiplier while the maximum HTLC msat value is adjusted via the
|
|
|
|
// decMultiplier. If adjustments of the HTLC values no longer make sense
|
|
|
|
// then the original HTLC value is used.
|
|
|
|
func addPolicyBuffer(policy *blindedHopPolicy, incMultiplier,
|
|
|
|
decMultiplier float64) (*blindedHopPolicy, error) {
|
|
|
|
|
|
|
|
if incMultiplier < 1 {
|
|
|
|
return nil, fmt.Errorf("blinded path policy increase " +
|
|
|
|
"multiplier must be greater than or equal to 1")
|
|
|
|
}
|
|
|
|
|
|
|
|
if decMultiplier < 0 || decMultiplier > 1 {
|
|
|
|
return nil, fmt.Errorf("blinded path policy decrease " +
|
|
|
|
"multiplier must be in the range [0;1]")
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
minHTLCMsat = lnwire.MilliSatoshi(
|
|
|
|
float64(policy.minHTLCMsat) * incMultiplier,
|
|
|
|
)
|
|
|
|
maxHTLCMsat = lnwire.MilliSatoshi(
|
|
|
|
float64(policy.maxHTLCMsat) * decMultiplier,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Make sure the new minimum is not more than the original maximum.
|
|
|
|
// If it is, then just stick to the original minimum.
|
|
|
|
if minHTLCMsat > policy.maxHTLCMsat {
|
|
|
|
minHTLCMsat = policy.minHTLCMsat
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure the new maximum is not less than the original minimum.
|
|
|
|
// If it is, then just stick to the original maximum.
|
|
|
|
if maxHTLCMsat < policy.minHTLCMsat {
|
|
|
|
maxHTLCMsat = policy.maxHTLCMsat
|
|
|
|
}
|
|
|
|
|
|
|
|
// Also ensure that the new htlc bounds make sense. If the new minimum
|
|
|
|
// is greater than the new maximum, then just let both to their original
|
|
|
|
// values.
|
|
|
|
if minHTLCMsat > maxHTLCMsat {
|
|
|
|
minHTLCMsat = policy.minHTLCMsat
|
|
|
|
maxHTLCMsat = policy.maxHTLCMsat
|
|
|
|
}
|
|
|
|
|
|
|
|
return &blindedHopPolicy{
|
|
|
|
cltvExpiryDelta: uint16(
|
|
|
|
float64(policy.cltvExpiryDelta) * incMultiplier,
|
|
|
|
),
|
|
|
|
feeRate: uint32(float64(policy.feeRate) * incMultiplier),
|
|
|
|
baseFee: lnwire.MilliSatoshi(
|
|
|
|
float64(policy.baseFee) * incMultiplier,
|
|
|
|
),
|
|
|
|
minHTLCMsat: minHTLCMsat,
|
|
|
|
maxHTLCMsat: maxHTLCMsat,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2024-05-04 10:43:03 +02:00
|
|
|
// calcBlindedPathPolicies computes the accumulated policy values for the path.
|
|
|
|
// These values include the total base fee, the total proportional fee and the
|
|
|
|
// total CLTV delta. This function assumes that all the passed relay infos have
|
|
|
|
// already been adjusted with a buffer to account for easy probing attacks.
|
|
|
|
func calcBlindedPathPolicies(relayInfo []*record.PaymentRelayInfo,
|
|
|
|
ourMinFinalCLTVDelta uint16) (lnwire.MilliSatoshi, uint32, uint16) {
|
|
|
|
|
|
|
|
var (
|
|
|
|
totalFeeBase lnwire.MilliSatoshi
|
|
|
|
totalFeeProp uint32
|
|
|
|
totalCLTV = ourMinFinalCLTVDelta
|
|
|
|
)
|
|
|
|
// Use the algorithms defined in BOLT 4 to calculate the accumulated
|
|
|
|
// relay fees for the route:
|
|
|
|
//nolint:lll
|
|
|
|
// https://github.com/lightning/bolts/blob/db278ab9b2baa0b30cfe79fb3de39280595938d3/04-onion-routing.md?plain=1#L255
|
|
|
|
for i := len(relayInfo) - 1; i >= 0; i-- {
|
|
|
|
info := relayInfo[i]
|
|
|
|
|
|
|
|
totalFeeBase = calcNextTotalBaseFee(
|
|
|
|
totalFeeBase, info.BaseFee, info.FeeRate,
|
|
|
|
)
|
|
|
|
|
|
|
|
totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
|
|
|
|
|
|
|
|
totalCLTV += info.CltvExpiryDelta
|
|
|
|
}
|
|
|
|
|
|
|
|
return totalFeeBase, totalFeeProp, totalCLTV
|
|
|
|
}
|
|
|
|
|
|
|
|
// calcNextTotalBaseFee takes the current total accumulated base fee of a
|
|
|
|
// blinded path at hop `n` along with the fee rate and base fee of the hop at
|
|
|
|
// `n-1` and uses these to calculate the accumulated base fee at hop `n-1`.
|
|
|
|
func calcNextTotalBaseFee(currentTotal, hopBaseFee lnwire.MilliSatoshi,
|
|
|
|
hopFeeRate uint32) lnwire.MilliSatoshi {
|
|
|
|
|
|
|
|
numerator := (uint32(hopBaseFee) * oneMillion) +
|
|
|
|
(uint32(currentTotal) * (oneMillion + hopFeeRate)) +
|
|
|
|
oneMillion - 1
|
|
|
|
|
|
|
|
return lnwire.MilliSatoshi(numerator / oneMillion)
|
|
|
|
}
|
|
|
|
|
|
|
|
// calculateNextTotalFeeRate takes the current total accumulated fee rate of a
|
|
|
|
// blinded path at hop `n` along with the fee rate of the hop at `n-1` and uses
|
|
|
|
// these to calculate the accumulated fee rate at hop `n-1`.
|
|
|
|
func calcNextTotalFeeRate(currentTotal, hopFeeRate uint32) uint32 {
|
|
|
|
numerator := (currentTotal+hopFeeRate)*oneMillion +
|
|
|
|
currentTotal*hopFeeRate + oneMillion - 1
|
|
|
|
|
|
|
|
return numerator / oneMillion
|
|
|
|
}
|
|
|
|
|
2024-05-04 10:37:41 +02:00
|
|
|
// hopData packages the record.BlindedRouteData for a hop on a blinded path with
|
|
|
|
// the real node ID of that hop.
|
|
|
|
type hopData struct {
|
|
|
|
data *record.BlindedRouteData
|
|
|
|
nodeID *btcec.PublicKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// padStats can be used to keep track of various pieces of data that we collect
|
|
|
|
// during a call to padHopInfo. This is useful for logging and for test
|
|
|
|
// assertions.
|
|
|
|
type padStats struct {
|
|
|
|
minPayloadSize int
|
|
|
|
maxPayloadSize int
|
|
|
|
finalPaddedSize int
|
|
|
|
numIterations int
|
|
|
|
}
|
|
|
|
|
|
|
|
// padHopInfo iterates over a set of record.BlindedRouteData and adds padding
|
|
|
|
// where needed until the resulting encrypted data blobs are all the same size.
|
|
|
|
// This may take a few iterations due to the fact that a TLV field is used to
|
|
|
|
// add this padding. For example, if we want to add a 1 byte padding to a
|
|
|
|
// record.BlindedRouteData when it does not yet have any padding, then adding
|
|
|
|
// a 1 byte padding will actually add 3 bytes due to the bytes required when
|
|
|
|
// adding the initial type and length bytes. However, on the next iteration if
|
|
|
|
// we again add just 1 byte, then only a single byte will be added. The same
|
|
|
|
// iteration is required for padding values on the BigSize encoding bucket
|
|
|
|
// edges. The number of iterations that this function takes is also returned for
|
|
|
|
// testing purposes. If prePad is true, then zero byte padding is added to each
|
|
|
|
// payload that does not yet have padding. This will save some iterations for
|
|
|
|
// the majority of cases.
|
|
|
|
func padHopInfo(hopInfo []*hopData, prePad bool) ([]*sphinx.HopInfo, *padStats,
|
|
|
|
error) {
|
|
|
|
|
|
|
|
var (
|
|
|
|
paymentPath = make([]*sphinx.HopInfo, len(hopInfo))
|
|
|
|
stats padStats
|
|
|
|
)
|
|
|
|
|
|
|
|
// Pre-pad each payload with zero byte padding (if it does not yet have
|
|
|
|
// padding) to save a couple of iterations in the majority of cases.
|
|
|
|
if prePad {
|
|
|
|
for _, info := range hopInfo {
|
|
|
|
if info.data.Padding.IsSome() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
info.data.PadBy(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
stats.numIterations++
|
|
|
|
|
|
|
|
// On each iteration of the loop, we first determine the
|
|
|
|
// current largest encoded data blob size. This will be the
|
|
|
|
// size we aim to get the others to match.
|
|
|
|
var (
|
|
|
|
maxLen int
|
|
|
|
minLen = math.MaxInt8
|
|
|
|
)
|
|
|
|
for i, hop := range hopInfo {
|
|
|
|
plainText, err := record.EncodeBlindedRouteData(
|
|
|
|
hop.data,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(plainText) > maxLen {
|
|
|
|
maxLen = len(plainText)
|
|
|
|
|
|
|
|
// Update the stats to take note of this new
|
|
|
|
// max since this may be the final max that all
|
|
|
|
// payloads will be padded to.
|
|
|
|
stats.finalPaddedSize = maxLen
|
|
|
|
}
|
|
|
|
if len(plainText) < minLen {
|
|
|
|
minLen = len(plainText)
|
|
|
|
}
|
|
|
|
|
|
|
|
paymentPath[i] = &sphinx.HopInfo{
|
|
|
|
NodePub: hop.nodeID,
|
|
|
|
PlainText: plainText,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If this is our first iteration, then we take note of the min
|
|
|
|
// and max lengths of the payloads pre-padding for logging
|
|
|
|
// later.
|
|
|
|
if stats.numIterations == 1 {
|
|
|
|
stats.minPayloadSize = minLen
|
|
|
|
stats.maxPayloadSize = maxLen
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now we iterate over them again and determine which ones we
|
|
|
|
// need to add padding to.
|
|
|
|
var numEqual int
|
|
|
|
for i, hop := range hopInfo {
|
|
|
|
plainText := paymentPath[i].PlainText
|
|
|
|
|
|
|
|
// If the plaintext length is equal to the desired
|
|
|
|
// length, then we can continue. We use numEqual to
|
|
|
|
// keep track of how many have the same length.
|
|
|
|
if len(plainText) == maxLen {
|
|
|
|
numEqual++
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we previously added padding to this hop, we keep
|
|
|
|
// the length of that initial padding too.
|
|
|
|
var existingPadding int
|
|
|
|
hop.data.Padding.WhenSome(
|
|
|
|
func(p tlv.RecordT[tlv.TlvType1, []byte]) {
|
|
|
|
existingPadding = len(p.Val)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
// Add some padding bytes to the hop.
|
|
|
|
hop.data.PadBy(
|
|
|
|
existingPadding + maxLen - len(plainText),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If all the payloads have the same length, we can exit the
|
|
|
|
// loop.
|
|
|
|
if numEqual == len(hopInfo) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Finished padding %d blinded path payloads to %d bytes "+
|
|
|
|
"each where the pre-padded min and max sizes were %d and %d "+
|
|
|
|
"bytes respectively", len(hopInfo), stats.finalPaddedSize,
|
|
|
|
stats.minPayloadSize, stats.maxPayloadSize)
|
|
|
|
|
|
|
|
return paymentPath, &stats, nil
|
|
|
|
}
|