package itest import ( "crypto/sha256" "encoding/hex" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // testQueryBlindedRoutes tests querying routes to blinded routes. To do this, // it sets up a nework of Alice - Bob - Carol and creates a mock blinded route // that uses Carol as the introduction node (plus dummy hops to cover multiple // hops). The test simply asserts that the structure of the route is as // expected. It also includes the edge case of a single-hop blinded route, // which indicates that the introduction node is the recipient. func testQueryBlindedRoutes(ht *lntest.HarnessTest) { var ( // Convenience aliases. alice = ht.Alice bob = ht.Bob ) // Setup a two hop channel network: Alice -- Bob -- Carol. // We set our proportional fee for these channels to zero, so that // our calculations are easier. This is okay, because we're not testing // the basic mechanics of pathfinding in this test. chanAmt := btcutil.Amount(100000) chanPointAliceBob := ht.OpenChannel( alice, bob, lntest.OpenChannelParams{ Amt: chanAmt, BaseFee: 10000, FeeRate: 0, UseBaseFee: true, UseFeeRate: true, }, ) carol := ht.NewNode("Carol", nil) ht.EnsureConnected(bob, carol) var bobCarolBase uint64 = 2000 chanPointBobCarol := ht.OpenChannel( bob, carol, lntest.OpenChannelParams{ Amt: chanAmt, BaseFee: bobCarolBase, FeeRate: 0, UseBaseFee: true, UseFeeRate: true, }, ) // Wait for Alice to see Bob/Carol's channel because she'll need it for // pathfinding. ht.AssertTopologyChannelOpen(alice, chanPointBobCarol) // Lookup full channel info so that we have channel ids for our route. aliceBobChan := ht.GetChannelByChanPoint(alice, chanPointAliceBob) bobCarolChan := ht.GetChannelByChanPoint(bob, chanPointBobCarol) // Sanity check that bob's fee is as expected. chanInfoReq := &lnrpc.ChanInfoRequest{ ChanId: bobCarolChan.ChanId, } bobCarolInfo := bob.RPC.GetChanInfo(chanInfoReq) // Our test relies on knowing the fee rate for bob - carol to set the // fees we expect for our route. Perform a quick sanity check that our // policy is as expected. var policy *lnrpc.RoutingPolicy if bobCarolInfo.Node1Pub == bob.PubKeyStr { policy = bobCarolInfo.Node1Policy } else { policy = bobCarolInfo.Node2Policy } require.Equal(ht, bobCarolBase, uint64(policy.FeeBaseMsat), "base fee") require.EqualValues(ht, 0, policy.FeeRateMilliMsat, "fee rate") // We'll also need the current block height to calculate our locktimes. info := alice.RPC.GetInfo() // Since we created channels with default parameters, we can assume // that all of our channels have the default cltv delta. bobCarolDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta) // Create arbitrary pubkeys for use in our blinded route. They're not // actually used functionally in this test, so we can just make them up. var ( _, blindingPoint = btcec.PrivKeyFromBytes([]byte{1}) _, carolBlinded = btcec.PrivKeyFromBytes([]byte{2}) _, blindedHop1 = btcec.PrivKeyFromBytes([]byte{3}) _, blindedHop2 = btcec.PrivKeyFromBytes([]byte{4}) encryptedDataCarol = []byte{1, 2, 3} encryptedData1 = []byte{4, 5, 6} encryptedData2 = []byte{7, 8, 9} blindingBytes = blindingPoint.SerializeCompressed() carolBlindedBytes = carolBlinded.SerializeCompressed() blinded1Bytes = blindedHop1.SerializeCompressed() blinded2Bytes = blindedHop2.SerializeCompressed() ) // Now we create a blinded route which uses carol as an introduction // node followed by two dummy hops (the arbitrary pubkeys in our // blinded route above: // Carol --- B1 --- B2 route := &lnrpc.BlindedPath{ IntroductionNode: carol.PubKey[:], BlindingPoint: blindingBytes, BlindedHops: []*lnrpc.BlindedHop{ { // The first hop in the blinded route is // expected to be the introduction node. BlindedNode: carolBlindedBytes, EncryptedData: encryptedDataCarol, }, { BlindedNode: blinded1Bytes, EncryptedData: encryptedData1, }, { BlindedNode: blinded2Bytes, EncryptedData: encryptedData2, }, }, } // Create a blinded payment that has aggregate cltv and fee params // for our route. var ( blindedBaseFee uint64 = 1500 blindedCltvDelta uint32 = 125 ) blindedPayment := &lnrpc.BlindedPaymentPath{ BlindedPath: route, BaseFeeMsat: blindedBaseFee, TotalCltvDelta: blindedCltvDelta, } // Query for a route to the blinded path constructed above. var paymentAmt int64 = 100_000 req := &lnrpc.QueryRoutesRequest{ AmtMsat: paymentAmt, BlindedPaymentPaths: []*lnrpc.BlindedPaymentPath{ blindedPayment, }, } resp := alice.RPC.QueryRoutes(req) require.Len(ht, resp.Routes, 1) // Payment amount and cltv will be included for the bob/carol edge // (because we apply on the outgoing hop), and the blinded portion of // the route. totalFee := bobCarolBase + blindedBaseFee totalAmt := uint64(paymentAmt) + totalFee totalCltv := info.BlockHeight + bobCarolDelta + blindedCltvDelta // Alice -> Bob // Forward: total - bob carol fees // Expiry: total - bob carol delta // // Bob -> Carol // Forward: 101500 (total + blinded fees) // Expiry: Height + blinded cltv delta // Encrypted Data: enc_carol // // Carol -> Blinded 1 // Forward/ Expiry: 0 // Encrypted Data: enc_1 // // Blinded 1 -> Blinded 2 // Forward/ Expiry: Height // Encrypted Data: enc_2 hop0Amount := int64(totalAmt - bobCarolBase) hop0Expiry := totalCltv - bobCarolDelta finalHopExpiry := totalCltv - bobCarolDelta - blindedCltvDelta expectedRoute := &lnrpc.Route{ TotalTimeLock: totalCltv, TotalAmtMsat: int64(totalAmt), TotalFeesMsat: int64(totalFee), Hops: []*lnrpc.Hop{ { ChanId: aliceBobChan.ChanId, Expiry: hop0Expiry, AmtToForwardMsat: hop0Amount, FeeMsat: int64(bobCarolBase), PubKey: bob.PubKeyStr, }, { ChanId: bobCarolChan.ChanId, PubKey: carol.PubKeyStr, BlindingPoint: blindingBytes, FeeMsat: int64(blindedBaseFee), EncryptedData: encryptedDataCarol, }, { PubKey: hex.EncodeToString( blinded1Bytes, ), EncryptedData: encryptedData1, }, { PubKey: hex.EncodeToString( blinded2Bytes, ), AmtToForwardMsat: paymentAmt, Expiry: finalHopExpiry, EncryptedData: encryptedData2, TotalAmtMsat: uint64(paymentAmt), }, }, } r := resp.Routes[0] assert.Equal(ht, expectedRoute.TotalTimeLock, r.TotalTimeLock) assert.Equal(ht, expectedRoute.TotalAmtMsat, r.TotalAmtMsat) assert.Equal(ht, expectedRoute.TotalFeesMsat, r.TotalFeesMsat) assert.Equal(ht, len(expectedRoute.Hops), len(r.Hops)) for i, hop := range expectedRoute.Hops { assert.Equal(ht, hop.PubKey, r.Hops[i].PubKey, "hop: %v pubkey", i) assert.Equal(ht, hop.ChanId, r.Hops[i].ChanId, "hop: %v chan id", i) assert.Equal(ht, hop.Expiry, r.Hops[i].Expiry, "hop: %v expiry", i) assert.Equal(ht, hop.AmtToForwardMsat, r.Hops[i].AmtToForwardMsat, "hop: %v forward", i) assert.Equal(ht, hop.FeeMsat, r.Hops[i].FeeMsat, "hop: %v fee", i) assert.Equal(ht, hop.BlindingPoint, r.Hops[i].BlindingPoint, "hop: %v blinding point", i) assert.Equal(ht, hop.EncryptedData, r.Hops[i].EncryptedData, "hop: %v encrypted data", i) } // Dispatch a payment to our blinded route. preimage := [33]byte{1, 2, 3} hash := sha256.Sum256(preimage[:]) sendReq := &routerrpc.SendToRouteRequest{ PaymentHash: hash[:], Route: r, } htlcAttempt := alice.RPC.SendToRouteV2(sendReq) // Since Carol doesn't understand blinded routes, we expect her to fail // the payment because the onion payload is invalid (missing amount to // forward). require.NotNil(ht, htlcAttempt.Failure) require.Equal(ht, uint32(2), htlcAttempt.Failure.FailureSourceIndex) // Next, we test an edge case where just an introduction node is // included as a "single hop blinded route". sendToIntroCLTVFinal := uint32(15) sendToIntroTimelock := info.BlockHeight + bobCarolDelta + sendToIntroCLTVFinal introNodeBlinded := &lnrpc.BlindedPaymentPath{ BlindedPath: &lnrpc.BlindedPath{ IntroductionNode: carol.PubKey[:], BlindingPoint: blindingBytes, BlindedHops: []*lnrpc.BlindedHop{ { // The first hop in the blinded route is // expected to be the introduction node. BlindedNode: carolBlindedBytes, EncryptedData: encryptedDataCarol, }, }, }, // Fees should be zero for a single hop blinded path, and the // total cltv expiry is just expected to cover the final cltv // delta of the receiving node (ie, the introduction node). BaseFeeMsat: 0, TotalCltvDelta: sendToIntroCLTVFinal, } req = &lnrpc.QueryRoutesRequest{ AmtMsat: paymentAmt, BlindedPaymentPaths: []*lnrpc.BlindedPaymentPath{ introNodeBlinded, }, } // Assert that we have one route, and two hops: Alice/Bob and Bob/Carol. resp = alice.RPC.QueryRoutes(req) require.Len(ht, resp.Routes, 1) require.Len(ht, resp.Routes[0].Hops, 2) require.Equal(ht, resp.Routes[0].TotalTimeLock, sendToIntroTimelock) ht.CloseChannel(alice, chanPointAliceBob) ht.CloseChannel(bob, chanPointBobCarol) }