Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions channeldb/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,10 @@ func serializeHop(w io.Writer, h *route.Hop) error {
records = append(records, h.MPP.Record())
}

if h.Metadata != nil {
records = append(records, record.NewMetadataRecord(&h.Metadata))
}

// Final sanity check to absolutely rule out custom records that are not
// custom and write into the standard range.
if err := h.CustomRecords.Validate(); err != nil {
Expand Down Expand Up @@ -1255,6 +1259,13 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
h.MPP = mpp
}

metadataType := uint64(record.MetadataOnionType)
if metadata, ok := tlvMap[metadataType]; ok {
delete(tlvMap, metadataType)

h.Metadata = metadata
}

h.CustomRecords = tlvMap

return h, nil
Expand Down
3 changes: 2 additions & 1 deletion channeldb/payments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ var (
65536: []byte{},
80001: []byte{},
},
MPP: record.NewMPP(32, [32]byte{0x42}),
MPP: record.NewMPP(32, [32]byte{0x42}),
Metadata: []byte{1, 2, 3},
}

testHop2 = &route.Hop{
Expand Down
11 changes: 11 additions & 0 deletions docs/release-notes/release-notes-0.15.0.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Release Notes

## Payments

Support according to the
[spec](https://github.com/lightningnetwork/lightning-rfc/pull/912) has been
added for [payment metadata in
invoices](https://github.com/lightningnetwork/lnd/pull/5810). If metadata is
present in the invoice, it is encoded as a tlv record for the receiver.

This functionality unlocks future features such as [stateless
invoices](https://lists.linuxfoundation.org/pipermail/lightning-dev/2021-September/003236.html).

## Security

* [Misconfigured ZMQ
Expand Down
29 changes: 24 additions & 5 deletions htlcswitch/hop/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ type Payload struct {
// customRecords are user-defined records in the custom type range that
// were included in the payload.
customRecords record.CustomSet

// metadata is additional data that is sent along with the payment to
// the payee.
metadata []byte
}

// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
Expand All @@ -115,11 +119,12 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload {
// should correspond to the bytes encapsulated in a TLV onion payload.
func NewPayloadFromReader(r io.Reader) (*Payload, error) {
var (
cid uint64
amt uint64
cltv uint32
mpp = &record.MPP{}
amp = &record.AMP{}
cid uint64
amt uint64
cltv uint32
mpp = &record.MPP{}
amp = &record.AMP{}
metadata []byte
)

tlvStream, err := tlv.NewStream(
Expand All @@ -128,6 +133,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
record.NewNextHopIDRecord(&cid),
mpp.Record(),
amp.Record(),
record.NewMetadataRecord(&metadata),
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -168,6 +174,12 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
amp = nil
}

// If no metadata field was parsed, set the metadata field on the
// resulting payload to nil.
if _, ok := parsedTypes[record.MetadataOnionType]; !ok {
metadata = nil
}

// Filter out the custom records.
customRecords := NewCustomRecords(parsedTypes)

Expand All @@ -180,6 +192,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
},
MPP: mpp,
AMP: amp,
metadata: metadata,
customRecords: customRecords,
}, nil
}
Expand Down Expand Up @@ -284,6 +297,12 @@ func (h *Payload) CustomRecords() record.CustomSet {
return h.customRecords
}

// Metadata returns the additional data that is sent along with the
// payment to the payee.
func (h *Payload) Metadata() []byte {
return h.metadata
}

// getMinRequiredViolation checks for unrecognized required (even) fields in the
// standard range and returns the lowest required type. Always returning the
// lowest required type allows a failure message to be deterministic.
Expand Down
37 changes: 30 additions & 7 deletions htlcswitch/hop/payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ import (
"github.com/stretchr/testify/require"
)

const testUnknownRequiredType = 0x10
const testUnknownRequiredType = 0x80

type decodePayloadTest struct {
name string
payload []byte
expErr error
expCustomRecords map[uint64][]byte
shouldHaveMPP bool
shouldHaveAMP bool
name string
payload []byte
expErr error
expCustomRecords map[uint64][]byte
shouldHaveMPP bool
shouldHaveAMP bool
shouldHaveMetadata bool
}

var decodePayloadTests = []decodePayloadTest{
Expand Down Expand Up @@ -258,6 +259,18 @@ var decodePayloadTests = []decodePayloadTest{
},
shouldHaveAMP: true,
},
{
name: "final hop with metadata",
payload: []byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// metadata
0x10, 0x03, 0x01, 0x02, 0x03,
},
shouldHaveMetadata: true,
},
}

// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
Expand Down Expand Up @@ -293,6 +306,7 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
}
testMetadata = []byte{1, 2, 3}
testChildIndex = uint32(9)
)

