mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
1464 lines
45 KiB
Go
1464 lines
45 KiB
Go
package itest
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"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/lightningnetwork/lnd/lntest/node"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// toLocalCSV is the CSV delay for the node's to_local output. We use a small
|
|
// value to save us from mining blocks.
|
|
var toLocalCSV = 2
|
|
|
|
// 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) {
|
|
alice := ht.NewNodeWithCoins("Alice", nil)
|
|
bob := ht.NewNodeWithCoins("Bob", nil)
|
|
ht.EnsureConnected(alice, 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.AssertChannelInGraph(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 won't be able to decrypt the dummy encrypted data
|
|
// containing the forwarding information, we expect her to fail the
|
|
// payment.
|
|
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)
|
|
}
|
|
|
|
type blindedForwardTest struct {
|
|
ht *lntest.HarnessTest
|
|
alice *node.HarnessNode
|
|
bob *node.HarnessNode
|
|
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},
|
|
}
|
|
}
|
|
|
|
// setupNetwork spins up additional nodes needed for our test and creates a four
|
|
// hop network for testing blinded path logic and an optional interceptor on
|
|
// Carol's node for those tests where we want to perhaps prevent the final hop
|
|
// from settling.
|
|
func (b *blindedForwardTest) setupNetwork(ctx context.Context,
|
|
withInterceptor bool) {
|
|
|
|
carolArgs := []string{
|
|
"--bitcoin.timelockdelta=18",
|
|
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV),
|
|
}
|
|
if withInterceptor {
|
|
carolArgs = append(carolArgs, "--requireinterceptor")
|
|
}
|
|
|
|
daveArgs := []string{
|
|
"--bitcoin.timelockdelta=18",
|
|
fmt.Sprintf("--bitcoin.defaultremotedelay=%v", toLocalCSV),
|
|
}
|
|
cfgs := [][]string{nil, nil, carolArgs, daveArgs}
|
|
param := lntest.OpenChannelParams{
|
|
Amt: chanAmt,
|
|
}
|
|
|
|
// Creates a network with the following topology and liquidity:
|
|
// Alice (100k)----- Bob (100k) ----- Carol (100k) ----- Dave
|
|
chanPoints, nodes := b.ht.CreateSimpleNetwork(cfgs, param)
|
|
b.channels = chanPoints
|
|
b.alice, b.bob, b.carol, b.dave = nodes[0], nodes[1], nodes[2], nodes[3]
|
|
|
|
if withInterceptor {
|
|
var err error
|
|
b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(
|
|
ctx,
|
|
)
|
|
require.NoError(b.ht, err, "interceptor")
|
|
}
|
|
}
|
|
|
|
// buildBlindedPath returns a blinded route from Bob -> Carol -> Dave, with Bob
|
|
// acting as the introduction point.
|
|
func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath {
|
|
// Let Dave add a blinded invoice.
|
|
// Add restrictions so that he only ever creates a single blinded path
|
|
// from Bob to himself.
|
|
var (
|
|
minNumRealHops uint32 = 2
|
|
numHops uint32 = 2
|
|
)
|
|
invoice := b.dave.RPC.AddInvoice(&lnrpc.Invoice{
|
|
RPreimage: b.preimage[:],
|
|
Memo: "test",
|
|
ValueMsat: 10_000_000,
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
MinNumRealHops: &minNumRealHops,
|
|
NumHops: &numHops,
|
|
},
|
|
})
|
|
|
|
// Assert that only one blinded path is selected and that it contains
|
|
// a 3 hop path starting at Bob.
|
|
payReq := b.dave.RPC.DecodePayReq(invoice.PaymentRequest)
|
|
require.Len(b.ht, payReq.BlindedPaths, 1)
|
|
path := payReq.BlindedPaths[0].BlindedPath
|
|
require.Len(b.ht, path.BlindedHops, 3)
|
|
require.EqualValues(b.ht, path.IntroductionNode, b.bob.PubKey[:])
|
|
|
|
return payReq.BlindedPaths[0]
|
|
}
|
|
|
|
// cleanup tears down all channels created by the test and cancels the top
|
|
// level context used in the test.
|
|
func (b *blindedForwardTest) cleanup() {
|
|
b.cancel()
|
|
}
|
|
|
|
// createRouteToBlinded queries for a route from alice to the blinded path
|
|
// provided.
|
|
//
|
|
//nolint:gomnd
|
|
func (b *blindedForwardTest) createRouteToBlinded(paymentAmt int64,
|
|
blindedPath *lnrpc.BlindedPaymentPath) *lnrpc.Route {
|
|
|
|
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.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.alice.RPC.Router.SendToRouteV2(ctx, sendReq)
|
|
|
|
// We may get a context canceled error when the test is
|
|
// finished.
|
|
if errors.Is(err, context.Canceled) {
|
|
b.ht.Logf("sendBlindedPayment: parent context canceled")
|
|
return
|
|
}
|
|
|
|
require.NoError(b.ht, err)
|
|
}()
|
|
|
|
return cancel
|
|
}
|
|
|
|
// sendToRoute synchronously lets Alice attempt to send to the given route
|
|
// using the SendToRouteV2 endpoint and asserts that the payment either
|
|
// succeeds or fails.
|
|
func (b *blindedForwardTest) sendToRoute(route *lnrpc.Route,
|
|
assertSuccess bool) {
|
|
|
|
hash := sha256.Sum256(b.preimage[:])
|
|
sendReq := &routerrpc.SendToRouteRequest{
|
|
PaymentHash: hash[:],
|
|
Route: route,
|
|
}
|
|
|
|
// Let Alice send to the blinded payment path and assert that it
|
|
// succeeds/fails.
|
|
htlcAttempt := b.alice.RPC.SendToRouteV2(sendReq)
|
|
if assertSuccess {
|
|
require.Nil(b.ht, htlcAttempt.Failure)
|
|
require.Equal(b.ht, htlcAttempt.Status,
|
|
lnrpc.HTLCAttempt_SUCCEEDED)
|
|
|
|
return
|
|
}
|
|
|
|
require.NotNil(b.ht, htlcAttempt.Failure)
|
|
require.Equal(b.ht, htlcAttempt.Status, lnrpc.HTLCAttempt_FAILED)
|
|
|
|
// Wait for the HTLC to reflect as failed for Alice.
|
|
preimage, err := lntypes.MakePreimage(b.preimage[:])
|
|
require.NoError(b.ht, err)
|
|
|
|
pmt := b.ht.AssertPaymentStatus(
|
|
b.alice, preimage, lnrpc.Payment_FAILED,
|
|
)
|
|
require.Len(b.ht, pmt.Htlcs, 1)
|
|
|
|
// Assert that the failure appears to originate from the introduction
|
|
// node hop.
|
|
require.EqualValues(b.ht, 1, pmt.Htlcs[0].Failure.FailureSourceIndex)
|
|
require.Equal(
|
|
b.ht, lnrpc.Failure_INVALID_ONION_BLINDING,
|
|
pmt.Htlcs[0].Failure.Code,
|
|
)
|
|
}
|
|
|
|
// 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.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)
|
|
}
|
|
|
|
// testBlindedRouteInvoices tests lnd's ability to create a blinded payment path
|
|
// which it then inserts into an invoice, sending to an invoice with a blinded
|
|
// path and forward payments in a blinded route and finally, receiving the
|
|
// payment.
|
|
func testBlindedRouteInvoices(ht *lntest.HarnessTest) {
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
|
|
// Set up the 4 hop network and let Dave create an invoice with a
|
|
// blinded path that uses Bob as an introduction node.
|
|
testCase.setupNetwork(ctx, false)
|
|
|
|
alice := testCase.alice
|
|
|
|
// Let Dave add a blinded invoice.
|
|
// Add restrictions so that he only ever creates a single blinded path
|
|
// from Bob to himself.
|
|
var (
|
|
minNumRealHops uint32 = 2
|
|
numHops uint32 = 2
|
|
)
|
|
invoice := testCase.dave.RPC.AddInvoice(&lnrpc.Invoice{
|
|
Memo: "test",
|
|
ValueMsat: 10_000_000,
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
MinNumRealHops: &minNumRealHops,
|
|
NumHops: &numHops,
|
|
},
|
|
})
|
|
|
|
// Now let Alice pay the invoice.
|
|
ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest})
|
|
|
|
// Let Dave add a blinded invoice.
|
|
// Once again let Dave create a blinded invoice.
|
|
// This time, add path restrictions that will result in him
|
|
// creating a blinded path that uses himself as the introduction node.
|
|
minNumRealHops = 0
|
|
numHops = 0
|
|
invoice = testCase.dave.RPC.AddInvoice(&lnrpc.Invoice{
|
|
Memo: "test",
|
|
ValueMsat: 10_000_000,
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
MinNumRealHops: &minNumRealHops,
|
|
NumHops: &numHops,
|
|
},
|
|
})
|
|
|
|
// Assert that it contains a single blinded path with only an
|
|
// introduction node hop where the introduction node is Dave.
|
|
payReq := testCase.dave.RPC.DecodePayReq(invoice.PaymentRequest)
|
|
require.Len(ht, payReq.BlindedPaths, 1)
|
|
path := payReq.BlindedPaths[0].BlindedPath
|
|
require.Len(ht, path.BlindedHops, 1)
|
|
require.EqualValues(ht, path.IntroductionNode, testCase.dave.PubKey[:])
|
|
|
|
// Now let Alice pay the invoice.
|
|
ht.CompletePaymentRequests(alice, []string{invoice.PaymentRequest})
|
|
}
|
|
|
|
// testReceiverBlindedError 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.
|
|
func testReceiverBlindedError(ht *lntest.HarnessTest) {
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
|
|
testCase.setupNetwork(ctx, false)
|
|
blindedPaymentPath := testCase.buildBlindedPath()
|
|
route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
|
|
|
|
// Replace the encrypted recipient data payload for Dave (the recipient)
|
|
// with an invalid payload which Dave will then fail to parse when he
|
|
// receives the incoming HTLC for this payment.
|
|
route.Hops[len(route.Hops)-1].EncryptedData = []byte{1, 2, 3}
|
|
|
|
// Subscribe to Dave's HTLC events so that we can observe the payment
|
|
// coming in.
|
|
daveEvents := testCase.dave.RPC.SubscribeHtlcEvents()
|
|
|
|
// Once subscribed, the first event will be UNKNOWN.
|
|
ht.AssertHtlcEventType(daveEvents, routerrpc.HtlcEvent_UNKNOWN)
|
|
|
|
// Let Alice send to the constructed route and assert that the payment
|
|
// fails.
|
|
testCase.sendToRoute(route, false)
|
|
|
|
// Make sure that the HTLC did in fact reach Dave and fail there.
|
|
ht.AssertHtlcEvents(daveEvents, 0, 0, 0, 1, routerrpc.HtlcEvent_FORWARD)
|
|
}
|
|
|
|
// 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()
|
|
|
|
testCase.setupNetwork(ctx, false)
|
|
blindedPaymentPath := testCase.buildBlindedPath()
|
|
route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
|
|
|
|
// Before we send our payment, drain all of Carol's liquidity
|
|
// so that she can't forward the payment to Dave.
|
|
testCase.drainCarolLiquidity(false)
|
|
|
|
// Subscribe to Carol's HTLC events so that we can observe the payment
|
|
// coming in.
|
|
carolEvents := testCase.carol.RPC.SubscribeHtlcEvents()
|
|
|
|
// Once subscribed, the first event will be UNKNOWN.
|
|
ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_UNKNOWN)
|
|
|
|
// Let Alice send to the constructed route and assert that the payment
|
|
// fails.
|
|
testCase.sendToRoute(route, false)
|
|
|
|
// Make sure that the HTLC did in fact reach Carol and fail there.
|
|
ht.AssertHtlcEvents(
|
|
carolEvents, 0, 0, 0, 1, routerrpc.HtlcEvent_FORWARD,
|
|
)
|
|
}
|
|
|
|
// 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()
|
|
testCase.setupNetwork(ctx, false)
|
|
blindedPaymentPath := testCase.buildBlindedPath()
|
|
route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
|
|
|
|
bob := testCase.bob
|
|
|
|
// 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)
|
|
|
|
// Subscribe to Bob's HTLC events so that we can observe the payment
|
|
// coming in.
|
|
bobEvents := bob.RPC.SubscribeHtlcEvents()
|
|
|
|
// Once subscribed, the first event will be UNKNOWN.
|
|
ht.AssertHtlcEventType(bobEvents, routerrpc.HtlcEvent_UNKNOWN)
|
|
|
|
// Let Alice send to the constructed route and assert that the payment
|
|
// fails.
|
|
testCase.sendToRoute(route, false)
|
|
|
|
// Make sure that the HTLC did in fact reach Bob and fail there.
|
|
ht.AssertHtlcEvents(
|
|
bobEvents, 0, 0, 0, 1, routerrpc.HtlcEvent_FORWARD,
|
|
)
|
|
}
|
|
|
|
// testDisableIntroductionNode tests disabling of blinded forwards for the
|
|
// introduction node.
|
|
func testDisableIntroductionNode(ht *lntest.HarnessTest) {
|
|
// First construct a blinded route while Bob is still advertising the
|
|
// route blinding feature bit to ensure that Bob is included in the
|
|
// blinded path that Dave selects.
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
testCase.setupNetwork(ctx, false)
|
|
blindedPaymentPath := testCase.buildBlindedPath()
|
|
route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
|
|
|
|
alice, bob := testCase.alice, testCase.bob
|
|
|
|
// Now, disable route blinding for Bob, then re-connect to Alice.
|
|
ht.RestartNodeWithExtraArgs(bob, []string{
|
|
"--protocol.no-route-blinding",
|
|
})
|
|
ht.EnsureConnected(alice, bob)
|
|
|
|
// Assert that this fails.
|
|
testCase.sendToRoute(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, so it'll be worthwhile for
|
|
// the sweeper to claim.
|
|
testCase.setupNetwork(ctx, true)
|
|
blindedPaymentPath := testCase.buildBlindedPath()
|
|
blindedRoute := testCase.createRouteToBlinded(
|
|
50_000_000, blindedPaymentPath,
|
|
)
|
|
|
|
alice, bob := testCase.alice, testCase.bob
|
|
|
|
// 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(alice, testCase.channels[0], hash[:])
|
|
ht.AssertOutgoingHTLCActive(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(
|
|
bob, testCase.channels[1], true,
|
|
)
|
|
|
|
ht.AssertStreamChannelForceClosed(
|
|
bob, testCase.channels[1], false, closeStream,
|
|
)
|
|
|
|
// SuspendCarol so that she can't interfere with the resolution of the
|
|
// HTLC from now on.
|
|
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(toLocalCSV - 1)
|
|
ht.AssertNumPendingSweeps(bob, 1)
|
|
ht.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(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 := bob.RPC.GetInfo()
|
|
target := carolHTLC.IncomingExpiry - info.BlockHeight
|
|
ht.Log(carolHTLC.IncomingExpiry, info.BlockHeight, target)
|
|
ht.MineBlocks(int(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.AssertNumPendingSweeps(bob, 0)
|
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
// Assert that the HTLC has cleared.
|
|
ht.AssertHTLCNotActive(bob, testCase.channels[0], hash[:])
|
|
ht.AssertHTLCNotActive(alice, testCase.channels[0], hash[:])
|
|
|
|
// Wait for the HTLC to reflect as failed for Alice.
|
|
paymentStream := 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,
|
|
)
|
|
|
|
// 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).
|
|
testCase.cancel()
|
|
}
|
|
|
|
// testMPPToSingleBlindedPath tests that a two-shard MPP payment can be sent
|
|
// over a single blinded path.
|
|
// The following graph is created where Dave is the destination node, and he
|
|
// will choose Carol as the introduction node. The channel capacities are set in
|
|
// such a way that Alice will have to split the payment to dave over both the
|
|
// A->B->C-D and A->E->C->D routes. The Carol-Dave channel will also be made
|
|
// a private channel so that we can test that Dave's private channels are in
|
|
// fact being used in the chosen blinded paths.
|
|
//
|
|
// ---- Bob ---
|
|
// / \
|
|
// Alice Carol --- Dave
|
|
// \ /
|
|
// ---- Eve ---
|
|
func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
|
|
// Create a five-node context consisting of Alice, Bob and three new
|
|
// nodes.
|
|
alice := ht.NewNode("Alice", nil)
|
|
bob := ht.NewNode("Bob", nil)
|
|
dave := ht.NewNode("dave", nil)
|
|
carol := ht.NewNode("carol", nil)
|
|
eve := ht.NewNode("eve", nil)
|
|
|
|
// Connect nodes to ensure propagation of channels.
|
|
ht.EnsureConnected(alice, bob)
|
|
ht.EnsureConnected(alice, eve)
|
|
ht.EnsureConnected(carol, bob)
|
|
ht.EnsureConnected(carol, eve)
|
|
ht.EnsureConnected(carol, dave)
|
|
|
|
// Send coins to the nodes and mine 1 blocks to confirm them.
|
|
for i := 0; i < 2; i++ {
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, alice)
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, bob)
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, eve)
|
|
ht.MineBlocksAndAssertNumTxes(1, 5)
|
|
}
|
|
|
|
const paymentAmt = btcutil.Amount(300000)
|
|
|
|
nodes := []*node.HarnessNode{alice, bob, carol, dave, eve}
|
|
reqs := []*lntest.OpenChannelRequest{
|
|
{
|
|
Local: alice,
|
|
Remote: bob,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2 / 3,
|
|
},
|
|
},
|
|
{
|
|
Local: alice,
|
|
Remote: eve,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2 / 3,
|
|
},
|
|
},
|
|
{
|
|
Local: bob,
|
|
Remote: carol,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2,
|
|
},
|
|
},
|
|
{
|
|
Local: eve,
|
|
Remote: carol,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2,
|
|
},
|
|
},
|
|
{
|
|
// Note that this is a private channel.
|
|
Local: carol,
|
|
Remote: dave,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2,
|
|
Private: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
channelPoints := ht.OpenMultiChannelsAsync(reqs)
|
|
|
|
// Make sure every node has heard about every public channel.
|
|
for _, hn := range nodes {
|
|
var numPublic int
|
|
for i, cp := range channelPoints {
|
|
if reqs[i].Param.Private {
|
|
continue
|
|
}
|
|
|
|
numPublic++
|
|
ht.AssertChannelInGraph(hn, cp)
|
|
}
|
|
|
|
// Each node should have exactly numPublic edges.
|
|
ht.AssertNumEdges(hn, numPublic, false)
|
|
}
|
|
|
|
// Make Dave create an invoice with a blinded path for Alice to pay.
|
|
// Restrict the blinded path config such that Dave only ever chooses
|
|
// the Carol->Dave path for a blinded route.
|
|
var (
|
|
numHops uint32 = 1
|
|
minNumRealHops uint32 = 1
|
|
)
|
|
invoice := &lnrpc.Invoice{
|
|
Memo: "test",
|
|
Value: int64(paymentAmt),
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
NumHops: &numHops,
|
|
MinNumRealHops: &minNumRealHops,
|
|
},
|
|
}
|
|
invoiceResp := dave.RPC.AddInvoice(invoice)
|
|
|
|
sendReq := &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: invoiceResp.PaymentRequest,
|
|
MaxParts: 10,
|
|
TimeoutSeconds: 60,
|
|
FeeLimitMsat: noFeeLimitMsat,
|
|
}
|
|
payment := ht.SendPaymentAssertSettled(alice, sendReq)
|
|
|
|
preimageBytes, err := hex.DecodeString(payment.PaymentPreimage)
|
|
require.NoError(ht, err)
|
|
|
|
preimage, err := lntypes.MakePreimage(preimageBytes)
|
|
require.NoError(ht, err)
|
|
|
|
hash, err := lntypes.MakeHash(invoiceResp.RHash)
|
|
require.NoError(ht, err)
|
|
|
|
// Make sure we got the preimage.
|
|
require.True(ht, preimage.Matches(hash), "preimage doesn't match")
|
|
|
|
// Check that Alice split the payment in at least two shards. Because
|
|
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
|
|
// there is some non-determinism in the process. Depending on whether
|
|
// the new pathfinding round is started before or after the htlc is
|
|
// locked into the channel, different sharding may occur. Therefore, we
|
|
// can only check if the number of shards isn't below the theoretical
|
|
// minimum.
|
|
succeeded := 0
|
|
for _, htlc := range payment.Htlcs {
|
|
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
|
succeeded++
|
|
}
|
|
}
|
|
|
|
const minExpectedShards = 2
|
|
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
|
|
"expected shards not reached")
|
|
|
|
// Make sure Dave show the invoice as settled for the full amount.
|
|
inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
|
|
|
|
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
|
|
"incorrect payment amt")
|
|
|
|
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
|
|
"Invoice not settled")
|
|
|
|
settled := 0
|
|
for _, htlc := range inv.Htlcs {
|
|
if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
|
|
settled++
|
|
}
|
|
}
|
|
require.Equal(ht, succeeded, settled, "num of HTLCs wrong")
|
|
|
|
// Close all channels without mining the closing transactions.
|
|
ht.CloseChannelAssertPending(alice, channelPoints[0], false)
|
|
ht.CloseChannelAssertPending(alice, channelPoints[1], false)
|
|
ht.CloseChannelAssertPending(bob, channelPoints[2], false)
|
|
ht.CloseChannelAssertPending(eve, channelPoints[3], false)
|
|
ht.CloseChannelAssertPending(carol, channelPoints[4], false)
|
|
|
|
// Now mine a block to include all the closing transactions.
|
|
ht.MineBlocksAndAssertNumTxes(1, 5)
|
|
|
|
// Assert that the channels are closed.
|
|
for _, hn := range nodes {
|
|
ht.AssertNumWaitingClose(hn, 0)
|
|
}
|
|
}
|
|
|
|
// testBlindedRouteDummyHops tests that the route blinding flow works as
|
|
// expected in the cases where the recipient chooses to pad the blinded path
|
|
// with dummy hops.
|
|
//
|
|
// We will set up the following network were Dave will always be the recipient
|
|
// and Alice is always the payer. Bob will not support route blinding and so
|
|
// will never be chosen as the introduction node.
|
|
//
|
|
// Alice -- Bob -- Carol -- Dave
|
|
//
|
|
// First we will start Carol _without_ route blinding support. Then we will
|
|
// configure Dave such that any blinded route he constructs must be 2 hops long.
|
|
// Since Carol cannot be chosen as an introduction node, Dave chooses himself
|
|
// as an introduction node and appends two dummy hops.
|
|
// Next, we will restart Carol with route blinding support and repeat the test
|
|
// but this time we force Dave to construct a path with a minimum distance of 1
|
|
// between him and the introduction node. So we expect that Carol is chosen as
|
|
// the intro node and that one dummy hops is appended.
|
|
func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
|
|
alice := ht.NewNodeWithCoins("Alice", nil)
|
|
|
|
// Disable route blinding for Bob so that he is never chosen as the
|
|
// introduction node.
|
|
bob := ht.NewNodeWithCoins("Bob", []string{
|
|
"--protocol.no-route-blinding",
|
|
})
|
|
|
|
// Start carol without route blinding so that initially Carol is also
|
|
// not chosen.
|
|
carol := ht.NewNode("carol", []string{
|
|
"--protocol.no-route-blinding",
|
|
})
|
|
|
|
dave := ht.NewNode("dave", nil)
|
|
|
|
ht.EnsureConnected(alice, bob)
|
|
ht.EnsureConnected(bob, carol)
|
|
ht.EnsureConnected(carol, dave)
|
|
|
|
// Send coins to carol and mine 1 blocks to confirm them.
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
|
|
ht.MineBlocksAndAssertNumTxes(1, 1)
|
|
|
|
const paymentAmt = btcutil.Amount(300000)
|
|
|
|
nodes := []*node.HarnessNode{alice, bob, carol, dave}
|
|
reqs := []*lntest.OpenChannelRequest{
|
|
{
|
|
Local: alice,
|
|
Remote: bob,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 3,
|
|
},
|
|
},
|
|
{
|
|
Local: bob,
|
|
Remote: carol,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 3,
|
|
},
|
|
},
|
|
{
|
|
Local: carol,
|
|
Remote: dave,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 3,
|
|
},
|
|
},
|
|
}
|
|
|
|
channelPoints := ht.OpenMultiChannelsAsync(reqs)
|
|
|
|
// Make sure every node has heard about every channel.
|
|
for _, hn := range nodes {
|
|
for _, cp := range channelPoints {
|
|
ht.AssertChannelInGraph(hn, cp)
|
|
}
|
|
|
|
// Each node should have exactly 5 edges.
|
|
ht.AssertNumEdges(hn, len(channelPoints), false)
|
|
}
|
|
|
|
// Make Dave create an invoice with a blinded path for Alice to pay.
|
|
// Configure the invoice so that all blinded paths always contain 2 hops
|
|
// and so that there is no minimum number of real hops.
|
|
var (
|
|
minNumRealHops uint32 = 0
|
|
numHops uint32 = 2
|
|
)
|
|
invoice := &lnrpc.Invoice{
|
|
Memo: "test",
|
|
Value: int64(paymentAmt),
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
MinNumRealHops: &minNumRealHops,
|
|
NumHops: &numHops,
|
|
},
|
|
}
|
|
invoiceResp := dave.RPC.AddInvoice(invoice)
|
|
|
|
// Assert that it contains a single blinded path and that the
|
|
// introduction node is dave.
|
|
payReq := dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
|
|
require.Len(ht, payReq.BlindedPaths, 1)
|
|
|
|
// The total number of hop payloads is 3: one for the introduction node
|
|
// and then one for each dummy hop.
|
|
path := payReq.BlindedPaths[0].BlindedPath
|
|
require.Len(ht, path.BlindedHops, 3)
|
|
require.EqualValues(ht, path.IntroductionNode, dave.PubKey[:])
|
|
|
|
// Now let Alice pay the invoice.
|
|
ht.CompletePaymentRequests(
|
|
alice, []string{invoiceResp.PaymentRequest},
|
|
)
|
|
|
|
// Make sure Dave show the invoice as settled.
|
|
inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
|
|
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State)
|
|
|
|
// Let's also test the case where Dave is not the introduction node.
|
|
// We restart Carol so that she supports route blinding.
|
|
ht.RestartNodeWithExtraArgs(carol, nil)
|
|
ht.EnsureConnected(bob, carol)
|
|
ht.EnsureConnected(carol, dave)
|
|
|
|
// Make Dave create an invoice with a blinded path for Alice to pay.
|
|
// This time, configure the invoice so that there is always a minimum
|
|
// of 1 real blinded hop. We keep the number of total hops to 2 meaning
|
|
// that one dummy hop should be added.
|
|
minNumRealHops = 1
|
|
invoice = &lnrpc.Invoice{
|
|
Memo: "test",
|
|
Value: int64(paymentAmt),
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
MinNumRealHops: &minNumRealHops,
|
|
NumHops: &numHops,
|
|
},
|
|
}
|
|
invoiceResp = dave.RPC.AddInvoice(invoice)
|
|
|
|
// Assert that it contains a single blinded path and that the
|
|
// introduction node is Carol.
|
|
payReq = dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
|
|
require.Len(ht, payReq.BlindedPaths, 1)
|
|
|
|
// The total number of hop payloads is 3: one for the introduction node
|
|
// and then one for each dummy hop.
|
|
path = payReq.BlindedPaths[0].BlindedPath
|
|
require.Len(ht, path.BlindedHops, 3)
|
|
require.EqualValues(ht, path.IntroductionNode, carol.PubKey[:])
|
|
|
|
// Now let Alice pay the invoice.
|
|
ht.CompletePaymentRequests(
|
|
alice, []string{invoiceResp.PaymentRequest},
|
|
)
|
|
|
|
// Make sure Dave show the invoice as settled.
|
|
inv = dave.RPC.LookupInvoice(invoiceResp.RHash)
|
|
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State)
|
|
|
|
// Close all channels without mining the closing transactions.
|
|
ht.CloseChannelAssertPending(alice, channelPoints[0], false)
|
|
ht.CloseChannelAssertPending(bob, channelPoints[1], false)
|
|
ht.CloseChannelAssertPending(carol, channelPoints[2], false)
|
|
|
|
// Now mine a block to include all the closing transactions.
|
|
ht.MineBlocksAndAssertNumTxes(1, 3)
|
|
|
|
// Assert that the channels are closed.
|
|
for _, hn := range nodes {
|
|
ht.AssertNumWaitingClose(hn, 0)
|
|
}
|
|
}
|
|
|
|
// testMPPToMultipleBlindedPaths tests that a two-shard MPP payment can be sent
|
|
// over a multiple blinded paths. The following network is created where Dave
|
|
// is the recipient and Alice the sender. Dave will create an invoice containing
|
|
// two blinded paths: one with Bob at the intro node and one with Carol as the
|
|
// intro node. Channel liquidity will be set up in such a way that Alice will be
|
|
// forced to send one shared via the Bob->Dave route and one over the
|
|
// Carol->Dave route.
|
|
//
|
|
// --- Bob ---
|
|
// / \
|
|
// Alice Dave
|
|
// \ /
|
|
// --- Carol ---
|
|
func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
|
|
alice := ht.NewNodeWithCoins("Alice", nil)
|
|
bob := ht.NewNodeWithCoins("Bob", nil)
|
|
|
|
// Create a four-node context consisting of Alice, Bob and three new
|
|
// nodes.
|
|
dave := ht.NewNode("dave", nil)
|
|
carol := ht.NewNode("carol", nil)
|
|
|
|
// Connect nodes to ensure propagation of channels.
|
|
ht.EnsureConnected(alice, carol)
|
|
ht.EnsureConnected(alice, bob)
|
|
ht.EnsureConnected(carol, dave)
|
|
ht.EnsureConnected(bob, dave)
|
|
|
|
// Fund the new nodes.
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
|
|
ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
|
|
ht.MineBlocksAndAssertNumTxes(1, 2)
|
|
|
|
const paymentAmt = btcutil.Amount(300000)
|
|
|
|
nodes := []*node.HarnessNode{alice, bob, carol, dave}
|
|
|
|
reqs := []*lntest.OpenChannelRequest{
|
|
{
|
|
Local: alice,
|
|
Remote: bob,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2 / 3,
|
|
},
|
|
},
|
|
{
|
|
Local: alice,
|
|
Remote: carol,
|
|
Param: lntest.OpenChannelParams{
|
|
Amt: paymentAmt * 2 / 3,
|
|
},
|
|
},
|
|
{
|
|
Local: bob,
|
|
Remote: dave,
|
|
Param: lntest.OpenChannelParams{Amt: paymentAmt * 2},
|
|
},
|
|
{
|
|
Local: carol,
|
|
Remote: dave,
|
|
Param: lntest.OpenChannelParams{Amt: paymentAmt * 2},
|
|
},
|
|
}
|
|
|
|
channelPoints := ht.OpenMultiChannelsAsync(reqs)
|
|
|
|
// Make sure every node has heard every channel.
|
|
for _, hn := range nodes {
|
|
for _, cp := range channelPoints {
|
|
ht.AssertChannelInGraph(hn, cp)
|
|
}
|
|
|
|
// Each node should have exactly 5 edges.
|
|
ht.AssertNumEdges(hn, len(channelPoints), false)
|
|
}
|
|
|
|
// Ok now make a payment that must be split to succeed.
|
|
|
|
// Make Dave create an invoice for Alice to pay
|
|
var (
|
|
minNumRealHops uint32 = 1
|
|
numHops uint32 = 1
|
|
)
|
|
invoice := &lnrpc.Invoice{
|
|
Memo: "test",
|
|
Value: int64(paymentAmt),
|
|
IsBlinded: true,
|
|
BlindedPathConfig: &lnrpc.BlindedPathConfig{
|
|
MinNumRealHops: &minNumRealHops,
|
|
NumHops: &numHops,
|
|
},
|
|
}
|
|
invoiceResp := dave.RPC.AddInvoice(invoice)
|
|
|
|
// Assert that two blinded paths are included in the invoice.
|
|
payReq := dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
|
|
require.Len(ht, payReq.BlindedPaths, 2)
|
|
|
|
sendReq := &routerrpc.SendPaymentRequest{
|
|
PaymentRequest: invoiceResp.PaymentRequest,
|
|
MaxParts: 10,
|
|
TimeoutSeconds: 60,
|
|
FeeLimitMsat: noFeeLimitMsat,
|
|
}
|
|
payment := ht.SendPaymentAssertSettled(alice, sendReq)
|
|
|
|
preimageBytes, err := hex.DecodeString(payment.PaymentPreimage)
|
|
require.NoError(ht, err)
|
|
|
|
preimage, err := lntypes.MakePreimage(preimageBytes)
|
|
require.NoError(ht, err)
|
|
|
|
hash, err := lntypes.MakeHash(invoiceResp.RHash)
|
|
require.NoError(ht, err)
|
|
|
|
// Make sure we got the preimage.
|
|
require.True(ht, preimage.Matches(hash), "preimage doesn't match")
|
|
|
|
// Check that Alice split the payment in at least two shards. Because
|
|
// the hand-off of the htlc to the link is asynchronous (via a mailbox),
|
|
// there is some non-determinism in the process. Depending on whether
|
|
// the new pathfinding round is started before or after the htlc is
|
|
// locked into the channel, different sharding may occur. Therefore we
|
|
// can only check if the number of shards isn't below the theoretical
|
|
// minimum.
|
|
succeeded := 0
|
|
for _, htlc := range payment.Htlcs {
|
|
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
|
|
succeeded++
|
|
}
|
|
}
|
|
|
|
const minExpectedShards = 2
|
|
require.GreaterOrEqual(ht, succeeded, minExpectedShards,
|
|
"expected shards not reached")
|
|
|
|
// Make sure Dave show the invoice as settled for the full amount.
|
|
inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
|
|
|
|
require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
|
|
"incorrect payment amt")
|
|
|
|
require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
|
|
"Invoice not settled")
|
|
|
|
settled := 0
|
|
for _, htlc := range inv.Htlcs {
|
|
if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
|
|
settled++
|
|
}
|
|
}
|
|
require.Equal(ht, succeeded, settled, "num of HTLCs wrong")
|
|
|
|
// Close all channels without mining the closing transactions.
|
|
ht.CloseChannelAssertPending(alice, channelPoints[0], false)
|
|
ht.CloseChannelAssertPending(alice, channelPoints[1], false)
|
|
ht.CloseChannelAssertPending(bob, channelPoints[2], false)
|
|
ht.CloseChannelAssertPending(carol, channelPoints[3], false)
|
|
|
|
// Now mine a block to include all the closing transactions. (first
|
|
// iteration: no blinded paths)
|
|
ht.MineBlocksAndAssertNumTxes(1, 4)
|
|
|
|
// Assert that the channels are closed.
|
|
for _, hn := range nodes {
|
|
ht.AssertNumWaitingClose(hn, 0)
|
|
}
|
|
}
|
|
|
|
// testBlindedPaymentHTLCReForward tests that an UpdateAddHTLC message is
|
|
// correctly persisted, reloaded and resent to the next peer on restart in the
|
|
// case where the sending peer does not get a chance to send the message before
|
|
// restarting. This test specifically tests that this is done correctly for
|
|
// a blinded path payment since this adds a new blinding point field to
|
|
// UpdateAddHTLC which we need to ensure gets included in the message on
|
|
// restart.
|
|
func testBlindedPaymentHTLCReForward(ht *lntest.HarnessTest) {
|
|
// Setup a test case.
|
|
ctx, testCase := newBlindedForwardTest(ht)
|
|
defer testCase.cleanup()
|
|
|
|
// Set up network with carol interceptor.
|
|
testCase.setupNetwork(ctx, true)
|
|
|
|
alice, bob := testCase.alice, testCase.bob
|
|
|
|
// Let dave create invoice.
|
|
blindedPaymentPath := testCase.buildBlindedPath()
|
|
route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
|
|
|
|
// Once our interceptor is set up, we can send the blinded payment.
|
|
hash := sha256.Sum256(testCase.preimage[:])
|
|
sendReq := &routerrpc.SendToRouteRequest{
|
|
PaymentHash: hash[:],
|
|
Route: route,
|
|
}
|
|
|
|
// In a goroutine, we let Alice pay the invoice from dave.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
|
|
htlcAttempt, err := testCase.alice.RPC.Router.SendToRouteV2(
|
|
ctx, sendReq,
|
|
)
|
|
require.NoError(testCase.ht, err)
|
|
require.Equal(
|
|
testCase.ht, lnrpc.HTLCAttempt_SUCCEEDED,
|
|
htlcAttempt.Status,
|
|
)
|
|
}()
|
|
|
|
// Wait for the HTLC to be active on Alice and Bob's channels.
|
|
ht.AssertOutgoingHTLCActive(alice, testCase.channels[0], hash[:])
|
|
ht.AssertOutgoingHTLCActive(bob, testCase.channels[1], hash[:])
|
|
|
|
// Intercept the forward on Carol's link. At this point, we know she
|
|
// has received the HTLC and so will persist this packet.
|
|
_, err := testCase.carolInterceptor.Recv()
|
|
require.NoError(ht, err)
|
|
|
|
// Now, restart Carol. Carol should load up any previously persisted
|
|
// UpdateAddHTLC messages and continue the processing of them. This
|
|
// time, again with an interceptor because we need to make sure that
|
|
// the HTLC is forwarded after the nodes carol-dave are connected
|
|
// and their channel active. Otherwise the reforwarding of the HTLC
|
|
// would fail.
|
|
ht.RestartNode(testCase.carol)
|
|
|
|
// Register the client for the interceptor because the previous one
|
|
// closed during the restart.
|
|
carolInterceptor, err := testCase.carol.RPC.Router.HtlcInterceptor(
|
|
ctx,
|
|
)
|
|
require.NoError(ht, err, "interceptor")
|
|
|
|
// Intercept the replayed forward on Carol's link again.
|
|
carolHTLC, err := carolInterceptor.Recv()
|
|
require.NoError(ht, err)
|
|
|
|
// Nodes need to be connected otherwise the forwarding of the
|
|
// intercepted htlc will fail.
|
|
ht.EnsureConnected(bob, testCase.carol)
|
|
ht.EnsureConnected(testCase.carol, testCase.dave)
|
|
|
|
// Now that carol and dave are connected signal the forwarding of the
|
|
// HTLC.
|
|
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
|
IncomingCircuitKey: carolHTLC.IncomingCircuitKey,
|
|
Action: routerrpc.ResolveHoldForwardAction_RESUME,
|
|
})
|
|
require.NoError(ht, err)
|
|
|
|
// Now, wait for the payment to complete.
|
|
select {
|
|
case <-done:
|
|
case <-time.After(defaultTimeout):
|
|
require.Fail(ht, "timeout waiting for sending payment")
|
|
}
|
|
}
|