lnd.xprv/lnrpc/invoicesrpc/addinvoice.go

419 lines
13 KiB
Go

package invoicesrpc
import (
"bytes"
"context"
"crypto/rand"
"errors"
"fmt"
"math"
"time"
"github.com/btcsuite/btcd/chaincfg"
"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/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
// MaxPaymentMSat is the maximum allowed payment.
MaxPaymentMSat lnwire.MilliSatoshi
// 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
}
// 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
// Deprecated. An optional cryptographic receipt of payment which is not
// implemented.
Receipt []byte
// 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 satoshis.
Value btcutil.Amount
// 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
}
// 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")
// Prevent the unknown preimage magic value from being used for a
// regular invoice. This would cause the invoice the be handled as if it
// was a hold invoice.
case invoice.Preimage != nil &&
*invoice.Preimage == channeldb.UnknownPreimage:
return nil, nil,
fmt.Errorf("cannot use all zeroes as a preimage")
// Prevent the hash of the unknown preimage magic value to be used for a
// hold invoice. This would make it impossible to settle the invoice,
// because it would still be interpreted as not having a preimage.
case invoice.Hash != nil &&
*invoice.Hash == channeldb.UnknownPreimage.Hash():
return nil, nil,
fmt.Errorf("cannot use hash of all zeroes preimage")
// If no hash or preimage is given, generate a random preimage.
case invoice.Preimage == nil && invoice.Hash == nil:
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:
paymentPreimage = channeldb.UnknownPreimage
paymentHash = *invoice.Hash
// A specific preimage was supplied. Use that for the invoice.
case invoice.Preimage != nil && invoice.Hash == nil:
paymentPreimage = *invoice.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.Receipt) > channeldb.MaxReceiptSize {
return nil, nil, fmt.Errorf("receipt too large: %v bytes "+
"(maxsize=%v)", len(invoice.Receipt), channeldb.MaxReceiptSize)
}
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))
}
// The value of the invoice must not be negative.
if invoice.Value < 0 {
return nil, nil, fmt.Errorf("payments of negative value "+
"are not allowed, value is %v", invoice.Value)
}
amtMSat := lnwire.NewMSatFromSatoshis(invoice.Value)
// The value of the invoice must also not exceed the current soft-limit
// on the largest payment within the network.
if amtMSat > cfg.MaxPaymentMSat {
return nil, nil, fmt.Errorf("payment of %v is too large, max "+
"payment allowed is %v", invoice.Value,
cfg.MaxPaymentMSat.ToSatoshis(),
)
}
// 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:
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")
}
graph := cfg.ChanDB.ChannelGraph()
numHints := 0
for _, channel := range openChannels {
// We'll restrict the number of individual route hints
// to 20 to avoid creating overly large invoices.
if numHints >= 20 {
break
}
// Since we're only interested in our private channels,
// we'll skip public ones.
isPublic := channel.ChannelFlags&lnwire.FFAnnounceChannel != 0
if isPublic {
continue
}
// Make sure the counterparty has enough balance in the
// channel for our amount. We do this in order to reduce
// payment errors when attempting to use this channel
// as a hint.
chanPoint := lnwire.NewChanIDFromOutPoint(
&channel.FundingOutpoint,
)
if amtMSat >= channel.LocalCommitment.RemoteBalance {
log.Debugf("Skipping channel %v due to "+
"not having enough remote balance",
chanPoint)
continue
}
// Make sure the channel is active.
if !cfg.IsChannelActive(chanPoint) {
log.Debugf("Skipping channel %v due to not "+
"being eligible to forward payments",
chanPoint)
continue
}
// 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)
continue
}
if !isRemoteNodePublic {
log.Debugf("Skipping channel %v due to "+
"counterparty %x being unadvertised",
chanPoint, remotePub)
continue
}
// 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)
continue
}
// 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
}
// If for some reason we don't yet have the edge for
// the remote party, then we'll just skip adding this
// channel as a routing hint.
if remotePolicy == nil {
continue
}
// Finally, create the routing hint for this channel and
// add it to our list of route hints.
hint := zpay32.HopHint{
NodeID: channel.IdentityPub,
ChannelID: chanID,
FeeBaseMSat: uint32(remotePolicy.FeeBaseMSat),
FeeProportionalMillionths: uint32(
remotePolicy.FeeProportionalMillionths,
),
CLTVExpiryDelta: remotePolicy.TimeLockDelta,
}
// Include the route hint in our set of options that
// will be used when creating the invoice.
routeHint := []zpay32.HopHint{hint}
options = append(options, zpay32.RouteHint(routeHint))
numHints++
}
}
// 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),
Receipt: invoice.Receipt,
PaymentRequest: []byte(payReqString),
FinalCltvDelta: int32(payReq.MinFinalCLTVExpiry()),
Expiry: payReq.Expiry(),
Terms: channeldb.ContractTerm{
Value: amtMSat,
PaymentPreimage: paymentPreimage,
},
}
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
}