Expand Down Expand Up @@ -331,6 +345,15 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
t.Fatalf("unexpected AMP payload")
}

if test.shouldHaveMetadata {
if p.Metadata() == nil {
t.Fatalf("payload should have metadata")
}
require.Equal(t, testMetadata, p.Metadata())
} else if p.Metadata() != nil {
t.Fatalf("unexpected metadata")
}

// Convert expected nil map to empty map, because we always expect an
// initiated map from the payload.
expCustomRecords := make(record.CustomSet)
Expand Down
4 changes: 4 additions & 0 deletions invoices/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ type Payload interface {
// CustomRecords returns the custom tlv type records that were parsed
// from the payload.
CustomRecords() record.CustomSet

// Metadata returns the additional data that is sent along with the
// payment to the payee.
Metadata() []byte
}
1 change: 1 addition & 0 deletions invoices/invoiceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
customRecords: payload.CustomRecords(),
mpp: payload.MultiPath(),
amp: payload.AMPRecord(),
metadata: payload.Metadata(),
Copy link
Contributor Author

@joostjager joostjager Nov 2, 2021

Choose a reason for hiding this comment

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

What is missing here is that we are assuming that an invoice already exists. The goal of the new metadata field is to enable stateless invoices. In that case, there isn't an invoice yet. It is a kind of spontaneous payment (not really spontaneous - the receiver just 'forgot' the invoice), but the difference with keysend/amp is that the receiver is able to re-derive the preimage. The preimage can be used as proof of payment.

I am wondering what the options are to implement this. Will it be yet another payment type? Or is this the point where it becomes necessary to allow invoice registry plugins so that users can implement the logic themselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

LDK approach with this: lightningdevkit/rust-lightning#1171

Copy link
Member

Choose a reason for hiding this comment

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

I am wondering what the options are to implement this. Will it be yet another payment type

I think we can just use the existing keysend flow here, which ends up inserting the the database, or using the existing AMP pattern to re-insert a new sub-invoice with a main tracking identifier. IIUC, with the proposal you'd still write the invoice to disk, but the "stateless" part is what lets you not have to remember the invoice until its paid?

Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, the existing HTLC interceptor logic can handle this as well assuming you have the invoice logic already living outside of lnd.

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 think "stateless" can be implemented in multiple ways, but doing a JIT insert similar to keysend seems definitely one of the options.

}

switch {
Expand Down
5 changes: 5 additions & 0 deletions invoices/test_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type mockPayload struct {
mpp *record.MPP
amp *record.AMP
customRecords record.CustomSet
metadata []byte
}

func (p *mockPayload) MultiPath() *record.MPP {
Expand All @@ -50,6 +51,10 @@ func (p *mockPayload) CustomRecords() record.CustomSet {
return p.customRecords
}

func (p *mockPayload) Metadata() []byte {
return p.metadata
}

const (
testHtlcExpiry = uint32(5)

Expand Down
13 changes: 11 additions & 2 deletions invoices/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package invoices

import (
"encoding/hex"
"errors"

"github.com/lightningnetwork/lnd/amp"
Expand All @@ -22,6 +23,7 @@ type invoiceUpdateCtx struct {
customRecords record.CustomSet
mpp *record.MPP
amp *record.AMP
metadata []byte
}

// invoiceRef returns an identifier that can be used to lookup or update the
Expand Down Expand Up @@ -52,9 +54,16 @@ func (i invoiceUpdateCtx) setID() *[32]byte {

// log logs a message specific to this update context.
func (i *invoiceUpdateCtx) log(s string) {
// Don't use %x in the log statement below, because it doesn't
// distinguish between nil and empty metadata.
metadata := "<nil>"
if i.metadata != nil {
metadata = hex.EncodeToString(i.metadata)
}

log.Debugf("Invoice%v: %v, amt=%v, expiry=%v, circuit=%v, mpp=%v, "+
"amp=%v", i.invoiceRef(), s, i.amtPaid, i.expiry, i.circuitKey,
i.mpp, i.amp)
"amp=%v, metadata=%v", i.invoiceRef(), s, i.amtPaid, i.expiry,
i.circuitKey, i.mpp, i.amp, metadata)
}

// failRes is a helper function which creates a failure resolution with
Expand Down
Loading