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/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/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 c1073b1da9..9bb9f0e645 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 @@ -170,7 +193,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 +228,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 } @@ -220,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 @@ -276,7 +303,7 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte, } err = tx.ProcessOnionPacket( - seqNum, onionPkt, req.RHash, req.IncomingCltv, + seqNum, onionPkt, req.RHash, req.IncomingCltv, req.BlindingPoint, ) switch err { case nil: @@ -383,6 +410,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, ) 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 be7be5eebb..96ed7ae867 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" @@ -27,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. @@ -41,6 +52,12 @@ func (v PayloadViolation) String() string { case RequiredViolation: return "required" + case OverloadedViolation: + return "overloaded" + + case InsufficientViolation: + return "insufficient" + default: return "unknown violation" } @@ -60,6 +77,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. @@ -69,8 +90,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. @@ -90,6 +116,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 +131,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 +152,303 @@ 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 { + // 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 +} + +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) +} + +// 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 +// 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) { +func NewPayloadFromReader(r io.Reader, isFinalHop bool) (*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 +456,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,8 +473,11 @@ 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) + err = ValidateParsedPayloadTypes(parsedTypes, isFinalHop) if err != nil { return nil, err } @@ -158,7 +488,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { return nil, ErrInvalidPayload{ Type: *violatingType, Violation: RequiredViolation, - FinalHop: nextHop == Exit, + FinalHop: isFinalHop, } } @@ -168,7 +498,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 +520,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,39 +551,142 @@ 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, + BlindHop: true, + } + } + + // An intermedate hop MUST specify the node to which we should forward. + if !hasNextHop && !hasNextNode { + return ErrInvalidPayload{ + Type: record.BlindedNextHopOnionType, + Violation: OmittedViolation, + FinalHop: false, + BlindHop: true, + } + } + } 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, + BlindHop: 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 { - - isFinalHop := nextHop == Exit + 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 + // 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] _, hasLockTime := parsedTypes[record.LockTimeOnionType] _, 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..9b61c5de20 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -14,16 +14,30 @@ 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, + }, + isFinalHop: true, + }, { name: "final hop valid", payload: []byte{0x02, 0x00, 0x04, 0x00}, @@ -42,6 +56,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.OmittedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "intermediate hop no amount", @@ -62,6 +77,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.OmittedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "intermediate hop no expiry", @@ -84,6 +100,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.IncludedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type after omitted hop id", @@ -96,6 +113,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.RequiredViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type after included hop id", @@ -118,6 +136,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.RequiredViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type zero final hop zero sid", @@ -129,6 +148,7 @@ var decodePayloadTests = []decodePayloadTest{ Violation: hop.IncludedViolation, FinalHop: true, }, + isFinalHop: true, }, { name: "required type zero intermediate hop", @@ -158,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", @@ -234,6 +255,7 @@ var decodePayloadTests = []decodePayloadTest{ }, expErr: nil, shouldHaveMPP: true, + isFinalHop: true, }, { name: "final hop with amp", @@ -258,6 +280,7 @@ var decodePayloadTests = []decodePayloadTest{ 0x09, }, shouldHaveAMP: true, + isFinalHop: true, }, { name: "final hop with metadata", @@ -271,6 +294,221 @@ 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, + BlindHop: true, + }, + }, + { + 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, + BlindHop: 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, + // 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, + }, + }, } // TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the @@ -310,7 +548,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) @@ -354,6 +594,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) @@ -364,3 +635,243 @@ 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, + BlindHop: true, + }, + }, + { + 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. + BlindHop: true, + }, + 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, + BlindHop: true, + }, + }, + { + 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, + BlindHop: true, + }, + }, + { + 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, + BlindHop: 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) + } + +} + +// 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 c3c824afc1..a55687deea 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -9,23 +9,28 @@ 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" "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" "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" ) @@ -151,6 +156,14 @@ 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 + + // 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 @@ -295,6 +308,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, @@ -1621,6 +1638,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) @@ -1813,6 +1854,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) @@ -1850,6 +1895,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 { @@ -2900,6 +2952,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) @@ -2954,6 +3012,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) @@ -2971,6 +3031,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, ) @@ -2999,6 +3061,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, @@ -3011,8 +3075,62 @@ 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. + // - Should whether we currently support + // processing blind hops affect the error we + // return to senders? + 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, ) @@ -3050,9 +3168,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 @@ -3078,6 +3197,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, outgoingTimeout: fwdInfo.OutgoingCTLV, customRecords: pld.CustomRecords(), } + switchPackets = append( switchPackets, updatePacket, ) @@ -3095,6 +3215,18 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, 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, } // Finally, we'll encode the onion packet for the @@ -3114,6 +3246,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, ) @@ -3205,6 +3339,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 @@ -3220,6 +3358,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 @@ -3260,6 +3402,389 @@ 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 +} + +// 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) { + + // 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) + 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 { diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index b16f3893c5..b28cb8f6b9 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" ) @@ -251,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 @@ -626,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) { @@ -6606,3 +7481,419 @@ func assertFailureCode(t *testing.T, err error, code lnwire.FailCode) { code, rtErr.WireMessage().Code()) } } + +// 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 + 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) + } + +} diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index 9eac221dd2..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,35 @@ 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 +} + func (r *mockHopIterator) ExtraOnionBlob() []byte { return nil } @@ -345,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))) @@ -354,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 } } @@ -363,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 @@ -383,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 @@ -477,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[:]) @@ -488,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) { @@ -525,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 { @@ -546,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 { @@ -568,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) 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/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) } 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/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) 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/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/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/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 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) } 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/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/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, + ) +} 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 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 6e44cbd6bf..57e71b05a4 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, ) @@ -537,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 { @@ -662,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 @@ -3697,6 +3704,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,