multi: add blinded path TLVs to onion payload / hops

This commit adds the encrypted_data, blinding_point and total_amt_msat
tlvs to the known set of even tlvs for the onion payload. These TLVs
are added in two places (the onion payload and hop struct) because
lnd uses the same set of TLV types for both structs (and they
inherently represent the same thing).

Note: in some places, unit tests intentionally mimic the style
of older tests, so as to be more consistently readable.
This commit is contained in:
Carla Kirk-Cohen 2022-10-26 10:57:37 -04:00 committed by Olaoluwa Osuntokun
parent 539a275faa
commit fee0e05708
7 changed files with 392 additions and 23 deletions

View file

@ -9,6 +9,7 @@ import (
"sort"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes"
@ -1143,10 +1144,30 @@ func serializeHop(w io.Writer, h *route.Hop) error {
records = append(records, h.MPP.Record())
}
// Add blinding point and encrypted data if present.
if h.EncryptedData != nil {
records = append(records, record.NewEncryptedDataRecord(
&h.EncryptedData,
))
}
if h.BlindingPoint != nil {
records = append(records, record.NewBlindingPointRecord(
&h.BlindingPoint,
))
}
if h.Metadata != nil {
records = append(records, record.NewMetadataRecord(&h.Metadata))
}
if h.TotalAmtMsat != 0 {
totalMsatInt := uint64(h.TotalAmtMsat)
records = append(
records, record.NewTotalAmtMsatBlinded(&totalMsatInt),
)
}
// 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 {
@ -1261,6 +1282,27 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
h.MPP = mpp
}
// If encrypted data or blinding key are present, remove them from
// the TLV map and parse into proper types.
encryptedDataType := uint64(record.EncryptedDataOnionType)
if data, ok := tlvMap[encryptedDataType]; ok {
delete(tlvMap, encryptedDataType)
h.EncryptedData = data
}
blindingType := uint64(record.BlindingPointOnionType)
if blindingPoint, ok := tlvMap[blindingType]; ok {
delete(tlvMap, blindingType)
h.BlindingPoint, err = btcec.ParsePubKey(blindingPoint)
if err != nil {
return nil, fmt.Errorf("invalid blinding point: %w",
err)
}
}
// If the metatdata type is present, remove it from the tlv map and
// populate directly on the hop.
metadataType := uint64(record.MetadataOnionType)
if metadata, ok := tlvMap[metadataType]; ok {
delete(tlvMap, metadataType)
@ -1268,6 +1310,26 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
h.Metadata = metadata
}
totalAmtMsatType := uint64(record.TotalAmtMsatBlindedType)
if totalAmtMsat, ok := tlvMap[totalAmtMsatType]; ok {
delete(tlvMap, totalAmtMsatType)
var (
totalAmtMsatInt uint64
buf [8]byte
)
if err := tlv.DTUint64(
bytes.NewReader(totalAmtMsat),
&totalAmtMsatInt,
&buf,
uint64(len(totalAmtMsat)),
); err != nil {
return nil, err
}
h.TotalAmtMsat = lnwire.MilliSatoshi(totalAmtMsatInt)
}
h.CustomRecords = tlvMap
return h, nil

View file

