diff --git a/routing/pathfind.go b/routing/pathfind.go index fca0b6e25..16f182b12 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -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), diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index e95548c0d..c016451bd 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -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) +} diff --git a/routing/payment_session.go b/routing/payment_session.go index bd8fff74f..0a9078502 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -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 diff --git a/routing/router.go b/routing/router.go index 4db775da3..827e21864 100644 --- a/routing/router.go +++ b/routing/router.go @@ -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, ) }