-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lntest/itest: add coverage for querying routes to blinded paths
- Loading branch information
Showing
2 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
package itest | ||
|
||
import ( | ||
"context" | ||
"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/lntemp" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// testQueryBlindedRoutes tests querying routes to blinded routes. | ||
func testQueryBlindedRoutes(ht *lntemp.HarnessTest) { | ||
var ( | ||
ctxb = context.Background() | ||
|
||
// 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, lntemp.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, lntemp.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, err := bob.RPC.LN.GetChanInfo(ctxb, chanInfoReq) | ||
require.NoError(ht, err) | ||
|
||
// 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.Equal(ht, int64(0), policy.FeeRateMilliMsat, "fee rate") | ||
|
||
// We'll also need the current block height to calculate our locktimes. | ||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) | ||
defer cancel() | ||
|
||
info, err := alice.RPC.LN.GetInfo(ctxt, &lnrpc.GetInfoRequest{}) | ||
require.NoError(ht, err) | ||
cancel() | ||
|
||
// 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: | ||
// Carol --- B1 --- B2 | ||
route := &lnrpc.BlindedRoute{ | ||
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 ( | ||
aggregateBaseFee uint64 = 1500 | ||
aggregateCltvDelta uint32 = 125 | ||
cltvLimit = info.BlockHeight + 500 | ||
) | ||
|
||
blindedPayment := &lnrpc.BlindedPayment{ | ||
Route: route, | ||
RelayParameters: &lnrpc.BlindedRelay{ | ||
AggregateBaseFeeMsat: aggregateBaseFee, | ||
TotalCltvDelta: aggregateCltvDelta, | ||
}, | ||
RelayConstraints: &lnrpc.BlindedConstraints{ | ||
CltvLimit: cltvLimit, | ||
}, | ||
} | ||
|
||
// Query for a route to the blinded path constructed above. | ||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) | ||
defer cancel() | ||
|
||
var ( | ||
paymentAmt int64 = 100_000 | ||
finalDelta uint32 = 50 | ||
) | ||
req := &lnrpc.QueryRoutesRequest{ | ||
AmtMsat: paymentAmt, | ||
BlindedPath: blindedPayment, | ||
FinalCltvDelta: int32(finalDelta), | ||
} | ||
|
||
resp, err := alice.RPC.LN.QueryRoutes(ctxt, req) | ||
require.NoError(ht, err) | ||
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 + aggregateBaseFee | ||
totalAmt := uint64(paymentAmt) + totalFee | ||
totalCltv := info.BlockHeight + bobCarolDelta + aggregateCltvDelta + | ||
finalDelta | ||
|
||
// Alice -> Bob | ||
// Forward: total - bob carol fees | ||
// Expiry: total - bob carol delta | ||
// | ||
// Bob -> Carol | ||
// Forward: 101500 (total + blinded fees) | ||
// Expiry: Height + 125 (final delta) | ||
// Encrypted Data: enc_carol | ||
// | ||
// Carol -> Blinded 1 | ||
// Forward/ Expiry: 0 | ||
// Encrypted Data: enc_1 | ||
// | ||
// Blinded 1 -> Blinded 2 | ||
// Forward/ Expiry: 0 | ||
// Encrypted Data: enc_2 | ||
hop0Amount := int64(totalAmt - bobCarolBase) | ||
hop0Expiry := totalCltv - bobCarolDelta | ||
blindedExpiry := hop0Expiry - aggregateCltvDelta | ||
|
||
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(aggregateBaseFee), | ||
EncryptedData: encryptedDataCarol, | ||
}, | ||
{ | ||
PubKey: hex.EncodeToString( | ||
blinded1Bytes, | ||
), | ||
EncryptedData: encryptedData1, | ||
}, | ||
{ | ||
PubKey: hex.EncodeToString( | ||
blinded2Bytes, | ||
), | ||
AmtToForwardMsat: paymentAmt, | ||
Expiry: blindedExpiry, | ||
EncryptedData: encryptedData2, | ||
}, | ||
}, | ||
} | ||
|
||
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, | ||
} | ||
|
||
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) | ||
htlcAttempt, err := alice.RPC.Router.SendToRouteV2(ctxt, sendReq) | ||
cancel() | ||
require.NoError(ht, err) | ||
|
||
// 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) | ||
} |