@ -21,9 +21,10 @@ import (
var (
priv, _ = btcec.NewPrivateKey()
pub = priv.PubKey()
vertex = route.NewVertex(pub)
testHop1 = &route.Hop{
PubKeyBytes: route.NewVertex(pub),
PubKeyBytes: vertex,
ChannelID: 12345,
OutgoingTimeLock: 111,
AmtToForward: 555,
@ -36,7 +37,7 @@ var (
}
testHop2 = &route.Hop{
PubKeyBytes: route.NewVertex(pub),
PubKeyBytes: vertex,
ChannelID: 12345,
OutgoingTimeLock: 111,
AmtToForward: 555,
@ -46,12 +47,39 @@ var (
testRoute = route.Route{
TotalTimeLock: 123,
TotalAmount: 1234567,
SourcePubKey: route.NewVertex(pub),
SourcePubKey: vertex,
Hops: []*route.Hop{
testHop2,
testHop1,
},
}
testBlindedRoute = route.Route{
TotalTimeLock: 150,
TotalAmount: 1000,
SourcePubKey: vertex,
Hops: []*route.Hop{
{
PubKeyBytes: vertex,
ChannelID: 9876,
OutgoingTimeLock: 120,
AmtToForward: 900,
EncryptedData: []byte{1, 3, 3},
BlindingPoint: pub,
},
{
PubKeyBytes: vertex,
EncryptedData: []byte{3, 2, 1},
},
{
PubKeyBytes: vertex,
Metadata: []byte{4, 5, 6},
AmtToForward: 500,
OutgoingTimeLock: 100,
TotalAmtMsat: 500,
},
},
}
)
func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
@ -140,27 +168,24 @@ func assertRouteEqual(a, b *route.Route) error {
return nil
}
// TestRouteSerialization tests serialization of a regular and blinded route.
func TestRouteSerialization(t *testing.T) {
t.Parallel()
testSerializeRoute(t, testRoute)
testSerializeRoute(t, testBlindedRoute)
}
func testSerializeRoute(t *testing.T, route route.Route) {
var b bytes.Buffer
if err := SerializeRoute(&b, testRoute); err != nil {
t.Fatal(err)
}
err := SerializeRoute(&b, route)
require.NoError(t, err)
r := bytes.NewReader(b.Bytes())
route2, err := DeserializeRoute(r)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
// First we verify all the records match up porperly, as they aren't
// able to be properly compared using reflect.DeepEqual.
err = assertRouteEqual(&testRoute, &route2)
if err != nil {
t.Fatalf("routes not equal: \n%v vs \n%v",
spew.Sdump(testRoute), spew.Sdump(route2))
}
reflect.DeepEqual(route, route2)
}
// deletePayment removes a payment with paymentHash from the payments database.

View file

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
@ -94,9 +95,20 @@ type Payload struct {
// were included in the payload.
customRecords record.CustomSet
// encryptedData is a blob of data encrypted by the receiver for use
// in blinded routes.
encryptedData []byte
// blindingPoint is an ephemeral pubkey for use in blinded routes.
blindingPoint *btcec.PublicKey
// metadata is additional data that is sent along with the payment to
// the payee.
metadata []byte
// totalAmtMsat holds the info provided in total_amount_msat when
// parsed from a TLV onion payload.
totalAmtMsat lnwire.MilliSatoshi
}
// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
@ -118,12 +130,15 @@ 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{}
metadata []byte
cid uint64
amt uint64
totalAmtMsat uint64
cltv uint32
mpp = &record.MPP{}
amp = &record.AMP{}
encryptedData []byte
blindingPoint *btcec.PublicKey
metadata []byte
)
tlvStream, err := tlv.NewStream(
@ -131,8 +146,11 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
record.NewLockTimeRecord(&cltv),
record.NewNextHopIDRecord(&cid),
mpp.Record(),
record.NewEncryptedDataRecord(&encryptedData),
record.NewBlindingPointRecord(&blindingPoint),
amp.Record(),
record.NewMetadataRecord(&metadata),
record.NewTotalAmtMsatBlinded(&totalAmtMsat),
)
if err != nil {
return nil, err
@ -175,6 +193,12 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
amp = nil
}
// If no encrypted data was parsed, set the field on our resulting
// payload to nil.
if _, ok := parsedTypes[record.EncryptedDataOnionType]; !ok {
encryptedData = nil
}
// If no metadata field was parsed, set the metadata field on the
// resulting payload to nil.
if _, ok := parsedTypes[record.MetadataOnionType]; !ok {
@ -193,7 +217,10 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
MPP: mpp,
AMP: amp,
metadata: metadata,
encryptedData: encryptedData,
blindingPoint: blindingPoint,
customRecords: customRecords,
totalAmtMsat: lnwire.MilliSatoshi(totalAmtMsat),
}, nil
}
@ -297,12 +324,29 @@ func (h *Payload) CustomRecords() record.CustomSet {
return h.customRecords
}
// EncryptedData returns the route blinding encrypted data parsed from the
// onion payload.
func (h *Payload) EncryptedData() []byte {
return h.encryptedData
}
// BlindingPoint returns the route blinding point parsed from the onion payload.
func (h *Payload) BlindingPoint() *btcec.PublicKey {
return h.blindingPoint
}
// Metadata returns the additional data that is sent along with the
// payment to the payee.
func (h *Payload) Metadata() []byte {
return h.metadata
}
// TotalAmtMsat returns the total amount sent to the final hop, as set by the
// payee.
func (h *Payload) TotalAmtMsat() lnwire.MilliSatoshi {
return h.totalAmtMsat
}
// 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.

View file

@ -2,15 +2,23 @@ package hop_test
import (
"bytes"
"encoding/hex"
"reflect"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/stretchr/testify/require"
)
var (
//nolint:lll
testPrivKeyBytes, _ = hex.DecodeString("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
_, testPubKey = btcec.PrivKeyFromBytes(testPrivKeyBytes)
)
const testUnknownRequiredType = 0x80
type decodePayloadTest struct {
@ -20,7 +28,10 @@ type decodePayloadTest struct {
expCustomRecords map[uint64][]byte
shouldHaveMPP bool
shouldHaveAMP bool
shouldHaveEncData bool
shouldHaveBlinding bool
shouldHaveMetadata bool
shouldHaveTotalAmt bool
}
var decodePayloadTests = []decodePayloadTest{
@ -217,6 +228,33 @@ var decodePayloadTests = []decodePayloadTest{
FinalHop: false,
},
},
{
name: "intermediate hop with encrypted data",
payload: []byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// encrypted data
0x0a, 0x03, 0x03, 0x02, 0x01,
},
shouldHaveEncData: true,
},
{
name: "intermediate hop with blinding point",
payload: append([]byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// blinding point (type / length)
0x0c, 0x21,
},
// blinding point (value)
testPubKey.SerializeCompressed()...,
),
shouldHaveBlinding: true,
},
{
name: "final hop with mpp",
payload: []byte{
@ -271,6 +309,18 @@ var decodePayloadTests = []decodePayloadTest{
},
shouldHaveMetadata: true,
},
{
name: "final hop with total amount",
payload: []byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// total amount
0x12, 0x01, 0x01,
},
shouldHaveTotalAmt: true,
},
}
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
@ -306,6 +356,7 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
}
testEncData = []byte{3, 2, 1}
testMetadata = []byte{1, 2, 3}
testChildIndex = uint32(9)
)
@ -354,6 +405,29 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
t.Fatalf("unexpected metadata")
}
if test.shouldHaveEncData {
require.NotNil(t, p.EncryptedData(),
"payment should have encrypted data")
require.Equal(t, testEncData, p.EncryptedData())
} else {
require.Nil(t, p.EncryptedData())
}
if test.shouldHaveBlinding {
require.NotNil(t, p.BlindingPoint())
require.Equal(t, testPubKey, p.BlindingPoint())
} else {
require.Nil(t, p.BlindingPoint())
}
if test.shouldHaveTotalAmt {
require.NotZero(t, p.TotalAmtMsat())
} else {
require.Zero(t, p.TotalAmtMsat())
}
// Convert expected nil map to empty map, because we always expect an
// initiated map from the payload.
expCustomRecords := make(record.CustomSet)

