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

invoices: expose custom tlv records from the payload #3742

Merged
merged 3 commits into from Dec 10, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
84 changes: 76 additions & 8 deletions channeldb/invoice_test.go
Expand Up @@ -7,6 +7,7 @@ import (
"time"

"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
Expand Down Expand Up @@ -209,13 +210,15 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) {

// Accept an htlc on this invoice.
key := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4}
htlc := HtlcAcceptDesc{
Amt: 500,
CustomRecords: make(hop.CustomRecordSet),
}
invoice, err := db.UpdateInvoice(paymentHash,
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
return &InvoiceUpdateDesc{
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
key: {
Amt: 500,
},
key: &htlc,
},
}, nil
})
Expand Down Expand Up @@ -432,10 +435,11 @@ func TestDuplicateSettleInvoice(t *testing.T) {
invoice.SettleDate = dbInvoice.SettleDate
invoice.Htlcs = map[CircuitKey]*InvoiceHTLC{
{}: {
Amt: amt,
AcceptTime: time.Unix(1, 0),
ResolveTime: time.Unix(1, 0),
State: HtlcStateSettled,
Amt: amt,
AcceptTime: time.Unix(1, 0),
ResolveTime: time.Unix(1, 0),
State: HtlcStateSettled,
CustomRecords: make(hop.CustomRecordSet),
},
}

Expand Down Expand Up @@ -747,18 +751,82 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) InvoiceUpdateCallback {
return nil, ErrInvoiceAlreadySettled
}

noRecords := make(hop.CustomRecordSet)

update := &InvoiceUpdateDesc{
State: &InvoiceStateUpdateDesc{
Preimage: invoice.Terms.PaymentPreimage,
NewState: ContractSettled,
},
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
{}: {
Amt: amt,
Amt: amt,
CustomRecords: noRecords,
},
},
}

return update, nil
}
}

// TestCustomRecords tests that custom records are properly recorded in the
// invoice database.
func TestCustomRecords(t *testing.T) {
t.Parallel()

db, cleanUp, err := makeTestDB()
defer cleanUp()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}

testInvoice := &Invoice{
Htlcs: map[CircuitKey]*InvoiceHTLC{},
}
testInvoice.Terms.Value = lnwire.NewMSatFromSatoshis(10000)
testInvoice.Terms.Features = emptyFeatures

var paymentHash lntypes.Hash
if _, err := db.AddInvoice(testInvoice, paymentHash); err != nil {
t.Fatalf("unable to find invoice: %v", err)
}

// Accept an htlc with custom records on this invoice.
key := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4}

records := hop.CustomRecordSet{
100000: []byte{},
100001: []byte{1, 2},
}

_, err = db.UpdateInvoice(paymentHash,
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
return &InvoiceUpdateDesc{
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
key: {
Amt: 500,
CustomRecords: records,
},
},
}, nil
},
)
if err != nil {
t.Fatalf("unable to add invoice htlc: %v", err)
}

// Retrieve the invoice from that database and verify that the custom
// records are present.
dbInvoice, err := db.LookupInvoice(paymentHash)
if err != nil {
t.Fatalf("unable to lookup invoice: %v", err)
}

if len(dbInvoice.Htlcs) != 1 {
t.Fatalf("expected the htlc to be added")
}
if !reflect.DeepEqual(records, dbInvoice.Htlcs[key].CustomRecords) {
t.Fatalf("invalid custom records")
}
}
47 changes: 40 additions & 7 deletions channeldb/invoices.go
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
Expand Down Expand Up @@ -308,6 +309,10 @@ type InvoiceHTLC struct {
// canceled htlc isn't just removed from the invoice htlcs map, because
// we need AcceptHeight to properly cancel the htlc back.
State HtlcState

// CustomRecords contains the custom key/value pairs that accompanied
// the htlc.
CustomRecords hop.CustomRecordSet
}

// HtlcAcceptDesc describes the details of a newly accepted htlc.
Expand All @@ -320,6 +325,10 @@ type HtlcAcceptDesc struct {

// Expiry is the expiry height of this htlc.
Expiry uint32

// CustomRecords contains the custom key/value pairs that accompanied
// the htlc.
CustomRecords hop.CustomRecordSet
}

