mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
f6d6d6609f
In this commit, we update the hop hint selection to account for the fact that with MPP, a single payment may consume multiple channels. As is, if a user only has two 0.5 BTC channels, and tries to make a 1 BTC channel, then the current logic won't include any hop hints. To solve this, we first add all the channels which in isolation can carry the payment in question. We then do another pass that accumulates channels until either we reach our hop-hint limit, or the total bandwidth that we've accumulate is greater than 2x the payment amount.
506 lines
16 KiB
Go
506 lines
16 KiB
Go
package invoicesrpc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/davecgh/go-spew/spew"
|
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/netann"
|
|
"github.com/lightningnetwork/lnd/routing"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
)
|
|
|
|
// AddInvoiceConfig contains dependencies for invoice creation.
|
|
type AddInvoiceConfig struct {
|
|
// AddInvoice is called to add the invoice to the registry.
|
|
AddInvoice func(invoice *channeldb.Invoice, paymentHash lntypes.Hash) (
|
|
uint64, error)
|
|
|
|
// 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.
|
|
ChanDB *channeldb.DB
|
|
|
|
// GenInvoiceFeatures returns a feature containing feature bits that
|
|
// should be advertised on freshly generated invoices.
|
|
GenInvoiceFeatures func() *lnwire.FeatureVector
|
|
}
|
|
|
|
// 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
|
|
// this preimage. If Preimage is set, Hash should be nil. If both
|
|
// Preimage and Hash are nil, a random preimage is generated.
|
|
Preimage *lntypes.Preimage
|
|
|
|
// 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
|
|
|
|
// The value of this invoice in millisatoshis.
|
|
Value lnwire.MilliSatoshi
|
|
|
|
// 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
|
|
|
|
// HodlInvoice signals that this invoice shouldn't be settled
|
|
// immediately upon receiving the payment.
|
|
HodlInvoice bool
|
|
}
|
|
|
|
// 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,
|
|
invoice *AddInvoiceData) (*lntypes.Hash, *channeldb.Invoice, error) {
|
|
|
|
var (
|
|
paymentPreimage *lntypes.Preimage
|
|
paymentHash lntypes.Hash
|
|
)
|
|
|
|
switch {
|
|
|
|
// Only either preimage or hash can be set.
|
|
case invoice.Preimage != nil && invoice.Hash != nil:
|
|
return nil, nil,
|
|
errors.New("preimage and hash both set")
|
|
|
|
// If no hash or preimage is given, generate a random preimage.
|
|
case invoice.Preimage == nil && invoice.Hash == nil:
|
|
paymentPreimage = &lntypes.Preimage{}
|
|
if _, err := rand.Read(paymentPreimage[:]); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
paymentHash = paymentPreimage.Hash()
|
|
|
|
// If just a hash is given, we create a hold invoice by setting the
|
|
// preimage to unknown.
|
|
case invoice.Preimage == nil && invoice.Hash != nil:
|
|
paymentHash = *invoice.Hash
|
|
|
|
// A specific preimage was supplied. Use that for the invoice.
|
|
case invoice.Preimage != nil && invoice.Hash == nil:
|
|
preimage := *invoice.Preimage
|
|
paymentPreimage = &preimage
|
|
paymentHash = invoice.Preimage.Hash()
|
|
}
|
|
|
|
// The size of the memo, receipt and description hash attached must not
|
|
// exceed the maximum values for either of the fields.
|
|
if len(invoice.Memo) > channeldb.MaxMemoSize {
|
|
return nil, nil, fmt.Errorf("memo too large: %v bytes "+
|
|
"(maxsize=%v)", len(invoice.Memo), channeldb.MaxMemoSize)
|
|
}
|
|
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))
|
|
}
|
|
|
|
// 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 {
|
|
// The value of the invoice must not be negative.
|
|
case int64(invoice.Value) < 0:
|
|
return nil, nil, fmt.Errorf("payments of negative value "+
|
|
"are not allowed, value is %v", int64(invoice.Value))
|
|
|
|
// 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)
|
|
}
|
|
|
|
amtMSat := invoice.Value
|
|
|
|
// 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
|
|
// allow the payee to specify the amount of satoshis they wish to send.
|
|
if amtMSat > 0 {
|
|
options = append(options, zpay32.Amount(amtMSat))
|
|
}
|
|
|
|
// If specified, add a fallback address to the payment request.
|
|
if len(invoice.FallbackAddr) > 0 {
|
|
addr, err := btcutil.DecodeAddress(invoice.FallbackAddr,
|
|
cfg.ChainParams)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid fallback address: %v",
|
|
err)
|
|
}
|
|
options = append(options, zpay32.FallbackAddr(addr))
|
|
}
|
|
|
|
// 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.
|
|
if invoice.Expiry > 0 {
|
|
|
|
// 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() {
|
|
return nil, nil, fmt.Errorf("expiry of %v seconds "+
|
|
"greater than max expiry of %v seconds",
|
|
float64(expSeconds), maxExpiry.Seconds())
|
|
}
|
|
|
|
expiry := time.Duration(invoice.Expiry) * time.Second
|
|
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.
|
|
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 {
|
|
case invoice.CltvExpiry > math.MaxUint16:
|
|
return nil, nil, fmt.Errorf("CLTV delta of %v is too large, max "+
|
|
"accepted is: %v", invoice.CltvExpiry, math.MaxUint16)
|
|
case invoice.CltvExpiry != 0:
|
|
// 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",
|
|
routing.MinCLTVDelta, invoice.CltvExpiry)
|
|
}
|
|
|
|
options = append(options,
|
|
zpay32.CLTVExpiry(invoice.CltvExpiry))
|
|
default:
|
|
// TODO(roasbeef): assumes set delta between versions
|
|
defaultDelta := cfg.DefaultCLTVExpiry
|
|
options = append(options, zpay32.CLTVExpiry(uint64(defaultDelta)))
|
|
}
|
|
|
|
// If we were requested to include routing hints in the invoice, then
|
|
// we'll fetch all of our available private channels and create routing
|
|
// hints for them.
|
|
if invoice.Private {
|
|
openChannels, err := cfg.ChanDB.FetchAllChannels()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not fetch all channels")
|
|
}
|
|
|
|
// We'll restrict the number of individual route hints
|
|
// to 20 to avoid creating overly large invoices.
|
|
const numMaxHophints = 20
|
|
hopHints := selectHopHints(
|
|
amtMSat, cfg, openChannels, numMaxHophints,
|
|
)
|
|
|
|
options = append(options, hopHints...)
|
|
}
|
|
|
|
// Set our desired invoice features and add them to our list of options.
|
|
invoiceFeatures := cfg.GenInvoiceFeatures()
|
|
options = append(options, zpay32.Features(invoiceFeatures))
|
|
|
|
// 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))
|
|
|
|
// Create and encode the payment request as a bech32 (zpay32) string.
|
|
creationDate := time.Now()
|
|
payReq, err := zpay32.NewInvoice(
|
|
cfg.ChainParams, paymentHash, creationDate, options...,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
payReqString, err := payReq.Encode(
|
|
zpay32.MessageSigner{
|
|
SignCompact: cfg.NodeSigner.SignDigestCompact,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
newInvoice := &channeldb.Invoice{
|
|
CreationDate: creationDate,
|
|
Memo: []byte(invoice.Memo),
|
|
PaymentRequest: []byte(payReqString),
|
|
Terms: channeldb.ContractTerm{
|
|
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
|
|
Expiry: payReq.Expiry(),
|
|
Value: amtMSat,
|
|
PaymentPreimage: paymentPreimage,
|
|
PaymentAddr: paymentAddr,
|
|
Features: invoiceFeatures,
|
|
},
|
|
HodlInvoice: invoice.HodlInvoice,
|
|
}
|
|
|
|
log.Tracef("[addinvoice] adding new invoice %v",
|
|
newLogClosure(func() string {
|
|
return spew.Sdump(newInvoice)
|
|
}),
|
|
)
|
|
|
|
// With all sanity checks passed, write the invoice to the database.
|
|
_, err = cfg.AddInvoice(newInvoice, paymentHash)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return &paymentHash, newInvoice, nil
|
|
}
|
|
|
|
// chanCanBeHopHint returns true if the target channel is eligible to be a hop
|
|
// hint.
|
|
func chanCanBeHopHint(channel *channeldb.OpenChannel,
|
|
graph *channeldb.ChannelGraph,
|
|
cfg *AddInvoiceConfig) (*channeldb.ChannelEdgePolicy, bool) {
|
|
|
|
// Since we're only interested in our private channels, we'll skip
|
|
// public ones.
|
|
isPublic := channel.ChannelFlags&lnwire.FFAnnounceChannel != 0
|
|
if isPublic {
|
|
return nil, false
|
|
}
|
|
|
|
// Make sure the channel is active.
|
|
chanPoint := lnwire.NewChanIDFromOutPoint(
|
|
&channel.FundingOutpoint,
|
|
)
|
|
if !cfg.IsChannelActive(chanPoint) {
|
|
log.Debugf("Skipping channel %v due to not "+
|
|
"being eligible to forward payments",
|
|
chanPoint)
|
|
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
|
|
copy(remotePub[:], channel.IdentityPub.SerializeCompressed())
|
|
isRemoteNodePublic, err := graph.IsPublicNode(remotePub)
|
|
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",
|
|
chanPoint, remotePub)
|
|
return nil, false
|
|
}
|
|
|
|
// Fetch the policies for each end of the channel.
|
|
chanID := channel.ShortChanID().ToUint64()
|
|
info, p1, p2, err := graph.FetchChannelEdgesByID(chanID)
|
|
if err != nil {
|
|
log.Errorf("Unable to fetch the routing "+
|
|
"policies for the edges of the channel "+
|
|
"%v: %v", chanPoint, err)
|
|
return nil, false
|
|
}
|
|
|
|
// Now, we'll need to determine which is the correct policy for HTLCs
|
|
// being sent from the remote node.
|
|
var remotePolicy *channeldb.ChannelEdgePolicy
|
|
if bytes.Equal(remotePub[:], info.NodeKey1Bytes[:]) {
|
|
remotePolicy = p1
|
|
} else {
|
|
remotePolicy = p2
|
|
}
|
|
|
|
return remotePolicy, true
|
|
}
|
|
|
|
// addHopHint creates a hop hint out of the passed channel and channel policy.
|
|
// The new hop hint is appended to the passed slice.
|
|
func addHopHint(hopHints *[]func(*zpay32.Invoice),
|
|
channel *channeldb.OpenChannel, chanPolicy *channeldb.ChannelEdgePolicy) {
|
|
|
|
hopHint := zpay32.HopHint{
|
|
NodeID: channel.IdentityPub,
|
|
ChannelID: channel.ShortChanID().ToUint64(),
|
|
FeeBaseMSat: uint32(chanPolicy.FeeBaseMSat),
|
|
FeeProportionalMillionths: uint32(
|
|
chanPolicy.FeeProportionalMillionths,
|
|
),
|
|
CLTVExpiryDelta: chanPolicy.TimeLockDelta,
|
|
}
|
|
*hopHints = append(
|
|
*hopHints, zpay32.RouteHint([]zpay32.HopHint{hopHint}),
|
|
)
|
|
}
|
|
|
|
// selectHopHints will select up to numMaxHophints from the set of passed 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 selectHopHints(amtMSat lnwire.MilliSatoshi, cfg *AddInvoiceConfig,
|
|
openChannels []*channeldb.OpenChannel,
|
|
numMaxHophints int) []func(*zpay32.Invoice) {
|
|
|
|
graph := cfg.ChanDB.ChannelGraph()
|
|
|
|
// We'll add our hop hints in two passes, first we'll add all channels
|
|
// that are eligible to be hop hints, and also have a local balance
|
|
// above the payment amount.
|
|
var totalHintBandwidth lnwire.MilliSatoshi
|
|
hopHintChans := make(map[wire.OutPoint]struct{})
|
|
hopHints := make([]func(*zpay32.Invoice), 0, numMaxHophints)
|
|
for _, channel := range openChannels {
|
|
// If this channel can't be a hop hint, then skip it.
|
|
edgePolicy, canBeHopHint := chanCanBeHopHint(
|
|
channel, graph, cfg,
|
|
)
|
|
if edgePolicy == nil || !canBeHopHint {
|
|
continue
|
|
}
|
|
|
|
// Similarly, in this first pass, we'll ignore all channels in
|
|
// isolation can't satisfy this payment.
|
|
if channel.LocalCommitment.RemoteBalance < amtMSat {
|
|
continue
|
|
}
|
|
|
|
// Now that we now this channel use usable, add it as a hop
|
|
// hint and the indexes we'll use later.
|
|
addHopHint(&hopHints, channel, edgePolicy)
|
|
|
|
hopHintChans[channel.FundingOutpoint] = struct{}{}
|
|
totalHintBandwidth += channel.LocalCommitment.RemoteBalance
|
|
}
|
|
|
|
// If we have enough hop hints at this point, then we'll exit early.
|
|
// Otherwise, we'll continue to add more that may help out mpp users.
|
|
if len(hopHints) >= numMaxHophints {
|
|
return hopHints
|
|
}
|
|
|
|
// In this second pass we'll add channels, and we'll either stop when
|
|
// we have 20 hop hints, we've run through all the available channels,
|
|
// or if the sum of available bandwidth in the routing hints exceeds 2x
|
|
// the payment amount. We do 2x here to account for a margin of error
|
|
// if some of the selected channels no longer become operable.
|
|
hopHintFactor := lnwire.MilliSatoshi(2)
|
|
for i := 0; i < len(openChannels); i++ {
|
|
// If we hit either of our early termination conditions, then
|
|
// we'll break the loop here.
|
|
if totalHintBandwidth > amtMSat*hopHintFactor ||
|
|
len(hopHints) >= numMaxHophints {
|
|
|
|
break
|
|
}
|
|
|
|
channel := openChannels[i]
|
|
|
|
// Skip the channel if we already selected it.
|
|
if _, ok := hopHintChans[channel.FundingOutpoint]; ok {
|
|
continue
|
|
}
|
|
|
|
// If the channel can't be a hop hint, then we'll skip it.
|
|
// Otherwise, we'll use the policy information to populate the
|
|
// hop hint.
|
|
remotePolicy, canBeHopHint := chanCanBeHopHint(
|
|
channel, graph, cfg,
|
|
)
|
|
if !canBeHopHint || remotePolicy == nil {
|
|
continue
|
|
}
|
|
|
|
// Include the route hint in our set of options that will be
|
|
// used when creating the invoice.
|
|
addHopHint(&hopHints, channel, remotePolicy)
|
|
|
|
// As we've just added a new hop hint, we'll accumulate it's
|
|
// available balance now to update our tally.
|
|
//
|
|
// TODO(roasbeef): have a cut off based on min bandwidth?
|
|
totalHintBandwidth += channel.LocalCommitment.RemoteBalance
|
|
}
|
|
|
|
return hopHints
|
|
}
|