View file

@ -1,6 +1,7 @@
package record
import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
)
@ -17,9 +18,21 @@ const (
// of the next hop.
NextHopOnionType tlv.Type = 6
// EncryptedDataOnionType is the type used to include encrypted data
// provided by the receiver in the onion for use in blinded paths.
EncryptedDataOnionType tlv.Type = 10
// BlindingPointOnionType is the type used to include receiver provided
// ephemeral keys in the onion that are used in blinded paths.
BlindingPointOnionType tlv.Type = 12
// MetadataOnionType is the type used in the onion for the payment
// metadata.
MetadataOnionType tlv.Type = 16
// TotalAmtMsatBlindedType is the type used in the onion for the total
// amount field that is included in the final hop for blinded payments.
TotalAmtMsatBlindedType tlv.Type = 18
)
// NewAmtToFwdRecord creates a tlv.Record that encodes the amount_to_forward
@ -50,6 +63,18 @@ func NewNextHopIDRecord(cid *uint64) tlv.Record {
return tlv.MakePrimitiveRecord(NextHopOnionType, cid)
}
// NewEncryptedDataRecord creates a tlv.Record that encodes the encrypted_data
// (type 10) record for an onion payload.
func NewEncryptedDataRecord(data *[]byte) tlv.Record {
return tlv.MakePrimitiveRecord(EncryptedDataOnionType, data)
}
// NewBlindingPointRecord creates a tlv.Record that encodes the blinding_point
// (type 12) record for an onion payload.
func NewBlindingPointRecord(point **btcec.PublicKey) tlv.Record {
return tlv.MakePrimitiveRecord(BlindingPointOnionType, point)
}
// NewMetadataRecord creates a tlv.Record that encodes the metadata (type 10)
// for an onion payload.
func NewMetadataRecord(metadata *[]byte) tlv.Record {
@ -61,3 +86,14 @@ func NewMetadataRecord(metadata *[]byte) tlv.Record {
tlv.EVarBytes, tlv.DVarBytes,
)
}
// NewTotalAmtMsatBlinded creates a tlv.Record that encodes the
// total_amount_msat for the final an onion payload within a blinded route.
func NewTotalAmtMsatBlinded(amt *uint64) tlv.Record {
return tlv.MakeDynamicRecord(
TotalAmtMsatBlindedType, amt, func() uint64 {
return tlv.SizeTUint64(*amt)
},
tlv.ETUint64, tlv.DTUint64,
)
}

View file

