Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

channeldb+invoices: add spontaneous AMP receiving + sending via SendToRoute #5108

Merged
merged 22 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8fe4de8
record: convert child_index from uint16 to uint32
cfromknecht Mar 25, 2021
82b4f11
record/amp: use switch from 10 -> 14 for AMP TLV type
cfromknecht Mar 25, 2021
135a0a9
htlcswitch/hop: make unknown required type const
cfromknecht Mar 25, 2021
c2729cb
htlcswitch/hop: parse and validate AMP records
cfromknecht Mar 25, 2021
438b03a
features: define temporary AMP feature bits 30/31
cfromknecht Mar 25, 2021
be66984
channeldb/invoice: add InvoiceRefByAddr
cfromknecht Mar 25, 2021
174d577
channeldb: make payhash on InvoiceRef optional
cfromknecht Mar 25, 2021
7bed359
channeldb: refactor InvoiceRef.String() with all optional fields
cfromknecht Mar 25, 2021
7e2f5a1
channeldb: validate feature dependencies when adding invoice
cfromknecht Mar 25, 2021
3fb70dd
invoices: add checkSettleResolution and checkFailResolution
cfromknecht Mar 25, 2021
88b72ab
invoices: add processAMP
cfromknecht Mar 22, 2021
24d283e
channeldb/invoice_test: only set htlc-level preimage on accept
cfromknecht Mar 25, 2021
6780f74
channeldb/invoices: set AMP HTLC preimages when settling invoice
cfromknecht Mar 25, 2021
0b5be85
channeldb/invoice: make Copy() a member of InvoiceHTLC
cfromknecht Mar 25, 2021
2a49b59
channeldb/invoices: rigorously test updateHtlc for MPP/AMP scenarios
cfromknecht Mar 25, 2021
90a2550
invoices: reconstruct AMP child preimages
cfromknecht Mar 25, 2021
ea934e1
invoices: add TestSpontaneousAmpPayment
cfromknecht Mar 25, 2021
cfa9e95
channeldb/invoice: allow creating AMP invoices w/o preiamge
cfromknecht Mar 25, 2021
00581ef
lnrpc: add AMPRecord to Hop
cfromknecht Mar 25, 2021
352ce10
lnrpc: add UnmarshalAMP decoding
cfromknecht Mar 25, 2021
888af51
lntest: make buildRoute method on mppTestContext
cfromknecht Mar 25, 2021
730b718
lntest: add AMP itest
cfromknecht Mar 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
860 changes: 847 additions & 13 deletions channeldb/invoice_test.go

Large diffs are not rendered by default.

203 changes: 158 additions & 45 deletions channeldb/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/lightningnetwork/lnd/channeldb/kvdb"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
Expand Down Expand Up @@ -115,13 +117,39 @@ var (
// match the invoice hash.
ErrInvoicePreimageMismatch = errors.New("preimage does not match")

// ErrHTLCPreimageMissing is returned when trying to accept/settle an
// AMP HTLC but the HTLC-level preimage has not been set.
ErrHTLCPreimageMissing = errors.New("AMP htlc missing preimage")

// ErrHTLCPreimageMismatch is returned when trying to accept/settle an
// AMP HTLC but the HTLC-level preimage does not satisfying the
// HTLC-level payment hash.
ErrHTLCPreimageMismatch = errors.New("htlc preimage mismatch")

// ErrHTLCAlreadySettled is returned when trying to settle an invoice
// but HTLC already exists in the settled state.
ErrHTLCAlreadySettled = errors.New("htlc already settled")

// ErrInvoiceHasHtlcs is returned when attempting to insert an invoice
// that already has HTLCs.
ErrInvoiceHasHtlcs = errors.New("cannot add invoice with htlcs")

// ErrEmptyHTLCSet is returned when attempting to accept or settle and
// HTLC set that has no HTLCs.
ErrEmptyHTLCSet = errors.New("cannot settle/accept empty HTLC set")

// ErrUnexpectedInvoicePreimage is returned when an invoice-level
// preimage is provided when trying to settle an invoice that shouldn't
// have one, e.g. an AMP invoice.
ErrUnexpectedInvoicePreimage = errors.New(
"unexpected invoice preimage provided on settle",
)

// ErrHTLCPreimageAlreadyExists is returned when trying to set an
// htlc-level preimage but one is already known.
ErrHTLCPreimageAlreadyExists = errors.New(
"htlc-level preimage already exists",
)
)

