itest: update route blind tests

The route blinding itests are now updated so that recipient logic is
tested. The creation of a blinded route is also now done through the
AddInvoice API instead of manually.
This commit is contained in:
Elle Mouton 2024-05-06 16:27:31 +02:00
parent 65aef6a69c
commit 34d8fff5f9
No known key found for this signature in database
GPG Key ID: D7D916376026F177
4 changed files with 189 additions and 408 deletions

View File

@ -563,8 +563,8 @@ var allTestCases = []*lntest.TestCase{
TestFunc: testQueryBlindedRoutes, TestFunc: testQueryBlindedRoutes,
}, },
{ {
Name: "forward blinded", Name: "forward and receive blinded",
TestFunc: testForwardBlindedRoute, TestFunc: testForwardAndReceiveBlindedRoute,
}, },
{ {
Name: "receiver blinded error", Name: "receiver blinded error",

View File

@ -214,24 +214,24 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
// We expect Carol to have successful forwards and settles for // We expect Carol to have successful forwards and settles for
// her sends. // her sends.
ht.AssertHtlcEvents( ht.AssertHtlcEvents(
carolEvents, numPayments, 0, numPayments, carolEvents, numPayments, 0, numPayments, 0,
routerrpc.HtlcEvent_SEND, routerrpc.HtlcEvent_SEND,
) )
// Dave and Alice should both have forwards and settles for // Dave and Alice should both have forwards and settles for
// their role as forwarding nodes. // their role as forwarding nodes.
ht.AssertHtlcEvents( ht.AssertHtlcEvents(
daveEvents, numPayments, 0, numPayments, daveEvents, numPayments, 0, numPayments, 0,
routerrpc.HtlcEvent_FORWARD, routerrpc.HtlcEvent_FORWARD,
) )
ht.AssertHtlcEvents( ht.AssertHtlcEvents(
aliceEvents, numPayments, 0, numPayments, aliceEvents, numPayments, 0, numPayments, 0,
routerrpc.HtlcEvent_FORWARD, routerrpc.HtlcEvent_FORWARD,
) )
// Bob should only have settle events for his receives. // Bob should only have settle events for his receives.
ht.AssertHtlcEvents( ht.AssertHtlcEvents(
bobEvents, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE, bobEvents, 0, 0, numPayments, 0, routerrpc.HtlcEvent_RECEIVE,
) )
// Finally, close all channels. // Finally, close all channels.

View File

@ -1,7 +1,6 @@
package itest package itest
import ( import (
"bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@ -10,16 +9,12 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntypes" "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/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -273,9 +268,9 @@ func testQueryBlindedRoutes(ht *lntest.HarnessTest) {
htlcAttempt := alice.RPC.SendToRouteV2(sendReq) htlcAttempt := alice.RPC.SendToRouteV2(sendReq)
// Since Carol doesn't understand blinded routes, we expect her to fail // Since Carol won't be able to decrypt the dummy encrypted data
// the payment because the onion payload is invalid (missing amount to // containing the forwarding information, we expect her to fail the
// forward). // payment.
require.NotNil(ht, htlcAttempt.Failure) require.NotNil(ht, htlcAttempt.Failure)
require.Equal(ht, uint32(2), htlcAttempt.Failure.FailureSourceIndex) require.Equal(ht, uint32(2), htlcAttempt.Failure.FailureSourceIndex)
@ -347,41 +342,58 @@ func newBlindedForwardTest(ht *lntest.HarnessTest) (context.Context,
} }
} }
// setup spins up additional nodes needed for our test and creates a four hop // setupNetwork spins up additional nodes needed for our test and creates a four
// network for testing blinded forwarding and returns a blinded route from // hop network for testing blinded path logic and an optional interceptor on
// Bob -> Carol -> Dave, with Bob acting as the introduction point and an // Carol's node for those tests where we want to perhaps prevent the final hop
// interceptor on Carol's node to manage HTLCs (as Dave does not yet support // from settling.
// receiving). func (b *blindedForwardTest) setupNetwork(ctx context.Context,
func (b *blindedForwardTest) setup( withInterceptor bool) {
ctx context.Context) *routing.BlindedPayment {
b.carol = b.ht.NewNode("Carol", []string{ carolArgs := []string{"--bitcoin.timelockdelta=18"}
"--requireinterceptor", "--bitcoin.timelockdelta=18", if withInterceptor {
}) carolArgs = append(carolArgs, "--requireinterceptor")
}
b.carol = b.ht.NewNode("Carol", carolArgs)
var err error if withInterceptor {
b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(ctx) var err error
require.NoError(b.ht, err, "interceptor") b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(
ctx,
)
require.NoError(b.ht, err, "interceptor")
}
// Restrict Dave so that he only ever creates a single blinded path from
// Bob to himself.
b.dave = b.ht.NewNode("Dave", []string{ b.dave = b.ht.NewNode("Dave", []string{
"--bitcoin.timelockdelta=18", "--bitcoin.timelockdelta=18",
"--invoices.blinding.min-num-real-hops=2",
"--invoices.blinding.num-hops=2",
}) })
b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave) b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave)
}
// Create a blinded route to Dave via Bob --- Carol --- Dave: // buildBlindedPath returns a blinded route from Bob -> Carol -> Dave, with Bob
bobChan := b.ht.GetChannelByChanPoint(b.ht.Bob, b.channels[1]) // acting as the introduction point.
carolChan := b.ht.GetChannelByChanPoint(b.carol, b.channels[2]) func (b *blindedForwardTest) buildBlindedPath() *lnrpc.BlindedPaymentPath {
// Let Dave add a blinded invoice.
invoice := b.dave.RPC.AddInvoice(&lnrpc.Invoice{
RPreimage: b.preimage[:],
Memo: "test",
ValueMsat: 10_000_000,
Blind: true,
})
edges := []*forwardingEdge{ // Assert that only one blinded path is selected and that it contains
getForwardingEdge(b.ht, b.ht.Bob, bobChan.ChanId), // a 3 hop path starting at Bob.
getForwardingEdge(b.ht, b.carol, carolChan.ChanId), 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.ht.Bob.PubKey[:])
davePk, err := btcec.ParsePubKey(b.dave.PubKey[:]) return payReq.BlindedPaths[0]
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 // cleanup tears down all channels created by the test and cancels the top
@ -399,41 +411,12 @@ func (b *blindedForwardTest) cleanup() {
// //
//nolint:gomnd //nolint:gomnd
func (b *blindedForwardTest) createRouteToBlinded(paymentAmt int64, func (b *blindedForwardTest) createRouteToBlinded(paymentAmt int64,
route *routing.BlindedPayment) *lnrpc.Route { blindedPath *lnrpc.BlindedPaymentPath) *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{ req := &lnrpc.QueryRoutesRequest{
AmtMsat: paymentAmt, AmtMsat: paymentAmt,
// Our fee limit doesn't really matter, we just want to // Our fee limit doesn't really matter, we just want to be able
// be able to make the payment. // to make the payment.
FeeLimit: &lnrpc.FeeLimit{ FeeLimit: &lnrpc.FeeLimit{
Limit: &lnrpc.FeeLimit_Percent{ Limit: &lnrpc.FeeLimit_Percent{
Percent: 50, Percent: 50,
@ -480,63 +463,48 @@ func (b *blindedForwardTest) sendBlindedPayment(ctx context.Context,
return cancel return cancel
} }
// interceptFinalHop launches a goroutine to intercept Carol's htlcs and // sendToRoute synchronously lets Alice attempt to send to the given route
// returns a closure that can be used to resolve intercepted htlcs. // using the SendToRouteV2 endpoint and asserts that the payment either
// // succeeds or fails.
//nolint:lll func (b *blindedForwardTest) sendToRoute(route *lnrpc.Route,
func (b *blindedForwardTest) interceptFinalHop() func(routerrpc.ResolveHoldForwardAction) { assertSuccess bool) {
hash := sha256.Sum256(b.preimage[:]) hash := sha256.Sum256(b.preimage[:])
htlcReceived := make(chan *routerrpc.ForwardHtlcInterceptRequest) sendReq := &routerrpc.SendToRouteRequest{
PaymentHash: hash[:],
// Launch a goroutine which will receive from the interceptor and pipe Route: route,
// 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 // Let Alice send to the blinded payment path and assert that it
// succeeds/fails.
htlcAttempt := b.ht.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.ht.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 // drainCarolLiquidity will drain all of the liquidity in Carol's channel in
@ -632,234 +600,55 @@ func setupFourHopNetwork(ht *lntest.HarnessTest,
} }
} }
// createBlindedRoute creates a blinded route to the recipient node provided. // testForwardAndReceiveBlindedRoute tests lnd's ability to create a blinded
// The set of hops is expected to start at the introduction node and end at // payment path, forward payments in a blinded route and receive the payment.
// the recipient. func testForwardAndReceiveBlindedRoute(ht *lntest.HarnessTest) {
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: lnwire.MilliSatoshi(
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.NewNonFinalBlindedRouteData(
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.NewNonFinalBlindedRouteData(
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) ctx, testCase := newBlindedForwardTest(ht)
defer testCase.cleanup() defer testCase.cleanup()
route := testCase.setup(ctx) // Set up the 4 hop network and let Dave create an invoice with a
blindedRoute := testCase.createRouteToBlinded(10_000_000, route) // blinded path that uses Bob as an introduction node.
testCase.setupNetwork(ctx, false)
blindedPaymentPath := testCase.buildBlindedPath()
// Receiving via blinded routes is not yet supported, so Dave won't be // Construct a full route from Alice to Dave using the blinded payment
// able to process the payment. // path.
// route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
// 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. // Let Alice send to the constructed route and assert that the payment
cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute) // succeeds.
defer cancelPmt() testCase.sendToRoute(route, true)
// 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 // testReceiverBlindedError tests handling of errors from the receiving node in
// a payment over: Alice -- Bob -- Carol -- Dave, where Bob is the introduction // a blinded route, testing a payment over: Alice -- Bob -- Carol -- Dave, where
// node. // 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) { func testReceiverBlindedError(ht *lntest.HarnessTest) {
ctx, testCase := newBlindedForwardTest(ht) ctx, testCase := newBlindedForwardTest(ht)
defer testCase.cleanup() defer testCase.cleanup()
route := testCase.setup(ctx)
sendAndResumeBlindedPayment(ctx, ht, testCase, route, true) 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 // testRelayingBlindedError tests handling of errors from relaying nodes in a
@ -869,61 +658,29 @@ func testReceiverBlindedError(ht *lntest.HarnessTest) {
func testRelayingBlindedError(ht *lntest.HarnessTest) { func testRelayingBlindedError(ht *lntest.HarnessTest) {
ctx, testCase := newBlindedForwardTest(ht) ctx, testCase := newBlindedForwardTest(ht)
defer testCase.cleanup() defer testCase.cleanup()
route := testCase.setup(ctx)
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 // Before we send our payment, drain all of Carol's liquidity
// so that she can't forward the payment to Dave. // so that she can't forward the payment to Dave.
testCase.drainCarolLiquidity(false) testCase.drainCarolLiquidity(false)
// Then dispatch the payment through Carol which will fail due to // Subscribe to Carol's HTLC events so that we can observe the payment
// a lack of liquidity. This check only happens _after_ the interceptor // coming in.
// has given the instruction to resume so we can use test carolEvents := testCase.carol.RPC.SubscribeHtlcEvents()
// infrastructure that will go ahead and intercept the payment.
sendAndResumeBlindedPayment(ctx, ht, testCase, route, true)
}
// sendAndResumeBlindedPayment sends a blinded payment through the test // Once subscribed, the first event will be UNKNOWN.
// network provided, intercepting the payment at Carol and allowing it to ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_UNKNOWN)
// 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) // Let Alice send to the constructed route and assert that the payment
// fails.
testCase.sendToRoute(route, false)
// Before we dispatch the payment, spin up a goroutine that will // Make sure that the HTLC did in fact reach Carol and fail there.
// intercept the HTLC on Carol's forward. This allows us to ensure ht.AssertHtlcEvents(
// that the HTLC actually reaches the location we expect it to. carolEvents, 0, 0, 0, 1, routerrpc.HtlcEvent_FORWARD,
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,
) )
} }
@ -934,35 +691,52 @@ func sendAndResumeBlindedPayment(ctx context.Context, ht *lntest.HarnessTest,
func testIntroductionNodeError(ht *lntest.HarnessTest) { func testIntroductionNodeError(ht *lntest.HarnessTest) {
ctx, testCase := newBlindedForwardTest(ht) ctx, testCase := newBlindedForwardTest(ht)
defer testCase.cleanup() defer testCase.cleanup()
route := testCase.setup(ctx) testCase.setupNetwork(ctx, false)
blindedPaymentPath := testCase.buildBlindedPath()
route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath)
// Before we send our payment, drain all of Carol's incoming liquidity // 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 // so that she can't receive the forward from Bob, causing a failure
// at the introduction node. // at the introduction node.
testCase.drainCarolLiquidity(true) testCase.drainCarolLiquidity(true)
// Send the payment, but do not expect it to reach Carol at all. // Subscribe to Bob's HTLC events so that we can observe the payment
sendAndResumeBlindedPayment(ctx, ht, testCase, route, false) // coming in.
bobEvents := ht.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 // testDisableIntroductionNode tests disabling of blinded forwards for the
// introduction node. // introduction node.
func testDisableIntroductionNode(ht *lntest.HarnessTest) { func testDisableIntroductionNode(ht *lntest.HarnessTest) {
// Disable route blinding for Bob, then re-connect to Alice. // 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)
// Now, disable route blinding for Bob, then re-connect to Alice.
ht.RestartNodeWithExtraArgs(ht.Bob, []string{ ht.RestartNodeWithExtraArgs(ht.Bob, []string{
"--protocol.no-route-blinding", "--protocol.no-route-blinding",
}) })
ht.EnsureConnected(ht.Alice, ht.Bob) ht.EnsureConnected(ht.Alice, ht.Bob)
ctx, testCase := newBlindedForwardTest(ht) // Assert that this fails.
defer testCase.cleanup() testCase.sendToRoute(route, false)
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 // testErrorHandlingOnChainFailure tests handling of blinded errors when we're
@ -971,14 +745,17 @@ func testDisableIntroductionNode(ht *lntest.HarnessTest) {
// infrastructure in place already for error testing. // infrastructure in place already for error testing.
func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) { func testErrorHandlingOnChainFailure(ht *lntest.HarnessTest) {
// Setup a test case, note that we don't use its built in clean up // 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 // because we're going to close a channel, so we'll close out the
// rest manually. // rest manually.
ctx, testCase := newBlindedForwardTest(ht) ctx, testCase := newBlindedForwardTest(ht)
// Note that we send a larger amount here do it'll be worthwhile for // Note that we send a larger amount here, so it'll be worthwhile for
// the sweeper to claim. // the sweeper to claim.
route := testCase.setup(ctx) testCase.setupNetwork(ctx, true)
blindedRoute := testCase.createRouteToBlinded(50_000_000, route) blindedPaymentPath := testCase.buildBlindedPath()
blindedRoute := testCase.createRouteToBlinded(
50_000_000, blindedPaymentPath,
)
// Once our interceptor is set up, we can send the blinded payment. // Once our interceptor is set up, we can send the blinded payment.
cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute) cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute)

View File

@ -2218,12 +2218,12 @@ func (h *HarnessTest) AssertFeeReport(hn *node.HarnessNode,
// //
// TODO(yy): needs refactor to reduce its complexity. // TODO(yy): needs refactor to reduce its complexity.
func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient, func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
fwdCount, fwdFailCount, settleCount int, fwdCount, fwdFailCount, settleCount, linkFailCount int,
userType routerrpc.HtlcEvent_EventType) []*routerrpc.HtlcEvent { userType routerrpc.HtlcEvent_EventType) []*routerrpc.HtlcEvent {
var forwards, forwardFails, settles int var forwards, forwardFails, settles, linkFails int
numEvents := fwdCount + fwdFailCount + settleCount numEvents := fwdCount + fwdFailCount + settleCount + linkFailCount
events := make([]*routerrpc.HtlcEvent, 0) events := make([]*routerrpc.HtlcEvent, 0)
// It's either the userType or the unknown type. // It's either the userType or the unknown type.
@ -2256,6 +2256,9 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
settles++ settles++
} }
case *routerrpc.HtlcEvent_LinkFailEvent:
linkFails++
default: default:
require.Fail(h, "assert event fail", require.Fail(h, "assert event fail",
"unexpected event: %T", event.Event) "unexpected event: %T", event.Event)
@ -2266,6 +2269,7 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
require.Equal(h, fwdFailCount, forwardFails, require.Equal(h, fwdFailCount, forwardFails,
"num of forward fails mismatch") "num of forward fails mismatch")
require.Equal(h, settleCount, settles, "num of settles mismatch") require.Equal(h, settleCount, settles, "num of settles mismatch")
require.Equal(h, linkFailCount, linkFails, "num of link fails mismatch")
return events return events
} }