From 114022918549b3a0074fb112971b4cfd8f441882 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Sun, 11 Sep 2022 23:34:40 -0400 Subject: [PATCH 01/16] record: add route blinding TLV records to top level onion payload equip the onion payload with additional fields for route blinding, namely the recipient_encrypted_data and blinding_point fields. --- record/hop.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/record/hop.go b/record/hop.go index 0611df0437..4e37b91c72 100644 --- a/record/hop.go +++ b/record/hop.go @@ -1,6 +1,7 @@ package record import ( + "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/tlv" ) @@ -17,9 +18,34 @@ const ( // of the next hop. NextHopOnionType tlv.Type = 6 + // RouteBlindingEncryptedDataOnionType refers to an encrypted route + // blinding TLV payload which contains information needed to forward + // in the blinded portion of a route. + RouteBlindingEncryptedDataOnionType tlv.Type = 10 + + // BlindingPointOnionType is the type used in the top level onion + // payload to reference an ephemeral public key which is used to + // establish a shared secret between a processing node in the + // blinded portion of a route and the node which built the blinded + // route (usually the recipient). The shared secret can then be used + // for the purpose of decrypting the route blinding payload. + // + // NOTE: The ephemeral blinding point is delivered in the onion payload + // for the first hop ("introduction node") of a blinded route ONLY! + // Other processing nodes in a blinded route receive their blinding + // point via TLV extension in the UpdateAddHTLC message. + BlindingPointOnionType tlv.Type = 12 + // MetadataOnionType is the type used in the onion for the payment // metadata. MetadataOnionType tlv.Type = 16 + + // TotalAmountMsatOnionType is the type used in the onion for + // the total value of the payment and is intended for use with + // the final hop in a route only. + // TODO(9/11/22): Determine if this is needed or if we can use the + // similar sounding field available on MPP. + TotalAmountMsatOnionType tlv.Type = 18 ) // NewAmtToFwdRecord creates a tlv.Record that encodes the amount_to_forward @@ -50,7 +76,19 @@ func NewNextHopIDRecord(cid *uint64) tlv.Record { return tlv.MakePrimitiveRecord(NextHopOnionType, cid) } -// NewMetadataRecord creates a tlv.Record that encodes the metadata (type 10) +// NewRouteBlindingEncryptedDataRecord creates a tlv.Record that encodes the +// encrypted_recipient_data (type 10) for an onion payload. +func NewRouteBlindingEncryptedDataRecord(data *[]byte) tlv.Record { + return tlv.MakePrimitiveRecord(RouteBlindingEncryptedDataOnionType, data) +} + +// NewBlindingPointRecord creates a tlv.Record that encodes the blinding_point +// (type 12) for an onion payload. +func NewBlindingPointRecord(key **btcec.PublicKey) tlv.Record { + return tlv.MakePrimitiveRecord(BlindingPointOnionType, key) +} + +// NewMetadataRecord creates a tlv.Record that encodes the metadata (type 16) // for an onion payload. func NewMetadataRecord(metadata *[]byte) tlv.Record { return tlv.MakeDynamicRecord( @@ -61,3 +99,15 @@ func NewMetadataRecord(metadata *[]byte) tlv.Record { tlv.EVarBytes, tlv.DVarBytes, ) } + +// NewTotalAmountMsatRecord creates a tlv.Record that encodes the +// total payment amount in millisatoshis, total_amount_msat, +// (type 18) for an onion payload. +func NewTotalAmountMsatRecord(amt *uint64) tlv.Record { + return tlv.MakeDynamicRecord( + TotalAmountMsatOnionType, amt, func() uint64 { + return tlv.SizeTUint64(*amt) + }, + tlv.ETUint64, tlv.DTUint64, + ) +} From 46751a1cb6fcd23a7c07f5c85a3823daa7a41951 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Mon, 12 Sep 2022 00:05:05 -0400 Subject: [PATCH 02/16] record: add route blinding payload TLV records These records comprise the route blinding payload which processing nodes will need in order to forward payments in the blinded portion of a route. --- record/blind_hop.go | 202 ++++++++++++++++++++++++++++++++++++++++++ record/record_test.go | 57 ++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 record/blind_hop.go diff --git a/record/blind_hop.go b/record/blind_hop.go new file mode 100644 index 0000000000..31e2a1e0d1 --- /dev/null +++ b/record/blind_hop.go @@ -0,0 +1,202 @@ +package record + +import ( + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + + // PaddingOnionType is used in a route blinding TLV payload + // to ensure that payloads are the same length across all + // hops in a blinded route. + PaddingOnionType tlv.Type = 1 + + // NextHopOnionType is used in a route blinding TLV payload + // to provide the short channel ID of the next hop. + BlindedNextHopOnionType tlv.Type = 2 + + // NextNodeIDOnionType is used in a route blinding TLV payload + // to provide the persistent node ID of the next hop. + NextNodeIDOnionType tlv.Type = 4 + + // PathIDOnionType is an optional field recipients can use + // to verify that a blinded route was used in the proper context. + PathIDOnionType tlv.Type = 6 + + // BlindingOverrideOnionType is field which can be used to + // concatenate several distinct blinded routes. + BlindingOverrideOnionType tlv.Type = 8 + + // PaymentRelayOnionType specifies the information + // necessary to compute the amount and timelock + // which a blinded hop must use to forward the onion. + PaymentRelayOnionType tlv.Type = 10 + + // PaymentContraintsOnionType specifies a set of constraints + // we are asked to honor during forwarding in the blinded + // portion of a route. The constraints are chosen by the + // route blinder in order to limit an adversary's ability to + // unblind nodes within the route. + // + // NOTE: These are additional constraints on forwarded payments + // above and beyond our usual forwarding policy. + PaymentConstraintsOnionType tlv.Type = 12 + + // AllowedFeaturesOnionType specifies the features a sender + // is permitted to use when paying to a blinded route. + AllowedFeaturesOnionType tlv.Type = 14 +) + +// NewPaddingRecord creates a tlv.Record that encodes the padding +// (type 1) for a route blinding payload. +func NewPaddingRecord(padding *[]byte) tlv.Record { + return tlv.MakePrimitiveRecord(PaddingOnionType, padding) +} + +// NewUnblindedNextHopRecord creates a tlv.Record that encodes the short_channel_id +// (type 2) for a route blinding payload. +func NewBlindedNextHopRecord(cid *uint64) tlv.Record { + return tlv.MakePrimitiveRecord(BlindedNextHopOnionType, cid) +} + +// NewNextNodeIDRecord creates a tlv.Record that encodes the next_node_id +// (type 4) for a route blinding payload. +func NewNextNodeIDRecord(nodeID **btcec.PublicKey) tlv.Record { + return tlv.MakePrimitiveRecord(NextNodeIDOnionType, nodeID) +} + +// NewPathIDRecord creates a tlv.Record that encodes the path_id +// (type 6) for a route blinding payload. +func NewPathIDRecord(pathID *[]byte) tlv.Record { + return tlv.MakePrimitiveRecord(PathIDOnionType, pathID) +} + +// NewBlindingOverrideRecord creates a tlv.Record that encodes the next_blinding_override +// (type 8) for a route blinding payload. +func NewBlindingOverrideRecord(point **btcec.PublicKey) tlv.Record { + return tlv.MakePrimitiveRecord(BlindingOverrideOnionType, point) +} + +// PaymentRelay is a tlv.Record which defines the payment_relay +// (type 10) for a route blinding payload. +type PaymentRelay struct { + CltvExpiryDelta uint16 + FeeRate uint32 + // NOTE(8/6/22): Is this in satoshis or msat? + // This has implications for fee computation. + BaseFee uint32 +} + +// satisfies the tlv.RecordProducer interface +// TODO(9/7/22): check length functions. +func (p *PaymentRelay) Record() tlv.Record { + return tlv.MakeDynamicRecord(PaymentRelayOnionType, + p, func() uint64 { return uint64(2 + 4 + 4) }, + paymentRelayEncoder, paymentRelayDecoder, + ) +} + +func paymentRelayEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*PaymentRelay); ok { + if err := tlv.EUint32(w, &v.BaseFee, buf); err != nil { + return err + } + + if err := tlv.EUint32(w, &v.FeeRate, buf); err != nil { + return err + } + + if err := tlv.EUint16(w, &v.CltvExpiryDelta, buf); err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "routeblinding.PaymentRelay") +} + +func paymentRelayDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*PaymentRelay); ok { + if err := tlv.DUint32(r, &v.BaseFee, buf, 4); err != nil { + return err + } + + if err := tlv.DUint32(r, &v.FeeRate, buf, 4); err != nil { + return err + } + + if err := tlv.DUint16(r, &v.CltvExpiryDelta, buf, 2); err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForDecodingErr(val, "routeblinding.PaymentRelay", l, 10) +} + +// PaymentConstraints is a tlv.Record which defines the payment_constraints +// (type 12) for a route blinding payload. +type PaymentConstraints struct { + MaxCltvExpiryDelta uint32 + HtlcMinimumMsat uint64 + AllowedFeatures []byte +} + +func (p *PaymentConstraints) Record() tlv.Record { + return tlv.MakeDynamicRecord(PaymentConstraintsOnionType, + p, func() uint64 { return uint64(4 + 8 + len(p.AllowedFeatures)) }, + paymentConstraintsEncoder, paymentConstraintsDecoder, + ) +} + +func paymentConstraintsEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*PaymentConstraints); ok { + if err := tlv.EUint32(w, &v.MaxCltvExpiryDelta, buf); err != nil { + return err + } + + if err := tlv.EUint64(w, &v.HtlcMinimumMsat, buf); err != nil { + return err + } + + if err := tlv.EVarBytes(w, &v.AllowedFeatures, buf); err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "routeblinding.PaymentConstraints") +} + +func paymentConstraintsDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*PaymentConstraints); ok { + if err := tlv.DUint32(r, &v.MaxCltvExpiryDelta, buf, 4); err != nil { + return err + } + + if err := tlv.DUint64(r, &v.HtlcMinimumMsat, buf, 8); err != nil { + return err + } + + if err := tlv.DVarBytes(r, &v.AllowedFeatures, buf, 0); err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForDecodingErr(val, "routeblinding.PaymentRelay", l, 10) +} + +// NewAllowedFeaturesRecord creates a tlv.Record that encodes the +// features permitted for use during forwarding inside a blinded route +// allowed features (type 14) for route blinding payload. +func NewAllowedFeaturesRecord(features *[]byte) tlv.Record { + return tlv.MakePrimitiveRecord(AllowedFeaturesOnionType, features) +} diff --git a/record/record_test.go b/record/record_test.go index 45faa9f73d..67c2f83e42 100644 --- a/record/record_test.go +++ b/record/record_test.go @@ -22,6 +22,13 @@ var ( testShare = [32]byte{0x03, 0x04} testSetID = [32]byte{0x05, 0x06} testChildIndex = uint32(17) + + testBaseFee uint32 = 100 + testFeeRate uint32 = 100 + testCltvExpiry uint16 = 100 + + testMinimumHtlc uint64 = 1 + testMaxCltvExpiry uint32 = uint32(testCltvExpiry) ) var recordEncDecTests = []recordEncDecTest{ @@ -66,6 +73,56 @@ var recordEncDecTests = []recordEncDecTest{ } }, }, + { + name: "route blinding payment relay", + encRecord: func() tlv.RecordProducer { + return &record.PaymentRelay{ + BaseFee: testBaseFee, + FeeRate: testFeeRate, + CltvExpiryDelta: testCltvExpiry, + } + }, + decRecord: func() tlv.RecordProducer { + return new(record.PaymentRelay) + }, + assert: func(t *testing.T, r interface{}) { + paymentRelay := r.(*record.PaymentRelay) + if paymentRelay.BaseFee != testBaseFee { + t.Fatal("incorrect base fee") + } + if paymentRelay.FeeRate != testFeeRate { + t.Fatal("incorrect fee rate") + } + if paymentRelay.CltvExpiryDelta != testCltvExpiry { + t.Fatal("incorrect cltv expiry") + } + }, + }, + { + name: "route blinding payment constraints", + encRecord: func() tlv.RecordProducer { + return &record.PaymentConstraints{ + MaxCltvExpiryDelta: testMaxCltvExpiry, + HtlcMinimumMsat: testMinimumHtlc, + // AllowedFeatures: []byte{}, + } + }, + decRecord: func() tlv.RecordProducer { + return new(record.PaymentConstraints) + }, + assert: func(t *testing.T, r interface{}) { + payConstraints := r.(*record.PaymentConstraints) + if payConstraints.MaxCltvExpiryDelta != testMaxCltvExpiry { + t.Fatal("incorrect blinded route expiry") + } + if payConstraints.HtlcMinimumMsat != testMinimumHtlc { + t.Fatal("incorrect minimum htlc") + } + // if bytes.Equal(payConstraints.AllowedFeatures, []byte{}) { + // t.Fatal("incorrect allowed features: ", payConstraints.AllowedFeatures) + // } + }, + }, } // TestRecordEncodeDecode is a generic test framework for custom TLV records. It From 44e034192357d6d0743260fe36aef5a4e2e74ab4 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Wed, 21 Sep 2022 23:08:58 -0400 Subject: [PATCH 03/16] multi: update to lightning-onion with route blinding functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See ‘Requiring external module code from your own repository fork’ from https://go.dev/doc/modules/managing-dependencies NOTE: this commmit can be removed once the 'lightning-onion' dependency is merged and in use by lnd. --- go.mod | 4 ++++ go.sum | 5 +++-- htlcswitch/hop/iterator.go | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 037aedb52f..ba7c2377fd 100644 --- a/go.mod +++ b/go.mod @@ -166,6 +166,10 @@ replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.8 // https://deps.dev/advisory/OSV/GO-2021-0053?from=%2Fgo%2Fgithub.com%252Fgogo%252Fprotobuf%2Fv1.3.1 replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 +// Swap to lightning-onion dependency which supports route blinding +// replace github.com/lightningnetwork/lightning-onion => /Users/calvinzachman/Documents/Workspace/src/github.com/lightning-onion //../lightning-onion +replace github.com/lightningnetwork/lightning-onion => github.com/calvinrzachman/lightning-onion v1.2.1-0.20220705121410-91a33c83e24d + // If you change this please also update .github/pull_request_template.md and // docs/INSTALL.md. go 1.18 diff --git a/go.sum b/go.sum index e7941778e5..185e070c7b 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/calvinrzachman/lightning-onion v1.2.1-0.20220705121410-91a33c83e24d h1:N2DvOgIQLyB/rFkpQUPlA+RhTc0JZJhBFM7XaqyInG0= +github.com/calvinrzachman/lightning-onion v1.2.1-0.20220705121410-91a33c83e24d/go.mod h1:IpXmxKG482HAQC/aCn0Sd+DYmKlkfaNF/iUz298HhC0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 h1:uH66TXeswKn5PW5zdZ39xEwfS9an067BirqA+P4QaLI= @@ -440,8 +442,6 @@ github.com/lightninglabs/neutrino v0.14.2 h1:yrnZUCYMZ5ECtXhgDrzqPq2oX8awoAN2D/c github.com/lightninglabs/neutrino v0.14.2/go.mod h1:OICUeTCn+4Tu27YRJIpWvvqySxx4oH4vgdP33Sw9RDc= github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display h1:RZJ8H4ueU/aQ9pFtx5wqsuD3B/DezrewJeVwDKKYY8E= github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display/go.mod h1:2oKOBU042GKFHrdbgGiKax4xVrFiZu51lhacUZQ9MnE= -github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5 h1:TkKwqFcQTGYoI+VEqyxA8rxpCin8qDaYX0AfVRinT3k= -github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5/go.mod h1:7dDx73ApjEZA0kcknI799m2O5kkpfg4/gr7N092ojNo= github.com/lightningnetwork/lnd/cert v1.1.1 h1:Nsav0RlIDRbOnzz2Yu69SQlK939IKya3Q2S0mDviIN8= github.com/lightningnetwork/lnd/cert v1.1.1/go.mod h1:1P46svkkd73oSoeI4zjkVKgZNwGq8bkGuPR8z+5vQUs= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= @@ -620,6 +620,7 @@ github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAV github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw= github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index c1073b1da9..8547e00926 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -170,7 +170,7 @@ func (p *OnionProcessor) DecodeHopIterator(r io.Reader, rHash []byte, // 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, + onionPkt, rHash, incomingCltv, nil, ) if err != nil { switch err { @@ -205,7 +205,7 @@ func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) ( // 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, nil) if err != nil { return nil, err } @@ -276,7 +276,7 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte, } err = tx.ProcessOnionPacket( - seqNum, onionPkt, req.RHash, req.IncomingCltv, + seqNum, onionPkt, req.RHash, req.IncomingCltv, nil, ) switch err { case nil: @@ -383,6 +383,8 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte, func (p *OnionProcessor) ExtractErrorEncrypter(ephemeralKey *btcec.PublicKey) ( ErrorEncrypter, lnwire.FailCode) { + // TODO(9/21/22): Figure how this changes when handling + // errors for a blinded hop. onionObfuscator, err := sphinx.NewOnionErrorEncrypter( p.router, ephemeralKey, ) From 7c8e24c37f0e8c0ef0dad775a946d01b34e213f5 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Sun, 6 Nov 2022 11:40:27 -0500 Subject: [PATCH 04/16] htlcswitch/hop: (en/de)code route blinding TLV payload Add ability to parse route blinding payload after it is decrypted, and validate presence and omission of TLV payloads according to BOLT-04. This does not perform all BOLT-04 TLV payload validation. We now have validation to do across two levels of TLV payloads (top level onion TLV payload & route blinding TLV payload) and the contents of the UpdateAddHTLC message. What is present in one payload effects our expectation of what is present in the other payload. As a result, we will need to validate the payloads together, which means waiting til after the route blinding payload is decrypted. We preserve the normal validation in the case we are forwarding for a normal (not blinded) route. When an onion contains an encrypted route blinding payload, we validate what we are able to as soon as the route blinding payload is parsed. Although enforcing different portions of the onion and route blinding payload validation in different stages of processing for a blind hop is a bit more complex, it provides a benefit over deferring validation in that we are able to fail early on in the processing and avoid unecessary computation only to fail later. --- htlcswitch/hop/payload.go | 284 ++++++++++++++++++++++++++++++--- htlcswitch/hop/payload_test.go | 263 +++++++++++++++++++++++++++++- 2 files changed, 515 insertions(+), 32 deletions(-) diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index be7be5eebb..1a67d93537 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -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" @@ -90,6 +91,14 @@ type Payload struct { // a TLV onion payload. AMP *record.AMP + // RouteBlindingEncryptedData contains information needed to forward in + // the blinded portion of a route. + RouteBlindingEncryptedData []byte + + // BlindingPoint delivered to the introductory node in the blinded route. + // NOTE: Could use [33]byte for compressed pubkey and remove btcec dependency. + BlindingPoint *btcec.PublicKey + // customRecords are user-defined records in the custom type range that // were included in the payload. customRecords record.CustomSet @@ -97,6 +106,9 @@ type Payload struct { // metadata is additional data that is sent along with the payment to // the payee. metadata []byte + + // TotalAmountMsat is the total payment amount. + TotalAmountMsat lnwire.MilliSatoshi } // NewLegacyPayload builds a Payload from the amount, cltv, and next hop @@ -115,16 +127,127 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload { } } +// BlindHopPayload encapsulates all the route blinding information +// which will be parsed by nodes in the blinded portion of a route. +type BlindHopPayload struct { + // probably don't need to save this as it's only used to ensure + // all payloads on a blinded route are the same length. + Padding []byte + NextHop lnwire.ShortChannelID + NextNodeID *btcec.PublicKey + PathID []byte + BlindingPointOverride *btcec.PublicKey + PaymentRelay *record.PaymentRelay + PaymentConstraints *record.PaymentConstraints + // AllowedFeatures *record.AllowedFeatures +} + +// NewBlindHopPayloadFromReader parses a route blinding payload from +// the passed io.Reader. The reader should correspond to the bytes +// encapsulated in the encrypted route blinding payload after they +// have been decrypted. +func NewBlindHopPayloadFromReader(r io.Reader, + isFinalHop bool) (*BlindHopPayload, error) { + + var ( + padding []byte + nextHop uint64 + nextNodeID *btcec.PublicKey + pathID []byte + blindingOverride *btcec.PublicKey + paymentRelay = &record.PaymentRelay{} + paymentConstraints = &record.PaymentConstraints{} + ) + + tlvStream, err := tlv.NewStream( + record.NewPaddingRecord(&padding), + record.NewBlindedNextHopRecord(&nextHop), + record.NewNextNodeIDRecord(&nextNodeID), + record.NewPathIDRecord(&pathID), + record.NewBlindingOverrideRecord(&blindingOverride), + paymentRelay.Record(), + paymentConstraints.Record(), + ) + if err != nil { + return nil, err + } + + parsedTypes, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return nil, err + } + + // Validate whether the sender properly included or omitted + // route blinding tlv records in accordance with BOLT 04 as + // early as possible. Additional validation will be performed later. + err = ValidateRouteBlindingPayloadTypes(parsedTypes, isFinalHop) + if err != nil { + return nil, err + } + + violatingType := getMinRequiredViolation(parsedTypes) + if violatingType != nil { + return nil, ErrInvalidPayload{ + Type: *violatingType, + Violation: RequiredViolation, + FinalHop: isFinalHop, + } + } + + // NOTE(8/13/22): We set the fields on our struct representing the + // route blinding payload to nil so later we can properly validate. + // Reconcile this with above. + // + // If no padding field was parsed, set the padding field + // on the resulting payload to nil. + if _, ok := parsedTypes[record.PaddingOnionType]; !ok { + padding = nil + } + + // If no path ID field was parsed, set the path ID field + // on the resulting payload to nil. + if _, ok := parsedTypes[record.PathIDOnionType]; !ok { + pathID = nil + } + + // If no payment relay field was parsed, set the payment relay field + // on the resulting payload to nil. + if _, ok := parsedTypes[record.PaymentRelayOnionType]; !ok { + paymentRelay = nil + } + + // If no payment constraints field was parsed, set the payment + // constraints field on the resulting payload to nil. + if _, ok := parsedTypes[record.PaymentConstraintsOnionType]; !ok { + paymentConstraints = nil + } + + return &BlindHopPayload{ + // NOTE: Likely do not need to expose padding to callers. + Padding: padding, + NextHop: lnwire.NewShortChanIDFromInt(nextHop), + NextNodeID: nextNodeID, + PathID: pathID, + BlindingPointOverride: blindingOverride, + PaymentRelay: paymentRelay, + PaymentConstraints: paymentConstraints, + }, nil +} + // NewPayloadFromReader builds a new Hop from the passed io.Reader. The reader // 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 + cltv uint32 + mpp = &record.MPP{} + amp = &record.AMP{} + metadata []byte + totalAmount uint64 + + blindedData []byte + blindingPoint *btcec.PublicKey ) tlvStream, err := tlv.NewStream( @@ -132,8 +255,11 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { record.NewLockTimeRecord(&cltv), record.NewNextHopIDRecord(&cid), mpp.Record(), + record.NewRouteBlindingEncryptedDataRecord(&blindedData), + record.NewBlindingPointRecord(&blindingPoint), amp.Record(), record.NewMetadataRecord(&metadata), + record.NewTotalAmountMsatRecord(&totalAmount), ) if err != nil { return nil, err @@ -146,6 +272,9 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { // Validate whether the sender properly included or omitted tlv records // in accordance with BOLT 04. + // NOTE(9/21/22): The 'nextHop' is passed so that our validation + // function can make the determination as to whether we are the + // final hop. We will replace this with all-zero onion HMAC. nextHop := lnwire.NewShortChanIDFromInt(cid) err = ValidateParsedPayloadTypes(parsedTypes, nextHop) if err != nil { @@ -168,7 +297,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { mpp = nil } - // If no AMP field was parsed, set the MPP field on the resulting + // If no AMP field was parsed, set the AMP field on the resulting // payload to nil. if _, ok := parsedTypes[record.AMPOnionType]; !ok { amp = nil @@ -190,10 +319,15 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { AmountToForward: lnwire.MilliSatoshi(amt), OutgoingCTLV: cltv, }, - MPP: mpp, - AMP: amp, - metadata: metadata, - customRecords: customRecords, + MPP: mpp, + AMP: amp, + // NOTE(9/2/22): This remains encrypted for now. + // We will parse (and validate) again as TLV stream after decryption. + RouteBlindingEncryptedData: blindedData, + BlindingPoint: blindingPoint, + metadata: metadata, + TotalAmountMsat: lnwire.MilliSatoshi(totalAmount), + customRecords: customRecords, }, nil } @@ -216,13 +350,76 @@ func NewCustomRecords(parsedTypes tlv.TypeMap) record.CustomSet { return customRecords } +// ValidateRouteBlindingPayloadTypes checks the types parsed from a route +// blinding payload to ensure that the proper fields are either included +// or omitted. The requirements for this method are described in BOLT 04. +func ValidateRouteBlindingPayloadTypes(parsedTypes tlv.TypeMap, + isFinalHop bool) error { + + _, hasNextHop := parsedTypes[record.BlindedNextHopOnionType] + _, hasNextNode := parsedTypes[record.NextNodeIDOnionType] + _, hasPathID := parsedTypes[record.PathIDOnionType] + _, hasForwardingParams := parsedTypes[record.PaymentRelayOnionType] + + // TODO(9/10/22): Figure out how to actually distinguish the final + // hop in a blinded route as TLV payload reader. + // UPDATE(9/15/22): Apparently this is supposed to be indicated by the + // sphinx implementation. + if !isFinalHop { + // An intermediate hop MUST specify how the payment is to be forwarded. + if !hasForwardingParams { + return ErrInvalidPayload{ + Type: record.PaymentRelayOnionType, + Violation: OmittedViolation, + FinalHop: false, + } + } + + // An intermedate hop MUST specify the node to which we should forward. + if !hasNextHop && !hasNextNode { + return ErrInvalidPayload{ + Type: record.BlindedNextHopOnionType, + Violation: OmittedViolation, + FinalHop: false, + } + } + } else { + // The final hop MUST have a path_id with which we can validate + // this payment is for a blind route we created. + if !hasPathID { + return ErrInvalidPayload{ + Type: record.PathIDOnionType, + Violation: OmittedViolation, + FinalHop: true, + } + } + } + + return nil +} + // 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. +// +// NOTE(9/2/22): We now have validation to do across two levels of TLV +// payloads. What is present in one payload effects our expectation of what +// is present in the other payload. As a result, we will likely need to validate +// the payloads together, which means waiting until after the route blinding +// payload is decrypted. We would like to preserve the normal validation +// in the case we are forwarding for a normal (not blinded) route. func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap, nextHop lnwire.ShortChannelID) error { + // NOTE(9/15/22): This may not serve as a proper determination of + // whether this is the final hop. Processing nodes in a blinded + // route are permitted to have an empty next hop in the top level TLV + // onion payload. They MUST have a next hop in the recipient encrypted + // data payload however. + // UPDATE(9/15/22): According to BOLT-04 this is supposed to be + // indicated by the sphinx implementation when it encounters + // an all-zero onion HMAC. isFinalHop := nextHop == Exit _, hasAmt := parsedTypes[record.AmtOnionType] @@ -230,25 +427,62 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap, _, hasNextHop := parsedTypes[record.NextHopOnionType] _, hasMPP := parsedTypes[record.MPPOnionType] _, hasAMP := parsedTypes[record.AMPOnionType] + _, isBlindHop := parsedTypes[record.RouteBlindingEncryptedDataOnionType] + + isNormalHop := !isBlindHop + + // If this is a normal hop, fall back to our usual validation. + if isNormalHop { + switch { + + // All hops must include an amount to forward, + // except those on blinded routes. + case !hasAmt: + return ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: OmittedViolation, + FinalHop: isFinalHop, + } + + // All normal hops must include a cltv expiry. + case !hasLockTime: + return ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: OmittedViolation, + FinalHop: isFinalHop, + } - switch { - - // All hops must include an amount to forward. - case !hasAmt: - return ErrInvalidPayload{ - Type: record.AmtOnionType, - Violation: OmittedViolation, - FinalHop: isFinalHop, } - // All hops must include a cltv expiry. - case !hasLockTime: - return ErrInvalidPayload{ - Type: record.LockTimeOnionType, - Violation: OmittedViolation, - FinalHop: isFinalHop, + } else { + // This is a blind hop so we'll apply additional validation + // as per BOLT-04. + switch { + + // Intermediate nodes in a blinded route should + // not contain an amount to forward. + case !isFinalHop && hasAmt: + return ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: IncludedViolation, + FinalHop: false, + } + + // Intermediate nodes in a blinded route should + // not contain an outgoing timelock. + case !isFinalHop && hasLockTime: + return ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: IncludedViolation, + FinalHop: false, + } + } + } + // NOTE(11/15/22): The following 3 checks are common for both + // normal and blind hops. + switch { // The exit hop should omit the next hop id. If nextHop != Exit, the // sender must have included a record, so we don't need to test for its // inclusion at intermediate hops directly. diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go index 130b363dd6..065e446a8e 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -14,16 +14,29 @@ import ( const testUnknownRequiredType = 0x80 type decodePayloadTest struct { - name string - payload []byte - expErr error - expCustomRecords map[uint64][]byte - shouldHaveMPP bool - shouldHaveAMP bool - shouldHaveMetadata bool + name string + payload []byte + expErr error + expCustomRecords map[uint64][]byte + shouldHaveMPP bool + shouldHaveAMP bool + shouldHaveMetadata bool + shouldHaveRouteBlindingInfo bool + expRouteBlindingErr error + isFinalHop bool } var decodePayloadTests = []decodePayloadTest{ + { + name: "empty hop TLV payload", + payload: []byte{}, + // All normal hops (ie: not blind) are expected to have an amount. + expErr: hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + }, + }, { name: "final hop valid", payload: []byte{0x02, 0x00, 0x04, 0x00}, @@ -271,6 +284,211 @@ var decodePayloadTests = []decodePayloadTest{ }, shouldHaveMetadata: true, }, + { + name: "introduction node blinded route", + payload: []byte{ + // - no amount + // - no timelock + // NOTE(9/21/22): the introduction node will receive an + // amount and timelock on the inbound HTLC but its top + // level onion TLV payload should not contain an amount + // to be forwarded or an outgoing timelock. + // no unblinded next hop id in blinded portion of route + // (route blinding - top level TLV payload) recipient encrypted data + // START: route blinding TLV data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x70, + // byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x56, + // (route blinding) padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // (route blinding) next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // (route blinding) next node ID + byte(int(record.NextNodeIDOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // (route blinding) path ID (ONLY set for final node) + // 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // (route blinding) blinding point override + byte(int(record.BlindingOverrideOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // (route blinding) payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // (route blinding) payment constraints + byte(int(record.PaymentConstraintsOnionType)), 0x0c, + 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // END: route blinding TLV data + // (route blinding - top level TLV payload) blinding point + byte(int(record.BlindingPointOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + }, + shouldHaveRouteBlindingInfo: true, + }, + { + name: "intermediate hop blinded route w/ next hop", + payload: []byte{ + // - no amount + // - no timelock + // START: route blinding TLV data + // recipient encrypted data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x1c, + // byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x10, // test no payment relay + // (route blinding) padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // (route blinding) next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // (route blinding) payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // END: route blinding TLV data + }, + shouldHaveRouteBlindingInfo: true, + }, + { + name: "intermediate hop blinded route w/ next node ID", + payload: []byte{ + // - no amount + // - no timelock + // START: route blinding TLV data + // recipient encrypted data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x35, + // byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x29, // test no payment relay + // (route blinding) padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // (route blinding) next node ID + byte(int(record.NextNodeIDOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // (route blinding) payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // END: route blinding TLV data + }, + shouldHaveRouteBlindingInfo: true, + }, + { + name: "intermediate hop blinded route missing payment relay", // error case + payload: []byte{ + // - no amount + // - no timelock + // START: route blinding TLV data + // recipient encrypted data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x10, + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // - no payment relay (MUST be set for blind hops) + // END: route blinding TLV data + }, + shouldHaveRouteBlindingInfo: true, + expRouteBlindingErr: hop.ErrInvalidPayload{ + Type: record.PaymentRelayOnionType, + Violation: hop.OmittedViolation, + FinalHop: false, + }, + }, + { + name: "final hop blinded route", + payload: []byte{ + // amount + 0x02, 0x01, 0x0b, + // cltv + 0x04, 0x01, 0x11, + // (route blinding - top level TLV payload) recipient encrypted data + // START: route blinding TLV data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x0c, + // (route blinding) padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // (route blinding) path ID (ONLY set for final node) + byte(int(record.PathIDOnionType)), 0x04, 0xff, 0x00, 0xff, 0x00, + // END: route blinding TLV data + }, + shouldHaveRouteBlindingInfo: true, + isFinalHop: true, + }, + { + name: "final hop blinded route missing path ID", // error case + payload: []byte{ + // amount + 0x02, 0x01, 0x0b, + // cltv + 0x04, 0x01, 0x11, + // (route blinding - top level TLV payload) recipient encrypted data + // START: route blinding TLV data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x06, + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // - no path ID (MUST be set for final node) + // END: route blinding TLV data + }, + shouldHaveRouteBlindingInfo: true, + expRouteBlindingErr: hop.ErrInvalidPayload{ + Type: record.PathIDOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + }, + isFinalHop: true, + }, + { + name: "christmas tree TLV payload (all lights on)", + payload: []byte{ + // amount + 0x02, 0x01, 0x0b, + // cltv + 0x04, 0x01, 0x11, + // (route blinding - top level TLV payload) recipient encrypted data + // START: route blinding TLV data + byte(int(record.RouteBlindingEncryptedDataOnionType)), 0x76, + // (route blinding) padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // (route blinding) next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // (route blinding) next node ID + byte(int(record.NextNodeIDOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // (route blinding) path ID (ONLY set for final node) + byte(int(record.PathIDOnionType)), 0x04, 0xff, 0x00, 0xff, 0x00, + // (route blinding) blinding point override + byte(int(record.BlindingOverrideOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // (route blinding) payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // (route blinding) payment constraints + byte(int(record.PaymentConstraintsOnionType)), 0x0c, + 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // END: route blinding TLV data + // (route blinding - top level TLV payload) blinding point + byte(int(record.BlindingPointOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + }, + shouldHaveRouteBlindingInfo: true, + }, } // TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the @@ -354,6 +572,37 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) { t.Fatalf("unexpected metadata") } + // If the top level onion TLV payload contains a route blinding + // TLV payload, then we'll parse that as well. + // NOTE: We assume the route blinding payload has already been decrypted. + var blindHopPayload *hop.BlindHopPayload + if p.RouteBlindingEncryptedData != nil { + blindHopPayload, err = hop.NewBlindHopPayloadFromReader( + bytes.NewReader(p.RouteBlindingEncryptedData), test.isFinalHop, + ) + + if !reflect.DeepEqual(test.expRouteBlindingErr, err) { + t.Fatalf("expected error mismatch, want: %v, got: %v", + test.expRouteBlindingErr, err) + } + if err != nil { + return + } + } + + if test.shouldHaveRouteBlindingInfo { + if p.RouteBlindingEncryptedData == nil { + t.Fatalf("payload should have encrypted data from route blinder") + } + + if blindHopPayload == nil { + t.Fatalf("should have parsed route blinding payload") + } + + } else if p.RouteBlindingEncryptedData != nil || p.BlindingPoint != nil { + t.Fatalf("unexpected route blinding info included in payload") + } + // Convert expected nil map to empty map, because we always expect an // initiated map from the payload. expCustomRecords := make(record.CustomSet) From 45ac059f7ffdf37dc132a461d6d085d313aee192 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Sun, 6 Nov 2022 11:49:10 -0500 Subject: [PATCH 05/16] hop/iterator: use all-zero 32 byte onion HMAC as indicator of final hop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ChannelLink’s decision about whether it is processing a final/exit hop used to be plugged (almost) directly into the sphinx package’s observation of 32 all 0x00-bytes for the onion HMAC. Somewhere along the line the condition which defines whether a node is the final/exit hop appears to have changed from 32 0x00-bytes for the onion HMAC to an empty short_channel_id in the top level onion TLV payload. It looks like things may have changed w.r.t determining the final/exit hop when moving TLV onion payloads? - https://github.com/lightningnetwork/lnd/blob/23cc8389f2e7db968e859f2ee2426b0906c2dd5d/htlcswitch/iterator_test.go#L78-L79 We will return to the old days where the sphinx implementation encountering an all-zero 32 byte onion HMAC signals we are the final hop. We can no longer rely on the presence of a next_hop in the top level onion TLV payload as it will not be there for blinded hops. --- htlcswitch/hop/iterator.go | 29 +++- htlcswitch/hop/iterator_test.go | 61 ++++++++ htlcswitch/hop/payload.go | 10 +- htlcswitch/hop/payload_test.go | 259 +++++++++++++++++++++++++++++++- htlcswitch/mock.go | 5 + 5 files changed, 352 insertions(+), 12 deletions(-) diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index 8547e00926..750676b1a2 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -33,6 +33,12 @@ type Iterator interface { // along with a failure code to signal if the decoding was successful. ExtractErrorEncrypter(ErrorEncrypterExtracter) (ErrorEncrypter, lnwire.FailCode) + + // IsFinalHop returns a boolean indicating whether we are the final hop. + IsFinalHop() bool + + // NOTE(9/23/22): Could also use a NextHop() and keep check for hop.Exit? + // NextHop() lnwire.ShortChannelID } // sphinxHopIterator is the Sphinx implementation of hop iterator which uses @@ -90,9 +96,10 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) { // Otherwise, if this is the TLV payload, then we'll make a new stream // to decode only what we need to make routing decisions. case sphinx.PayloadTLV: - return NewPayloadFromReader(bytes.NewReader( - r.processedPacket.Payload.Payload, - )) + return NewPayloadFromReader( + bytes.NewReader(r.processedPacket.Payload.Payload), + r.IsFinalHop(), + ) default: return nil, fmt.Errorf("unknown sphinx payload type: %v", @@ -100,6 +107,22 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) { } } +// IsFinalHop leverages the processed sphinx packet's +// 'Action' to distinguish whether we are the final hop. +func (r *sphinxHopIterator) IsFinalHop() bool { + return int(r.processedPacket.Action) == 0 +} + +// func (r *sphinxHopIterator) NextHop() lnwire.ShortChannelID { +// if r.processedPacket.Action == sphinx.ExitNode { +// return Exit +// } + +// // NOTE: iterator doesn't have access to the next hop. +// // in the case we're not the exit hop +// return Exit +// } + // ExtractErrorEncrypter decodes and returns the ErrorEncrypter for this hop, // along with a failure code to signal if the decoding was successful. The // ErrorEncrypter is used to encrypt errors back to the sender in the event that diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go index 524b70df09..d4943403ea 100644 --- a/htlcswitch/hop/iterator_test.go +++ b/htlcswitch/hop/iterator_test.go @@ -62,20 +62,26 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) { }, Action: sphinx.MoreHops, ForwardingInstructions: &hopData, + // NOTE(9/15/22): This field will only be populated iff the above Action is MoreHops. }, expectedFwdInfo: expectedFwdInfo, }, // A TLV payload, we can leave off the action as we'll always // read the cid encoded. + // NOTE(9/15/22): The disconnect between sphinx and htlcswitch + // packages w.r.t determining the final/exit hop happened with + // the move to TLV payload? { sphinxPacket: &sphinx.ProcessedPacket{ Payload: sphinx.HopPayload{ Type: sphinx.PayloadTLV, Payload: b.Bytes(), }, + Action: sphinx.MoreHops, }, expectedFwdInfo: expectedFwdInfo, }, + // TODO(11/16/22): Add test with TLV payload and Action: sphinx.ExitNode? } // Finally, we'll test that we get the same set of @@ -98,3 +104,58 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) { } } } + +// TestSphinxHopIteratorFinalHop confirms that the iterator +// correctly passed through the signal from the underlying +// sphinx implementation as to whether a hop is the last hop +// in the route. +func TestSphinxHopIteratorFinalHop(t *testing.T) { + t.Parallel() + + var testCases = []struct { + sphinxPacket *sphinx.ProcessedPacket + expectedFinalHop bool + }{ + // A regular legacy payload that signals more hops. + { + sphinxPacket: &sphinx.ProcessedPacket{ + Payload: sphinx.HopPayload{ + Type: sphinx.PayloadLegacy, + }, + Action: sphinx.MoreHops, + }, + expectedFinalHop: false, + }, + // A regular legacy payload that signals final hop. + { + sphinxPacket: &sphinx.ProcessedPacket{ + Payload: sphinx.HopPayload{ + Type: sphinx.PayloadLegacy, + }, + Action: sphinx.ExitNode, + }, + expectedFinalHop: true, + }, + // A TLV payload that signals final hop. + { + sphinxPacket: &sphinx.ProcessedPacket{ + Payload: sphinx.HopPayload{ + Type: sphinx.PayloadTLV, + }, + Action: sphinx.ExitNode, + }, + expectedFinalHop: true, + }, + } + + iterator := sphinxHopIterator{} + for _, testCase := range testCases { + iterator.processedPacket = testCase.sphinxPacket + + isFinalHop := iterator.IsFinalHop() + if isFinalHop != testCase.expectedFinalHop { + t.Fatalf("expected final hop: %t, got: %t", + testCase.expectedFinalHop, isFinalHop) + } + } +} diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 1a67d93537..5b742ee153 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -236,7 +236,7 @@ func NewBlindHopPayloadFromReader(r io.Reader, // NewPayloadFromReader builds a new Hop from the passed io.Reader. The reader // should correspond to the bytes encapsulated in a TLV onion payload. -func NewPayloadFromReader(r io.Reader) (*Payload, error) { +func NewPayloadFromReader(r io.Reader, isFinalHop bool) (*Payload, error) { var ( cid uint64 amt uint64 @@ -276,7 +276,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { // function can make the determination as to whether we are the // final hop. We will replace this with all-zero onion HMAC. nextHop := lnwire.NewShortChanIDFromInt(cid) - err = ValidateParsedPayloadTypes(parsedTypes, nextHop) + err = ValidateParsedPayloadTypes(parsedTypes, isFinalHop) if err != nil { return nil, err } @@ -287,7 +287,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { return nil, ErrInvalidPayload{ Type: *violatingType, Violation: RequiredViolation, - FinalHop: nextHop == Exit, + FinalHop: isFinalHop, } } @@ -410,7 +410,7 @@ func ValidateRouteBlindingPayloadTypes(parsedTypes tlv.TypeMap, // payload is decrypted. We would like to preserve the normal validation // in the case we are forwarding for a normal (not blinded) route. func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap, - nextHop lnwire.ShortChannelID) error { + isFinalHop bool) error { // NOTE(9/15/22): This may not serve as a proper determination of // whether this is the final hop. Processing nodes in a blinded @@ -420,7 +420,7 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap, // UPDATE(9/15/22): According to BOLT-04 this is supposed to be // indicated by the sphinx implementation when it encounters // an all-zero onion HMAC. - isFinalHop := nextHop == Exit + // isFinalHop := nextHop == Exit _, hasAmt := parsedTypes[record.AmtOnionType] _, hasLockTime := parsedTypes[record.LockTimeOnionType] diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go index 065e446a8e..d323731be9 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -36,6 +36,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.OmittedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "final hop valid", @@ -55,6 +56,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.OmittedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "intermediate hop no amount", @@ -75,6 +77,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.OmittedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "intermediate hop no expiry", @@ -97,6 +100,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.IncludedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type after omitted hop id", @@ -109,6 +113,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.RequiredViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type after included hop id", @@ -131,6 +136,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.RequiredViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type zero final hop zero sid", @@ -142,6 +148,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.IncludedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type zero intermediate hop", @@ -171,9 +178,10 @@ var decodePayloadTests = []decodePayloadTest{ expErr: nil, }, { - name: "valid final hop", - payload: []byte{0x02, 0x00, 0x04, 0x00}, - expErr: nil, + name: "valid final hop", + payload: []byte{0x02, 0x00, 0x04, 0x00}, + expErr: nil, + isFinalHop: true, }, { name: "intermediate hop with mpp", @@ -247,6 +255,7 @@ var decodePayloadTests = []decodePayloadTest{ }, expErr: nil, shouldHaveMPP: true, + isFinalHop: true, }, { name: "final hop with amp", @@ -271,6 +280,7 @@ var decodePayloadTests = []decodePayloadTest{ 0x09, }, shouldHaveAMP: true, + isFinalHop: true, }, { name: "final hop with metadata", @@ -488,6 +498,14 @@ var decodePayloadTests = []decodePayloadTest{ 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, }, shouldHaveRouteBlindingInfo: true, + // Expect validation for intermediate hop to complain about + // inclusion of first (lowest type ID #) missing type (amt). + // isFinalHop: true, // christmas-tree does not error for final hop. + expErr: hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.IncludedViolation, + FinalHop: false, + }, }, } @@ -528,7 +546,9 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) { testChildIndex = uint32(9) ) - p, err := hop.NewPayloadFromReader(bytes.NewReader(test.payload)) + p, err := hop.NewPayloadFromReader( + bytes.NewReader(test.payload), test.isFinalHop, + ) if !reflect.DeepEqual(test.expErr, err) { t.Fatalf("expected error mismatch, want: %v, got: %v", test.expErr, err) @@ -613,3 +633,234 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) { t.Fatalf("invalid custom records") } } + +type decodeRouteBlindingPayloadTest struct { + name string + routeBlindingPayload []byte + expectedErr error + + // isFinalHop indicates that our sphinx implementation has encountered + // an all-zero 32 byte onion HMAC signaling we are the final hop. + isFinalHop bool +} + +var decodeRouteBlindingPayloadTests = []decodeRouteBlindingPayloadTest{ + { + name: "empty route blinding TLV payload", + routeBlindingPayload: []byte{}, + expectedErr: hop.ErrInvalidPayload{ + Type: record.PaymentRelayOnionType, + Violation: hop.OmittedViolation, + FinalHop: false, + }, + }, + { + name: "empty route blinding TLV payload for final hop", + routeBlindingPayload: []byte{}, + expectedErr: hop.ErrInvalidPayload{ + Type: record.PathIDOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, // Needs to match what sphinx package says. + }, + isFinalHop: true, // sphinx package says... + }, + { + name: "introduction node blinded route", + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // next node ID + byte(int(record.NextNodeIDOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // path ID (ONLY set for final node) + // 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // blinding point override + byte(int(record.BlindingOverrideOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // payment constraints + byte(int(record.PaymentConstraintsOnionType)), 0x0c, + 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // END: route blinding TLV data + }, + }, + { + name: "intermediate hop blinded route w/ next hop", + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // END: route blinding TLV data + }, + }, + { + name: "intermediate hop blinded route w/ next node ID", + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // next node ID + byte(int(record.NextNodeIDOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // END: route blinding TLV data + }, + }, + { + name: "intermediate hop blinded route missing both next_hop " + // error case + "and next_node_id", + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // - no next hop + // - no next node ID + // payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // END: route blinding TLV data + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.BlindedNextHopOnionType, + Violation: hop.OmittedViolation, + FinalHop: false, + }, + }, + { + name: "intermediate hop blinded route missing payment relay", // error case + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // - no payment relay (MUST be set for blind hops) + // END: route blinding TLV data + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.PaymentRelayOnionType, + Violation: hop.OmittedViolation, + FinalHop: false, + }, + }, + { + name: "final hop blinded route", + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // path ID (ONLY set for final node) + byte(int(record.PathIDOnionType)), 0x04, 0xff, 0x00, 0xff, 0x00, + // END: route blinding TLV data + }, + isFinalHop: true, + }, + { + name: "final hop blinded route missing path ID", // error case + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // - no path ID (MUST be set for final node) + // END: route blinding TLV data + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.PathIDOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + }, + isFinalHop: true, + }, + { + name: "christmas tree route blinding payload (all lights on)", + routeBlindingPayload: []byte{ + // START: route blinding TLV data + // padding + byte(int(record.PaddingOnionType)), 0x04, 0x00, 0x01, 0x00, 0x00, + // next hop + byte(int(record.BlindedNextHopOnionType)), 0x08, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // next node ID + byte(int(record.NextNodeIDOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // path ID (ONLY set for final node) + byte(int(record.PathIDOnionType)), 0x04, 0xff, 0x00, 0xff, 0x00, + // blinding point override + byte(int(record.BlindingOverrideOnionType)), 0x21, + 0x02, 0xee, 0xc7, 0x24, 0x5d, 0x6b, 0x7d, 0x2c, 0xcb, 0x30, 0x38, + 0x0b, 0xfb, 0xe2, 0xa3, 0x64, 0x8c, 0xd7, 0xa9, 0x42, 0x65, 0x3f, + 0x5a, 0xa3, 0x40, 0xed, 0xce, 0xa1, 0xf2, 0x83, 0x68, 0x66, 0x19, + // payment relay + byte(int(record.PaymentRelayOnionType)), 0x0a, + 0x00, 0x00, 0x4e, 0x20, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x28, + // payment constraints + byte(int(record.PaymentConstraintsOnionType)), 0x0c, + 0x00, 0x00, 0x03, 0xe8, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + // END: route blinding TLV data + }, + }, +} + +// TestDecodeBlindHopPayloadRecordValidation asserts that parsing the route +// blinding TLV payloads in the tests yields the expected errors depending +// on whether the proper fields were included or omitted. +// +// NOTE(9/16/22): This test isolates the parsing of the route blinding TLV +// payload from the parsing of the top level onion TLV payload and excercises +// the little validation logic we can at this stage. +func TestDecodeBlindHopPayloadRecordValidation(t *testing.T) { + for _, test := range decodeRouteBlindingPayloadTests { + t.Run(test.name, func(t *testing.T) { + testDecodeBlindHopPayloadValidation(t, test) + }) + } +} + +func testDecodeBlindHopPayloadValidation(t *testing.T, + test decodeRouteBlindingPayloadTest) { + + // Parse the route blinding TLV payload. + // NOTE: This test assumes the route blinding + // payload has already been decrypted. + _, err := hop.NewBlindHopPayloadFromReader( + bytes.NewReader(test.routeBlindingPayload), test.isFinalHop, + ) + + if !reflect.DeepEqual(test.expectedErr, err) { + t.Fatalf("expected error mismatch, want: %v, got: %v", + test.expectedErr, err) + } + +} diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index 9eac221dd2..c94c893ff2 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -334,6 +334,11 @@ func (r *mockHopIterator) HopPayload() (*hop.Payload, error) { return h, nil } +func (r *mockHopIterator) IsFinalHop() bool { + + return len(r.hops) == 0 +} + func (r *mockHopIterator) ExtraOnionBlob() []byte { return nil } From b9d84f39c4c5c9d266be49c0e65856c7feccdb5e Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Wed, 16 Nov 2022 18:59:36 -0500 Subject: [PATCH 06/16] htlcswitch/hop: add invalid blind hop payload error --- htlcswitch/hop/payload.go | 16 ++++++++++++++-- htlcswitch/hop/payload_test.go | 7 +++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 5b742ee153..8f719bbef9 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -61,6 +61,10 @@ type ErrInvalidPayload struct { // in the route (identified by next hop id), otherwise the violation is // for an intermediate hop. FinalHop bool + + // BlindHop indicates that the violation occurred while our node was + // processing a hop for a blinded route, otherwise we are a hop in a normal route. + BlindHop bool } // Error returns a human-readable description of the invalid payload error. @@ -70,8 +74,13 @@ func (e ErrInvalidPayload) Error() string { hopType = "final" } - return fmt.Sprintf("onion payload for %s hop %v record with type %d", - hopType, e.Violation, e.Type) + payloadType := "onion" + if e.BlindHop { + payloadType = "route blinding" + } + + return fmt.Sprintf("%s payload for %s hop %v record with type %d", + payloadType, hopType, e.Violation, e.Type) } // Payload encapsulates all information delivered to a hop in an onion payload. @@ -372,6 +381,7 @@ func ValidateRouteBlindingPayloadTypes(parsedTypes tlv.TypeMap, Type: record.PaymentRelayOnionType, Violation: OmittedViolation, FinalHop: false, + BlindHop: true, } } @@ -381,6 +391,7 @@ func ValidateRouteBlindingPayloadTypes(parsedTypes tlv.TypeMap, Type: record.BlindedNextHopOnionType, Violation: OmittedViolation, FinalHop: false, + BlindHop: true, } } } else { @@ -391,6 +402,7 @@ func ValidateRouteBlindingPayloadTypes(parsedTypes tlv.TypeMap, Type: record.PathIDOnionType, Violation: OmittedViolation, FinalHop: true, + BlindHop: true, } } } diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go index d323731be9..024385fd4f 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -410,6 +410,7 @@ var decodePayloadTests = []decodePayloadTest{ Type: record.PaymentRelayOnionType, Violation: hop.OmittedViolation, FinalHop: false, + BlindHop: true, }, }, { @@ -451,6 +452,7 @@ var decodePayloadTests = []decodePayloadTest{ Type: record.PathIDOnionType, Violation: hop.OmittedViolation, FinalHop: true, + BlindHop: true, }, isFinalHop: true, }, @@ -652,6 +654,7 @@ var decodeRouteBlindingPayloadTests = []decodeRouteBlindingPayloadTest{ Type: record.PaymentRelayOnionType, Violation: hop.OmittedViolation, FinalHop: false, + BlindHop: true, }, }, { @@ -661,6 +664,7 @@ var decodeRouteBlindingPayloadTests = []decodeRouteBlindingPayloadTest{ Type: record.PathIDOnionType, Violation: hop.OmittedViolation, FinalHop: true, // Needs to match what sphinx package says. + BlindHop: true, }, isFinalHop: true, // sphinx package says... }, @@ -750,6 +754,7 @@ var decodeRouteBlindingPayloadTests = []decodeRouteBlindingPayloadTest{ Type: record.BlindedNextHopOnionType, Violation: hop.OmittedViolation, FinalHop: false, + BlindHop: true, }, }, { @@ -768,6 +773,7 @@ var decodeRouteBlindingPayloadTests = []decodeRouteBlindingPayloadTest{ Type: record.PaymentRelayOnionType, Violation: hop.OmittedViolation, FinalHop: false, + BlindHop: true, }, }, { @@ -795,6 +801,7 @@ var decodeRouteBlindingPayloadTests = []decodeRouteBlindingPayloadTest{ Type: record.PathIDOnionType, Violation: hop.OmittedViolation, FinalHop: true, + BlindHop: true, }, isFinalHop: true, }, From eb929f46e5c6163be5b3905145dc5e2563397cbb Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 20 Sep 2022 20:30:30 -0400 Subject: [PATCH 07/16] multi: initialize `ChannelLink` with persistent node ID key descriptor The ChannelLink must have access to our persistent node ID key in order to process hops in a blinded route. TODO(9/20/22): Ensure that we do not undo the hardwork done to tighten up signing abstractions and remove all raw private key handling in: https://github.com/lightningnetwork/lnd/issues/3929 --- htlcswitch/link.go | 5 +++++ peer/brontide.go | 9 +++++++++ server.go | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index c3c824afc1..77fffc7f2f 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/htlcswitch/hodl" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" @@ -295,6 +296,10 @@ type ChannelLinkConfig struct { // events through. HtlcNotifier htlcNotifier + // NodeKeyECDH provides access to our persistent node ID private key + // which is needed to process onions for hops in a blinded route. + NodeKeyECDH keychain.SingleKeyECDH + // FailAliasUpdate is a function used to fail an HTLC for an // option_scid_alias channel. FailAliasUpdate func(sid lnwire.ShortChannelID, diff --git a/peer/brontide.go b/peer/brontide.go index 018f81245d..d28045bba9 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -31,6 +31,7 @@ import ( "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" @@ -309,6 +310,13 @@ type Config struct { // ChannelLink. ServerPubKey [33]byte + // NodeKeyECDH is the the ECDH capable wrapper of our persistent node + // ID private key. This key is needed in order to process onions for + // hops in a blinded route. NOTE(9/20/22): With a library change it + // could be had via the sphinx.Router. Does this make the above + // 'ServerPubKey' redundant? + NodeKeyECDH keychain.SingleKeyECDH + // ChannelCommitInterval is the maximum time that is allowed to pass between // receiving a channel state update and signing the next commitment. // Setting this to a longer duration allows for more efficient channel @@ -978,6 +986,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, NotifyInactiveChannel: p.cfg.ChannelNotifier.NotifyInactiveChannelEvent, HtlcNotifier: p.cfg.HtlcNotifier, GetAliases: p.cfg.GetAliases, + NodeKeyECDH: p.cfg.NodeKeyECDH, } // Before adding our new link, purge the switch of any pending or live diff --git a/server.go b/server.go index 6e44cbd6bf..935af78c41 100644 --- a/server.go +++ b/server.go @@ -506,6 +506,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, replayLog := htlcswitch.NewDecayedLog( dbs.DecayedLogDB, cc.ChainNotifier, ) + + // Configure our sphinx onion packet router with + // our node's key pair (p, P). sphinxRouter := sphinx.NewRouter( nodeKeyECDH, cfg.ActiveNetParams.Params, replayLog, ) @@ -3697,6 +3700,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, WritePool: s.writePool, ReadPool: s.readPool, Switch: s.htlcSwitch, + NodeKeyECDH: s.identityECDH, // Need access to nodeID private key (either directly or via abstraction) in order to process onions for hops in a blinded route. InterceptSwitch: s.interceptableSwitch, ChannelDB: s.chanStateDB, ChannelGraph: s.graphDB, From 332e7fdd5b739177d20d05aad1c96db3b6dd53d6 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 20 Sep 2022 20:49:26 -0400 Subject: [PATCH 08/16] lnwire: add route blinding TLV to `UpdateAddHTLC` message Add the ephemeral (route) blinding point as a TLV extension to the UpdateAddHTLC message. If an HTLC has an ephemeral blinding point, it will be needed to decrypt the onion as the onion encryptor is expected to have encrypted the onion to a blinded form of our node ID, rather than to our persistent node ID. --- lnwire/blinding_point.go | 72 +++++++++++++++++++++++++++++++++++ lnwire/blinding_point_test.go | 26 +++++++++++++ lnwire/lnwire_test.go | 1 + lnwire/update_add_htlc.go | 54 +++++++++++++++++++++++++- 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 lnwire/blinding_point.go create mode 100644 lnwire/blinding_point_test.go diff --git a/lnwire/blinding_point.go b/lnwire/blinding_point.go new file mode 100644 index 0000000000..dedb642d3a --- /dev/null +++ b/lnwire/blinding_point.go @@ -0,0 +1,72 @@ +package lnwire + +import ( + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // BlindingPointRecordType is the type which refers to an ephemeral + // public key used in route blinding. + BlindingPointRecordType tlv.Type = 1 +) + +// NOTE(9/20/22): The swap from tlv.Record to tlv.RecordProducer inside +// ExtractRecords() requires us to define this type and associated (en/de)code +// methods, as it precludes us from being able to leverage the 'record' +// package's "primitive" TLV types. Instead it pushes us to define our own +// custom type so that we may satisfy the RecordProducer interface{}. +// It might be nice to be able to take advantage of the primitive types that +// the 'record' package offers. No type casting. The change to the +// RecordProducer{} interface is said to "reduce line noise", but I am not yet +// sure what this means. Are there any more details on why this was swapped? +// https://github.com/lightningnetwork/lnd/pull/5669/commits/57b7a668c00ad3ebc049fd3517517118389238e0#diff-0352ea3877666d95afd22632bf69ef124ae1d072a206cf257a7cef618b6562b1R54 +type BlindingPoint btcec.PublicKey + +// Record returns a TLV record that can be used to encode/decode the +// ephemeral (route) blinding point type from a given TLV stream. +func (b *BlindingPoint) Record() tlv.Record { + return tlv.MakeStaticRecord( + BlindingPointRecordType, b, 33, blindingPointEncoder, blindingPointDecoder, + ) +} + +// blindingPointEncoder is a custom TLV encoder for the BlindingPoint record. +func blindingPointEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*BlindingPoint); ok { + // Convert from *BlindingPoint to **btcec.PublicKey + // so that we can use tlv.EPubKey()? + key := new(btcec.PublicKey) + *key = btcec.PublicKey(*v) + if err := tlv.EPubKey(w, &key, buf); err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "lnwire.BlindingPoint") +} + +// blindingPointDecoder is a custom TLV decoder for the BlindingPoint record. +func blindingPointDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*BlindingPoint); ok { + + // 1. Read bytes into internally defined variable. + // Define **btcec.PublicKey so that we can use tlv.DPubKey()? + var blindingPoint *btcec.PublicKey + + if err := tlv.DPubKey(r, &blindingPoint, buf, l); err != nil { + return err + } + + // 2. Convert internal variable to desired custom type. + *v = BlindingPoint(*blindingPoint) + + return nil + } + + return tlv.NewTypeForDecodingErr(val, "lnwire.BlindingPoint", l, 33) +} diff --git a/lnwire/blinding_point_test.go b/lnwire/blinding_point_test.go new file mode 100644 index 0000000000..ffa4689fb9 --- /dev/null +++ b/lnwire/blinding_point_test.go @@ -0,0 +1,26 @@ +package lnwire + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestRouteBlindingPointEncodeDecode tests that we're able to properly +// encode and decode a BlindingPoint type within TLV streams. +func TestRouteBlindingPointEncodeDecode(t *testing.T) { + t.Parallel() + + pubKey, _ := randPubKey() + blindingPoint := BlindingPoint(*pubKey) + + var extraData ExtraOpaqueData + require.NoError(t, extraData.PackRecords(&blindingPoint)) + + var blindingPoint2 BlindingPoint + tlvs, err := extraData.ExtractRecords(&blindingPoint2) + require.NoError(t, err) + + require.Contains(t, tlvs, BlindingPointRecordType) + require.Equal(t, blindingPoint, blindingPoint2) +} diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index 44d6cfb9b3..c2eb052585 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -947,6 +947,7 @@ func TestLightningWireProtocol(t *testing.T) { v[0] = reflect.ValueOf(req) }, + // TODO(9/20/22): create test to exercise route blinding field. } // With the above types defined, we'll now generate a slice of diff --git a/lnwire/update_add_htlc.go b/lnwire/update_add_htlc.go index 666a549427..2e511ac00c 100644 --- a/lnwire/update_add_htlc.go +++ b/lnwire/update_add_htlc.go @@ -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 @@ -54,6 +57,18 @@ type UpdateAddHTLC struct { // used in the subsequent UpdateAddHTLC message. OnionBlob [OnionPacketSize]byte + // BlindingPoint is an ephemeral public key used for route blinding to + // establish a shared secret between a processing node in the + // blinded portion of a route and the node which built the blinded + // route (usually the recipient). The shared secret can then be used + // to derive a blinded version of our persistent node ID key pair + // necessary to decrypt the onion or to decrypt the route blinding payload. + // When received via TLV extension to UpdateAddHTLC, the blinding point/ + // shared secret should be used to decrypt the onion as the onion + // encryptor is expected to have encrypted the onion to a blinded form + // of our node ID, rather than to our persistent node ID. + BlindingPoint *btcec.PublicKey + // 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. @@ -74,7 +89,8 @@ 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, + // Read all the mandatory fields in the UpdateAddHTLC message. + err := ReadElements(r, &c.ChanID, &c.ID, &c.Amount, @@ -83,6 +99,27 @@ func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error { c.OnionBlob[:], &c.ExtraData, ) + if err != nil { + return err + } + + // Attempt to parse ephemeral (route) blinding point record. + var blindingPoint BlindingPoint + typeMap, err := c.ExtraData.ExtractRecords( + &blindingPoint, + ) + if err != nil { + return err + } + + // Set the corresponding TLV types if they were included in the stream. + if val, ok := typeMap[BlindingPointRecordType]; ok && val == nil { + point := new(btcec.PublicKey) + *point = btcec.PublicKey(blindingPoint) + c.BlindingPoint = point + } + + return nil } // Encode serializes the target UpdateAddHTLC into the passed io.Writer @@ -90,6 +127,7 @@ func (c *UpdateAddHTLC) Decode(r io.Reader, pver uint32) error { // // This is part of the lnwire.Message interface. func (c *UpdateAddHTLC) Encode(w *bytes.Buffer, pver uint32) error { + if err := WriteChannelID(w, c.ChanID); err != nil { return err } @@ -114,6 +152,20 @@ func (c *UpdateAddHTLC) Encode(w *bytes.Buffer, pver uint32) error { return err } + // If present, write the the ephemeral (route) blinding + // point as a TLV extension. + recordProducers := []tlv.RecordProducer{} + if c.BlindingPoint != nil { + point := new(BlindingPoint) + *point = BlindingPoint(*c.BlindingPoint) + recordProducers = append(recordProducers, point) + } + + err := EncodeMessageExtraData(&c.ExtraData, recordProducers...) + if err != nil { + return err + } + return WriteBytes(w, c.ExtraData) } From 29c8b964a49dc795a1ce0afdc725f55821d68d73 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 20 Sep 2022 20:52:18 -0400 Subject: [PATCH 09/16] lnwallet: add ephemeral route blinding point to `PaymentDescriptor` This places the route blinding point in our in-memory update log, so it is available for use during onion & route blinding payload decryption for nodes inside a blinded route. The blinding point may also be used to determine how errors, both local and downstream, will be processed. A forwarding node has two channel links involved in forwarding an HTLC, both of which have their own update logs, payment descriptors, & channel state machines. When an HTLC is forwarded, it is received and added to a remote update log by the incoming link and added to a local update log prior to commitment by the outgoing link. If we would like the channel state machine to preserve whether or not the HTLC had an ephemeral blinding point, then the blinding point must be looked after in both locations. TODO(11/21/22): - When processing a failure, the HTLC flows through the switch in the opposite direction. We need to decide which link will handle determining how errors are to be forwarded. Consider HTLC update retransmission (in-memory and from disk) code path. It seems to me we have two types of retransmission: - In memory retransmission (ie: our node had an internal error processing the HTLC update, or our peer restarted) - From disk retransmission (ie: our link/switch/node restarted - power outage, crashed due to bug, etc.) We will need to make sure that HTLC updates (adds, settle, fails) for blind hops are processed correctly after a restart. --- lnwallet/channel.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index acdfdea057..a90a67f797 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -376,6 +376,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 ephemeral public key used to derive a blinded + // version of our persistent node ID key pair for decrypting the onion + // when forwarding in the blinded portion of a route. Our upstream peer + // is expected to include this as a TLV extension to UpdateAddHTLC. + BlindingPoint *btcec.PublicKey } // PayDescsFromRemoteLogUpdates converts a slice of LogUpdates received from the @@ -5300,6 +5306,13 @@ func (lc *LightningChannel) htlcAddDescriptor(htlc *lnwire.UpdateAddHTLC, HtlcIndex: lc.localUpdateLog.htlcCounter, OnionBlob: htlc.OnionBlob[:], OpenCircuitKey: openKey, + // IMPORTANT NOTE(11/18/22): + // If an HTLC contains an ephemeral (route) blinding + // point, then we must store a reference to it inside + // the update log. Later, if the Add is failed, we + // may be able to use it to determine how to forward the + // error back towards the sender. + BlindingPoint: htlc.BlindingPoint, } } @@ -5355,6 +5368,19 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, err LogIndex: lc.remoteUpdateLog.logIndex, HtlcIndex: lc.remoteUpdateLog.htlcCounter, OnionBlob: htlc.OnionBlob[:], + // NOTE(11/21/22): We batch process HTLC updates in an asynchronous + // manner. The incoming link for an HTLC Add update received a + // blinding point. We'll make sure to include it in the payment + // descriptor we store (in-memory) on our update log so that it + // is available to decrypt the onion with when we begin batch + // processing this HTLC. + // + // KEY QUESTION(11/21/22): When an HTLC update is added to our + // LN channel state machine, how is it represented and where + // does it go (ie: live so that we can recover it later)? + // I am not sure that PaymenDescriptors in our in-memory + // update log or LogUpdate is the full story here!! + BlindingPoint: htlc.BlindingPoint, } localACKedIndex := lc.remoteCommitChain.tail().ourMessageIndex @@ -5625,6 +5651,18 @@ func (lc *LightningChannel) ReceiveFailHTLC(htlcIndex uint64, reason []byte, LogIndex: lc.remoteUpdateLog.logIndex, EntryType: Fail, FailReason: reason, + // IMPORTANT NOTE(11/17/22): + // Where we need the blinding point in channel state machine's + // update log depends on how errors are to be processed and + // possibly on how HTLC retransmission is handled. + // + // The LN state machine stores the blinding point + // associated with an Add if the ADD was associated + // with a blinded route. If that Add is now being + // failed, we may want a reference to its ephemeral + // blinding point so we can use it to determine which + // error will be forwarded back towards the sender. + BlindingPoint: htlc.BlindingPoint, } lc.remoteUpdateLog.appendUpdate(pd) From 04b2f1852cc54bdfc51f2fbcff3635e415a99aaf Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Wed, 21 Sep 2022 22:53:09 -0400 Subject: [PATCH 10/16] lnwire: define `InvalidOnionBlinding` failure code onion error Define an opaque (non-privacy-leaking) error type which is to be used ubiquitously for errors which occur inside a blinded route regardless of the internal or downstream reason for failure. --- lnwire/onion_error.go | 51 +++++++++++++++++++++++++++++++++++++- lnwire/onion_error_test.go | 1 + 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lnwire/onion_error.go b/lnwire/onion_error.go index 593f58e3d6..017401bf42 100644 --- a/lnwire/onion_error.go +++ b/lnwire/onion_error.go @@ -80,6 +80,7 @@ const ( CodeExpiryTooFar FailCode = 21 CodeInvalidOnionPayload = FlagPerm | 22 CodeMPPTimeout FailCode = 23 + CodeInvalidOnionBlinding = FlagBadOnion | FlagPerm | 24 ) // String returns the string representation of the failure code. @@ -157,6 +158,9 @@ func (c FailCode) String() string { case CodeMPPTimeout: return "MPPTimeout" + case CodeInvalidOnionBlinding: + return "InvalidOnionBlinding" + default: return "" } @@ -496,7 +500,7 @@ func (f *FailInvalidOnionVersion) Encode(w *bytes.Buffer, pver uint32) error { // // NOTE: May only be returned by intermediate nodes. type FailInvalidOnionHmac struct { - // OnionSHA256 hash of the onion blob which haven't been proceeded. + // OnionSHA256 hash of the onion blob which we failed to process. OnionSHA256 [sha256.Size]byte } @@ -1215,6 +1219,48 @@ func (f *FailMPPTimeout) Error() string { return f.Code().String() } +// InvalidOnionBlinding is a catch all error signalling something +// went wrong during the processing of a hop in a blinded route. +// +// NOTE: This MUST be returned by nodes in a blinded route. +type InvalidOnionBlinding struct { + // OnionSHA256 hash of the onion blob which haven't been proceeded. + OnionSHA256 [sha256.Size]byte +} + +// NewInvalidOnionBlinding creates new instance of the InvalidOnionBlinding. +func NewInvalidOnionBlinding(onion []byte) *InvalidOnionBlinding { + return &InvalidOnionBlinding{OnionSHA256: sha256.Sum256(onion)} +} + +// Code returns the failure unique code. +// +// NOTE: Part of the FailureMessage interface. +func (f *InvalidOnionBlinding) Code() FailCode { + return CodeInvalidOnionBlinding +} + +// Decode decodes the failure from a byte stream. +// +// NOTE: Part of the Serializable interface. +func (f *InvalidOnionBlinding) Decode(r io.Reader, pver uint32) error { + return ReadElement(r, f.OnionSHA256[:]) +} + +// Encode writes the failure in a byte stream. +// +// NOTE: Part of the Serializable interface. +func (f *InvalidOnionBlinding) Encode(w *bytes.Buffer, pver uint32) error { + return WriteBytes(w, f.OnionSHA256[:]) +} + +// Returns a human readable string describing the target FailureMessage. +// +// NOTE: Implements the error interface. +func (f *InvalidOnionBlinding) Error() string { + return fmt.Sprintf("InvalidOnionBlinding(onion_sha=%x)", f.OnionSHA256[:]) +} + // DecodeFailure decodes, validates, and parses the lnwire onion failure, for // the provided protocol version. func DecodeFailure(r io.Reader, pver uint32) (FailureMessage, error) { @@ -1410,6 +1456,9 @@ func makeEmptyOnionError(code FailCode) (FailureMessage, error) { case CodeMPPTimeout: return &FailMPPTimeout{}, nil + case CodeInvalidOnionBlinding: + return &InvalidOnionBlinding{}, nil + default: return nil, errors.Errorf("unknown error code: %v", code) } diff --git a/lnwire/onion_error_test.go b/lnwire/onion_error_test.go index 27bba342e7..4c5ca23cb9 100644 --- a/lnwire/onion_error_test.go +++ b/lnwire/onion_error_test.go @@ -56,6 +56,7 @@ var onionFailures = []FailureMessage{ NewFinalIncorrectCltvExpiry(testCtlvExpiry), NewFinalIncorrectHtlcAmount(testAmount), NewInvalidOnionPayload(testType, testOffset), + NewInvalidOnionBlinding(testOnionHash), } // TestEncodeDecodeCode tests the ability of onion errors to be properly encoded From 41b1eb9b741aa5085eec765771f49bc858a76940 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Sun, 6 Nov 2022 17:07:23 -0500 Subject: [PATCH 11/16] htlcswitch/link: add onion payload validation for route blinding We can no longer perform the totality of the validation when the onion TLV payload is parsed. The validation we perform depends on the contents of the top level onion TLV payload, the route blinding payload, and the UpdateAddHTLC message. --- htlcswitch/hop/payload.go | 16 ++ htlcswitch/link.go | 210 ++++++++++++++++++++ htlcswitch/link_test.go | 406 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 632 insertions(+) diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 8f719bbef9..784177323b 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -28,6 +28,16 @@ const ( // RequiredViolation indicates that an unknown even type was found in // the payload that we could not process. RequiredViolation + + // OverloadedViolation indicates that an expected type was provided + // in more than one place. (used only for blinding point) + OverloadedViolation + + // InsufficientViolation indicates that the provided type does + // not satisfy constraints. + // NOTE(10/5/22): Used for payment constraints. Does this belong here? + // Should payment constraints be handled separately? + InsufficientViolation ) // String returns a human-readable description of the violation as a verb. @@ -42,6 +52,12 @@ func (v PayloadViolation) String() string { case RequiredViolation: return "required" + case OverloadedViolation: + return "overloaded" + + case InsufficientViolation: + return "insufficient" + default: return "unknown violation" } diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 77fffc7f2f..90e8602154 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" + "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/ticker" ) @@ -3265,6 +3266,215 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, return l.processHtlcResolution(event, htlc) } +// ValidateTopLevelPayloadAndAddMsg verifies that the top level onion TLV +// payload and UpdateAddHTLC message are properly formed for blind hops +// as described in BOLT-04. +// +// TODO(10/5/22): Decide whether to go with human readable error or +// TLV type error (hop.ErrInvalidPayload) for validation errors. +// Would we benefit from a human readable string? Who is consuming the error? +// Is it weird to use another package's error type? +func ValidateTopLevelPayloadAndAddMsg(p *hop.Payload, pd *lnwallet.PaymentDescriptor) error { + + // Blind hops must contain an encrypted route blinding payload. + if p.RouteBlindingEncryptedData == nil { + return hop.ErrInvalidPayload{ + Type: record.RouteBlindingEncryptedDataOnionType, + Violation: hop.OmittedViolation, + } + } + + // Do not accept HTLCs for which a blinding point has been set + // in both the onion TLV payload and the UpdateAddHTLC message. + if p.BlindingPoint != nil && pd.BlindingPoint != nil { + return hop.ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: hop.OverloadedViolation, + } + } + + // Do not accept HTLCs for which we have no blinding point in + // either the onion TLV payload or the UpdateAddHTLC message. + // The sender is trying to use route blinding, but we didn't receive + // the blinding point used to derive the key needed to decrypt the + // onion. The sender or the previous peer is buggy or malicious. (eclair) + if p.BlindingPoint == nil && pd.BlindingPoint == nil { + return hop.ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: hop.OmittedViolation, + } + } + + return nil +} + +// ValidateTopLevelPayloadRouteBlinding, with knowledge of the contents +// of the route blinding payload, validates that the top level onion TLV +// payload is properly formed for blind hops as described in BOLT-04. +// +// NOTE: We now have validation to do across two levels of TLV payloads. +// What is present in one payload effects our expectation of what is present +// in the other payload. As a result, this stage of validation must wait +// until after the route blinding payload is decrypted. +func ValidateTopLevelPayloadRouteBlinding(pld *hop.Payload, isFinalHop bool) error { + + // Validation for intermediate hops. + if !isFinalHop { + // We MUST not use ANY TLVs other than encrypted_recipient_data + // & current_blinding_point. + if pld.FwdInfo.AmountToForward != 0 { + return hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.IncludedViolation, + } + } + if pld.FwdInfo.OutgoingCTLV != 0 { + return hop.ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: hop.IncludedViolation, + } + } + // The top level onion payload should not list the next hop + // we should forward to. This will be found in the encrypted + // route blinding TLV payload. + if pld.FwdInfo.NextHop != hop.Exit { + return hop.ErrInvalidPayload{ + Type: record.NextHopOnionType, + Violation: hop.IncludedViolation, + } + } + + if pld.TotalAmountMsat != 0 { + return hop.ErrInvalidPayload{ + Type: record.TotalAmountMsatOnionType, + Violation: hop.IncludedViolation, + } + } + } + + // Validation for final hops. + if isFinalHop { + // We MUST use amt_to_forward & outgoing_cltv_value. + if pld.FwdInfo.AmountToForward == 0 { + return hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + } + } + if pld.FwdInfo.OutgoingCTLV == 0 { + return hop.ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + } + } + + // We MUST NOT use ANY TLVs other than encrypted_recipient_data, + // current_blinding_point, amt_to_forward, outgoing_cltv_value, + // & total_amount_msat. + if pld.FwdInfo.NextHop != hop.Exit { + return hop.ErrInvalidPayload{ + Type: record.NextHopOnionType, + Violation: hop.IncludedViolation, + FinalHop: true, + } + } + // TODO(11/15/22): We MUST use total_amount_msat. + // if pld.TotalAmountMsat == 0 { + // return hop.ErrInvalidPayload{ + // Type: record.TotalAmountMsatOnionType, + // Violation: hop.OmittedViolation, + // FinalHop: true, + // } + // } + } + + // Validation common to both intermediate and final hops. + // Are we double dipping with validation testing done at + // payload parse time here? + if pld.MPP != nil { // checked at parse time! + return hop.ErrInvalidPayload{ + Type: record.MPPOnionType, + Violation: hop.IncludedViolation, + } + } + if pld.AMP != nil { // checked at parse time! + return hop.ErrInvalidPayload{ + Type: record.AMPOnionType, + Violation: hop.IncludedViolation, + } + } + if pld.Metadata() != nil { + return hop.ErrInvalidPayload{ + Type: record.MetadataOnionType, + Violation: hop.IncludedViolation, + } + } + + return nil +} + +// ValidateRouteBlindingPaymentConstraints verifies the incoming payment +// satisfies the set of payment constraints specified by the creator of +// the blinded route. The constraints are chosen by the route blinder in +// order to limit an adversary's ability to unblind nodes within the route. +// +// NOTE: These are additional constraints on forwarded payments +// above and beyond our usual forwarding policy. +func ValidateRouteBlindingPaymentConstraints(p *hop.BlindHopPayload, + pd *lnwallet.PaymentDescriptor) error { + + // Only validate payment constraints if included by the route blinder. + if p.PaymentConstraints == nil { + return nil + } + + maxCltvExpiry := p.PaymentConstraints.MaxCltvExpiryDelta + minimumHtlcMsat := p.PaymentConstraints.HtlcMinimumMsat + // allowedFeatures := p.PaymentConstraints.AllowedFeatures + + // If the builder of the blinded route included payment + // constraints then we should validate that our forwarding + // information satisfies them here. + // - MUST return an error if the expiry is greater than + // `encrypted_recipient_data.payment_constraints.max_cltv_expiry`.' + if pd.Timeout > maxCltvExpiry { + // TODO(10/5/22): Figure out if this error type + // works here. Would it not be better to provide info + // on how the value is insufficient? + return hop.ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: hop.InsufficientViolation, + } + + } + + // - MUST return an error if the amount is below + // `encrypted_recipient_data.payment_constraints.htlc_minimum_msat`. + if uint64(pd.Amount) < minimumHtlcMsat { + return hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.InsufficientViolation, + } + } + + // TODO(8/14/22): at some point we may need to check + // that payments do not attempt to use features which + // have not been explicitly permitted. + // - MUST return an error if `encrypted_recipient_data.allowed_features.features` contains an unknown feature bit (even if it is odd) + // - MUST return an error if the payment uses a feature not included in `encrypted_recipient_data.payment_constraints.allowed_features`. + // UPDATE(8/24/22): This is being moved to its own TLV record. + // UPDATE(9/2/22): We need to make sure our node knows + // about ALL features, even the it's okay to be ODD ones. + // if allowedFeatures == nil { + // return fmt.Errorf("blind hop made use of " + + // "disallowed feature") + // } + + return nil +} + // settleHTLC settles the HTLC on the channel. func (l *channelLink) settleHTLC(preimage lntypes.Preimage, pd *lnwallet.PaymentDescriptor) error { diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index b16f3893c5..b2bbace47d 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "crypto/sha256" "encoding/binary" + "encoding/hex" "fmt" "io" "net" @@ -34,6 +35,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/ticker" "github.com/stretchr/testify/require" ) @@ -6606,3 +6608,407 @@ func assertFailureCode(t *testing.T, err error, code lnwire.FailCode) { code, rtErr.WireMessage().Code()) } } + +type validateOnionPayloadTest struct { + name string + onionPayload *hop.Payload + routeBlindingPayload *hop.BlindHopPayload + // The internal representation of the information contained + // in the update message for this htlc (UpdateAddHTLC). + // Decide which is better to use for the test. + htlc lnwallet.PaymentDescriptor + // htlc lnwire.UpdateAddHTLC + isFinalHop bool + expectedErr error +} + +var validateOnionPayloadTests = []validateOnionPayloadTest{ + { + name: "introduction node blinded route", + onionPayload: generatePayload("intro"), + routeBlindingPayload: generateBlindPayload("intro"), + }, + { + name: "intermediate hop blinded route w/ next hop", + onionPayload: generatePayload("intermediate"), + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + // intermediate node receives blinding point in update message. + BlindingPoint: &btcec.PublicKey{}, + }, + }, + { + name: "final hop blinded route", + onionPayload: generatePayload("final"), + routeBlindingPayload: generateBlindPayload("final"), + htlc: lnwallet.PaymentDescriptor{ + // final node receives blinding point in update message + BlindingPoint: &btcec.PublicKey{}, + }, + isFinalHop: true, + }, + { + name: "blind hop with blinding point in both msg and tlv payload", + onionPayload: generatePayload("intro"), + routeBlindingPayload: generateBlindPayload("intro"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: hop.OverloadedViolation, + }, + }, + { + name: "blind hop missing blinding point in msg and tlv payload", + onionPayload: &hop.Payload{ + RouteBlindingEncryptedData: []byte{}, + }, + routeBlindingPayload: &hop.BlindHopPayload{}, + htlc: lnwallet.PaymentDescriptor{}, + expectedErr: hop.ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: hop.OmittedViolation, + }, + }, + // { + // name: "blind hop missing payment_relay", (covered at parse time!!) + // onionPayload: generatePayload("intermediate"), + // routeBlindingPayload: generateBlindPayload("unsatisfiable-constraints"), + // htlc: lnwallet.PaymentDescriptor{ + // BlindingPoint: &btcec.PublicKey{}, + // }, + // // errExpected: true, + // }, + { + name: "blind hop payment_relay which fails to meet constraints", + onionPayload: generatePayload("intermediate"), + routeBlindingPayload: generateBlindPayload("unsatisfiable-constraints"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.InsufficientViolation, + }, + }, + { + name: "blind hop payment_relay fails to meet constraints - expired", + onionPayload: generatePayload("intermediate"), + routeBlindingPayload: func() *hop.BlindHopPayload { + pld := generateBlindPayload("unsatisfiable-constraints") + pld.PaymentConstraints.MaxCltvExpiryDelta = 100 + return pld + }(), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + Timeout: 101, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: hop.InsufficientViolation, + }, + }, + { + name: "blind hop with amt_to_forward improperly set in " + + "top level TLV payload", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + AmountToForward: lnwire.MilliSatoshi(100), + }, + RouteBlindingEncryptedData: []byte{}, + }, + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.IncludedViolation, + }, + }, + { + name: "blind hop with timelock improperly set in top level TLV payload", + onionPayload: func() *hop.Payload { + pld := generatePayload("intermediate") + pld.FwdInfo.OutgoingCTLV = 1000 + return pld + }(), + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: hop.IncludedViolation, + }, + }, + { + name: "blind hop with short_channel_id improperly set in top level TLV payload", + onionPayload: func() *hop.Payload { + pld := generatePayload("intermediate") + pld.FwdInfo.NextHop = lnwire.NewShortChanIDFromInt(1) + return pld + }(), + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.NextHopOnionType, + Violation: hop.IncludedViolation, + }, + }, + { + name: "blind hop with MPP improperly set in top level TLV payload", + onionPayload: &hop.Payload{ + RouteBlindingEncryptedData: []byte{}, + MPP: &record.MPP{}, + }, + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.MPPOnionType, + Violation: hop.IncludedViolation, + }, + }, + { + name: "blind hop with AMP improperly set in top level TLV payload", + onionPayload: &hop.Payload{ + RouteBlindingEncryptedData: []byte{}, + AMP: &record.AMP{}, + }, + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.AMPOnionType, + Violation: hop.IncludedViolation, + }, + }, + // { + // name: "blind hop with metadata improperly set in top level TLV payload", + // onionPayload: &hop.Payload{ + // RouteBlindingEncryptedData: []byte{}, + // }, + // routeBlindingPayload: generateBlindPayload("intermediate"), + // htlc: lnwallet.PaymentDescriptor{ + // BlindingPoint: &btcec.PublicKey{}, + // }, + // errExpected: true, + // }, + { + name: "blind hop with total_amount_msat improperly set in top level TLV payload", + onionPayload: &hop.Payload{ + RouteBlindingEncryptedData: []byte{}, + TotalAmountMsat: lnwire.MilliSatoshi(100), + }, + routeBlindingPayload: generateBlindPayload("intermediate"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + expectedErr: hop.ErrInvalidPayload{ + Type: record.TotalAmountMsatOnionType, + Violation: hop.IncludedViolation, + }, + }, + { + name: "final hop blinded route missing amt in top level TLV payload", + onionPayload: func() *hop.Payload { + pld := generatePayload("final") + pld.FwdInfo.AmountToForward = lnwire.MilliSatoshi(0) + return pld + }(), + routeBlindingPayload: generateBlindPayload("final"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + isFinalHop: true, + expectedErr: hop.ErrInvalidPayload{ + Type: record.AmtOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + }, + }, + { + name: "final hop blinded route missing outgoing_cltv_value " + + "in top level TLV payload", + onionPayload: func() *hop.Payload { + pld := generatePayload("final") + pld.FwdInfo.OutgoingCTLV = 0 + return pld + }(), + routeBlindingPayload: generateBlindPayload("final"), + htlc: lnwallet.PaymentDescriptor{ + BlindingPoint: &btcec.PublicKey{}, + }, + isFinalHop: true, + expectedErr: hop.ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: hop.OmittedViolation, + FinalHop: true, + }, + }, + { + name: "intro node is last node", + onionPayload: func() *hop.Payload { + pld := generatePayload("intro") + // The sender must add + pld.FwdInfo.AmountToForward = lnwire.MilliSatoshi(100) + pld.FwdInfo.OutgoingCTLV = 100 + pld.TotalAmountMsat = lnwire.MilliSatoshi(100) + return pld + }(), + routeBlindingPayload: generateBlindPayload("final"), + htlc: lnwallet.PaymentDescriptor{}, + isFinalHop: true, + }, +} + +// NOTE: Consider just specifying the payloads direclty in the test case +// if it wouldn't be too much room. +// generateBlindPayload creates a variety of route blinding TLV payloads. +func generateBlindPayload(typ string) *hop.BlindHopPayload { + var blindPayload *hop.BlindHopPayload + + switch typ { + case "intro": + blindPayload = &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: lnwire.NewShortChanIDFromInt(1), + PaymentRelay: &record.PaymentRelay{ + CltvExpiryDelta: 0, + FeeRate: 0, + BaseFee: 100, + }, + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: 1000, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + } + + case "intermediate": + blindPayload = &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: lnwire.NewShortChanIDFromInt(1), + PaymentRelay: &record.PaymentRelay{ + CltvExpiryDelta: 0, + FeeRate: 0, + BaseFee: 100, + }, + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: 1000, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + } + + case "final": + blindPayload = &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + PathID: []byte{0xff, 0x00, 0xff, 0x00}, + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: 1000, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + } + + case "unsatisfiable-constraints": + blindPayload = &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: lnwire.NewShortChanIDFromInt(1), + PaymentRelay: &record.PaymentRelay{ + CltvExpiryDelta: 0, + FeeRate: 0, + BaseFee: 100, + }, + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: 0, + HtlcMinimumMsat: ^uint64(0), // max value + // AllowedFeatures: []byte, + }, + } + + default: + blindPayload = nil + } + + return blindPayload +} + +// generatePayload creates a variety of top level onion TLV payloads. +func generatePayload(typ string) *hop.Payload { + var hopPayload *hop.Payload + pubkeyBytes, _ := hex.DecodeString("02bedd1e7865e7476f522b02b13f137f418105154312b48c45985dd72cbf47c143") + pubKey, _ := btcec.ParsePubKey(pubkeyBytes) + + switch typ { + case "intro": + hopPayload = &hop.Payload{ + RouteBlindingEncryptedData: []byte{}, + BlindingPoint: pubKey, + } + + case "intermediate": + hopPayload = &hop.Payload{ + RouteBlindingEncryptedData: []byte{}, + } + + case "final": + hopPayload = &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + NextHop: hop.Exit, + AmountToForward: lnwire.MilliSatoshi(100), + OutgoingCTLV: 1000, + }, + TotalAmountMsat: lnwire.MilliSatoshi(100), + RouteBlindingEncryptedData: []byte{}, + } + + default: + hopPayload = nil + } + + return hopPayload +} + +// TestHopPayloadRecordValidation validates that the top level onion TLV +// payload, route blinding TLV payload, and UpdateAddHTLC message conform +// to the BOLT-04 specification. +func TestRouteBlindingRecordValidation(t *testing.T) { + for _, test := range validateOnionPayloadTests { + t.Run(test.name, func(t *testing.T) { + testRouteBlindingValidation(t, test) + }) + } +} + +func testRouteBlindingValidation(t *testing.T, test validateOnionPayloadTest) { + + errTop := ValidateTopLevelPayloadAndAddMsg(test.onionPayload, &test.htlc) + errRB := ValidateTopLevelPayloadRouteBlinding(test.onionPayload, test.isFinalHop) + errPC := ValidateRouteBlindingPaymentConstraints(test.routeBlindingPayload, &test.htlc) + + var err error + if errTop != nil { + err = errTop + } + if errRB != nil { + err = errRB + } + if errPC != nil { + err = errPC + } + + if test.expectedErr != nil { + require.ErrorIsf(t, err, test.expectedErr, "unexpected error. want: %v, got:%v") + } else { + require.NoError(t, err) + } + +} From 80073157886f6716013242de5b8d894650bc6611 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Fri, 11 Nov 2022 16:57:37 -0600 Subject: [PATCH 12/16] htlcswitch/link: add blind hop processing to `ChannelLink` Equip the channel link to process HTLCs inside a blinded route. The link will decrypt and parse the route blinding payload, use data from the payload to make a forwarding "decision", and compute the ephemeral (route) blinding point for the next hop in route. --- htlcswitch/hop/blind_hop.go | 30 ++++ htlcswitch/hop/iterator.go | 6 +- htlcswitch/hop/payload.go | 92 ++++++++++++ htlcswitch/hop/payload_test.go | 4 + htlcswitch/interfaces.go | 21 +++ htlcswitch/link.go | 247 +++++++++++++++++++++++++++++++-- 6 files changed, 391 insertions(+), 9 deletions(-) create mode 100644 htlcswitch/hop/blind_hop.go diff --git a/htlcswitch/hop/blind_hop.go b/htlcswitch/hop/blind_hop.go new file mode 100644 index 0000000000..81a7f6839d --- /dev/null +++ b/htlcswitch/hop/blind_hop.go @@ -0,0 +1,30 @@ +package hop + +import ( + "github.com/btcsuite/btcd/btcec/v2" + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/keychain" +) + +type BlindHopProcessor struct{} + +func NewBlindHopProcessor() *BlindHopProcessor { + return &BlindHopProcessor{} +} + +func (b *BlindHopProcessor) DecryptBlindedPayload(nodeID keychain.SingleKeyECDH, + blindingPoint *btcec.PublicKey, payload []byte) ([]byte, error) { + + return sphinx.DecryptBlindedData( + nodeID, blindingPoint, payload, + ) +} + +func (b *BlindHopProcessor) NextBlindingPoint(nodeID keychain.SingleKeyECDH, + blindingPoint *btcec.PublicKey) (*btcec.PublicKey, error) { + + // NOTE(8/8/22): We have pulled the sphinx dependency + // out of 'htlcswitch' only to depend on it here? + // To what end? + return sphinx.NextEphemeral(nodeID, blindingPoint) +} diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index 750676b1a2..9bb9f0e645 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -243,6 +243,10 @@ type DecodeHopIteratorRequest struct { OnionReader io.Reader RHash []byte IncomingCltv uint32 + + // An ephemeral public key which is used to decrypt the onion + // when forwarding in the blinded portion of a route. + BlindingPoint *btcec.PublicKey } // DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion @@ -299,7 +303,7 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte, } err = tx.ProcessOnionPacket( - seqNum, onionPkt, req.RHash, req.IncomingCltv, nil, + seqNum, onionPkt, req.RHash, req.IncomingCltv, req.BlindingPoint, ) switch err { case nil: diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 784177323b..a56c3fb1b6 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -167,6 +167,98 @@ type BlindHopPayload struct { // AllowedFeatures *record.AllowedFeatures } +const ( + // feeRateParts is the total number of parts used to express fee rates. + feeRateParts = 1000000 +) + +// ForwardingInfo returns the basic parameters required for HTLC forwarding +// in a blinded route, e.g. amount, cltv, and next hop. +func (b *BlindHopPayload) ForwardingInfo(incomingAmt lnwire.MilliSatoshi, + incomingTimelock uint32) ForwardingInfo { + + return ForwardingInfo{ + Network: BitcoinNetwork, + NextHop: b.NextHop, // Assumes the next channel ID is given. + // TODO(11/15/22): Do not assume that the next hop will be + // specified by short_channel_id. Add support for forwarding to + // a node_id (convert node_id --> scid) + // NextHop: computeNextHop(b), + AmountToForward: computeAmountToForward(incomingAmt, b.PaymentRelay), + OutgoingCTLV: computeOutgoingCltv(incomingTimelock, b.PaymentRelay), + } +} + +// computeAmountToForward computes the amount to forward for HTLCs +// processed as part of a blinded route. +// QUESTION(10/17/22): Should there be something apart from the function +// signature to indicate these are to be used for blinded hops? +func computeAmountToForward(incomingAmt lnwire.MilliSatoshi, + paymentRelay *record.PaymentRelay) lnwire.MilliSatoshi { + + var base, rate uint64 + if paymentRelay != nil { + base = uint64(paymentRelay.BaseFee) + rate = uint64(paymentRelay.FeeRate) //* 1000 // ppm + } + + amt := uint64(incomingAmt) + + // NOTE(10/17/22): In normal routing, we are given the amount to + // forward directly in the onion payload. We need only verify that + // the difference between incoming and outgoing amounts satisfies + // our fee requirement. + // + // When forwarding a payment, the fee we take is calculated, not on + // the incoming amount, but rather on the amount we forward. We charge + // fees based on our own liquidity we are forwarding downstream. + // With route blinding, we are NOT given the amount to forward. + // This unintuitive looking formula comes from the fact that without + // the amount to forward, we cannot compute the fees taken directly. + // + // The amount to be forwarded can be computed as follows: + // + // amt_to_forward = incoming_amount - total_fees + // total_fees = base_fee + amt_to_forward*(fee_rate/1000000) + // + // After substitution and some massaging you will get: + // + // amt_to_forward = (incoming_amount - base_fee) / + // ( 1 + fee_rate / 1000000 ) + // + // From there we use a ceiling formula for integer division so that + // we always round up, otherwise the sender may receive slightly + // less than intended: + // + // ceil(a/b) = (a + b - 1)/(b) + // + fwdAmount := ((amt-base)*feeRateParts + feeRateParts + rate - 1) / + (feeRateParts + rate) + // fwdAmount := divideCeil( + // feeRateParts*(amt-base), + // feeRateParts+rate, + // ) + + return lnwire.MilliSatoshi(fwdAmount) +} + +// // divideCeil divides dividend by factor and rounds the result up. +// func divideCeil(dividend, factor uint64) uint64 { +// return (dividend + factor - 1) / factor +// } + +// computeOutgoingCltv computes the outgoing timelock for HTLCs +// processed as part of a blinded route. +func computeOutgoingCltv(incomingTimelock uint32, + paymentRelay *record.PaymentRelay) uint32 { + + if paymentRelay == nil { + return incomingTimelock + } + + return incomingTimelock - uint32(paymentRelay.CltvExpiryDelta) +} + // NewBlindHopPayloadFromReader parses a route blinding payload from // the passed io.Reader. The reader should correspond to the bytes // encapsulated in the encrypted route blinding payload after they diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go index 024385fd4f..9b61c5de20 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -871,3 +871,7 @@ func testDecodeBlindHopPayloadValidation(t *testing.T, } } + +// TODO(10/16/22): Test compute to amount forward requires it be exported +// as our test code lives in a different package. +// func TestComputeAmountToForward(t *testing.T) {} diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index 8e2a1c43b3..5c7aea4979 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -1,9 +1,11 @@ package htlcswitch import ( + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" @@ -69,6 +71,25 @@ type dustHandler interface { getDustClosure() dustClosure } +// BlindHopProcessor provides the functionality necessary for +// the channel link to process hops as part of a blinded route. +type BlindHopProcessor interface { + // // DeriveBlindingFactor... + // // Note(8/8/22): This is not needed with Elle's current implementation. + // DeriveBlindingFactor(*btcec.PrivateKey, *btcec.PublicKey) ( + // *btcec.PrivateKey, error) + + // DecryptBlindedPayload decrypts the route blinding payload. + DecryptBlindedPayload(nodeID keychain.SingleKeyECDH, blindingPoint *btcec.PublicKey, + payload []byte) ([]byte, error) + + // NextBlindingPoint computes the ephemeral blinding point + // that the next hop (our downstream peer) in a blinded route + // will need in order to decrypt the onion. + NextBlindingPoint(keychain.SingleKeyECDH, *btcec.PublicKey) ( + *btcec.PublicKey, error) +} + // scidAliasHandler is an interface that the ChannelLink implements so it can // properly handle option_scid_alias channels. type scidAliasHandler interface { diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 90e8602154..f8cddd2056 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -9,11 +9,14 @@ import ( "sync/atomic" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog" "github.com/davecgh/go-spew/spew" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/contractcourt" @@ -153,6 +156,10 @@ type ChannelLinkConfig struct { DecodeHopIterators func([]byte, []hop.DecodeHopIteratorRequest) ( []hop.DecodeHopIteratorResponse, error) + // BlindHopProcessor provides the functionality necessary for + // the channel link to process hops as part of a blinded route. + BlindHopProcessor + // ExtractErrorEncrypter function is responsible for decoding HTLC // Sphinx onion blob, and creating onion failure obfuscator. ExtractErrorEncrypter hop.ErrorEncrypterExtracter @@ -1819,6 +1826,10 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { failure = &lnwire.FailInvalidOnionKey{ OnionSHA256: msg.ShaOnionBlob, } + + case lnwire.CodeInvalidOnionBlinding: + failure = lnwire.NewInvalidOnionBlinding(msg.ShaOnionBlob[:]) + default: l.log.Warnf("unexpected failure code received in "+ "UpdateFailMailformedHTLC: %v", msg.FailureCode) @@ -2906,6 +2917,12 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, OnionReader: onionReader, RHash: pd.RHash[:], IncomingCltv: pd.Timeout, + // If this HTLC has an ephemeral blinding point, + // it will be needed to decrypt the onion as the + // onion encryptor is expected to have encrypted + // the onion to a blinded form of our node ID, + // rather than to our persistent node ID. + BlindingPoint: pd.BlindingPoint, } decodeReqs = append(decodeReqs, req) @@ -3017,8 +3034,59 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, fwdInfo := pld.ForwardingInfo() - switch fwdInfo.NextHop { - case hop.Exit: + // NOTE(11/22/20): This is payment processing hot path. + // Avoid the allocation unless strictly necessary. + var nextBlindingPoint *btcec.PublicKey + var blindPayload *hop.BlindHopPayload + + // Process the route blinding payload if present. + isBlindedHop := pld.RouteBlindingEncryptedData != nil + if isBlindedHop { + l.log.Debugf("received htlc(%x) for blinded route with "+ + "route_blinding_data=%v", + pd.RHash, pld.RouteBlindingEncryptedData) + + // Extract forwarding information for the blind hop and + // compute a blinding point for the next node in the route. + blindPayload, nextBlindingPoint, err = + l.processBlindHop(pd, pld, chanIterator.IsFinalHop()) + if err != nil { + // Do we need to fail the link? + failure := lnwire.NewInvalidOnionBlinding( + pd.ShaOnionBlob[:], + ) + + // TODO(8/14/22): + // - Delay sending error if we are the + // introduction node. + // - Go over error handling with a microscope as + // apparently it is fraught with danger. + l.sendMalformedHTLCError(pd.HtlcIndex, failure.Code(), + onionBlob[:], pd.SourceRef) + + l.log.Errorf("unable to process blind hop: %v", err) + + continue + } + + // Overwrite the forwarding info from the top level + // onion payload with the information found in the + // route blinding payload. + // NOTE(11/15/22): This is compact but does incur + // the computation for the final hop even when it + // will not be required. + fwdInfo = blindPayload.ForwardingInfo(pd.Amount, pd.Timeout) + + } + + // NOTE(9/15/22): We return to our roots and consider a 32 0x00 + // byte onion HMAC as the signal that we are the final hop. + // We cannot rely on the presence of short_channel_id in the top + // level onion TLV payload, as it will not be set for hops in a + // blinded route. We will need this knowledge for BOLT-04 + // validation. + switch chanIterator.IsFinalHop() { + case true: err := l.processExitHop( pd, obfuscator, fwdInfo, heightNow, pld, ) @@ -3056,9 +3124,10 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // Otherwise, it was already processed, we can // can collect it and continue. addMsg := &lnwire.UpdateAddHTLC{ - Expiry: fwdInfo.OutgoingCTLV, - Amount: fwdInfo.AmountToForward, - PaymentHash: pd.RHash, + Expiry: fwdInfo.OutgoingCTLV, + Amount: fwdInfo.AmountToForward, + PaymentHash: pd.RHash, + BlindingPoint: nextBlindingPoint, } // Finally, we'll encode the onion packet for @@ -3084,6 +3153,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, outgoingTimeout: fwdInfo.OutgoingCTLV, customRecords: pld.CustomRecords(), } + switchPackets = append( switchPackets, updatePacket, ) @@ -3098,9 +3168,10 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // create the outgoing HTLC using the parameters as // specified in the forwarding info. addMsg := &lnwire.UpdateAddHTLC{ - Expiry: fwdInfo.OutgoingCTLV, - Amount: fwdInfo.AmountToForward, - PaymentHash: pd.RHash, + Expiry: fwdInfo.OutgoingCTLV, + Amount: fwdInfo.AmountToForward, + PaymentHash: pd.RHash, + BlindingPoint: nextBlindingPoint, } // Finally, we'll encode the onion packet for the @@ -3475,6 +3546,166 @@ func ValidateRouteBlindingPaymentConstraints(p *hop.BlindHopPayload, return nil } +// processBlindHop handles an htlc for which the link is a blinded hop. +// As a processing node in the blinded portion of a route, we first decrypt +// and parse the route blinding payload, then validate that the information +// contained across the onion payload, route blinding payload, +// and UpdateAddHTLC adheres to specification outlined in BOLT-04. +// Finally, we provide the blinding point which the next routing node will need +// in order to decrypt the onion. The blinding point is to be communicated with +// the next hop in the route via TLV extension to the UpdateAddHtlc message. +func (l *channelLink) processBlindHop(pd *lnwallet.PaymentDescriptor, + payload *hop.Payload, + isFinalHop bool) (*hop.BlindHopPayload, *btcec.PublicKey, error) { + + // Validate that top level onion TLV payload adheres to the + // route blinding specification. + err := ValidateTopLevelPayloadAndAddMsg(payload, pd) + if err != nil { + return nil, nil, err + } + + // Grab the ephemeral blinding point as it's needed to + // decrypt the route blinding TLV payload. + var ephemeralBlindingPoint *btcec.PublicKey + + // Processing nodes in the blinded route receive their + // ephemeral blinding point in the TLV extension of UpdateAddHTLC! + if pd.BlindingPoint != nil { + ephemeralBlindingPoint = pd.BlindingPoint + } + + // The introduction node gets its ephemeral blinding + // point from the TLV onion payload!! + if payload.BlindingPoint != nil { + ephemeralBlindingPoint = payload.BlindingPoint + } + + // Decrypt the route blinding payload left for us + // by the builder of the blinded route. + // NOTE(8/8/22): Shared secret between routing node and blinded route + // builder is computed twice by sphinx package. Once for generating the + // routing node's blinded ID key pair needed to decrypt the onion packet + // and again for decrypting the inner route blinding payload. + // Would it be better to compute the shared secret once and pass as + // dependency to both functions? + blindedPayload := payload.RouteBlindingEncryptedData + routeBlindingPayload, err := l.cfg.DecryptBlindedPayload( + l.cfg.NodeKeyECDH, ephemeralBlindingPoint, blindedPayload, + ) + if err != nil { + // There are two possibilities in this case: + // - the blinding point is invalid: the sender or the previous node + // is buggy or malicious. NOTE(9/22/22): This is true for intro nodes. + // If the blinding point were invalid, we could not have decrypted + // the onion payload (for non-intro hops). + // - the encrypted data is invalid: the recipient is buggy or malicious + // or the sender is including bogus route blinding data. + return nil, nil, err + } + + // Parse routing information from route blinding TLV payload. + blindHopPayload, err := hop.NewBlindHopPayloadFromReader( + bytes.NewReader(routeBlindingPayload), isFinalHop, + ) + if err != nil { + return nil, nil, err + } + + // Given the route blinding payload, validate that the top level + // onion payload adheres to BOLT-O4 specification. + err = ValidateTopLevelPayloadRouteBlinding(payload, isFinalHop) + if err != nil { + return blindHopPayload, nil, err + } + + // Apply payment constraints as requested by the creator of the blinded + // route. These are additional constraints on forwarded payments above + // and beyond our usual forwarding policy. + err = ValidateRouteBlindingPaymentConstraints(blindHopPayload, pd) + if err != nil { + return blindHopPayload, nil, err + } + + // TODO(11/22/20): If we encounter an unexpected path_id, + // drop the HTLC on the floor. + if err := ValidatePathID(blindHopPayload.PathID, pd.RHash); err != nil { + return blindHopPayload, nil, err + } + + // If this is the last hop, we'll bail before computing the + // next route blinding point. + if isFinalHop { + l.log.Debug("final hop in blinded route, skipping computation of" + + "next ephemeral blinding point, amount and timelock") + + return blindHopPayload, nil, nil + } + + // Compute the ephemeral route blinding point for next node in route. + var nextBlindingPoint *secp256k1.PublicKey + + // If the route blinder provided a blinding point override, + // we will send it to the downstream peer. + if blindHopPayload.BlindingPointOverride != nil { + nextBlindingPoint = blindHopPayload.BlindingPointOverride + return blindHopPayload, nextBlindingPoint, nil + } + + // Otherwise, we'll compute the next blinding point ourselves. + nextBlindingPoint, err = l.cfg.NextBlindingPoint( + l.cfg.NodeKeyECDH, ephemeralBlindingPoint, + ) + if err != nil { + return nil, nil, err + } + + return blindHopPayload, nextBlindingPoint, nil +} + +// ValidatePathID checks that the path_id is properly constructed. +// We assume, for now, that any blinded routes we (LND) create will store +// the preimage in the path_id field of the route blinding payload for +// the final hop. In the future we might expand the information stored +// in the path_id so that we can properly handle dummy hops. +// +// TODO(11/5/22): In order to support processing dummy hops in a blinded +// route (for more privacy!), the HTLCSwitch/ChannelLink needs to support +// processing the onion packet for a given HTLC multiple times! +// +// We MUST NOT accept payment to a blinded route in which we do not process +// all dummy hops. Otherwise, probing senders, under the expectation that +// we padded our blinded route with dummy hops, could experiment with +// chopping off hops at the end of the route. If we accept the payment +// anyways, then they decrease our anonymity set as they know know a closer +// bound on the distance between recipient and introduction node. +func ValidatePathID(pathID []byte, paymentHash lnwallet.PaymentHash) error { + + // Only validate pathID if it's included by this hop. + if pathID == nil { + return nil + } + + // We'll verify that the path ID data matches what we expect. + pathIDHash := sha256.Sum256(pathID) + + log.Debugf("sha-256(path_id)=%v, payment hash=%v", + pathIDHash, paymentHash) + + // Check that the path_id is the payment preimage. + if !bytes.Equal(pathIDHash[:], paymentHash[:]) { + log.Errorf("unexpected path_id=%v for payment, expected=%v", + pathIDHash, paymentHash, + ) + + // TODO(11/15/22): Create error types for blind hop processing? + var errUnexpectedPathID error = fmt.Errorf("unexpected path_id for payment") + return errUnexpectedPathID + } + + return nil +} + // settleHTLC settles the HTLC on the channel. func (l *channelLink) settleHTLC(preimage lntypes.Preimage, pd *lnwallet.PaymentDescriptor) error { From dcfeaa776b230dffa8c77d1a1408d4ec41c61dd9 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Mon, 21 Nov 2022 20:19:18 -0500 Subject: [PATCH 13/16] htlcswitch/link: handle errors encountered during blind hop processing This is still a work in progress. Go over error handling with a microscope as apparently it is fraught with danger. --- htlcswitch/link.go | 64 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index f8cddd2056..c811643ec2 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -1634,6 +1634,30 @@ func (l *channelLink) handleDownstreamPkt(pkt *htlcPacket) { htlc.ChanID = l.ChanID() htlc.ID = pkt.incomingHTLCID + // According to the spec, when failing an incoming HTLC: + // - If `current_blinding_point` is set in the onion payload: + // - MUST send an `update_fail_htlc` error using the + // `invalid_onion_blinding` failure code with the `sha256_of_onion` + // of the onion it received, for any local or downstream errors. + // - SHOULD add a random delay before sending `update_fail_htlc`. + + // - If `blinding_point` is set in the incoming `update_add_htlc`: + // - MUST send an `update_fail_malformed_htlc` error using the + // `invalid_onion_blinding` failure code with the `sha256_of_onion` + // of the onion it received, for any local or downstream errors. + // + // NOTE(11/20/22): All downstream and local errors which do not occur + // in the incoming link (ie: switch/outgoing link) must flow through + // this path of execution by the incoming link in order to be sent + // back towards the sender. This might prove a natural catch-all point + // to convert any error to InvalidOnionBlinding or add delay if we're + // the introduction node, however we do not have access to the + // information we would need to make such a decision at the moment. + // Also, the incoming link routinely responds directly (ie: not via + // this code path) to peers for local errors. Will we need a way to + // intercept and convert these? + // pkt.SomethingWhichIndicatesIntroductionNode + // We send the HTLC message to the peer which initially created // the HTLC. l.cfg.Peer.SendMessage(false, htlc) @@ -1867,6 +1891,13 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { } case *lnwire.UpdateFailHTLC: + // NOTE(11/18/22): We just received a FAIL from our peer. + // The FAIL could be for an HTLC ADD which is part of a blinded + // route, but we have no way of knowing whether this is the case here! + // If the FAIL does correspond to an ADD which is part of a blinded + // route, then there is a chance that the failure reason risks + // leaking information about which node failed. We will need to + // protect against this somewhere. idx := msg.ID err := l.channel.ReceiveFailHTLC(idx, msg.Reason[:]) if err != nil { @@ -2977,6 +3008,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // If we're unable to process the onion blob than we // should send the malformed htlc error to payment // sender. + // NOTE(11/18/22): If this is a blind hop, then this + // error might need to be converted to InvalidOnionBlinding. l.sendMalformedHTLCError(pd.HtlcIndex, failureCode, onionBlob[:], pd.SourceRef) @@ -2994,6 +3027,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // If we're unable to process the onion blob than we // should send the malformed htlc error to payment // sender. + // NOTE(11/18/22): If this is a blind hop, then this + // error might need to be converted to InvalidOnionBlinding. l.sendMalformedHTLCError( pd.HtlcIndex, failureCode, onionBlob[:], pd.SourceRef, ) @@ -3022,6 +3057,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // for TLV payloads that also supports injecting invalid // payloads. Deferring this non-trival effort till a // later date + // NOTE(11/18/22): If this is a blind hop, then this + // error might need to be converted to InvalidOnionBlinding. failure := lnwire.NewInvalidOnionPayload(failedType, 0) l.sendHTLCError( pd, NewLinkError(failure), obfuscator, false, @@ -3168,9 +3205,20 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // create the outgoing HTLC using the parameters as // specified in the forwarding info. addMsg := &lnwire.UpdateAddHTLC{ - Expiry: fwdInfo.OutgoingCTLV, - Amount: fwdInfo.AmountToForward, - PaymentHash: pd.RHash, + Expiry: fwdInfo.OutgoingCTLV, + Amount: fwdInfo.AmountToForward, + PaymentHash: pd.RHash, + // NOTE(11/18/22): Whether our node is the introduction + // node or an intermediate node in the blinded route, + // we set a (route) blinding point on the UpdateAddHTLC + // for the next hop in the incoming link. This means, + // as is, the outgoing link will not be able to determine + // whether we were the introduction node or intermediate + // node when processing errors from downstream in a blinded + // route. This doesn't matter if we don't handle errors there. + // According to spec, introduction vs. intermediate nodes + // are supposed to handle errors a bit differently. Do we + // need to persist whether we are intro/intermediate? BlindingPoint: nextBlindingPoint, } @@ -3191,6 +3239,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, true, hop.Source, cb, ) + // NOTE(11/18/22): If this is a blind hop, then this + // error might need to be converted to InvalidOnionBlinding. l.sendHTLCError( pd, NewLinkError(failure), obfuscator, false, ) @@ -3282,6 +3332,10 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, failure := NewLinkError( lnwire.NewFinalIncorrectHtlcAmount(pd.Amount), ) + // NOTE(11/18/22): If this is a blind hop, then this + // error might need to be converted to InvalidOnionBlinding. + // Unless we received the (route) blinding point in the + // onion payload, in which case we can send errors like normal! l.sendHTLCError(pd, failure, obfuscator, true) return nil @@ -3297,6 +3351,10 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, failure := NewLinkError( lnwire.NewFinalIncorrectCltvExpiry(pd.Timeout), ) + // NOTE(11/18/22): If this is a blind hop, then this + // error might need to be converted to InvalidOnionBlinding. + // Unless we received the (route) blinding point in the + // onion payload, in which case we can send errors like normal! l.sendHTLCError(pd, failure, obfuscator, true) return nil From 9e2a8632116417427b093ac67fd7ea045cbd3104 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Tue, 18 Oct 2022 17:22:27 -0500 Subject: [PATCH 14/16] multi: make blind hop processing configurable allow a user to specify whether they would like to enable the HTLCSwitch to process HTLCs on a blind route --- feature/manager.go | 8 +++++++- htlcswitch/link.go | 21 +++++++++++++++++++++ htlcswitch/switch.go | 13 +++++++++++++ lncfg/protocol.go | 9 +++++++++ lncfg/protocol_rpctest.go | 9 +++++++++ lntest/itest/lnd_mpp_test.go | 18 +++++++++++++++--- lnwire/features.go | 10 ++++++++++ sample-lnd.conf | 3 +++ server.go | 4 ++++ 9 files changed, 91 insertions(+), 4 deletions(-) diff --git a/feature/manager.go b/feature/manager.go index 26a3d4a31a..461e1592f7 100644 --- a/feature/manager.go +++ b/feature/manager.go @@ -41,6 +41,9 @@ type Config struct { // keep option-scid-alias support. NoZeroConf bool + // RouteBlinding sets bit to signal support for route blinding. + RouteBlinding bool + // NoAnySegwit unsets any bits that signal support for using other // segwit witness versions for co-op closes. NoAnySegwit bool @@ -89,7 +92,7 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { } } - // Now, remove any features as directed by the config. + // Now, add or remove any features as directed by the config. for set, raw := range fsets { if cfg.NoTLVOnion { raw.Unset(lnwire.TLVOnionPayloadOptional) @@ -146,6 +149,9 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) { raw.Unset(lnwire.ZeroConfOptional) raw.Unset(lnwire.ZeroConfRequired) } + if cfg.RouteBlinding { + raw.Set(lnwire.RouteBlindingOptional) + } if cfg.NoAnySegwit { raw.Unset(lnwire.ShutdownAnySegwitOptional) raw.Unset(lnwire.ShutdownAnySegwitRequired) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index c811643ec2..a55687deea 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -160,6 +160,10 @@ type ChannelLinkConfig struct { // the channel link to process hops as part of a blinded route. BlindHopProcessor + // RouteBlindingEnabled indicates whether the link should process blind hops. + // NOTE(10/1/22): Use this instead of function. + RouteBlindingEnabled bool + // ExtractErrorEncrypter function is responsible for decoding HTLC // Sphinx onion blob, and creating onion failure obfuscator. ExtractErrorEncrypter hop.ErrorEncrypterExtracter @@ -3098,6 +3102,9 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // introduction node. // - Go over error handling with a microscope as // apparently it is fraught with danger. + // - Should whether we currently support + // processing blind hops affect the error we + // return to senders? l.sendMalformedHTLCError(pd.HtlcIndex, failure.Code(), onionBlob[:], pd.SourceRef) @@ -3616,6 +3623,20 @@ func (l *channelLink) processBlindHop(pd *lnwallet.PaymentDescriptor, payload *hop.Payload, isFinalHop bool) (*hop.BlindHopPayload, *btcec.PublicKey, error) { + // NOTE(9/30/22): Do not assume other network participant's will + // respect our feature vector. Enforce that it be respected. + // Only process blinded hops if we support doing so. Right now + // the link will reference a nil pointer if not supported. + // Ensure that our node has signaled support for route blinding + // before going any further. If not, return some generic/specific error? + // if l.cfg.BlindHopProcessor == nil { + if !l.cfg.RouteBlindingEnabled { + l.log.Debug("unable to process blind hop, we have not " + + "signaled support for route blinding") + + return nil, nil, fmt.Errorf("unable to process blind hop") + } + // Validate that top level onion TLV payload adheres to the // route blinding specification. err := ValidateTopLevelPayloadAndAddMsg(payload, pd) diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index 8b2120d3e8..78f26fc5f3 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -141,6 +141,10 @@ type Config struct { // persistent circuit map. DB kvdb.Backend + // RouteBlindingEnabled signals that our switch should configure its + // links to support the route blinding protocol. + RouteBlindingEnabled bool + // FetchAllOpenChannels is a function that fetches all currently open // channels from the channel database. FetchAllOpenChannels func() ([]*channeldb.OpenChannel, error) @@ -2261,6 +2265,15 @@ func (s *Switch) Stop() error { func (s *Switch) CreateAndAddLink(linkCfg ChannelLinkConfig, lnChan *lnwallet.LightningChannel) error { + // Passthrough any Link level configuration from our Switch. + if s.cfg.RouteBlindingEnabled { + log.Debug("configuring switch to support route blinding, " + + "adding blind hop processor to link") + + linkCfg.RouteBlindingEnabled = true + linkCfg.BlindHopProcessor = hop.NewBlindHopProcessor() + } + link := NewChannelLink(linkCfg, lnChan) return s.AddLink(link) } diff --git a/lncfg/protocol.go b/lncfg/protocol.go index 238e8c485a..c841b94bf1 100644 --- a/lncfg/protocol.go +++ b/lncfg/protocol.go @@ -41,6 +41,10 @@ type ProtocolOptions struct { // feature bit. OptionZeroConf bool `long:"zero-conf" description:"enable support for zero-conf channels, must have option-scid-alias set also"` + // OptionRouteBlinding should be set if we want to signal support + // for route blinding. + OptionRouteBlinding bool `long:"route-blinding" description:"enable support for route blinding"` + // NoOptionAnySegwit should be set to true if we don't want to use any // Taproot (and beyond) addresses for co-op closing. NoOptionAnySegwit bool `long:"no-any-segwit" description:"disallow using any segiwt witness version as a co-op close address"` @@ -74,6 +78,11 @@ func (l *ProtocolOptions) ZeroConf() bool { return l.OptionZeroConf } +// RouteBlinding returns true if we have enabled the route blinding feature bit. +func (l *ProtocolOptions) RouteBlinding() bool { + return l.OptionRouteBlinding +} + // NoAnySegwit returns true if we don't signal that we understand other newer // segwit witness versions for co-op close addresses. func (l *ProtocolOptions) NoAnySegwit() bool { diff --git a/lncfg/protocol_rpctest.go b/lncfg/protocol_rpctest.go index 7fca60f10f..b047b5f19c 100644 --- a/lncfg/protocol_rpctest.go +++ b/lncfg/protocol_rpctest.go @@ -42,6 +42,10 @@ type ProtocolOptions struct { // feature bit. OptionZeroConf bool `long:"zero-conf" description:"enable support for zero-conf channels, must have option-scid-alias set also"` + // OptionRouteBlinding should be set if we want to signal support + // for route blinding. + OptionRouteBlinding bool `long:"route-blinding" description:"enable support for route blinding"` + // NoOptionAnySegwit should be set to true if we don't want to use any // Taproot (and beyond) addresses for co-op closing. NoOptionAnySegwit bool `long:"no-any-segwit" description:"disallow using any segiwt witness version as a co-op close address"` @@ -75,6 +79,11 @@ func (l *ProtocolOptions) ZeroConf() bool { return l.OptionZeroConf } +// RouteBlinding returns true if we have enabled the route blinding feature bit. +func (l *ProtocolOptions) RouteBlinding() bool { + return l.OptionRouteBlinding +} + // NoAnySegwit returns true if we don't signal that we understand other newer // segwit witness versions for co-op close addresses. func (l *ProtocolOptions) NoAnySegwit() bool { diff --git a/lntest/itest/lnd_mpp_test.go b/lntest/itest/lnd_mpp_test.go index 7288f42a9e..17dc656a38 100644 --- a/lntest/itest/lnd_mpp_test.go +++ b/lntest/itest/lnd_mpp_test.go @@ -256,12 +256,24 @@ func newMppTestContext(t *harnessTest, net *lntest.NetworkHarness) *mppTestContext { alice := net.NewNode(t.t, "alice", nil) - bob := net.NewNode(t.t, "bob", []string{"--accept-amp"}) + + // NOTE(8/7/22): For simplicity of blinded route testing setup each node in route + // to charge 10 sat (10,000 msat) base forwarding fee and zero proportional fee. + bob := net.NewNode(t.t, "bob", []string{ + "--accept-amp", "--bitcoin.basefee=10000", + "--bitcoin.feerate=0", "--protocol.route-blinding", + }) // Create a five-node context consisting of Alice, Bob and three new // nodes. - carol := net.NewNode(t.t, "carol", nil) - dave := net.NewNode(t.t, "dave", nil) + carol := net.NewNode(t.t, "carol", []string{ + "--bitcoin.basefee=10000", + "--bitcoin.feerate=0", "--protocol.route-blinding", + }) + dave := net.NewNode(t.t, "dave", []string{ + "--bitcoin.basefee=10000", + "--bitcoin.feerate=0", "--protocol.route-blinding", + }) eve := net.NewNode(t.t, "eve", nil) // Connect nodes to ensure propagation of channels. diff --git a/lnwire/features.go b/lnwire/features.go index c386fd37be..fb23d94759 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -129,6 +129,14 @@ const ( // transactions, which also imply anchor commitments. AnchorsZeroFeeHtlcTxOptional FeatureBit = 23 + // RouteBlindingRequired is a required feature bit that signals that + // the node supports sending to/receiving from blinded routes. + RouteBlindingRequired FeatureBit = 24 + + // RouteBlindingOptional is an optional feature bit that signals that + // the node supports sending to/receiving from blinded routes. + RouteBlindingOptional FeatureBit = 25 + // ShutdownAnySegwitRequired is an required feature bit that signals // that the sender is able to properly handle/parse segwit witness // programs up to version 16. This enables utilization of Taproot @@ -268,6 +276,8 @@ var Features = map[FeatureBit]string{ AMPOptional: "amp", PaymentMetadataOptional: "payment-metadata", PaymentMetadataRequired: "payment-metadata", + RouteBlindingRequired: "route-blinding", + RouteBlindingOptional: "route-blinding", ExplicitChannelTypeOptional: "explicit-commitment-type", ExplicitChannelTypeRequired: "explicit-commitment-type", KeysendOptional: "keysend", diff --git a/sample-lnd.conf b/sample-lnd.conf index a06342ece1..7227009f53 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1192,6 +1192,9 @@ litecoin.node=ltcd ; option-scid-alias flag to also be set. ; protocol.zero-conf=true +; Set to enable support for route blinding. +; protocol.route-blinding=true + ; Set to disable support for using P2TR addresses (and beyond) for co-op ; closing. ; protocol.no-any-segwit diff --git a/server.go b/server.go index 935af78c41..57e71b05a4 100644 --- a/server.go +++ b/server.go @@ -540,6 +540,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, NoKeysend: !cfg.AcceptKeySend, NoOptionScidAlias: !cfg.ProtocolOptions.ScidAlias(), NoZeroConf: !cfg.ProtocolOptions.ZeroConf(), + RouteBlinding: cfg.ProtocolOptions.RouteBlinding(), NoAnySegwit: cfg.ProtocolOptions.NoAnySegwit(), }) if err != nil { @@ -665,6 +666,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, DustThreshold: thresholdMSats, SignAliasUpdate: s.signAliasUpdate, IsAlias: aliasmgr.IsAlias, + // If route blinding is enabled, we'll configure the htlcswitch + // so the links we create are ready to process blinded hops. + RouteBlindingEnabled: cfg.ProtocolOptions.RouteBlinding(), }, uint32(currentHeight)) if err != nil { return nil, err From 61cd629c516cc0846784251b41b4f1ea75eb51e0 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Sun, 6 Nov 2022 16:54:43 -0500 Subject: [PATCH 15/16] htlcswitch/mock: allow `mockHopIterator` to handle both normal and blind hops We expand the capability of the mockHopIterator such that it is able to handle both normal and blind hops. The destinction is made via a check for sentinel value which delineates the byte boundary between serialized hops. The implemenation is functional yet somewhat propietary in the sense that it is specific to handling blind hops and not as general as it could be. This lack of generality leaves the door open to eventual inclusion of the full blown TLV serialization scheme into the mock iterator. Without this or something like TLV we do not have a way to know whether we should or even how to deserialize a variable length field like the route blinding payload. --- htlcswitch/hop/payload.go | 18 +++ htlcswitch/mock.go | 266 +++++++++++++++++++++++++++++++++++--- 2 files changed, 269 insertions(+), 15 deletions(-) diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index a56c3fb1b6..40db517e1c 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -152,6 +152,24 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload { } } +// NOTE(10/26/22): This function is currently only used to get around +// the fact that customRecords is unexported and is required to be set. +// I don't think a function like this would have much utility otherwise. +// Given that we use TLV en/decoding I am a bit unsure how this function +// will behave/get access to the information it would need. +// The data included in a TLV payload is highly variable. That is why it makes +// sense to define a struct and then TLV encode each of the structs types +// that the user sets. +func NewTLVPayload() *Payload { + + // Set the unexported customRecords field so that we can carry + // on with our Link testing. + return &Payload{ + FwdInfo: ForwardingInfo{}, + customRecords: make(record.CustomSet), + } +} + // BlindHopPayload encapsulates all the route blinding information // which will be parsed by nodes in the blinded portion of a route. type BlindHopPayload struct { diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index c94c893ff2..66e37f266e 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lntypes" @@ -328,12 +329,30 @@ func newMockHopIterator(hops ...*hop.Payload) hop.Iterator { return &mockHopIterator{hops: hops} } +// HopPayload returns the set of fields that detail exactly _how_ this hop +// should forward the HTLC to the next hop. For normal (ie: non-blind) +// hops, the information encoded within the returned ForwardingInfo is to +// be used by each hop to authenticate the information given to it by the +// prior hop. For blind hops, callers will find the necessary forwarding +// information one layer deeper, inside the route blinding TLV payload. +// +// Every time this method is called we peel off a layer of the onion +// and our hop iterator contains one less hop! func (r *mockHopIterator) HopPayload() (*hop.Payload, error) { h := r.hops[0] r.hops = r.hops[1:] return h, nil } +// IsFinalHop indicates whether a hop is the final hop in a payment route. +// When the last hop parses its TLV payload via call to HopPayload(), +// it will leave us with an empty hop iterator. +// +// NOTE: As this is a mock which does not use a real sphinx implementation +// to signal the final hop via all-zero onion HMAC, we are relying on this +// method being called AFTER HopPayload(). If this method is called BEFORE +// parsing the TLV payload then it will NOT correctly report that we are +// the final hop! func (r *mockHopIterator) IsFinalHop() bool { return len(r.hops) == 0 @@ -350,6 +369,8 @@ func (r *mockHopIterator) ExtractErrorEncrypter( return extracter(nil) } +// NOTE: This function name implies it encodes a single hop, +// but in actuality it encodes all hops in the route? func (r *mockHopIterator) EncodeNextHop(w io.Writer) error { var hopLength [4]byte binary.BigEndian.PutUint32(hopLength[:], uint32(len(r.hops))) @@ -359,8 +380,7 @@ func (r *mockHopIterator) EncodeNextHop(w io.Writer) error { } for _, hop := range r.hops { - fwdInfo := hop.ForwardingInfo() - if err := encodeFwdInfo(w, &fwdInfo); err != nil { + if err := encodeHopPayload(w, hop); err != nil { return err } } @@ -368,6 +388,30 @@ func (r *mockHopIterator) EncodeNextHop(w io.Writer) error { return nil } +func encodeHopPayload(w io.Writer, hop *hop.Payload) error { + fwdInfo := hop.ForwardingInfo() + if err := encodeFwdInfo(w, &fwdInfo); err != nil { + return err + } + + // If this hop has a route blinding payload, + // we'll serialize that as well. + if hop.RouteBlindingEncryptedData != nil { + err := encodeBlindHop(w, hop) + if err != nil { + return err + } + } + + // Add a (few) sentinel byte(s) in order to mark the end + // of the serialization for each hop. + if err := encodeHopBoundaryMarker(w); err != nil { + return err + } + + return nil +} + func encodeFwdInfo(w io.Writer, f *hop.ForwardingInfo) error { if _, err := w.Write([]byte{byte(f.Network)}); err != nil { return err @@ -388,6 +432,60 @@ func encodeFwdInfo(w io.Writer, f *hop.ForwardingInfo) error { return nil } +func encodeBlindHop(w io.Writer, p *hop.Payload) error { + + // NOTE(10/25/22): We recreate the "LV" in TLV here! + // We write the length of the route blinding payload so + // that the variable length payload can be properly decoded. + var blindPayloadLength [4]byte + binary.BigEndian.PutUint32(blindPayloadLength[:], uint32(len(p.RouteBlindingEncryptedData))) + + _, err := w.Write(blindPayloadLength[:]) + if err != nil { + return err + } + + if err := binary.Write(w, binary.BigEndian, p.RouteBlindingEncryptedData); err != nil { + return err + } + + if p.BlindingPoint != nil { + var fieldLength [4]byte + + pubKeyBytes := p.BlindingPoint.SerializeCompressed() + binary.BigEndian.PutUint32(fieldLength[:], uint32(len(pubKeyBytes))) + _, err = w.Write(fieldLength[:]) + if err != nil { + return err + } + + if err := binary.Write(w, binary.BigEndian, pubKeyBytes); err != nil { + return err + } + } + + return nil +} + +// sentinel is used to mark the boundary between serialized hops +// in the onion blob in the absense of TLV. +// +// TODO(11/5/22): add TLV to mockHopIterator? +var sentinel = [4]byte{0xff, 0xff, 0xff, 0xff} + +// encodeHopBoundaryMarker writes our sentinel value which delineates +// the boundary between the hop currently being encoded and any subsequent +// hops yet to be serialized. This allows us to handle variable length +// payloads which is necessary to distinguish between normal and blind +// hops (ie: those with a route blinding payload) during deserialization/decoding. +func encodeHopBoundaryMarker(w io.Writer) error { + if _, err := w.Write(sentinel[:]); err != nil { + return err + } + + return nil +} + var _ hop.Iterator = (*mockHopIterator)(nil) // mockObfuscator mock implementation of the failure obfuscator which only @@ -482,7 +580,7 @@ func newMockIteratorDecoder() *mockIteratorDecoder { } func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, rHash []byte, - cltv uint32) (hop.Iterator, lnwire.FailCode) { + cltv uint32, blindingPoint *btcec.PublicKey) (hop.Iterator, lnwire.FailCode) { var b [4]byte _, err := r.Read(b[:]) @@ -493,25 +591,22 @@ func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, rHash []byte, hops := make([]*hop.Payload, hopLength) for i := uint32(0); i < hopLength; i++ { - var f hop.ForwardingInfo - if err := decodeFwdInfo(r, &f); err != nil { + p := hop.NewTLVPayload() + if err := decodeHopPayload(r, p); err != nil { return nil, lnwire.CodeTemporaryChannelFailure } - var nextHopBytes [8]byte - binary.BigEndian.PutUint64(nextHopBytes[:], f.NextHop.ToUint64()) - - hops[i] = hop.NewLegacyPayload(&sphinx.HopData{ - Realm: [1]byte{}, // hop.BitcoinNetwork - NextAddress: nextHopBytes, - ForwardAmount: uint64(f.AmountToForward), - OutgoingCltv: f.OutgoingCTLV, - }) + hops[i] = p } return newMockHopIterator(hops...), lnwire.CodeNone } +// NOTE(10/22/22): DecodeHopIteratorRequest's will have a non-nil ephemeral +// blinding point for blind hops. In a real implementation this will be used by +// the underlying Sphinx library to decrypt the onion. For testing, it can +// probably be ignored as we just pass the public key through to the Sphinx +// implementation, but we are not dealing with encrypted data for Link testing. func (p *mockIteratorDecoder) DecodeHopIterators(id []byte, reqs []hop.DecodeHopIteratorRequest) ( []hop.DecodeHopIteratorResponse, error) { @@ -530,7 +625,7 @@ func (p *mockIteratorDecoder) DecodeHopIterators(id []byte, resps := make([]hop.DecodeHopIteratorResponse, 0, batchSize) for _, req := range reqs { iterator, failcode := p.DecodeHopIterator( - req.OnionReader, req.RHash, req.IncomingCltv, + req.OnionReader, req.RHash, req.IncomingCltv, req.BlindingPoint, ) if p.decodeFail { @@ -551,6 +646,18 @@ func (p *mockIteratorDecoder) DecodeHopIterators(id []byte, return resps, nil } +func decodeHopPayload(r io.Reader, p *hop.Payload) error { + if err := decodeFwdInfo(r, &p.FwdInfo); err != nil { + return err + } + + if err := decodeBlindHop(r, p); err != nil { + return err + } + + return nil +} + func decodeFwdInfo(r io.Reader, f *hop.ForwardingInfo) error { var net [1]byte if _, err := r.Read(net[:]); err != nil { @@ -573,6 +680,135 @@ func decodeFwdInfo(r io.Reader, f *hop.ForwardingInfo) error { return nil } +func decodeBlindHop(r io.Reader, p *hop.Payload) error { + + // NOTE(10/26/22): If we read these 4 bytes to determine whether we + // should parse the route blinding payload and this is not a blind hop, + // then we are eating 4 bytes that ought to have been decoded/interpreted + // differently. This leads to mistakenly decoded/parsed payloads. + var b [4]byte + _, err := r.Read(b[:]) + if err != nil { + return err + } + + // Check for hop boundary sentinel. If we are at a hop boundary, + // then we should bail early without reading any more bytes. + // If this is not the hop boundary, then we should interpret the bytes + // just read as the length of the route blinding payload. + if ok := isHopBoundary(b[:]); ok { + return nil + } + + // This hop as a route blinding payload, so we'll decode that now. + payloadLength := binary.BigEndian.Uint32(b[:]) + buf := make([]byte, payloadLength) + n, err := io.ReadFull(r, buf) + if err != nil { + return err + } + + // Only set the route blinding payload if it exists. + // Otherwise, leave the slice nil so we do not incorrectly + // believe the hop to be blind. + if n != 0 { + p.RouteBlindingEncryptedData = buf + } + + // Similar procedure for blinding point. + _, err = r.Read(b[:]) + if err != nil { + return err + } + + // If this is not the hop boundary, then we should interpret + // the bytes just read as the length of the next field + // (I see the need for something like TLV). + if ok := isHopBoundary(b[:]); ok { + return nil + } + + fieldLength := binary.BigEndian.Uint32(b[:]) + pubKeyBytes := make([]byte, fieldLength) + n, err = io.ReadFull(r, pubKeyBytes) + if err != nil { + return err + } + + p.BlindingPoint, _ = btcec.ParsePubKey(pubKeyBytes) + + // Don't forget to trim off the sentinel, so that any hops + // after this one are parsed correctly. + return trimSentinel(r) + +} + +func isHopBoundary(b []byte) bool { + return bytes.Equal(sentinel[:], b) +} + +func trimSentinel(r io.Reader) error { + var b [4]byte + _, err := r.Read(b[:]) + + return err +} + +// A simplified implementation of the BlindHopProcessor interface. +type mockBlindHopProcessor struct{} + +// For the sake of testing, we assume that the payload is already decrypted. +// From the perspective of the link, we expect this function implementation +// (sphinx, test, or otherwise) to deliver us a proper serialized route blinding +// payload, which we can then parse into a BlindHopPayload{}. +func (b *mockBlindHopProcessor) DecryptBlindedPayload(nodeID keychain.SingleKeyECDH, + blindingPoint *btcec.PublicKey, payload []byte) ([]byte, error) { + + return payload, nil +} + +// For simplicity's sake we just pass back the same blinding point. +func (b *mockBlindHopProcessor) NextBlindingPoint(sessionKey keychain.SingleKeyECDH, + blindingPoint *btcec.PublicKey) (*btcec.PublicKey, error) { + + return blindingPoint, nil +} + +type brokenBlindHopProcessor struct { + // errOnDecrypt is a flag which informs the broken blind hop processor + // whether it should fail during an attempt to decrypt a route blinding + // payload or fail while computing the next blinding point. + // This provides control over how the blind hop processor will fail, + // allowing us to observe how the link handles either scenario. + errOnDecrypt bool +} + +func (b *brokenBlindHopProcessor) DecryptBlindedPayload( + nodeID keychain.SingleKeyECDH, blindingPoint *btcec.PublicKey, + payload []byte) ([]byte, error) { + + // Simulate an error during decryption of the route blinding TLV payload. + if b.errOnDecrypt { + return nil, errors.New("unable to decrypt route blinding TLV payload") + } + + // Otherwise, return successfully and allow failure to occur later. + return payload, nil +} + +func (b *brokenBlindHopProcessor) NextBlindingPoint( + sessionKey keychain.SingleKeyECDH, blindingPoint *btcec.PublicKey) ( + *btcec.PublicKey, error) { + + // Simulate an error during computation of the epemeral blinding point + // for the next hop in a blinded route. + if !b.errOnDecrypt { + return nil, errors.New("unable to compute next ephemeral blinding point") + } + + return blindingPoint, nil +} + // messageInterceptor is function that handles the incoming peer messages and // may decide should the peer skip the message or not. type messageInterceptor func(m lnwire.Message) (bool, error) From f4b505ff1440145e41c644f28ad193ccc40db987 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Wed, 16 Nov 2022 21:51:42 -0500 Subject: [PATCH 16/16] htlcswitch: add ChannelLink tests for blind hop processing Add several test cases which verify that the link supports route blinding. More specifically we verify that the ChannelLink can process an onion packet "sent" to a blinded node ID public key rather than the usual persistent node ID public key, unblind the next hop in the route and forward the HTLC. We also verify that a ChannelLink processing a blind hop returns the opaque InvalidOnionBlinding error under the expected scenarios. This provides assurance that would be probers will always receive the same (hopefully) opaque/non-privacy leaking error message regardless of the internal reason for failure. It also provides indirect assurance that the Link is actually validating the blind hop as specified by BOLT-04!! --- htlcswitch/hop/payload.go | 66 +++ htlcswitch/link_test.go | 885 ++++++++++++++++++++++++++++++++++++++ htlcswitch/test_utils.go | 180 +++++++- 3 files changed, 1116 insertions(+), 15 deletions(-) diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index 40db517e1c..96ed7ae867 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -277,6 +277,72 @@ func computeOutgoingCltv(incomingTimelock uint32, return incomingTimelock - uint32(paymentRelay.CltvExpiryDelta) } +// PackRouteBlindingPayload writes the series of bytes that can be placed +// directly into the route blinding TLV payload for this hop. This will +// include the required information for relaying payment, as well as any +// constraints meant to be enforced by processing nodes in the blinded route. +// +// NOTE(10/26/22): This currently lives in the route package and is outside +// the scope of our first PR for blind hop processing. However, it could be +// that our testing code can be made cleaner if we bring something like this +// in to our first PR as it provides a more general way of encoding a route +// blinding TLV payload. Build the struct{}, then call this method. +func PackRouteBlindingPayload(w io.Writer, b *BlindHopPayload) error { + + // Encode route blinding payload as TLV stream. + var records = []tlv.Record{} + + // NOTE(8/13/22): The following checks ensure that we do not waste + // bytes on the wire for empty TLV fields. + // As an example, if we encode a nil slice in our TLV stream we will + // waste 2 bytes on the type and length (0). + if b.Padding != nil { + records = append(records, + record.NewPaddingRecord(&b.Padding), + ) + } + + if b.NextHop != Exit { + nextChanID := b.NextHop.ToUint64() + records = append(records, + record.NewBlindedNextHopRecord(&nextChanID), + ) + } + + if b.NextNodeID != nil { + records = append(records, + record.NewNextNodeIDRecord(&b.NextNodeID), + ) + } + + if b.PathID != nil { + records = append(records, record.NewPathIDRecord(&b.PathID)) + } + + if b.BlindingPointOverride != nil { + records = append(records, + record.NewBlindingOverrideRecord( + &b.BlindingPointOverride, + ), + ) + } + + if b.PaymentRelay != nil { + records = append(records, b.PaymentRelay.Record()) + } + + if b.PaymentConstraints != nil { + records = append(records, b.PaymentConstraints.Record()) + } + + tlvStream, err := tlv.NewStream(records...) + if err != nil { + return err + } + + return tlvStream.Encode(w) +} + // NewBlindHopPayloadFromReader parses a route blinding payload from // the passed io.Reader. The reader should correspond to the bytes // encapsulated in the encrypted route blinding payload after they diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index b2bbace47d..b28cb8f6b9 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -253,6 +253,8 @@ func TestChannelLinkRevThenSig(t *testing.T) { } // Restart Alice so she sends and accepts ChannelReestablish. + // TODO(10/22/22): Show that we correctly re process HTLCs in + // a blinded route after restart too! alice.restart(false, true) ctx.aliceLink = alice.link @@ -628,6 +630,877 @@ func testChannelLinkMultiHopPayment(t *testing.T, } } +/* + + Blinded Route + + +-----------+ +-----------+ +-----------+ +-----------+ + | Alice | | Bob | | Carol | | Dave | + | +-----------+-> +-----------+-> +-----------+-> | + | Sender | | Intro | | (Blind) | | (Blind) | + +-----------+ +-----------+ +-----------+ +-----------+ + + NOTE(11/20/22): Tests might be best performed against a "4 hop network" + as this would facilitate easy exercise for each of introduction node, + blind intermediate node, and blind recipient node. Absent adding a + newFourHopNetwork() or newNHopNetwork() we do our best here. + +*/ + +// TestChannelLinkBlindedPathPayment verifies that the link supports route +// blinding. More specifically we verify that the ChannelLink can process an +// onion packet "sent" to our blinded node ID public key rather than the usual +// persistent node ID public key, unblind the next hop in the route and +// forward the HTLC. We also verify that a ChannelLink processing a blind hop +// returns the opaque InvalidOnionBlinding error under the expected scenarios. +// This provides assurance that would be probers will always receive the same +// (hopefully) opaque/non-privacy leaking error message regardless of the +// internal reason for failure. It also provides indirect assurance that the +// Link is actually validating the blind hop as specified by BOLT-04!! +func TestChannelLinkBlindedPathPayment(t *testing.T) { + t.Run( + "blind hop processing - configuration + simple payment", + func(t *testing.T) { + testChannelLinkBlindHopProcessing(t) + }, + ) + t.Run( + "blind hop processing - broken blind hop processor implementation", + func(t *testing.T) { + testLinkBrokenBlindHopProcessor(t) + }, + ) + t.Run("blind hop processing payload validation", + func(t *testing.T) { + testLinkEnforcesBOLT04(t) + }, + ) +} + +var ( + amount = lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) + _, ephemeralBlindingPoint = btcec.PrivKeyFromBytes([]byte("test private key")) +) + +// testChannelLinkBlindHopProcessing verifies that a link +// correctly refuses to process blind hops when it has not +// been configured to do so. We also check that a basic +// payment is able to be delivered through a blinded route. +func testChannelLinkBlindHopProcessing(t *testing.T) { + t.Parallel() + + channels, _, err := createClusterChannels( + t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5, + ) + require.NoError(t, err, "unable to create channel") + + // Do not configure the links to support blind hop processing. + n := newThreeHopNetwork( + t, channels.aliceToBob, channels.bobToAlice, + channels.bobToCarol, channels.carolToBob, + testStartingHeight, + ) + + if err := n.start(); err != nil { + t.Fatal(err) + } + t.Cleanup(n.stop) + + // Generate a payment preimage. + preimage, rhash, _ := generatePaymentSecret() + payAddr, _ := generatePaymentAddress() + + // Generate blinded hops, taking care to use the payment + // preimage as the path_id for the final hop. + amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) + htlcAmt, totalTimelock, hops := generateBlindHops( + amount, testStartingHeight, preimage[:], + n.firstBobChannelLink, n.carolChannelLink, + ) + + blob, err := generateRoute(hops...) + if err != nil { + t.Fatal(err) + } + + // Send payment from Aice to Carol, via Bob. + // Expect that the Alice receives an InvalidOnionBlinding error as + // the downstream routing nodes are not configured to process bind hops. + firstHop := n.firstBobChannelLink.ShortChanID() + _, err = makePayment( + n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt, + totalTimelock, + ).Wait(30 * time.Second) + + assertFailureCode(t, err, lnwire.CodeInvalidOnionBlinding) + + // Now, configure the links to process blind hops. + // Modify the state of the links directly. No need to + // modify the function signature of newThreeHopNetwork + // to pass blindHopProcessor through there! + n.aliceChannelLink.cfg.RouteBlindingEnabled = true + n.firstBobChannelLink.cfg.RouteBlindingEnabled = true + n.secondBobChannelLink.cfg.RouteBlindingEnabled = true + n.carolChannelLink.cfg.RouteBlindingEnabled = true + + // Make another payment attempt. + invoice, htlc, pid, err := generatePaymentWithPreimage( + amount, htlcAmt, totalTimelock, blob, &preimage, rhash, payAddr, + ) + if err != nil { + t.Fatal(err) + } + + // Add the invoice to Carol's invoice registry so that she's + // expecting payment. + err = n.carolServer.registry.AddInvoice(*invoice, htlc.PaymentHash) + require.NoError(t, err, "unable to add invoice in carol registry") + + carolBandwidthBefore := n.carolChannelLink.Bandwidth() + firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth() + secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth() + aliceBandwidthBefore := n.aliceChannelLink.Bandwidth() + + // Send the HTLC from Alice to Carol, via Bob. + err = n.aliceServer.htlcSwitch.SendHTLC( + n.firstBobChannelLink.ShortChanID(), pid, htlc, + ) + require.NoError(t, err, "unable to send payment to carol") + + // Confirm that Alice receives the proper payment result. + resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult( + pid, htlc.PaymentHash, newMockDeobfuscator(), + ) + require.NoError(t, err, "unable to get payment result") + + select { + case result, ok := <-resultChan: + if !ok { + t.Fatalf("unexpected shutdown") + } + + require.NoError(t, result.Error) + + case <-time.After(5 * time.Second): + t.Fatalf("payment result did not arrive") + } + + // Wait for Alice and Bob's second link to receive the revocation. + // TODO(11/4/22): Figure out why these sleeps are used. See + // if we can do this via explicit synchronization. + time.Sleep(2 * time.Second) + + // Check that Carol invoice was settled and bandwidth of HTLC + // links were changed. + inv, err := n.carolServer.registry.LookupInvoice(rhash) + require.NoError(t, err, "unable to get invoice") + if inv.State != channeldb.ContractSettled { + t.Fatal("carol invoice haven't been settled") + } + + expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt + if expectedAliceBandwidth != n.aliceChannelLink.Bandwidth() { + t.Fatalf("channel bandwidth incorrect: expected %v, got %v", + expectedAliceBandwidth, n.aliceChannelLink.Bandwidth()) + } + + expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt + if expectedBobBandwidth1 != n.firstBobChannelLink.Bandwidth() { + t.Fatalf("channel bandwidth incorrect: expected %v, got %v", + expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth()) + } + + expectedBobBandwidth2 := secondBobBandwidthBefore - amount + if expectedBobBandwidth2 != n.secondBobChannelLink.Bandwidth() { + t.Fatalf("channel bandwidth incorrect: expected %v, got %v", + expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth()) + } + + expectedCarolBandwidth := carolBandwidthBefore + amount + if expectedCarolBandwidth != n.carolChannelLink.Bandwidth() { + t.Fatalf("channel bandwidth incorrect: expected %v, got %v", + expectedCarolBandwidth, n.carolChannelLink.Bandwidth()) + } + +} + +// testLinkBrokenBlindHopProcessor verifies that the link hands back an +// InvalidOnionBlinding error if it is unable to decrypt the route blinding +// payload or compute the next ephemeral (route) blinding point. +func testLinkBrokenBlindHopProcessor(t *testing.T) { + t.Parallel() + + channels, _, err := createClusterChannels( + t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5, + ) + require.NoError(t, err, "unable to create channel") + + n := newThreeHopNetwork( + t, channels.aliceToBob, channels.bobToAlice, + channels.bobToCarol, channels.carolToBob, + testStartingHeight, + ) + + if err := n.start(); err != nil { + t.Fatal(err) + } + t.Cleanup(n.stop) + + // Configure the links to support blind hop processing. + n.aliceChannelLink.cfg.RouteBlindingEnabled = true + n.firstBobChannelLink.cfg.RouteBlindingEnabled = true + n.secondBobChannelLink.cfg.RouteBlindingEnabled = true + n.carolChannelLink.cfg.RouteBlindingEnabled = true + + // Generate blinded hops. For ease of testing the + // route blinding payload is NOT encrypted. + amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin) + htlcAmt, totalTimelock, hops := generateBlindHops( + amount, testStartingHeight, nil, + n.firstBobChannelLink, n.carolChannelLink, + ) + + blob, err := generateRoute(hops...) + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + RouteBlindingTest + onionBlob [1366]byte + }{ + { + onionBlob: blob, + RouteBlindingTest: RouteBlindingTest{ + name: "fail to decrypt the route blinding " + + "payload", + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + OnionBlob: blob, + }, + blindHopProcessor: &brokenBlindHopProcessor{ + errOnDecrypt: true, + }, + errExpected: true, + }, + }, + { + onionBlob: blob, + RouteBlindingTest: RouteBlindingTest{ + name: "fail to compute a blinding point for " + + "the next hop in a blind route", + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + OnionBlob: blob, + }, + blindHopProcessor: func() BlindHopProcessor { + p := &brokenBlindHopProcessor{ + errOnDecrypt: false, + } + return p + }(), + errExpected: true, + }, + }, + } + + // Try sending the payment under a variety of scenarios and + // observe how the Link responds. + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + + // Configure the links, with a faulty (error prone) + // blind hop processor implementation so we can verify + // that the Link correctly handles errors encountered + // during blind hop processing. + if test.blindHopProcessor != nil { + n.firstBobChannelLink.cfg.BlindHopProcessor = + test.blindHopProcessor + } + defer func() { + n.firstBobChannelLink.cfg.BlindHopProcessor = + &mockBlindHopProcessor{} + }() + + // Generate a fresh invoice and payment hash for every + // test. This way we get unique payment hashes and + // payment addresses across tests. + preimage, rhash, _ := generatePaymentSecret() + payAddr, _ := generatePaymentAddress() + test.htlc.PaymentHash = rhash + + invoice, _, pid, err := generatePaymentWithPreimage( + amount, htlcAmt, totalTimelock, + blob, &preimage, rhash, payAddr, + ) + if err != nil { + t.Fatal(err) + } + + // Add the invoice to Carol's invoice registry so that she's + // expecting payment. + err = n.carolServer.registry.AddInvoice( + *invoice, test.htlc.PaymentHash, + ) + require.NoError(t, err, "unable to add invoice in carol registry") + + // Send the HTLC from Alice to Carol, via Bob. + err = n.aliceServer.htlcSwitch.SendHTLC( + n.firstBobChannelLink.ShortChanID(), + pid, test.htlc, + ) + require.NoError(t, err, "unable to send payment to carol") + + // Confirm that Alice receives the proper payment result. + resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult( + pid, test.htlc.PaymentHash, + newMockDeobfuscator(), + ) + require.NoError(t, err, "unable to get payment result") + + select { + case result, ok := <-resultChan: + if !ok { + t.Fatalf("unexpected shutdown") + } + + payErr := result.Error + + if test.errExpected { + assertFailureCode(t, payErr, lnwire.CodeInvalidOnionBlinding) + } else { + require.NoError(t, payErr) + } + + case <-time.After(5 * time.Second): + t.Fatalf("payment result did not arrive") + } + }) + } +} + +// Verify that the link correctly enforces BOLT-04 validation +// when processing blind hops. This should check that the link +// returns errors (and the correct error) when expected and also +// that blind hops traverse the Link under the correct scenarios. +func testLinkEnforcesBOLT04(t *testing.T) { + t.Parallel() + + channels, _, err := createClusterChannels( + t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5, + ) + require.NoError(t, err, "unable to create channel") + + n := newThreeHopNetwork( + t, channels.aliceToBob, channels.bobToAlice, + channels.bobToCarol, channels.carolToBob, + testStartingHeight, + ) + + if err := n.start(); err != nil { + t.Fatal(err) + } + t.Cleanup(n.stop) + + // Configure the links to support blind hop processing. + n.aliceChannelLink.cfg.RouteBlindingEnabled = true + n.firstBobChannelLink.cfg.RouteBlindingEnabled = true + n.secondBobChannelLink.cfg.RouteBlindingEnabled = true + n.carolChannelLink.cfg.RouteBlindingEnabled = true + + debug := false + if debug { + // Log messages that alice receives from bob. + n.aliceServer.intersect(createLogFunc("[alice]<-bob<-carol: ", + n.aliceChannelLink.ChanID())) + + // Log messages that bob receives from alice. + n.bobServer.intersect(createLogFunc("alice->[bob]->carol: ", + n.firstBobChannelLink.ChanID())) + + // Log messages that bob receives from carol. + n.bobServer.intersect(createLogFunc("alice<-[bob]<-carol: ", + n.secondBobChannelLink.ChanID())) + + // Log messages that carol receives from bob. + n.carolServer.intersect(createLogFunc("alice->bob->[carol]", + n.carolChannelLink.ChanID())) + } + + // Generate blinded hops. For ease of testing the + // route blinding payload is NOT encrypted. + amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin / 10) + htlcAmt, totalTimelock, _ := generateBlindHops( + amount, testStartingHeight, nil, + n.firstBobChannelLink, n.carolChannelLink, + ) + + testCases := []RouteBlindingTest{ + { + name: "introduction node blinded route", + onionPayload: &hop.Payload{ + BlindingPoint: ephemeralBlindingPoint, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + // No ephemeral blinding point in UpdateAddHTLC. + }, + }, + { + name: "intermediate hop blinded route w/ next hop", + onionPayload: &hop.Payload{}, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + }, + { + name: "final hop blinded route", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + Network: hop.BitcoinNetwork, + NextHop: hop.Exit, + AmountToForward: amount, + OutgoingCTLV: testStartingHeight + n.defaultDelta, + }, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + // This is an unexpected path_id. If we are going to + // demand that the path_id contain the payment preimage, + // then we must have already created it by the time + // we define this test case! + PathID: bytes.Repeat([]byte{1}, 32), + }, + htlc: &lnwire.UpdateAddHTLC{ + // Alice is sending HTLC directly to Bob. + Amount: amount, + Expiry: testStartingHeight + n.defaultDelta, + BlindingPoint: ephemeralBlindingPoint, + }, + isFinalHop: true, + }, + // { + // // NOTE(11/15/22): With how the test is driven here, + // // this test case might be better in standalone story test, + // // rather than as a table driven test. + // name: "final hop with unexpected path_id", + // onionPayload: &hop.Payload{ + // FwdInfo: hop.ForwardingInfo{ + // Network: hop.BitcoinNetwork, + // NextHop: hop.Exit, + // AmountToForward: amount, + // OutgoingCTLV: testStartingHeight + n.defaultDelta, + // }, + // }, + // routeBlindingPayload: &hop.BlindHopPayload{ + // PathID: bytes.Repeat([]byte{1}, 32), + // }, + // htlc: &lnwire.UpdateAddHTLC{ + // // Alice is sending HTLC directly to Bob. + // Amount: amount, + // Expiry: testStartingHeight + n.defaultDelta, + // BlindingPoint: ephemeralBlindingPoint, + // }, + // isFinalHop: true, + // errExpected: true, + // }, + { + name: "blind hop with blinding point in both msg and tlv payload", + onionPayload: &hop.Payload{ + BlindingPoint: ephemeralBlindingPoint, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop missing blinding point in msg" + + "and tlv payload", + onionPayload: &hop.Payload{}, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + // BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop missing payment_relay", // error case (covered at parse time!!) + onionPayload: &hop.Payload{}, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: n.carolChannelLink.ShortChanID(), + // PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "intermediate blind hop missing both next_hop " + // error case (covered at parse time!!) + "and next_node_id", + onionPayload: &hop.Payload{}, + routeBlindingPayload: &hop.BlindHopPayload{ + // NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop payment_relay which fails to meet constraints", + onionPayload: &hop.Payload{}, + routeBlindingPayload: &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: totalTimelock, + HtlcMinimumMsat: ^uint64(0), + // AllowedFeatures: []byte, + }, + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop payment_relay fails to meet constraints - expired", + onionPayload: &hop.Payload{}, + routeBlindingPayload: &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: totalTimelock - 1, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop with amt_to_forward improperly set in top level TLV payload", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + AmountToForward: 100, + }, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: totalTimelock, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop with timelock improperly set in top level TLV payload", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + OutgoingCTLV: 10, + }, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: totalTimelock, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + { + name: "blind hop with short_channel_id improperly set in top level TLV payload", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + NextHop: n.carolChannelLink.ShortChanID(), + }, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + Padding: []byte{0, 0, 0, 0}, + NextHop: n.carolChannelLink.ShortChanID(), + PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + PaymentConstraints: &record.PaymentConstraints{ + MaxCltvExpiryDelta: totalTimelock, + HtlcMinimumMsat: 0, + // AllowedFeatures: []byte, + }, + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: htlcAmt, + Expiry: totalTimelock, + BlindingPoint: ephemeralBlindingPoint, + }, + errExpected: true, + }, + // NOTE(11/16/22): We cannot currrently validate that the link + // returns the expected error here as our mockHopIterator does + // not support MPP or AMP encoding. + // TODO(11/16/22): Revisit once we add TLV to mockHopIterator. + // Are we double dipping with validation testing done at + // payload parse time here? + // { + // name: "blind hop with MPP improperly set in top level TLV payload", // error case (cross onion + RB payload) + // onionPayload: &hop.Payload{ + // MPP: &record.MPP{}, + // }, + // routeBlindingPayload: &hop.BlindHopPayload{ + // Padding: []byte{0, 0, 0, 0}, + // NextHop: n.carolChannelLink.ShortChanID(), + // PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + // PaymentConstraints: &record.PaymentConstraints{ + // MaxCltvExpiryDelta: totalTimelock, + // HtlcMinimumMsat: 0, + // }, + // }, + // htlc: &lnwire.UpdateAddHTLC{ + // Amount: htlcAmt, + // Expiry: totalTimelock, + // BlindingPoint: ephemeralBlindingPoint, + // }, + // errExpected: true, + // failureCode: lnwire.CodeInvalidOnionBlinding, + // }, + // { + // name: "blind hop with AMP improperly set in top level TLV payload", + // onionPayload: &hop.Payload{ + // AMP: &record.AMP{}, + // }, + // routeBlindingPayload: &hop.BlindHopPayload{ + // Padding: []byte{0, 0, 0, 0}, + // NextHop: n.carolChannelLink.ShortChanID(), + // PaymentRelay: computePaymentRelay(n.secondBobChannelLink), + // PaymentConstraints: &record.PaymentConstraints{ + // MaxCltvExpiryDelta: totalTimelock, + // HtlcMinimumMsat: 0, + // }, + // }, + // htlc: &lnwire.UpdateAddHTLC{ + // Amount: htlcAmt, + // Expiry: totalTimelock, + // BlindingPoint: ephemeralBlindingPoint, + // }, + // errExpected: true, + // failureCode: lnwire.CodeInvalidOnionBlinding, + // }, + { + name: "final hop blinded route missing amt in top level TLV payload", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + NextHop: hop.Exit, + OutgoingCTLV: testStartingHeight, + }, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: hop.Exit, + PathID: bytes.Repeat([]byte{1}, 32), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: amount, + Expiry: testStartingHeight + n.defaultDelta, + BlindingPoint: ephemeralBlindingPoint, + }, + isFinalHop: true, + errExpected: true, + }, + { + name: "final hop blinded route missing outgoing_cltv_value in top level TLV payload", + onionPayload: &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + NextHop: hop.Exit, + AmountToForward: amount, + }, + }, + routeBlindingPayload: &hop.BlindHopPayload{ + NextHop: hop.Exit, + PathID: bytes.Repeat([]byte{1}, 32), + }, + htlc: &lnwire.UpdateAddHTLC{ + Amount: amount, + Expiry: testStartingHeight + n.defaultDelta, + BlindingPoint: ephemeralBlindingPoint, + }, + isFinalHop: true, + errExpected: true, + }, + } + + // Try sending the payment under a variety of scenarios and + // observe how the Link responds. + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + + // We BYOHTLC ("Bring Your Own HTLC") but randomize the + // payment hash for each test run. + preimage, rhash, _ := generatePaymentSecret() + payAddr, _ := generatePaymentAddress() + test.htlc.PaymentHash = rhash + + // Serialize the route blinding payload which will be + // delivered to Bob. + var b bytes.Buffer + test.routeBlindingPayload.PathID = preimage[:] + hop.PackRouteBlindingPayload(&b, test.routeBlindingPayload) + test.onionPayload.RouteBlindingEncryptedData = b.Bytes() + + // If Bob is not the final hop for this test, we'll add Carol as + // the final hop. This will let us see how Bob's ChannelLink + // behaves as an intermediate processing node. + var routeBlindingBytes bytes.Buffer + if !test.isFinalHop { + finalHopRBPayload := &hop.BlindHopPayload{ + Padding: []byte{0x00, 0x01, 0x00, 0x00}, + NextHop: hop.Exit, + PathID: preimage[:], + // PaymentConstraints: &record.PaymentConstraints{}, + } + hop.PackRouteBlindingPayload(&routeBlindingBytes, finalHopRBPayload) + } + + // If we're putting Bob's link under test as an intermediate hop, + // then construct a series of two hops. + hops := []*hop.Payload{ + test.onionPayload, + { + FwdInfo: hop.ForwardingInfo{ + Network: hop.BitcoinNetwork, + NextHop: hop.Exit, + AmountToForward: amount, + OutgoingCTLV: testStartingHeight + n.defaultDelta, + }, + RouteBlindingEncryptedData: routeBlindingBytes.Bytes(), + }, + } + + // Otherwise, we'll test Bob's link as a final hop. + if test.isFinalHop { + hops = hops[:1] + } + + // Construct an onion blob for a either one or two hops. (depending on test) + // Add the newly serialized onion blob to the test's HTLC. + blob, err := generateRoute(hops...) + if err != nil { + t.Fatal(err) + } + test.htlc.OnionBlob = blob + + // Generate an invoice. + invoice, _, pid, err := generatePaymentWithPreimage( + amount, htlcAmt, totalTimelock, + blob, &preimage, rhash, payAddr, + ) + if err != nil { + t.Fatal(err) + } + + // If we're putting Bob's link under test as an intermediate hop, + // then add the invoice to Carol's registry so she expects payment. + err = n.carolServer.registry.AddInvoice( + *invoice, test.htlc.PaymentHash, + ) + require.NoError(t, err, "unable to add invoice in carol registry") + + // Otherwise, we'll test Bob's link as a final hop. + // Add the invoice to Bob's invoice registry so that + // he's expecting payment. + if test.isFinalHop { + err = n.bobServer.registry.AddInvoice( + *invoice, test.htlc.PaymentHash, + ) + require.NoError(t, err, "unable to add invoice in bob registry") + } + + // Either way, Alice sends the HTLC using the link with Bob + // as the first hop. + err = n.aliceServer.htlcSwitch.SendHTLC( + n.firstBobChannelLink.ShortChanID(), pid, test.htlc, + ) + require.NoError(t, err, "unable to send payment to bob") + + // Confirm that Alice receives the proper payment result. + resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult( + pid, test.htlc.PaymentHash, newMockDeobfuscator(), + ) + require.NoError(t, err, "unable to get payment result") + + select { + case result, ok := <-resultChan: + if !ok { + t.Fatalf("unexpected shutdown") + } + + payErr := result.Error + + if test.errExpected { + assertFailureCode(t, payErr, lnwire.CodeInvalidOnionBlinding) + } else { + require.NoError(t, payErr) + } + + case <-time.After(5 * time.Second): + t.Fatalf("payment result did not arrive") + } + + }) + } +} + // TestChannelLinkCancelFullCommitment tests the ability for links to cancel // forwarded HTLCs once all of their commitment slots are full. func TestChannelLinkCancelFullCommitment(t *testing.T) { @@ -6609,6 +7482,18 @@ func assertFailureCode(t *testing.T, err error, code lnwire.FailCode) { } } +// TODO(11/21/22): combine these two? +type RouteBlindingTest struct { + name string + onionPayload *hop.Payload + routeBlindingPayload *hop.BlindHopPayload + htlc *lnwire.UpdateAddHTLC + blindHopProcessor BlindHopProcessor + isFinalHop bool + errExpected bool + expectedErr error +} + type validateOnionPayloadTest struct { name string onionPayload *hop.Payload diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index e4568fc0e9..d4da664eda 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -21,7 +21,6 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" - sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/htlcswitch/hop" @@ -36,6 +35,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/shachain" "github.com/lightningnetwork/lnd/ticker" "github.com/stretchr/testify/require" @@ -481,6 +481,7 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte, // getChanID retrieves the channel point from an lnnwire message. func getChanID(msg lnwire.Message) (lnwire.ChannelID, error) { var chanID lnwire.ChannelID + switch msg := msg.(type) { case *lnwire.UpdateAddHTLC: chanID = msg.ChanID @@ -488,6 +489,8 @@ func getChanID(msg lnwire.Message) (lnwire.ChannelID, error) { chanID = msg.ChanID case *lnwire.UpdateFailHTLC: chanID = msg.ChanID + case *lnwire.UpdateFailMalformedHTLC: + chanID = msg.ChanID case *lnwire.RevokeAndAck: chanID = msg.ChanID case *lnwire.CommitSig: @@ -549,27 +552,42 @@ func generatePaymentWithPreimage(invoiceAmt, htlcAmt lnwire.MilliSatoshi, return invoice, htlc, paymentID, nil } +func generatePaymentAddress() ([32]byte, error) { + + var payAddr lntypes.Hash + r, err := generateRandomBytes(sha256.Size) + copy(payAddr[:], r) + + return payAddr, err +} + +func generatePaymentSecret() (lntypes.Preimage, [32]byte, error) { + + var preimage lntypes.Preimage + var rHash lntypes.Hash + r, err := generateRandomBytes(lntypes.PreimageSize) + copy(preimage[:], r) + + rHash = sha256.Sum256(r) + + return preimage, rHash, err +} + // generatePayment generates the htlc add request by given path blob and // invoice which should be added by destination peer. func generatePayment(invoiceAmt, htlcAmt lnwire.MilliSatoshi, timelock uint32, blob [lnwire.OnionPacketSize]byte) (*channeldb.Invoice, *lnwire.UpdateAddHTLC, uint64, error) { - var preimage lntypes.Preimage - r, err := generateRandomBytes(sha256.Size) + preimage, rhash, err := generatePaymentSecret() if err != nil { return nil, nil, 0, err } - copy(preimage[:], r) - - rhash := sha256.Sum256(preimage[:]) - var payAddr [sha256.Size]byte - r, err = generateRandomBytes(sha256.Size) + payAddr, err := generatePaymentAddress() if err != nil { return nil, nil, 0, err } - copy(payAddr[:], r) return generatePaymentWithPreimage( invoiceAmt, htlcAmt, timelock, blob, &preimage, rhash, payAddr, @@ -662,15 +680,140 @@ func generateHops(payAmt lnwire.MilliSatoshi, startingHeight uint32, amount = runningAmt - fee } + // NOTE(10/22/22): We still only ever use legacy onion payloads. + // We should create a new version of this function or update + // this one to use the now required TLV onion hop payload! + hop := &hop.Payload{ + FwdInfo: hop.ForwardingInfo{ + Network: hop.BitcoinNetwork, + NextHop: nextHop, + AmountToForward: amount, + OutgoingCTLV: timeLock, + }, + } + hops[i] = hop + } + + return runningAmt, totalTimelock, hops +} + +// computePaymentRelay computes a the minimal set of information +// necessary to forward a payment inside a blinded route. +// This method does NOT take the perspective of a blinded +// route builder looking to maximize privacy. +func computePaymentRelay(link *channelLink) *record.PaymentRelay { + return &record.PaymentRelay{ + // QUESTION(10/25/22): Why do all the route + // blinding spec types not match LND types? + BaseFee: uint32(link.cfg.FwrdingPolicy.BaseFee), + FeeRate: uint32(link.cfg.FwrdingPolicy.FeeRate), + CltvExpiryDelta: uint16(link.cfg.FwrdingPolicy.TimeLockDelta), + } +} + +// generateBlindHops constructs the onion and route blinding payload +// for each hop in a blind route. It also provideds the total amount to +// be sent, and the time lock value needed to route an HTLC with the +// target amount over the specified path. +func generateBlindHops(payAmt lnwire.MilliSatoshi, startingHeight uint32, + pathID []byte, path ...*channelLink) (lnwire.MilliSatoshi, uint32, []*hop.Payload) { + + totalTimelock := startingHeight + runningAmt := payAmt + + hops := make([]*hop.Payload, len(path)) + for i := len(path) - 1; i >= 0; i-- { + + var finalHop bool = i == len(path)-1 + + // If this is the last hop, then the next hop is the special + // "exit node". Otherwise, we look to the "prior" hop. + // NOTE(10/28/22): The conditionals which handle next hop, + // amount and timelock could be combined, but the function + // might be more readable if they are handled separately. + // This is test code so performance is not as critical. + nextHop := hop.Exit + if !finalHop { + nextHop = path[i+1].channel.ShortChanID() + } + + // If this is the last hop, then the time lock will be their + // specified delta policy plus our starting height. + var timeLock uint32 + if finalHop { + totalTimelock += testInvoiceCltvExpiry + timeLock = totalTimelock + } else { + // Otherwise, the outgoing time lock should be the + // incoming timelock minus their specified delta. + delta := path[i+1].cfg.FwrdingPolicy.TimeLockDelta + totalTimelock += delta + timeLock = totalTimelock - delta + } + + // Finally, we'll need to calculate the amount to forward. For + // the last hop, it's just the payment amount. + amount := payAmt + if !finalHop { + prevHop := hops[i+1] + prevAmount := prevHop.ForwardingInfo().AmountToForward + + fee := ExpectedFee(path[i].cfg.FwrdingPolicy, prevAmount) + runningAmt += fee + + // Otherwise, for a node to forward an HTLC, then + // following inequality most hold true: + // * amt_in - fee >= amt_to_forward + amount = runningAmt - fee + } + var nextHopBytes [8]byte binary.BigEndian.PutUint64(nextHopBytes[:], nextHop.ToUint64()) - hops[i] = hop.NewLegacyPayload(&sphinx.HopData{ - Realm: [1]byte{}, // hop.BitcoinNetwork - NextAddress: nextHopBytes, - ForwardAmount: uint64(amount), - OutgoingCltv: timeLock, - }) + // Construct the onion and route blinding payloads for this hop. + payload := &hop.Payload{} + + blindPayload := hop.BlindHopPayload{ + Padding: []byte{0x00, 0x01, 0x00, 0x00}, + NextHop: nextHop, + // PaymentConstraints: &record.PaymentConstraints{}, + } + + // Configuration meant for the first blind hop only. + if i == 0 { + _, ephemeralBlindingPoint := btcec.PrivKeyFromBytes([]byte("test private key")) + payload.BlindingPoint = ephemeralBlindingPoint + } + + // Configuration meant for all intermediate (non-final) hops. + if !finalHop { + // Each intermediate hop requires fee and timelock + // information in order to relay payment. + blindPayload.PaymentRelay = computePaymentRelay(path[i]) + } + + // Configuration meant for the final blind hop only. + if finalHop { + // The final hop in a blinded route must have a + // path ID and top level forwarding information set. + blindPayload.PathID = bytes.Repeat([]byte{1}, 32) + if pathID != nil { + blindPayload.PathID = pathID + } + + payload.FwdInfo = hop.ForwardingInfo{ + Network: hop.BitcoinNetwork, + NextHop: nextHop, + AmountToForward: amount, + OutgoingCTLV: timeLock, + } + } + + // Serialize the route blinding TLV payload + var b bytes.Buffer + hop.PackRouteBlindingPayload(&b, &blindPayload) + payload.RouteBlindingEncryptedData = b.Bytes() + hops[i] = payload } return runningAmt, totalTimelock, hops @@ -850,6 +993,8 @@ func (n *threeHopNetwork) stop() { } } +// NOTE(10/22/22): this is a 3 (or less) node network proprietary structure. +// Would need to be generalized to support N node networks. type clusterChannels struct { aliceToBob *lnwallet.LightningChannel bobToAlice *lnwallet.LightningChannel @@ -859,6 +1004,8 @@ type clusterChannels struct { // createClusterChannels creates lightning channels which are needed for // network cluster to be initialized. +// NOTE(10/22/22): this is a 3 (or less) node network proprietary function. +// Would need to be generalized to support N node networks. func createClusterChannels(t *testing.T, aliceToBob, bobToCarol btcutil.Amount) ( *clusterChannels, func() (*clusterChannels, error), error) { @@ -1017,6 +1164,7 @@ func newThreeHopNetwork(t testing.TB, aliceChannel, firstBobChannel, } } +// NOTE(10/22/22): this is a 3 (or less) node network proprietary type/function. // serverOption is a function which alters the three servers created for // a three hop network to allow custom settings on each server. type serverOption func(aliceServer, bobServer, carolServer *mockServer) @@ -1160,9 +1308,11 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, NotifyInactiveChannel: func(wire.OutPoint) {}, HtlcNotifier: server.htlcSwitch.cfg.HtlcNotifier, GetAliases: getAliases, + BlindHopProcessor: &mockBlindHopProcessor{}, }, channel, ) + if err := server.htlcSwitch.AddLink(link); err != nil { return nil, fmt.Errorf("unable to add channel link: %v", err) }