From f7a9aa875ee313430361edc1fccb277a3b606d64 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 13 May 2024 13:44:56 +0200 Subject: [PATCH 01/11] routing+refactor: let BlindedEdge carry BlindedPayment This commit is purely a refactor. In it, we let the `BlindedEdge` struct carry a pointer to the `BlindedPayment` that it was derived from. This is done now because later on in the PR series, we will need more information about the `BlindedPayment` that an edge was derived from. Since we now pass in the whole BlindedPayment, we swap out the `cipherText` member for a `hopIndex` member so that we dont carry around two sources of truth in the same struct. --- routing/additional_edge.go | 47 +++++++++++++++++++++---- routing/additional_edge_test.go | 22 ++++++++---- routing/blinding.go | 28 +++++++-------- routing/blinding_test.go | 62 ++++++++++++++++++--------------- routing/pathfind_test.go | 4 ++- routing/router.go | 6 +++- 6 files changed, 109 insertions(+), 60 deletions(-) diff --git a/routing/additional_edge.go b/routing/additional_edge.go index eee17cce1a..a1e5fc8564 100644 --- a/routing/additional_edge.go +++ b/routing/additional_edge.go @@ -2,8 +2,8 @@ package routing import ( "errors" + "fmt" - "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -61,11 +61,42 @@ func (p *PrivateEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi, } // BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed -// as additional edges because they are appened at the end of a normal route. +// as additional edges because they are appended at the end of a normal route. type BlindedEdge struct { - policy *models.CachedEdgePolicy - cipherText []byte - blindingPoint *btcec.PublicKey + policy *models.CachedEdgePolicy + + // blindedPayment is the BlindedPayment that this blinded edge was + // derived from. + blindedPayment *BlindedPayment + + // hopIndex is the index of the hop in the blinded payment path that + // this edge is associated with. + hopIndex int +} + +// NewBlindedEdge constructs a new BlindedEdge which packages the policy info +// for a specific hop within the given blinded payment path. The hop index +// should correspond to the hop within the blinded payment that this edge is +// associated with. +func NewBlindedEdge(policy *models.CachedEdgePolicy, payment *BlindedPayment, + hopIndex int) (*BlindedEdge, error) { + + if payment == nil { + return nil, fmt.Errorf("blinded payment cannot be nil for " + + "blinded edge") + } + + if hopIndex < 0 || hopIndex >= len(payment.BlindedPath.BlindedHops) { + return nil, fmt.Errorf("the hop index %d is outside the "+ + "valid range between 0 and %d", hopIndex, + len(payment.BlindedPath.BlindedHops)-1) + } + + return &BlindedEdge{ + policy: policy, + hopIndex: hopIndex, + blindedPayment: payment, + }, nil } // EdgePolicy return the policy of the BlindedEdge. @@ -78,9 +109,11 @@ func (b *BlindedEdge) EdgePolicy() *models.CachedEdgePolicy { func (b *BlindedEdge) IntermediatePayloadSize(_ lnwire.MilliSatoshi, _ uint32, _ uint64) uint64 { + blindedPath := b.blindedPayment.BlindedPath + hop := route.Hop{ - BlindingPoint: b.blindingPoint, - EncryptedData: b.cipherText, + BlindingPoint: blindedPath.BlindingPoint, + EncryptedData: blindedPath.BlindedHops[b.hopIndex].CipherText, } // For blinded paths the next chanID is in the encrypted data tlv. diff --git a/routing/additional_edge_test.go b/routing/additional_edge_test.go index b5628c6b7f..0324e2e106 100644 --- a/routing/additional_edge_test.go +++ b/routing/additional_edge_test.go @@ -42,9 +42,13 @@ func TestIntermediatePayloadSize(t *testing.T) { hop: route.Hop{ EncryptedData: []byte{12, 13}, }, - edge: &BlindedEdge{ - cipherText: []byte{12, 13}, - }, + edge: &BlindedEdge{blindedPayment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindedHops: []*sphinx.BlindedHopInfo{ + {CipherText: []byte{12, 13}}, + }, + }, + }}, }, { name: "Blinded edge - introduction point", @@ -52,10 +56,14 @@ func TestIntermediatePayloadSize(t *testing.T) { EncryptedData: []byte{12, 13}, BlindingPoint: blindedPoint, }, - edge: &BlindedEdge{ - cipherText: []byte{12, 13}, - blindingPoint: blindedPoint, - }, + edge: &BlindedEdge{blindedPayment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindingPoint: blindedPoint, + BlindedHops: []*sphinx.BlindedHopInfo{ + {CipherText: []byte{12, 13}}, + }, + }, + }}, }, } diff --git a/routing/blinding.go b/routing/blinding.go index d2d64aa5dd..788fb7b77e 100644 --- a/routing/blinding.go +++ b/routing/blinding.go @@ -88,13 +88,13 @@ func (b *BlindedPayment) Validate() error { // the case of multiple blinded hops, CLTV delta is fully accounted for in the // hints (both for intermediate hops and the final_cltv_delta for the receiving // node). -func (b *BlindedPayment) toRouteHints() RouteHints { +func (b *BlindedPayment) toRouteHints() (RouteHints, error) { // If we just have a single hop in our blinded route, it just contains // an introduction node (this is a valid path according to the spec). // Since we have the un-blinded node ID for the introduction node, we // don't need to add any route hints. if len(b.BlindedPath.BlindedHops) == 1 { - return nil + return nil, nil } hintCount := len(b.BlindedPath.BlindedHops) - 1 @@ -136,14 +136,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints { ToNodeFeatures: features, } - hints[fromNode] = []AdditionalEdge{ - &BlindedEdge{ - policy: edgePolicy, - cipherText: b.BlindedPath.BlindedHops[0].CipherText, - blindingPoint: b.BlindedPath.BlindingPoint, - }, + edge, err := NewBlindedEdge(edgePolicy, b, 0) + if err != nil { + return nil, err } + hints[fromNode] = []AdditionalEdge{edge} + // Start at an offset of 1 because the first node in our blinded hops // is the introduction node and terminate at the second-last node // because we're dealing with hops as pairs. @@ -169,14 +168,13 @@ func (b *BlindedPayment) toRouteHints() RouteHints { ToNodeFeatures: features, } - hints[fromNode] = []AdditionalEdge{ - &BlindedEdge{ - policy: edgePolicy, - cipherText: b.BlindedPath.BlindedHops[i]. - CipherText, - }, + edge, err := NewBlindedEdge(edgePolicy, b, i) + if err != nil { + return nil, err } + + hints[fromNode] = []AdditionalEdge{edge} } - return hints + return hints, nil } diff --git a/routing/blinding_test.go b/routing/blinding_test.go index f6327ecd5d..58ad565949 100644 --- a/routing/blinding_test.go +++ b/routing/blinding_test.go @@ -128,7 +128,9 @@ func TestBlindedPaymentToHints(t *testing.T) { HtlcMaximum: htlcMax, Features: features, } - require.Nil(t, blindedPayment.toRouteHints()) + hints, err := blindedPayment.toRouteHints() + require.NoError(t, err) + require.Nil(t, hints) // Populate the blinded payment with hops. blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{ @@ -146,41 +148,43 @@ func TestBlindedPaymentToHints(t *testing.T) { }, } + policy1 := &models.CachedEdgePolicy{ + TimeLockDelta: cltvDelta, + MinHTLC: lnwire.MilliSatoshi(htlcMin), + MaxHTLC: lnwire.MilliSatoshi(htlcMax), + FeeBaseMSat: lnwire.MilliSatoshi(baseFee), + FeeProportionalMillionths: lnwire.MilliSatoshi( + ppmFee, + ), + ToNodePubKey: func() route.Vertex { + return vb2 + }, + ToNodeFeatures: features, + } + policy2 := &models.CachedEdgePolicy{ + ToNodePubKey: func() route.Vertex { + return vb3 + }, + ToNodeFeatures: features, + } + + blindedEdge1, err := NewBlindedEdge(policy1, blindedPayment, 0) + require.NoError(t, err) + + blindedEdge2, err := NewBlindedEdge(policy2, blindedPayment, 1) + require.NoError(t, err) + expected := RouteHints{ v1: { - //nolint:lll - &BlindedEdge{ - policy: &models.CachedEdgePolicy{ - TimeLockDelta: cltvDelta, - MinHTLC: lnwire.MilliSatoshi(htlcMin), - MaxHTLC: lnwire.MilliSatoshi(htlcMax), - FeeBaseMSat: lnwire.MilliSatoshi(baseFee), - FeeProportionalMillionths: lnwire.MilliSatoshi( - ppmFee, - ), - ToNodePubKey: func() route.Vertex { - return vb2 - }, - ToNodeFeatures: features, - }, - blindingPoint: blindedPoint, - cipherText: cipherText, - }, + blindedEdge1, }, vb2: { - &BlindedEdge{ - policy: &models.CachedEdgePolicy{ - ToNodePubKey: func() route.Vertex { - return vb3 - }, - ToNodeFeatures: features, - }, - cipherText: cipherText, - }, + blindedEdge2, }, } - actual := blindedPayment.toRouteHints() + actual, err := blindedPayment.toRouteHints() + require.NoError(t, err) require.Equal(t, len(expected), len(actual)) for vertex, expectedHint := range expected { diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 06005716f5..fd9839dbaf 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -3341,7 +3341,9 @@ func TestBlindedRouteConstruction(t *testing.T) { // that make up the graph we'll give to route construction. The hints // map is keyed by source node, so we can retrieve our blinded edges // accordingly. - blindedEdges := blindedPayment.toRouteHints() + blindedEdges, err := blindedPayment.toRouteHints() + require.NoError(t, err) + carolDaveEdge := blindedEdges[carolVertex][0] daveEveEdge := blindedEdges[daveBlindedVertex][0] diff --git a/routing/router.go b/routing/router.go index 851db4af0b..149cd34156 100644 --- a/routing/router.go +++ b/routing/router.go @@ -2001,6 +2001,7 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex, // Assume that we're starting off with a regular payment. requestHints = routeHints requestExpiry = finalExpiry + err error ) if blindedPayment != nil { @@ -2038,7 +2039,10 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex, requestExpiry = blindedPayment.CltvExpiryDelta } - requestHints = blindedPayment.toRouteHints() + requestHints, err = blindedPayment.toRouteHints() + if err != nil { + return nil, err + } } requestTarget, err := getTargetNode(target, blindedPayment) From 28d1227c04278f212734f9877e99ba471c3c9442 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 14 May 2024 12:34:33 +0200 Subject: [PATCH 02/11] routing: add BlindedPayment() method to AdditionalEdges Expand the AdditionalEdges interface with a BlindedPayment method. In upcoming commits, we will want to know if an AdditionalEdge was derived from a blinded payment or not and we will also need some information from the blinded payment it was derived from. So we expand the interface here to avoid needing to do type casts later on. The new method may return nil if the edge was not derived from a blinded payment. --- routing/additional_edge.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/routing/additional_edge.go b/routing/additional_edge.go index a1e5fc8564..5f2d42eebc 100644 --- a/routing/additional_edge.go +++ b/routing/additional_edge.go @@ -29,6 +29,11 @@ type AdditionalEdge interface { // EdgePolicy returns the policy of the additional edge. EdgePolicy() *models.CachedEdgePolicy + + // BlindedPayment returns the BlindedPayment that this additional edge + // info was derived from. It will return nil if this edge was not + // derived from a blinded route. + BlindedPayment() *BlindedPayment } // PayloadSizeFunc defines the interface for the payload size function. @@ -60,6 +65,12 @@ func (p *PrivateEdge) IntermediatePayloadSize(amount lnwire.MilliSatoshi, return hop.PayloadSize(channelID) } +// BlindedPayment is a no-op for a PrivateEdge since it is not associated with +// a blinded payment. This will thus return nil. +func (p *PrivateEdge) BlindedPayment() *BlindedPayment { + return nil +} + // BlindedEdge implements the AdditionalEdge interface. Blinded hops are viewed // as additional edges because they are appended at the end of a normal route. type BlindedEdge struct { @@ -120,6 +131,12 @@ func (b *BlindedEdge) IntermediatePayloadSize(_ lnwire.MilliSatoshi, _ uint32, return hop.PayloadSize(0) } +// BlindedPayment returns the blinded payment that this edge is associated +// with. +func (b *BlindedEdge) BlindedPayment() *BlindedPayment { + return b.blindedPayment +} + // Compile-time constraints to ensure the PrivateEdge and the BlindedEdge // implement the AdditionalEdge interface. var _ AdditionalEdge = (*PrivateEdge)(nil) From 1ec2a1be1168255174d3793529393876ab97c2cf Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 14 May 2024 12:47:36 +0200 Subject: [PATCH 03/11] routing+refactor: add a constructor for unifiedEdge Add a constructor for unified edge. In upcoming commits, we will add a new member to unifiedEdge and a constructor forces us to not forget to populate a required member. --- routing/unified_edges.go | 53 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/routing/unified_edges.go b/routing/unified_edges.go index 232c92e1c6..61c756ff34 100644 --- a/routing/unified_edges.go +++ b/routing/unified_edges.go @@ -86,12 +86,9 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, inboundFee = models.InboundFee{} } - unifier.edges = append(unifier.edges, &unifiedEdge{ - policy: edge, - capacity: capacity, - hopPayloadSizeFn: hopPayloadSizeFn, - inboundFees: inboundFee, - }) + unifier.edges = append(unifier.edges, newUnifiedEdge( + edge, capacity, inboundFee, hopPayloadSizeFn, + )) } // addGraphPolicies adds all policies that are known for the toNode in the @@ -139,6 +136,19 @@ type unifiedEdge struct { hopPayloadSizeFn PayloadSizeFunc } +// newUnifiedEdge constructs a new unifiedEdge. +func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount, + inboundFees models.InboundFee, + hopPayloadSizeFn PayloadSizeFunc) *unifiedEdge { + + return &unifiedEdge{ + policy: policy, + capacity: capacity, + inboundFees: inboundFees, + hopPayloadSizeFn: hopPayloadSizeFn, + } +} + // amtInRange checks whether an amount falls within the valid range for a // channel. func (u *unifiedEdge) amtInRange(amt lnwire.MilliSatoshi) bool { @@ -292,12 +302,10 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi, maxBandwidth = bandwidth // Update best edge. - bestEdge = &unifiedEdge{ - policy: edge.policy, - capacity: edge.capacity, - hopPayloadSizeFn: edge.hopPayloadSizeFn, - inboundFees: edge.inboundFees, - } + bestEdge = newUnifiedEdge( + edge.policy, edge.capacity, edge.inboundFees, + edge.hopPayloadSizeFn, + ) } return bestEdge @@ -376,10 +384,9 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, } maxFee = fee - bestPolicy = &unifiedEdge{ - policy: edge.policy, - inboundFees: edge.inboundFees, - } + bestPolicy = newUnifiedEdge( + edge.policy, 0, edge.inboundFees, nil, + ) // The payload size function for edges to a connected peer is // always the same hence there is not need to find the maximum. @@ -404,15 +411,13 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, // chance for this node pair. But this is all only needed for nodes that // have distinct policies for channels to the same peer. policyCopy := *bestPolicy.policy - modifiedEdge := unifiedEdge{ - policy: &policyCopy, - inboundFees: bestPolicy.inboundFees, - } - modifiedEdge.policy.TimeLockDelta = maxTimelock - modifiedEdge.capacity = maxCapMsat.ToSatoshis() - modifiedEdge.hopPayloadSizeFn = hopPayloadSizeFn + policyCopy.TimeLockDelta = maxTimelock + modifiedEdge := newUnifiedEdge( + &policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees, + hopPayloadSizeFn, + ) - return &modifiedEdge + return modifiedEdge } // minAmt returns the minimum amount that can be forwarded on this connection. From 925b68c1ed1de530fd35b7d6341e5bd3d7df0b54 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 14 May 2024 12:53:02 +0200 Subject: [PATCH 04/11] routing: add BlindedPayment to unifiedEdge Later on in this series, we will need to know during path finding if an edge we are traversing was derived from a blinded payment path. In preparation for that, we add a BlindedPayment member to the `unifiedEdge` struct. The reason we will need this later on is because: In the case where we receive multiple blinded paths from the receipient, we will first swap out the final hop node of each path with a single unified target node so that path finding can work as normal. Once we have selected a route though, we will want to know which path an edge belongs to so that we can swap the correct destination node back in. --- routing/pathfind.go | 1 + routing/unified_edges.go | 21 ++++++++++++++------- routing/unified_edges_test.go | 19 ++++++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/routing/pathfind.go b/routing/pathfind.go index fbe4b58308..801725d3e6 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -968,6 +968,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, inboundFee, fakeHopHintCapacity, reverseEdge.edge.IntermediatePayloadSize, + reverseEdge.edge.BlindedPayment(), ) } diff --git a/routing/unified_edges.go b/routing/unified_edges.go index 61c756ff34..d39eda1efd 100644 --- a/routing/unified_edges.go +++ b/routing/unified_edges.go @@ -51,7 +51,8 @@ func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, useInboundFees bool, // incorrectly specified. func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, edge *models.CachedEdgePolicy, inboundFee models.InboundFee, - capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc) { + capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc, + blindedPayment *BlindedPayment) { localChan := fromNode == u.sourceNode @@ -87,7 +88,7 @@ func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, } unifier.edges = append(unifier.edges, newUnifiedEdge( - edge, capacity, inboundFee, hopPayloadSizeFn, + edge, capacity, inboundFee, hopPayloadSizeFn, blindedPayment, )) } @@ -112,7 +113,7 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error { u.addPolicy( channel.OtherNode, channel.InPolicy, inboundFee, - channel.Capacity, defaultHopPayloadSize, + channel.Capacity, defaultHopPayloadSize, nil, ) return nil @@ -134,18 +135,23 @@ type unifiedEdge struct { // is needed because hops of a blinded path differ in their payload // structure compared to cleartext hops. hopPayloadSizeFn PayloadSizeFunc + + // blindedPayment if set, is the BlindedPayment that this edge was + // derived from originally. + blindedPayment *BlindedPayment } // newUnifiedEdge constructs a new unifiedEdge. func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount, - inboundFees models.InboundFee, - hopPayloadSizeFn PayloadSizeFunc) *unifiedEdge { + inboundFees models.InboundFee, hopPayloadSizeFn PayloadSizeFunc, + blindedPayment *BlindedPayment) *unifiedEdge { return &unifiedEdge{ policy: policy, capacity: capacity, inboundFees: inboundFees, hopPayloadSizeFn: hopPayloadSizeFn, + blindedPayment: blindedPayment, } } @@ -304,7 +310,7 @@ func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi, // Update best edge. bestEdge = newUnifiedEdge( edge.policy, edge.capacity, edge.inboundFees, - edge.hopPayloadSizeFn, + edge.hopPayloadSizeFn, edge.blindedPayment, ) } @@ -386,6 +392,7 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, bestPolicy = newUnifiedEdge( edge.policy, 0, edge.inboundFees, nil, + edge.blindedPayment, ) // The payload size function for edges to a connected peer is @@ -414,7 +421,7 @@ func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi, policyCopy.TimeLockDelta = maxTimelock modifiedEdge := newUnifiedEdge( &policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees, - hopPayloadSizeFn, + hopPayloadSizeFn, bestPolicy.blindedPayment, ) return modifiedEdge diff --git a/routing/unified_edges_test.go b/routing/unified_edges_test.go index 7b1650c025..82605e9b37 100644 --- a/routing/unified_edges_test.go +++ b/routing/unified_edges_test.go @@ -59,37 +59,37 @@ func TestNodeEdgeUnifier(t *testing.T) { unifierFilled := newNodeEdgeUnifier(source, toNode, false, nil) unifierFilled.addPolicy( - fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil, ) unifierFilled.addPolicy( - fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, + fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, nil, ) unifierNoCapacity := newNodeEdgeUnifier(source, toNode, false, nil) unifierNoCapacity.addPolicy( - fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, 0, defaultHopPayloadSize, nil, ) unifierNoCapacity.addPolicy( - fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize, + fromNode, &p2, inboundFee2, 0, defaultHopPayloadSize, nil, ) unifierNoInfo := newNodeEdgeUnifier(source, toNode, false, nil) unifierNoInfo.addPolicy( fromNode, &models.CachedEdgePolicy{}, models.InboundFee{}, - 0, defaultHopPayloadSize, + 0, defaultHopPayloadSize, nil, ) unifierInboundFee := newNodeEdgeUnifier(source, toNode, true, nil) unifierInboundFee.addPolicy( - fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil, ) unifierInboundFee.addPolicy( - fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, + fromNode, &p2, inboundFee2, c2, defaultHopPayloadSize, nil, ) unifierLocal := newNodeEdgeUnifier(fromNode, toNode, true, nil) unifierLocal.addPolicy( - fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFee1, c1, defaultHopPayloadSize, nil, ) inboundFeeZero := models.InboundFee{} @@ -98,10 +98,11 @@ func TestNodeEdgeUnifier(t *testing.T) { } unifierNegInboundFee := newNodeEdgeUnifier(source, toNode, true, nil) unifierNegInboundFee.addPolicy( - fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize, + fromNode, &p1, inboundFeeZero, c1, defaultHopPayloadSize, nil, ) unifierNegInboundFee.addPolicy( fromNode, &p2, inboundFeeNegative, c2, defaultHopPayloadSize, + nil, ) tests := []struct { From ad0905f10e394a27e9a3c4717ffce32de0fcba2d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 2 May 2024 14:22:34 +0200 Subject: [PATCH 05/11] record+htlcswitch: convert BlindedRouteData fields to optional For the final hop in a blinded route, the SCID and RelayInfo fields will _not_ be set. So these fields need to be converted to optional records. The existing BlindedRouteData constructor is also renamed to `NewNonFinalBlindedRouteData` in preparation for a `NewFinalBlindedRouteData` constructor which will be used to construct the blinded data for the final hop which will contain a much smaller set of data. The SCID and RelayInfo parameters of the constructor are left as non-pointers in order to force the caller to set them in the case that the constructor is called for non-final nodes. The other option would be to create a single constructor where all parameters are optional but I think this makes it easier for the caller to make a mistake. --- htlcswitch/hop/iterator.go | 31 +++++++++++++++++++--- htlcswitch/hop/iterator_test.go | 2 +- htlcswitch/hop/payload_test.go | 12 ++++----- itest/lnd_route_blinding_test.go | 4 +-- record/blinded_data.go | 44 +++++++++++++++++++++++--------- record/blinded_data_test.go | 6 ++--- 6 files changed, 71 insertions(+), 28 deletions(-) diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index a89f1b7347..ddfbe5934f 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -360,6 +360,7 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, if err != nil { return nil, err } + // Validate the data in the blinded route against our incoming htlc's // information. if err := ValidateBlindedRouteData( @@ -368,9 +369,31 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, return nil, err } + // Exit early if this onion is for the exit hop of the route since + // route blinding receives are not yet supported. + if isFinalHop { + return nil, fmt.Errorf("being the final hop in a blinded " + + "path is not yet supported") + } + + // At this point, we know we are a forwarding node for this onion + // and so we expect the relay info and next SCID fields to be set. + relayInfo, err := routeData.RelayInfo.UnwrapOrErr( + fmt.Errorf("relay info not set for non-final blinded hop"), + ) + if err != nil { + return nil, err + } + + nextSCID, err := routeData.ShortChannelID.UnwrapOrErr( + fmt.Errorf("next SCID not set for non-final blinded hop"), + ) + if err != nil { + return nil, err + } + fwdAmt, err := calculateForwardingAmount( - b.IncomingAmount, routeData.RelayInfo.Val.BaseFee, - routeData.RelayInfo.Val.FeeRate, + b.IncomingAmount, relayInfo.Val.BaseFee, relayInfo.Val.FeeRate, ) if err != nil { return nil, err @@ -400,10 +423,10 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, } return &ForwardingInfo{ - NextHop: routeData.ShortChannelID.Val, + NextHop: nextSCID.Val, AmountToForward: fwdAmt, OutgoingCTLV: b.IncomingCltv - uint32( - routeData.RelayInfo.Val.CltvExpiryDelta, + relayInfo.Val.CltvExpiryDelta, ), // Remap from blinding override type to blinding point type. NextBlinding: tlv.SomeRecordT( diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go index e69361b0a4..9995c71baf 100644 --- a/htlcswitch/hop/iterator_test.go +++ b/htlcswitch/hop/iterator_test.go @@ -186,7 +186,7 @@ func TestDecryptAndValidateFwdInfo(t *testing.T) { // Encode valid blinding data that we'll fake decrypting for our test. maxCltv := 1000 - blindedData := record.NewBlindedRouteData( + blindedData := record.NewNonFinalBlindedRouteData( lnwire.NewShortChanIDFromInt(1500), nil, record.PaymentRelayInfo{ CltvExpiryDelta: 10, diff --git a/htlcswitch/hop/payload_test.go b/htlcswitch/hop/payload_test.go index c22144d7b8..7398813a3e 100644 --- a/htlcswitch/hop/payload_test.go +++ b/htlcswitch/hop/payload_test.go @@ -646,7 +646,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }{ { name: "max cltv expired", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -663,7 +663,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "zero max cltv", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -682,7 +682,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "amount below minimum", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -699,7 +699,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "valid, no features", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -714,7 +714,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "unknown features", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{}, @@ -738,7 +738,7 @@ func TestValidateBlindedRouteData(t *testing.T) { }, { name: "valid data", - data: record.NewBlindedRouteData( + data: record.NewNonFinalBlindedRouteData( scid, nil, record.PaymentRelayInfo{ diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 5c32a85a39..514e856d89 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -676,7 +676,7 @@ func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge, // Encode the route's blinded data and include it in the // blinded hop. - payload := record.NewBlindedRouteData( + payload := record.NewNonFinalBlindedRouteData( scid, nil, *relayInfo, constraints, nil, ) payloadBytes, err := record.EncodeBlindedRouteData(payload) @@ -739,7 +739,7 @@ func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge, // node ID here so that it _looks like_ a valid // forwarding hop (though in reality it's the last // hop). - record.NewBlindedRouteData( + record.NewNonFinalBlindedRouteData( lnwire.NewShortChanIDFromInt(100), nil, record.PaymentRelayInfo{}, nil, nil, ), diff --git a/record/blinded_data.go b/record/blinded_data.go index 7990fa7388..e133a47639 100644 --- a/record/blinded_data.go +++ b/record/blinded_data.go @@ -15,7 +15,7 @@ import ( // forwarding information. type BlindedRouteData struct { // ShortChannelID is the channel ID of the next hop. - ShortChannelID tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID] + ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID] // NextBlindingOverride is a blinding point that should be switched // in for the next hop. This is used to combine two blinded paths into @@ -24,7 +24,7 @@ type BlindedRouteData struct { NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey] // RelayInfo provides the relay parameters for the hop. - RelayInfo tlv.RecordT[tlv.TlvType10, PaymentRelayInfo] + RelayInfo tlv.OptionalRecordT[tlv.TlvType10, PaymentRelayInfo] // Constraints provides the payment relay constraints for the hop. Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints] @@ -33,16 +33,20 @@ type BlindedRouteData struct { Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector] } -// NewBlindedRouteData creates the data that's provided for hops within a -// blinded route. -func NewBlindedRouteData(chanID lnwire.ShortChannelID, +// NewNonFinalBlindedRouteData creates the data that's provided for hops within +// a blinded route. +func NewNonFinalBlindedRouteData(chanID lnwire.ShortChannelID, blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo, constraints *PaymentConstraints, features *lnwire.FeatureVector) *BlindedRouteData { info := &BlindedRouteData{ - ShortChannelID: tlv.NewRecordT[tlv.TlvType2](chanID), - RelayInfo: tlv.NewRecordT[tlv.TlvType10](relayInfo), + ShortChannelID: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType2](chanID), + ), + RelayInfo: tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType10](relayInfo), + ), } if blindingOverride != nil { @@ -69,7 +73,9 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { var ( d BlindedRouteData + scid = d.ShortChannelID.Zero() blindingOverride = d.NextBlindingOverride.Zero() + relayInfo = d.RelayInfo.Zero() constraints = d.Constraints.Zero() features = d.Features.Zero() ) @@ -80,19 +86,25 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { } typeMap, err := tlvRecords.ExtractRecords( - &d.ShortChannelID, - &blindingOverride, &d.RelayInfo.Val, &constraints, - &features, + &scid, &blindingOverride, &relayInfo, &constraints, &features, ) if err != nil { return nil, err } + if val, ok := typeMap[d.ShortChannelID.TlvType()]; ok && val == nil { + d.ShortChannelID = tlv.SomeRecordT(scid) + } + val, ok := typeMap[d.NextBlindingOverride.TlvType()] if ok && val == nil { d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride) } + if val, ok := typeMap[d.RelayInfo.TlvType()]; ok && val == nil { + d.RelayInfo = tlv.SomeRecordT(relayInfo) + } + if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil { d.Constraints = tlv.SomeRecordT(constraints) } @@ -111,7 +123,11 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { recordProducers = make([]tlv.RecordProducer, 0, 5) ) - recordProducers = append(recordProducers, &data.ShortChannelID) + data.ShortChannelID.WhenSome(func(scid tlv.RecordT[tlv.TlvType2, + lnwire.ShortChannelID]) { + + recordProducers = append(recordProducers, &scid) + }) data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) { @@ -119,7 +135,11 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { recordProducers = append(recordProducers, &pk) }) - recordProducers = append(recordProducers, &data.RelayInfo.Val) + data.RelayInfo.WhenSome(func(r tlv.RecordT[tlv.TlvType10, + PaymentRelayInfo]) { + + recordProducers = append(recordProducers, &r) + }) data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12, PaymentConstraints]) { diff --git a/record/blinded_data_test.go b/record/blinded_data_test.go index f8e95cdcc0..2394c8ac97 100644 --- a/record/blinded_data_test.go +++ b/record/blinded_data_test.go @@ -101,7 +101,7 @@ func TestBlindedDataEncoding(t *testing.T) { } } - encodedData := NewBlindedRouteData( + encodedData := NewNonFinalBlindedRouteData( channelID, pubkey(t), info, constraints, testCase.features, ) @@ -134,7 +134,7 @@ func TestBlindingSpecTestVectors(t *testing.T) { }{ { encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", - expectedPaymentData: NewBlindedRouteData( + expectedPaymentData: NewNonFinalBlindedRouteData( lnwire.ShortChannelID{ BlockHeight: 0, TxIndex: 0, @@ -158,7 +158,7 @@ func TestBlindingSpecTestVectors(t *testing.T) { }, { encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", - expectedPaymentData: NewBlindedRouteData( + expectedPaymentData: NewNonFinalBlindedRouteData( lnwire.ShortChannelID{ TxPosition: 1105, }, From 15f3cce27d10e9a2e4743619154224b655e42053 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 2 May 2024 14:28:00 +0200 Subject: [PATCH 06/11] record: add PathID to BlindedRouteData Add the PathID (tlv type 6) field to BlindedRouteData. This will be used for the final hop of a blinded route. A new constructor is also added for BlindedRouteData which can specifically be used for the final hop. --- record/blinded_data.go | 37 +++++++++++++++++++++++++- record/blinded_data_test.go | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/record/blinded_data.go b/record/blinded_data.go index e133a47639..16ff6bd5a4 100644 --- a/record/blinded_data.go +++ b/record/blinded_data.go @@ -17,6 +17,11 @@ type BlindedRouteData struct { // ShortChannelID is the channel ID of the next hop. ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID] + // PathID is a secret set of bytes that the blinded path creator will + // set so that they can check the value on decryption to ensure that the + // path they created was used for the intended purpose. + PathID tlv.OptionalRecordT[tlv.TlvType6, []byte] + // NextBlindingOverride is a blinding point that should be switched // in for the next hop. This is used to combine two blinded paths into // one (which primarily is used in onion messaging, but in theory @@ -68,12 +73,33 @@ func NewNonFinalBlindedRouteData(chanID lnwire.ShortChannelID, return info } +// NewFinalHopBlindedRouteData creates the data that's provided for the final +// hop in a blinded route. +func NewFinalHopBlindedRouteData(constraints *PaymentConstraints, + pathID []byte) *BlindedRouteData { + + var data BlindedRouteData + if pathID != nil { + data.PathID = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6](pathID), + ) + } + + if constraints != nil { + data.Constraints = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType12](*constraints)) + } + + return &data +} + // DecodeBlindedRouteData decodes the data provided within a blinded route. func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { var ( d BlindedRouteData scid = d.ShortChannelID.Zero() + pathID = d.PathID.Zero() blindingOverride = d.NextBlindingOverride.Zero() relayInfo = d.RelayInfo.Zero() constraints = d.Constraints.Zero() @@ -86,7 +112,8 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { } typeMap, err := tlvRecords.ExtractRecords( - &scid, &blindingOverride, &relayInfo, &constraints, &features, + &scid, &pathID, &blindingOverride, &relayInfo, &constraints, + &features, ) if err != nil { return nil, err @@ -96,6 +123,10 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { d.ShortChannelID = tlv.SomeRecordT(scid) } + if val, ok := typeMap[d.PathID.TlvType()]; ok && val == nil { + d.PathID = tlv.SomeRecordT(pathID) + } + val, ok := typeMap[d.NextBlindingOverride.TlvType()] if ok && val == nil { d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride) @@ -129,6 +160,10 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { recordProducers = append(recordProducers, &scid) }) + data.PathID.WhenSome(func(pathID tlv.RecordT[tlv.TlvType6, []byte]) { + recordProducers = append(recordProducers, &pathID) + }) + data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) { diff --git a/record/blinded_data_test.go b/record/blinded_data_test.go index 2394c8ac97..bc1e706ef7 100644 --- a/record/blinded_data_test.go +++ b/record/blinded_data_test.go @@ -118,6 +118,59 @@ func TestBlindedDataEncoding(t *testing.T) { } } +// TestBlindedDataFinalHopEncoding tests the encoding and decoding of a blinded +// data blob intended for the final hop of a blinded path where only the pathID +// will potentially be set. +func TestBlindedDataFinalHopEncoding(t *testing.T) { + tests := []struct { + name string + pathID []byte + constraints bool + }{ + { + name: "with path ID", + pathID: []byte{1, 2, 3, 4, 5, 6}, + }, + { + name: "with no path ID", + pathID: nil, + }, + { + name: "with path ID and constraints", + pathID: []byte{1, 2, 3, 4, 5, 6}, + constraints: true, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + var constraints *PaymentConstraints + if test.constraints { + constraints = &PaymentConstraints{ + MaxCltvExpiry: 4, + HtlcMinimumMsat: 5, + } + } + + encodedData := NewFinalHopBlindedRouteData( + constraints, test.pathID, + ) + + encoded, err := EncodeBlindedRouteData(encodedData) + require.NoError(t, err) + + b := bytes.NewBuffer(encoded) + decodedData, err := DecodeBlindedRouteData(b) + require.NoError(t, err) + + require.Equal(t, encodedData, decodedData) + }) + } +} + // TestBlindedRouteVectors tests encoding/decoding of the test vectors for // blinded route data provided in the specification. // From 9ada4a9068438768ceb80ec9e0654d0bc8acad38 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Thu, 2 May 2024 14:35:34 +0200 Subject: [PATCH 07/11] record: add Padding field to BlindedRouteData When we start creating blinded paths to ourselves, we will want to be able to pad the data for each hop so that the `encrypted_recipient_data` for each hop is the same. We add a `PadBy` method that allows a caller to add a certain number of bytes to the padding field. Note that adding n bytes won't always mean that the encoded payload will increase by size n since there will be overhead for the type and lenght fields for the new TLV field. This will also be the case when the number of bytes added results in a BigSize bucket jump for TLV length field. The responsibility of ensuring that the final payloads are the same size is left to the caller who may need to call PadBy iteratively to achieve the goal. I decided to leave this to the caller since doing this at the actual TLV level will be quite intrusive & I think it is uneccessary to touch that code for this unique use case. --- record/blinded_data.go | 38 +++++++++++++++--- record/blinded_data_test.go | 79 ++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 6 deletions(-) diff --git a/record/blinded_data.go b/record/blinded_data.go index 16ff6bd5a4..7b7081b3be 100644 --- a/record/blinded_data.go +++ b/record/blinded_data.go @@ -14,6 +14,11 @@ import ( // route encrypted data blob that is created by the recipient to provide // forwarding information. type BlindedRouteData struct { + // Padding is an optional set of bytes that a recipient can use to pad + // the data so that the encrypted recipient data blobs are all the same + // length. + Padding tlv.OptionalRecordT[tlv.TlvType1, []byte] + // ShortChannelID is the channel ID of the next hop. ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID] @@ -98,6 +103,7 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { var ( d BlindedRouteData + padding = d.Padding.Zero() scid = d.ShortChannelID.Zero() pathID = d.PathID.Zero() blindingOverride = d.NextBlindingOverride.Zero() @@ -112,13 +118,18 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { } typeMap, err := tlvRecords.ExtractRecords( - &scid, &pathID, &blindingOverride, &relayInfo, &constraints, - &features, + &padding, &scid, &pathID, &blindingOverride, &relayInfo, + &constraints, &features, ) if err != nil { return nil, err } + val, ok := typeMap[d.Padding.TlvType()] + if ok && val == nil { + d.Padding = tlv.SomeRecordT(padding) + } + if val, ok := typeMap[d.ShortChannelID.TlvType()]; ok && val == nil { d.ShortChannelID = tlv.SomeRecordT(scid) } @@ -127,7 +138,7 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { d.PathID = tlv.SomeRecordT(pathID) } - val, ok := typeMap[d.NextBlindingOverride.TlvType()] + val, ok = typeMap[d.NextBlindingOverride.TlvType()] if ok && val == nil { d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride) } @@ -154,6 +165,10 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { recordProducers = make([]tlv.RecordProducer, 0, 5) ) + data.Padding.WhenSome(func(p tlv.RecordT[tlv.TlvType1, []byte]) { + recordProducers = append(recordProducers, &p) + }) + data.ShortChannelID.WhenSome(func(scid tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID]) { @@ -195,6 +210,19 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { return e[:], nil } +// PadBy adds "n" padding bytes to the BlindedRouteData using the Padding field. +// Callers should be aware that the total payload size will change by more than +// "n" since the "n" bytes will be prefixed by BigSize type and length fields. +// Callers may need to call PadBy iteratively until each encrypted data packet +// is the same size and so each call will overwrite the Padding record. +// Note that calling PadBy with an n value of 0 will still result in a zero +// length TLV entry being added. +func (b *BlindedRouteData) PadBy(n int) { + b.Padding = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType1](make([]byte, n)), + ) +} + // PaymentRelayInfo describes the relay policy for a blinded path. type PaymentRelayInfo struct { // CltvExpiryDelta is the expiry delta for the payment. @@ -208,8 +236,8 @@ type PaymentRelayInfo struct { BaseFee uint32 } -// newPaymentRelayRecord creates a tlv.Record that encodes the payment relay -// (type 10) type for an encrypted blob payload. +// Record creates a tlv.Record that encodes the payment relay (type 10) type for +// an encrypted blob payload. func (i *PaymentRelayInfo) Record() tlv.Record { return tlv.MakeDynamicRecord( 10, &i, func() uint64 { diff --git a/record/blinded_data_test.go b/record/blinded_data_test.go index bc1e706ef7..88ab9cddd6 100644 --- a/record/blinded_data_test.go +++ b/record/blinded_data_test.go @@ -171,6 +171,74 @@ func TestBlindedDataFinalHopEncoding(t *testing.T) { } } +// TestBlindedRouteDataPadding tests the PadBy method of BlindedRouteData. +func TestBlindedRouteDataPadding(t *testing.T) { + newBlindedRouteData := func() *BlindedRouteData { + channelID := lnwire.NewShortChanIDFromInt(1) + info := PaymentRelayInfo{ + FeeRate: 2, + CltvExpiryDelta: 3, + BaseFee: 30, + } + + constraints := &PaymentConstraints{ + MaxCltvExpiry: 4, + HtlcMinimumMsat: 100, + } + + return NewNonFinalBlindedRouteData( + channelID, pubkey(t), info, constraints, nil, + ) + } + + tests := []struct { + name string + paddingSize int + expectedSizeIncrease uint64 + }{ + { + // Calling PadBy with an n value of 0 in the case where + // there is not yet a padding field will result in a + // zero length TLV entry being added. This will add 2 + // bytes for the type and length fields. + name: "no extra padding", + expectedSizeIncrease: 2, + }, + { + name: "small padding (length " + + "field of 1 byte)", + paddingSize: 200, + expectedSizeIncrease: 202, + }, + { + name: "medium padding (length field " + + "of 3 bytes)", + paddingSize: 256, + expectedSizeIncrease: 260, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + data := newBlindedRouteData() + + prePaddingEncoding, err := EncodeBlindedRouteData(data) + require.NoError(t, err) + + data.PadBy(test.paddingSize) + + postPaddingEncoding, err := EncodeBlindedRouteData(data) + require.NoError(t, err) + + require.EqualValues( + t, test.expectedSizeIncrease, + len(postPaddingEncoding)- + len(prePaddingEncoding), + ) + }) + } +} + // TestBlindedRouteVectors tests encoding/decoding of the test vectors for // blinded route data provided in the specification. // @@ -184,6 +252,7 @@ func TestBlindingSpecTestVectors(t *testing.T) { tests := []struct { encoded string expectedPaymentData *BlindedRouteData + expectedPadding int }{ { encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", @@ -208,6 +277,7 @@ func TestBlindingSpecTestVectors(t *testing.T) { lnwire.Features, ), ), + expectedPadding: 26, }, { encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", @@ -228,7 +298,8 @@ func TestBlindingSpecTestVectors(t *testing.T) { lnwire.NewFeatureVector( lnwire.NewRawFeatureVector(), lnwire.Features, - )), + ), + ), }, } @@ -242,6 +313,12 @@ func TestBlindingSpecTestVectors(t *testing.T) { decodedRoute, err := DecodeBlindedRouteData(buff) require.NoError(t, err) + if test.expectedPadding != 0 { + test.expectedPaymentData.PadBy( + test.expectedPadding, + ) + } + require.Equal( t, test.expectedPaymentData, decodedRoute, ) From f6a54c2ede43bae26cd70f4740548902659b0bb2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 10 Apr 2024 12:54:26 +0200 Subject: [PATCH 08/11] zpay: encoding and decoding of a BlindedPaymentPath In this commit, the ability is added to encode blinded payment paths and add them to a Bolt 11 invoice. --- zpay32/blinded_path.go | 246 ++++++++++++++++++++++++++++++++++++++ zpay32/decode.go | 44 ++++++- zpay32/encode.go | 23 ++++ zpay32/invoice.go | 30 ++++- zpay32/invoice_test.go | 260 +++++++++++++++++++++++------------------ 5 files changed, 486 insertions(+), 117 deletions(-) create mode 100644 zpay32/blinded_path.go diff --git a/zpay32/blinded_path.go b/zpay32/blinded_path.go new file mode 100644 index 0000000000..128a05e4ba --- /dev/null +++ b/zpay32/blinded_path.go @@ -0,0 +1,246 @@ +package zpay32 + +import ( + "encoding/binary" + "fmt" + "io" + "math" + + "github.com/btcsuite/btcd/btcec/v2" + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // relayInfoSize is the number of bytes that the relay info of a blinded + // payment will occupy. + // base fee: 4 bytes + // prop fee: 4 bytes + // cltv delta: 2 bytes + // min htlc: 8 bytes + // max htlc: 8 bytes + relayInfoSize = 26 + + // maxNumHopsPerPath is the maximum number of blinded path hops that can + // be included in a single encoded blinded path. This is calculated + // based on the `data_length` limit of 638 bytes for any tagged field in + // a BOLT 11 invoice along with the estimated number of bytes required + // for encoding the most minimal blinded path hop. See the [bLIP + // proposal](https://github.com/lightning/blips/pull/39) for a detailed + // calculation. + maxNumHopsPerPath = 7 +) + +// BlindedPaymentPath holds all the information a payer needs to know about a +// blinded path to a receiver of a payment. +type BlindedPaymentPath struct { + // FeeBaseMsat is the total base fee for the path in milli-satoshis. + FeeBaseMsat uint32 + + // FeeRate is the total fee rate for the path in parts per million. + FeeRate uint32 + + // CltvExpiryDelta is the total CLTV delta to apply to the path. + CltvExpiryDelta uint16 + + // HTLCMinMsat is the minimum number of milli-satoshis that any hop in + // the path will route. + HTLCMinMsat uint64 + + // HTLCMaxMsat is the maximum number of milli-satoshis that a hop in the + // path will route. + HTLCMaxMsat uint64 + + // Features is the feature bit vector for the path. + Features *lnwire.FeatureVector + + // FirstEphemeralBlindingPoint is the blinding point to send to the + // introduction node. It will be used by the introduction node to derive + // a shared secret with the receiver which can then be used to decode + // the encrypted payload from the receiver. + FirstEphemeralBlindingPoint *btcec.PublicKey + + // Hops is the blinded path. The first hop is the introduction node and + // so the BlindedNodeID of this hop will be the real node ID. + Hops []*sphinx.BlindedHopInfo +} + +// DecodeBlindedPayment attempts to parse a BlindedPaymentPath from the passed +// reader. +func DecodeBlindedPayment(r io.Reader) (*BlindedPaymentPath, error) { + var relayInfo [relayInfoSize]byte + n, err := r.Read(relayInfo[:]) + if err != nil { + return nil, err + } + if n != relayInfoSize { + return nil, fmt.Errorf("unable to read %d relay info bytes "+ + "off of the given stream: %w", relayInfoSize, err) + } + + var payment BlindedPaymentPath + + // Parse the relay info fields. + payment.FeeBaseMsat = binary.BigEndian.Uint32(relayInfo[:4]) + payment.FeeRate = binary.BigEndian.Uint32(relayInfo[4:8]) + payment.CltvExpiryDelta = binary.BigEndian.Uint16(relayInfo[8:10]) + payment.HTLCMinMsat = binary.BigEndian.Uint64(relayInfo[10:18]) + payment.HTLCMaxMsat = binary.BigEndian.Uint64(relayInfo[18:]) + + // Parse the feature bit vector. + f := lnwire.EmptyFeatureVector() + err = f.Decode(r) + if err != nil { + return nil, err + } + payment.Features = f + + // Parse the first ephemeral blinding point. + var blindingPointBytes [btcec.PubKeyBytesLenCompressed]byte + _, err = r.Read(blindingPointBytes[:]) + if err != nil { + return nil, err + } + + blinding, err := btcec.ParsePubKey(blindingPointBytes[:]) + if err != nil { + return nil, err + } + payment.FirstEphemeralBlindingPoint = blinding + + // Read the one byte hop number. + var numHops [1]byte + _, err = r.Read(numHops[:]) + if err != nil { + return nil, err + } + + payment.Hops = make([]*sphinx.BlindedHopInfo, int(numHops[0])) + + // Parse each hop. + for i := 0; i < len(payment.Hops); i++ { + hop, err := DecodeBlindedHop(r) + if err != nil { + return nil, err + } + + payment.Hops[i] = hop + } + + return &payment, nil +} + +// Encode serialises the BlindedPaymentPath and writes the bytes to the passed +// writer. +// 1) The first 26 bytes contain the relay info: +// - Base Fee in msat: uint32 (4 bytes). +// - Proportional Fee in PPM: uint32 (4 bytes). +// - CLTV expiry delta: uint16 (2 bytes). +// - HTLC min msat: uint64 (8 bytes). +// - HTLC max msat: uint64 (8 bytes). +// +// 2) Feature bit vector length (2 bytes). +// 3) Feature bit vector (can be zero length). +// 4) First blinding point: 33 bytes. +// 5) Number of hops: 1 byte. +// 6) Encoded BlindedHops. +func (p *BlindedPaymentPath) Encode(w io.Writer) error { + var relayInfo [26]byte + binary.BigEndian.PutUint32(relayInfo[:4], p.FeeBaseMsat) + binary.BigEndian.PutUint32(relayInfo[4:8], p.FeeRate) + binary.BigEndian.PutUint16(relayInfo[8:10], p.CltvExpiryDelta) + binary.BigEndian.PutUint64(relayInfo[10:18], p.HTLCMinMsat) + binary.BigEndian.PutUint64(relayInfo[18:], p.HTLCMaxMsat) + + _, err := w.Write(relayInfo[:]) + if err != nil { + return err + } + + err = p.Features.Encode(w) + if err != nil { + return err + } + + _, err = w.Write(p.FirstEphemeralBlindingPoint.SerializeCompressed()) + if err != nil { + return err + } + + numHops := len(p.Hops) + if numHops > maxNumHopsPerPath { + return fmt.Errorf("the number of hops, %d, exceeds the "+ + "maximum of %d", numHops, maxNumHopsPerPath) + } + + _, err = w.Write([]byte{byte(numHops)}) + if err != nil { + return err + } + + for _, hop := range p.Hops { + err = EncodeBlindedHop(w, hop) + if err != nil { + return err + } + } + + return nil +} + +// DecodeBlindedHop reads a sphinx.BlindedHopInfo from the passed reader. +func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) { + var nodeIDBytes [btcec.PubKeyBytesLenCompressed]byte + _, err := r.Read(nodeIDBytes[:]) + if err != nil { + return nil, err + } + + nodeID, err := btcec.ParsePubKey(nodeIDBytes[:]) + if err != nil { + return nil, err + } + + dataLen, err := tlv.ReadVarInt(r, &[8]byte{}) + if err != nil { + return nil, err + } + + encryptedData := make([]byte, dataLen) + _, err = r.Read(encryptedData) + if err != nil { + return nil, err + } + + return &sphinx.BlindedHopInfo{ + BlindedNodePub: nodeID, + CipherText: encryptedData, + }, nil +} + +// EncodeBlindedHop writes the passed BlindedHopInfo to the given writer. +// +// 1) Blinded node pub key: 33 bytes +// 2) Cipher text length: BigSize +// 3) Cipher text. +func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error { + _, err := w.Write(hop.BlindedNodePub.SerializeCompressed()) + if err != nil { + return err + } + + if len(hop.CipherText) > math.MaxUint16 { + return fmt.Errorf("encrypted recipient data can not exceed a "+ + "length of %d bytes", math.MaxUint16) + } + + err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{}) + if err != nil { + return err + } + + _, err = w.Write(hop.CipherText) + + return err +} diff --git a/zpay32/decode.go b/zpay32/decode.go index d37a34cf9d..59bfccf001 100644 --- a/zpay32/decode.go +++ b/zpay32/decode.go @@ -215,6 +215,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.PaymentHash, err = parse32Bytes(base32Data) + case fieldTypeS: if invoice.PaymentAddr != nil { // We skip the field if we have already seen a @@ -223,6 +224,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.PaymentAddr, err = parse32Bytes(base32Data) + case fieldTypeD: if invoice.Description != nil { // We skip the field if we have already seen a @@ -231,6 +233,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.Description, err = parseDescription(base32Data) + case fieldTypeM: if invoice.Metadata != nil { // We skip the field if we have already seen a @@ -248,6 +251,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.Destination, err = parseDestination(base32Data) + case fieldTypeH: if invoice.DescriptionHash != nil { // We skip the field if we have already seen a @@ -256,6 +260,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.DescriptionHash, err = parse32Bytes(base32Data) + case fieldTypeX: if invoice.expiry != nil { // We skip the field if we have already seen a @@ -264,6 +269,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.expiry, err = parseExpiry(base32Data) + case fieldTypeC: if invoice.minFinalCLTVExpiry != nil { // We skip the field if we have already seen a @@ -271,7 +277,9 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er continue } - invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data) + invoice.minFinalCLTVExpiry, err = + parseMinFinalCLTVExpiry(base32Data) + case fieldTypeF: if invoice.FallbackAddr != nil { // We skip the field if we have already seen a @@ -279,7 +287,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er continue } - invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net) + invoice.FallbackAddr, err = parseFallbackAddr( + base32Data, net, + ) + case fieldTypeR: // An `r` field can be included in an invoice multiple // times, so we won't skip it if we have already seen @@ -289,7 +300,10 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er return err } - invoice.RouteHints = append(invoice.RouteHints, routeHint) + invoice.RouteHints = append( + invoice.RouteHints, routeHint, + ) + case fieldType9: if invoice.Features != nil { // We skip the field if we have already seen a @@ -298,6 +312,19 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.Features, err = parseFeatures(base32Data) + + case fieldTypeB: + blindedPaymentPath, err := parseBlindedPaymentPath( + base32Data, + ) + if err != nil { + return err + } + + invoice.BlindedPaymentPaths = append( + invoice.BlindedPaymentPaths, blindedPaymentPath, + ) + default: // Ignore unknown type. } @@ -495,6 +522,17 @@ func parseRouteHint(data []byte) ([]HopHint, error) { return routeHint, nil } +// parseBlindedPaymentPath attempts to parse a BlindedPaymentPath from the given +// byte slice. +func parseBlindedPaymentPath(data []byte) (*BlindedPaymentPath, error) { + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + return DecodeBlindedPayment(bytes.NewReader(base256Data)) +} + // parseFeatures decodes any feature bits directly from the base32 // representation. func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { diff --git a/zpay32/encode.go b/zpay32/encode.go index bf544d062e..130abdcfd5 100644 --- a/zpay32/encode.go +++ b/zpay32/encode.go @@ -260,6 +260,29 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { } } + for _, path := range invoice.BlindedPaymentPaths { + var buf bytes.Buffer + + err := path.Encode(&buf) + if err != nil { + return err + } + + blindedPathBase32, err := bech32.ConvertBits( + buf.Bytes(), 8, 5, true, + ) + if err != nil { + return err + } + + err = writeTaggedField( + bufferBase32, fieldTypeB, blindedPathBase32, + ) + if err != nil { + return err + } + } + if invoice.Destination != nil { // Convert 33 byte pubkey to 53 5-bit groups. pubKeyBase32, err := bech32.ConvertBits( diff --git a/zpay32/invoice.go b/zpay32/invoice.go index f23992ec9b..2afc59d95a 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -76,6 +76,10 @@ const ( // probing the recipient. fieldTypeS = 16 + // fieldTypeB contains blinded payment path information. This field may + // be repeated to include multiple blinded payment paths in the invoice. + fieldTypeB = 20 + // maxInvoiceLength is the maximum total length an invoice can have. // This is chosen to be the maximum number of bytes that can fit into a // single QR code: https://en.wikipedia.org/wiki/QR_code#Storage @@ -180,9 +184,17 @@ type Invoice struct { // hint can be individually used to reach the destination. These usually // represent private routes. // - // NOTE: This is optional. + // NOTE: This is optional and should not be set at the same time as + // BlindedPaymentPaths. RouteHints [][]HopHint + // BlindedPaymentPaths is a set of blinded payment paths that can be + // used to find the payment receiver. + // + // NOTE: This is optional and should not be set at the same time as + // RouteHints. + BlindedPaymentPaths []*BlindedPaymentPath + // Features represents an optional field used to signal optional or // required support for features by the receiver. Features *lnwire.FeatureVector @@ -263,6 +275,15 @@ func RouteHint(routeHint []HopHint) func(*Invoice) { } } +// WithBlindedPaymentPath is a functional option that allows a caller of +// NewInvoice to attach a blinded payment path to the invoice. The option can +// be used multiple times to attach multiple paths. +func WithBlindedPaymentPath(p *BlindedPaymentPath) func(*Invoice) { + return func(i *Invoice) { + i.BlindedPaymentPaths = append(i.BlindedPaymentPaths, p) + } +} + // Features is a functional option that allows callers of NewInvoice to set the // desired feature bits that are advertised on the invoice. If this option is // not used, an empty feature vector will automatically be populated. @@ -355,6 +376,13 @@ func validateInvoice(invoice *Invoice) error { return fmt.Errorf("no payment hash found") } + if len(invoice.RouteHints) != 0 && + len(invoice.BlindedPaymentPaths) != 0 { + + return fmt.Errorf("cannot have both route hints and blinded " + + "payment paths") + } + // Either Description or DescriptionHash must be set, not both. if invoice.Description != nil && invoice.DescriptionHash != nil { return fmt.Errorf("both description and description hash set") diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index a360ed2a07..006b4fb6d7 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -7,7 +7,6 @@ import ( "bytes" "encoding/hex" "fmt" - "reflect" "strings" "testing" "time" @@ -17,7 +16,9 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" ) var ( @@ -116,6 +117,62 @@ var ( // Must be initialized in init(). testDescriptionHash [32]byte + + testBlindedPK1Bytes, _ = hex.DecodeString("03f3311e948feb5115242c4e39" + + "6c81c448ab7ee5fd24c4e24e66c73533cc4f98b8") + testBlindedHopPK1, _ = btcec.ParsePubKey(testBlindedPK1Bytes) + testBlindedPK2Bytes, _ = hex.DecodeString("03a8c97ed5cd40d474e4ef18c8" + + "99854b25e5070106504cb225e6d2c112d61a805e") + testBlindedHopPK2, _ = btcec.ParsePubKey(testBlindedPK2Bytes) + testBlindedPK3Bytes, _ = hex.DecodeString("0220293926219d8efe733336e2" + + "b674570dd96aa763acb3564e6e367b384d861a0a") + testBlindedHopPK3, _ = btcec.ParsePubKey(testBlindedPK3Bytes) + testBlindedPK4Bytes, _ = hex.DecodeString("02c75eb336a038294eaaf76015" + + "8b2e851c3c0937262e35401ae64a1bee71a2e40c") + testBlindedHopPK4, _ = btcec.ParsePubKey(testBlindedPK4Bytes) + + blindedPath1 = &BlindedPaymentPath{ + FeeBaseMsat: 40, + FeeRate: 20, + CltvExpiryDelta: 130, + HTLCMinMsat: 2, + HTLCMaxMsat: 100, + Features: lnwire.EmptyFeatureVector(), + FirstEphemeralBlindingPoint: testBlindedHopPK1, + Hops: []*sphinx.BlindedHopInfo{ + { + BlindedNodePub: testBlindedHopPK2, + CipherText: []byte{1, 2, 3, 4, 5}, + }, + { + BlindedNodePub: testBlindedHopPK3, + CipherText: []byte{5, 4, 3, 2, 1}, + }, + { + BlindedNodePub: testBlindedHopPK4, + CipherText: []byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, + }, + }, + }, + } + + blindedPath2 = &BlindedPaymentPath{ + FeeBaseMsat: 4, + FeeRate: 2, + CltvExpiryDelta: 10, + HTLCMinMsat: 0, + HTLCMaxMsat: 10, + Features: lnwire.EmptyFeatureVector(), + FirstEphemeralBlindingPoint: testBlindedHopPK4, + Hops: []*sphinx.BlindedHopInfo{ + { + BlindedNodePub: testBlindedHopPK3, + CipherText: []byte{1, 2, 3, 4, 5}, + }, + }, + } ) func init() { @@ -125,6 +182,8 @@ func init() { // TestDecodeEncode tests that an encoded invoice gets decoded into the expected // Invoice object, and that reencoding the decoded invoice gets us back to the // original encoded string. +// +//nolint:lll func TestDecodeEncode(t *testing.T) { t.Parallel() @@ -673,52 +732,77 @@ func TestDecodeEncode(t *testing.T) { i.Destination = nil }, }, + { + // Invoice with blinded payment paths. + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f", + valid: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + MilliSat: &testMillisat20mBTC, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCupOfCoffee, + Destination: testPubKey, + Features: emptyFeatures, + BlindedPaymentPaths: []*BlindedPaymentPath{ + blindedPath1, + blindedPath2, + }, + } + }, + beforeEncoding: func(i *Invoice) { + // Since this destination pubkey was recovered + // from the signature, we must set it nil before + // encoding to get back the same invoice string. + i.Destination = nil + }, + }, } for i, test := range tests { - var decodedInvoice *Invoice - net := &chaincfg.MainNetParams - if test.decodedInvoice != nil { - decodedInvoice = test.decodedInvoice() - net = decodedInvoice.Net - } + test := test - invoice, err := Decode(test.encodedInvoice, net) - if (err == nil) != test.valid { - t.Errorf("Decoding test %d failed: %v", i, err) - return - } + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() - if test.valid { - if err := compareInvoices(decodedInvoice, invoice); err != nil { - t.Errorf("Invoice decoding result %d not as expected: %v", i, err) + var decodedInvoice *Invoice + net := &chaincfg.MainNetParams + if test.decodedInvoice != nil { + decodedInvoice = test.decodedInvoice() + net = decodedInvoice.Net + } + + invoice, err := Decode(test.encodedInvoice, net) + if !test.valid { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, decodedInvoice, invoice) + } + + if test.skipEncoding { return } - } - if test.skipEncoding { - continue - } + if test.beforeEncoding != nil { + test.beforeEncoding(decodedInvoice) + } - if test.beforeEncoding != nil { - test.beforeEncoding(decodedInvoice) - } + if decodedInvoice == nil { + return + } - if decodedInvoice != nil { reencoded, err := decodedInvoice.Encode( testMessageSigner, ) - if (err == nil) != test.valid { - t.Errorf("Encoding test %d failed: %v", i, err) + if !test.valid { + require.Error(t, err) return } - - if test.valid && test.encodedInvoice != reencoded { - t.Errorf("Encoding %d failed, expected %v, got %v", - i, test.encodedInvoice, reencoded) - return - } - } + require.NoError(t, err) + require.Equal(t, test.encodedInvoice, reencoded) + }) } } @@ -805,25 +889,42 @@ func TestNewInvoice(t *testing.T) { valid: true, encodedInvoice: "lnbcrt241pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66df5c8pqjjt4z4ymmuaxfx8eh5v7hmzs3wrfas8m2sz5qz56rw2lxy8mmgm4xln0ha26qkw6u3vhu22pss2udugr9g74c3x20slpcqjgq0el4h6", }, + { + // Mainnet invoice with two blinded paths. + newInvoice: func() (*Invoice, error) { + return NewInvoice(&chaincfg.MainNetParams, + testPaymentHash, + time.Unix(1496314658, 0), + Amount(testMillisat20mBTC), + Description(testCupOfCoffee), + WithBlindedPaymentPath(blindedPath1), + WithBlindedPaymentPath(blindedPath2), + ) + }, + valid: true, + //nolint:lll + encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4js5fdqqqqq2qqqqqpgqyzqqqqqqqqqqqqyqqqqqqqqqqqvsqqqqlnxy0ffrlt2y2jgtzw89kgr3zg4dlwtlfycn3yuek8x5eucnuchqps82xf0m2u6sx5wnjw7xxgnxz5kf09quqsv5zvkgj7d5kpzttp4qz7q5qsyqcyq5pzq2feycsemrh7wvendc4kw3tsmkt25a36ev6kfehrv7ecfkrp5zs9q5zqxqspqtr4avek5quzjn427asptzews5wrczfhychr2sq6ue9phmn35tjqcrspqgpsgpgxquyqjzstpsxsu59zqqqqqpqqqqqqyqq2qqqqqqqqqqqqqqqqqqqqqqqqpgqqqqk8t6endgpc99824amqzk9japgu8synwf3wx4qp4ej2r0h8rghypsqsygpf8ynzr8vwleenxdhzke69wrwed2nk8t9n2e8xudnm8pxcvxs2q5qsyqcyq5y4rdlhtf84f8rgdj34275juwls2ftxtcfh035863q3p9k6s94hpxhdmzfn5gxpsazdznxs56j4vt3fdhe00g9v2l3szher50hp4xlggqkxf77f", + }, } for i, test := range tests { + test := test - invoice, err := test.newInvoice() - if err != nil && !test.valid { - continue - } - encoded, err := invoice.Encode(testMessageSigner) - if (err == nil) != test.valid { - t.Errorf("NewInvoice test %d failed: %v", i, err) - return - } + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() - if test.valid && test.encodedInvoice != encoded { - t.Errorf("Encoding %d failed, expected %v, got %v", - i, test.encodedInvoice, encoded) - return - } + invoice, err := test.newInvoice() + if !test.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + + encoded, err := invoice.Encode(testMessageSigner) + require.NoError(t, err) + + require.Equal(t, test.encodedInvoice, encoded) + }) } } @@ -909,73 +1010,6 @@ func TestInvoiceChecksumMalleability(t *testing.T) { } } -func compareInvoices(expected, actual *Invoice) error { - if !reflect.DeepEqual(expected.Net, actual.Net) { - return fmt.Errorf("expected net %v, got %v", - expected.Net, actual.Net) - } - - if !reflect.DeepEqual(expected.MilliSat, actual.MilliSat) { - return fmt.Errorf("expected milli sat %d, got %d", - *expected.MilliSat, *actual.MilliSat) - } - - if expected.Timestamp != actual.Timestamp { - return fmt.Errorf("expected timestamp %v, got %v", - expected.Timestamp, actual.Timestamp) - } - - if !compareHashes(expected.PaymentHash, actual.PaymentHash) { - return fmt.Errorf("expected payment hash %x, got %x", - *expected.PaymentHash, *actual.PaymentHash) - } - - if !reflect.DeepEqual(expected.Description, actual.Description) { - return fmt.Errorf("expected description \"%s\", got \"%s\"", - *expected.Description, *actual.Description) - } - - if !comparePubkeys(expected.Destination, actual.Destination) { - return fmt.Errorf("expected destination pubkey %x, got %x", - expected.Destination.SerializeCompressed(), - actual.Destination.SerializeCompressed()) - } - - if !compareHashes(expected.DescriptionHash, actual.DescriptionHash) { - return fmt.Errorf("expected description hash %x, got %x", - *expected.DescriptionHash, *actual.DescriptionHash) - } - - if expected.Expiry() != actual.Expiry() { - return fmt.Errorf("expected expiry %d, got %d", - expected.Expiry(), actual.Expiry()) - } - - if !reflect.DeepEqual(expected.FallbackAddr, actual.FallbackAddr) { - return fmt.Errorf("expected FallbackAddr %v, got %v", - expected.FallbackAddr, actual.FallbackAddr) - } - - if len(expected.RouteHints) != len(actual.RouteHints) { - return fmt.Errorf("expected %d RouteHints, got %d", - len(expected.RouteHints), len(actual.RouteHints)) - } - - for i, routeHint := range expected.RouteHints { - err := compareRouteHints(routeHint, actual.RouteHints[i]) - if err != nil { - return err - } - } - - if !reflect.DeepEqual(expected.Features, actual.Features) { - return fmt.Errorf("expected features %v, got %v", - expected.Features, actual.Features) - } - - return nil -} - func comparePubkeys(a, b *btcec.PublicKey) bool { if a == b { return true From 93f89512aeea5599eb50aefc244bc9eb388bb48b Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 10 Apr 2024 12:59:41 +0200 Subject: [PATCH 09/11] lnrpc+rpcserver: Add blinded payment paths to PayReq This commit adds a blinded_paths field to the PayReq proto message. A new helper called `CreateRPCBlindedPayments` is then added to convert the zpay32 type to the existing `lnrpc.BlindedPaymentPath` type and add this to the `PayReq` in the `DecodePayReq` rpc method. --- lnrpc/invoicesrpc/utils.go | 54 +++++ lnrpc/lightning.pb.go | 415 ++++++++++++++++++----------------- lnrpc/lightning.proto | 1 + lnrpc/lightning.swagger.json | 6 + rpcserver.go | 8 + 5 files changed, 283 insertions(+), 201 deletions(-) diff --git a/lnrpc/invoicesrpc/utils.go b/lnrpc/invoicesrpc/utils.go index 70d7740828..2d1507267b 100644 --- a/lnrpc/invoicesrpc/utils.go +++ b/lnrpc/invoicesrpc/utils.go @@ -266,6 +266,60 @@ func CreateRPCRouteHints(routeHints [][]zpay32.HopHint) []*lnrpc.RouteHint { return res } +// CreateRPCBlindedPayments takes a set of zpay32.BlindedPaymentPath and +// converts them into a set of lnrpc.BlindedPaymentPaths. +func CreateRPCBlindedPayments(blindedPaths []*zpay32.BlindedPaymentPath) ( + []*lnrpc.BlindedPaymentPath, error) { + + var res []*lnrpc.BlindedPaymentPath + for _, path := range blindedPaths { + features := path.Features.Features() + var featuresSlice []lnrpc.FeatureBit + for feature := range features { + featuresSlice = append( + featuresSlice, lnrpc.FeatureBit(feature), + ) + } + + if len(path.Hops) == 0 { + return nil, fmt.Errorf("each blinded path must " + + "contain at least one hop") + } + + var hops []*lnrpc.BlindedHop + for _, hop := range path.Hops { + blindedNodeID := hop.BlindedNodePub. + SerializeCompressed() + hops = append(hops, &lnrpc.BlindedHop{ + BlindedNode: blindedNodeID, + EncryptedData: hop.CipherText, + }) + } + + introNode := path.Hops[0].BlindedNodePub + firstBlindingPoint := path.FirstEphemeralBlindingPoint + + blindedPath := &lnrpc.BlindedPath{ + IntroductionNode: introNode.SerializeCompressed(), + BlindingPoint: firstBlindingPoint. + SerializeCompressed(), + BlindedHops: hops, + } + + res = append(res, &lnrpc.BlindedPaymentPath{ + BlindedPath: blindedPath, + BaseFeeMsat: uint64(path.FeeBaseMsat), + ProportionalFeeRate: path.FeeRate, + TotalCltvDelta: uint32(path.CltvExpiryDelta), + HtlcMinMsat: path.HTLCMinMsat, + HtlcMaxMsat: path.HTLCMaxMsat, + Features: featuresSlice, + }) + } + + return res, nil +} + // CreateZpay32HopHints takes in the lnrpc form of route hints and converts them // into an invoice decoded form. func CreateZpay32HopHints(routeHints []*lnrpc.RouteHint) ([][]zpay32.HopHint, error) { diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 305e624124..3cd5d9767d 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -14304,19 +14304,20 @@ type PayReq struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` - PaymentHash string `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` - NumSatoshis int64 `protobuf:"varint,3,opt,name=num_satoshis,json=numSatoshis,proto3" json:"num_satoshis,omitempty"` - Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - Expiry int64 `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"` - Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` - DescriptionHash string `protobuf:"bytes,7,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"` - FallbackAddr string `protobuf:"bytes,8,opt,name=fallback_addr,json=fallbackAddr,proto3" json:"fallback_addr,omitempty"` - CltvExpiry int64 `protobuf:"varint,9,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"` - RouteHints []*RouteHint `protobuf:"bytes,10,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"` - PaymentAddr []byte `protobuf:"bytes,11,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"` - NumMsat int64 `protobuf:"varint,12,opt,name=num_msat,json=numMsat,proto3" json:"num_msat,omitempty"` - Features map[uint32]*Feature `protobuf:"bytes,13,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Destination string `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` + PaymentHash string `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` + NumSatoshis int64 `protobuf:"varint,3,opt,name=num_satoshis,json=numSatoshis,proto3" json:"num_satoshis,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Expiry int64 `protobuf:"varint,5,opt,name=expiry,proto3" json:"expiry,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + DescriptionHash string `protobuf:"bytes,7,opt,name=description_hash,json=descriptionHash,proto3" json:"description_hash,omitempty"` + FallbackAddr string `protobuf:"bytes,8,opt,name=fallback_addr,json=fallbackAddr,proto3" json:"fallback_addr,omitempty"` + CltvExpiry int64 `protobuf:"varint,9,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"` + RouteHints []*RouteHint `protobuf:"bytes,10,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"` + PaymentAddr []byte `protobuf:"bytes,11,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"` + NumMsat int64 `protobuf:"varint,12,opt,name=num_msat,json=numMsat,proto3" json:"num_msat,omitempty"` + Features map[uint32]*Feature `protobuf:"bytes,13,rep,name=features,proto3" json:"features,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + BlindedPaths []*BlindedPaymentPath `protobuf:"bytes,14,rep,name=blinded_paths,json=blindedPaths,proto3" json:"blinded_paths,omitempty"` } func (x *PayReq) Reset() { @@ -14442,6 +14443,13 @@ func (x *PayReq) GetFeatures() map[uint32]*Feature { return nil } +func (x *PayReq) GetBlindedPaths() []*BlindedPaymentPath { + if x != nil { + return x.BlindedPaths + } + return nil +} + type Feature struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -20202,7 +20210,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x75, 0x62, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x27, 0x0a, 0x0c, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x79, 0x52, - 0x65, 0x71, 0x22, 0xb0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, + 0x65, 0x71, 0x22, 0xf0, 0x04, 0x0a, 0x06, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, @@ -20232,7 +20240,11 @@ var file_lightning_proto_rawDesc = []byte{ 0x37, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x61, 0x79, 0x52, 0x65, 0x71, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, - 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, + 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x62, 0x6c, 0x69, 0x6e, + 0x64, 0x65, 0x64, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x64, 0x50, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0c, 0x62, 0x6c, 0x69, 0x6e, + 0x64, 0x65, 0x64, 0x50, 0x61, 0x74, 0x68, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, @@ -21520,192 +21532,193 @@ var file_lightning_proto_depIdxs = []int32{ 38, // 138: lnrpc.AbandonChannelRequest.channel_point:type_name -> lnrpc.ChannelPoint 150, // 139: lnrpc.PayReq.route_hints:type_name -> lnrpc.RouteHint 244, // 140: lnrpc.PayReq.features:type_name -> lnrpc.PayReq.FeaturesEntry - 179, // 141: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport - 38, // 142: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint - 181, // 143: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee - 39, // 144: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint - 11, // 145: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure - 183, // 146: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate - 186, // 147: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent - 38, // 148: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint - 38, // 149: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint - 38, // 150: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint - 193, // 151: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups - 190, // 152: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup - 189, // 153: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup - 193, // 154: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups - 198, // 155: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission - 198, // 156: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission - 245, // 157: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry - 20, // 158: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode - 209, // 159: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate - 211, // 160: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op - 198, // 161: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission - 215, // 162: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth - 216, // 163: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage - 216, // 164: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage - 218, // 165: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration - 219, // 166: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback - 177, // 167: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature - 177, // 168: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature - 4, // 169: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator - 3, // 170: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType - 226, // 171: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 226, // 172: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 229, // 173: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments - 226, // 174: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 226, // 175: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel - 108, // 176: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC - 15, // 177: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState - 113, // 178: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance - 177, // 179: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature - 137, // 180: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric - 177, // 181: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature - 177, // 182: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature - 154, // 183: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState - 177, // 184: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature - 205, // 185: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList - 114, // 186: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest - 117, // 187: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest - 30, // 188: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest - 42, // 189: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest - 46, // 190: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest - 48, // 191: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest - 30, // 192: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest - 44, // 193: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest - 50, // 194: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest - 52, // 195: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest - 54, // 196: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest - 56, // 197: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest - 58, // 198: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest - 74, // 199: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest - 76, // 200: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription - 78, // 201: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest - 80, // 202: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest - 82, // 203: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest - 109, // 204: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest - 63, // 205: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest - 111, // 206: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription - 70, // 207: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest - 96, // 208: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest - 96, // 209: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest - 93, // 210: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest - 106, // 211: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg - 37, // 212: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse - 88, // 213: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest - 171, // 214: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest - 33, // 215: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest - 33, // 216: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest - 35, // 217: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest - 35, // 218: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest - 155, // 219: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice - 160, // 220: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest - 159, // 221: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash - 162, // 222: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription - 175, // 223: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString - 165, // 224: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest - 167, // 225: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest - 168, // 226: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest - 133, // 227: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest - 135, // 228: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest - 138, // 229: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest - 127, // 230: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest - 119, // 231: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest - 139, // 232: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest - 141, // 233: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest - 143, // 234: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription - 173, // 235: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest - 178, // 236: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest - 182, // 237: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest - 185, // 238: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest - 188, // 239: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest - 191, // 240: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest - 192, // 241: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot - 194, // 242: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest - 196, // 243: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription - 199, // 244: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest - 201, // 245: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest - 203, // 246: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest - 206, // 247: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest - 212, // 248: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest - 217, // 249: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse - 25, // 250: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest - 23, // 251: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest - 66, // 252: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest - 21, // 253: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest - 115, // 254: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse - 118, // 255: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse - 31, // 256: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails - 43, // 257: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse - 47, // 258: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse - 49, // 259: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse - 29, // 260: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction - 45, // 261: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse - 51, // 262: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse - 53, // 263: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse - 55, // 264: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse - 57, // 265: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse - 59, // 266: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse - 75, // 267: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse - 77, // 268: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent - 79, // 269: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse - 81, // 270: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse - 83, // 271: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse - 110, // 272: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse - 64, // 273: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse - 112, // 274: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate - 71, // 275: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse - 38, // 276: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint - 97, // 277: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate - 95, // 278: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse - 107, // 279: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp - 36, // 280: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest - 89, // 281: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate - 172, // 282: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse - 34, // 283: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse - 34, // 284: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse - 34, // 285: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse - 34, // 286: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse - 158, // 287: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse - 161, // 288: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse - 155, // 289: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice - 155, // 290: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice - 176, // 291: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq - 166, // 292: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse - 169, // 293: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse - 170, // 294: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse - 134, // 295: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph - 136, // 296: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse - 132, // 297: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge - 128, // 298: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo - 122, // 299: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse - 140, // 300: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo - 142, // 301: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse - 144, // 302: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate - 174, // 303: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse - 180, // 304: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse - 184, // 305: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse - 187, // 306: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse - 189, // 307: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup - 192, // 308: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot - 197, // 309: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse - 195, // 310: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse - 192, // 311: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot - 200, // 312: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse - 202, // 313: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse - 204, // 314: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse - 207, // 315: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse - 213, // 316: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse - 214, // 317: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest - 26, // 318: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse - 24, // 319: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage - 67, // 320: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse - 22, // 321: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse - 254, // [254:322] is the sub-list for method output_type - 186, // [186:254] is the sub-list for method input_type - 186, // [186:186] is the sub-list for extension type_name - 186, // [186:186] is the sub-list for extension extendee - 0, // [0:186] is the sub-list for field type_name + 151, // 141: lnrpc.PayReq.blinded_paths:type_name -> lnrpc.BlindedPaymentPath + 179, // 142: lnrpc.FeeReportResponse.channel_fees:type_name -> lnrpc.ChannelFeeReport + 38, // 143: lnrpc.PolicyUpdateRequest.chan_point:type_name -> lnrpc.ChannelPoint + 181, // 144: lnrpc.PolicyUpdateRequest.inbound_fee:type_name -> lnrpc.InboundFee + 39, // 145: lnrpc.FailedUpdate.outpoint:type_name -> lnrpc.OutPoint + 11, // 146: lnrpc.FailedUpdate.reason:type_name -> lnrpc.UpdateFailure + 183, // 147: lnrpc.PolicyUpdateResponse.failed_updates:type_name -> lnrpc.FailedUpdate + 186, // 148: lnrpc.ForwardingHistoryResponse.forwarding_events:type_name -> lnrpc.ForwardingEvent + 38, // 149: lnrpc.ExportChannelBackupRequest.chan_point:type_name -> lnrpc.ChannelPoint + 38, // 150: lnrpc.ChannelBackup.chan_point:type_name -> lnrpc.ChannelPoint + 38, // 151: lnrpc.MultiChanBackup.chan_points:type_name -> lnrpc.ChannelPoint + 193, // 152: lnrpc.ChanBackupSnapshot.single_chan_backups:type_name -> lnrpc.ChannelBackups + 190, // 153: lnrpc.ChanBackupSnapshot.multi_chan_backup:type_name -> lnrpc.MultiChanBackup + 189, // 154: lnrpc.ChannelBackups.chan_backups:type_name -> lnrpc.ChannelBackup + 193, // 155: lnrpc.RestoreChanBackupRequest.chan_backups:type_name -> lnrpc.ChannelBackups + 198, // 156: lnrpc.BakeMacaroonRequest.permissions:type_name -> lnrpc.MacaroonPermission + 198, // 157: lnrpc.MacaroonPermissionList.permissions:type_name -> lnrpc.MacaroonPermission + 245, // 158: lnrpc.ListPermissionsResponse.method_permissions:type_name -> lnrpc.ListPermissionsResponse.MethodPermissionsEntry + 20, // 159: lnrpc.Failure.code:type_name -> lnrpc.Failure.FailureCode + 209, // 160: lnrpc.Failure.channel_update:type_name -> lnrpc.ChannelUpdate + 211, // 161: lnrpc.MacaroonId.ops:type_name -> lnrpc.Op + 198, // 162: lnrpc.CheckMacPermRequest.permissions:type_name -> lnrpc.MacaroonPermission + 215, // 163: lnrpc.RPCMiddlewareRequest.stream_auth:type_name -> lnrpc.StreamAuth + 216, // 164: lnrpc.RPCMiddlewareRequest.request:type_name -> lnrpc.RPCMessage + 216, // 165: lnrpc.RPCMiddlewareRequest.response:type_name -> lnrpc.RPCMessage + 218, // 166: lnrpc.RPCMiddlewareResponse.register:type_name -> lnrpc.MiddlewareRegistration + 219, // 167: lnrpc.RPCMiddlewareResponse.feedback:type_name -> lnrpc.InterceptFeedback + 177, // 168: lnrpc.Peer.FeaturesEntry.value:type_name -> lnrpc.Feature + 177, // 169: lnrpc.GetInfoResponse.FeaturesEntry.value:type_name -> lnrpc.Feature + 4, // 170: lnrpc.PendingChannelsResponse.PendingChannel.initiator:type_name -> lnrpc.Initiator + 3, // 171: lnrpc.PendingChannelsResponse.PendingChannel.commitment_type:type_name -> lnrpc.CommitmentType + 226, // 172: lnrpc.PendingChannelsResponse.PendingOpenChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 226, // 173: lnrpc.PendingChannelsResponse.WaitingCloseChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 229, // 174: lnrpc.PendingChannelsResponse.WaitingCloseChannel.commitments:type_name -> lnrpc.PendingChannelsResponse.Commitments + 226, // 175: lnrpc.PendingChannelsResponse.ClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 226, // 176: lnrpc.PendingChannelsResponse.ForceClosedChannel.channel:type_name -> lnrpc.PendingChannelsResponse.PendingChannel + 108, // 177: lnrpc.PendingChannelsResponse.ForceClosedChannel.pending_htlcs:type_name -> lnrpc.PendingHTLC + 15, // 178: lnrpc.PendingChannelsResponse.ForceClosedChannel.anchor:type_name -> lnrpc.PendingChannelsResponse.ForceClosedChannel.AnchorState + 113, // 179: lnrpc.WalletBalanceResponse.AccountBalanceEntry.value:type_name -> lnrpc.WalletAccountBalance + 177, // 180: lnrpc.LightningNode.FeaturesEntry.value:type_name -> lnrpc.Feature + 137, // 181: lnrpc.NodeMetricsResponse.BetweennessCentralityEntry.value:type_name -> lnrpc.FloatMetric + 177, // 182: lnrpc.NodeUpdate.FeaturesEntry.value:type_name -> lnrpc.Feature + 177, // 183: lnrpc.Invoice.FeaturesEntry.value:type_name -> lnrpc.Feature + 154, // 184: lnrpc.Invoice.AmpInvoiceStateEntry.value:type_name -> lnrpc.AMPInvoiceState + 177, // 185: lnrpc.PayReq.FeaturesEntry.value:type_name -> lnrpc.Feature + 205, // 186: lnrpc.ListPermissionsResponse.MethodPermissionsEntry.value:type_name -> lnrpc.MacaroonPermissionList + 114, // 187: lnrpc.Lightning.WalletBalance:input_type -> lnrpc.WalletBalanceRequest + 117, // 188: lnrpc.Lightning.ChannelBalance:input_type -> lnrpc.ChannelBalanceRequest + 30, // 189: lnrpc.Lightning.GetTransactions:input_type -> lnrpc.GetTransactionsRequest + 42, // 190: lnrpc.Lightning.EstimateFee:input_type -> lnrpc.EstimateFeeRequest + 46, // 191: lnrpc.Lightning.SendCoins:input_type -> lnrpc.SendCoinsRequest + 48, // 192: lnrpc.Lightning.ListUnspent:input_type -> lnrpc.ListUnspentRequest + 30, // 193: lnrpc.Lightning.SubscribeTransactions:input_type -> lnrpc.GetTransactionsRequest + 44, // 194: lnrpc.Lightning.SendMany:input_type -> lnrpc.SendManyRequest + 50, // 195: lnrpc.Lightning.NewAddress:input_type -> lnrpc.NewAddressRequest + 52, // 196: lnrpc.Lightning.SignMessage:input_type -> lnrpc.SignMessageRequest + 54, // 197: lnrpc.Lightning.VerifyMessage:input_type -> lnrpc.VerifyMessageRequest + 56, // 198: lnrpc.Lightning.ConnectPeer:input_type -> lnrpc.ConnectPeerRequest + 58, // 199: lnrpc.Lightning.DisconnectPeer:input_type -> lnrpc.DisconnectPeerRequest + 74, // 200: lnrpc.Lightning.ListPeers:input_type -> lnrpc.ListPeersRequest + 76, // 201: lnrpc.Lightning.SubscribePeerEvents:input_type -> lnrpc.PeerEventSubscription + 78, // 202: lnrpc.Lightning.GetInfo:input_type -> lnrpc.GetInfoRequest + 80, // 203: lnrpc.Lightning.GetDebugInfo:input_type -> lnrpc.GetDebugInfoRequest + 82, // 204: lnrpc.Lightning.GetRecoveryInfo:input_type -> lnrpc.GetRecoveryInfoRequest + 109, // 205: lnrpc.Lightning.PendingChannels:input_type -> lnrpc.PendingChannelsRequest + 63, // 206: lnrpc.Lightning.ListChannels:input_type -> lnrpc.ListChannelsRequest + 111, // 207: lnrpc.Lightning.SubscribeChannelEvents:input_type -> lnrpc.ChannelEventSubscription + 70, // 208: lnrpc.Lightning.ClosedChannels:input_type -> lnrpc.ClosedChannelsRequest + 96, // 209: lnrpc.Lightning.OpenChannelSync:input_type -> lnrpc.OpenChannelRequest + 96, // 210: lnrpc.Lightning.OpenChannel:input_type -> lnrpc.OpenChannelRequest + 93, // 211: lnrpc.Lightning.BatchOpenChannel:input_type -> lnrpc.BatchOpenChannelRequest + 106, // 212: lnrpc.Lightning.FundingStateStep:input_type -> lnrpc.FundingTransitionMsg + 37, // 213: lnrpc.Lightning.ChannelAcceptor:input_type -> lnrpc.ChannelAcceptResponse + 88, // 214: lnrpc.Lightning.CloseChannel:input_type -> lnrpc.CloseChannelRequest + 171, // 215: lnrpc.Lightning.AbandonChannel:input_type -> lnrpc.AbandonChannelRequest + 33, // 216: lnrpc.Lightning.SendPayment:input_type -> lnrpc.SendRequest + 33, // 217: lnrpc.Lightning.SendPaymentSync:input_type -> lnrpc.SendRequest + 35, // 218: lnrpc.Lightning.SendToRoute:input_type -> lnrpc.SendToRouteRequest + 35, // 219: lnrpc.Lightning.SendToRouteSync:input_type -> lnrpc.SendToRouteRequest + 155, // 220: lnrpc.Lightning.AddInvoice:input_type -> lnrpc.Invoice + 160, // 221: lnrpc.Lightning.ListInvoices:input_type -> lnrpc.ListInvoiceRequest + 159, // 222: lnrpc.Lightning.LookupInvoice:input_type -> lnrpc.PaymentHash + 162, // 223: lnrpc.Lightning.SubscribeInvoices:input_type -> lnrpc.InvoiceSubscription + 175, // 224: lnrpc.Lightning.DecodePayReq:input_type -> lnrpc.PayReqString + 165, // 225: lnrpc.Lightning.ListPayments:input_type -> lnrpc.ListPaymentsRequest + 167, // 226: lnrpc.Lightning.DeletePayment:input_type -> lnrpc.DeletePaymentRequest + 168, // 227: lnrpc.Lightning.DeleteAllPayments:input_type -> lnrpc.DeleteAllPaymentsRequest + 133, // 228: lnrpc.Lightning.DescribeGraph:input_type -> lnrpc.ChannelGraphRequest + 135, // 229: lnrpc.Lightning.GetNodeMetrics:input_type -> lnrpc.NodeMetricsRequest + 138, // 230: lnrpc.Lightning.GetChanInfo:input_type -> lnrpc.ChanInfoRequest + 127, // 231: lnrpc.Lightning.GetNodeInfo:input_type -> lnrpc.NodeInfoRequest + 119, // 232: lnrpc.Lightning.QueryRoutes:input_type -> lnrpc.QueryRoutesRequest + 139, // 233: lnrpc.Lightning.GetNetworkInfo:input_type -> lnrpc.NetworkInfoRequest + 141, // 234: lnrpc.Lightning.StopDaemon:input_type -> lnrpc.StopRequest + 143, // 235: lnrpc.Lightning.SubscribeChannelGraph:input_type -> lnrpc.GraphTopologySubscription + 173, // 236: lnrpc.Lightning.DebugLevel:input_type -> lnrpc.DebugLevelRequest + 178, // 237: lnrpc.Lightning.FeeReport:input_type -> lnrpc.FeeReportRequest + 182, // 238: lnrpc.Lightning.UpdateChannelPolicy:input_type -> lnrpc.PolicyUpdateRequest + 185, // 239: lnrpc.Lightning.ForwardingHistory:input_type -> lnrpc.ForwardingHistoryRequest + 188, // 240: lnrpc.Lightning.ExportChannelBackup:input_type -> lnrpc.ExportChannelBackupRequest + 191, // 241: lnrpc.Lightning.ExportAllChannelBackups:input_type -> lnrpc.ChanBackupExportRequest + 192, // 242: lnrpc.Lightning.VerifyChanBackup:input_type -> lnrpc.ChanBackupSnapshot + 194, // 243: lnrpc.Lightning.RestoreChannelBackups:input_type -> lnrpc.RestoreChanBackupRequest + 196, // 244: lnrpc.Lightning.SubscribeChannelBackups:input_type -> lnrpc.ChannelBackupSubscription + 199, // 245: lnrpc.Lightning.BakeMacaroon:input_type -> lnrpc.BakeMacaroonRequest + 201, // 246: lnrpc.Lightning.ListMacaroonIDs:input_type -> lnrpc.ListMacaroonIDsRequest + 203, // 247: lnrpc.Lightning.DeleteMacaroonID:input_type -> lnrpc.DeleteMacaroonIDRequest + 206, // 248: lnrpc.Lightning.ListPermissions:input_type -> lnrpc.ListPermissionsRequest + 212, // 249: lnrpc.Lightning.CheckMacaroonPermissions:input_type -> lnrpc.CheckMacPermRequest + 217, // 250: lnrpc.Lightning.RegisterRPCMiddleware:input_type -> lnrpc.RPCMiddlewareResponse + 25, // 251: lnrpc.Lightning.SendCustomMessage:input_type -> lnrpc.SendCustomMessageRequest + 23, // 252: lnrpc.Lightning.SubscribeCustomMessages:input_type -> lnrpc.SubscribeCustomMessagesRequest + 66, // 253: lnrpc.Lightning.ListAliases:input_type -> lnrpc.ListAliasesRequest + 21, // 254: lnrpc.Lightning.LookupHtlcResolution:input_type -> lnrpc.LookupHtlcResolutionRequest + 115, // 255: lnrpc.Lightning.WalletBalance:output_type -> lnrpc.WalletBalanceResponse + 118, // 256: lnrpc.Lightning.ChannelBalance:output_type -> lnrpc.ChannelBalanceResponse + 31, // 257: lnrpc.Lightning.GetTransactions:output_type -> lnrpc.TransactionDetails + 43, // 258: lnrpc.Lightning.EstimateFee:output_type -> lnrpc.EstimateFeeResponse + 47, // 259: lnrpc.Lightning.SendCoins:output_type -> lnrpc.SendCoinsResponse + 49, // 260: lnrpc.Lightning.ListUnspent:output_type -> lnrpc.ListUnspentResponse + 29, // 261: lnrpc.Lightning.SubscribeTransactions:output_type -> lnrpc.Transaction + 45, // 262: lnrpc.Lightning.SendMany:output_type -> lnrpc.SendManyResponse + 51, // 263: lnrpc.Lightning.NewAddress:output_type -> lnrpc.NewAddressResponse + 53, // 264: lnrpc.Lightning.SignMessage:output_type -> lnrpc.SignMessageResponse + 55, // 265: lnrpc.Lightning.VerifyMessage:output_type -> lnrpc.VerifyMessageResponse + 57, // 266: lnrpc.Lightning.ConnectPeer:output_type -> lnrpc.ConnectPeerResponse + 59, // 267: lnrpc.Lightning.DisconnectPeer:output_type -> lnrpc.DisconnectPeerResponse + 75, // 268: lnrpc.Lightning.ListPeers:output_type -> lnrpc.ListPeersResponse + 77, // 269: lnrpc.Lightning.SubscribePeerEvents:output_type -> lnrpc.PeerEvent + 79, // 270: lnrpc.Lightning.GetInfo:output_type -> lnrpc.GetInfoResponse + 81, // 271: lnrpc.Lightning.GetDebugInfo:output_type -> lnrpc.GetDebugInfoResponse + 83, // 272: lnrpc.Lightning.GetRecoveryInfo:output_type -> lnrpc.GetRecoveryInfoResponse + 110, // 273: lnrpc.Lightning.PendingChannels:output_type -> lnrpc.PendingChannelsResponse + 64, // 274: lnrpc.Lightning.ListChannels:output_type -> lnrpc.ListChannelsResponse + 112, // 275: lnrpc.Lightning.SubscribeChannelEvents:output_type -> lnrpc.ChannelEventUpdate + 71, // 276: lnrpc.Lightning.ClosedChannels:output_type -> lnrpc.ClosedChannelsResponse + 38, // 277: lnrpc.Lightning.OpenChannelSync:output_type -> lnrpc.ChannelPoint + 97, // 278: lnrpc.Lightning.OpenChannel:output_type -> lnrpc.OpenStatusUpdate + 95, // 279: lnrpc.Lightning.BatchOpenChannel:output_type -> lnrpc.BatchOpenChannelResponse + 107, // 280: lnrpc.Lightning.FundingStateStep:output_type -> lnrpc.FundingStateStepResp + 36, // 281: lnrpc.Lightning.ChannelAcceptor:output_type -> lnrpc.ChannelAcceptRequest + 89, // 282: lnrpc.Lightning.CloseChannel:output_type -> lnrpc.CloseStatusUpdate + 172, // 283: lnrpc.Lightning.AbandonChannel:output_type -> lnrpc.AbandonChannelResponse + 34, // 284: lnrpc.Lightning.SendPayment:output_type -> lnrpc.SendResponse + 34, // 285: lnrpc.Lightning.SendPaymentSync:output_type -> lnrpc.SendResponse + 34, // 286: lnrpc.Lightning.SendToRoute:output_type -> lnrpc.SendResponse + 34, // 287: lnrpc.Lightning.SendToRouteSync:output_type -> lnrpc.SendResponse + 158, // 288: lnrpc.Lightning.AddInvoice:output_type -> lnrpc.AddInvoiceResponse + 161, // 289: lnrpc.Lightning.ListInvoices:output_type -> lnrpc.ListInvoiceResponse + 155, // 290: lnrpc.Lightning.LookupInvoice:output_type -> lnrpc.Invoice + 155, // 291: lnrpc.Lightning.SubscribeInvoices:output_type -> lnrpc.Invoice + 176, // 292: lnrpc.Lightning.DecodePayReq:output_type -> lnrpc.PayReq + 166, // 293: lnrpc.Lightning.ListPayments:output_type -> lnrpc.ListPaymentsResponse + 169, // 294: lnrpc.Lightning.DeletePayment:output_type -> lnrpc.DeletePaymentResponse + 170, // 295: lnrpc.Lightning.DeleteAllPayments:output_type -> lnrpc.DeleteAllPaymentsResponse + 134, // 296: lnrpc.Lightning.DescribeGraph:output_type -> lnrpc.ChannelGraph + 136, // 297: lnrpc.Lightning.GetNodeMetrics:output_type -> lnrpc.NodeMetricsResponse + 132, // 298: lnrpc.Lightning.GetChanInfo:output_type -> lnrpc.ChannelEdge + 128, // 299: lnrpc.Lightning.GetNodeInfo:output_type -> lnrpc.NodeInfo + 122, // 300: lnrpc.Lightning.QueryRoutes:output_type -> lnrpc.QueryRoutesResponse + 140, // 301: lnrpc.Lightning.GetNetworkInfo:output_type -> lnrpc.NetworkInfo + 142, // 302: lnrpc.Lightning.StopDaemon:output_type -> lnrpc.StopResponse + 144, // 303: lnrpc.Lightning.SubscribeChannelGraph:output_type -> lnrpc.GraphTopologyUpdate + 174, // 304: lnrpc.Lightning.DebugLevel:output_type -> lnrpc.DebugLevelResponse + 180, // 305: lnrpc.Lightning.FeeReport:output_type -> lnrpc.FeeReportResponse + 184, // 306: lnrpc.Lightning.UpdateChannelPolicy:output_type -> lnrpc.PolicyUpdateResponse + 187, // 307: lnrpc.Lightning.ForwardingHistory:output_type -> lnrpc.ForwardingHistoryResponse + 189, // 308: lnrpc.Lightning.ExportChannelBackup:output_type -> lnrpc.ChannelBackup + 192, // 309: lnrpc.Lightning.ExportAllChannelBackups:output_type -> lnrpc.ChanBackupSnapshot + 197, // 310: lnrpc.Lightning.VerifyChanBackup:output_type -> lnrpc.VerifyChanBackupResponse + 195, // 311: lnrpc.Lightning.RestoreChannelBackups:output_type -> lnrpc.RestoreBackupResponse + 192, // 312: lnrpc.Lightning.SubscribeChannelBackups:output_type -> lnrpc.ChanBackupSnapshot + 200, // 313: lnrpc.Lightning.BakeMacaroon:output_type -> lnrpc.BakeMacaroonResponse + 202, // 314: lnrpc.Lightning.ListMacaroonIDs:output_type -> lnrpc.ListMacaroonIDsResponse + 204, // 315: lnrpc.Lightning.DeleteMacaroonID:output_type -> lnrpc.DeleteMacaroonIDResponse + 207, // 316: lnrpc.Lightning.ListPermissions:output_type -> lnrpc.ListPermissionsResponse + 213, // 317: lnrpc.Lightning.CheckMacaroonPermissions:output_type -> lnrpc.CheckMacPermResponse + 214, // 318: lnrpc.Lightning.RegisterRPCMiddleware:output_type -> lnrpc.RPCMiddlewareRequest + 26, // 319: lnrpc.Lightning.SendCustomMessage:output_type -> lnrpc.SendCustomMessageResponse + 24, // 320: lnrpc.Lightning.SubscribeCustomMessages:output_type -> lnrpc.CustomMessage + 67, // 321: lnrpc.Lightning.ListAliases:output_type -> lnrpc.ListAliasesResponse + 22, // 322: lnrpc.Lightning.LookupHtlcResolution:output_type -> lnrpc.LookupHtlcResolutionResponse + 255, // [255:323] is the sub-list for method output_type + 187, // [187:255] is the sub-list for method input_type + 187, // [187:187] is the sub-list for extension type_name + 187, // [187:187] is the sub-list for extension extendee + 0, // [0:187] is the sub-list for field type_name } func init() { file_lightning_proto_init() } diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index a13ca95c5f..5b912e9c00 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -4289,6 +4289,7 @@ message PayReq { bytes payment_addr = 11; int64 num_msat = 12; map features = 13; + repeated BlindedPaymentPath blinded_paths = 14; } enum FeatureBit { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index ae59f4a196..a0c6761ffd 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -6299,6 +6299,12 @@ "additionalProperties": { "$ref": "#/definitions/lnrpcFeature" } + }, + "blinded_paths": { + "type": "array", + "items": { + "$ref": "#/definitions/lnrpcBlindedPaymentPath" + } } } }, diff --git a/rpcserver.go b/rpcserver.go index 4cd0fc4587..a306d4fd29 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6971,6 +6971,13 @@ func (r *rpcServer) DecodePayReq(ctx context.Context, // Convert between the `lnrpc` and `routing` types. routeHints := invoicesrpc.CreateRPCRouteHints(payReq.RouteHints) + blindedPaymentPaths, err := invoicesrpc.CreateRPCBlindedPayments( + payReq.BlindedPaymentPaths, + ) + if err != nil { + return nil, err + } + var amtSat, amtMsat int64 if payReq.MilliSat != nil { amtSat = int64(payReq.MilliSat.ToSatoshis()) @@ -6996,6 +7003,7 @@ func (r *rpcServer) DecodePayReq(ctx context.Context, Expiry: expiry, CltvExpiry: int64(payReq.MinFinalCLTVExpiry()), RouteHints: routeHints, + BlindedPaths: blindedPaymentPaths, PaymentAddr: paymentAddr, Features: invoicesrpc.CreateRPCFeatures(payReq.Features), }, nil From cd3da40fb99f0e43738576699650d472e4045c1d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 7 May 2024 12:16:17 +0200 Subject: [PATCH 10/11] routing: dont include final hop cltv in blinded path Only include the final hop's cltv delta in the total timelock calculation if the route does not include a blinded path. This is because in a blinded path, the final hops final cltv delta will be included in the blinded path's accumlated cltv delta value. With this commit, we remove the responsibility of remembering not to set the `finalHop.cltvDelta` from the caller of `newRoute`. The relevant test is updated accordingly. --- cmd/lncli/cmd_payments.go | 16 ++++++++++++++-- routing/pathfind.go | 33 ++++++++++++++++++++++++++------- routing/pathfind_test.go | 13 ++++++++----- zpay32/invoice.go | 4 ++++ 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/cmd/lncli/cmd_payments.go b/cmd/lncli/cmd_payments.go index ec6a04b37c..228dd3415f 100644 --- a/cmd/lncli/cmd_payments.go +++ b/cmd/lncli/cmd_payments.go @@ -1099,8 +1099,13 @@ var queryRoutesCommand = cli.Command{ }, cli.Int64Flag{ Name: "final_cltv_delta", - Usage: "(optional) number of blocks the last hop has to reveal " + - "the preimage", + Usage: "(optional) number of blocks the last hop has " + + "to reveal the preimage. Note that this " + + "should not be set in the case where the " + + "path includes a blinded path since in " + + "that case, the receiver will already have " + + "accounted for this value in the " + + "blinded_cltv value", }, cli.BoolFlag{ Name: "use_mc", @@ -1238,6 +1243,13 @@ func parseBlindedPaymentParameters(ctx *cli.Context) ( return nil, nil } + // If a blinded path has been provided, then the final_cltv_delta flag + // should not be provided since this value will be ignored. + if ctx.IsSet("final_cltv_delta") { + return nil, fmt.Errorf("`final_cltv_delta` should not be " + + "provided if a blinded path is provided") + } + // If any one of our blinding related flags is set, we expect the // full set to be set and we'll error out accordingly. introNode, err := route.NewVertexFromStr( diff --git a/routing/pathfind.go b/routing/pathfind.go index 801725d3e6..208a550858 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -96,9 +96,17 @@ type edgePolicyWithSource struct { // such as amounts and cltvs, as well as more complex features like destination // custom records and payment address. type finalHopParams struct { - amt lnwire.MilliSatoshi - totalAmt lnwire.MilliSatoshi - cltvDelta uint16 + amt lnwire.MilliSatoshi + totalAmt lnwire.MilliSatoshi + + // cltvDelta is the final hop's minimum CLTV expiry delta. + // + // NOTE that in the case of paying to a blinded path, this value will + // be set to a duplicate of the blinded path's accumulated CLTV value. + // We would then only need to use this value in the case where the + // introduction node of the path is also the destination node. + cltvDelta uint16 + records record.CustomSet paymentAddr *[32]byte @@ -190,10 +198,21 @@ func newRoute(sourceVertex route.Vertex, // reporting through RPC. Set to zero for the final hop. fee = 0 - // As this is the last hop, we'll use the specified - // final CLTV delta value instead of the value from the - // last link in the route. - totalTimeLock += uint32(finalHop.cltvDelta) + // Only include the final hop CLTV delta in the total + // time lock value if this is not a route to a blinded + // path. For blinded paths, the total time-lock from the + // whole path will be deduced from the introduction + // node's CLTV delta. The exception is for the case + // where the final hop is the blinded path introduction + // node. + if blindedPath == nil || + len(blindedPath.BlindedHops) == 1 { + + // As this is the last hop, we'll use the + // specified final CLTV delta value instead of + // the value from the last link in the route. + totalTimeLock += uint32(finalHop.cltvDelta) + } outgoingTimeLock = totalTimeLock // Attach any custom records to the final hop. diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index fd9839dbaf..0f2a2659b1 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -3322,16 +3322,19 @@ func TestBlindedRouteConstruction(t *testing.T) { ToNodeFeatures: tlvFeatures, } - // Create final hop parameters for payment amount = 110. Note - // that final cltv delta is not set because blinded paths - // include this final delta in their aggregate delta. A - // sender-set delta may be added to account for block arrival - // during payment, but we do not set it in this test. + // Create final hop parameters for payment amount = 110. totalAmt lnwire.MilliSatoshi = 110 finalHopParams = finalHopParams{ amt: totalAmt, totalAmt: totalAmt, metadata: metadata, + + // We set a CLTV delta here just to test that this will + // be ignored by newRoute since this is a blinded path + // where the accumulated CLTV delta for the route + // communicated in the blinded path should be assumed to + // include the CLTV delta of the final hop. + cltvDelta: MaxCLTVDelta, } ) diff --git a/zpay32/invoice.go b/zpay32/invoice.go index 2afc59d95a..8b1457ab43 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -156,6 +156,10 @@ type Invoice struct { // This field is un-exported and can only be read by the // MinFinalCLTVExpiry() method. By forcing callers to read via this // method, we can easily enforce the default if not specified. + // + // NOTE: this field is ignored in the case that the invoice contains + // blinded paths since then the final minimum cltv expiry delta is + // expected to be included in the route's accumulated CLTV delta value. minFinalCLTVExpiry *uint64 // Description is a short description of the purpose of this invoice. From 85ddffb17dd0e253f3422de95b622e6e88bda528 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Tue, 14 May 2024 15:39:09 +0200 Subject: [PATCH 11/11] docs: update release notes --- docs/release-notes/release-notes-0.18.3.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.18.3.md b/docs/release-notes/release-notes-0.18.3.md index 3017b1ffd2..9245903954 100644 --- a/docs/release-notes/release-notes-0.18.3.md +++ b/docs/release-notes/release-notes-0.18.3.md @@ -89,6 +89,9 @@ channel. We will still wait for the channel to have at least one confirmation and so the main change here is that we don't error out for such a case. +* [Groundwork](https://github.com/lightningnetwork/lnd/pull/8752) in preparation + for implementing route blinding receives. + ## Testing ## Database