// InvoiceUpdateDesc describes the changes that should be applied to the
Expand Down Expand Up @@ -1013,7 +1022,8 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
resolveTime := uint64(htlc.ResolveTime.UnixNano())
state := uint8(htlc.State)

tlvStream, err := tlv.NewStream(
var records []tlv.Record
records = append(records,
tlv.MakePrimitiveRecord(chanIDType, &chanID),
tlv.MakePrimitiveRecord(htlcIDType, &key.HtlcID),
tlv.MakePrimitiveRecord(amtType, &amt),
Expand All @@ -1025,6 +1035,16 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
tlv.MakePrimitiveRecord(htlcStateType, &state),
)

// Convert the custom records to tlv.Record types that are ready
// for serialization.
customRecords := tlv.MapToRecords(htlc.CustomRecords)

// Append the custom records. Their ids are in the experimental
// range and sorted, so there is no need to sort again.
records = append(records, customRecords...)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Offline comment from @cfromknecht: not sure if it is a good idea to mix the custom records with the db types here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Discussed this further. It may be better to create a single tlv record for which the value is a tlv stream with just the custom records. As a simple insurance against accidental modification of standard fields.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Discussed offline. There seems to be more support for a flat structure, so leaving it as is.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am still somewhat concerned about mingling minimally validated user data with known records. I think it is possible to do it safely, but i think it forces us to be stricter in how we handle migrations.

Consider the scenario where we add a field to the invoice body but do not bump the db version. If a user downgrades, this will now appear in the custom record set even tho its type is beneath the cutoff. I think it's possible that data gets written back, but it's difficult imo to ascertain whether this can lead to unforeseen bugs.

We could prevent this by filter the custom records when reading as we do in the hop, then the data is actually dropped when writing back. This would maintain the current behavior in that scenario.


tlvStream, err := tlv.NewStream(records...)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Question open as to whether we want to bump the db version, to prevent users running master from downgrading and loosing their custom records

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Discussed this in the context of another PR. We don't give this guarantee to users running master generally.

if err != nil {
return err
}
Expand Down Expand Up @@ -1191,7 +1211,8 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
return nil, err
}

if err := tlvStream.Decode(htlcReader); err != nil {
parsedTypes, err := tlvStream.DecodeWithParsedTypes(htlcReader)
if err != nil {
return nil, err
}

Expand All @@ -1201,6 +1222,10 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
htlc.State = HtlcState(state)
htlc.Amt = lnwire.MilliSatoshi(amt)

// Reconstruct the custom records fields from the parsed types
// map return from the tlv parser.
htlc.CustomRecords = hop.NewCustomRecords(parsedTypes)

htlcs[key] = &htlc
}

Expand Down Expand Up @@ -1290,12 +1315,20 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex *bbolt.Bucke
if _, exists := invoice.Htlcs[key]; exists {
return nil, fmt.Errorf("duplicate add of htlc %v", key)
}

// Force caller to supply htlc without custom records in a
// consistent way.
if htlcUpdate.CustomRecords == nil {
cfromknecht marked this conversation as resolved.
Show resolved Hide resolved
return nil, errors.New("nil custom records map")
}

htlc := &InvoiceHTLC{
Amt: htlcUpdate.Amt,
Expiry: htlcUpdate.Expiry,
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
AcceptTime: now,
State: HtlcStateAccepted,
Amt: htlcUpdate.Amt,
Expiry: htlcUpdate.Expiry,
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
AcceptTime: now,
State: HtlcStateAccepted,
CustomRecords: htlcUpdate.CustomRecords,
}

invoice.Htlcs[key] = htlc
Expand Down
41 changes: 36 additions & 5 deletions htlcswitch/hop/payload.go
Expand Up @@ -79,6 +79,9 @@ func (e ErrInvalidPayload) Error() string {
hopType, e.Violation, e.Type)
}

// CustomRecordSet stores a set of custom key/value pairs.
type CustomRecordSet map[uint64][]byte

