mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
791 lines
24 KiB
Go
791 lines
24 KiB
Go
package itest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
|
"github.com/lightningnetwork/lnd/chainreg"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
|
"github.com/lightningnetwork/lnd/lntest"
|
|
"github.com/lightningnetwork/lnd/lntest/node"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/lightningnetwork/lnd/routing"
|
|
"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)
|
|
}
|
|
|
|
type blindedForwardTest struct {
|
|
ht *lntest.HarnessTest
|
|
carol *node.HarnessNode
|
|
dave *node.HarnessNode
|
|
channels []*lnrpc.ChannelPoint
|
|
|
|
carolInterceptor routerrpc.Router_HtlcInterceptorClient
|
|
|
|
preimage [32]byte
|
|
|
|
// cancel will cancel the test's top level context.
|
|
cancel func()
|
|
}
|
|
|
|
func newBlindedForwardTest(ht *lntest.HarnessTest) (context.Context,
|
|
*blindedForwardTest) {
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return ctx, &blindedForwardTest{
|
|
ht: ht,
|
|
cancel: cancel,
|
|
preimage: [32]byte{1, 2, 3},
|
|
}
|
|
}
|
|
|
|
// setup spins up additional nodes needed for our test and creates a four hop
|
|
// network for testing blinded forwarding and returns a blinded route from
|
|
// Bob -> Carol -> Dave, with Bob acting as the introduction point and an
|
|
// interceptor on Carol's node to manage HTLCs (as Dave does not yet support
|
|
// receiving).
|
|
func (b *blindedForwardTest) setup(
|
|
ctx context.Context) *routing.BlindedPayment {
|
|
|
|
b.carol = b.ht.NewNode("Carol", []string{
|
|
"requireinterceptor",
|
|
})
|
|
|
|
var err error
|
|
b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(ctx)
|
|
require.NoError(b.ht, err, "interceptor")
|
|
|
|
b.dave = b.ht.NewNode("Dave", nil)
|
|
|
|
b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave)
|
|
|
|
// Create a blinded route to Dave via Bob --- Carol --- Dave:
|
|
bobChan := b.ht.GetChannelByChanPoint(b.ht.Bob, b.channels[1])
|
|
carolChan := b.ht.GetChannelByChanPoint(b.carol, b.channels[2])
|
|
|
|
edges := []*forwardingEdge{
|
|
getForwardingEdge(b.ht, b.ht.Bob, bobChan.ChanId),
|
|
getForwardingEdge(b.ht, b.carol, carolChan.ChanId),
|
|
}
|
|
|
|
davePk, err := btcec.ParsePubKey(b.dave.PubKey[:])
|
|
require.NoError(b.ht, err, "dave pubkey")
|
|
|
|
return b.createBlindedRoute(edges, davePk, 50)
|
|
}
|
|
|
|
// cleanup tears down all channels created by the test and cancels the top
|
|
// level context used in the test.
|
|
func (b *blindedForwardTest) cleanup() {
|
|
b.ht.CloseChannel(b.ht.Alice, b.channels[0])
|
|
b.ht.CloseChannel(b.ht.Bob, b.channels[1])
|
|
b.ht.CloseChannel(b.carol, b.channels[2])
|
|
|
|
b.cancel()
|
|
}
|
|
|
|
// createRouteToBlinded queries for a route from alice to the blinded path
|
|
// provided.
|
|
//
|
|
//nolint:gomnd
|
|
func (b *blindedForwardTest) createRouteToBlinded(paymentAmt int64,
|
|
route *routing.BlindedPayment) *lnrpc.Route {
|
|
|
|
intro := route.BlindedPath.IntroductionPoint.SerializeCompressed()
|
|
blinding := route.BlindedPath.BlindingPoint.SerializeCompressed()
|
|
|
|
blindedRoute := &lnrpc.BlindedPath{
|
|
IntroductionNode: intro,
|
|
BlindingPoint: blinding,
|
|
BlindedHops: make(
|
|
[]*lnrpc.BlindedHop,
|
|
len(route.BlindedPath.BlindedHops),
|
|
),
|
|
}
|
|
|
|
for i, hop := range route.BlindedPath.BlindedHops {
|
|
blindedRoute.BlindedHops[i] = &lnrpc.BlindedHop{
|
|
BlindedNode: hop.BlindedNodePub.SerializeCompressed(),
|
|
EncryptedData: hop.CipherText,
|
|
}
|
|
}
|
|
blindedPath := &lnrpc.BlindedPaymentPath{
|
|
BlindedPath: blindedRoute,
|
|
BaseFeeMsat: uint64(
|
|
route.BaseFee,
|
|
),
|
|
ProportionalFeeRate: route.ProportionalFeeRate,
|
|
TotalCltvDelta: uint32(
|
|
route.CltvExpiryDelta,
|
|
),
|
|
}
|
|
|
|
req := &lnrpc.QueryRoutesRequest{
|
|
AmtMsat: paymentAmt,
|
|
// Our fee limit doesn't really matter, we just want to
|
|
// be able to make the payment.
|
|
FeeLimit: &lnrpc.FeeLimit{
|
|
Limit: &lnrpc.FeeLimit_Percent{
|
|
Percent: 50,
|
|
},
|
|
},
|
|
BlindedPaymentPaths: []*lnrpc.BlindedPaymentPath{
|
|
blindedPath,
|
|
},
|
|
}
|
|
|
|
resp := b.ht.Alice.RPC.QueryRoutes(req)
|
|
require.Greater(b.ht, len(resp.Routes), 0, "no routes")
|
|
require.Len(b.ht, resp.Routes[0].Hops, 3, "unexpected route length")
|
|
|
|
return resp.Routes[0]
|
|
}
|
|
|
|
// sendBlindedPayment dispatches a payment to the route provided.
|
|
func (b *blindedForwardTest) sendBlindedPayment(ctx context.Context,
|
|
route *lnrpc.Route) {
|
|
|
|
hash := sha256.Sum256(b.preimage[:])
|
|
sendReq := &routerrpc.SendToRouteRequest{
|
|
PaymentHash: hash[:],
|
|
Route: route,
|
|
}
|
|
|
|
// Dispatch in a goroutine because this call is blocking - we assume
|
|
// that we'll have assertions that this payment is sent by the caller.
|
|
go func() {
|
|
b.ht.Alice.RPC.SendToRouteV2(sendReq)
|
|
}()
|
|
}
|
|
|
|
// interceptFinalHop launches a goroutine to intercept Carol's htlcs and
|
|
// returns a closure that can be used to resolve intercepted htlcs.
|
|
//
|
|
//nolint:lll
|
|
func (b *blindedForwardTest) interceptFinalHop() func(routerrpc.ResolveHoldForwardAction) {
|
|
hash := sha256.Sum256(b.preimage[:])
|
|
htlcReceived := make(chan *routerrpc.ForwardHtlcInterceptRequest)
|
|
|
|
// Launch a goroutine which will receive from the interceptor and pipe
|
|
// it into our request channel.
|
|
go func() {
|
|
forward, err := b.carolInterceptor.Recv()
|
|
if err != nil {
|
|
b.ht.Fatalf("intercept receive failed: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(forward.PaymentHash, hash[:]) {
|
|
b.ht.Fatalf("unexpected payment hash: %v", hash)
|
|
}
|
|
|
|
select {
|
|
case htlcReceived <- forward:
|
|
|
|
case <-time.After(lntest.DefaultTimeout):
|
|
b.ht.Fatal("timeout waiting to send intercepted htlc")
|
|
}
|
|
}()
|
|
|
|
// Create a closure that will wait for the intercept request and
|
|
// resolve the HTLC with the appropriate action.
|
|
resolve := func(action routerrpc.ResolveHoldForwardAction) {
|
|
select {
|
|
case forward := <-htlcReceived:
|
|
resp := &routerrpc.ForwardHtlcInterceptResponse{
|
|
IncomingCircuitKey: forward.IncomingCircuitKey,
|
|
}
|
|
|
|
switch action {
|
|
case routerrpc.ResolveHoldForwardAction_FAIL:
|
|
resp.Action = routerrpc.ResolveHoldForwardAction_FAIL
|
|
|
|
case routerrpc.ResolveHoldForwardAction_SETTLE:
|
|
resp.Action = routerrpc.ResolveHoldForwardAction_SETTLE
|
|
resp.Preimage = b.preimage[:]
|
|
|
|
case routerrpc.ResolveHoldForwardAction_RESUME:
|
|
resp.Action = routerrpc.ResolveHoldForwardAction_RESUME
|
|
}
|
|
|
|
require.NoError(b.ht, b.carolInterceptor.Send(resp))
|
|
|
|
case <-time.After(lntest.DefaultTimeout):
|
|
b.ht.Fatal("timeout waiting for htlc intercept")
|
|
}
|
|
}
|
|
|
|
return resolve
|
|
}
|
|
|
|
// setupFourHopNetwork creates a network with the following topology and
|
|
// liquidity:
|
|
// Alice (100k)----- Bob (100k) ----- Carol (100k) ----- Dave
|
|
//
|
|
// The funding outpoint for AB / BC / CD are returned in-order.
|
|
func setupFourHopNetwork(ht *lntest.HarnessTest,
|
|
carol, dave *node.HarnessNode) []*lnrpc.ChannelPoint {
|
|
|
|
const chanAmt = btcutil.Amount(100000)
|
|
var networkChans []*lnrpc.ChannelPoint
|
|
|
|
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
|
// being the sole funder of the channel.
|
|
chanPointAlice := ht.OpenChannel(
|
|
ht.Alice, ht.Bob, lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
},
|
|
)
|
|
networkChans = append(networkChans, chanPointAlice)
|
|
|
|
// Create a channel between bob and carol.
|
|
ht.EnsureConnected(ht.Bob, carol)
|
|
chanPointBob := ht.OpenChannel(
|
|
ht.Bob, carol, lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
},
|
|
)
|
|
networkChans = append(networkChans, chanPointBob)
|
|
|
|
// Fund carol and connect her and dave so that she can create a channel
|
|
// between them.
|
|
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
|
|
ht.EnsureConnected(carol, dave)
|
|
|
|
chanPointCarol := ht.OpenChannel(
|
|
carol, dave, lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
},
|
|
)
|
|
networkChans = append(networkChans, chanPointCarol)
|
|
|
|
// Wait for all nodes to have seen all channels.
|
|
nodes := []*node.HarnessNode{ht.Alice, ht.Bob, carol, dave}
|
|
for _, chanPoint := range networkChans {
|
|
for _, node := range nodes {
|
|
ht.AssertTopologyChannelOpen(node, chanPoint)
|
|
}
|
|
}
|
|
|
|
return []*lnrpc.ChannelPoint{
|
|
chanPointAlice,
|
|
chanPointBob,
|
|
chanPointCarol,
|
|
}
|
|
}
|
|
|
|
// createBlindedRoute creates a blinded route to the recipient node provided.
|
|
// The set of hops is expected to start at the introduction node and end at
|
|
// the recipient.
|
|
func (b *blindedForwardTest) createBlindedRoute(hops []*forwardingEdge,
|
|
dest *btcec.PublicKey, finalCLTV uint16) *routing.BlindedPayment {
|
|
|
|
// Create a path with space for each of our hops + the destination
|
|
// node. We include our passed final cltv delta here because blinded
|
|
// paths include the delta in the blinded portion (not the invoice).
|
|
blindedPayment := &routing.BlindedPayment{
|
|
CltvExpiryDelta: finalCLTV,
|
|
}
|
|
|
|
pathLength := len(hops) + 1
|
|
blindedPath := make([]*sphinx.HopInfo, pathLength)
|
|
|
|
// Run forwards through our hops to create blinded route data for each
|
|
// node with the next node's short channel id and our payment
|
|
// constraints.
|
|
for i := 0; i < len(hops); i++ {
|
|
node := hops[i]
|
|
scid := node.channelID
|
|
|
|
// Set the relay information for this edge based on its policy.
|
|
delta := uint16(node.edge.TimeLockDelta)
|
|
relayInfo := &record.PaymentRelayInfo{
|
|
BaseFee: uint32(node.edge.FeeBaseMsat),
|
|
FeeRate: uint32(node.edge.FeeRateMilliMsat),
|
|
CltvExpiryDelta: delta,
|
|
}
|
|
|
|
// We set our constraints with our edge's actual htlc min, and
|
|
// an arbitrary maximum expiry (since it's just an anti-probing
|
|
// mechanism).
|
|
constraints := &record.PaymentConstraints{
|
|
HtlcMinimumMsat: lnwire.MilliSatoshi(node.edge.MinHtlc),
|
|
MaxCltvExpiry: 100000,
|
|
}
|
|
|
|
// Add CLTV delta of each hop to the blinded payment.
|
|
blindedPayment.CltvExpiryDelta += delta
|
|
|
|
// Encode the route's blinded data and include it in the
|
|
// blinded hop.
|
|
payload := record.NewBlindedRouteData(
|
|
scid, nil, *relayInfo, constraints, nil,
|
|
)
|
|
payloadBytes, err := record.EncodeBlindedRouteData(payload)
|
|
require.NoError(b.ht, err)
|
|
|
|
blindedPath[i] = &sphinx.HopInfo{
|
|
NodePub: node.pubkey,
|
|
PlainText: payloadBytes,
|
|
}
|
|
}
|
|
|
|
// Next, we'll run backwards through our route to build up the aggregate
|
|
// fees for the blinded payment as a whole. This is done in a separate
|
|
// loop for the sake of readability.
|
|
//
|
|
// For blinded path aggregated fees, we start at the receiving node
|
|
// and add up base an proportional fees *including* the fees that we'll
|
|
// charge on accumulated fees. We use the int ceiling to round up so
|
|
// that the sender will always over-pay, ensuring that we don't round
|
|
// down along the route leaving one forwarding node short of what
|
|
// they're expecting.
|
|
var (
|
|
hopCount = len(hops) - 1
|
|
currentHopBaseFee = hops[hopCount].edge.FeeBaseMsat
|
|
currentHopPropFee = hops[hopCount].edge.FeeRateMilliMsat
|
|
feeParts int64 = 1e6
|
|
)
|
|
|
|
// Note: the spec says to iterate backwards, but then uses n / n +1 to
|
|
// express the "next" hop in the route going backwards. This works for
|
|
// languages where we can iterate backwards and get an increasing
|
|
// index, but since we're counting backwards we use n-1 instead.
|
|
//
|
|
// Specification reference:
|
|
//nolint:lll
|
|
// https://github.com/lightning/bolts/blob/60de4a09727c20dea330f9ee8313034de6e50594/proposals/route-blinding.md?plain=1#L253-L254
|
|
for i := hopCount; i > 0; i-- {
|
|
preceedingBase := hops[i-1].edge.FeeBaseMsat
|
|
preceedingProp := hops[i-1].edge.FeeBaseMsat
|
|
|
|
// Separate numerator from ceiling division to break up large
|
|
// lines.
|
|
baseFeeNumerator := preceedingBase*feeParts +
|
|
currentHopBaseFee*(feeParts+preceedingProp)
|
|
currentHopBaseFee = (baseFeeNumerator + feeParts - 1) / feeParts
|
|
|
|
propFeeNumerator := (currentHopPropFee+preceedingProp)*
|
|
feeParts + currentHopPropFee*preceedingProp
|
|
currentHopPropFee = (propFeeNumerator + feeParts - 1) / feeParts
|
|
}
|
|
|
|
blindedPayment.BaseFee = uint32(currentHopBaseFee)
|
|
blindedPayment.ProportionalFeeRate = uint32(currentHopPropFee)
|
|
|
|
// Add our destination node at the end of the path. We don't need to
|
|
// add any forwarding parameters because we're at the final hop.
|
|
payloadBytes, err := record.EncodeBlindedRouteData(
|
|
// TODO: we don't have support for the final hop fields,
|
|
// because only forwarding is supported. We add a next
|
|
// node ID here so that it _looks like_ a valid
|
|
// forwarding hop (though in reality it's the last
|
|
// hop).
|
|
record.NewBlindedRouteData(
|
|
lnwire.NewShortChanIDFromInt(100), nil,
|
|
record.PaymentRelayInfo{}, nil, nil,
|
|
),
|
|
)
|
|
require.NoError(b.ht, err, "final payload")
|
|
|
|
blindedPath[pathLength-1] = &sphinx.HopInfo{
|
|
NodePub: dest,
|
|
PlainText: payloadBytes,
|
|
}
|
|
|
|
// Blind the path.
|
|
blindingKey, err := btcec.NewPrivateKey()
|
|
require.NoError(b.ht, err)
|
|
|
|
blindedPayment.BlindedPath, err = sphinx.BuildBlindedPath(
|
|
blindingKey, blindedPath,
|
|
)
|
|
require.NoError(b.ht, err, "build blinded path")
|
|
|
|
return blindedPayment
|
|
}
|
|
|
|
// forwardingEdge contains the channel id/source public key for a forwarding
|
|
// edge and the policy associated with the channel in that direction.
|
|
type forwardingEdge struct {
|
|
pubkey *btcec.PublicKey
|
|
channelID lnwire.ShortChannelID
|
|
edge *lnrpc.RoutingPolicy
|
|
}
|
|
|
|
func getForwardingEdge(ht *lntest.HarnessTest,
|
|
node *node.HarnessNode, chanID uint64) *forwardingEdge {
|
|
|
|
chanInfo := node.RPC.GetChanInfo(&lnrpc.ChanInfoRequest{
|
|
ChanId: chanID,
|
|
})
|
|
|
|
pubkey, err := btcec.ParsePubKey(node.PubKey[:])
|
|
require.NoError(ht, err, "%v pubkey", node.Cfg.Name)
|
|
|
|
fwdEdge := &forwardingEdge{
|
|
pubkey: pubkey,
|
|
channelID: lnwire.NewShortChanIDFromInt(chanID),
|
|
}
|
|
|
|
if chanInfo.Node1Pub == node.PubKeyStr {
|
|
fwdEdge.edge = chanInfo.Node1Policy
|
|
} else {
|
|
require.Equal(ht, node.PubKeyStr, chanInfo.Node2Pub,
|
|
"policy edge sanity check")
|
|
|
|
fwdEdge.edge = chanInfo.Node2Policy
|
|
}
|
|
|
|
return fwdEdge
|
|
}
|
|
|
|
// testForwardBlindedRoute tests lnd's ability to forward payments in a blinded
|
|
// route.
|
|
func testForwardBlindedRoute(ht *lntest.HarnessTest) {
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
|
|
route := testCase.setup(ctx)
|
|
blindedRoute := testCase.createRouteToBlinded(10_000_000, route)
|
|
|
|
// Receiving via blinded routes is not yet supported, so Dave won't be
|
|
// able to process the payment.
|
|
//
|
|
// We have an interceptor at our disposal that will catch htlcs as they
|
|
// are forwarded (ie, it won't intercept a HTLC that dave is receiving,
|
|
// since no forwarding occurs). We initiate this interceptor with
|
|
// Carol, so that we can catch it and settle on the outgoing link to
|
|
// Dave. Once we hit the outgoing link, we know that we successfully
|
|
// parsed the htlc, so this is an acceptable compromise.
|
|
// Assert that our interceptor has exited without an error.
|
|
resolveHTLC := testCase.interceptFinalHop()
|
|
|
|
// Once our interceptor is set up, we can send the blinded payment.
|
|
testCase.sendBlindedPayment(ctx, blindedRoute)
|
|
|
|
// Wait for the HTLC to be active on Alice's channel.
|
|
hash := sha256.Sum256(testCase.preimage[:])
|
|
ht.AssertOutgoingHTLCActive(ht.Alice, testCase.channels[0], hash[:])
|
|
ht.AssertOutgoingHTLCActive(ht.Bob, testCase.channels[1], hash[:])
|
|
|
|
// Intercept and settle the HTLC.
|
|
resolveHTLC(routerrpc.ResolveHoldForwardAction_SETTLE)
|
|
|
|
// Wait for the HTLC to reflect as settled for Alice.
|
|
preimage, err := lntypes.MakePreimage(testCase.preimage[:])
|
|
require.NoError(ht, err)
|
|
ht.AssertPaymentStatus(ht.Alice, preimage, lnrpc.Payment_SUCCEEDED)
|
|
|
|
// Assert that the HTLC has settled before test cleanup runs so that
|
|
// we can cooperatively close all channels.
|
|
ht.AssertHTLCNotActive(ht.Bob, testCase.channels[1], hash[:])
|
|
ht.AssertHTLCNotActive(ht.Alice, testCase.channels[0], hash[:])
|
|
}
|