diff --git a/itest/list_on_test.go b/itest/list_on_test.go index bdf7a3855..0674d646b 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -546,4 +546,8 @@ var allTestCases = []*lntest.TestCase{ Name: "update pending open channels", TestFunc: testUpdateOnPendingOpenChannels, }, + { + Name: "query blinded route", + TestFunc: testQueryBlindedRoutes, + }, } diff --git a/itest/lnd_route_blinding.go b/itest/lnd_route_blinding.go new file mode 100644 index 000000000..736d21862 --- /dev/null +++ b/itest/lnd_route_blinding.go @@ -0,0 +1,273 @@ +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. +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) + + ht.CloseChannel(alice, chanPointAliceBob) + ht.CloseChannel(bob, chanPointBobCarol) +}