mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
Merge pull request #2022 from joostjager/holdinvoice
htlcswitch: hodl invoice
This commit is contained in:
commit
aa1cd04dbf
@ -2,7 +2,6 @@ package channeldb
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
@ -67,17 +66,18 @@ func TestInvoiceWorkflow(t *testing.T) {
|
||||
copy(fakeInvoice.Terms.PaymentPreimage[:], rev[:])
|
||||
fakeInvoice.Terms.Value = lnwire.NewMSatFromSatoshis(10000)
|
||||
|
||||
paymentHash := fakeInvoice.Terms.PaymentPreimage.Hash()
|
||||
|
||||
// Add the invoice to the database, this should succeed as there aren't
|
||||
// any existing invoices within the database with the same payment
|
||||
// hash.
|
||||
if _, err := db.AddInvoice(fakeInvoice); err != nil {
|
||||
if _, err := db.AddInvoice(fakeInvoice, paymentHash); err != nil {
|
||||
t.Fatalf("unable to find invoice: %v", err)
|
||||
}
|
||||
|
||||
// Attempt to retrieve the invoice which was just added to the
|
||||
// database. It should be found, and the invoice returned should be
|
||||
// identical to the one created above.
|
||||
paymentHash := sha256.Sum256(fakeInvoice.Terms.PaymentPreimage[:])
|
||||
dbInvoice, err := db.LookupInvoice(paymentHash)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find invoice: %v", err)
|
||||
@ -99,7 +99,7 @@ func TestInvoiceWorkflow(t *testing.T) {
|
||||
// now have the settled bit toggle to true and a non-default
|
||||
// SettledDate
|
||||
payAmt := fakeInvoice.Terms.Value * 2
|
||||
if _, err := db.SettleInvoice(paymentHash, payAmt); err != nil {
|
||||
if _, err := db.AcceptOrSettleInvoice(paymentHash, payAmt); err != nil {
|
||||
t.Fatalf("unable to settle invoice: %v", err)
|
||||
}
|
||||
dbInvoice2, err := db.LookupInvoice(paymentHash)
|
||||
@ -126,7 +126,7 @@ func TestInvoiceWorkflow(t *testing.T) {
|
||||
|
||||
// Attempt to insert generated above again, this should fail as
|
||||
// duplicates are rejected by the processing logic.
|
||||
if _, err := db.AddInvoice(fakeInvoice); err != ErrDuplicateInvoice {
|
||||
if _, err := db.AddInvoice(fakeInvoice, paymentHash); err != ErrDuplicateInvoice {
|
||||
t.Fatalf("invoice insertion should fail due to duplication, "+
|
||||
"instead %v", err)
|
||||
}
|
||||
@ -149,7 +149,8 @@ func TestInvoiceWorkflow(t *testing.T) {
|
||||
t.Fatalf("unable to create invoice: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.AddInvoice(invoice); err != nil {
|
||||
hash := invoice.Terms.PaymentPreimage.Hash()
|
||||
if _, err := db.AddInvoice(invoice, hash); err != nil {
|
||||
t.Fatalf("unable to add invoice %v", err)
|
||||
}
|
||||
|
||||
@ -198,7 +199,9 @@ func TestInvoiceAddTimeSeries(t *testing.T) {
|
||||
t.Fatalf("unable to create invoice: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.AddInvoice(invoice); err != nil {
|
||||
paymentHash := invoice.Terms.PaymentPreimage.Hash()
|
||||
|
||||
if _, err := db.AddInvoice(invoice, paymentHash); err != nil {
|
||||
t.Fatalf("unable to add invoice %v", err)
|
||||
}
|
||||
|
||||
@ -256,11 +259,9 @@ func TestInvoiceAddTimeSeries(t *testing.T) {
|
||||
for i := 10; i < len(invoices); i++ {
|
||||
invoice := &invoices[i]
|
||||
|
||||
paymentHash := sha256.Sum256(
|
||||
invoice.Terms.PaymentPreimage[:],
|
||||
)
|
||||
paymentHash := invoice.Terms.PaymentPreimage.Hash()
|
||||
|
||||
_, err := db.SettleInvoice(paymentHash, 0)
|
||||
_, err := db.AcceptOrSettleInvoice(paymentHash, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to settle invoice: %v", err)
|
||||
}
|
||||
@ -334,13 +335,14 @@ func TestDuplicateSettleInvoice(t *testing.T) {
|
||||
t.Fatalf("unable to create invoice: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.AddInvoice(invoice); err != nil {
|
||||
payHash := invoice.Terms.PaymentPreimage.Hash()
|
||||
|
||||
if _, err := db.AddInvoice(invoice, payHash); err != nil {
|
||||
t.Fatalf("unable to add invoice %v", err)
|
||||
}
|
||||
|
||||
// With the invoice in the DB, we'll now attempt to settle the invoice.
|
||||
payHash := sha256.Sum256(invoice.Terms.PaymentPreimage[:])
|
||||
dbInvoice, err := db.SettleInvoice(payHash, amt)
|
||||
dbInvoice, err := db.AcceptOrSettleInvoice(payHash, amt)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to settle invoice: %v", err)
|
||||
}
|
||||
@ -360,7 +362,7 @@ func TestDuplicateSettleInvoice(t *testing.T) {
|
||||
|
||||
// If we try to settle the invoice again, then we should get the very
|
||||
// same invoice back, but with an error this time.
|
||||
dbInvoice, err = db.SettleInvoice(payHash, amt)
|
||||
dbInvoice, err = db.AcceptOrSettleInvoice(payHash, amt)
|
||||
if err != ErrInvoiceAlreadySettled {
|
||||
t.Fatalf("expected ErrInvoiceAlreadySettled")
|
||||
}
|
||||
@ -397,14 +399,15 @@ func TestQueryInvoices(t *testing.T) {
|
||||
t.Fatalf("unable to create invoice: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.AddInvoice(invoice); err != nil {
|
||||
paymentHash := invoice.Terms.PaymentPreimage.Hash()
|
||||
|
||||
if _, err := db.AddInvoice(invoice, paymentHash); err != nil {
|
||||
t.Fatalf("unable to add invoice: %v", err)
|
||||
}
|
||||
|
||||
// We'll only settle half of all invoices created.
|
||||
if i%2 == 0 {
|
||||
paymentHash := sha256.Sum256(invoice.Terms.PaymentPreimage[:])
|
||||
if _, err := db.SettleInvoice(paymentHash, i); err != nil {
|
||||
if _, err := db.AcceptOrSettleInvoice(paymentHash, i); err != nil {
|
||||
t.Fatalf("unable to settle invoice: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package channeldb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -16,6 +15,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// UnknownPreimage is an all-zeroes preimage that indicates that the
|
||||
// preimage for this invoice is not yet known.
|
||||
UnknownPreimage lntypes.Preimage
|
||||
|
||||
// invoiceBucket is the name of the bucket within the database that
|
||||
// stores all data related to invoices no matter their final state.
|
||||
// Within the invoice bucket, each invoice is keyed by its invoice ID
|
||||
@ -66,6 +69,13 @@ var (
|
||||
// ErrInvoiceAlreadyCanceled is returned when the invoice is already
|
||||
// canceled.
|
||||
ErrInvoiceAlreadyCanceled = errors.New("invoice already canceled")
|
||||
|
||||
// ErrInvoiceAlreadyAccepted is returned when the invoice is already
|
||||
// accepted.
|
||||
ErrInvoiceAlreadyAccepted = errors.New("invoice already accepted")
|
||||
|
||||
// ErrInvoiceStillOpen is returned when the invoice is still open.
|
||||
ErrInvoiceStillOpen = errors.New("invoice still open")
|
||||
)
|
||||
|
||||
const (
|
||||
@ -97,6 +107,10 @@ const (
|
||||
|
||||
// ContractCanceled means the invoice has been canceled.
|
||||
ContractCanceled ContractState = 2
|
||||
|
||||
// ContractAccepted means the HTLC has been accepted but not settled
|
||||
// yet.
|
||||
ContractAccepted ContractState = 3
|
||||
)
|
||||
|
||||
// String returns a human readable identifier for the ContractState type.
|
||||
@ -108,6 +122,8 @@ func (c ContractState) String() string {
|
||||
return "Settled"
|
||||
case ContractCanceled:
|
||||
return "Canceled"
|
||||
case ContractAccepted:
|
||||
return "Accepted"
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
@ -213,11 +229,14 @@ func validateInvoice(i *Invoice) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddInvoice inserts the targeted invoice into the database. If the invoice
|
||||
// has *any* payment hashes which already exists within the database, then the
|
||||
// AddInvoice inserts the targeted invoice into the database. If the invoice has
|
||||
// *any* payment hashes which already exists within the database, then the
|
||||
// insertion will be aborted and rejected due to the strict policy banning any
|
||||
// duplicate payment hashes.
|
||||
func (d *DB) AddInvoice(newInvoice *Invoice) (uint64, error) {
|
||||
// duplicate payment hashes. A side effect of this function is that it sets
|
||||
// AddIndex on newInvoice.
|
||||
func (d *DB) AddInvoice(newInvoice *Invoice, paymentHash lntypes.Hash) (
|
||||
uint64, error) {
|
||||
|
||||
if err := validateInvoice(newInvoice); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -244,9 +263,6 @@ func (d *DB) AddInvoice(newInvoice *Invoice) (uint64, error) {
|
||||
|
||||
// Ensure that an invoice an identical payment hash doesn't
|
||||
// already exist within the index.
|
||||
paymentHash := sha256.Sum256(
|
||||
newInvoice.Terms.PaymentPreimage[:],
|
||||
)
|
||||
if invoiceIndex.Get(paymentHash[:]) != nil {
|
||||
return ErrDuplicateInvoice
|
||||
}
|
||||
@ -268,6 +284,7 @@ func (d *DB) AddInvoice(newInvoice *Invoice) (uint64, error) {
|
||||
|
||||
newIndex, err := putInvoice(
|
||||
invoices, invoiceIndex, addIndex, newInvoice, invoiceNum,
|
||||
paymentHash,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -607,11 +624,14 @@ func (d *DB) QueryInvoices(q InvoiceQuery) (InvoiceSlice, error) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SettleInvoice attempts to mark an invoice corresponding to the passed
|
||||
// payment hash as fully settled. If an invoice matching the passed payment
|
||||
// hash doesn't existing within the database, then the action will fail with a
|
||||
// "not found" error.
|
||||
func (d *DB) SettleInvoice(paymentHash [32]byte,
|
||||
// AcceptOrSettleInvoice attempts to mark an invoice corresponding to the passed
|
||||
// payment hash as settled. If an invoice matching the passed payment hash
|
||||
// doesn't existing within the database, then the action will fail with a "not
|
||||
// found" error.
|
||||
//
|
||||
// When the preimage for the invoice is unknown (hold invoice), the invoice is
|
||||
// marked as accepted.
|
||||
func (d *DB) AcceptOrSettleInvoice(paymentHash [32]byte,
|
||||
amtPaid lnwire.MilliSatoshi) (*Invoice, error) {
|
||||
|
||||
var settledInvoice *Invoice
|
||||
@ -640,7 +660,7 @@ func (d *DB) SettleInvoice(paymentHash [32]byte,
|
||||
return ErrInvoiceNotFound
|
||||
}
|
||||
|
||||
settledInvoice, err = settleInvoice(
|
||||
settledInvoice, err = acceptOrSettleInvoice(
|
||||
invoices, settleIndex, invoiceNum, amtPaid,
|
||||
)
|
||||
|
||||
@ -650,6 +670,46 @@ func (d *DB) SettleInvoice(paymentHash [32]byte,
|
||||
return settledInvoice, err
|
||||
}
|
||||
|
||||
// SettleHoldInvoice sets the preimage of a hodl invoice and marks the invoice
|
||||
// as settled.
|
||||
func (d *DB) SettleHoldInvoice(preimage lntypes.Preimage) (*Invoice, error) {
|
||||
var updatedInvoice *Invoice
|
||||
hash := preimage.Hash()
|
||||
err := d.Update(func(tx *bbolt.Tx) error {
|
||||
invoices, err := tx.CreateBucketIfNotExists(invoiceBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoiceIndex, err := invoices.CreateBucketIfNotExists(
|
||||
invoiceIndexBucket,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settleIndex, err := invoices.CreateBucketIfNotExists(
|
||||
settleIndexBucket,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check the invoice index to see if an invoice paying to this
|
||||
// hash exists within the DB.
|
||||
invoiceNum := invoiceIndex.Get(hash[:])
|
||||
if invoiceNum == nil {
|
||||
return ErrInvoiceNotFound
|
||||
}
|
||||
|
||||
updatedInvoice, err = settleHoldInvoice(
|
||||
invoices, settleIndex, invoiceNum, preimage,
|
||||
)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return updatedInvoice, err
|
||||
}
|
||||
|
||||
// CancelInvoice attempts to cancel the invoice corresponding to the passed
|
||||
// payment hash.
|
||||
func (d *DB) CancelInvoice(paymentHash lntypes.Hash) (*Invoice, error) {
|
||||
@ -743,7 +803,8 @@ func (d *DB) InvoicesSettledSince(sinceSettleIndex uint64) ([]Invoice, error) {
|
||||
}
|
||||
|
||||
func putInvoice(invoices, invoiceIndex, addIndex *bbolt.Bucket,
|
||||
i *Invoice, invoiceNum uint32) (uint64, error) {
|
||||
i *Invoice, invoiceNum uint32, paymentHash lntypes.Hash) (
|
||||
uint64, error) {
|
||||
|
||||
// Create the invoice key which is just the big-endian representation
|
||||
// of the invoice number.
|
||||
@ -762,7 +823,6 @@ func putInvoice(invoices, invoiceIndex, addIndex *bbolt.Bucket,
|
||||
// Add the payment hash to the invoice index. This will let us quickly
|
||||
// identify if we can settle an incoming payment, and also to possibly
|
||||
// allow a single invoice to have multiple payment installations.
|
||||
paymentHash := sha256.Sum256(i.Terms.PaymentPreimage[:])
|
||||
err := invoiceIndex.Put(paymentHash[:], invoiceKey[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -928,7 +988,7 @@ func deserializeInvoice(r io.Reader) (Invoice, error) {
|
||||
return invoice, nil
|
||||
}
|
||||
|
||||
func settleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
|
||||
func acceptOrSettleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
|
||||
amtPaid lnwire.MilliSatoshi) (*Invoice, error) {
|
||||
|
||||
invoice, err := fetchInvoice(invoiceNum, invoices)
|
||||
@ -936,32 +996,90 @@ func settleInvoice(invoices, settleIndex *bbolt.Bucket, invoiceNum []byte,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch invoice.Terms.State {
|
||||
case ContractSettled:
|
||||
state := invoice.Terms.State
|
||||
|
||||
switch {
|
||||
case state == ContractAccepted:
|
||||
return &invoice, ErrInvoiceAlreadyAccepted
|
||||
case state == ContractSettled:
|
||||
return &invoice, ErrInvoiceAlreadySettled
|
||||
case ContractCanceled:
|
||||
case state == ContractCanceled:
|
||||
return &invoice, ErrInvoiceAlreadyCanceled
|
||||
}
|
||||
|
||||
holdInvoice := invoice.Terms.PaymentPreimage == UnknownPreimage
|
||||
if holdInvoice {
|
||||
invoice.Terms.State = ContractAccepted
|
||||
} else {
|
||||
err := setSettleFields(settleIndex, invoiceNum, &invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
invoice.AmtPaid = amtPaid
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := serializeInvoice(&buf, &invoice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := invoices.Put(invoiceNum[:], buf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &invoice, nil
|
||||
}
|
||||
|
||||
func setSettleFields(settleIndex *bbolt.Bucket, invoiceNum []byte,
|
||||
invoice *Invoice) error {
|
||||
|
||||
// Now that we know the invoice hasn't already been settled, we'll
|
||||
// update the settle index so we can place this settle event in the
|
||||
// proper location within our time series.
|
||||
nextSettleSeqNo, err := settleIndex.NextSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var seqNoBytes [8]byte
|
||||
byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo)
|
||||
if err := settleIndex.Put(seqNoBytes[:], invoiceNum); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
invoice.AmtPaid = amtPaid
|
||||
invoice.Terms.State = ContractSettled
|
||||
invoice.SettleDate = time.Now()
|
||||
invoice.SettleIndex = nextSettleSeqNo
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func settleHoldInvoice(invoices, settleIndex *bbolt.Bucket,
|
||||
invoiceNum []byte, preimage lntypes.Preimage) (*Invoice,
|
||||
error) {
|
||||
|
||||
invoice, err := fetchInvoice(invoiceNum, invoices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch invoice.Terms.State {
|
||||
case ContractOpen:
|
||||
return &invoice, ErrInvoiceStillOpen
|
||||
case ContractCanceled:
|
||||
return &invoice, ErrInvoiceAlreadyCanceled
|
||||
case ContractSettled:
|
||||
return &invoice, ErrInvoiceAlreadySettled
|
||||
}
|
||||
|
||||
invoice.Terms.PaymentPreimage = preimage
|
||||
|
||||
err = setSettleFields(settleIndex, invoiceNum, &invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := serializeInvoice(&buf, &invoice); err != nil {
|
||||
return nil, err
|
||||
@ -991,6 +1109,9 @@ func cancelInvoice(invoices *bbolt.Bucket, invoiceNum []byte) (
|
||||
|
||||
invoice.Terms.State = ContractCanceled
|
||||
|
||||
// Set AmtPaid back to 0, in case the invoice was already accepted.
|
||||
invoice.AmtPaid = 0
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := serializeInvoice(&buf, &invoice); err != nil {
|
||||
return nil, err
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
@ -15,6 +17,8 @@ import (
|
||||
func invoicesCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
cancelInvoiceCommand,
|
||||
addHoldInvoiceCommand,
|
||||
settleInvoiceCommand,
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +32,60 @@ func getInvoicesClient(ctx *cli.Context) (invoicesrpc.InvoicesClient, func()) {
|
||||
return invoicesrpc.NewInvoicesClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var settleInvoiceCommand = cli.Command{
|
||||
Name: "settleinvoice",
|
||||
Category: "Payments",
|
||||
Usage: "Reveal a preimage and use it to settle the corresponding invoice.",
|
||||
Description: `
|
||||
Todo.`,
|
||||
ArgsUsage: "preimage",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "preimage",
|
||||
Usage: "the hex-encoded preimage (32 byte) which will " +
|
||||
"allow settling an incoming HTLC payable to this " +
|
||||
"preimage.",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(settleInvoice),
|
||||
}
|
||||
|
||||
func settleInvoice(ctx *cli.Context) error {
|
||||
var (
|
||||
preimage []byte
|
||||
err error
|
||||
)
|
||||
|
||||
client, cleanUp := getInvoicesClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("preimage"):
|
||||
preimage, err = hex.DecodeString(ctx.String("preimage"))
|
||||
case args.Present():
|
||||
preimage, err = hex.DecodeString(args.First())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse preimage: %v", err)
|
||||
}
|
||||
|
||||
invoice := &invoicesrpc.SettleInvoiceMsg{
|
||||
Preimage: preimage,
|
||||
}
|
||||
|
||||
resp, err := client.SettleInvoice(context.Background(), invoice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var cancelInvoiceCommand = cli.Command{
|
||||
Name: "cancelinvoice",
|
||||
Category: "Payments",
|
||||
@ -80,3 +138,120 @@ func cancelInvoice(ctx *cli.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var addHoldInvoiceCommand = cli.Command{
|
||||
Name: "addholdinvoice",
|
||||
Category: "Payments",
|
||||
Usage: "Add a new hold invoice.",
|
||||
Description: `
|
||||
Add a new invoice, expressing intent for a future payment.
|
||||
|
||||
Invoices without an amount can be created by not supplying any
|
||||
parameters or providing an amount of 0. These invoices allow the payee
|
||||
to specify the amount of satoshis they wish to send.`,
|
||||
ArgsUsage: "hash [amt]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "memo",
|
||||
Usage: "a description of the payment to attach along " +
|
||||
"with the invoice (default=\"\")",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "amt",
|
||||
Usage: "the amt of satoshis in this invoice",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "description_hash",
|
||||
Usage: "SHA-256 hash of the description of the payment. " +
|
||||
"Used if the purpose of payment cannot naturally " +
|
||||
"fit within the memo. If provided this will be " +
|
||||
"used instead of the description(memo) field in " +
|
||||
"the encoded invoice.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "fallback_addr",
|
||||
Usage: "fallback on-chain address that can be used in " +
|
||||
"case the lightning payment fails",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "expiry",
|
||||
Usage: "the invoice's expiry time in seconds. If not " +
|
||||
"specified, an expiry of 3600 seconds (1 hour) " +
|
||||
"is implied.",
|
||||
},
|
||||
cli.BoolTFlag{
|
||||
Name: "private",
|
||||
Usage: "encode routing hints in the invoice with " +
|
||||
"private channels in order to assist the " +
|
||||
"payer in reaching you",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(addHoldInvoice),
|
||||
}
|
||||
|
||||
func addHoldInvoice(ctx *cli.Context) error {
|
||||
var (
|
||||
descHash []byte
|
||||
amt int64
|
||||
err error
|
||||
)
|
||||
|
||||
client, cleanUp := getInvoicesClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
if ctx.NArg() == 0 {
|
||||
cli.ShowCommandHelp(ctx, "addholdinvoice")
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, err := hex.DecodeString(args.First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse hash: %v", err)
|
||||
}
|
||||
|
||||
args = args.Tail()
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("amt"):
|
||||
amt = ctx.Int64("amt")
|
||||
case args.Present():
|
||||
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode amt argument: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse preimage: %v", err)
|
||||
}
|
||||
|
||||
descHash, err = hex.DecodeString(ctx.String("description_hash"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse description_hash: %v", err)
|
||||
}
|
||||
|
||||
invoice := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
Memo: ctx.String("memo"),
|
||||
Hash: hash,
|
||||
Value: amt,
|
||||
DescriptionHash: descHash,
|
||||
FallbackAddr: ctx.String("fallback_addr"),
|
||||
Expiry: ctx.Int64("expiry"),
|
||||
Private: ctx.Bool("private"),
|
||||
}
|
||||
|
||||
resp, err := client.AddHoldInvoice(context.Background(), invoice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printJSON(struct {
|
||||
PayReq string `json:"pay_req"`
|
||||
}{
|
||||
PayReq: resp.PaymentRequest,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
@ -137,10 +137,9 @@ type ChainArbitratorConfig struct {
|
||||
// Sweeper allows resolvers to sweep their final outputs.
|
||||
Sweeper *sweep.UtxoSweeper
|
||||
|
||||
// SettleInvoice attempts to settle an existing invoice on-chain with
|
||||
// the given payment hash. ErrInvoiceNotFound is returned if an invoice
|
||||
// is not found.
|
||||
SettleInvoice func(lntypes.Hash, lnwire.MilliSatoshi) error
|
||||
// Registry is the invoice database that is used by resolvers to lookup
|
||||
// preimages and settle invoices.
|
||||
Registry *invoices.InvoiceRegistry
|
||||
|
||||
// NotifyClosedChannel is a function closure that the ChainArbitrator
|
||||
// will use to notify the ChannelNotifier about a newly closed channel.
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
@ -177,9 +176,6 @@ func createTestChannelArbitrator(log ArbitratorLog) (*ChannelArbitrator,
|
||||
*lnwallet.IncomingHtlcResolution, uint32) error {
|
||||
return nil
|
||||
},
|
||||
SettleInvoice: func(lntypes.Hash, lnwire.MilliSatoshi) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// We'll use the resolvedChan to synchronize on call to
|
||||
|
@ -1,11 +1,13 @@
|
||||
package contractcourt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
@ -70,11 +72,18 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
return nil, h.Checkpoint(h)
|
||||
}
|
||||
|
||||
// applyPreimage is a helper function that will populate our internal
|
||||
// tryApplyPreimage is a helper function that will populate our internal
|
||||
// resolver with the preimage we learn of. This should be called once
|
||||
// the preimage is revealed so the inner resolver can properly complete
|
||||
// its duties.
|
||||
applyPreimage := func(preimage lntypes.Preimage) {
|
||||
// its duties. The boolean return value indicates whether the preimage
|
||||
// was properly applied.
|
||||
tryApplyPreimage := func(preimage lntypes.Preimage) bool {
|
||||
// Check to see if this preimage matches our htlc.
|
||||
if !preimage.Matches(h.payHash) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update htlcResolution with the matching preimage.
|
||||
h.htlcResolution.Preimage = preimage
|
||||
|
||||
log.Infof("%T(%v): extracted preimage=%v from beacon!", h,
|
||||
@ -93,6 +102,8 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
// preimage.
|
||||
h.htlcResolution.SignedSuccessTx.TxIn[0].Witness[3] = preimage[:]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// If the HTLC hasn't expired yet, then we may still be able to claim
|
||||
@ -112,6 +123,26 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
blockEpochs.Cancel()
|
||||
}()
|
||||
|
||||
// Create a buffered hodl chan to prevent deadlock.
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
|
||||
// Notify registry that we are potentially settling as exit hop
|
||||
// on-chain, so that we will get a hodl event when a corresponding hodl
|
||||
// invoice is settled.
|
||||
event, err := h.Registry.NotifyExitHopHtlc(h.payHash, h.htlcAmt, hodlChan)
|
||||
if err != nil && err != channeldb.ErrInvoiceNotFound {
|
||||
return nil, err
|
||||
}
|
||||
defer h.Registry.HodlUnsubscribeAll(hodlChan)
|
||||
|
||||
// If the htlc can be settled directly, we can progress to the inner
|
||||
// resolver immediately.
|
||||
if event != nil && event.Preimage != nil {
|
||||
if tryApplyPreimage(*event.Preimage) {
|
||||
return &h.htlcSuccessResolver, nil
|
||||
}
|
||||
}
|
||||
|
||||
// With the epochs and preimage subscriptions initialized, we'll query
|
||||
// to see if we already know the preimage.
|
||||
preimage, ok := h.PreimageDB.LookupPreimage(h.payHash)
|
||||
@ -119,26 +150,35 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
// If we do, then this means we can claim the HTLC! However,
|
||||
// we don't know how to ourselves, so we'll return our inner
|
||||
// resolver which has the knowledge to do so.
|
||||
applyPreimage(preimage)
|
||||
return &h.htlcSuccessResolver, nil
|
||||
if tryApplyPreimage(preimage) {
|
||||
return &h.htlcSuccessResolver, nil
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
select {
|
||||
case preimage := <-preimageSubscription.WitnessUpdates:
|
||||
// If this isn't our preimage, then we'll continue
|
||||
// onwards.
|
||||
hash := preimage.Hash()
|
||||
preimageMatches := bytes.Equal(hash[:], h.payHash[:])
|
||||
if !preimageMatches {
|
||||
if !tryApplyPreimage(preimage) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise, we've learned of the preimage! We'll add
|
||||
// this information to our inner resolver, then return
|
||||
// it so it can continue contract resolution.
|
||||
applyPreimage(preimage)
|
||||
// We've learned of the preimage and this information
|
||||
// has been added to our inner resolver. We return it so
|
||||
// it can continue contract resolution.
|
||||
return &h.htlcSuccessResolver, nil
|
||||
|
||||
case hodlItem := <-hodlChan:
|
||||
hodlEvent := hodlItem.(invoices.HodlEvent)
|
||||
|
||||
// Only process settle events.
|
||||
if hodlEvent.Preimage == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !tryApplyPreimage(*hodlEvent.Preimage) {
|
||||
continue
|
||||
}
|
||||
return &h.htlcSuccessResolver, nil
|
||||
|
||||
case newBlock, ok := <-blockEpochs.Epochs:
|
||||
|
@ -178,8 +178,13 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
||||
}
|
||||
|
||||
// With the HTLC claimed, we can attempt to settle its
|
||||
// corresponding invoice if we were the original destination.
|
||||
err = h.SettleInvoice(h.payHash, h.htlcAmt)
|
||||
// corresponding invoice if we were the original destination. As
|
||||
// the htlc is already settled at this point, we don't need to
|
||||
// read on the hodl channel.
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
_, err = h.Registry.NotifyExitHopHtlc(
|
||||
h.payHash, h.htlcAmt, hodlChan,
|
||||
)
|
||||
if err != nil && err != channeldb.ErrInvoiceNotFound {
|
||||
log.Errorf("Unable to settle invoice with payment "+
|
||||
"hash %x: %v", h.payHash, err)
|
||||
@ -251,8 +256,11 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
||||
}
|
||||
|
||||
// With the HTLC claimed, we can attempt to settle its corresponding
|
||||
// invoice if we were the original destination.
|
||||
err = h.SettleInvoice(h.payHash, h.htlcAmt)
|
||||
// invoice if we were the original destination. As the htlc is already
|
||||
// settled at this point, we don't need to read on the hodl
|
||||
// channel.
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
_, err = h.Registry.NotifyExitHopHtlc(h.payHash, h.htlcAmt, hodlChan)
|
||||
if err != nil && err != channeldb.ErrInvoiceNotFound {
|
||||
log.Errorf("Unable to settle invoice with payment "+
|
||||
"hash %x: %v", h.payHash, err)
|
||||
|
@ -3,6 +3,7 @@ package htlcswitch
|
||||
import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnpeer"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@ -17,13 +18,23 @@ type InvoiceDatabase interface {
|
||||
// extended to us gives us enough time to settle as we prescribe.
|
||||
LookupInvoice(lntypes.Hash) (channeldb.Invoice, uint32, error)
|
||||
|
||||
// SettleInvoice attempts to mark an invoice corresponding to the
|
||||
// passed payment hash as fully settled.
|
||||
SettleInvoice(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi) error
|
||||
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the
|
||||
// invoice is a debug invoice, then this method is a noop as debug
|
||||
// invoices are never fully settled. The return value describes how the
|
||||
// htlc should be resolved. If the htlc cannot be resolved immediately,
|
||||
// the resolution is sent on the passed in hodlChan later.
|
||||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||
hodlChan chan<- interface{}) (*invoices.HodlEvent, error)
|
||||
|
||||
// CancelInvoice attempts to cancel the invoice corresponding to the
|
||||
// passed payment hash.
|
||||
CancelInvoice(payHash lntypes.Hash) error
|
||||
|
||||
// SettleHodlInvoice settles a hold invoice.
|
||||
SettleHodlInvoice(preimage lntypes.Preimage) error
|
||||
|
||||
// HodlUnsubscribeAll unsubscribes from all hodl events.
|
||||
HodlUnsubscribeAll(subscriber chan<- interface{})
|
||||
}
|
||||
|
||||
// ChannelLink is an interface which represents the subsystem for managing the
|
||||
|
@ -16,10 +16,12 @@ import (
|
||||
"github.com/lightningnetwork/lnd/contractcourt"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnpeer"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/queue"
|
||||
"github.com/lightningnetwork/lnd/ticker"
|
||||
)
|
||||
|
||||
@ -344,10 +346,24 @@ type channelLink struct {
|
||||
|
||||
sync.RWMutex
|
||||
|
||||
// hodlQueue is used to receive exit hop htlc resolutions from invoice
|
||||
// registry.
|
||||
hodlQueue *queue.ConcurrentQueue
|
||||
|
||||
// hodlMap stores a list of htlc data structs per hash. It allows
|
||||
// resolving those htlcs when we receive a message on hodlQueue.
|
||||
hodlMap map[lntypes.Hash][]hodlHtlc
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// hodlHtlc contains htlc data that is required for resolution.
|
||||
type hodlHtlc struct {
|
||||
pd *lnwallet.PaymentDescriptor
|
||||
obfuscator ErrorEncrypter
|
||||
}
|
||||
|
||||
// NewChannelLink creates a new instance of a ChannelLink given a configuration
|
||||
// and active channel that will be used to verify/apply updates to.
|
||||
func NewChannelLink(cfg ChannelLinkConfig,
|
||||
@ -361,6 +377,8 @@ func NewChannelLink(cfg ChannelLinkConfig,
|
||||
logCommitTimer: time.NewTimer(300 * time.Millisecond),
|
||||
overflowQueue: newPacketQueue(input.MaxHTLCNumber / 2),
|
||||
htlcUpdates: make(chan []channeldb.HTLC),
|
||||
hodlMap: make(map[lntypes.Hash][]hodlHtlc),
|
||||
hodlQueue: queue.NewConcurrentQueue(10),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@ -384,6 +402,7 @@ func (l *channelLink) Start() error {
|
||||
|
||||
l.mailBox.ResetMessages()
|
||||
l.overflowQueue.Start()
|
||||
l.hodlQueue.Start()
|
||||
|
||||
// Before launching the htlcManager messages, revert any circuits that
|
||||
// were marked open in the switch's circuit map, but did not make it
|
||||
@ -449,12 +468,17 @@ func (l *channelLink) Stop() {
|
||||
|
||||
log.Infof("ChannelLink(%v) is stopping", l)
|
||||
|
||||
// As the link is stopping, we are no longer interested in hodl events
|
||||
// coming from the invoice registry.
|
||||
l.cfg.Registry.HodlUnsubscribeAll(l.hodlQueue.ChanIn())
|
||||
|
||||
if l.cfg.ChainEvents.Cancel != nil {
|
||||
l.cfg.ChainEvents.Cancel()
|
||||
}
|
||||
|
||||
l.updateFeeTimer.Stop()
|
||||
l.overflowQueue.Stop()
|
||||
l.hodlQueue.Stop()
|
||||
|
||||
close(l.quit)
|
||||
l.wg.Wait()
|
||||
@ -1058,12 +1082,119 @@ out:
|
||||
case msg := <-l.upstream:
|
||||
l.handleUpstreamMsg(msg)
|
||||
|
||||
// A hodl event is received. This means that we now have a
|
||||
// resolution for a previously accepted htlc.
|
||||
case hodlItem := <-l.hodlQueue.ChanOut():
|
||||
hodlEvent := hodlItem.(invoices.HodlEvent)
|
||||
err := l.processHodlQueue(hodlEvent)
|
||||
if err != nil {
|
||||
l.fail(LinkFailureError{code: ErrInternalError},
|
||||
fmt.Sprintf("process hodl queue: %v",
|
||||
err.Error()),
|
||||
)
|
||||
break out
|
||||
}
|
||||
|
||||
case <-l.quit:
|
||||
break out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processHodlQueue processes a received hodl event and continues reading from
|
||||
// the hodl queue until no more events remain. When this function returns
|
||||
// without an error, the commit tx should be updated.
|
||||
func (l *channelLink) processHodlQueue(firstHodlEvent invoices.HodlEvent) error {
|
||||
// Try to read all waiting resolution messages, so that they can all be
|
||||
// processed in a single commitment tx update.
|
||||
hodlEvent := firstHodlEvent
|
||||
loop:
|
||||
for {
|
||||
if err := l.processHodlMapEvent(hodlEvent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case item := <-l.hodlQueue.ChanOut():
|
||||
hodlEvent = item.(invoices.HodlEvent)
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Update the commitment tx.
|
||||
if err := l.updateCommitTx(); err != nil {
|
||||
return fmt.Errorf("unable to update commitment: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processHodlMapEvent resolves stored hodl htlcs based using the information in
|
||||
// hodlEvent.
|
||||
func (l *channelLink) processHodlMapEvent(hodlEvent invoices.HodlEvent) error {
|
||||
// Lookup all hodl htlcs that can be failed or settled with this event.
|
||||
// The hodl htlc must be present in the map.
|
||||
hash := hodlEvent.Hash
|
||||
hodlHtlcs, ok := l.hodlMap[hash]
|
||||
if !ok {
|
||||
return fmt.Errorf("hodl htlc not found: %v", hash)
|
||||
}
|
||||
|
||||
if err := l.processHodlEvent(hodlEvent, hodlHtlcs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean up hodl map.
|
||||
delete(l.hodlMap, hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processHodlEvent applies a received hodl event to the provided htlc. When
|
||||
// this function returns without an error, the commit tx should be updated.
|
||||
func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent,
|
||||
htlcs ...hodlHtlc) error {
|
||||
|
||||
hash := hodlEvent.Hash
|
||||
if hodlEvent.Preimage == nil {
|
||||
l.debugf("Received hodl cancel event for %v", hash)
|
||||
} else {
|
||||
l.debugf("Received hodl settle event for %v", hash)
|
||||
}
|
||||
|
||||
// Determine required action for the resolution.
|
||||
var hodlAction func(htlc hodlHtlc) error
|
||||
if hodlEvent.Preimage != nil {
|
||||
hodlAction = func(htlc hodlHtlc) error {
|
||||
return l.settleHTLC(
|
||||
*hodlEvent.Preimage, htlc.pd.HtlcIndex,
|
||||
htlc.pd.SourceRef,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
hodlAction = func(htlc hodlHtlc) error {
|
||||
failure := lnwire.NewFailUnknownPaymentHash(
|
||||
htlc.pd.Amount,
|
||||
)
|
||||
l.sendHTLCError(
|
||||
htlc.pd.HtlcIndex, failure, htlc.obfuscator,
|
||||
htlc.pd.SourceRef,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Apply action for all htlcs matching this hash.
|
||||
for _, htlc := range htlcs {
|
||||
if err := hodlAction(htlc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// randomFeeUpdateTimeout returns a random timeout between the bounds defined
|
||||
// within the link's configuration that will be used to determine when the link
|
||||
// should propose an update to its commitment fee rate.
|
||||
@ -2354,228 +2485,20 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
|
||||
fwdInfo := chanIterator.ForwardingInstructions()
|
||||
switch fwdInfo.NextHop {
|
||||
case exitHop:
|
||||
// If hodl.ExitSettle is requested, we will not validate
|
||||
// the final hop's ADD, nor will we settle the
|
||||
// corresponding invoice or respond with the preimage.
|
||||
if l.cfg.DebugHTLC &&
|
||||
l.cfg.HodlMask.Active(hodl.ExitSettle) {
|
||||
l.warnf(hodl.ExitSettle.Warning())
|
||||
continue
|
||||
}
|
||||
|
||||
// First, we'll check the expiry of the HTLC itself
|
||||
// against, the current block height. If the timeout is
|
||||
// too soon, then we'll reject the HTLC.
|
||||
if pd.Timeout-expiryGraceDelta <= heightNow {
|
||||
log.Errorf("htlc(%x) has an expiry that's too "+
|
||||
"soon: expiry=%v, best_height=%v",
|
||||
pd.RHash[:], pd.Timeout, heightNow)
|
||||
|
||||
failure := lnwire.FailFinalExpiryTooSoon{}
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, &failure, obfuscator, pd.SourceRef,
|
||||
)
|
||||
needUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
// We're the designated payment destination. Therefore
|
||||
// we attempt to see if we have an invoice locally
|
||||
// which'll allow us to settle this htlc.
|
||||
invoiceHash := lntypes.Hash(pd.RHash)
|
||||
invoice, minCltvDelta, err := l.cfg.Registry.LookupInvoice(
|
||||
invoiceHash,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("unable to query invoice registry: "+
|
||||
" %v", err)
|
||||
failure := lnwire.NewFailUnknownPaymentHash(
|
||||
pd.Amount,
|
||||
)
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, failure, obfuscator, pd.SourceRef,
|
||||
)
|
||||
|
||||
needUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Reject htlcs for canceled invoices.
|
||||
if invoice.Terms.State == channeldb.ContractCanceled {
|
||||
l.errorf("Rejecting htlc due to canceled " +
|
||||
"invoice")
|
||||
|
||||
failure := lnwire.NewFailUnknownPaymentHash(
|
||||
pd.Amount,
|
||||
)
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, failure, obfuscator,
|
||||
pd.SourceRef,
|
||||
)
|
||||
|
||||
needUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
// If the invoice is already settled, we choose to
|
||||
// accept the payment to simplify failure recovery.
|
||||
//
|
||||
// NOTE: Though our recovery and forwarding logic is
|
||||
// predominately batched, settling invoices happens
|
||||
// iteratively. We may reject one of two payments
|
||||
// for the same rhash at first, but then restart and
|
||||
// reject both after seeing that the invoice has been
|
||||
// settled. Without any record of which one settles
|
||||
// first, it is ambiguous as to which one actually
|
||||
// settled the invoice. Thus, by accepting all
|
||||
// payments, we eliminate the race condition that can
|
||||
// lead to this inconsistency.
|
||||
//
|
||||
// TODO(conner): track ownership of settlements to
|
||||
// properly recover from failures? or add batch invoice
|
||||
// settlement
|
||||
if invoice.Terms.State != channeldb.ContractOpen {
|
||||
log.Warnf("Accepting duplicate payment for "+
|
||||
"hash=%x", pd.RHash[:])
|
||||
}
|
||||
|
||||
// If we're not currently in debug mode, and the
|
||||
// extended htlc doesn't meet the value requested, then
|
||||
// we'll fail the htlc. Otherwise, we settle this htlc
|
||||
// within our local state update log, then send the
|
||||
// update entry to the remote party.
|
||||
//
|
||||
// NOTE: We make an exception when the value requested
|
||||
// by the invoice is zero. This means the invoice
|
||||
// allows the payee to specify the amount of satoshis
|
||||
// they wish to send. So since we expect the htlc to
|
||||
// have a different amount, we should not fail.
|
||||
if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 &&
|
||||
pd.Amount < invoice.Terms.Value {
|
||||
|
||||
log.Errorf("rejecting htlc due to incorrect "+
|
||||
"amount: expected %v, received %v",
|
||||
invoice.Terms.Value, pd.Amount)
|
||||
|
||||
failure := lnwire.NewFailUnknownPaymentHash(
|
||||
pd.Amount,
|
||||
)
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, failure, obfuscator, pd.SourceRef,
|
||||
)
|
||||
|
||||
needUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
// As we're the exit hop, we'll double check the
|
||||
// hop-payload included in the HTLC to ensure that it
|
||||
// was crafted correctly by the sender and matches the
|
||||
// HTLC we were extended.
|
||||
//
|
||||
// NOTE: We make an exception when the value requested
|
||||
// by the invoice is zero. This means the invoice
|
||||
// allows the payee to specify the amount of satoshis
|
||||
// they wish to send. So since we expect the htlc to
|
||||
// have a different amount, we should not fail.
|
||||
if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 &&
|
||||
fwdInfo.AmountToForward < invoice.Terms.Value {
|
||||
|
||||
log.Errorf("Onion payload of incoming htlc(%x) "+
|
||||
"has incorrect value: expected %v, "+
|
||||
"got %v", pd.RHash, invoice.Terms.Value,
|
||||
fwdInfo.AmountToForward)
|
||||
|
||||
failure := lnwire.NewFailUnknownPaymentHash(
|
||||
pd.Amount,
|
||||
)
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, failure, obfuscator, pd.SourceRef,
|
||||
)
|
||||
|
||||
needUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
// We'll also ensure that our time-lock value has been
|
||||
// computed correctly.
|
||||
expectedHeight := heightNow + minCltvDelta
|
||||
switch {
|
||||
case !l.cfg.DebugHTLC && pd.Timeout < expectedHeight:
|
||||
log.Errorf("Incoming htlc(%x) has an "+
|
||||
"expiration that is too soon: "+
|
||||
"expected at least %v, got %v",
|
||||
pd.RHash[:], expectedHeight, pd.Timeout)
|
||||
|
||||
failure := lnwire.FailFinalExpiryTooSoon{}
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, failure, obfuscator,
|
||||
pd.SourceRef,
|
||||
)
|
||||
|
||||
needUpdate = true
|
||||
continue
|
||||
|
||||
case !l.cfg.DebugHTLC && pd.Timeout != fwdInfo.OutgoingCTLV:
|
||||
log.Errorf("HTLC(%x) has incorrect "+
|
||||
"time-lock: expected %v, got %v",
|
||||
pd.RHash[:], pd.Timeout,
|
||||
fwdInfo.OutgoingCTLV)
|
||||
|
||||
failure := lnwire.NewFinalIncorrectCltvExpiry(
|
||||
fwdInfo.OutgoingCTLV,
|
||||
)
|
||||
l.sendHTLCError(
|
||||
pd.HtlcIndex, failure, obfuscator, pd.SourceRef,
|
||||
)
|
||||
|
||||
needUpdate = true
|
||||
continue
|
||||
}
|
||||
|
||||
preimage := invoice.Terms.PaymentPreimage
|
||||
err = l.channel.SettleHTLC(
|
||||
preimage, pd.HtlcIndex, pd.SourceRef, nil, nil,
|
||||
updated, err := l.processExitHop(
|
||||
pd, obfuscator, fwdInfo, heightNow,
|
||||
)
|
||||
if err != nil {
|
||||
l.fail(LinkFailureError{code: ErrInternalError},
|
||||
"unable to settle htlc: %v", err)
|
||||
err.Error(),
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Notify the invoiceRegistry of the invoices we just
|
||||
// settled (with the amount accepted at settle time)
|
||||
// with this latest commitment update.
|
||||
err = l.cfg.Registry.SettleInvoice(
|
||||
invoiceHash, pd.Amount,
|
||||
)
|
||||
if err != nil {
|
||||
l.fail(LinkFailureError{code: ErrInternalError},
|
||||
"unable to settle invoice: %v", err)
|
||||
return false
|
||||
if updated {
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
l.infof("settling %x as exit hop", pd.RHash)
|
||||
|
||||
// If the link is in hodl.BogusSettle mode, replace the
|
||||
// preimage with a fake one before sending it to the
|
||||
// peer.
|
||||
if l.cfg.DebugHTLC &&
|
||||
l.cfg.HodlMask.Active(hodl.BogusSettle) {
|
||||
l.warnf(hodl.BogusSettle.Warning())
|
||||
preimage = [32]byte{}
|
||||
copy(preimage[:], bytes.Repeat([]byte{2}, 32))
|
||||
}
|
||||
|
||||
// HTLC was successfully settled locally send
|
||||
// notification about it remote peer.
|
||||
l.cfg.Peer.SendMessage(false, &lnwire.UpdateFulfillHTLC{
|
||||
ChanID: l.ChanID(),
|
||||
ID: pd.HtlcIndex,
|
||||
PaymentPreimage: preimage,
|
||||
})
|
||||
needUpdate = true
|
||||
|
||||
// There are additional channels left within this route. So
|
||||
// we'll simply do some forwarding package book-keeping.
|
||||
default:
|
||||
@ -2733,6 +2656,208 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
|
||||
return needUpdate
|
||||
}
|
||||
|
||||
// processExitHop handles an htlc for which this link is the exit hop. It
|
||||
// returns a boolean indicating whether the commitment tx needs an update.
|
||||
func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
|
||||
obfuscator ErrorEncrypter, fwdInfo ForwardingInfo, heightNow uint32) (
|
||||
bool, error) {
|
||||
|
||||
// If hodl.ExitSettle is requested, we will not validate the final hop's
|
||||
// ADD, nor will we settle the corresponding invoice or respond with the
|
||||
// preimage.
|
||||
if l.cfg.DebugHTLC && l.cfg.HodlMask.Active(hodl.ExitSettle) {
|
||||
l.warnf(hodl.ExitSettle.Warning())
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// First, we'll check the expiry of the HTLC itself against, the current
|
||||
// block height. If the timeout is too soon, then we'll reject the HTLC.
|
||||
if pd.Timeout-expiryGraceDelta <= heightNow {
|
||||
log.Errorf("htlc(%x) has an expiry that's too soon: expiry=%v"+
|
||||
", best_height=%v", pd.RHash[:], pd.Timeout, heightNow)
|
||||
|
||||
failure := lnwire.NewFinalExpiryTooSoon()
|
||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We're the designated payment destination. Therefore we attempt to
|
||||
// see if we have an invoice locally which'll allow us to settle this
|
||||
// htlc.
|
||||
//
|
||||
// Only the immutable data from LookupInvoice is used, because otherwise
|
||||
// a race condition may be created with concurrent writes to the invoice
|
||||
// registry. For example: cancelation of an invoice.
|
||||
invoiceHash := lntypes.Hash(pd.RHash)
|
||||
invoice, minCltvDelta, err := l.cfg.Registry.LookupInvoice(invoiceHash)
|
||||
if err != nil {
|
||||
log.Errorf("unable to query invoice registry: %v", err)
|
||||
failure := lnwire.NewFailUnknownPaymentHash(pd.Amount)
|
||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If the invoice is already settled, we choose to accept the payment to
|
||||
// simplify failure recovery.
|
||||
//
|
||||
// NOTE: Though our recovery and forwarding logic is predominately
|
||||
// batched, settling invoices happens iteratively. We may reject one of
|
||||
// two payments for the same rhash at first, but then restart and reject
|
||||
// both after seeing that the invoice has been settled. Without any
|
||||
// record of which one settles first, it is ambiguous as to which one
|
||||
// actually settled the invoice. Thus, by accepting all payments, we
|
||||
// eliminate the race condition that can lead to this inconsistency.
|
||||
//
|
||||
// TODO(conner): track ownership of settlements to properly recover from
|
||||
// failures? or add batch invoice settlement
|
||||
//
|
||||
// TODO(joostjager): The log statement below is not always accurate, as
|
||||
// the invoice may have been canceled after the LookupInvoice call.
|
||||
// Leaving it as is for now, because fixing this would involve changing
|
||||
// the signature of InvoiceRegistry.SettleInvoice just because of this
|
||||
// log statement.
|
||||
if invoice.Terms.State == channeldb.ContractSettled {
|
||||
log.Warnf("Accepting duplicate payment for hash=%x",
|
||||
pd.RHash[:])
|
||||
}
|
||||
|
||||
// If we're not currently in debug mode, and the extended htlc doesn't
|
||||
// meet the value requested, then we'll fail the htlc. Otherwise, we
|
||||
// settle this htlc within our local state update log, then send the
|
||||
// update entry to the remote party.
|
||||
//
|
||||
// NOTE: We make an exception when the value requested by the invoice is
|
||||
// zero. This means the invoice allows the payee to specify the amount
|
||||
// of satoshis they wish to send. So since we expect the htlc to have a
|
||||
// different amount, we should not fail.
|
||||
if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 &&
|
||||
pd.Amount < invoice.Terms.Value {
|
||||
|
||||
log.Errorf("rejecting htlc due to incorrect amount: expected "+
|
||||
"%v, received %v", invoice.Terms.Value, pd.Amount)
|
||||
|
||||
failure := lnwire.NewFailUnknownPaymentHash(pd.Amount)
|
||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// As we're the exit hop, we'll double check the hop-payload included in
|
||||
// the HTLC to ensure that it was crafted correctly by the sender and
|
||||
// matches the HTLC we were extended.
|
||||
//
|
||||
// NOTE: We make an exception when the value requested by the invoice is
|
||||
// zero. This means the invoice allows the payee to specify the amount
|
||||
// of satoshis they wish to send. So since we expect the htlc to have a
|
||||
// different amount, we should not fail.
|
||||
if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 &&
|
||||
fwdInfo.AmountToForward < invoice.Terms.Value {
|
||||
|
||||
log.Errorf("Onion payload of incoming htlc(%x) has incorrect "+
|
||||
"value: expected %v, got %v", pd.RHash,
|
||||
invoice.Terms.Value, fwdInfo.AmountToForward)
|
||||
|
||||
failure := lnwire.NewFailUnknownPaymentHash(pd.Amount)
|
||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We'll also ensure that our time-lock value has been computed
|
||||
// correctly.
|
||||
expectedHeight := heightNow + minCltvDelta
|
||||
switch {
|
||||
case !l.cfg.DebugHTLC && pd.Timeout < expectedHeight:
|
||||
log.Errorf("Incoming htlc(%x) has an expiration that is too "+
|
||||
"soon: expected at least %v, got %v",
|
||||
pd.RHash[:], expectedHeight, pd.Timeout)
|
||||
|
||||
failure := lnwire.FailFinalExpiryTooSoon{}
|
||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
||||
|
||||
return true, nil
|
||||
|
||||
case !l.cfg.DebugHTLC && pd.Timeout != fwdInfo.OutgoingCTLV:
|
||||
log.Errorf("HTLC(%x) has incorrect time-lock: expected %v, "+
|
||||
"got %v", pd.RHash[:], pd.Timeout, fwdInfo.OutgoingCTLV)
|
||||
|
||||
failure := lnwire.NewFinalIncorrectCltvExpiry(
|
||||
fwdInfo.OutgoingCTLV,
|
||||
)
|
||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Notify the invoiceRegistry of the exit hop htlc. If we crash right
|
||||
// after this, this code will be re-executed after restart. We will
|
||||
// receive back a resolution event.
|
||||
event, err := l.cfg.Registry.NotifyExitHopHtlc(
|
||||
invoiceHash, pd.Amount, l.hodlQueue.ChanIn(),
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Create a hodlHtlc struct and decide either resolved now or later.
|
||||
htlc := hodlHtlc{
|
||||
pd: pd,
|
||||
obfuscator: obfuscator,
|
||||
}
|
||||
|
||||
if event == nil {
|
||||
// Save payment descriptor for future reference.
|
||||
hodlHtlcs := l.hodlMap[invoiceHash]
|
||||
l.hodlMap[invoiceHash] = append(hodlHtlcs, htlc)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Process the received resolution.
|
||||
err = l.processHodlEvent(*event, htlc)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// settleHTLC settles the HTLC on the channel.
|
||||
func (l *channelLink) settleHTLC(preimage lntypes.Preimage, htlcIndex uint64,
|
||||
sourceRef *channeldb.AddRef) error {
|
||||
|
||||
hash := preimage.Hash()
|
||||
|
||||
l.infof("settling htlc %v as exit hop", hash)
|
||||
|
||||
err := l.channel.SettleHTLC(
|
||||
preimage, htlcIndex, sourceRef, nil, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to settle htlc: %v", err)
|
||||
}
|
||||
|
||||
// If the link is in hodl.BogusSettle mode, replace the preimage with a
|
||||
// fake one before sending it to the peer.
|
||||
if l.cfg.DebugHTLC && l.cfg.HodlMask.Active(hodl.BogusSettle) {
|
||||
l.warnf(hodl.BogusSettle.Warning())
|
||||
preimage = [32]byte{}
|
||||
copy(preimage[:], bytes.Repeat([]byte{2}, 32))
|
||||
}
|
||||
|
||||
// HTLC was successfully settled locally send notification about it
|
||||
// remote peer.
|
||||
l.cfg.Peer.SendMessage(false, &lnwire.UpdateFulfillHTLC{
|
||||
ChanID: l.ChanID(),
|
||||
ID: htlcIndex,
|
||||
PaymentPreimage: preimage,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// forwardBatch forwards the given htlcPackets to the switch, and waits on the
|
||||
// err chan for the individual responses. This method is intended to be spawned
|
||||
// as a goroutine so the responses can be handled in the background.
|
||||
|
@ -1066,22 +1066,15 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate payment: invoice and htlc.
|
||||
invoice, htlc, err := generatePayment(amount, htlcAmt, totalTimelock,
|
||||
// Generate payment invoice and htlc, but don't add this invoice to the
|
||||
// receiver registry. This should trigger an unknown payment hash
|
||||
// failure.
|
||||
_, htlc, err := generatePayment(amount, htlcAmt, totalTimelock,
|
||||
blob)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We need to have wrong rhash for that reason we should change the
|
||||
// preimage. Inverse first byte by xoring with 0xff.
|
||||
invoice.Terms.PaymentPreimage[0] ^= byte(255)
|
||||
|
||||
// Check who is last in the route and add invoice to server registry.
|
||||
if err := n.carolServer.registry.AddInvoice(*invoice); err != nil {
|
||||
t.Fatalf("unable to add invoice in carol registry: %v", err)
|
||||
}
|
||||
|
||||
// Send payment and expose err channel.
|
||||
_, err = n.aliceServer.htlcSwitch.SendHTLC(
|
||||
n.firstBobChannelLink.ShortChanID(), htlc,
|
||||
@ -1095,12 +1088,6 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
|
||||
// Wait for Alice to receive the revocation.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Check that alice invoice wasn't settled and bandwidth of htlc
|
||||
// links hasn't been changed.
|
||||
if invoice.Terms.State == channeldb.ContractSettled {
|
||||
t.Fatal("alice invoice was settled")
|
||||
}
|
||||
|
||||
if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore {
|
||||
t.Fatal("the bandwidth of alice channel link which handles " +
|
||||
"alice->bob channel should be the same")
|
||||
@ -2059,7 +2046,9 @@ func TestChannelLinkBandwidthConsistency(t *testing.T) {
|
||||
|
||||
// We must add the invoice to the registry, such that Alice expects
|
||||
// this payment.
|
||||
err = coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(*invoice)
|
||||
err = coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(
|
||||
*invoice, htlc.PaymentHash,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add invoice to registry: %v", err)
|
||||
}
|
||||
@ -2161,7 +2150,9 @@ func TestChannelLinkBandwidthConsistency(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create payment: %v", err)
|
||||
}
|
||||
err = coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(*invoice)
|
||||
err = coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(
|
||||
*invoice, htlc.PaymentHash,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add invoice to registry: %v", err)
|
||||
}
|
||||
@ -3817,7 +3808,9 @@ func TestChannelLinkAcceptDuplicatePayment(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := n.carolServer.registry.AddInvoice(*invoice); err != nil {
|
||||
|
||||
err = n.carolServer.registry.AddInvoice(*invoice, htlc.PaymentHash)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add invoice in carol registry: %v", err)
|
||||
}
|
||||
|
||||
@ -4195,7 +4188,8 @@ func generateHtlc(t *testing.T, coreLink *channelLink,
|
||||
// We must add the invoice to the registry, such that Alice
|
||||
// expects this payment.
|
||||
err := coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(
|
||||
*invoice)
|
||||
*invoice, htlc.PaymentHash,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to add invoice to registry: %v", err)
|
||||
}
|
||||
@ -5595,3 +5589,169 @@ func TestChannelLinkCanceledInvoice(t *testing.T) {
|
||||
t.Fatalf("expected unknown payment hash, but got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type hodlInvoiceTestCtx struct {
|
||||
n *twoHopNetwork
|
||||
startBandwidthAlice lnwire.MilliSatoshi
|
||||
startBandwidthBob lnwire.MilliSatoshi
|
||||
hash lntypes.Hash
|
||||
preimage lntypes.Preimage
|
||||
amount lnwire.MilliSatoshi
|
||||
errChan chan error
|
||||
|
||||
cleanUp func()
|
||||
}
|
||||
|
||||
func newHodlInvoiceTestCtx(t *testing.T) (*hodlInvoiceTestCtx, error) {
|
||||
// Setup a alice-bob network.
|
||||
aliceChannel, bobChannel, cleanUp, err := createTwoClusterChannels(
|
||||
btcutil.SatoshiPerBitcoin*3,
|
||||
btcutil.SatoshiPerBitcoin*5,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create channel: %v", err)
|
||||
}
|
||||
|
||||
n := newTwoHopNetwork(t, aliceChannel, bobChannel, testStartingHeight)
|
||||
if err := n.start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
|
||||
bobBandwidthBefore := n.bobChannelLink.Bandwidth()
|
||||
|
||||
debug := false
|
||||
if debug {
|
||||
// Log message that alice receives.
|
||||
n.aliceServer.intersect(
|
||||
createLogFunc("alice", n.aliceChannelLink.ChanID()),
|
||||
)
|
||||
|
||||
// Log message that bob receives.
|
||||
n.bobServer.intersect(
|
||||
createLogFunc("bob", n.bobChannelLink.ChanID()),
|
||||
)
|
||||
}
|
||||
|
||||
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
|
||||
htlcAmt, totalTimelock, hops := generateHops(
|
||||
amount, testStartingHeight, n.bobChannelLink,
|
||||
)
|
||||
|
||||
// Generate hold invoice preimage.
|
||||
r, err := generateRandomBytes(sha256.Size)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preimage, err := lntypes.MakePreimage(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash := preimage.Hash()
|
||||
|
||||
// Have alice pay the hodl invoice, wait for bob's commitment state to
|
||||
// be updated and the invoice state to be updated.
|
||||
receiver := n.bobServer
|
||||
receiver.registry.settleChan = make(chan lntypes.Hash)
|
||||
firstHop := n.bobChannelLink.ShortChanID()
|
||||
errChan := n.makeHoldPayment(
|
||||
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
|
||||
totalTimelock, preimage,
|
||||
)
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
t.Fatalf("no payment result expected: %v", err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout")
|
||||
case h := <-receiver.registry.settleChan:
|
||||
if hash != h {
|
||||
t.Fatal("unexpect invoice settled")
|
||||
}
|
||||
}
|
||||
|
||||
return &hodlInvoiceTestCtx{
|
||||
n: n,
|
||||
startBandwidthAlice: aliceBandwidthBefore,
|
||||
startBandwidthBob: bobBandwidthBefore,
|
||||
preimage: preimage,
|
||||
hash: hash,
|
||||
amount: amount,
|
||||
errChan: errChan,
|
||||
|
||||
cleanUp: func() {
|
||||
cleanUp()
|
||||
n.stop()
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestChannelLinkHoldInvoiceSettle asserts that a hodl invoice can be settled.
|
||||
func TestChannelLinkHoldInvoiceSettle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, err := newHodlInvoiceTestCtx(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ctx.cleanUp()
|
||||
|
||||
err = ctx.n.bobServer.registry.SettleHodlInvoice(ctx.preimage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for payment to succeed.
|
||||
select {
|
||||
case err := <-ctx.errChan:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
|
||||
// Wait for Bob to receive the revocation.
|
||||
if ctx.startBandwidthAlice-ctx.amount !=
|
||||
ctx.n.aliceChannelLink.Bandwidth() {
|
||||
|
||||
t.Fatal("alice bandwidth should have decrease on payment " +
|
||||
"amount")
|
||||
}
|
||||
|
||||
if ctx.startBandwidthBob+ctx.amount !=
|
||||
ctx.n.bobChannelLink.Bandwidth() {
|
||||
|
||||
t.Fatalf("bob bandwidth isn't match: expected %v, got %v",
|
||||
ctx.startBandwidthBob+ctx.amount,
|
||||
ctx.n.bobChannelLink.Bandwidth())
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelLinkHoldInvoiceSettle asserts that a hodl invoice can be canceled.
|
||||
func TestChannelLinkHoldInvoiceCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, err := newHodlInvoiceTestCtx(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ctx.cleanUp()
|
||||
|
||||
err = ctx.n.bobServer.registry.CancelInvoice(ctx.hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for payment to succeed.
|
||||
select {
|
||||
case err := <-ctx.errChan:
|
||||
if !strings.Contains(err.Error(),
|
||||
lnwire.CodeUnknownPaymentHash.String()) {
|
||||
|
||||
t.Fatal("expected unknown payment hash")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@ -23,6 +24,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/contractcourt"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnpeer"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
@ -131,6 +133,7 @@ type mockServer struct {
|
||||
htlcSwitch *Switch
|
||||
|
||||
registry *mockInvoiceRegistry
|
||||
pCache *mockPreimageCache
|
||||
interceptorFuncs []messageInterceptor
|
||||
}
|
||||
|
||||
@ -186,19 +189,24 @@ func newMockServer(t testing.TB, name string, startingHeight uint32,
|
||||
h := sha256.Sum256([]byte(name))
|
||||
copy(id[:], h[:])
|
||||
|
||||
pCache := newMockPreimageCache()
|
||||
|
||||
htlcSwitch, err := initSwitchWithDB(startingHeight, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
registry := newMockRegistry(defaultDelta)
|
||||
|
||||
return &mockServer{
|
||||
t: t,
|
||||
id: id,
|
||||
name: name,
|
||||
messages: make(chan lnwire.Message, 3000),
|
||||
quit: make(chan struct{}),
|
||||
registry: newMockRegistry(defaultDelta),
|
||||
registry: registry,
|
||||
htlcSwitch: htlcSwitch,
|
||||
pCache: pCache,
|
||||
interceptorFuncs: make([]messageInterceptor, 0),
|
||||
}, nil
|
||||
}
|
||||
@ -697,82 +705,92 @@ func (f *mockChannelLink) UpdateShortChanID() (lnwire.ShortChannelID, error) {
|
||||
|
||||
var _ ChannelLink = (*mockChannelLink)(nil)
|
||||
|
||||
type mockInvoiceRegistry struct {
|
||||
sync.Mutex
|
||||
func newDB() (*channeldb.DB, func(), error) {
|
||||
// First, create a temporary directory to be used for the duration of
|
||||
// this test.
|
||||
tempDirName, err := ioutil.TempDir("", "channeldb")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
invoices map[lntypes.Hash]channeldb.Invoice
|
||||
finalDelta uint32
|
||||
// Next, create channeldb for the first time.
|
||||
cdb, err := channeldb.Open(tempDirName)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDirName)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cleanUp := func() {
|
||||
cdb.Close()
|
||||
os.RemoveAll(tempDirName)
|
||||
}
|
||||
|
||||
return cdb, cleanUp, nil
|
||||
}
|
||||
|
||||
type mockInvoiceRegistry struct {
|
||||
settleChan chan lntypes.Hash
|
||||
|
||||
registry *invoices.InvoiceRegistry
|
||||
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
|
||||
cdb, cleanup, err := newDB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
decodeExpiry := func(invoice string) (uint32, error) {
|
||||
return 3, nil
|
||||
}
|
||||
|
||||
registry := invoices.NewRegistry(cdb, decodeExpiry)
|
||||
registry.Start()
|
||||
|
||||
return &mockInvoiceRegistry{
|
||||
finalDelta: minDelta,
|
||||
invoices: make(map[lntypes.Hash]channeldb.Invoice),
|
||||
registry: registry,
|
||||
cleanup: cleanup,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *mockInvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice, uint32, error) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
invoice, ok := i.invoices[rHash]
|
||||
if !ok {
|
||||
return channeldb.Invoice{}, 0, fmt.Errorf("can't find mock "+
|
||||
"invoice: %x", rHash[:])
|
||||
}
|
||||
|
||||
return invoice, i.finalDelta, nil
|
||||
return i.registry.LookupInvoice(rHash)
|
||||
}
|
||||
|
||||
func (i *mockInvoiceRegistry) SettleInvoice(rhash lntypes.Hash,
|
||||
amt lnwire.MilliSatoshi) error {
|
||||
func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
|
||||
return i.registry.SettleHodlInvoice(preimage)
|
||||
}
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
|
||||
amt lnwire.MilliSatoshi, hodlChan chan<- interface{}) (
|
||||
*invoices.HodlEvent, error) {
|
||||
|
||||
invoice, ok := i.invoices[rhash]
|
||||
if !ok {
|
||||
return fmt.Errorf("can't find mock invoice: %x", rhash[:])
|
||||
event, err := i.registry.NotifyExitHopHtlc(rhash, amt, hodlChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i.settleChan != nil {
|
||||
i.settleChan <- rhash
|
||||
}
|
||||
|
||||
if invoice.Terms.State == channeldb.ContractSettled {
|
||||
return nil
|
||||
}
|
||||
|
||||
invoice.Terms.State = channeldb.ContractSettled
|
||||
invoice.AmtPaid = amt
|
||||
i.invoices[rhash] = invoice
|
||||
|
||||
return nil
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (i *mockInvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
invoice, ok := i.invoices[payHash]
|
||||
if !ok {
|
||||
return channeldb.ErrInvoiceNotFound
|
||||
}
|
||||
|
||||
if invoice.Terms.State == channeldb.ContractCanceled {
|
||||
return nil
|
||||
}
|
||||
|
||||
invoice.Terms.State = channeldb.ContractCanceled
|
||||
i.invoices[payHash] = invoice
|
||||
|
||||
return nil
|
||||
return i.registry.CancelInvoice(payHash)
|
||||
}
|
||||
|
||||
func (i *mockInvoiceRegistry) AddInvoice(invoice channeldb.Invoice) error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
func (i *mockInvoiceRegistry) AddInvoice(invoice channeldb.Invoice,
|
||||
paymentHash lntypes.Hash) error {
|
||||
|
||||
rhash := invoice.Terms.PaymentPreimage.Hash()
|
||||
i.invoices[rhash] = invoice
|
||||
_, err := i.registry.AddInvoice(&invoice, paymentHash)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (i *mockInvoiceRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {
|
||||
i.registry.HodlUnsubscribeAll(subscriber)
|
||||
}
|
||||
|
||||
var _ InvoiceDatabase = (*mockInvoiceRegistry)(nil)
|
||||
|
@ -520,19 +520,12 @@ func getChanID(msg lnwire.Message) (lnwire.ChannelID, error) {
|
||||
return chanID, nil
|
||||
}
|
||||
|
||||
// generatePayment generates the htlc add request by given path blob and
|
||||
// generateHoldPayment generates the htlc add request by given path blob and
|
||||
// invoice which should be added by destination peer.
|
||||
func generatePayment(invoiceAmt, htlcAmt lnwire.MilliSatoshi, timelock uint32,
|
||||
blob [lnwire.OnionPacketSize]byte) (*channeldb.Invoice, *lnwire.UpdateAddHTLC, error) {
|
||||
|
||||
var preimage [sha256.Size]byte
|
||||
r, err := generateRandomBytes(sha256.Size)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
copy(preimage[:], r)
|
||||
|
||||
rhash := fastsha256.Sum256(preimage[:])
|
||||
func generatePaymentWithPreimage(invoiceAmt, htlcAmt lnwire.MilliSatoshi,
|
||||
timelock uint32, blob [lnwire.OnionPacketSize]byte,
|
||||
preimage, rhash [32]byte) (*channeldb.Invoice, *lnwire.UpdateAddHTLC,
|
||||
error) {
|
||||
|
||||
invoice := &channeldb.Invoice{
|
||||
CreationDate: time.Now(),
|
||||
@ -552,6 +545,24 @@ func generatePayment(invoiceAmt, htlcAmt lnwire.MilliSatoshi, timelock uint32,
|
||||
return invoice, htlc, nil
|
||||
}
|
||||
|
||||
// generatePayment generates the htlc add request by given path blob and
|
||||
// invoice which should be added by destination peer.
|
||||
func generatePayment(invoiceAmt, htlcAmt lnwire.MilliSatoshi, timelock uint32,
|
||||
blob [lnwire.OnionPacketSize]byte) (*channeldb.Invoice, *lnwire.UpdateAddHTLC, error) {
|
||||
|
||||
var preimage [sha256.Size]byte
|
||||
r, err := generateRandomBytes(sha256.Size)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
copy(preimage[:], r)
|
||||
|
||||
rhash := fastsha256.Sum256(preimage[:])
|
||||
return generatePaymentWithPreimage(
|
||||
invoiceAmt, htlcAmt, timelock, blob, preimage, rhash,
|
||||
)
|
||||
}
|
||||
|
||||
// generateRoute generates the path blob by given array of peers.
|
||||
func generateRoute(hops ...ForwardingInfo) ([lnwire.OnionPacketSize]byte, error) {
|
||||
var blob [lnwire.OnionPacketSize]byte
|
||||
@ -742,7 +753,8 @@ func preparePayment(sendingPeer, receivingPeer lnpeer.Peer,
|
||||
}
|
||||
|
||||
// Check who is last in the route and add invoice to server registry.
|
||||
if err := receiver.registry.AddInvoice(*invoice); err != nil {
|
||||
hash := invoice.Terms.PaymentPreimage.Hash()
|
||||
if err := receiver.registry.AddInvoice(*invoice, hash); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@ -971,7 +983,6 @@ type hopNetwork struct {
|
||||
feeEstimator *mockFeeEstimator
|
||||
globalPolicy ForwardingPolicy
|
||||
obfuscator ErrorEncrypter
|
||||
pCache *mockPreimageCache
|
||||
|
||||
defaultDelta uint32
|
||||
}
|
||||
@ -979,8 +990,6 @@ type hopNetwork struct {
|
||||
func newHopNetwork() *hopNetwork {
|
||||
defaultDelta := uint32(6)
|
||||
|
||||
pCache := newMockPreimageCache()
|
||||
|
||||
globalPolicy := ForwardingPolicy{
|
||||
MinHTLC: lnwire.NewMSatFromSatoshis(5),
|
||||
BaseFee: lnwire.NewMSatFromSatoshis(1),
|
||||
@ -997,7 +1006,6 @@ func newHopNetwork() *hopNetwork {
|
||||
feeEstimator: feeEstimator,
|
||||
globalPolicy: globalPolicy,
|
||||
obfuscator: obfuscator,
|
||||
pCache: pCache,
|
||||
defaultDelta: defaultDelta,
|
||||
}
|
||||
}
|
||||
@ -1028,7 +1036,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
|
||||
FetchLastChannelUpdate: mockGetChanUpdateMessage,
|
||||
Registry: server.registry,
|
||||
FeeEstimator: h.feeEstimator,
|
||||
PreimageCache: h.pCache,
|
||||
PreimageCache: server.pCache,
|
||||
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
|
||||
return nil
|
||||
},
|
||||
@ -1136,7 +1144,7 @@ func newTwoHopNetwork(t testing.TB,
|
||||
}
|
||||
}
|
||||
|
||||
// start starts the three hop network alice,bob,carol servers.
|
||||
// start starts the two hop network alice,bob servers.
|
||||
func (n *twoHopNetwork) start() error {
|
||||
if err := n.aliceServer.Start(); err != nil {
|
||||
return err
|
||||
@ -1166,3 +1174,48 @@ func (n *twoHopNetwork) stop() {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func (n *twoHopNetwork) makeHoldPayment(sendingPeer, receivingPeer lnpeer.Peer,
|
||||
firstHop lnwire.ShortChannelID, hops []ForwardingInfo,
|
||||
invoiceAmt, htlcAmt lnwire.MilliSatoshi,
|
||||
timelock uint32, preimage lntypes.Preimage) chan error {
|
||||
|
||||
paymentErr := make(chan error, 1)
|
||||
|
||||
sender := sendingPeer.(*mockServer)
|
||||
receiver := receivingPeer.(*mockServer)
|
||||
|
||||
// Generate route convert it to blob, and return next destination for
|
||||
// htlc add request.
|
||||
blob, err := generateRoute(hops...)
|
||||
if err != nil {
|
||||
paymentErr <- err
|
||||
return paymentErr
|
||||
}
|
||||
|
||||
rhash := preimage.Hash()
|
||||
|
||||
// Generate payment: invoice and htlc.
|
||||
invoice, htlc, err := generatePaymentWithPreimage(invoiceAmt, htlcAmt, timelock, blob,
|
||||
channeldb.UnknownPreimage, rhash)
|
||||
if err != nil {
|
||||
paymentErr <- err
|
||||
return paymentErr
|
||||
}
|
||||
|
||||
// Check who is last in the route and add invoice to server registry.
|
||||
if err := receiver.registry.AddInvoice(*invoice, rhash); err != nil {
|
||||
paymentErr <- err
|
||||
return paymentErr
|
||||
}
|
||||
|
||||
// Send payment and expose err channel.
|
||||
go func() {
|
||||
_, err := sender.htlcSwitch.SendHTLC(
|
||||
firstHop, htlc, newMockDeobfuscator(),
|
||||
)
|
||||
paymentErr <- err
|
||||
}()
|
||||
|
||||
return paymentErr
|
||||
}
|
||||
|
@ -7,14 +7,12 @@ import (
|
||||
"sync/atomic"
|
||||
"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/queue"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -29,6 +27,14 @@ var (
|
||||
DebugHash = DebugPre.Hash()
|
||||
)
|
||||
|
||||
// HodlEvent describes how an htlc should be resolved. If HodlEvent.Preimage is
|
||||
// set, the event indicates a settle event. If Preimage is nil, it is a cancel
|
||||
// event.
|
||||
type HodlEvent struct {
|
||||
Preimage *lntypes.Preimage
|
||||
Hash lntypes.Hash
|
||||
}
|
||||
|
||||
// InvoiceRegistry is a central registry of all the outstanding invoices
|
||||
// created by the daemon. The registry is a thin wrapper around a map in order
|
||||
// to ensure that all updates/reads are thread safe.
|
||||
@ -52,7 +58,17 @@ type InvoiceRegistry struct {
|
||||
// that *all* nodes are able to fully settle.
|
||||
debugInvoices map[lntypes.Hash]*channeldb.Invoice
|
||||
|
||||
activeNetParams *chaincfg.Params
|
||||
// decodeFinalCltvExpiry is a function used to decode the final expiry
|
||||
// value from the payment request.
|
||||
decodeFinalCltvExpiry func(invoice string) (uint32, error)
|
||||
|
||||
// subscriptions is a map from a payment hash to a list of subscribers.
|
||||
// It is used for efficient notification of links.
|
||||
hodlSubscriptions map[lntypes.Hash]map[chan<- interface{}]struct{}
|
||||
|
||||
// reverseSubscriptions tracks hashes subscribed to per subscriber. This
|
||||
// is used to unsubscribe from all hashes efficiently.
|
||||
hodlReverseSubscriptions map[chan<- interface{}]map[lntypes.Hash]struct{}
|
||||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
@ -62,8 +78,8 @@ type InvoiceRegistry struct {
|
||||
// wraps the persistent on-disk invoice storage with an additional in-memory
|
||||
// layer. The in-memory layer is in place such that debug invoices can be added
|
||||
// which are volatile yet available system wide within the daemon.
|
||||
func NewRegistry(cdb *channeldb.DB,
|
||||
activeNetParams *chaincfg.Params) *InvoiceRegistry {
|
||||
func NewRegistry(cdb *channeldb.DB, decodeFinalCltvExpiry func(invoice string) (
|
||||
uint32, error)) *InvoiceRegistry {
|
||||
|
||||
return &InvoiceRegistry{
|
||||
cdb: cdb,
|
||||
@ -74,7 +90,9 @@ func NewRegistry(cdb *channeldb.DB,
|
||||
newSingleSubscriptions: make(chan *SingleInvoiceSubscription),
|
||||
subscriptionCancels: make(chan uint32),
|
||||
invoiceEvents: make(chan *invoiceEvent, 100),
|
||||
activeNetParams: activeNetParams,
|
||||
hodlSubscriptions: make(map[lntypes.Hash]map[chan<- interface{}]struct{}),
|
||||
hodlReverseSubscriptions: make(map[chan<- interface{}]map[lntypes.Hash]struct{}),
|
||||
decodeFinalCltvExpiry: decodeFinalCltvExpiry,
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@ -163,8 +181,10 @@ func (i *InvoiceRegistry) invoiceEventNotifier() {
|
||||
// dispatch notifications to all registered clients.
|
||||
case event := <-i.invoiceEvents:
|
||||
// For backwards compatibility, do not notify all
|
||||
// invoice subscribers of cancel events
|
||||
if event.state != channeldb.ContractCanceled {
|
||||
// invoice subscribers of cancel and accept events.
|
||||
if event.state != channeldb.ContractCanceled &&
|
||||
event.state != channeldb.ContractAccepted {
|
||||
|
||||
i.dispatchToClients(event)
|
||||
}
|
||||
i.dispatchToSingleClients(event)
|
||||
@ -380,7 +400,8 @@ func (i *InvoiceRegistry) AddDebugInvoice(amt btcutil.Amount,
|
||||
// daemon add/forward HTLCs are able to obtain the proper preimage required for
|
||||
// redemption in the case that we're the final destination. We also return the
|
||||
// addIndex of the newly created invoice which monotonically increases for each
|
||||
// new invoice added.
|
||||
// new invoice added. A side effect of this function is that it also sets
|
||||
// AddIndex on the invoice argument.
|
||||
func (i *InvoiceRegistry) AddInvoice(invoice *channeldb.Invoice,
|
||||
paymentHash lntypes.Hash) (uint64, error) {
|
||||
|
||||
@ -391,7 +412,7 @@ func (i *InvoiceRegistry) AddInvoice(invoice *channeldb.Invoice,
|
||||
return spew.Sdump(invoice)
|
||||
}))
|
||||
|
||||
addIndex, err := i.cdb.AddInvoice(invoice)
|
||||
addIndex, err := i.cdb.AddInvoice(invoice, paymentHash)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -429,53 +450,110 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
|
||||
return channeldb.Invoice{}, 0, err
|
||||
}
|
||||
|
||||
payReq, err := zpay32.Decode(
|
||||
string(invoice.PaymentRequest), i.activeNetParams,
|
||||
)
|
||||
expiry, err := i.decodeFinalCltvExpiry(string(invoice.PaymentRequest))
|
||||
if err != nil {
|
||||
return channeldb.Invoice{}, 0, err
|
||||
}
|
||||
|
||||
return invoice, uint32(payReq.MinFinalCLTVExpiry()), nil
|
||||
return invoice, expiry, nil
|
||||
}
|
||||
|
||||
// SettleInvoice attempts to mark an invoice as settled. If the invoice is a
|
||||
// NotifyExitHopHtlc attempts to mark an invoice as settled. If the invoice is a
|
||||
// debug invoice, then this method is a noop as debug invoices are never fully
|
||||
// settled.
|
||||
func (i *InvoiceRegistry) SettleInvoice(rHash lntypes.Hash,
|
||||
amtPaid lnwire.MilliSatoshi) error {
|
||||
// settled. The return value describes how the htlc should be resolved.
|
||||
//
|
||||
// When the preimage of the invoice is not yet known (hodl invoice), this
|
||||
// function moves the invoice to the accepted state. When SettleHoldInvoice is
|
||||
// called later, a resolution message will be send back to the caller via the
|
||||
// provided hodlChan. Invoice registry sends on this channel what action needs
|
||||
// to be taken on the htlc (settle or cancel). The caller needs to ensure that
|
||||
// the channel is either buffered or received on from another goroutine to
|
||||
// prevent deadlock.
|
||||
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||
amtPaid lnwire.MilliSatoshi, hodlChan chan<- interface{}) (
|
||||
*HodlEvent, error) {
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
log.Debugf("Settling invoice %x", rHash[:])
|
||||
|
||||
createEvent := func(preimage *lntypes.Preimage) *HodlEvent {
|
||||
return &HodlEvent{
|
||||
Hash: rHash,
|
||||
Preimage: preimage,
|
||||
}
|
||||
}
|
||||
|
||||
// First check the in-memory debug invoice index to see if this is an
|
||||
// existing invoice added for debugging.
|
||||
if _, ok := i.debugInvoices[rHash]; ok {
|
||||
// Debug invoices are never fully settled, so we simply return
|
||||
// immediately in this case.
|
||||
return nil
|
||||
if invoice, ok := i.debugInvoices[rHash]; ok {
|
||||
// Debug invoices are never fully settled, so we just settle the
|
||||
// htlc in this case.
|
||||
return createEvent(&invoice.Terms.PaymentPreimage), nil
|
||||
}
|
||||
|
||||
// If this isn't a debug invoice, then we'll attempt to settle an
|
||||
// invoice matching this rHash on disk (if one exists).
|
||||
invoice, err := i.cdb.SettleInvoice(rHash, amtPaid)
|
||||
invoice, err := i.cdb.AcceptOrSettleInvoice(rHash, amtPaid)
|
||||
switch err {
|
||||
|
||||
// Implement idempotency by returning success if the invoice was already
|
||||
// settled.
|
||||
if err == channeldb.ErrInvoiceAlreadySettled {
|
||||
log.Debugf("Invoice %v already settled", rHash)
|
||||
return nil
|
||||
// If invoice is already settled, settle htlc. This means we accept more
|
||||
// payments to the same invoice hash.
|
||||
case channeldb.ErrInvoiceAlreadySettled:
|
||||
return createEvent(&invoice.Terms.PaymentPreimage), nil
|
||||
|
||||
// If invoice is already canceled, cancel htlc.
|
||||
case channeldb.ErrInvoiceAlreadyCanceled:
|
||||
return createEvent(nil), nil
|
||||
|
||||
// If invoice is already accepted, add this htlc to the list of
|
||||
// subscribers.
|
||||
case channeldb.ErrInvoiceAlreadyAccepted:
|
||||
i.hodlSubscribe(hodlChan, rHash)
|
||||
return nil, nil
|
||||
|
||||
// If this call settled the invoice, settle the htlc. Otherwise
|
||||
// subscribe for a future hodl event.
|
||||
case nil:
|
||||
i.notifyClients(rHash, invoice, invoice.Terms.State)
|
||||
switch invoice.Terms.State {
|
||||
case channeldb.ContractSettled:
|
||||
return createEvent(&invoice.Terms.PaymentPreimage), nil
|
||||
case channeldb.ContractAccepted:
|
||||
// Subscribe to updates to this invoice.
|
||||
i.hodlSubscribe(hodlChan, rHash)
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected invoice state %v",
|
||||
invoice.Terms.State)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// SettleHodlInvoice sets the preimage of a hodl invoice.
|
||||
func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
invoice, err := i.cdb.SettleHoldInvoice(preimage)
|
||||
if err != nil {
|
||||
log.Errorf("Invoice SetPreimage %v: %v", preimage, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Payment received: %v", spew.Sdump(invoice))
|
||||
hash := preimage.Hash()
|
||||
log.Infof("Notifying clients of set preimage to %v",
|
||||
invoice.Terms.PaymentPreimage)
|
||||
|
||||
i.notifyClients(rHash, invoice, channeldb.ContractSettled)
|
||||
i.notifyHodlSubscribers(HodlEvent{
|
||||
Hash: hash,
|
||||
Preimage: &preimage,
|
||||
})
|
||||
i.notifyClients(hash, invoice, invoice.Terms.State)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -496,13 +574,14 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
|
||||
log.Debugf("Invoice %v already canceled", payHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Invoice %v canceled", payHash)
|
||||
|
||||
i.notifyHodlSubscribers(HodlEvent{
|
||||
Hash: payHash,
|
||||
})
|
||||
i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
|
||||
|
||||
return nil
|
||||
@ -754,3 +833,60 @@ func (i *InvoiceRegistry) SubscribeSingleInvoice(
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// notifyHodlSubscribers sends out the hodl event to all current subscribers.
|
||||
func (i *InvoiceRegistry) notifyHodlSubscribers(hodlEvent HodlEvent) {
|
||||
subscribers, ok := i.hodlSubscriptions[hodlEvent.Hash]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all interested subscribers and remove subscription from both
|
||||
// maps. The subscription can be removed as there only ever will be a
|
||||
// single resolution for each hash.
|
||||
for subscriber := range subscribers {
|
||||
select {
|
||||
case subscriber <- hodlEvent:
|
||||
case <-i.quit:
|
||||
return
|
||||
}
|
||||
|
||||
delete(i.hodlReverseSubscriptions[subscriber], hodlEvent.Hash)
|
||||
}
|
||||
|
||||
delete(i.hodlSubscriptions, hodlEvent.Hash)
|
||||
}
|
||||
|
||||
// hodlSubscribe adds a new invoice subscription.
|
||||
func (i *InvoiceRegistry) hodlSubscribe(subscriber chan<- interface{},
|
||||
hash lntypes.Hash) {
|
||||
|
||||
log.Debugf("Hodl subscribe for %v", hash)
|
||||
|
||||
subscriptions, ok := i.hodlSubscriptions[hash]
|
||||
if !ok {
|
||||
subscriptions = make(map[chan<- interface{}]struct{})
|
||||
i.hodlSubscriptions[hash] = subscriptions
|
||||
}
|
||||
subscriptions[subscriber] = struct{}{}
|
||||
|
||||
reverseSubscriptions, ok := i.hodlReverseSubscriptions[subscriber]
|
||||
if !ok {
|
||||
reverseSubscriptions = make(map[lntypes.Hash]struct{})
|
||||
i.hodlReverseSubscriptions[subscriber] = reverseSubscriptions
|
||||
}
|
||||
reverseSubscriptions[hash] = struct{}{}
|
||||
}
|
||||
|
||||
// HodlUnsubscribeAll cancels the subscription.
|
||||
func (i *InvoiceRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
hashes := i.hodlReverseSubscriptions[subscriber]
|
||||
for hash := range hashes {
|
||||
delete(i.hodlSubscriptions[hash], subscriber)
|
||||
}
|
||||
|
||||
delete(i.hodlReverseSubscriptions, subscriber)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -28,22 +29,49 @@ var (
|
||||
testPayReq = "lnbc500u1pwywxzwpp5nd2u9xzq02t0tuf2654as7vma42lwkcjptx4yzfq0umq4swpa7cqdqqcqzysmlpc9ewnydr8rr8dnltyxphdyf6mcqrsd6dml8zajtyhwe6a45d807kxtmzayuf0hh2d9tn478ecxkecdg7c5g85pntupug5kakm7xcpn63zqk"
|
||||
)
|
||||
|
||||
// TestSettleInvoice tests settling of an invoice and related notifications.
|
||||
func TestSettleInvoice(t *testing.T) {
|
||||
func decodeExpiry(payReq string) (uint32, error) {
|
||||
invoice, err := zpay32.Decode(payReq, &chaincfg.MainNetParams)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint32(invoice.MinFinalCLTVExpiry()), nil
|
||||
}
|
||||
|
||||
var (
|
||||
testInvoice = &channeldb.Invoice{
|
||||
Terms: channeldb.ContractTerm{
|
||||
PaymentPreimage: preimage,
|
||||
Value: lnwire.MilliSatoshi(100000),
|
||||
},
|
||||
PaymentRequest: []byte(testPayReq),
|
||||
}
|
||||
)
|
||||
|
||||
func newTestContext(t *testing.T) (*InvoiceRegistry, func()) {
|
||||
cdb, cleanup, err := newDB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Instantiate and start the invoice registry.
|
||||
registry := NewRegistry(cdb, &chaincfg.MainNetParams)
|
||||
registry := NewRegistry(cdb, decodeExpiry)
|
||||
|
||||
err = registry.Start()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registry.Stop()
|
||||
|
||||
return registry, func() {
|
||||
registry.Stop()
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// TestSettleInvoice tests settling of an invoice and related notifications.
|
||||
func TestSettleInvoice(t *testing.T) {
|
||||
registry, cleanup := newTestContext(t)
|
||||
defer cleanup()
|
||||
|
||||
allSubscriptions := registry.SubscribeNotifications(0, 0)
|
||||
defer allSubscriptions.Cancel()
|
||||
@ -57,15 +85,7 @@ func TestSettleInvoice(t *testing.T) {
|
||||
}
|
||||
|
||||
// Add the invoice.
|
||||
invoice := &channeldb.Invoice{
|
||||
Terms: channeldb.ContractTerm{
|
||||
PaymentPreimage: preimage,
|
||||
Value: lnwire.MilliSatoshi(100000),
|
||||
},
|
||||
PaymentRequest: []byte(testPayReq),
|
||||
}
|
||||
|
||||
addIdx, err := registry.AddInvoice(invoice, hash)
|
||||
addIdx, err := registry.AddInvoice(testInvoice, hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -97,9 +117,11 @@ func TestSettleInvoice(t *testing.T) {
|
||||
t.Fatal("no update received")
|
||||
}
|
||||
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
|
||||
// Settle invoice with a slightly higher amount.
|
||||
amtPaid := lnwire.MilliSatoshi(100500)
|
||||
err = registry.SettleInvoice(hash, amtPaid)
|
||||
_, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -131,13 +153,13 @@ func TestSettleInvoice(t *testing.T) {
|
||||
}
|
||||
|
||||
// Try to settle again.
|
||||
err = registry.SettleInvoice(hash, amtPaid)
|
||||
_, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
if err != nil {
|
||||
t.Fatal("expected duplicate settle to succeed")
|
||||
}
|
||||
|
||||
// Try to settle again with a different amount.
|
||||
err = registry.SettleInvoice(hash, amtPaid+600)
|
||||
_, err = registry.NotifyExitHopHtlc(hash, amtPaid+600, hodlChan)
|
||||
if err != nil {
|
||||
t.Fatal("expected duplicate settle to succeed")
|
||||
}
|
||||
@ -156,30 +178,25 @@ func TestSettleInvoice(t *testing.T) {
|
||||
if err != channeldb.ErrInvoiceAlreadySettled {
|
||||
t.Fatal("expected cancelation of a settled invoice to fail")
|
||||
}
|
||||
|
||||
// As this is a direct sette, we expect nothing on the hodl chan.
|
||||
select {
|
||||
case <-hodlChan:
|
||||
t.Fatal("unexpected event")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// TestCancelInvoice tests cancelation of an invoice and related notifications.
|
||||
func TestCancelInvoice(t *testing.T) {
|
||||
cdb, cleanup, err := newDB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
registry, cleanup := newTestContext(t)
|
||||
defer cleanup()
|
||||
|
||||
// Instantiate and start the invoice registry.
|
||||
registry := NewRegistry(cdb, &chaincfg.MainNetParams)
|
||||
|
||||
err = registry.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registry.Stop()
|
||||
|
||||
allSubscriptions := registry.SubscribeNotifications(0, 0)
|
||||
defer allSubscriptions.Cancel()
|
||||
|
||||
// Try to cancel the not yet existing invoice. This should fail.
|
||||
err = registry.CancelInvoice(hash)
|
||||
err := registry.CancelInvoice(hash)
|
||||
if err != channeldb.ErrInvoiceNotFound {
|
||||
t.Fatalf("expected ErrInvoiceNotFound, but got %v", err)
|
||||
}
|
||||
@ -194,14 +211,7 @@ func TestCancelInvoice(t *testing.T) {
|
||||
|
||||
// Add the invoice.
|
||||
amt := lnwire.MilliSatoshi(100000)
|
||||
invoice := &channeldb.Invoice{
|
||||
Terms: channeldb.ContractTerm{
|
||||
PaymentPreimage: preimage,
|
||||
Value: amt,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = registry.AddInvoice(invoice, hash)
|
||||
_, err = registry.AddInvoice(testInvoice, hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -261,10 +271,144 @@ func TestCancelInvoice(t *testing.T) {
|
||||
t.Fatal("expected cancelation of a canceled invoice to succeed")
|
||||
}
|
||||
|
||||
// Try to settle. This should not be possible.
|
||||
err = registry.SettleInvoice(hash, amt)
|
||||
if err != channeldb.ErrInvoiceAlreadyCanceled {
|
||||
t.Fatal("expected settlement of a canceled invoice to fail")
|
||||
// Notify arrival of a new htlc paying to this invoice. This should
|
||||
// succeed.
|
||||
hodlChan := make(chan interface{})
|
||||
event, err := registry.NotifyExitHopHtlc(hash, amt, hodlChan)
|
||||
if err != nil {
|
||||
t.Fatal("expected settlement of a canceled invoice to succeed")
|
||||
}
|
||||
|
||||
if event.Preimage != nil {
|
||||
t.Fatal("expected cancel hodl event")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHoldInvoice tests settling of a hold invoice and related notifications.
|
||||
func TestHoldInvoice(t *testing.T) {
|
||||
defer timeout(t)()
|
||||
|
||||
cdb, cleanup, err := newDB()
|
||||
defer cleanup()
|
||||
|
||||
// Instantiate and start the invoice registry.
|
||||
registry := NewRegistry(cdb, decodeExpiry)
|
||||
|
||||
err = registry.Start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registry.Stop()
|
||||
|
||||
allSubscriptions := registry.SubscribeNotifications(0, 0)
|
||||
defer allSubscriptions.Cancel()
|
||||
|
||||
// Subscribe to the not yet existing invoice.
|
||||
subscription := registry.SubscribeSingleInvoice(hash)
|
||||
defer subscription.Cancel()
|
||||
|
||||
if subscription.hash != hash {
|
||||
t.Fatalf("expected subscription for provided hash")
|
||||
}
|
||||
|
||||
// Add the invoice.
|
||||
invoice := &channeldb.Invoice{
|
||||
Terms: channeldb.ContractTerm{
|
||||
PaymentPreimage: channeldb.UnknownPreimage,
|
||||
Value: lnwire.MilliSatoshi(100000),
|
||||
},
|
||||
}
|
||||
|
||||
_, err = registry.AddInvoice(invoice, hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We expect the open state to be sent to the single invoice subscriber.
|
||||
update := <-subscription.Updates
|
||||
if update.Terms.State != channeldb.ContractOpen {
|
||||
t.Fatalf("expected state ContractOpen, but got %v",
|
||||
update.Terms.State)
|
||||
}
|
||||
|
||||
// We expect a new invoice notification to be sent out.
|
||||
newInvoice := <-allSubscriptions.NewInvoices
|
||||
if newInvoice.Terms.State != channeldb.ContractOpen {
|
||||
t.Fatalf("expected state ContractOpen, but got %v",
|
||||
newInvoice.Terms.State)
|
||||
}
|
||||
|
||||
// Use slightly higher amount for accept/settle.
|
||||
amtPaid := lnwire.MilliSatoshi(100500)
|
||||
|
||||
hodlChan := make(chan interface{}, 1)
|
||||
|
||||
// NotifyExitHopHtlc without a preimage present in the invoice registry
|
||||
// should be possible.
|
||||
event, err := registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
}
|
||||
if event != nil {
|
||||
t.Fatalf("unexpect direct settle")
|
||||
}
|
||||
|
||||
// Test idempotency.
|
||||
event, err = registry.NotifyExitHopHtlc(hash, amtPaid, hodlChan)
|
||||
if err != nil {
|
||||
t.Fatalf("expected settle to succeed but got %v", err)
|
||||
}
|
||||
if event != nil {
|
||||
t.Fatalf("unexpect direct settle")
|
||||
}
|
||||
|
||||
// We expect the accepted state to be sent to the single invoice
|
||||
// subscriber. For all invoice subscribers, we don't expect an update.
|
||||
// Those only get notified on settle.
|
||||
update = <-subscription.Updates
|
||||
if update.Terms.State != channeldb.ContractAccepted {
|
||||
t.Fatalf("expected state ContractAccepted, but got %v",
|
||||
update.Terms.State)
|
||||
}
|
||||
if update.AmtPaid != amtPaid {
|
||||
t.Fatal("invoice AmtPaid incorrect")
|
||||
}
|
||||
|
||||
// Settling with preimage should succeed.
|
||||
err = registry.SettleHodlInvoice(preimage)
|
||||
if err != nil {
|
||||
t.Fatal("expected set preimage to succeed")
|
||||
}
|
||||
|
||||
hodlEvent := (<-hodlChan).(HodlEvent)
|
||||
if *hodlEvent.Preimage != preimage {
|
||||
t.Fatal("unexpected preimage in hodl event")
|
||||
}
|
||||
|
||||
// We expect a settled notification to be sent out for both all and
|
||||
// single invoice subscribers.
|
||||
settledInvoice := <-allSubscriptions.SettledInvoices
|
||||
if settledInvoice.Terms.State != channeldb.ContractSettled {
|
||||
t.Fatalf("expected state ContractSettled, but got %v",
|
||||
settledInvoice.Terms.State)
|
||||
}
|
||||
|
||||
update = <-subscription.Updates
|
||||
if update.Terms.State != channeldb.ContractSettled {
|
||||
t.Fatalf("expected state ContractSettled, but got %v",
|
||||
update.Terms.State)
|
||||
}
|
||||
|
||||
// Idempotency.
|
||||
err = registry.SettleHodlInvoice(preimage)
|
||||
if err != channeldb.ErrInvoiceAlreadySettled {
|
||||
t.Fatalf("expected ErrInvoiceAlreadySettled but got %v", err)
|
||||
}
|
||||
|
||||
// Try to cancel.
|
||||
err = registry.CancelInvoice(hash)
|
||||
if err == nil {
|
||||
t.Fatal("expected cancelation of a settled invoice to fail")
|
||||
}
|
||||
}
|
||||
|
||||
|
26
invoices/utils_test.go
Normal file
26
invoices/utils_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package invoices
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// timeout implements a test level timeout.
|
||||
func timeout(t *testing.T) func() {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||
|
||||
panic("test timeout")
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
close(done)
|
||||
}
|
||||
}
|
416
lnrpc/invoicesrpc/addinvoice.go
Normal file
416
lnrpc/invoicesrpc/addinvoice.go
Normal file
@ -0,0 +1,416 @@
|
||||
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 %v",
|
||||
len(invoice.DescriptionHash), channeldb.MaxPaymentRequestSize)
|
||||
}
|
||||
|
||||
// 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),
|
||||
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
|
||||
}
|
@ -4,8 +4,11 @@ package invoicesrpc
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/netann"
|
||||
)
|
||||
|
||||
// Config is the primary configuration struct for the invoices RPC server. It
|
||||
@ -26,7 +29,25 @@ type Config struct {
|
||||
// created by the daemon.
|
||||
InvoiceRegistry *invoices.InvoiceRegistry
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ var _ = math.Inf
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type CancelInvoiceMsg struct {
|
||||
// / Hash corresponding to the invoice to cancel.
|
||||
// / Hash corresponding to the (hold) invoice to cancel.
|
||||
PaymentHash []byte `protobuf:"bytes,1,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
@ -37,7 +37,7 @@ func (m *CancelInvoiceMsg) Reset() { *m = CancelInvoiceMsg{} }
|
||||
func (m *CancelInvoiceMsg) String() string { return proto.CompactTextString(m) }
|
||||
func (*CancelInvoiceMsg) ProtoMessage() {}
|
||||
func (*CancelInvoiceMsg) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_invoices_1b708c9c030aea0e, []int{0}
|
||||
return fileDescriptor_invoices_faecc7e411e82f9d, []int{0}
|
||||
}
|
||||
func (m *CancelInvoiceMsg) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_CancelInvoiceMsg.Unmarshal(m, b)
|
||||
@ -74,7 +74,7 @@ func (m *CancelInvoiceResp) Reset() { *m = CancelInvoiceResp{} }
|
||||
func (m *CancelInvoiceResp) String() string { return proto.CompactTextString(m) }
|
||||
func (*CancelInvoiceResp) ProtoMessage() {}
|
||||
func (*CancelInvoiceResp) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_invoices_1b708c9c030aea0e, []int{1}
|
||||
return fileDescriptor_invoices_faecc7e411e82f9d, []int{1}
|
||||
}
|
||||
func (m *CancelInvoiceResp) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_CancelInvoiceResp.Unmarshal(m, b)
|
||||
@ -94,9 +94,244 @@ func (m *CancelInvoiceResp) XXX_DiscardUnknown() {
|
||||
|
||||
var xxx_messageInfo_CancelInvoiceResp proto.InternalMessageInfo
|
||||
|
||||
type AddHoldInvoiceRequest 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 `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"`
|
||||
// / The hash of the preimage
|
||||
Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"`
|
||||
// / The value of this invoice in satoshis
|
||||
Value int64 `protobuf:"varint,3,opt,name=value,proto3" json:"value,omitempty"`
|
||||
// *
|
||||
// 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 `protobuf:"bytes,4,opt,name=description_hash,proto3" json:"description_hash,omitempty"`
|
||||
// / Payment request expiry time in seconds. Default is 3600 (1 hour).
|
||||
Expiry int64 `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"`
|
||||
// / Fallback on-chain address.
|
||||
FallbackAddr string `protobuf:"bytes,6,opt,name=fallback_addr,proto3" json:"fallback_addr,omitempty"`
|
||||
// / Delta to use for the time-lock of the CLTV extended to the final hop.
|
||||
CltvExpiry uint64 `protobuf:"varint,7,opt,name=cltv_expiry,proto3" json:"cltv_expiry,omitempty"`
|
||||
// *
|
||||
// Route hints that can each be individually used to assist in reaching the
|
||||
// invoice's destination.
|
||||
RouteHints []*lnrpc.RouteHint `protobuf:"bytes,8,rep,name=route_hints,proto3" json:"route_hints,omitempty"`
|
||||
// / Whether this invoice should include routing hints for private channels.
|
||||
Private bool `protobuf:"varint,9,opt,name=private,proto3" json:"private,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) Reset() { *m = AddHoldInvoiceRequest{} }
|
||||
func (m *AddHoldInvoiceRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*AddHoldInvoiceRequest) ProtoMessage() {}
|
||||
func (*AddHoldInvoiceRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_invoices_faecc7e411e82f9d, []int{2}
|
||||
}
|
||||
func (m *AddHoldInvoiceRequest) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_AddHoldInvoiceRequest.Unmarshal(m, b)
|
||||
}
|
||||
func (m *AddHoldInvoiceRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_AddHoldInvoiceRequest.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *AddHoldInvoiceRequest) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_AddHoldInvoiceRequest.Merge(dst, src)
|
||||
}
|
||||
func (m *AddHoldInvoiceRequest) XXX_Size() int {
|
||||
return xxx_messageInfo_AddHoldInvoiceRequest.Size(m)
|
||||
}
|
||||
func (m *AddHoldInvoiceRequest) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_AddHoldInvoiceRequest.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_AddHoldInvoiceRequest proto.InternalMessageInfo
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetMemo() string {
|
||||
if m != nil {
|
||||
return m.Memo
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetHash() []byte {
|
||||
if m != nil {
|
||||
return m.Hash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetValue() int64 {
|
||||
if m != nil {
|
||||
return m.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetDescriptionHash() []byte {
|
||||
if m != nil {
|
||||
return m.DescriptionHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetExpiry() int64 {
|
||||
if m != nil {
|
||||
return m.Expiry
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetFallbackAddr() string {
|
||||
if m != nil {
|
||||
return m.FallbackAddr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetCltvExpiry() uint64 {
|
||||
if m != nil {
|
||||
return m.CltvExpiry
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetRouteHints() []*lnrpc.RouteHint {
|
||||
if m != nil {
|
||||
return m.RouteHints
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceRequest) GetPrivate() bool {
|
||||
if m != nil {
|
||||
return m.Private
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type AddHoldInvoiceResp struct {
|
||||
// *
|
||||
// A bare-bones invoice for a payment within the Lightning Network. With the
|
||||
// details of the invoice, the sender has all the data necessary to send a
|
||||
// payment to the recipient.
|
||||
PaymentRequest string `protobuf:"bytes,1,opt,name=payment_request,proto3" json:"payment_request,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *AddHoldInvoiceResp) Reset() { *m = AddHoldInvoiceResp{} }
|
||||
func (m *AddHoldInvoiceResp) String() string { return proto.CompactTextString(m) }
|
||||
func (*AddHoldInvoiceResp) ProtoMessage() {}
|
||||
func (*AddHoldInvoiceResp) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_invoices_faecc7e411e82f9d, []int{3}
|
||||
}
|
||||
func (m *AddHoldInvoiceResp) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_AddHoldInvoiceResp.Unmarshal(m, b)
|
||||
}
|
||||
func (m *AddHoldInvoiceResp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_AddHoldInvoiceResp.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *AddHoldInvoiceResp) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_AddHoldInvoiceResp.Merge(dst, src)
|
||||
}
|
||||
func (m *AddHoldInvoiceResp) XXX_Size() int {
|
||||
return xxx_messageInfo_AddHoldInvoiceResp.Size(m)
|
||||
}
|
||||
func (m *AddHoldInvoiceResp) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_AddHoldInvoiceResp.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_AddHoldInvoiceResp proto.InternalMessageInfo
|
||||
|
||||
func (m *AddHoldInvoiceResp) GetPaymentRequest() string {
|
||||
if m != nil {
|
||||
return m.PaymentRequest
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SettleInvoiceMsg struct {
|
||||
// / Externally discovered pre-image that should be used to settle the hold invoice.
|
||||
Preimage []byte `protobuf:"bytes,1,opt,name=preimage,proto3" json:"preimage,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *SettleInvoiceMsg) Reset() { *m = SettleInvoiceMsg{} }
|
||||
func (m *SettleInvoiceMsg) String() string { return proto.CompactTextString(m) }
|
||||
func (*SettleInvoiceMsg) ProtoMessage() {}
|
||||
func (*SettleInvoiceMsg) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_invoices_faecc7e411e82f9d, []int{4}
|
||||
}
|
||||
func (m *SettleInvoiceMsg) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SettleInvoiceMsg.Unmarshal(m, b)
|
||||
}
|
||||
func (m *SettleInvoiceMsg) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_SettleInvoiceMsg.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *SettleInvoiceMsg) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_SettleInvoiceMsg.Merge(dst, src)
|
||||
}
|
||||
func (m *SettleInvoiceMsg) XXX_Size() int {
|
||||
return xxx_messageInfo_SettleInvoiceMsg.Size(m)
|
||||
}
|
||||
func (m *SettleInvoiceMsg) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_SettleInvoiceMsg.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_SettleInvoiceMsg proto.InternalMessageInfo
|
||||
|
||||
func (m *SettleInvoiceMsg) GetPreimage() []byte {
|
||||
if m != nil {
|
||||
return m.Preimage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SettleInvoiceResp struct {
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *SettleInvoiceResp) Reset() { *m = SettleInvoiceResp{} }
|
||||
func (m *SettleInvoiceResp) String() string { return proto.CompactTextString(m) }
|
||||
func (*SettleInvoiceResp) ProtoMessage() {}
|
||||
func (*SettleInvoiceResp) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_invoices_faecc7e411e82f9d, []int{5}
|
||||
}
|
||||
func (m *SettleInvoiceResp) XXX_Unmarshal(b []byte) error {
|
||||
return xxx_messageInfo_SettleInvoiceResp.Unmarshal(m, b)
|
||||
}
|
||||
func (m *SettleInvoiceResp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
return xxx_messageInfo_SettleInvoiceResp.Marshal(b, m, deterministic)
|
||||
}
|
||||
func (dst *SettleInvoiceResp) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_SettleInvoiceResp.Merge(dst, src)
|
||||
}
|
||||
func (m *SettleInvoiceResp) XXX_Size() int {
|
||||
return xxx_messageInfo_SettleInvoiceResp.Size(m)
|
||||
}
|
||||
func (m *SettleInvoiceResp) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_SettleInvoiceResp.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_SettleInvoiceResp proto.InternalMessageInfo
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*CancelInvoiceMsg)(nil), "invoicesrpc.CancelInvoiceMsg")
|
||||
proto.RegisterType((*CancelInvoiceResp)(nil), "invoicesrpc.CancelInvoiceResp")
|
||||
proto.RegisterType((*AddHoldInvoiceRequest)(nil), "invoicesrpc.AddHoldInvoiceRequest")
|
||||
proto.RegisterType((*AddHoldInvoiceResp)(nil), "invoicesrpc.AddHoldInvoiceResp")
|
||||
proto.RegisterType((*SettleInvoiceMsg)(nil), "invoicesrpc.SettleInvoiceMsg")
|
||||
proto.RegisterType((*SettleInvoiceResp)(nil), "invoicesrpc.SettleInvoiceResp")
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
@ -121,6 +356,14 @@ type InvoicesClient interface {
|
||||
// canceled, this call will succeed. If the invoice is already settled, it will
|
||||
// fail.
|
||||
CancelInvoice(ctx context.Context, in *CancelInvoiceMsg, opts ...grpc.CallOption) (*CancelInvoiceResp, error)
|
||||
// *
|
||||
// AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
|
||||
// supplied in the request.
|
||||
AddHoldInvoice(ctx context.Context, in *AddHoldInvoiceRequest, opts ...grpc.CallOption) (*AddHoldInvoiceResp, error)
|
||||
// *
|
||||
// SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
// settled, this call will succeed.
|
||||
SettleInvoice(ctx context.Context, in *SettleInvoiceMsg, opts ...grpc.CallOption) (*SettleInvoiceResp, error)
|
||||
}
|
||||
|
||||
type invoicesClient struct {
|
||||
@ -172,6 +415,24 @@ func (c *invoicesClient) CancelInvoice(ctx context.Context, in *CancelInvoiceMsg
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *invoicesClient) AddHoldInvoice(ctx context.Context, in *AddHoldInvoiceRequest, opts ...grpc.CallOption) (*AddHoldInvoiceResp, error) {
|
||||
out := new(AddHoldInvoiceResp)
|
||||
err := c.cc.Invoke(ctx, "/invoicesrpc.Invoices/AddHoldInvoice", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *invoicesClient) SettleInvoice(ctx context.Context, in *SettleInvoiceMsg, opts ...grpc.CallOption) (*SettleInvoiceResp, error) {
|
||||
out := new(SettleInvoiceResp)
|
||||
err := c.cc.Invoke(ctx, "/invoicesrpc.Invoices/SettleInvoice", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// InvoicesServer is the server API for Invoices service.
|
||||
type InvoicesServer interface {
|
||||
// *
|
||||
@ -184,6 +445,14 @@ type InvoicesServer interface {
|
||||
// canceled, this call will succeed. If the invoice is already settled, it will
|
||||
// fail.
|
||||
CancelInvoice(context.Context, *CancelInvoiceMsg) (*CancelInvoiceResp, error)
|
||||
// *
|
||||
// AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
|
||||
// supplied in the request.
|
||||
AddHoldInvoice(context.Context, *AddHoldInvoiceRequest) (*AddHoldInvoiceResp, error)
|
||||
// *
|
||||
// SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
// settled, this call will succeed.
|
||||
SettleInvoice(context.Context, *SettleInvoiceMsg) (*SettleInvoiceResp, error)
|
||||
}
|
||||
|
||||
func RegisterInvoicesServer(s *grpc.Server, srv InvoicesServer) {
|
||||
@ -229,6 +498,42 @@ func _Invoices_CancelInvoice_Handler(srv interface{}, ctx context.Context, dec f
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Invoices_AddHoldInvoice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AddHoldInvoiceRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InvoicesServer).AddHoldInvoice(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/invoicesrpc.Invoices/AddHoldInvoice",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InvoicesServer).AddHoldInvoice(ctx, req.(*AddHoldInvoiceRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Invoices_SettleInvoice_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SettleInvoiceMsg)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(InvoicesServer).SettleInvoice(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/invoicesrpc.Invoices/SettleInvoice",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(InvoicesServer).SettleInvoice(ctx, req.(*SettleInvoiceMsg))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _Invoices_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "invoicesrpc.Invoices",
|
||||
HandlerType: (*InvoicesServer)(nil),
|
||||
@ -237,6 +542,14 @@ var _Invoices_serviceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CancelInvoice",
|
||||
Handler: _Invoices_CancelInvoice_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddHoldInvoice",
|
||||
Handler: _Invoices_AddHoldInvoice_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SettleInvoice",
|
||||
Handler: _Invoices_SettleInvoice_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
@ -249,25 +562,40 @@ var _Invoices_serviceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("invoicesrpc/invoices.proto", fileDescriptor_invoices_1b708c9c030aea0e)
|
||||
proto.RegisterFile("invoicesrpc/invoices.proto", fileDescriptor_invoices_faecc7e411e82f9d)
|
||||
}
|
||||
|
||||
var fileDescriptor_invoices_1b708c9c030aea0e = []byte{
|
||||
// 246 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x90, 0xbf, 0x4b, 0x43, 0x31,
|
||||
0x10, 0xc7, 0x79, 0x8b, 0x68, 0x5a, 0x45, 0x23, 0x88, 0x04, 0x15, 0xed, 0xe4, 0x94, 0xa8, 0xc5,
|
||||
0xd5, 0x41, 0x17, 0x1d, 0x14, 0x69, 0x37, 0x17, 0xc9, 0x8b, 0x21, 0x39, 0x4c, 0xef, 0x42, 0x92,
|
||||
0x2a, 0xfe, 0x2b, 0xfe, 0xb5, 0xd2, 0x36, 0xe2, 0x7b, 0x42, 0xb7, 0xcb, 0x7d, 0x7f, 0xe4, 0x93,
|
||||
0x30, 0x01, 0xf8, 0x41, 0x60, 0x6c, 0x4e, 0xd1, 0xa8, 0xdf, 0x59, 0xc6, 0x44, 0x85, 0xf8, 0xa0,
|
||||
0xa3, 0x89, 0x23, 0x47, 0xe4, 0x82, 0x55, 0x3a, 0x82, 0xd2, 0x88, 0x54, 0x74, 0x01, 0xc2, 0x6a,
|
||||
0x15, 0x5b, 0x29, 0x9a, 0xd5, 0x38, 0xba, 0x66, 0xbb, 0x77, 0x1a, 0x8d, 0x0d, 0x0f, 0xab, 0xf4,
|
||||
0x63, 0x76, 0xfc, 0x8c, 0x0d, 0xa3, 0xfe, 0x9a, 0x59, 0x2c, 0xaf, 0x5e, 0x67, 0x7f, 0xd8, 0x9c,
|
||||
0x36, 0xe7, 0xc3, 0xc9, 0xa0, 0xee, 0xee, 0x75, 0xf6, 0xa3, 0x7d, 0xb6, 0xd7, 0x8b, 0x4d, 0x6c,
|
||||
0x8e, 0x57, 0xdf, 0x0d, 0xdb, 0xac, 0xe7, 0xcc, 0x6f, 0xd8, 0xc1, 0x74, 0xde, 0x66, 0x93, 0xa0,
|
||||
0xb5, 0x53, 0x40, 0x17, 0x6c, 0x95, 0x38, 0x97, 0x01, 0x17, 0x00, 0xcf, 0x7f, 0x7d, 0x62, 0xa7,
|
||||
0xee, 0xaa, 0xe7, 0xa2, 0xe1, 0x4f, 0x6c, 0xbb, 0x77, 0x03, 0x3f, 0x96, 0x9d, 0x07, 0xca, 0xff,
|
||||
0xd0, 0xe2, 0x64, 0xbd, 0xbc, 0x80, 0xbb, 0x1d, 0xbf, 0x5c, 0x3a, 0x28, 0x7e, 0xde, 0x4a, 0x43,
|
||||
0x33, 0x15, 0xc0, 0xf9, 0x82, 0x80, 0x0e, 0x6d, 0xf9, 0xa4, 0xf4, 0xae, 0x02, 0xbe, 0xa9, 0x25,
|
||||
0x82, 0xea, 0xd4, 0xb4, 0x1b, 0xcb, 0x4f, 0x1a, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0xd2, 0xc3,
|
||||
0x7e, 0x3a, 0x78, 0x01, 0x00, 0x00,
|
||||
var fileDescriptor_invoices_faecc7e411e82f9d = []byte{
|
||||
// 490 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0x4f, 0x8f, 0xd3, 0x3c,
|
||||
0x10, 0xc6, 0x95, 0xb6, 0xdb, 0x6d, 0xa7, 0xbb, 0xfb, 0xf6, 0x35, 0xb0, 0x8a, 0x22, 0xfe, 0x84,
|
||||
0x88, 0x43, 0xc4, 0x21, 0x81, 0xae, 0xb8, 0xae, 0x04, 0x5c, 0xca, 0x01, 0x84, 0x52, 0x71, 0xe1,
|
||||
0x52, 0xb9, 0x89, 0x49, 0xac, 0x75, 0x6d, 0x63, 0xbb, 0x85, 0xfd, 0x54, 0x7c, 0x06, 0xbe, 0x19,
|
||||
0x8a, 0xe3, 0x96, 0x24, 0x0b, 0xdc, 0x66, 0x1e, 0xcf, 0x3c, 0x19, 0xff, 0x3c, 0x81, 0x80, 0xf2,
|
||||
0xbd, 0xa0, 0x39, 0xd1, 0x4a, 0xe6, 0xe9, 0x21, 0x4e, 0xa4, 0x12, 0x46, 0xa0, 0x59, 0xeb, 0x2c,
|
||||
0x78, 0x58, 0x0a, 0x51, 0x32, 0x92, 0x62, 0x49, 0x53, 0xcc, 0xb9, 0x30, 0xd8, 0x50, 0xc1, 0x5d,
|
||||
0x69, 0x30, 0x55, 0x32, 0x6f, 0xc2, 0xe8, 0x15, 0xcc, 0xdf, 0x62, 0x9e, 0x13, 0xf6, 0xae, 0xe9,
|
||||
0x7e, 0xaf, 0x4b, 0xf4, 0x14, 0xce, 0x24, 0xbe, 0xdd, 0x12, 0x6e, 0xd6, 0x15, 0xd6, 0x95, 0xef,
|
||||
0x85, 0x5e, 0x7c, 0x96, 0xcd, 0x9c, 0xb6, 0xc4, 0xba, 0x8a, 0xee, 0xc1, 0xff, 0x9d, 0xb6, 0x8c,
|
||||
0x68, 0x19, 0xfd, 0x18, 0xc0, 0x83, 0xd7, 0x45, 0xb1, 0x14, 0xac, 0x38, 0xca, 0x5f, 0x77, 0x44,
|
||||
0x1b, 0x84, 0x60, 0xb4, 0x25, 0x5b, 0x61, 0x9d, 0xa6, 0x99, 0x8d, 0x6b, 0xcd, 0xba, 0x0f, 0xac,
|
||||
0xbb, 0x8d, 0xd1, 0x7d, 0x38, 0xd9, 0x63, 0xb6, 0x23, 0xfe, 0x30, 0xf4, 0xe2, 0x61, 0xd6, 0x24,
|
||||
0xe8, 0x39, 0xcc, 0x0b, 0xa2, 0x73, 0x45, 0x65, 0x7d, 0x89, 0x66, 0xa6, 0x91, 0xed, 0xba, 0xa3,
|
||||
0xa3, 0x4b, 0x18, 0x93, 0xef, 0x92, 0xaa, 0x5b, 0xff, 0xc4, 0x5a, 0xb8, 0x0c, 0x3d, 0x83, 0xf3,
|
||||
0x2f, 0x98, 0xb1, 0x0d, 0xce, 0x6f, 0xd6, 0xb8, 0x28, 0x94, 0x3f, 0xb6, 0xa3, 0x74, 0x45, 0x14,
|
||||
0xc2, 0x2c, 0x67, 0x66, 0xbf, 0x76, 0x16, 0xa7, 0xa1, 0x17, 0x8f, 0xb2, 0xb6, 0x84, 0x16, 0x30,
|
||||
0x53, 0x62, 0x67, 0xc8, 0xba, 0xa2, 0xdc, 0x68, 0x7f, 0x12, 0x0e, 0xe3, 0xd9, 0x62, 0x9e, 0x30,
|
||||
0x5e, 0x23, 0xcd, 0xea, 0x93, 0x25, 0xe5, 0x26, 0x6b, 0x17, 0x21, 0x1f, 0x4e, 0xa5, 0xa2, 0x7b,
|
||||
0x6c, 0x88, 0x3f, 0x0d, 0xbd, 0x78, 0x92, 0x1d, 0xd2, 0xe8, 0x1a, 0x50, 0x1f, 0x98, 0x96, 0x28,
|
||||
0x86, 0xff, 0x0e, 0xfc, 0x55, 0x03, 0xd0, 0x81, 0xeb, 0xcb, 0x51, 0x02, 0xf3, 0x15, 0x31, 0x86,
|
||||
0x91, 0xd6, 0xeb, 0x05, 0x30, 0x91, 0x8a, 0xd0, 0x2d, 0x2e, 0x89, 0x7b, 0xb9, 0x63, 0x5e, 0x3f,
|
||||
0x5b, 0xa7, 0xbe, 0xfe, 0xdc, 0xe2, 0xe7, 0x00, 0x26, 0x2e, 0xd7, 0xe8, 0x1a, 0x2e, 0x57, 0xbb,
|
||||
0x4d, 0x0d, 0x75, 0x43, 0x56, 0x94, 0x97, 0xc7, 0x52, 0x84, 0xdc, 0x25, 0x3f, 0xfe, 0x5e, 0x83,
|
||||
0xe0, 0xc2, 0x69, 0xae, 0xe6, 0x85, 0x87, 0x3e, 0xc0, 0x79, 0x67, 0x31, 0xd0, 0xa3, 0xa4, 0xb5,
|
||||
0x97, 0x49, 0x7f, 0xd7, 0x82, 0xc7, 0x7f, 0x3f, 0xb6, 0x2c, 0x3e, 0xc1, 0x45, 0x97, 0x10, 0x8a,
|
||||
0x3a, 0x1d, 0x7f, 0xdc, 0xb7, 0xe0, 0xc9, 0x3f, 0x6b, 0xb4, 0xac, 0xc7, 0xec, 0x80, 0xe8, 0x8d,
|
||||
0xd9, 0x87, 0xda, 0x1b, 0xf3, 0x0e, 0xc3, 0x37, 0x57, 0x9f, 0x5f, 0x96, 0xd4, 0x54, 0xbb, 0x4d,
|
||||
0x92, 0x8b, 0x6d, 0xca, 0x68, 0x59, 0x19, 0x4e, 0x79, 0xc9, 0x89, 0xf9, 0x26, 0xd4, 0x4d, 0xca,
|
||||
0x78, 0x91, 0x5a, 0x52, 0x69, 0xcb, 0x66, 0x33, 0xb6, 0xbf, 0xe0, 0xd5, 0xaf, 0x00, 0x00, 0x00,
|
||||
0xff, 0xff, 0x89, 0x60, 0x5e, 0x11, 0xd6, 0x03, 0x00, 0x00,
|
||||
}
|
||||
|
@ -23,10 +23,79 @@ service Invoices {
|
||||
fail.
|
||||
*/
|
||||
rpc CancelInvoice(CancelInvoiceMsg) returns (CancelInvoiceResp);
|
||||
|
||||
/**
|
||||
AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
|
||||
supplied in the request.
|
||||
*/
|
||||
rpc AddHoldInvoice(AddHoldInvoiceRequest) returns (AddHoldInvoiceResp);
|
||||
|
||||
/**
|
||||
SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
settled, this call will succeed.
|
||||
*/
|
||||
rpc SettleInvoice(SettleInvoiceMsg) returns (SettleInvoiceResp);
|
||||
}
|
||||
|
||||
message CancelInvoiceMsg {
|
||||
/// Hash corresponding to the invoice to cancel.
|
||||
/// Hash corresponding to the (hold) invoice to cancel.
|
||||
bytes payment_hash = 1;
|
||||
}
|
||||
message CancelInvoiceResp {}
|
||||
|
||||
message AddHoldInvoiceRequest {
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
string memo = 1 [json_name = "memo"];
|
||||
|
||||
/// The hash of the preimage
|
||||
bytes hash = 2 [json_name = "hash"];
|
||||
|
||||
/// The value of this invoice in satoshis
|
||||
int64 value = 3 [json_name = "value"];
|
||||
|
||||
/**
|
||||
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.
|
||||
*/
|
||||
bytes description_hash = 4 [json_name = "description_hash"];
|
||||
|
||||
/// Payment request expiry time in seconds. Default is 3600 (1 hour).
|
||||
int64 expiry = 5 [json_name = "expiry"];
|
||||
|
||||
/// Fallback on-chain address.
|
||||
string fallback_addr = 6 [json_name = "fallback_addr"];
|
||||
|
||||
/// Delta to use for the time-lock of the CLTV extended to the final hop.
|
||||
uint64 cltv_expiry = 7 [json_name = "cltv_expiry"];
|
||||
|
||||
/**
|
||||
Route hints that can each be individually used to assist in reaching the
|
||||
invoice's destination.
|
||||
*/
|
||||
repeated lnrpc.RouteHint route_hints = 8 [json_name = "route_hints"];
|
||||
|
||||
/// Whether this invoice should include routing hints for private channels.
|
||||
bool private = 9 [json_name = "private"];
|
||||
}
|
||||
|
||||
message AddHoldInvoiceResp {
|
||||
/**
|
||||
A bare-bones invoice for a payment within the Lightning Network. With the
|
||||
details of the invoice, the sender has all the data necessary to send a
|
||||
payment to the recipient.
|
||||
*/
|
||||
string payment_request = 1 [json_name = "payment_request"];
|
||||
}
|
||||
|
||||
message SettleInvoiceMsg {
|
||||
/// Externally discovered pre-image that should be used to settle the hold invoice.
|
||||
bytes preimage = 1;
|
||||
}
|
||||
|
||||
message SettleInvoiceResp {}
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
@ -43,10 +45,18 @@ var (
|
||||
Entity: "invoices",
|
||||
Action: "read",
|
||||
}},
|
||||
"/invoicesrpc.Invoices/SettleInvoice": {{
|
||||
Entity: "invoices",
|
||||
Action: "write",
|
||||
}},
|
||||
"/invoicesrpc.Invoices/CancelInvoice": {{
|
||||
Entity: "invoices",
|
||||
Action: "write",
|
||||
}},
|
||||
"/invoicesrpc.Invoices/AddHoldInvoice": {{
|
||||
Entity: "invoices",
|
||||
Action: "write",
|
||||
}},
|
||||
}
|
||||
|
||||
// DefaultInvoicesMacFilename is the default name of the invoices
|
||||
@ -163,12 +173,12 @@ func (s *Server) RegisterWithRootServer(grpcServer *grpc.Server) error {
|
||||
func (s *Server) SubscribeSingleInvoice(req *lnrpc.PaymentHash,
|
||||
updateStream Invoices_SubscribeSingleInvoiceServer) error {
|
||||
|
||||
hash, err := lntypes.NewHash(req.RHash)
|
||||
hash, err := lntypes.MakeHash(req.RHash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
invoiceClient := s.cfg.InvoiceRegistry.SubscribeSingleInvoice(*hash)
|
||||
invoiceClient := s.cfg.InvoiceRegistry.SubscribeSingleInvoice(hash)
|
||||
defer invoiceClient.Cancel()
|
||||
|
||||
for {
|
||||
@ -191,18 +201,36 @@ func (s *Server) SubscribeSingleInvoice(req *lnrpc.PaymentHash,
|
||||
}
|
||||
}
|
||||
|
||||
// SettleInvoice settles an accepted invoice. If the invoice is already settled,
|
||||
// this call will succeed.
|
||||
func (s *Server) SettleInvoice(ctx context.Context,
|
||||
in *SettleInvoiceMsg) (*SettleInvoiceResp, error) {
|
||||
|
||||
preimage, err := lntypes.MakePreimage(in.Preimage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.cfg.InvoiceRegistry.SettleHodlInvoice(preimage)
|
||||
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SettleInvoiceResp{}, nil
|
||||
}
|
||||
|
||||
// CancelInvoice cancels a currently open invoice. If the invoice is already
|
||||
// canceled, this call will succeed. If the invoice is already settled, it will
|
||||
// fail.
|
||||
func (s *Server) CancelInvoice(ctx context.Context,
|
||||
in *CancelInvoiceMsg) (*CancelInvoiceResp, error) {
|
||||
|
||||
paymentHash, err := lntypes.NewHash(in.PaymentHash)
|
||||
paymentHash, err := lntypes.MakeHash(in.PaymentHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.cfg.InvoiceRegistry.CancelInvoice(*paymentHash)
|
||||
err = s.cfg.InvoiceRegistry.CancelInvoice(paymentHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -211,3 +239,45 @@ func (s *Server) CancelInvoice(ctx context.Context,
|
||||
|
||||
return &CancelInvoiceResp{}, nil
|
||||
}
|
||||
|
||||
// AddHoldInvoice attempts to add a new hold invoice to the invoice database.
|
||||
// Any duplicated invoices are rejected, therefore all invoices *must* have a
|
||||
// unique payment hash.
|
||||
func (s *Server) AddHoldInvoice(ctx context.Context,
|
||||
invoice *AddHoldInvoiceRequest) (*AddHoldInvoiceResp, error) {
|
||||
|
||||
addInvoiceCfg := &AddInvoiceConfig{
|
||||
AddInvoice: s.cfg.InvoiceRegistry.AddInvoice,
|
||||
IsChannelActive: s.cfg.IsChannelActive,
|
||||
ChainParams: s.cfg.ChainParams,
|
||||
NodeSigner: s.cfg.NodeSigner,
|
||||
MaxPaymentMSat: s.cfg.MaxPaymentMSat,
|
||||
DefaultCLTVExpiry: s.cfg.DefaultCLTVExpiry,
|
||||
ChanDB: s.cfg.ChanDB,
|
||||
}
|
||||
|
||||
hash, err := lntypes.MakeHash(invoice.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addInvoiceData := &AddInvoiceData{
|
||||
Memo: invoice.Memo,
|
||||
Hash: &hash,
|
||||
Value: btcutil.Amount(invoice.Value),
|
||||
DescriptionHash: invoice.DescriptionHash,
|
||||
Expiry: invoice.Expiry,
|
||||
FallbackAddr: invoice.FallbackAddr,
|
||||
CltvExpiry: invoice.CltvExpiry,
|
||||
Private: invoice.Private,
|
||||
}
|
||||
|
||||
_, dbInvoice, err := AddInvoice(ctx, addInvoiceCfg, addInvoiceData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AddHoldInvoiceResp{
|
||||
PaymentRequest: string(dbInvoice.PaymentRequest),
|
||||
}, nil
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
@ -61,16 +60,17 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
||||
state = lnrpc.Invoice_SETTLED
|
||||
case channeldb.ContractCanceled:
|
||||
state = lnrpc.Invoice_CANCELED
|
||||
case channeldb.ContractAccepted:
|
||||
state = lnrpc.Invoice_ACCEPTED
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown invoice state %v",
|
||||
invoice.Terms.State)
|
||||
}
|
||||
|
||||
return &lnrpc.Invoice{
|
||||
rpcInvoice := &lnrpc.Invoice{
|
||||
Memo: string(invoice.Memo[:]),
|
||||
Receipt: invoice.Receipt[:],
|
||||
RHash: decoded.PaymentHash[:],
|
||||
RPreimage: preimage[:],
|
||||
Value: int64(satAmt),
|
||||
CreationDate: invoice.CreationDate.Unix(),
|
||||
SettleDate: settleDate,
|
||||
@ -88,12 +88,18 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
||||
AmtPaidMsat: int64(invoice.AmtPaid),
|
||||
AmtPaid: int64(invoice.AmtPaid),
|
||||
State: state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if preimage != channeldb.UnknownPreimage {
|
||||
rpcInvoice.RPreimage = preimage[:]
|
||||
}
|
||||
|
||||
return rpcInvoice, nil
|
||||
}
|
||||
|
||||
// CreateRPCRouteHints takes in the decoded form of an invoice's route hints
|
||||
// and converts them into the lnrpc type.
|
||||
func CreateRPCRouteHints(routeHints [][]routing.HopHint) []*lnrpc.RouteHint {
|
||||
func CreateRPCRouteHints(routeHints [][]zpay32.HopHint) []*lnrpc.RouteHint {
|
||||
var res []*lnrpc.RouteHint
|
||||
|
||||
for _, route := range routeHints {
|
||||
|
1144
lnrpc/rpc.pb.go
1144
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@ -1892,6 +1892,7 @@ message Invoice {
|
||||
OPEN = 0;
|
||||
SETTLED = 1;
|
||||
CANCELED = 2;
|
||||
ACCEPTED = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1162,7 +1162,8 @@
|
||||
"enum": [
|
||||
"OPEN",
|
||||
"SETTLED",
|
||||
"CANCELED"
|
||||
"CANCELED",
|
||||
"ACCEPTED"
|
||||
],
|
||||
"default": "OPEN"
|
||||
},
|
||||
|
@ -17,33 +17,33 @@ func (hash Hash) String() string {
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// NewHash returns a new Hash from a byte slice. An error is returned if
|
||||
// MakeHash returns a new Hash from a byte slice. An error is returned if
|
||||
// the number of bytes passed in is not HashSize.
|
||||
func NewHash(newHash []byte) (*Hash, error) {
|
||||
func MakeHash(newHash []byte) (Hash, error) {
|
||||
nhlen := len(newHash)
|
||||
if nhlen != HashSize {
|
||||
return nil, fmt.Errorf("invalid hash length of %v, want %v",
|
||||
return Hash{}, fmt.Errorf("invalid hash length of %v, want %v",
|
||||
nhlen, HashSize)
|
||||
}
|
||||
|
||||
var hash Hash
|
||||
copy(hash[:], newHash)
|
||||
|
||||
return &hash, nil
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// NewHashFromStr creates a Hash from a hex hash string.
|
||||
func NewHashFromStr(newHash string) (*Hash, error) {
|
||||
// MakeHashFromStr creates a Hash from a hex hash string.
|
||||
func MakeHashFromStr(newHash string) (Hash, error) {
|
||||
// Return error if hash string is of incorrect length.
|
||||
if len(newHash) != HashSize*2 {
|
||||
return nil, fmt.Errorf("invalid hash string length of %v, "+
|
||||
return Hash{}, fmt.Errorf("invalid hash string length of %v, "+
|
||||
"want %v", len(newHash), HashSize*2)
|
||||
}
|
||||
|
||||
hash, err := hex.DecodeString(newHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Hash{}, err
|
||||
}
|
||||
|
||||
return NewHash(hash)
|
||||
return MakeHash(hash)
|
||||
}
|
||||
|
@ -53,3 +53,8 @@ func MakePreimageFromStr(newPreimage string) (Preimage, error) {
|
||||
func (p *Preimage) Hash() Hash {
|
||||
return Hash(sha256.Sum256(p[:]))
|
||||
}
|
||||
|
||||
// Matches returns whether this preimage is the preimage of the given hash.
|
||||
func (p *Preimage) Matches(h Hash) bool {
|
||||
return h == p.Hash()
|
||||
}
|
||||
|
@ -409,6 +409,11 @@ func (f FailFinalExpiryTooSoon) Error() string {
|
||||
return f.Code().String()
|
||||
}
|
||||
|
||||
// NewFinalExpiryTooSoon creates new instance of the FailFinalExpiryTooSoon.
|
||||
func NewFinalExpiryTooSoon() *FailFinalExpiryTooSoon {
|
||||
return &FailFinalExpiryTooSoon{}
|
||||
}
|
||||
|
||||
// FailInvalidOnionVersion is returned if the onion version byte is unknown.
|
||||
//
|
||||
// NOTE: May be returned only by intermediate nodes.
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/coreos/bbolt"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -152,7 +153,7 @@ func (m *missionControl) GraphPruneView() graphPruneView {
|
||||
// view from Mission Control. An optional set of routing hints can be provided
|
||||
// in order to populate additional edges to explore when finding a path to the
|
||||
// payment's destination.
|
||||
func (m *missionControl) NewPaymentSession(routeHints [][]HopHint,
|
||||
func (m *missionControl) NewPaymentSession(routeHints [][]zpay32.HopHint,
|
||||
target Vertex) (*paymentSession, error) {
|
||||
|
||||
viewSnapshot := m.GraphPruneView()
|
||||
|
@ -39,28 +39,6 @@ const (
|
||||
RiskFactorBillionths = 15
|
||||
)
|
||||
|
||||
// HopHint is a routing hint that contains the minimum information of a channel
|
||||
// required for an intermediate hop in a route to forward the payment to the
|
||||
// next. This should be ideally used for private channels, since they are not
|
||||
// publicly advertised to the network for routing.
|
||||
type HopHint struct {
|
||||
// NodeID is the public key of the node at the start of the channel.
|
||||
NodeID *btcec.PublicKey
|
||||
|
||||
// ChannelID is the unique identifier of the channel.
|
||||
ChannelID uint64
|
||||
|
||||
// FeeBaseMSat is the base fee of the channel in millisatoshis.
|
||||
FeeBaseMSat uint32
|
||||
|
||||
// FeeProportionalMillionths is the fee rate, in millionths of a
|
||||
// satoshi, for every satoshi sent through the channel.
|
||||
FeeProportionalMillionths uint32
|
||||
|
||||
// CLTVExpiryDelta is the time-lock delta of the channel.
|
||||
CLTVExpiryDelta uint16
|
||||
}
|
||||
|
||||
// Hop represents an intermediate or final node of the route. This naming
|
||||
// is in line with the definition given in BOLT #4: Onion Routing Protocol.
|
||||
// The struct houses the channel along which this hop can be reached and
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -1746,11 +1747,11 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
// The CLTV expiry should be the current height plus 9 (the expiry for
|
||||
// the B -> C channel.
|
||||
if firstRoute.TotalTimeLock !=
|
||||
startingHeight+DefaultFinalCLTVDelta {
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
firstRoute.TotalTimeLock,
|
||||
startingHeight+DefaultFinalCLTVDelta)
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
|
||||
// Next, we'll set A as the source node so we can assert that we create
|
||||
@ -1845,11 +1846,11 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
// The outgoing CLTV value itself should be the current height plus 30
|
||||
// to meet Carol's requirements.
|
||||
if routes[0].Hops[0].OutgoingTimeLock !=
|
||||
startingHeight+DefaultFinalCLTVDelta {
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
routes[0].Hops[0].OutgoingTimeLock,
|
||||
startingHeight+DefaultFinalCLTVDelta)
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
|
||||
// For B -> C, we assert that the final hop also has the proper
|
||||
@ -1860,11 +1861,11 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
lastHop.AmtToForward, amt)
|
||||
}
|
||||
if lastHop.OutgoingTimeLock !=
|
||||
startingHeight+DefaultFinalCLTVDelta {
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
lastHop.OutgoingTimeLock,
|
||||
startingHeight+DefaultFinalCLTVDelta)
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
|
||||
// We'll also make similar assertions for the second route from A to C
|
||||
@ -1875,7 +1876,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
t.Fatalf("wrong amount: got %v, expected %v",
|
||||
secondRoute.TotalAmount, expectedAmt)
|
||||
}
|
||||
expectedTimeLock := startingHeight + daveFinalCLTV + DefaultFinalCLTVDelta
|
||||
expectedTimeLock := startingHeight + daveFinalCLTV + zpay32.DefaultFinalCLTVDelta
|
||||
if secondRoute.TotalTimeLock != uint32(expectedTimeLock) {
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
secondRoute.TotalTimeLock, expectedTimeLock)
|
||||
@ -1885,7 +1886,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
t.Fatalf("wrong forward amount: got %v, expected %v",
|
||||
onionPayload.AmtToForward, amt)
|
||||
}
|
||||
expectedTimeLock = startingHeight + DefaultFinalCLTVDelta
|
||||
expectedTimeLock = startingHeight + zpay32.DefaultFinalCLTVDelta
|
||||
if onionPayload.OutgoingTimeLock != uint32(expectedTimeLock) {
|
||||
t.Fatalf("wrong outgoing time lock: got %v, expecting %v",
|
||||
onionPayload.OutgoingTimeLock,
|
||||
@ -1899,11 +1900,11 @@ func TestPathFindSpecExample(t *testing.T) {
|
||||
lastHop.AmtToForward, amt)
|
||||
}
|
||||
if lastHop.OutgoingTimeLock !=
|
||||
startingHeight+DefaultFinalCLTVDelta {
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta {
|
||||
|
||||
t.Fatalf("wrong total time lock: got %v, expecting %v",
|
||||
lastHop.OutgoingTimeLock,
|
||||
startingHeight+DefaultFinalCLTVDelta)
|
||||
startingHeight+zpay32.DefaultFinalCLTVDelta)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,13 +25,10 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/multimutex"
|
||||
"github.com/lightningnetwork/lnd/routing/chainview"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultFinalCLTVDelta is the default value to be used as the final
|
||||
// CLTV delta for a route if one is unspecified.
|
||||
DefaultFinalCLTVDelta = 9
|
||||
|
||||
// defaultPayAttemptTimeout is a duration that we'll use to determine
|
||||
// if we should give up on a payment attempt. This will be used if a
|
||||
// value isn't specified in the LightningNode struct.
|
||||
@ -1331,7 +1328,7 @@ func (r *ChannelRouter) FindRoutes(source, target Vertex,
|
||||
|
||||
var finalCLTVDelta uint16
|
||||
if len(finalExpiry) == 0 {
|
||||
finalCLTVDelta = DefaultFinalCLTVDelta
|
||||
finalCLTVDelta = zpay32.DefaultFinalCLTVDelta
|
||||
} else {
|
||||
finalCLTVDelta = finalExpiry[0]
|
||||
}
|
||||
@ -1551,7 +1548,7 @@ type LightningPayment struct {
|
||||
// multiple routes, ensure the hop hints within each route are chained
|
||||
// together and sorted in forward order in order to reach the
|
||||
// destination successfully.
|
||||
RouteHints [][]HopHint
|
||||
RouteHints [][]zpay32.HopHint
|
||||
|
||||
// OutgoingChannelID is the channel that needs to be taken to the first
|
||||
// hop. If nil, any channel may be used.
|
||||
@ -1628,7 +1625,7 @@ func (r *ChannelRouter) sendPayment(payment *LightningPayment,
|
||||
|
||||
var finalCLTVDelta uint16
|
||||
if payment.FinalCLTVDelta == nil {
|
||||
finalCLTVDelta = DefaultFinalCLTVDelta
|
||||
finalCLTVDelta = zpay32.DefaultFinalCLTVDelta
|
||||
} else {
|
||||
finalCLTVDelta = *payment.FinalCLTVDelta
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
// defaultNumRoutes is the default value for the maximum number of routes to
|
||||
@ -186,7 +187,7 @@ func TestFindRoutesFeeSorting(t *testing.T) {
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
target, paymentAmt, noRestrictions, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
@ -248,7 +249,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
target, paymentAmt, restrictions, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
@ -1345,7 +1346,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
routes, err := ctx.router.FindRoutes(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
@ -1391,7 +1392,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
||||
routes, err = ctx.router.FindRoutes(
|
||||
ctx.router.selfNode.PubKeyBytes,
|
||||
targetPubKeyBytes, paymentAmt, noRestrictions, defaultNumRoutes,
|
||||
DefaultFinalCLTVDelta,
|
||||
zpay32.DefaultFinalCLTVDelta,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find any routes: %v", err)
|
||||
|
333
rpcserver.go
333
rpcserver.go
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
@ -39,6 +38,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
@ -422,7 +422,8 @@ func newRPCServer(s *server, macService *macaroons.Service,
|
||||
// server configuration struct.
|
||||
err := subServerCgs.PopulateDependencies(
|
||||
s.cc, networkDir, macService, atpl, invoiceRegistry,
|
||||
activeNetParams.Params, s.chanRouter,
|
||||
s.htlcSwitch, activeNetParams.Params, s.chanRouter,
|
||||
s.nodeSigner, s.chanDB,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -2796,7 +2797,7 @@ type rpcPaymentIntent struct {
|
||||
dest routing.Vertex
|
||||
rHash [32]byte
|
||||
cltvDelta uint16
|
||||
routeHints [][]routing.HopHint
|
||||
routeHints [][]zpay32.HopHint
|
||||
outgoingChannelID *uint64
|
||||
|
||||
routes []*routing.Route
|
||||
@ -3277,309 +3278,51 @@ func (r *rpcServer) sendPaymentSync(ctx context.Context,
|
||||
func (r *rpcServer) AddInvoice(ctx context.Context,
|
||||
invoice *lnrpc.Invoice) (*lnrpc.AddInvoiceResponse, error) {
|
||||
|
||||
var paymentPreimage [32]byte
|
||||
defaultDelta := cfg.Bitcoin.TimeLockDelta
|
||||
if registeredChains.PrimaryChain() == litecoinChain {
|
||||
defaultDelta = cfg.Litecoin.TimeLockDelta
|
||||
}
|
||||
|
||||
switch {
|
||||
// If a preimage wasn't specified, then we'll generate a new preimage
|
||||
// from fresh cryptographic randomness.
|
||||
case len(invoice.RPreimage) == 0:
|
||||
if _, err := rand.Read(paymentPreimage[:]); err != nil {
|
||||
addInvoiceCfg := &invoicesrpc.AddInvoiceConfig{
|
||||
AddInvoice: r.server.invoices.AddInvoice,
|
||||
IsChannelActive: r.server.htlcSwitch.HasActiveLink,
|
||||
ChainParams: activeNetParams.Params,
|
||||
NodeSigner: r.server.nodeSigner,
|
||||
MaxPaymentMSat: maxPaymentMSat,
|
||||
DefaultCLTVExpiry: defaultDelta,
|
||||
ChanDB: r.server.chanDB,
|
||||
}
|
||||
|
||||
addInvoiceData := &invoicesrpc.AddInvoiceData{
|
||||
Memo: invoice.Memo,
|
||||
Receipt: invoice.Receipt,
|
||||
Value: btcutil.Amount(invoice.Value),
|
||||
DescriptionHash: invoice.DescriptionHash,
|
||||
Expiry: invoice.Expiry,
|
||||
FallbackAddr: invoice.FallbackAddr,
|
||||
CltvExpiry: invoice.CltvExpiry,
|
||||
Private: invoice.Private,
|
||||
}
|
||||
|
||||
if invoice.RPreimage != nil {
|
||||
preimage, err := lntypes.MakePreimage(invoice.RPreimage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Otherwise, if a preimage was specified, then it MUST be exactly
|
||||
// 32-bytes.
|
||||
case len(invoice.RPreimage) > 0 && len(invoice.RPreimage) != 32:
|
||||
return nil, fmt.Errorf("payment preimage must be exactly "+
|
||||
"32 bytes, is instead %v", len(invoice.RPreimage))
|
||||
|
||||
// If the preimage meets the size specifications, then it can be used
|
||||
// as is.
|
||||
default:
|
||||
copy(paymentPreimage[:], invoice.RPreimage[:])
|
||||
addInvoiceData.Preimage = &preimage
|
||||
}
|
||||
|
||||
// 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, fmt.Errorf("memo too large: %v bytes "+
|
||||
"(maxsize=%v)", len(invoice.Memo), channeldb.MaxMemoSize)
|
||||
}
|
||||
if len(invoice.Receipt) > channeldb.MaxReceiptSize {
|
||||
return 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, fmt.Errorf("description hash is %v bytes, must be %v",
|
||||
len(invoice.DescriptionHash), channeldb.MaxPaymentRequestSize)
|
||||
}
|
||||
|
||||
// The value of the invoice must not be negative.
|
||||
if invoice.Value < 0 {
|
||||
return nil, fmt.Errorf("payments of negative value "+
|
||||
"are not allowed, value is %v", invoice.Value)
|
||||
}
|
||||
|
||||
amt := btcutil.Amount(invoice.Value)
|
||||
amtMSat := lnwire.NewMSatFromSatoshis(amt)
|
||||
|
||||
// The value of the invoice must also not exceed the current soft-limit
|
||||
// on the largest payment within the network.
|
||||
if amtMSat > maxPaymentMSat {
|
||||
return nil, fmt.Errorf("payment of %v is too large, max "+
|
||||
"payment allowed is %v", amt, maxPaymentMSat.ToSatoshis())
|
||||
}
|
||||
|
||||
// Next, generate the payment hash itself from the preimage. This will
|
||||
// be used by clients to query for the state of a particular invoice.
|
||||
rHash := sha256.Sum256(paymentPreimage[:])
|
||||
|
||||
// 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,
|
||||
activeNetParams.Params)
|
||||
if err != nil {
|
||||
return 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, 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, 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.Bitcoin.TimeLockDelta
|
||||
if registeredChains.PrimaryChain() == litecoinChain {
|
||||
defaultDelta = cfg.Litecoin.TimeLockDelta
|
||||
}
|
||||
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 := r.server.chanDB.FetchAllChannels()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not fetch all channels")
|
||||
}
|
||||
|
||||
graph := r.server.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 {
|
||||
rpcsLog.Debugf("Skipping channel %v due to "+
|
||||
"not having enough remote balance",
|
||||
chanPoint)
|
||||
continue
|
||||
}
|
||||
|
||||
// Make sure the channel is active.
|
||||
link, err := r.server.htlcSwitch.GetLink(chanPoint)
|
||||
if err != nil {
|
||||
rpcsLog.Errorf("Unable to get link for "+
|
||||
"channel %v: %v", chanPoint, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !link.EligibleToForward() {
|
||||
rpcsLog.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 {
|
||||
rpcsLog.Errorf("Unable to determine if node %x "+
|
||||
"is advertised: %v", remotePub, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !isRemoteNodePublic {
|
||||
rpcsLog.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 {
|
||||
rpcsLog.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 := routing.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 := []routing.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(
|
||||
activeNetParams.Params, rHash, creationDate, options...,
|
||||
hash, dbInvoice, err := invoicesrpc.AddInvoice(
|
||||
ctx, addInvoiceCfg, addInvoiceData,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payReqString, err := payReq.Encode(
|
||||
zpay32.MessageSigner{
|
||||
SignCompact: r.server.nodeSigner.SignDigestCompact,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newInvoice := &channeldb.Invoice{
|
||||
CreationDate: creationDate,
|
||||
Memo: []byte(invoice.Memo),
|
||||
Receipt: invoice.Receipt,
|
||||
PaymentRequest: []byte(payReqString),
|
||||
Terms: channeldb.ContractTerm{
|
||||
Value: amtMSat,
|
||||
},
|
||||
}
|
||||
copy(newInvoice.Terms.PaymentPreimage[:], paymentPreimage[:])
|
||||
|
||||
rpcsLog.Tracef("[addinvoice] adding new invoice %v",
|
||||
newLogClosure(func() string {
|
||||
return spew.Sdump(newInvoice)
|
||||
}),
|
||||
)
|
||||
|
||||
// With all sanity checks passed, write the invoice to the database.
|
||||
addIndex, err := r.server.invoices.AddInvoice(newInvoice, rHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lnrpc.AddInvoiceResponse{
|
||||
RHash: rHash[:],
|
||||
PaymentRequest: payReqString,
|
||||
AddIndex: addIndex,
|
||||
AddIndex: dbInvoice.AddIndex,
|
||||
PaymentRequest: string(dbInvoice.PaymentRequest),
|
||||
RHash: hash[:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
13
server.go
13
server.go
@ -46,6 +46,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"github.com/lightningnetwork/lnd/ticker"
|
||||
"github.com/lightningnetwork/lnd/tor"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -284,6 +285,14 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
|
||||
readBufferPool, runtime.NumCPU(), pool.DefaultWorkerTimeout,
|
||||
)
|
||||
|
||||
decodeFinalCltvExpiry := func(payReq string) (uint32, error) {
|
||||
invoice, err := zpay32.Decode(payReq, activeNetParams.Params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint32(invoice.MinFinalCLTVExpiry()), nil
|
||||
}
|
||||
|
||||
s := &server{
|
||||
chanDB: chanDB,
|
||||
cc: cc,
|
||||
@ -291,7 +300,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
|
||||
writePool: writePool,
|
||||
readPool: readPool,
|
||||
|
||||
invoices: invoices.NewRegistry(chanDB, activeNetParams.Params),
|
||||
invoices: invoices.NewRegistry(chanDB, decodeFinalCltvExpiry),
|
||||
|
||||
channelNotifier: channelnotifier.New(chanDB),
|
||||
|
||||
@ -778,7 +787,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
|
||||
},
|
||||
DisableChannel: s.chanStatusMgr.RequestDisable,
|
||||
Sweeper: s.sweeper,
|
||||
SettleInvoice: s.invoices.SettleInvoice,
|
||||
Registry: s.invoices,
|
||||
NotifyClosedChannel: s.channelNotifier.NotifyClosedChannelEvent,
|
||||
}, chanDB)
|
||||
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd/autopilot"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||
@ -14,6 +16,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/netann"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
)
|
||||
|
||||
@ -64,8 +67,11 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
|
||||
networkDir string, macService *macaroons.Service,
|
||||
atpl *autopilot.Manager,
|
||||
invoiceRegistry *invoices.InvoiceRegistry,
|
||||
htlcSwitch *htlcswitch.Switch,
|
||||
activeNetParams *chaincfg.Params,
|
||||
chanRouter *routing.ChannelRouter) error {
|
||||
chanRouter *routing.ChannelRouter,
|
||||
nodeSigner *netann.NodeSigner,
|
||||
chanDB *channeldb.DB) error {
|
||||
|
||||
// First, we'll use reflect to obtain a version of the config struct
|
||||
// that allows us to programmatically inspect its fields.
|
||||
@ -90,9 +96,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
|
||||
continue
|
||||
}
|
||||
|
||||
switch cfg := field.Interface().(type) {
|
||||
switch subCfg := field.Interface().(type) {
|
||||
case *signrpc.Config:
|
||||
subCfgValue := extractReflectValue(cfg)
|
||||
subCfgValue := extractReflectValue(subCfg)
|
||||
|
||||
subCfgValue.FieldByName("MacService").Set(
|
||||
reflect.ValueOf(macService),
|
||||
@ -105,7 +111,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
|
||||
)
|
||||
|
||||
case *walletrpc.Config:
|
||||
subCfgValue := extractReflectValue(cfg)
|
||||
subCfgValue := extractReflectValue(subCfg)
|
||||
|
||||
subCfgValue.FieldByName("NetworkDir").Set(
|
||||
reflect.ValueOf(networkDir),
|
||||
@ -124,14 +130,14 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
|
||||
)
|
||||
|
||||
case *autopilotrpc.Config:
|
||||
subCfgValue := extractReflectValue(cfg)
|
||||
subCfgValue := extractReflectValue(subCfg)
|
||||
|
||||
subCfgValue.FieldByName("Manager").Set(
|
||||
reflect.ValueOf(atpl),
|
||||
)
|
||||
|
||||
case *chainrpc.Config:
|
||||
subCfgValue := extractReflectValue(cfg)
|
||||
subCfgValue := extractReflectValue(subCfg)
|
||||
|
||||
subCfgValue.FieldByName("NetworkDir").Set(
|
||||
reflect.ValueOf(networkDir),
|
||||
@ -144,7 +150,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
|
||||
)
|
||||
|
||||
case *invoicesrpc.Config:
|
||||
subCfgValue := extractReflectValue(cfg)
|
||||
subCfgValue := extractReflectValue(subCfg)
|
||||
|
||||
subCfgValue.FieldByName("NetworkDir").Set(
|
||||
reflect.ValueOf(networkDir),
|
||||
@ -155,9 +161,28 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
|
||||
subCfgValue.FieldByName("InvoiceRegistry").Set(
|
||||
reflect.ValueOf(invoiceRegistry),
|
||||
)
|
||||
subCfgValue.FieldByName("IsChannelActive").Set(
|
||||
reflect.ValueOf(htlcSwitch.HasActiveLink),
|
||||
)
|
||||
subCfgValue.FieldByName("ChainParams").Set(
|
||||
reflect.ValueOf(activeNetParams),
|
||||
)
|
||||
subCfgValue.FieldByName("NodeSigner").Set(
|
||||
reflect.ValueOf(nodeSigner),
|
||||
)
|
||||
subCfgValue.FieldByName("MaxPaymentMSat").Set(
|
||||
reflect.ValueOf(maxPaymentMSat),
|
||||
)
|
||||
defaultDelta := cfg.Bitcoin.TimeLockDelta
|
||||
if registeredChains.PrimaryChain() == litecoinChain {
|
||||
defaultDelta = cfg.Litecoin.TimeLockDelta
|
||||
}
|
||||
subCfgValue.FieldByName("DefaultCLTVExpiry").Set(
|
||||
reflect.ValueOf(defaultDelta),
|
||||
)
|
||||
subCfgValue.FieldByName("ChanDB").Set(
|
||||
reflect.ValueOf(chanDB),
|
||||
)
|
||||
|
||||
case *routerrpc.Config:
|
||||
subCfgValue := extractReflectValue(cfg)
|
||||
|
@ -85,7 +85,9 @@ func (p *preimageBeacon) LookupPreimage(
|
||||
|
||||
// If we've found the invoice, then we can return the preimage
|
||||
// directly.
|
||||
if err != channeldb.ErrInvoiceNotFound {
|
||||
if err != channeldb.ErrInvoiceNotFound &&
|
||||
invoice.Terms.PaymentPreimage != channeldb.UnknownPreimage {
|
||||
|
||||
return invoice.Terms.PaymentPreimage, true
|
||||
}
|
||||
|
||||
|
31
zpay32/hophint.go
Normal file
31
zpay32/hophint.go
Normal file
@ -0,0 +1,31 @@
|
||||
package zpay32
|
||||
|
||||
import "github.com/btcsuite/btcd/btcec"
|
||||
|
||||
const (
|
||||
// DefaultFinalCLTVDelta is the default value to be used as the final
|
||||
// CLTV delta for a route if one is unspecified.
|
||||
DefaultFinalCLTVDelta = 9
|
||||
)
|
||||
|
||||
// HopHint is a routing hint that contains the minimum information of a channel
|
||||
// required for an intermediate hop in a route to forward the payment to the
|
||||
// next. This should be ideally used for private channels, since they are not
|
||||
// publicly advertised to the network for routing.
|
||||
type HopHint struct {
|
||||
// NodeID is the public key of the node at the start of the channel.
|
||||
NodeID *btcec.PublicKey
|
||||
|
||||
// ChannelID is the unique identifier of the channel.
|
||||
ChannelID uint64
|
||||
|
||||
// FeeBaseMSat is the base fee of the channel in millisatoshis.
|
||||
FeeBaseMSat uint32
|
||||
|
||||
// FeeProportionalMillionths is the fee rate, in millionths of a
|
||||
// satoshi, for every satoshi sent through the channel.
|
||||
FeeProportionalMillionths uint32
|
||||
|
||||
// CLTVExpiryDelta is the time-lock delta of the channel.
|
||||
CLTVExpiryDelta uint16
|
||||
}
|
@ -13,7 +13,6 @@ import (
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -146,7 +145,7 @@ type Invoice struct {
|
||||
// represent private routes.
|
||||
//
|
||||
// NOTE: This is optional.
|
||||
RouteHints [][]routing.HopHint
|
||||
RouteHints [][]HopHint
|
||||
}
|
||||
|
||||
// Amount is a functional option that allows callers of NewInvoice to set the
|
||||
@ -214,7 +213,7 @@ func FallbackAddr(fallbackAddr btcutil.Address) func(*Invoice) {
|
||||
|
||||
// RouteHint is a functional option that allows callers of NewInvoice to add
|
||||
// one or more hop hints that represent a private route to the destination.
|
||||
func RouteHint(routeHint []routing.HopHint) func(*Invoice) {
|
||||
func RouteHint(routeHint []HopHint) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.RouteHints = append(i.RouteHints, routeHint)
|
||||
}
|
||||
@ -476,7 +475,7 @@ func (invoice *Invoice) MinFinalCLTVExpiry() uint64 {
|
||||
return *invoice.minFinalCLTVExpiry
|
||||
}
|
||||
|
||||
return routing.DefaultFinalCLTVDelta
|
||||
return DefaultFinalCLTVDelta
|
||||
}
|
||||
|
||||
// validateInvoice does a sanity check of the provided Invoice, making sure it
|
||||
@ -843,7 +842,7 @@ func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, erro
|
||||
|
||||
// parseRouteHint converts the data (encoded in base32) into an array containing
|
||||
// one or more routing hop hints that represent a single route hint.
|
||||
func parseRouteHint(data []byte) ([]routing.HopHint, error) {
|
||||
func parseRouteHint(data []byte) ([]HopHint, error) {
|
||||
base256Data, err := bech32.ConvertBits(data, 5, 8, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -854,10 +853,10 @@ func parseRouteHint(data []byte) ([]routing.HopHint, error) {
|
||||
"got %d", hopHintLen, len(base256Data))
|
||||
}
|
||||
|
||||
var routeHint []routing.HopHint
|
||||
var routeHint []HopHint
|
||||
|
||||
for len(base256Data) > 0 {
|
||||
hopHint := routing.HopHint{}
|
||||
hopHint := HopHint{}
|
||||
hopHint.NodeID, err = btcec.ParsePubKey(base256Data[:33], btcec.S256())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
)
|
||||
|
||||
// TestDecodeAmount ensures that the amount string in the hrp of the Invoice
|
||||
@ -738,7 +737,7 @@ func TestParseRouteHint(t *testing.T) {
|
||||
tests := []struct {
|
||||
data []byte
|
||||
valid bool
|
||||
result []routing.HopHint
|
||||
result []HopHint
|
||||
}{
|
||||
{
|
||||
data: []byte{0x0, 0x0, 0x0, 0x0},
|
||||
@ -747,7 +746,7 @@ func TestParseRouteHint(t *testing.T) {
|
||||
{
|
||||
data: []byte{},
|
||||
valid: true,
|
||||
result: []routing.HopHint{},
|
||||
result: []HopHint{},
|
||||
},
|
||||
{
|
||||
data: testSingleHopData,
|
||||
|
@ -17,7 +17,6 @@ import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
|
||||
litecoinCfg "github.com/ltcsuite/ltcd/chaincfg"
|
||||
)
|
||||
@ -53,7 +52,7 @@ var (
|
||||
testHopHintPubkeyBytes2, _ = hex.DecodeString("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
|
||||
testHopHintPubkey2, _ = btcec.ParsePubKey(testHopHintPubkeyBytes2, btcec.S256())
|
||||
|
||||
testSingleHop = []routing.HopHint{
|
||||
testSingleHop = []HopHint{
|
||||
{
|
||||
NodeID: testHopHintPubkey1,
|
||||
ChannelID: 0x0102030405060708,
|
||||
@ -62,7 +61,7 @@ var (
|
||||
CLTVExpiryDelta: 3,
|
||||
},
|
||||
}
|
||||
testDoubleHop = []routing.HopHint{
|
||||
testDoubleHop = []HopHint{
|
||||
{
|
||||
NodeID: testHopHintPubkey1,
|
||||
ChannelID: 0x0102030405060708,
|
||||
@ -414,7 +413,7 @@ func TestDecodeEncode(t *testing.T) {
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testRustyAddr,
|
||||
RouteHints: [][]routing.HopHint{testSingleHop},
|
||||
RouteHints: [][]HopHint{testSingleHop},
|
||||
}
|
||||
},
|
||||
beforeEncoding: func(i *Invoice) {
|
||||
@ -437,7 +436,7 @@ func TestDecodeEncode(t *testing.T) {
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testRustyAddr,
|
||||
RouteHints: [][]routing.HopHint{testDoubleHop},
|
||||
RouteHints: [][]HopHint{testDoubleHop},
|
||||
}
|
||||
},
|
||||
beforeEncoding: func(i *Invoice) {
|
||||
@ -844,7 +843,7 @@ func compareHashes(a, b *[32]byte) bool {
|
||||
return bytes.Equal(a[:], b[:])
|
||||
}
|
||||
|
||||
func compareRouteHints(a, b []routing.HopHint) error {
|
||||
func compareRouteHints(a, b []HopHint) error {
|
||||
if len(a) != len(b) {
|
||||
return fmt.Errorf("expected len routingInfo %d, got %d",
|
||||
len(a), len(b))
|
||||
|
Loading…
Reference in New Issue
Block a user