@ -131,6 +131,20 @@ type Hop struct {
// Metadata is additional data that is sent along with the payment to
// the payee.
Metadata []byte
// EncryptedData is an encrypted data blob includes for hops that are
// part of a blinded route.
EncryptedData []byte
// BlindingPoint is an ephemeral public key used by introduction nodes
// in blinded routes to unblind their portion of the route and pass on
// the next ephemeral key to the next blinded node to do the same.
BlindingPoint *btcec.PublicKey
// TotalAmtMsat is the total amount for a blinded payment, potentially
// spread over more than one HTLC. This field should only be set for
// the final hop in a blinded path.
TotalAmtMsat lnwire.MilliSatoshi
}
// Copy returns a deep copy of the Hop.
@ -147,6 +161,11 @@ func (h *Hop) Copy() *Hop {
c.AMP = &a
}
if h.BlindingPoint != nil {
b := *h.BlindingPoint
c.BlindingPoint = &b
}
return &c
}
@ -197,6 +216,19 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
}
}
// Add encrypted data and blinding point if present.
if h.EncryptedData != nil {
records = append(records, record.NewEncryptedDataRecord(
&h.EncryptedData,
))
}
if h.BlindingPoint != nil {
records = append(records, record.NewBlindingPointRecord(
&h.BlindingPoint,
))
}
// If an AMP record is destined for this hop, ensure that we only ever
// attach it if we also have an MPP record. We can infer that this is
// already a final hop if MPP is non-nil otherwise we would have exited
@ -216,6 +248,13 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
)
}
if h.TotalAmtMsat != 0 {
totalAmtInt := uint64(h.TotalAmtMsat)
records = append(records,
record.NewTotalAmtMsatBlinded(&totalAmtInt),
)
}
// Append any custom types destined for this hop.
tlvRecords := tlv.MapToRecords(h.CustomRecords)
records = append(records, tlvRecords...)
@ -270,11 +309,33 @@ func (h *Hop) PayloadSize(nextChanID uint64) uint64 {
addRecord(record.AMPOnionType, h.AMP.PayloadSize())
}
// Add encrypted data and blinding point if present.
if h.EncryptedData != nil {
addRecord(
record.EncryptedDataOnionType,
uint64(len(h.EncryptedData)),
)
}
if h.BlindingPoint != nil {
addRecord(
record.BlindingPointOnionType,
btcec.PubKeyBytesLenCompressed,
)
}
// Add metadata if present.
if h.Metadata != nil {
addRecord(record.MetadataOnionType, uint64(len(h.Metadata)))
}
if h.TotalAmtMsat != 0 {
addRecord(
record.TotalAmtMsatBlindedType,
tlv.SizeTUint64(uint64(h.AmtToForward)),
)
}
// Add custom records.
for k, v := range h.CustomRecords {
addRecord(tlv.Type(k), uint64(len(v)))

View file

@ -162,6 +162,8 @@ func TestAMPHop(t *testing.T) {
// TestPayloadSize tests the payload size calculation that is provided by Hop
// structs.
func TestPayloadSize(t *testing.T) {
t.Parallel()
hops := []*Hop{
{
PubKeyBytes: testPubKeyBytes,
@ -181,7 +183,9 @@ func TestPayloadSize(t *testing.T) {
AmtToForward: 1200,
OutgoingTimeLock: 700000,
MPP: record.NewMPP(500, [32]byte{}),
AMP: record.NewAMP([32]byte{}, [32]byte{}, 8),
AMP: record.NewAMP(
[32]byte{}, [32]byte{}, 8,
),
CustomRecords: map[uint64][]byte{
100000: {1, 2, 3},
1000000: {4, 5},
@ -190,6 +194,69 @@ func TestPayloadSize(t *testing.T) {
},
}
blindedHops := []*Hop{
{
// Unblinded hop to introduction node.
PubKeyBytes: testPubKeyBytes,
AmtToForward: 1000,
OutgoingTimeLock: 600000,
ChannelID: 3432483437438,
LegacyPayload: true,
},
{
// Payload for an introduction node in a blinded route
// that has the blinding point provided in the onion
// payload, and encrypted data pointing it to the next
// node.
PubKeyBytes: testPubKeyBytes,
EncryptedData: []byte{12, 13},
BlindingPoint: testPubKey,
},
{
// Payload for a forwarding node in a blinded route
// that has encrypted data provided in the onion
// payload, but no blinding point (it's provided in
// update_add_htlc).
PubKeyBytes: testPubKeyBytes,
EncryptedData: []byte{12, 13},
},
{
// Final hop has encrypted data and other final hop
// fields like metadata.
PubKeyBytes: testPubKeyBytes,
AmtToForward: 900,
OutgoingTimeLock: 50000,
Metadata: []byte{10, 11},
EncryptedData: []byte{12, 13},
TotalAmtMsat: lnwire.MilliSatoshi(900),
},
}
testCases := []struct {
name string
hops []*Hop
}{
{
name: "clear route",
hops: hops,
},
{
name: "blinded route",
hops: blindedHops,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
testPayloadSize(t, testCase.hops)
})
}
}
func testPayloadSize(t *testing.T, hops []*Hop) {
rt := Route{
Hops: hops,
}