// ErrDuplicateSetID is an error returned when attempting to adding an AMP HTLC
Expand Down Expand Up @@ -198,7 +226,7 @@ type InvoiceRef struct {
// payHash is the payment hash of the target invoice. All invoices are
// currently indexed by payment hash. This value will be used as a
// fallback when no payment address is known.
payHash lntypes.Hash
payHash *lntypes.Hash

// payAddr is the payment addr of the target invoice. Newer invoices
// (0.11 and up) are indexed by payment address in addition to payment
Expand All @@ -220,7 +248,7 @@ type InvoiceRef struct {
// its payment hash.
func InvoiceRefByHash(payHash lntypes.Hash) InvoiceRef {
return InvoiceRef{
payHash: payHash,
payHash: &payHash,
}
}

Expand All @@ -231,11 +259,19 @@ func InvoiceRefByHashAndAddr(payHash lntypes.Hash,
payAddr [32]byte) InvoiceRef {

return InvoiceRef{
payHash: payHash,
payHash: &payHash,
payAddr: &payAddr,
}
}

// InvoiceRefByAddr creates an InvoiceRef that queries the payment addr index
// for an invoice with the provided payment address.
func InvoiceRefByAddr(addr [32]byte) InvoiceRef {
return InvoiceRef{
payAddr: &addr,
}
}

// InvoiceRefBySetID creates an InvoiceRef that queries the set id index for an
// invoice with the provided setID. If the invoice is not found, the query will
// not fallback to payHash or payAddr.
Expand All @@ -245,9 +281,15 @@ func InvoiceRefBySetID(setID [32]byte) InvoiceRef {
}
}

// PayHash returns the target invoice's payment hash.
func (r InvoiceRef) PayHash() lntypes.Hash {
return r.payHash
// PayHash returns the optional payment hash of the target invoice.
//
// NOTE: This value may be nil.
func (r InvoiceRef) PayHash() *lntypes.Hash {
if r.payHash != nil {
hash := *r.payHash
return &hash
}
return nil
}

// PayAddr returns the optional payment address of the target invoice.
Expand All @@ -274,10 +316,17 @@ func (r InvoiceRef) SetID() *[32]byte {

// String returns a human-readable representation of an InvoiceRef.
func (r InvoiceRef) String() string {
var ids []string
if r.payHash != nil {
ids = append(ids, fmt.Sprintf("pay_hash=%v", *r.payHash))
}
if r.payAddr != nil {
return fmt.Sprintf("(pay_hash=%v, pay_addr=%x)", r.payHash, *r.payAddr)
ids = append(ids, fmt.Sprintf("pay_addr=%x", *r.payAddr))
}
return fmt.Sprintf("(pay_hash=%v)", r.payHash)
if r.setID != nil {
ids = append(ids, fmt.Sprintf("set_id=%x", *r.setID))
}
return fmt.Sprintf("(%s)", strings.Join(ids, ", "))
}

// ContractState describes the state the invoice is in.
Expand Down Expand Up @@ -499,6 +548,21 @@ type InvoiceHTLC struct {
AMP *InvoiceHtlcAMPData
}

// Copy makes a deep copy of the target InvoiceHTLC.
func (h *InvoiceHTLC) Copy() *InvoiceHTLC {
result := *h

// Make a copy of the CustomSet map.
result.CustomRecords = make(record.CustomSet)
for k, v := range h.CustomRecords {
result.CustomRecords[k] = v
}

result.AMP = h.AMP.Copy()

return &result
}

// IsInHTLCSet returns true if this HTLC is part an HTLC set. If nil is passed,
// this method returns true if this is an MPP HTLC. Otherwise, it only returns
// true if the AMP HTLC's set id matches the populated setID.
Expand Down Expand Up @@ -616,6 +680,11 @@ type InvoiceStateUpdateDesc struct {
// Preimage must be set to the preimage when NewState is settled.
Preimage *lntypes.Preimage

// HTLCPreimages set the HTLC-level preimages stored for AMP HTLCs.
// These are only learned when settling the invoice as a whole. Must be
// set when settling an invoice with non-nil SetID.
HTLCPreimages map[CircuitKey]lntypes.Preimage
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved

// SetID identifies a specific set of HTLCs destined for the same
// invoice as part of a larger AMP payment. This value will be nil for
// legacy or MPP payments.
Expand Down Expand Up @@ -645,7 +714,17 @@ func validateInvoice(i *Invoice, paymentHash lntypes.Hash) error {
return errors.New("invoice must have a feature vector")
}

if i.Terms.PaymentPreimage == nil && !i.HodlInvoice {
err := feature.ValidateDeps(i.Terms.Features)
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

// AMP invoices and hodl invoices are allowed to have no preimage
// specified.
isAMP := i.Terms.Features.HasFeature(
cfromknecht marked this conversation as resolved.
Show resolved Hide resolved
lnwire.AMPOptional,
)
if i.Terms.PaymentPreimage == nil && !(i.HodlInvoice || isAMP) {
return errors.New("non-hodl invoices must have a preimage")
}

Expand Down Expand Up @@ -879,19 +958,27 @@ func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex, setIDIndex kvdb.RBucket,
payHash := ref.PayHash()
payAddr := ref.PayAddr()

var (
invoiceNumByHash = invoiceIndex.Get(payHash[:])
invoiceNumByAddr []byte
)
if payAddr != nil {
// Only allow lookups for payment address if it is not a blank
// payment address, which is a special-cased value for legacy
// keysend invoices.
if *payAddr != BlankPayAddr {
invoiceNumByAddr = payAddrIndex.Get(payAddr[:])
getInvoiceNumByHash := func() []byte {
if payHash != nil {
return invoiceIndex.Get(payHash[:])
}
return nil
}

getInvoiceNumByAddr := func() []byte {
if payAddr != nil {
// Only allow lookups for payment address if it is not a
// blank payment address, which is a special-cased value
// for legacy keysend invoices.
if *payAddr != BlankPayAddr {
return payAddrIndex.Get(payAddr[:])
}
}
return nil
}

invoiceNumByHash := getInvoiceNumByHash()
invoiceNumByAddr := getInvoiceNumByAddr()
switch {

// If payment address and payment hash both reference an existing
Expand All @@ -903,6 +990,10 @@ func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex, setIDIndex kvdb.RBucket,

return invoiceNumByAddr, nil

// Return invoices by payment addr only.
case invoiceNumByAddr != nil:
return invoiceNumByAddr, nil

// If we were only able to reference the invoice by hash, return the
// corresponding invoice number. This can happen when no payment address
// was provided, or if it didn't match anything in our records.
Expand Down Expand Up @@ -1684,21 +1775,6 @@ func copySlice(src []byte) []byte {
return dest
}

// copyInvoiceHTLC makes a deep copy of the supplied invoice HTLC.
func copyInvoiceHTLC(src *InvoiceHTLC) *InvoiceHTLC {
result := *src

// Make a copy of the CustomSet map.
result.CustomRecords = make(record.CustomSet)
for k, v := range src.CustomRecords {
result.CustomRecords[k] = v
}

result.AMP = src.AMP.Copy()

return &result
}

// copyInvoice makes a deep copy of the supplied invoice.
func copyInvoice(src *Invoice) *Invoice {
dest := Invoice{
Expand All @@ -1725,15 +1801,15 @@ func copyInvoice(src *Invoice) *Invoice {
}

for k, v := range src.Htlcs {
dest.Htlcs[k] = copyInvoiceHTLC(v)
dest.Htlcs[k] = v.Copy()
}

return &dest
}

// updateInvoice fetches the invoice, obtains the update descriptor from the
// callback and applies the updates in a single db transaction.
func (d *DB) updateInvoice(hash lntypes.Hash, invoices,
func (d *DB) updateInvoice(hash *lntypes.Hash, invoices,
settleIndex, setIDIndex kvdb.RwBucket, invoiceNum []byte,
callback InvoiceUpdateCallback) (*Invoice, error) {

Expand Down Expand Up @@ -1867,7 +1943,25 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices,
// the process by updating the state transitions for individual HTLCs
// and recalculate the total amount paid to the invoice.
var amtPaid lnwire.MilliSatoshi
for _, htlc := range invoice.Htlcs {
for key, htlc := range invoice.Htlcs {
// Set the HTLC preimage for any AMP HTLCs.
if setID != nil {
preimage, ok := update.State.HTLCPreimages[key]
switch {

// If we don't already have a preiamge for this HTLC, we
// can set it now.
case ok && htlc.AMP.Preimage == nil:
htlc.AMP.Preimage = &preimage
halseth marked this conversation as resolved.
Show resolved Hide resolved

// Otherwise, prevent over-writing an existing preimage.
// Ignore the case where the preimage is identical.
case ok && *htlc.AMP.Preimage != preimage:
return nil, ErrHTLCPreimageAlreadyExists
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this end up causing all the HTLCs w/ the same set ID to be cancelled back? If so this could be useful for probing using AMP as we can just repeat a share when we send things over, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would need to check (part of the TODO w/ adding more tests around invalid HTLC sets). as far as probing though, repeating the same share on a different HTLC won't hit this check since it's keyed by CircuitKey. we really shouldn't hit this check ever anyways, it is more of a sanity check to prevent us from making incorrect transitions at the db level.


}
}

// The invoice state may have changed and this could have
// implications for the states of the individual htlcs. Align
// the htlc state with the current invoice state.
Expand Down Expand Up @@ -1901,7 +1995,7 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices,
}

// updateInvoiceState validates and processes an invoice state update.
func updateInvoiceState(invoice *Invoice, hash lntypes.Hash,
func updateInvoiceState(invoice *Invoice, hash *lntypes.Hash,
update InvoiceStateUpdateDesc) error {

// Returning to open is never allowed from any state.
Expand Down Expand Up @@ -1950,9 +2044,14 @@ func updateInvoiceState(invoice *Invoice, hash lntypes.Hash,

switch {

// If an invoice-level preimage was supplied, but the InvoiceRef
// doesn't specify a hash (e.g. AMP invoices) we fail.
case update.Preimage != nil && hash == nil:
return ErrUnexpectedInvoicePreimage

// Validate the supplied preimage for non-AMP invoices.
case update.Preimage != nil:
if update.Preimage.Hash() != hash {
if update.Preimage.Hash() != *hash {
return ErrInvoicePreimageMismatch
}
invoice.Terms.PaymentPreimage = update.Preimage
Expand Down Expand Up @@ -2027,10 +2126,25 @@ func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC,
// already know the preimage is valid due to checks at
// the invoice level. For AMP HTLCs, verify that the
// per-HTLC preimage-hash pair is valid.
if setID != nil && !htlc.AMP.Preimage.Matches(htlc.AMP.Hash) {
return fmt.Errorf("AMP preimage mismatch, "+
"preimage=%v hash=%v", *htlc.AMP.Preimage,
htlc.AMP.Hash)
switch {

// Non-AMP HTLCs can be settle immediately since we
// already know the preimage is valid due to checks at
// the invoice level.
case setID == nil:

// At this popint, the setID is non-nil, meaning this is
// an AMP HTLC. We know that htlc.AMP cannot be nil,
// otherwise IsInHTLCSet would have returned false.
//
// Fail if an accepted AMP HTLC has no preimage.
case htlc.AMP.Preimage == nil:
return ErrHTLCPreimageMissing

// Fail if the accepted AMP HTLC has an invalid
// preimage.
case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash):
return ErrHTLCPreimageMismatch
}

htlcState = HtlcStateSettled
Expand Down Expand Up @@ -2059,8 +2173,7 @@ func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC,
// We should never find a settled HTLC on an invoice that isn't in
// ContractSettled.
if htlc.State == HtlcStateSettled {
return fmt.Errorf("cannot have a settled htlc with "+
"invoice in state %v", invState)
return ErrHTLCAlreadySettled
cfromknecht marked this conversation as resolved.
Show resolved Hide resolved
}

switch invState {
Expand Down
5 changes: 5 additions & 0 deletions feature/default_sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.AMPOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
SetInvoice: {}, // 9
},
}
3 changes: 3 additions & 0 deletions feature/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ var deps = depDesc{
lnwire.AnchorsOptional: {
lnwire.StaticRemoteKeyOptional: {},
},
lnwire.AMPOptional: {
lnwire.PaymentAddrOptional: {},
},
}

// ValidateDeps asserts that a feature vector sets all features and their
Expand Down
2 changes: 2 additions & 0 deletions feature/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
raw.Unset(lnwire.PaymentAddrRequired)
raw.Unset(lnwire.MPPOptional)
raw.Unset(lnwire.MPPRequired)
raw.Unset(lnwire.AMPOptional)
raw.Unset(lnwire.AMPRequired)
}
if cfg.NoStaticRemoteKey {
raw.Unset(lnwire.StaticRemoteKeyOptional)
Expand Down
Loading