mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-22 14:22:37 +01:00
This itest adds a test that we still propagate blinded errors back properly after a restart with an on-chain resolution. The test also updates our sendpayment timeout to longer so that there's time to resolve the on chain claim.
1058 lines
34 KiB
Go
1058 lines
34 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", "--bitcoin.timelockdelta=18",
|
|
})
|
|
|
|
var err error
|
|
b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(ctx)
|
|
require.NoError(b.ht, err, "interceptor")
|
|
|
|
b.dave = b.ht.NewNode("Dave", []string{
|
|
"--bitcoin.timelockdelta=18",
|
|
})
|
|
|
|
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, returning a
|
|
// cancel function for the payment. Timeout is set for very long to allow
|
|
// time for on-chain resolution.
|
|
func (b *blindedForwardTest) sendBlindedPayment(ctx context.Context,
|
|
route *lnrpc.Route) func() {
|
|
|
|
hash := sha256.Sum256(b.preimage[:])
|
|
sendReq := &routerrpc.SendToRouteRequest{
|
|
PaymentHash: hash[:],
|
|
Route: route,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, time.Hour)
|
|
go func() {
|
|
_, err := b.ht.Alice.RPC.Router.SendToRouteV2(ctx, sendReq)
|
|
require.NoError(b.ht, err)
|
|
}()
|
|
|
|
return cancel
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// drainCarolLiquidity will drain all of the liquidity in Carol's channel in
|
|
// the direction requested:
|
|
// - incoming: Carol has no incoming liquidity from Bob
|
|
// - outgoing: Carol has no outgoing liquidity to Dave.
|
|
func (b *blindedForwardTest) drainCarolLiquidity(incoming bool) {
|
|
sendingNode := b.carol
|
|
receivingNode := b.dave
|
|
|
|
if incoming {
|
|
sendingNode = b.ht.Bob
|
|
receivingNode = b.carol
|
|
}
|
|
|
|
resp := sendingNode.RPC.ListChannels(&lnrpc.ListChannelsRequest{
|
|
Peer: receivingNode.PubKey[:],
|
|
})
|
|
require.Len(b.ht, resp.Channels, 1)
|
|
|
|
// We can't send our channel reserve, and leave some buffer for fees.
|
|
paymentAmt := resp.Channels[0].LocalBalance -
|
|
int64(resp.Channels[0].RemoteConstraints.ChanReserveSat) - 25000
|
|
|
|
invoice := receivingNode.RPC.AddInvoice(&lnrpc.Invoice{
|
|
// Leave some leeway for fees for the HTLC.
|
|
Value: paymentAmt,
|
|
})
|
|
|
|
pmtClient := sendingNode.RPC.SendPayment(
|
|
&routerrpc.SendPaymentRequest{
|
|
PaymentRequest: invoice.PaymentRequest,
|
|
TimeoutSeconds: 60,
|
|
},
|
|
)
|
|
|
|
b.ht.AssertPaymentStatusFromStream(pmtClient, lnrpc.Payment_SUCCEEDED)
|
|
}
|
|
|
|
// 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.
|
|
cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute)
|
|
defer cancelPmt()
|
|
|
|
// 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[:])
|
|
}
|
|
|
|
// Tests handling of errors from the receiving node in a blinded route, testing
|
|
// a payment over: Alice -- Bob -- Carol -- Dave, where Bob is the introduction
|
|
// node.
|
|
//
|
|
// Note that at present the payment fails at Dave because we do not yet support
|
|
// receiving to blinded routes. In future, we can substitute this test out to
|
|
// trigger an IncorrectPaymentDetails failure. In the meantime, this test
|
|
// provides valuable coverage for the case where a node in the route is not
|
|
// spec compliant (ie, does not return the blinded failure and just uses a
|
|
// normal one) because Dave will not appropriately convert the error.
|
|
func testReceiverBlindedError(ht *lntest.HarnessTest) {
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
route := testCase.setup(ctx)
|
|
|
|
sendAndResumeBlindedPayment(ctx, ht, testCase, route, true)
|
|
}
|
|
|
|
// testRelayingBlindedError tests handling of errors from relaying nodes in a
|
|
// blinded route, testing a failure over on Carol's outgoing link in the
|
|
// following topology: Alice -- Bob -- Carol -- Dave, where Bob is the
|
|
// introduction node.
|
|
func testRelayingBlindedError(ht *lntest.HarnessTest) {
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
route := testCase.setup(ctx)
|
|
|
|
// Before we send our payment, drain all of Carol's liquidity
|
|
// so that she can't forward the payment to Dave.
|
|
testCase.drainCarolLiquidity(false)
|
|
|
|
// Then dispatch the payment through Carol which will fail due to
|
|
// a lack of liquidity. This check only happens _after_ the interceptor
|
|
// has given the instruction to resume so we can use test
|
|
// infrastructure that will go ahead and intercept the payment.
|
|
sendAndResumeBlindedPayment(ctx, ht, testCase, route, true)
|
|
}
|
|
|
|
// sendAndResumeBlindedPayment sends a blinded payment through the test
|
|
// network provided, intercepting the payment at Carol and allowing it to
|
|
// resume. This utility function allows us to ensure that payments at least
|
|
// reach Carol and asserts that all errors appear to originate from the
|
|
// introduction node.
|
|
func sendAndResumeBlindedPayment(ctx context.Context, ht *lntest.HarnessTest,
|
|
testCase *blindedForwardTest, route *routing.BlindedPayment,
|
|
interceptAtCarol bool) {
|
|
|
|
blindedRoute := testCase.createRouteToBlinded(10_000_000, route)
|
|
|
|
// Before we dispatch the payment, spin up a goroutine that will
|
|
// intercept the HTLC on Carol's forward. This allows us to ensure
|
|
// that the HTLC actually reaches the location we expect it to.
|
|
var resolveHTLC func(routerrpc.ResolveHoldForwardAction)
|
|
if interceptAtCarol {
|
|
resolveHTLC = testCase.interceptFinalHop()
|
|
}
|
|
|
|
// First, test sending the payment all the way through to Dave. We
|
|
// expect this payment to fail, because he does not know how to
|
|
// process payments to a blinded route (not yet supported).
|
|
cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute)
|
|
defer cancelPmt()
|
|
|
|
// When Carol intercepts the HTLC, instruct her to resume the payment
|
|
// so that it'll reach Dave and fail.
|
|
if interceptAtCarol {
|
|
resolveHTLC(routerrpc.ResolveHoldForwardAction_RESUME)
|
|
}
|
|
|
|
// Wait for the HTLC to reflect as failed for Alice.
|
|
preimage, err := lntypes.MakePreimage(testCase.preimage[:])
|
|
require.NoError(ht, err)
|
|
pmt := ht.AssertPaymentStatus(ht.Alice, preimage, lnrpc.Payment_FAILED)
|
|
require.Len(ht, pmt.Htlcs, 1)
|
|
require.EqualValues(
|
|
ht, 1, pmt.Htlcs[0].Failure.FailureSourceIndex,
|
|
)
|
|
require.Equal(
|
|
ht, lnrpc.Failure_INVALID_ONION_BLINDING,
|
|
pmt.Htlcs[0].Failure.Code,
|
|
)
|
|
}
|
|
|
|
// testIntroductionNodeError tests handling of errors in a blinded route when
|
|
// the introduction node is the source of the error. This test sends a payment
|
|
// over Alice -- Bob -- Carol -- Dave, where Bob is the introduction node and
|
|
// has insufficient outgoing liquidity to forward on to carol.
|
|
func testIntroductionNodeError(ht *lntest.HarnessTest) {
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
route := testCase.setup(ctx)
|
|
|
|
// Before we send our payment, drain all of Carol's incoming liquidity
|
|
// so that she can't receive the forward from Bob, causing a failure
|
|
// at the introduction node.
|
|
testCase.drainCarolLiquidity(true)
|
|
|
|
// Send the payment, but do not expect it to reach Carol at all.
|
|
sendAndResumeBlindedPayment(ctx, ht, testCase, route, false)
|
|
}
|
|
|
|
// testDisableIntroductionNode tests disabling of blinded forwards for the
|
|
// introduction node.
|
|
func testDisableIntroductionNode(ht *lntest.HarnessTest) {
|
|
// Disable route blinding for Bob, then re-connect to Alice.
|
|
ht.RestartNodeWithExtraArgs(ht.Bob, []string{
|
|
"--protocol.no-route-blinding",
|
|
})
|
|
ht.EnsureConnected(ht.Alice, ht.Bob)
|
|
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
route := testCase.setup(ctx)
|
|
// We always expect failures to look like they originated at Bob
|
|
// because blinded errors are converted. However, our tests intercepts
|
|
// all of Carol's forwards and we're not providing it any interceptor
|
|
// instructions. This means that the test will hang/timeout at Carol
|
|
// if Bob _doesn't_ fail the HTLC back as expected.
|
|
sendAndResumeBlindedPayment(ctx, ht, testCase, route, false)
|
|
}
|
|
|
|
// testErrorHandlingOnChainFailure tests handling of blinded errors when we're
|
|
// resolving from an on-chain resolution. This test also tests that we're able
|
|
// to resolve blinded HTLCs on chain between restarts, as we've got all the
|
|
// infrastructure in place already for error testing.
|
|
func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) {
|
|
// Setup a test case, note that we don't use its built in clean up
|
|
// because we're going to close a channel so we'll close out the
|
|
// rest manually.
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
|
|
// Note that we send a larger amount here do it'll be worthwhile for
|
|
// the sweeper to claim.
|
|
route := testCase.setup(ctx)
|
|
blindedRoute := testCase.createRouteToBlinded(50_000_000, route)
|
|
|
|
// Once our interceptor is set up, we can send the blinded payment.
|
|
cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute)
|
|
defer cancelPmt()
|
|
|
|
// Wait for the HTLC to be active on Alice and Bob's channels.
|
|
hash := sha256.Sum256(testCase.preimage[:])
|
|
ht.AssertOutgoingHTLCActive(ht.Alice, testCase.channels[0], hash[:])
|
|
ht.AssertOutgoingHTLCActive(ht.Bob, testCase.channels[1], hash[:])
|
|
|
|
// Intercept the forward on Carol's link, but do not take any action
|
|
// so that we have the chance to force close with this HTLC in flight.
|
|
carolHTLC, err := testCase.carolInterceptor.Recv()
|
|
require.NoError(ht, err)
|
|
|
|
// Force close Bob <-> Carol.
|
|
closeStream, _ := ht.CloseChannelAssertPending(
|
|
ht.Bob, testCase.channels[1], true,
|
|
)
|
|
|
|
ht.AssertStreamChannelForceClosed(
|
|
ht.Bob, testCase.channels[1], false, closeStream,
|
|
)
|
|
|
|
// SuspendCarol so that she can't interfere with the resolution of the
|
|
// HTLC from now on.
|
|
restartCarol := ht.SuspendNode(testCase.carol)
|
|
|
|
// Mine blocks so that Bob will claim his CSV delayed local commitment,
|
|
// we've already mined 1 block so we need one less than our CSV.
|
|
ht.MineBlocks(node.DefaultCSV - 1)
|
|
ht.AssertNumPendingSweeps(ht.Bob, 1)
|
|
ht.MineEmptyBlocks(1)
|
|
ht.Miner.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
// Restart bob so that we can test that he's able to recover everything
|
|
// he needs to claim a blinded HTLC.
|
|
ht.RestartNode(ht.Bob)
|
|
|
|
// Mine enough blocks for Bob to trigger timeout of his outgoing HTLC.
|
|
// Carol's incoming expiry height is Bob's outgoing so we can use this
|
|
// value.
|
|
info := ht.Bob.RPC.GetInfo()
|
|
target := carolHTLC.IncomingExpiry - info.BlockHeight
|
|
ht.MineBlocks(target)
|
|
|
|
// Wait for Bob's timeout transaction in the mempool, since we've
|
|
// suspended Carol we don't need to account for her commitment output
|
|
// claim.
|
|
ht.Miner.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
// Assert that the HTLC has cleared.
|
|
ht.AssertHTLCNotActive(ht.Alice, testCase.channels[0], hash[:])
|
|
ht.AssertHTLCNotActive(ht.Bob, testCase.channels[0], hash[:])
|
|
|
|
// Wait for the HTLC to reflect as failed for Alice.
|
|
paymentStream := ht.Alice.RPC.TrackPaymentV2(hash[:])
|
|
htlcs := ht.ReceiveTrackPayment(paymentStream).Htlcs
|
|
require.Len(ht, htlcs, 1)
|
|
require.NotNil(ht, htlcs[0].Failure)
|
|
require.Equal(
|
|
ht, htlcs[0].Failure.Code,
|
|
lnrpc.Failure_INVALID_ONION_BLINDING,
|
|
)
|
|
|
|
// Clean up the rest of our force close: mine blocks so that Bob's CSV
|
|
// expires plus one block to trigger his sweep and then mine it.
|
|
ht.MineBlocks(node.DefaultCSV + 1)
|
|
ht.Miner.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
// Bring carol back up so that we can close out the rest of our
|
|
// channels cooperatively. She requires an interceptor to start up
|
|
// so we just re-register our interceptor.
|
|
require.NoError(ht, restartCarol())
|
|
_, err = testCase.carol.RPC.Router.HtlcInterceptor(ctx)
|
|
require.NoError(ht, err, "interceptor")
|
|
|
|
// Assert that Carol has started up and reconnected to dave so that
|
|
// we can close out channels cooperatively.
|
|
ht.EnsureConnected(testCase.carol, testCase.dave)
|
|
|
|
// Manually close out the rest of our channels and cancel (don't use
|
|
// built in cleanup which will try close the already-force-closed
|
|
// channel).
|
|
ht.CloseChannel(ht.Alice, testCase.channels[0])
|
|
ht.CloseChannel(testCase.carol, testCase.channels[2])
|
|
testCase.cancel()
|
|
}
|