routing: include route blinding fields in blinded portion of path

This commit updates route construction to backfill the fields
required for payment to blinded paths and set amount to forward
and expiry fields to zero for intermediate hops (as is instructed
in the route blinding specification).

We could attempt to do this in the first pass, but that loop
relies on fields like amount to forward and expiry to calculate
each hop backwards, so we keep it simple (stupid) and post
processes the blinded portion, since it's computationally cheap
and more readable.
This commit is contained in:
Carla Kirk-Cohen 2022-12-20 14:03:58 -05:00 committed by Olaoluwa Osuntokun
parent c9609b8214
commit 014683ee66
4 changed files with 306 additions and 16 deletions

View file

@ -1,6 +1,7 @@
package routing
import (
"bytes"
"container/heap"
"errors"
"fmt"
@ -110,14 +111,23 @@ type finalHopParams struct {
// assuming the destination's feature vector signals support, otherwise this
// method will fail. If the route is too long, or the selected path cannot
// support the fully payment including fees, then a non-nil error is returned.
// If the route is to a blinded path, the blindedPath parameter is used to
// back fill additional fields that are required for a blinded payment. This is
// done in a separate pass to keep our route construction simple, as blinded
// paths require zero expiry and amount values for intermediate hops (which
// makes calculating the totals during route construction difficult if we
// include blinded paths on the first pass).
//
// NOTE: The passed slice of ChannelHops MUST be sorted in forward order: from
// the source to the target node of the path finding attempt. It is assumed that
// any feature vectors on all hops have been validated for transitive
// dependencies.
// NOTE: If a non-nil blinded path is provided it is assumed to have been
// validated by the caller.
func newRoute(sourceVertex route.Vertex,
pathEdges []*channeldb.CachedEdgePolicy, currentHeight uint32,
finalHop finalHopParams) (*route.Route, error) {
finalHop finalHopParams, blindedPath *sphinx.BlindedPath) (
*route.Route, error) {
var (
hops []*route.Hop
@ -146,13 +156,14 @@ func newRoute(sourceVertex route.Vertex,
// contributions from the preceding hops back to the sender as
// we compute the route in reverse.
var (
amtToForward lnwire.MilliSatoshi
fee lnwire.MilliSatoshi
outgoingTimeLock uint32
tlvPayload bool
customRecords record.CustomSet
mpp *record.MPP
metadata []byte
amtToForward lnwire.MilliSatoshi
fee lnwire.MilliSatoshi
totalAmtMsatBlinded lnwire.MilliSatoshi
outgoingTimeLock uint32
tlvPayload bool
customRecords record.CustomSet
mpp *record.MPP
metadata []byte
)
// Define a helper function that checks this edge's feature
@ -219,6 +230,10 @@ func newRoute(sourceVertex route.Vertex,
}
metadata = finalHop.metadata
if blindedPath != nil {
totalAmtMsatBlinded = finalHop.totalAmt
}
} else {
// The amount that the current hop needs to forward is
// equal to the incoming amount of the next hop.
@ -250,6 +265,7 @@ func newRoute(sourceVertex route.Vertex,
CustomRecords: customRecords,
MPP: mpp,
Metadata: metadata,
TotalAmtMsat: totalAmtMsatBlinded,
}
hops = append([]*route.Hop{currentHop}, hops...)
@ -260,6 +276,47 @@ func newRoute(sourceVertex route.Vertex,
nextIncomingAmount = amtToForward + fee
}
// If we are creating a route to a blinded path, we need to add some
// additional data to the route that is required for blinded forwarding.
// We do another pass on our edges to append this data.
if blindedPath != nil {
var (
inBlindedRoute bool
dataIndex = 0
introVertex = route.NewVertex(
blindedPath.IntroductionPoint,
)
)
for i, hop := range hops {
// Once we locate our introduction node, we know that
// every hop after this is part of the blinded route.
if bytes.Equal(hop.PubKeyBytes[:], introVertex[:]) {
inBlindedRoute = true
hop.BlindingPoint = blindedPath.BlindingPoint
}
// We don't need to modify edges outside of our blinded
// route.
if !inBlindedRoute {
continue
}
payload := blindedPath.BlindedHops[dataIndex].CipherText
hop.EncryptedData = payload
// All of the hops in a blinded route *except* the
// final hop should have zero amounts / time locks.
if i != len(hops)-1 {
hop.AmtToForward = 0
hop.OutgoingTimeLock = 0
}
dataIndex++
}
}
// With the base routing data expressed as hops, build the full route
newRoute, err := route.NewRouteFromHops(
nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex),

View file

@ -21,6 +21,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/kvdb"
@ -958,7 +959,7 @@ func runFindLowestFeePath(t *testing.T, useCache bool) {
amt: paymentAmt,
cltvDelta: finalHopCLTV,
records: nil,
},
}, nil,
)
require.NoError(t, err, "unable to create path")
@ -1100,7 +1101,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
amt: paymentAmt,
cltvDelta: finalHopCLTV,
records: nil,
},
}, nil,
)
require.NoError(t, err, "unable to create path")
@ -1638,7 +1639,7 @@ func TestNewRoute(t *testing.T) {
records: nil,
paymentAddr: testCase.paymentAddr,
metadata: testCase.metadata,
},
}, nil,
)
if testCase.expectError {
@ -2646,7 +2647,7 @@ func testCltvLimit(t *testing.T, useCache bool, limit uint32,
amt: paymentAmt,
cltvDelta: finalHopCLTV,
records: nil,
},
}, nil,
)
require.NoError(t, err, "unable to create path")
@ -2969,7 +2970,7 @@ func runNoCycle(t *testing.T, useCache bool) {
amt: paymentAmt,
cltvDelta: finalHopCLTV,
records: nil,
},
}, nil,
)
require.NoError(t, err, "unable to create path")
@ -3134,3 +3135,225 @@ func dbFindPath(graph *channeldb.ChannelGraph,
return route, err
}
// TestBlindedRouteConstruction tests creation of a blinded route with the
// following topology:
//
// A -- B -- C (introduction point) - D (blinded) - E (blinded).
func TestBlindedRouteConstruction(t *testing.T) {
t.Parallel()
var (
// We need valid pubkeys for the blinded portion of our route,
// so we just produce all of our pubkeys in the same way.
_, alicePk = btcec.PrivKeyFromBytes([]byte{1})
_, bobPk = btcec.PrivKeyFromBytes([]byte{2})
_, carolPk = btcec.PrivKeyFromBytes([]byte{3})
_, daveBlindedPk = btcec.PrivKeyFromBytes([]byte{4})
_, eveBlindedPk = btcec.PrivKeyFromBytes([]byte{5})
_, blindingPk = btcec.PrivKeyFromBytes([]byte{9})
// Convenient type conversions for the pieces of code that use
// vertexes.
sourceVertex = route.NewVertex(alicePk)
bobVertex = route.NewVertex(bobPk)
carolVertex = route.NewVertex(carolPk)
daveBlindedVertex = route.NewVertex(daveBlindedPk)
eveBlindedVertex = route.NewVertex(eveBlindedPk)
currentHeight uint32 = 100
// Create arbitrary encrypted data for each hop (we don't need
// to actually read this data to test route construction, since
// it's only used by forwarding nodes) and metadata for the
// final hop.
metadata = []byte{1, 2, 3}
carolEncryptedData = []byte{4, 5, 6}
daveEncryptedData = []byte{7, 8, 9}
eveEncryptedData = []byte{10, 11, 12}
// Create a blinded route with carol as the introduction point.
blindedPath = &sphinx.BlindedPath{
IntroductionPoint: carolPk,
BlindingPoint: blindingPk,
BlindedHops: []*sphinx.BlindedHopInfo{
{
// Note: no pubkey is provided for
// Carol because we use the pubkey
// given by IntroductionPoint.
CipherText: carolEncryptedData,
},
{
BlindedNodePub: daveBlindedPk,
CipherText: daveEncryptedData,
},
{
BlindedNodePub: eveBlindedPk,
CipherText: eveEncryptedData,
},
},
}
// Create a blinded payment, which contains the aggregate relay
// information and constraints for the blinded portion of the
// path.
blindedPayment = &BlindedPayment{
BlindedPath: blindedPath,
CltvExpiryDelta: 120,
// Set only a base fee for easier calculations.
BaseFee: 5000,
Features: tlvFeatures,
}
// Create channel edges for the unblinded portion of our
// route. Proportional fees are omitted for easy test
// calculations, but non-zero base fees ensure our fee is
// still accounted for.
aliceBobEdge = &channeldb.CachedEdgePolicy{
ChannelID: 1,
// We won't actually use this timelock / fee (since
// it's the sender's outbound channel), but we include
// it with different values so that the test will trip
// up if we were to include the fee/delay.
TimeLockDelta: 10,
FeeBaseMSat: 50,
ToNodePubKey: func() route.Vertex {
return bobVertex
},
ToNodeFeatures: tlvFeatures,
}
bobCarolEdge = &channeldb.CachedEdgePolicy{
ChannelID: 2,
TimeLockDelta: 15,
FeeBaseMSat: 20,
ToNodePubKey: func() route.Vertex {
return carolVertex
},
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.
totalAmt lnwire.MilliSatoshi = 110
finalHopParams = finalHopParams{
amt: totalAmt,
totalAmt: totalAmt,
metadata: metadata,
}
)
require.NoError(t, blindedPayment.Validate())
// Generate route hints from our blinded payment and a set of edges
// 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()
carolDaveEdge := blindedEdges[carolVertex][0]
daveEveEdge := blindedEdges[daveBlindedVertex][0]
edges := []*channeldb.CachedEdgePolicy{
aliceBobEdge,
bobCarolEdge,
carolDaveEdge,
daveEveEdge,
}
// Total timelock for the route should include:
// - Starting block height
// - CLTV delta for Bob -> Carol (unblinded hop)
// - Aggregate cltv from blinded path, which includes
// - CLTV delta for Carol -> Dave -> Eve (blinded route)
// - Eve's chosen final CLTV delta
totalTimelock := currentHeight +
uint32(bobCarolEdge.TimeLockDelta) +
uint32(blindedPayment.CltvExpiryDelta)
// Total amount for the route should include:
// - Total amount being sent
// - Fee for Bob -> Carol (unblinded hop)
// - Fee for Carol -> Dave -> Eve (blinded route)
totalAmount := totalAmt + bobCarolEdge.FeeBaseMSat +
lnwire.MilliSatoshi(blindedPayment.BaseFee)
// Bob's outgoing timelock and amount are the total less his own
// outgoing channel's delta and fee.
bobTimelock := currentHeight + uint32(blindedPayment.CltvExpiryDelta)
bobAmount := totalAmt + lnwire.MilliSatoshi(
blindedPayment.BaseFee,
)
aliceBobRouteHop := &route.Hop{
PubKeyBytes: bobVertex,
ChannelID: aliceBobEdge.ChannelID,
// Alice -> Bob is a regular hop, so it should include all the
// regular forwarding values for Bob to send an outgoing HTLC
// to Carol.
OutgoingTimeLock: bobTimelock,
AmtToForward: bobAmount,
}
bobCarolRouteHop := &route.Hop{
PubKeyBytes: carolVertex,
ChannelID: bobCarolEdge.ChannelID,
// Bob -> Carol sends the HTLC to the introduction node, so
// it should not have forwarding values for Carol (these will
// be obtained from the encrypted data) and should include both
// the blinding point and encrypted data to be passed to Carol.
OutgoingTimeLock: 0,
AmtToForward: 0,
BlindingPoint: blindingPk,
EncryptedData: carolEncryptedData,
}
carolDaveRouteHop := &route.Hop{
PubKeyBytes: daveBlindedVertex,
// Carol -> Dave is within the blinded route, so should not
// set any outgoing values but must include Dave's encrypted
// data.
OutgoingTimeLock: 0,
AmtToForward: 0,
EncryptedData: daveEncryptedData,
}
daveEveRouteHop := &route.Hop{
PubKeyBytes: eveBlindedVertex,
// Dave -> Eve is the final hop in a blinded route, so it
// should include outgoing values for the final value, along
// with any encrypted data for Eve. Since we have not added
// any block padding to the final hop, this value is _just_
// the current height (as the final CTLV delta is covered in
// the blinded path.
OutgoingTimeLock: currentHeight,
AmtToForward: totalAmt,
EncryptedData: eveEncryptedData,
// The last hop should also contain final-hop fields such as
// metadata and total amount.
Metadata: metadata,
TotalAmtMsat: totalAmt,
}
expectedRoute := &route.Route{
SourcePubKey: sourceVertex,
Hops: []*route.Hop{
aliceBobRouteHop,
bobCarolRouteHop,
carolDaveRouteHop,
daveEveRouteHop,
},
TotalTimeLock: totalTimelock,
TotalAmount: totalAmount,
}
route, err := newRoute(
sourceVertex, edges, currentHeight, finalHopParams,
blindedPath,
)
require.NoError(t, err)
require.Equal(t, expectedRoute, route)
}

View file

@ -390,7 +390,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
records: p.payment.DestCustomRecords,
paymentAddr: p.payment.PaymentAddr,
metadata: p.payment.Metadata,
},
}, nil,
)
if err != nil {
return nil, err

View file

@ -1991,6 +1991,16 @@ func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
}
}
// blindedPath returns the request's blinded path, which is set if the payment
// is to a blinded route.
func (r *RouteRequest) blindedPath() *sphinx.BlindedPath {
if r.BlindedPayment == nil {
return nil
}
return r.BlindedPayment.BlindedPath
}
// FindRoute attempts to query the ChannelRouter for the optimum path to a
// particular target destination to which it is able to send `amt` after
// factoring in channel capacities and cumulative fees along the route.
@ -2047,7 +2057,7 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
totalAmt: req.Amount,
cltvDelta: req.FinalExpiry,
records: req.CustomRecords,
},
}, req.blindedPath(),
)
if err != nil {
return nil, 0, err
@ -3032,7 +3042,7 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
cltvDelta: uint16(finalCltvDelta),
records: nil,
paymentAddr: payAddr,
},
}, nil,
)
}