Merge pull request #8159 from carlaKC/7298-1-forwardblindedroutes

[1/3]: Preparatory work for Forwarding Blinded Routes
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2024-03-28 15:13:05 -07:00
commit 1d61de28c0
26 files changed
+1262 -160

No files matched your search

+67 -1
View File
@@ -35,6 +35,10 @@ const (
// begins to be interpreted as an absolute block height, rather than a
// relative one.
AbsoluteThawHeightThreshold uint32 = 500000
// HTLCBlindingPointTLV is the tlv type used for storing blinding
// points with HTLCs.
HTLCBlindingPointTLV tlv.Type = 0
)
var (
@@ -2316,7 +2320,56 @@ type HTLC struct {
// Note that this extra data is stored inline with the OnionBlob for
// legacy reasons, see serialization/deserialization functions for
// detail.
ExtraData []byte
ExtraData lnwire.ExtraOpaqueData
// BlindingPoint is an optional blinding point included with the HTLC.
//
// Note: this field is not a part of on-disk representation of the
// HTLC. It is stored in the ExtraData field, which is used to store
// a TLV stream of additional information associated with the HTLC.
BlindingPoint lnwire.BlindingPointRecord
}
// serializeExtraData encodes a TLV stream of extra data to be stored with a
// HTLC. It uses the update_add_htlc TLV types, because this is where extra
// data is passed with a HTLC. At present blinding points are the only extra
// data that we will store, and the function is a no-op if a nil blinding
// point is provided.
//
// This function MUST be called to persist all HTLC values when they are
// serialized.
func (h *HTLC) serializeExtraData() error {
var records []tlv.RecordProducer
h.BlindingPoint.WhenSome(func(b tlv.RecordT[lnwire.BlindingPointTlvType,
*btcec.PublicKey]) {
records = append(records, &b)
})
return h.ExtraData.PackRecords(records...)
}
// deserializeExtraData extracts TLVs from the extra data persisted for the
// htlc and populates values in the struct accordingly.
//
// This function MUST be called to populate the struct properly when HTLCs
// are deserialized.
func (h *HTLC) deserializeExtraData() error {
if len(h.ExtraData) == 0 {
return nil
}
blindingPoint := h.BlindingPoint.Zero()
tlvMap, err := h.ExtraData.ExtractRecords(&blindingPoint)
if err != nil {
return err
}
if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil {
h.BlindingPoint = tlv.SomeRecordT(blindingPoint)
}
return nil
}
// SerializeHtlcs writes out the passed set of HTLC's into the passed writer
@@ -2340,6 +2393,12 @@ func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error {
}
for _, htlc := range htlcs {
// Populate TLV stream for any additional fields contained
// in the TLV.
if err := htlc.serializeExtraData(); err != nil {
return err
}
// The onion blob and hltc data are stored as a single var
// bytes blob.
onionAndExtraData := make(
@@ -2425,6 +2484,12 @@ func DeserializeHtlcs(r io.Reader) ([]HTLC, error) {
onionAndExtraData[lnwire.OnionPacketSize:],
)
}
// Finally, deserialize any TLVs contained in that extra data
// if they are present.
if err := htlcs[i].deserializeExtraData(); err != nil {
return nil, err
}
}
return htlcs, nil
@@ -2440,6 +2505,7 @@ func (h *HTLC) Copy() HTLC {
}
copy(clone.Signature[:], h.Signature)
copy(clone.RHash[:], h.RHash[:])
copy(clone.ExtraData, h.ExtraData)
return clone
}
+37 -34
View File
@@ -23,6 +23,7 @@ import (
"github.com/lightningnetwork/lnd/lntest/channels"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
@@ -651,8 +652,7 @@ func TestChannelStateTransition(t *testing.T) {
{
LogIndex: 2,
UpdateMsg: &lnwire.UpdateAddHTLC{
ChanID: lnwire.ChannelID{1, 2, 3},
ExtraData: make([]byte, 0),
ChanID: lnwire.ChannelID{1, 2, 3},
},
},
}
@@ -710,25 +710,22 @@ func TestChannelStateTransition(t *testing.T) {
wireSig,
wireSig,
},
ExtraData: make([]byte, 0),
},
LogUpdates: []LogUpdate{
{
LogIndex: 1,
UpdateMsg: &lnwire.UpdateAddHTLC{
ID: 1,
Amount: lnwire.NewMSatFromSatoshis(100),
Expiry: 25,
ExtraData: make([]byte, 0),
ID: 1,
Amount: lnwire.NewMSatFromSatoshis(100),
Expiry: 25,
},
},
{
LogIndex: 2,
UpdateMsg: &lnwire.UpdateAddHTLC{
ID: 2,
Amount: lnwire.NewMSatFromSatoshis(200),
Expiry: 50,
ExtraData: make([]byte, 0),
ID: 2,
Amount: lnwire.NewMSatFromSatoshis(200),
Expiry: 50,
},
},
},
@@ -1610,9 +1607,25 @@ func TestHTLCsExtraData(t *testing.T) {
OnionBlob: lnmock.MockOnion(),
}
// Add a blinding point to a htlc.
blindingPointHTLC := HTLC{
Signature: testSig.Serialize(),
Incoming: false,
Amt: 10,
RHash: key,
RefundTimeout: 1,
OnionBlob: lnmock.MockOnion(),
BlindingPoint: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
pubKey,
),
),
}
testCases := []struct {
name string
htlcs []HTLC
name string
htlcs []HTLC
blindingIdx int
}{
{
// Serialize multiple HLTCs with no extra data to
@@ -1624,30 +1637,12 @@ func TestHTLCsExtraData(t *testing.T) {
},
},
{
// Some HTLCs with extra data, some without.
name: "mixed extra data",
htlcs: []HTLC{
mockHtlc,
{
Signature: testSig.Serialize(),
Incoming: false,
Amt: 10,
RHash: key,
RefundTimeout: 1,
OnionBlob: lnmock.MockOnion(),
ExtraData: []byte{1, 2, 3},
},
blindingPointHTLC,
mockHtlc,
{
Signature: testSig.Serialize(),
Incoming: false,
Amt: 10,
RHash: key,
RefundTimeout: 1,
OnionBlob: lnmock.MockOnion(),
ExtraData: bytes.Repeat(
[]byte{9}, 999,
),
},
},
},
}
@@ -1665,7 +1660,15 @@ func TestHTLCsExtraData(t *testing.T) {
r := bytes.NewReader(b.Bytes())
htlcs, err := DeserializeHtlcs(r)
require.NoError(t, err)
require.Equal(t, testCase.htlcs, htlcs)
require.EqualValues(t, len(testCase.htlcs), len(htlcs))
for i, htlc := range htlcs {
// We use the extra data field when we
// serialize, so we set to nil to be able to
// assert on equal for the test.
htlc.ExtraData = nil
require.Equal(t, testCase.htlcs[i], htlc)
}
})
}
}
+2 -2
View File
@@ -1250,9 +1250,9 @@ func parseBlindedPaymentParameters(ctx *cli.Context) (
BaseFeeMsat: ctx.Uint64(
blindedBaseFlag.Name,
),
ProportionalFeeMsat: ctx.Uint64(
ProportionalFeeRate: uint32(ctx.Uint64(
blindedPPMFlag.Name,
),
)),
TotalCltvDelta: uint32(ctx.Uint64(
blindedCLTVFlag.Name,
)),
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/lightningnetwork/lnd/channeldb"
@@ -17,6 +18,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/tlv"
)
// htlcIncomingContestResolver is a ContractResolver that's able to resolve an
@@ -520,9 +522,18 @@ func (h *htlcIncomingContestResolver) Supplement(htlc channeldb.HTLC) {
func (h *htlcIncomingContestResolver) decodePayload() (*hop.Payload,
[]byte, error) {
var blindingPoint *btcec.PublicKey
h.htlc.BlindingPoint.WhenSome(
func(b tlv.RecordT[lnwire.BlindingPointTlvType,
*btcec.PublicKey]) {
blindingPoint = b.Val
},
)
onionReader := bytes.NewReader(h.htlc.OnionBlob[:])
iterator, err := h.OnionProcessor.ReconstructHopIterator(
onionReader, h.htlc.RHash[:],
onionReader, h.htlc.RHash[:], blindingPoint,
)
if err != nil {
return nil, nil, err
@@ -6,6 +6,7 @@ import (
"io/ioutil"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
@@ -288,8 +289,8 @@ type mockOnionProcessor struct {
offeredOnionBlob []byte
}
func (o *mockOnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
hop.Iterator, error) {
func (o *mockOnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
blindingPoint *btcec.PublicKey) (hop.Iterator, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
@@ -40,7 +41,8 @@ type Registry interface {
type OnionProcessor interface {
// ReconstructHopIterator attempts to decode a valid sphinx packet from
// the passed io.Reader instance.
ReconstructHopIterator(r io.Reader, rHash []byte) (hop.Iterator, error)
ReconstructHopIterator(r io.Reader, rHash []byte,
blindingKey *btcec.PublicKey) (hop.Iterator, error)
}
// UtxoSweeper defines the sweep functions that contract court requires.
@@ -172,6 +172,9 @@
* When computing a minimum fee for transaction construction, `lnd` [now takes our
bitcoin peers' feefilter values into account](https://github.com/lightningnetwork/lnd/pull/8418).
* [Preparatory work](https://github.com/lightningnetwork/lnd/pull/8159) for
forwarding of blinded routes was added.
## RPC Additions
* [Deprecated](https://github.com/lightningnetwork/lnd/pull/7175)
+23 -51
View File
@@ -147,67 +147,30 @@ func (p *OnionProcessor) Stop() error {
return nil
}
// DecodeHopIterator attempts to decode a valid sphinx packet from the passed io.Reader
// instance using the rHash as the associated data when checking the relevant
// MACs during the decoding process.
func (p *OnionProcessor) DecodeHopIterator(r io.Reader, rHash []byte,
incomingCltv uint32) (Iterator, lnwire.FailCode) {
onionPkt := &sphinx.OnionPacket{}
if err := onionPkt.Decode(r); err != nil {
switch err {
case sphinx.ErrInvalidOnionVersion:
return nil, lnwire.CodeInvalidOnionVersion
case sphinx.ErrInvalidOnionKey:
return nil, lnwire.CodeInvalidOnionKey
default:
log.Errorf("unable to decode onion packet: %v", err)
return nil, lnwire.CodeInvalidOnionKey
}
}
// Attempt to process the Sphinx packet. We include the payment hash of
// the HTLC as it's authenticated within the Sphinx packet itself as
// associated data in order to thwart attempts a replay attacks. In the
// case of a replay, an attacker is *forced* to use the same payment
// hash twice, thereby losing their money entirely.
sphinxPacket, err := p.router.ProcessOnionPacket(
onionPkt, rHash, incomingCltv,
)
if err != nil {
switch err {
case sphinx.ErrInvalidOnionVersion:
return nil, lnwire.CodeInvalidOnionVersion
case sphinx.ErrInvalidOnionHMAC:
return nil, lnwire.CodeInvalidOnionHmac
case sphinx.ErrInvalidOnionKey:
return nil, lnwire.CodeInvalidOnionKey
default:
log.Errorf("unable to process onion packet: %v", err)
return nil, lnwire.CodeInvalidOnionKey
}
}
return makeSphinxHopIterator(onionPkt, sphinxPacket), lnwire.CodeNone
}
// ReconstructHopIterator attempts to decode a valid sphinx packet from the passed io.Reader
// instance using the rHash as the associated data when checking the relevant
// MACs during the decoding process.
func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
Iterator, error) {
func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
blindingPoint *btcec.PublicKey) (Iterator, error) {
onionPkt := &sphinx.OnionPacket{}
if err := onionPkt.Decode(r); err != nil {
return nil, err
}
var opts []sphinx.ProcessOnionOpt
if blindingPoint != nil {
opts = append(opts, sphinx.WithBlindingPoint(blindingPoint))
}
// Attempt to process the Sphinx packet. We include the payment hash of
// the HTLC as it's authenticated within the Sphinx packet itself as
// associated data in order to thwart attempts a replay attacks. In the
// case of a replay, an attacker is *forced* to use the same payment
// hash twice, thereby losing their money entirely.
sphinxPacket, err := p.router.ReconstructOnionPacket(onionPkt, rHash)
sphinxPacket, err := p.router.ReconstructOnionPacket(
onionPkt, rHash, opts...,
)
if err != nil {
return nil, err
}
@@ -219,9 +182,11 @@ func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
// packet, perform sphinx replay detection, and schedule the entry for garbage
// collection.
type DecodeHopIteratorRequest struct {
OnionReader io.Reader
RHash []byte
IncomingCltv uint32
OnionReader io.Reader
RHash []byte
IncomingCltv uint32
IncomingAmount lnwire.MilliSatoshi
BlindingPoint *btcec.PublicKey
}
// DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion
@@ -277,8 +242,15 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte,
return lnwire.CodeInvalidOnionKey
}
var opts []sphinx.ProcessOnionOpt
if req.BlindingPoint != nil {
opts = append(opts, sphinx.WithBlindingPoint(
req.BlindingPoint,
))
}
err = tx.ProcessOnionPacket(
seqNum, onionPkt, req.RHash, req.IncomingCltv,
seqNum, onionPkt, req.RHash, req.IncomingCltv, opts...,
)
switch err {
case nil:
+74
View File
@@ -28,6 +28,10 @@ const (
// RequiredViolation indicates that an unknown even type was found in
// the payload that we could not process.
RequiredViolation
// InsufficientViolation indicates that the provided type does
// not satisfy constraints.
InsufficientViolation
)
// String returns a human-readable description of the violation as a verb.
@@ -42,6 +46,9 @@ func (v PayloadViolation) String() string {
case RequiredViolation:
return "required"
case InsufficientViolation:
return "insufficient"
default:
return "unknown violation"
}
@@ -410,3 +417,70 @@ func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
return nil
}
// ValidateBlindedRouteData performs the additional validation that is
// required for payments that rely on data provided in an encrypted blob to
// be forwarded. We enforce the blinded route's maximum expiry height so that
// the route "expires" and a malicious party does not have endless opportunity
// to probe the blinded route and compare it to updated channel policies in
// the network.
//
// Note that this function only validates blinded route data for forwarding
// nodes, as LND does not yet support receiving via a blinded route (which has
// different validation rules).
func ValidateBlindedRouteData(blindedData *record.BlindedRouteData,
incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error {
// Bolt 04 notes that we should enforce payment constraints _if_ they
// are present, so we do not fail if not provided.
var err error
blindedData.Constraints.WhenSome(
func(c tlv.RecordT[tlv.TlvType12, record.PaymentConstraints]) {
// MUST fail if the expiry is greater than
// max_cltv_expiry.
if incomingTimelock > c.Val.MaxCltvExpiry {
err = ErrInvalidPayload{
Type: record.LockTimeOnionType,
Violation: InsufficientViolation,
}
}
// MUST fail if the amount is below htlc_minimum_msat.
if incomingAmount < c.Val.HtlcMinimumMsat {
err = ErrInvalidPayload{
Type: record.AmtOnionType,
Violation: InsufficientViolation,
}
}
},
)
if err != nil {
return err
}
// Fail if we don't understand any features (even or odd), because we
// expect the features to have been set from our announcement. If the
// feature vector TLV is not included, it's interpreted as an empty
// vector (no validation required).
// expect the features to have been set from our announcement.
//
// Note that we do not yet check the features that the blinded payment
// is using against our own features, because there are currently no
// payment-related features that they utilize other than tlv-onion,
// which is implicitly supported.
blindedData.Features.WhenSome(
func(f tlv.RecordT[tlv.TlvType14, lnwire.FeatureVector]) {
if f.Val.UnknownFeatures() {
err = ErrInvalidPayload{
Type: 14,
Violation: IncludedViolation,
}
}
},
)
if err != nil {
return err
}
return nil
}
+138
View File
@@ -557,3 +557,141 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
t.Fatalf("invalid custom records")
}
}
// TestValidateBlindedRouteData tests validation of the values provided in a
// blinded route.
func TestValidateBlindedRouteData(t *testing.T) {
scid := lnwire.NewShortChanIDFromInt(1)
tests := []struct {
name string
data *record.BlindedRouteData
incomingAmount lnwire.MilliSatoshi
incomingTimelock uint32
err error
}{
{
name: "max cltv expired",
data: record.NewBlindedRouteData(
scid,
nil,
record.PaymentRelayInfo{},
&record.PaymentConstraints{
MaxCltvExpiry: 100,
},
nil,
),
incomingTimelock: 200,
err: hop.ErrInvalidPayload{
Type: record.LockTimeOnionType,
Violation: hop.InsufficientViolation,
},
},
{
name: "zero max cltv",
data: record.NewBlindedRouteData(
scid,
nil,
record.PaymentRelayInfo{},
&record.PaymentConstraints{
MaxCltvExpiry: 0,
HtlcMinimumMsat: 10,
},
nil,
),
incomingAmount: 100,
incomingTimelock: 10,
err: hop.ErrInvalidPayload{
Type: record.LockTimeOnionType,
Violation: hop.InsufficientViolation,
},
},
{
name: "amount below minimum",
data: record.NewBlindedRouteData(
scid,
nil,
record.PaymentRelayInfo{},
&record.PaymentConstraints{
HtlcMinimumMsat: 15,
},
nil,
),
incomingAmount: 10,
err: hop.ErrInvalidPayload{
Type: record.AmtOnionType,
Violation: hop.InsufficientViolation,
},
},
{
name: "valid, no features",
data: record.NewBlindedRouteData(
scid,
nil,
record.PaymentRelayInfo{},
&record.PaymentConstraints{
MaxCltvExpiry: 100,
HtlcMinimumMsat: 20,
},
nil,
),
incomingAmount: 40,
incomingTimelock: 80,
},
{
name: "unknown features",
data: record.NewBlindedRouteData(
scid,
nil,
record.PaymentRelayInfo{},
&record.PaymentConstraints{
MaxCltvExpiry: 100,
HtlcMinimumMsat: 20,
},
lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(
lnwire.FeatureBit(9999),
),
lnwire.Features,
),
),
incomingAmount: 40,
incomingTimelock: 80,
err: hop.ErrInvalidPayload{
Type: 14,
Violation: hop.IncludedViolation,
},
},
{
name: "valid data",
data: record.NewBlindedRouteData(
scid,
nil,
record.PaymentRelayInfo{
CltvExpiryDelta: 10,
FeeRate: 10,
BaseFee: 100,
},
&record.PaymentConstraints{
MaxCltvExpiry: 100,
HtlcMinimumMsat: 20,
},
nil,
),
incomingAmount: 40,
incomingTimelock: 80,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
err := hop.ValidateBlindedRouteData(
testCase.data, testCase.incomingAmount,
testCase.incomingTimelock,
)
require.Equal(t, testCase.err, err)
})
}
}
+5 -3
View File
@@ -3160,9 +3160,11 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
onionReader := bytes.NewReader(pd.OnionBlob)
req := hop.DecodeHopIteratorRequest{
OnionReader: onionReader,
RHash: pd.RHash[:],
IncomingCltv: pd.Timeout,
OnionReader: onionReader,
RHash: pd.RHash[:],
IncomingCltv: pd.Timeout,
IncomingAmount: pd.Amount,
BlindingPoint: pd.BlindingPoint,
}
decodeReqs = append(decodeReqs, req)
+8 -7
View File
@@ -11992,8 +11992,9 @@ type BlindedPaymentPath struct {
BlindedPath *BlindedPath `protobuf:"bytes,1,opt,name=blinded_path,json=blindedPath,proto3" json:"blinded_path,omitempty"`
// The base fee for the blinded path provided, expressed in msat.
BaseFeeMsat uint64 `protobuf:"varint,2,opt,name=base_fee_msat,json=baseFeeMsat,proto3" json:"base_fee_msat,omitempty"`
// The proportional fee for the blinded path provided, expressed in msat.
ProportionalFeeMsat uint64 `protobuf:"varint,3,opt,name=proportional_fee_msat,json=proportionalFeeMsat,proto3" json:"proportional_fee_msat,omitempty"`
// The proportional fee for the blinded path provided, expressed in parts
// per million.
ProportionalFeeRate uint32 `protobuf:"varint,3,opt,name=proportional_fee_rate,json=proportionalFeeRate,proto3" json:"proportional_fee_rate,omitempty"`
// The total CLTV delta for the blinded path provided, including the
// final CLTV delta for the receiving node.
TotalCltvDelta uint32 `protobuf:"varint,4,opt,name=total_cltv_delta,json=totalCltvDelta,proto3" json:"total_cltv_delta,omitempty"`
@@ -12053,9 +12054,9 @@ func (x *BlindedPaymentPath) GetBaseFeeMsat() uint64 {
return 0
}
func (x *BlindedPaymentPath) GetProportionalFeeMsat() uint64 {
func (x *BlindedPaymentPath) GetProportionalFeeRate() uint32 {
if x != nil {
return x.ProportionalFeeMsat
return x.ProportionalFeeRate
}
return 0
}
@@ -19611,9 +19612,9 @@ var file_lightning_proto_rawDesc = []byte{
0x74, 0x68, 0x12, 0x22, 0x0a, 0x0d, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d,
0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x61, 0x73, 0x65, 0x46,
0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72,
0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18,
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f,
0x6e, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f,
0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6f,
0x6e, 0x61, 0x6c, 0x46, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f,
0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6c, 0x74, 0x76, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6c, 0x74, 0x76, 0x44,
0x65, 0x6c, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0d, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x69, 0x6e,
+5 -2
View File
@@ -3545,8 +3545,11 @@ message BlindedPaymentPath {
// The base fee for the blinded path provided, expressed in msat.
uint64 base_fee_msat = 2;
// The proportional fee for the blinded path provided, expressed in msat.
uint64 proportional_fee_msat = 3;
/*
The proportional fee for the blinded path provided, expressed in parts
per million.
*/
uint32 proportional_fee_rate = 3;
/*
The total CLTV delta for the blinded path provided, including the
+4 -4
View File
@@ -3523,10 +3523,10 @@
"format": "uint64",
"description": "The base fee for the blinded path provided, expressed in msat."
},
"proportional_fee_msat": {
"type": "string",
"format": "uint64",
"description": "The proportional fee for the blinded path provided, expressed in msat."
"proportional_fee_rate": {
"type": "integer",
"format": "int64",
"description": "The proportional fee for the blinded path provided, expressed in parts\nper million."
},
"total_cltv_delta": {
"type": "integer",
+7 -9
View File
@@ -487,15 +487,13 @@ func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
}
return &routing.BlindedPayment{
BlindedPath: path,
CltvExpiryDelta: uint16(rpcPayment.TotalCltvDelta),
BaseFee: uint32(rpcPayment.BaseFeeMsat),
ProportionalFee: uint32(
rpcPayment.ProportionalFeeMsat,
),
HtlcMinimum: rpcPayment.HtlcMinMsat,
HtlcMaximum: rpcPayment.HtlcMaxMsat,
Features: features,
BlindedPath: path,
CltvExpiryDelta: uint16(rpcPayment.TotalCltvDelta),
BaseFee: uint32(rpcPayment.BaseFeeMsat),
ProportionalFeeRate: rpcPayment.ProportionalFeeRate,
HtlcMinimum: rpcPayment.HtlcMinMsat,
HtlcMaximum: rpcPayment.HtlcMaxMsat,
Features: features,
}, nil
}
+71 -13
View File
@@ -31,6 +31,7 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain"
"github.com/lightningnetwork/lnd/tlv"
)
var (
@@ -371,6 +372,12 @@ type PaymentDescriptor struct {
// isForwarded denotes if an incoming HTLC has been forwarded to any
// possible upstream peers in the route.
isForwarded bool
// BlindingPoint is an optional ephemeral key used in route blinding.
// This value is set for nodes that are relaying payments inside of a
// blinded route (ie, not the introduction node) from update_add_htlc's
// TLVs.
BlindingPoint *btcec.PublicKey
}
// PayDescsFromRemoteLogUpdates converts a slice of LogUpdates received from the
@@ -411,6 +418,7 @@ func PayDescsFromRemoteLogUpdates(chanID lnwire.ShortChannelID, height uint64,
Height: height,
Index: uint16(i),
},
BlindingPoint: wireMsg.BlingingPointOrNil(),
}
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
@@ -736,6 +744,14 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment {
Incoming: false,
}
copy(h.OnionBlob[:], htlc.OnionBlob)
if htlc.BlindingPoint != nil {
h.BlindingPoint = tlv.SomeRecordT(
//nolint:lll
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
htlc.BlindingPoint,
),
)
}
if ourCommit && htlc.sig != nil {
h.Signature = htlc.sig.Serialize()
@@ -760,7 +776,14 @@ func (c *commitment) toDiskCommit(ourCommit bool) *channeldb.ChannelCommitment {
Incoming: true,
}
copy(h.OnionBlob[:], htlc.OnionBlob)
if htlc.BlindingPoint != nil {
h.BlindingPoint = tlv.SomeRecordT(
//nolint:lll
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
htlc.BlindingPoint,
),
)
}
if ourCommit && htlc.sig != nil {
h.Signature = htlc.sig.Serialize()
}
@@ -859,6 +882,12 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight,
theirWitnessScript: theirWitnessScript,
}
htlc.BlindingPoint.WhenSome(func(b tlv.RecordT[
lnwire.BlindingPointTlvType, *btcec.PublicKey]) {
pd.BlindingPoint = b.Val
})
return pd, nil
}
@@ -1548,6 +1577,7 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
HtlcIndex: wireMsg.ID,
LogIndex: logUpdate.LogIndex,
addCommitHeightRemote: commitHeight,
BlindingPoint: wireMsg.BlingingPointOrNil(),
}
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
copy(pd.OnionBlob[:], wireMsg.OnionBlob[:])
@@ -1745,6 +1775,7 @@ func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpd
HtlcIndex: wireMsg.ID,
LogIndex: logUpdate.LogIndex,
addCommitHeightLocal: commitHeight,
BlindingPoint: wireMsg.BlingingPointOrNil(),
}
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
copy(pd.OnionBlob, wireMsg.OnionBlob[:])
@@ -3607,6 +3638,14 @@ func (lc *LightningChannel) createCommitDiff(
PaymentHash: pd.RHash,
}
copy(htlc.OnionBlob[:], pd.OnionBlob)
if pd.BlindingPoint != nil {
htlc.BlindingPoint = tlv.SomeRecordT(
//nolint:lll
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
pd.BlindingPoint,
),
)
}
logUpdate.UpdateMsg = htlc
// Gather any references for circuits opened by this Add
@@ -3736,12 +3775,21 @@ func (lc *LightningChannel) getUnsignedAckedUpdates() []channeldb.LogUpdate {
// four messages that it corresponds to.
switch pd.EntryType {
case Add:
var b lnwire.BlindingPointRecord
if pd.BlindingPoint != nil {
tlv.SomeRecordT(
//nolint:lll
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](pd.BlindingPoint),
)
}
htlc := &lnwire.UpdateAddHTLC{
ChanID: chanID,
ID: pd.HtlcIndex,
Amount: pd.Amount,
Expiry: pd.Timeout,
PaymentHash: pd.RHash,
ChanID: chanID,
ID: pd.HtlcIndex,
Amount: pd.Amount,
Expiry: pd.Timeout,
PaymentHash: pd.RHash,
BlindingPoint: b,
}
copy(htlc.OnionBlob[:], pd.OnionBlob)
logUpdate.UpdateMsg = htlc
@@ -5742,6 +5790,14 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) (
Expiry: pd.Timeout,
PaymentHash: pd.RHash,
}
if pd.BlindingPoint != nil {
htlc.BlindingPoint = tlv.SomeRecordT(
//nolint:lll
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
pd.BlindingPoint,
),
)
}
copy(htlc.OnionBlob[:], pd.OnionBlob)
logUpdate.UpdateMsg = htlc
addUpdates = append(addUpdates, logUpdate)
@@ -6079,6 +6135,7 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC,
HtlcIndex: lc.localUpdateLog.htlcCounter,
OnionBlob: htlc.OnionBlob[:],
OpenCircuitKey: openKey,
BlindingPoint: htlc.BlingingPointOrNil(),
}
}
@@ -6129,13 +6186,14 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, err
}
pd := &PaymentDescriptor{
EntryType: Add,
RHash: PaymentHash(htlc.PaymentHash),
Timeout: htlc.Expiry,
Amount: htlc.Amount,
LogIndex: lc.remoteUpdateLog.logIndex,
HtlcIndex: lc.remoteUpdateLog.htlcCounter,
OnionBlob: htlc.OnionBlob[:],
EntryType: Add,
RHash: PaymentHash(htlc.PaymentHash),
Timeout: htlc.Expiry,
Amount: htlc.Amount,
LogIndex: lc.remoteUpdateLog.logIndex,
HtlcIndex: lc.remoteUpdateLog.htlcCounter,
OnionBlob: htlc.OnionBlob[:],
BlindingPoint: htlc.BlingingPointOrNil(),
}
localACKedIndex := lc.remoteCommitChain.tail().ourMessageIndex
+66 -5
View File
@@ -25,6 +25,7 @@ import (
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
@@ -3037,7 +3038,6 @@ func TestChanSyncOweCommitment(t *testing.T) {
Amount: htlcAmt,
Expiry: uint32(10),
OnionBlob: fakeOnionBlob,
ExtraData: make([]byte, 0),
}
htlcIndex, err := bobChannel.AddHTLC(h, nil)
@@ -3082,7 +3082,6 @@ func TestChanSyncOweCommitment(t *testing.T) {
Amount: htlcAmt,
Expiry: uint32(10),
OnionBlob: fakeOnionBlob,
ExtraData: make([]byte, 0),
}
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
require.NoError(t, err, "unable to add alice's htlc")
@@ -10421,8 +10420,9 @@ func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC {
_, err = rand.Read(sig)
require.NoError(t, err)
extra := make([]byte, 1000)
_, err = rand.Read(extra)
blinding, err := pubkeyFromHex(
"0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d48236c39", //nolint:lll
)
require.NoError(t, err)
return channeldb.HTLC{
@@ -10435,7 +10435,10 @@ func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC {
OnionBlob: onionBlob,
HtlcIndex: rand.Uint64(),
LogIndex: rand.Uint64(),
ExtraData: extra,
BlindingPoint: tlv.SomeRecordT(
//nolint:lll
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](blinding),
),
}
}
@@ -11002,3 +11005,61 @@ func TestEnforceFeeBuffer(t *testing.T) {
require.Equal(t, aliceBalance, expectedAmt)
}
// TestBlindingPointPersistence tests persistence of blinding points attached
// to htlcs across restarts.
func TestBlindingPointPersistence(t *testing.T) {
// Create a test channel which will be used for the duration of this
// test. The channel will be funded evenly with Alice having 5 BTC,
// and Bob having 5 BTC.
aliceChannel, bobChannel, err := CreateTestChannels(
t, channeldb.SingleFunderTweaklessBit,
)
require.NoError(t, err, "unable to create test channels")
// Send a HTLC from Alice to Bob that has a blinding point populated.
htlc, _ := createHTLC(0, 100_000_000)
blinding, err := pubkeyFromHex(
"0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d48236c39", //nolint:lll
)
require.NoError(t, err)
htlc.BlindingPoint = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](blinding),
)
_, err = aliceChannel.AddHTLC(htlc, nil)
require.NoError(t, err)
_, err = bobChannel.ReceiveHTLC(htlc)
require.NoError(t, err)
// Now, Alice will send a new commitment to Bob, which will persist our
// pending HTLC to disk.
aliceCommit, err := aliceChannel.SignNextCommitment()
require.NoError(t, err, "unable to sign commitment")
// Restart alice to force fetching state from disk.
aliceChannel, err = restartChannel(aliceChannel)
require.NoError(t, err, "unable to restart alice")
// Assert that the blinding point is restored from disk.
remoteCommit := aliceChannel.remoteCommitChain.tip()
require.Len(t, remoteCommit.outgoingHTLCs, 1)
require.Equal(t, blinding, remoteCommit.outgoingHTLCs[0].BlindingPoint)
// Next, update bob's commitment and assert that we can still retrieve
// his incoming blinding point after restart.
err = bobChannel.ReceiveNewCommitment(aliceCommit.CommitSigs)
require.NoError(t, err, "bob unable to receive new commitment")
_, _, _, err = bobChannel.RevokeCurrentCommitment()
require.NoError(t, err, "bob unable to revoke current commitment")
bobChannel, err = restartChannel(bobChannel)
require.NoError(t, err, "unable to restart bob's channel")
// Assert that Bob is able to recover the blinding point from disk.
bobCommit := bobChannel.localCommitChain.tip()
require.Len(t, bobCommit.incomingHTLCs, 1)
require.Equal(t, blinding, bobCommit.incomingHTLCs[0].BlindingPoint)
}
+10 -8
View File
@@ -19,7 +19,7 @@ type ChannelType RawFeatureVector
// featureBitLen returns the length in bytes of the encoded feature bits.
func (c ChannelType) featureBitLen() uint64 {
fv := RawFeatureVector(c)
return uint64(fv.SerializeSize())
return fv.sizeFunc()
}
// Record returns a TLV record that can be used to encode/decode the channel
@@ -34,25 +34,27 @@ func (c *ChannelType) Record() tlv.Record {
// channelTypeEncoder is a custom TLV encoder for the ChannelType record.
func channelTypeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*ChannelType); ok {
// Encode the feature bits as a byte slice without its length
// prepended, as that's already taken care of by the TLV record.
fv := RawFeatureVector(*v)
return fv.encode(w, fv.SerializeSize(), 8)
return rawFeatureEncoder(w, &fv, buf)
}
return tlv.NewTypeForEncodingErr(val, "lnwire.ChannelType")
return tlv.NewTypeForEncodingErr(val, "*lnwire.ChannelType")
}
// channelTypeDecoder is a custom TLV decoder for the ChannelType record.
func channelTypeDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
func channelTypeDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*ChannelType); ok {
fv := NewRawFeatureVector()
if err := fv.decode(r, int(l), 8); err != nil {
if err := rawFeatureDecoder(r, fv, buf, l); err != nil {
return err
}
*v = ChannelType(*fv)
return nil
}
return tlv.NewTypeForEncodingErr(val, "lnwire.ChannelType")
return tlv.NewTypeForEncodingErr(val, "*lnwire.ChannelType")
}
+93
View File
@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"io"
"github.com/lightningnetwork/lnd/tlv"
)
var (
@@ -612,6 +614,41 @@ func (fv *RawFeatureVector) decode(r io.Reader, length, width int) error {
return nil
}
// sizeFunc returns the length required to encode the feature vector.
func (fv *RawFeatureVector) sizeFunc() uint64 {
return uint64(fv.SerializeSize())
}
// Record returns a TLV record that can be used to encode/decode raw feature
// vectors. Note that the length of the feature vector is not included, because
// it is covered by the TLV record's length field.
func (fv *RawFeatureVector) Record(recordType tlv.Type) tlv.Record {
return tlv.MakeDynamicRecord(
recordType, fv, fv.sizeFunc, rawFeatureEncoder,
rawFeatureDecoder,
)
}
// rawFeatureEncoder is a custom TLV encoder for raw feature vectors.
func rawFeatureEncoder(w io.Writer, val interface{}, _ *[8]byte) error {
if f, ok := val.(*RawFeatureVector); ok {
return f.encode(w, f.SerializeSize(), 8)
}
return tlv.NewTypeForEncodingErr(val, "*lnwire.RawFeatureVector")
}
// rawFeatureDecoder is a custom TLV decoder for raw feature vectors.
func rawFeatureDecoder(r io.Reader, val interface{}, _ *[8]byte,
l uint64) error {
if f, ok := val.(*RawFeatureVector); ok {
return f.decode(r, int(l), 8)
}
return tlv.NewTypeForEncodingErr(val, "*lnwire.RawFeatureVector")
}
// FeatureVector represents a set of enabled features. The set stores
// information on enabled flags and metadata about the feature names. A feature
// vector is serializable to a compact byte representation that is included in
@@ -641,6 +678,50 @@ func EmptyFeatureVector() *FeatureVector {
return NewFeatureVector(nil, Features)
}
// Record implements the RecordProducer interface for FeatureVector. Note that
// it uses a zero-value type is used to produce the record, as we expect this
// type value to be overwritten when used in generic TLV record production.
// This allows a single Record function to serve in the many different contexts
// in which feature vectors are encoded. This record wraps the encoding/
// decoding for our raw feature vectors so that we can directly parse fully
// formed feature vector types.
func (fv *FeatureVector) Record() tlv.Record {
return tlv.MakeDynamicRecord(0, fv, fv.sizeFunc,
func(w io.Writer, val interface{}, buf *[8]byte) error {
if f, ok := val.(*FeatureVector); ok {
return rawFeatureEncoder(
w, f.RawFeatureVector, buf,
)
}
return tlv.NewTypeForEncodingErr(
val, "*lnwire.FeatureVector",
)
},
func(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if f, ok := val.(*FeatureVector); ok {
features := NewFeatureVector(nil, Features)
err := rawFeatureDecoder(
r, features.RawFeatureVector, buf, l,
)
if err != nil {
return err
}
*f = *features
return nil
}
return tlv.NewTypeForDecodingErr(
val, "*lnwire.FeatureVector", l, l,
)
},
)
}
// HasFeature returns whether a particular feature is included in the set. The
// feature can be seen as set either if the bit is set directly OR the queried
// bit has the same meaning as its corresponding even/odd bit, which is set
@@ -678,6 +759,18 @@ func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
return unknown
}
// UnknownFeatures returns a boolean if a feature vector contains *any*
// unknown features (even if they are odd).
func (fv *FeatureVector) UnknownFeatures() bool {
for feature := range fv.features {
if !fv.IsKnown(feature) {
return true
}
}
return false
}
// Name returns a string identifier for the feature represented by this bit. If
// the bit does not represent a known feature, this returns a string indicating
// as such.
+36
View File
@@ -1337,6 +1337,42 @@ func TestLightningWireProtocol(t *testing.T) {
v[0] = reflect.ValueOf(req)
},
MsgUpdateAddHTLC: func(v []reflect.Value, r *rand.Rand) {
req := &UpdateAddHTLC{
ID: r.Uint64(),
Amount: MilliSatoshi(r.Uint64()),
Expiry: r.Uint32(),
}
_, err := r.Read(req.ChanID[:])
require.NoError(t, err)
_, err = r.Read(req.PaymentHash[:])
require.NoError(t, err)
_, err = r.Read(req.OnionBlob[:])
require.NoError(t, err)
// Generate a blinding point 50% of the time, since not
// all update adds will use route blinding.
if r.Int31()%2 == 0 {
pubkey, err := randPubKey()
if err != nil {
t.Fatalf("unable to generate key: %v",
err)
return
}
req.BlindingPoint = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType0](
pubkey,
),
)
}
v[0] = reflect.ValueOf(*req)
},
}
// With the above types defined, we'll now generate a slice of
+67 -2
View File
@@ -3,6 +3,9 @@ package lnwire
import (
"bytes"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
)
// OnionPacketSize is the size of the serialized Sphinx onion packet included
@@ -11,6 +14,17 @@ import (
// of per-hop data, and a 32-byte HMAC over the entire packet.
const OnionPacketSize = 1366
type (
// BlindingPointTlvType is the type for ephemeral pubkeys used in
// route blinding.
BlindingPointTlvType = tlv.TlvType0
// BlindingPointRecord holds an optional blinding point on update add
// htlc.
//nolint:lll
BlindingPointRecord = tlv.OptionalRecordT[BlindingPointTlvType, *btcec.PublicKey]
)
// UpdateAddHTLC is the message sent by Alice to Bob when she wishes to add an
// HTLC to his remote commitment transaction. In addition to information
// detailing the value, the ID, expiry, and the onion blob is also included
@@ -54,12 +68,29 @@ type UpdateAddHTLC struct {
// used in the subsequent UpdateAddHTLC message.
OnionBlob [OnionPacketSize]byte
// BlindingPoint is the ephemeral pubkey used to optionally blind the
// next hop for this htlc.
BlindingPoint BlindingPointRecord
// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
ExtraData ExtraOpaqueData
}
// BlingingPointOrNil returns the blinding point associated with the update, or
// nil.
func (c *UpdateAddHTLC) BlingingPointOrNil() *btcec.PublicKey {
var blindingPoint *btcec.PublicKey
c.BlindingPoint.WhenSome(func(b tlv.RecordT[BlindingPointTlvType,
*btcec.PublicKey]) {
blindingPoint = b.Val
})
return blindingPoint
}
// NewUpdateAddHTLC returns a new empty UpdateAddHTLC message.
func NewUpdateAddHTLC() *UpdateAddHTLC {
return &UpdateAddHTLC{}
@@ -74,7 +105,7 @@ var _ Message = (*UpdateAddHTLC)(nil)
//
// This is part of the lnwire.Message interface.
func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
if err := ReadElements(r,
&c.ChanID,
&c.ID,
&c.Amount,
@@ -82,7 +113,27 @@ func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error {
&c.Expiry,
c.OnionBlob[:],
&c.ExtraData,
)
); err != nil {
return err
}
blindingRecord := c.BlindingPoint.Zero()
tlvMap, err := c.ExtraData.ExtractRecords(&blindingRecord)
if err != nil {
return err
}
if val, ok := tlvMap[c.BlindingPoint.TlvType()]; ok && val == nil {
c.BlindingPoint = tlv.SomeRecordT(blindingRecord)
}
// Set extra data to nil if we didn't parse anything out of it so that
// we can use assert.Equal in tests.
if len(tlvMap) == 0 {
c.ExtraData = nil
}
return nil
}
// Encode serializes the target UpdateAddHTLC into the passed io.Writer
@@ -114,6 +165,20 @@ func (c *UpdateAddHTLC) Encode(w *bytes.Buffer, pver uint32) error {
return err
}
// Only include blinding point in extra data if present.
var records []tlv.RecordProducer
c.BlindingPoint.WhenSome(func(b tlv.RecordT[BlindingPointTlvType,
*btcec.PublicKey]) {
records = append(records, &b)
})
err := EncodeMessageExtraData(&c.ExtraData, records...)
if err != nil {
return err
}
return WriteBytes(w, c.ExtraData)
}
+14 -2
View File
@@ -45,6 +45,7 @@ import (
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/subscribe"
"github.com/lightningnetwork/lnd/ticker"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/watchtower/wtclient"
)
@@ -1985,8 +1986,19 @@ func messageSummary(msg lnwire.Message) string {
msg.FeeSatoshis)
case *lnwire.UpdateAddHTLC:
return fmt.Sprintf("chan_id=%v, id=%v, amt=%v, expiry=%v, hash=%x",
msg.ChanID, msg.ID, msg.Amount, msg.Expiry, msg.PaymentHash[:])
var blindingPoint []byte
msg.BlindingPoint.WhenSome(
func(b tlv.RecordT[lnwire.BlindingPointTlvType,
*btcec.PublicKey]) {
blindingPoint = b.Val.SerializeCompressed()
},
)
return fmt.Sprintf("chan_id=%v, id=%v, amt=%v, expiry=%v, "+
"hash=%x, blinding_point=%x", msg.ChanID, msg.ID,
msg.Amount, msg.Expiry, msg.PaymentHash[:],
blindingPoint)
case *lnwire.UpdateFailHTLC:
return fmt.Sprintf("chan_id=%v, id=%v, reason=%x", msg.ChanID,
+304
View File
@@ -0,0 +1,304 @@
package record
import (
"bytes"
"encoding/binary"
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// BlindedRouteData contains the information that is included in a blinded
// route encrypted data blob that is created by the recipient to provide
// forwarding information.
type BlindedRouteData struct {
// ShortChannelID is the channel ID of the next hop.
ShortChannelID tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID]
// NextBlindingOverride is a blinding point that should be switched
// in for the next hop. This is used to combine two blinded paths into
// one (which primarily is used in onion messaging, but in theory
// could be used for payments as well).
NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey]
// RelayInfo provides the relay parameters for the hop.
RelayInfo tlv.RecordT[tlv.TlvType10, PaymentRelayInfo]
// Constraints provides the payment relay constraints for the hop.
Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints]
// Features is the set of features the payment requires.
Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector]
}
// NewBlindedRouteData creates the data that's provided for hops within a
// blinded route.
func NewBlindedRouteData(chanID lnwire.ShortChannelID,
blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo,
constraints *PaymentConstraints,
features *lnwire.FeatureVector) *BlindedRouteData {
info := &BlindedRouteData{
ShortChannelID: tlv.NewRecordT[tlv.TlvType2](chanID),
RelayInfo: tlv.NewRecordT[tlv.TlvType10](relayInfo),
}
if blindingOverride != nil {
info.NextBlindingOverride = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType8](blindingOverride))
}
if constraints != nil {
info.Constraints = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType12](*constraints))
}
if features != nil {
info.Features = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType14](*features),
)
}
return info
}
// DecodeBlindedRouteData decodes the data provided within a blinded route.
func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
var (
d BlindedRouteData
blindingOverride = d.NextBlindingOverride.Zero()
constraints = d.Constraints.Zero()
features = d.Features.Zero()
)
var tlvRecords lnwire.ExtraOpaqueData
if err := lnwire.ReadElements(r, &tlvRecords); err != nil {
return nil, err
}
typeMap, err := tlvRecords.ExtractRecords(
&d.ShortChannelID,
&blindingOverride, &d.RelayInfo.Val, &constraints,
&features,
)
if err != nil {
return nil, err
}
val, ok := typeMap[d.NextBlindingOverride.TlvType()]
if ok && val == nil {
d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride)
}
if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil {
d.Constraints = tlv.SomeRecordT(constraints)
}
if val, ok := typeMap[d.Features.TlvType()]; ok && val == nil {
d.Features = tlv.SomeRecordT(features)
}
return &d, nil
}
// EncodeBlindedRouteData encodes the blinded route data provided.
func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
var (
e lnwire.ExtraOpaqueData
recordProducers = make([]tlv.RecordProducer, 0, 5)
)
recordProducers = append(recordProducers, &data.ShortChannelID)
data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8,
*btcec.PublicKey]) {
recordProducers = append(recordProducers, &pk)
})
recordProducers = append(recordProducers, &data.RelayInfo.Val)
data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12,
PaymentConstraints]) {
recordProducers = append(recordProducers, &cs)
})
data.Features.WhenSome(func(f tlv.RecordT[tlv.TlvType14,
lnwire.FeatureVector]) {
recordProducers = append(recordProducers, &f)
})
if err := e.PackRecords(recordProducers...); err != nil {
return nil, err
}
return e[:], nil
}
// PaymentRelayInfo describes the relay policy for a blinded path.
type PaymentRelayInfo struct {
// CltvExpiryDelta is the expiry delta for the payment.
CltvExpiryDelta uint16
// FeeRate is the fee rate that will be charged per millionth of a
// satoshi.
FeeRate uint32
// BaseFee is the per-htlc fee charged.
BaseFee uint32
}
// newPaymentRelayRecord creates a tlv.Record that encodes the payment relay
// (type 10) type for an encrypted blob payload.
func (i *PaymentRelayInfo) Record() tlv.Record {
return tlv.MakeDynamicRecord(
10, &i, func() uint64 {
// uint16 + uint32 + tuint32
return 2 + 4 + tlv.SizeTUint32(i.BaseFee)
}, encodePaymentRelay, decodePaymentRelay,
)
}
func encodePaymentRelay(w io.Writer, val interface{}, buf *[8]byte) error {
if t, ok := val.(**PaymentRelayInfo); ok {
relayInfo := *t
// Just write our first 6 bytes directly.
binary.BigEndian.PutUint16(buf[:2], relayInfo.CltvExpiryDelta)
binary.BigEndian.PutUint32(buf[2:6], relayInfo.FeeRate)
if _, err := w.Write(buf[0:6]); err != nil {
return err
}
// We can safely reuse buf here because we overwrite its
// contents.
return tlv.ETUint32(w, &relayInfo.BaseFee, buf)
}
return tlv.NewTypeForEncodingErr(val, "**hop.PaymentRelayInfo")
}
func decodePaymentRelay(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if t, ok := val.(**PaymentRelayInfo); ok && l <= 10 {
scratch := make([]byte, l)
n, err := io.ReadFull(r, scratch)
if err != nil {
return err
}
// We expect at least 6 bytes, because we have 2 bytes for
// cltv delta and 4 bytes for fee rate.
if n < 6 {
return tlv.NewTypeForDecodingErr(val,
"*hop.paymentRelayInfo", uint64(n), 6)
}
relayInfo := *t
relayInfo.CltvExpiryDelta = binary.BigEndian.Uint16(
scratch[0:2],
)
relayInfo.FeeRate = binary.BigEndian.Uint32(scratch[2:6])
// To be able to re-use the DTUint32 function we create a
// buffer with just the bytes holding the variable length u32.
// If the base fee is zero, this will be an empty buffer, which
// is okay.
b := bytes.NewBuffer(scratch[6:])
return tlv.DTUint32(b, &relayInfo.BaseFee, buf, l-6)
}
return tlv.NewTypeForDecodingErr(val, "*hop.paymentRelayInfo", l, 10)
}
// PaymentConstraints is a set of restrictions on a payment.
type PaymentConstraints struct {
// MaxCltvExpiry is the maximum expiry height for the payment.
MaxCltvExpiry uint32
// HtlcMinimumMsat is the minimum htlc size for the payment.
HtlcMinimumMsat lnwire.MilliSatoshi
}
func (p *PaymentConstraints) Record() tlv.Record {
return tlv.MakeDynamicRecord(
12, &p, func() uint64 {
// uint32 + tuint64.
return 4 + tlv.SizeTUint64(uint64(
p.HtlcMinimumMsat,
))
},
encodePaymentConstraints, decodePaymentConstraints,
)
}
func encodePaymentConstraints(w io.Writer, val interface{},
buf *[8]byte) error {
if c, ok := val.(**PaymentConstraints); ok {
constraints := *c
binary.BigEndian.PutUint32(buf[:4], constraints.MaxCltvExpiry)
if _, err := w.Write(buf[:4]); err != nil {
return err
}
// We can safely re-use buf here because we overwrite its
// contents.
htlcMsat := uint64(constraints.HtlcMinimumMsat)
return tlv.ETUint64(w, &htlcMsat, buf)
}
return tlv.NewTypeForEncodingErr(val, "**PaymentConstraints")
}
func decodePaymentConstraints(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if c, ok := val.(**PaymentConstraints); ok && l <= 12 {
scratch := make([]byte, l)
n, err := io.ReadFull(r, scratch)
if err != nil {
return err
}
// We expect at least 4 bytes for our uint32.
if n < 4 {
return tlv.NewTypeForDecodingErr(val,
"*paymentConstraints", uint64(n), 4)
}
payConstraints := *c
payConstraints.MaxCltvExpiry = binary.BigEndian.Uint32(
scratch[:4],
)
// This could be empty if our minimum is zero, that's okay.
var (
b = bytes.NewBuffer(scratch[4:])
minHtlc uint64
)
err = tlv.DTUint64(b, &minHtlc, buf, l-4)
if err != nil {
return err
}
payConstraints.HtlcMinimumMsat = lnwire.MilliSatoshi(minHtlc)
return nil
}
return tlv.NewTypeForDecodingErr(val, "**PaymentConstraints", l, l)
}
+197
View File
@@ -0,0 +1,197 @@
package record
import (
"bytes"
"encoding/hex"
"fmt"
"math"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
//nolint:lll
const pubkeyStr = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"
func pubkey(t *testing.T) *btcec.PublicKey {
t.Helper()
nodeBytes, err := hex.DecodeString(pubkeyStr)
require.NoError(t, err)
nodePk, err := btcec.ParsePubKey(nodeBytes)
require.NoError(t, err)
return nodePk
}
// TestBlindedDataEncoding tests encoding and decoding of blinded data blobs.
// These tests specifically cover cases where the variable length encoded
// integers values have different numbers of leading zeros trimmed because
// these TLVs are the first composite records with variable length tlvs
// (previously, a variable length integer would take up the whole record).
func TestBlindedDataEncoding(t *testing.T) {
t.Parallel()
tests := []struct {
name string
baseFee uint32
htlcMin lnwire.MilliSatoshi
features *lnwire.FeatureVector
constraints bool
}{
{
name: "zero variable values",
baseFee: 0,
htlcMin: 0,
},
{
name: "zeros trimmed",
baseFee: math.MaxUint32 / 2,
htlcMin: math.MaxUint64 / 2,
},
{
name: "no zeros trimmed",
baseFee: math.MaxUint32,
htlcMin: math.MaxUint64,
},
{
name: "nil feature vector",
features: nil,
},
{
name: "non-nil, but empty feature vector",
features: lnwire.EmptyFeatureVector(),
},
{
name: "populated feature vector",
features: lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(lnwire.AMPOptional),
lnwire.Features,
),
},
{
name: "no payment constraints",
constraints: true,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
// Create a standard set of blinded route data, using
// the values from our test case for the variable
// length encoded values.
channelID := lnwire.NewShortChanIDFromInt(1)
info := PaymentRelayInfo{
FeeRate: 2,
CltvExpiryDelta: 3,
BaseFee: testCase.baseFee,
}
var constraints *PaymentConstraints
if testCase.constraints {
constraints = &PaymentConstraints{
MaxCltvExpiry: 4,
HtlcMinimumMsat: testCase.htlcMin,
}
}
encodedData := NewBlindedRouteData(
channelID, pubkey(t), info, constraints,
testCase.features,
)
encoded, err := EncodeBlindedRouteData(encodedData)
require.NoError(t, err)
b := bytes.NewBuffer(encoded)
decodedData, err := DecodeBlindedRouteData(b)
require.NoError(t, err)
require.Equal(t, encodedData, decodedData)
})
}
}
// TestBlindedRouteVectors tests encoding/decoding of the test vectors for
// blinded route data provided in the specification.
//
//nolint:lll
func TestBlindingSpecTestVectors(t *testing.T) {
nextBlindingOverrideStr, err := hex.DecodeString("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")
require.NoError(t, err)
nextBlindingOverride, err := btcec.ParsePubKey(nextBlindingOverrideStr)
require.NoError(t, err)
tests := []struct {
encoded string
expectedPaymentData *BlindedRouteData
}{
{
encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456",
expectedPaymentData: NewBlindedRouteData(
lnwire.ShortChannelID{
BlockHeight: 0,
TxIndex: 0,
TxPosition: 1729,
},
nil,
PaymentRelayInfo{
CltvExpiryDelta: 36,
FeeRate: 150,
BaseFee: 10000,
},
&PaymentConstraints{
MaxCltvExpiry: 748005,
HtlcMinimumMsat: 1500,
},
lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(),
lnwire.Features,
),
),
},
{
encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00",
expectedPaymentData: NewBlindedRouteData(
lnwire.ShortChannelID{
TxPosition: 1105,
},
nextBlindingOverride,
PaymentRelayInfo{
CltvExpiryDelta: 48,
FeeRate: 100,
BaseFee: 500,
},
&PaymentConstraints{
MaxCltvExpiry: 747969,
HtlcMinimumMsat: 1500,
},
lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(),
lnwire.Features,
)),
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
route, err := hex.DecodeString(test.encoded)
require.NoError(t, err)
buff := bytes.NewBuffer(route)
decodedRoute, err := DecodeBlindedRouteData(buff)
require.NoError(t, err)
require.Equal(
t, test.expectedPaymentData, decodedRoute,
)
})
}
}
+7 -7
View File
@@ -36,13 +36,13 @@ type BlindedPayment struct {
// blinded path.
BaseFee uint32
// ProportionalFee is the aggregated proportional fee for payments
// made over the blinded path.
ProportionalFee uint32
// ProportionalFeeRate is the aggregated proportional fee rate for
// payments made over the blinded path.
ProportionalFeeRate uint32
// CltvExpiryDelta is the total expiry delta for the blinded path. Note
// this does not include the final cltv delta for the receiving node
// (which should be provided in an invoice).
// CltvExpiryDelta is the total expiry delta for the blinded path. This
// field includes the CLTV for the blinded hops *and* the final cltv
// delta for the receiver.
CltvExpiryDelta uint16
// HtlcMinimum is the highest HLTC minimum supported along the blinded
@@ -122,7 +122,7 @@ func (b *BlindedPayment) toRouteHints() RouteHints {
MaxHTLC: lnwire.MilliSatoshi(b.HtlcMaximum),
FeeBaseMSat: lnwire.MilliSatoshi(b.BaseFee),
FeeProportionalMillionths: lnwire.MilliSatoshi(
b.ProportionalFee,
b.ProportionalFeeRate,
),
ToNodePubKey: func() route.Vertex {
return route.NewVertex(
+6 -6
View File
@@ -121,12 +121,12 @@ func TestBlindedPaymentToHints(t *testing.T) {
{},
},
},
BaseFee: baseFee,
ProportionalFee: ppmFee,
CltvExpiryDelta: cltvDelta,
HtlcMinimum: htlcMin,
HtlcMaximum: htlcMax,
Features: features,
BaseFee: baseFee,
ProportionalFeeRate: ppmFee,
CltvExpiryDelta: cltvDelta,
HtlcMinimum: htlcMin,
HtlcMaximum: htlcMax,
Features: features,
}
require.Nil(t, blindedPayment.toRouteHints())