// Payload encapsulates all information delivered to a hop in an onion payload.
// A Hop can represent either a TLV or legacy payload. The primary forwarding
// instruction can be accessed via ForwardingInfo, and additional records can be
Expand All @@ -91,6 +94,10 @@ type Payload struct {
// MPP holds the info provided in an option_mpp record when parsed from
// a TLV onion payload.
MPP *record.MPP

// customRecords are user-defined records in the custom type range that
// were included in the payload.
customRecords CustomRecordSet
}

// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
Expand All @@ -105,6 +112,7 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload {
AmountToForward: lnwire.MilliSatoshi(f.ForwardAmount),
OutgoingCTLV: f.OutgoingCltv,
},
customRecords: make(CustomRecordSet),
}
}

Expand Down Expand Up @@ -157,14 +165,18 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
mpp = nil
}

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

return &Payload{
FwdInfo: ForwardingInfo{
Network: BitcoinNetwork,
NextHop: nextHop,
AmountToForward: lnwire.MilliSatoshi(amt),
OutgoingCTLV: cltv,
},
MPP: mpp,
MPP: mpp,
customRecords: customRecords,
}, nil
}

Expand All @@ -174,11 +186,24 @@ func (h *Payload) ForwardingInfo() ForwardingInfo {
return h.FwdInfo
}

// NewCustomRecords filters the types parsed from the tlv stream for custom
// records.
func NewCustomRecords(parsedTypes tlv.TypeMap) CustomRecordSet {
customRecords := make(CustomRecordSet)
for t, parseResult := range parsedTypes {
if parseResult == nil || t < CustomTypeStart {
continue
}
customRecords[uint64(t)] = parseResult
}
return customRecords
}

// ValidateParsedPayloadTypes checks the types parsed from a hop payload to
// ensure that the proper fields are either included or omitted. The finalHop
// boolean should be true if the payload was parsed for an exit hop. The
// requirements for this method are described in BOLT 04.
func ValidateParsedPayloadTypes(parsedTypes tlv.TypeSet,
func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap,
nextHop lnwire.ShortChannelID) error {

isFinalHop := nextHop == Exit
Expand Down Expand Up @@ -234,22 +259,28 @@ func (h *Payload) MultiPath() *record.MPP {
return h.MPP
}

// CustomRecords returns the custom tlv type records that were parsed from the
// payload.
func (h *Payload) CustomRecords() CustomRecordSet {
return h.customRecords
}

// 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.
func getMinRequiredViolation(set tlv.TypeSet) *tlv.Type {
func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
var (
requiredViolation bool
minRequiredViolationType tlv.Type
)
for t, known := range set {
for t, parseResult := range set {
// If a type is even but not known to us, we cannot process the
// payload. We are required to understand a field that we don't
// support.
//
// We always accept custom fields, because a higher level
// application may understand them.
if known || t%2 != 0 || t >= CustomTypeStart {
if parseResult == nil || t%2 != 0 || t >= CustomTypeStart {
continue
}

Expand Down
24 changes: 19 additions & 5 deletions htlcswitch/hop/payload_test.go
Expand Up @@ -11,10 +11,11 @@ import (
)

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

var decodePayloadTests = []decodePayloadTest{
Expand Down Expand Up @@ -133,7 +134,10 @@ var decodePayloadTests = []decodePayloadTest{
{
name: "required type in custom range",
payload: []byte{0x02, 0x00, 0x04, 0x00,
0xfe, 0x00, 0x01, 0x00, 0x00, 0x00,
0xfe, 0x00, 0x01, 0x00, 0x00, 0x02, 0x10, 0x11,
},
expCustomRecords: map[uint64][]byte{
65536: {0x10, 0x11},
},
},
{
Expand Down Expand Up @@ -237,4 +241,14 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
} else if p.MPP != nil {
t.Fatalf("unexpected MPP payload")
}

// Convert expected nil map to empty map, because we always expect an
joostjager marked this conversation as resolved.
Show resolved Hide resolved
// initiated map from the payload.
expCustomRecords := make(hop.CustomRecordSet)
if test.expCustomRecords != nil {
expCustomRecords = test.expCustomRecords
}
if !reflect.DeepEqual(expCustomRecords, p.CustomRecords()) {
t.Fatalf("invalid custom records")
}
}