From 34d8fff5f93e388abd2b255158c5c4bf013338f0 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 6 May 2024 16:27:31 +0200 Subject: [PATCH] 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. --- itest/list_on_test.go | 4 +- itest/lnd_multi-hop-payments_test.go | 8 +- itest/lnd_route_blinding_test.go | 575 ++++++++------------------- lntest/harness_assertion.go | 10 +- 4 files changed, 189 insertions(+), 408 deletions(-) diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 3619c2282..606a27794 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -563,8 +563,8 @@ var allTestCases = []*lntest.TestCase{ TestFunc: testQueryBlindedRoutes, }, { - Name: "forward blinded", - TestFunc: testForwardBlindedRoute, + Name: "forward and receive blinded", + TestFunc: testForwardAndReceiveBlindedRoute, }, { Name: "receiver blinded error", diff --git a/itest/lnd_multi-hop-payments_test.go b/itest/lnd_multi-hop-payments_test.go index ee07d6f8c..ba7797d7f 100644 --- a/itest/lnd_multi-hop-payments_test.go +++ b/itest/lnd_multi-hop-payments_test.go @@ -214,24 +214,24 @@ func testMultiHopPayments(ht *lntest.HarnessTest) { // We expect Carol to have successful forwards and settles for // her sends. ht.AssertHtlcEvents( - carolEvents, numPayments, 0, numPayments, + carolEvents, numPayments, 0, numPayments, 0, routerrpc.HtlcEvent_SEND, ) // Dave and Alice should both have forwards and settles for // their role as forwarding nodes. ht.AssertHtlcEvents( - daveEvents, numPayments, 0, numPayments, + daveEvents, numPayments, 0, numPayments, 0, routerrpc.HtlcEvent_FORWARD, ) ht.AssertHtlcEvents( - aliceEvents, numPayments, 0, numPayments, + aliceEvents, numPayments, 0, numPayments, 0, routerrpc.HtlcEvent_FORWARD, ) // Bob should only have settle events for his receives. ht.AssertHtlcEvents( - bobEvents, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE, + bobEvents, 0, 0, numPayments, 0, routerrpc.HtlcEvent_RECEIVE, ) // Finally, close all channels. diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 7cddff70b..f0373a77d 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -1,7 +1,6 @@ package itest import ( - "bytes" "context" "crypto/sha256" "encoding/hex" @@ -10,16 +9,12 @@ import ( "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" ) @@ -273,9 +268,9 @@ func testQueryBlindedRoutes(ht *lntest.HarnessTest) { 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). + // 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) @@ -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 -// 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 { +// 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) { - b.carol = b.ht.NewNode("Carol", []string{ - "--requireinterceptor", "--bitcoin.timelockdelta=18", - }) + carolArgs := []string{"--bitcoin.timelockdelta=18"} + if withInterceptor { + carolArgs = append(carolArgs, "--requireinterceptor") + } + b.carol = b.ht.NewNode("Carol", carolArgs) - var err error - b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(ctx) - require.NoError(b.ht, err, "interceptor") + if withInterceptor { + var err error + 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{ "--bitcoin.timelockdelta=18", + "--invoices.blinding.min-num-real-hops=2", + "--invoices.blinding.num-hops=2", }) 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]) +// 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. + invoice := b.dave.RPC.AddInvoice(&lnrpc.Invoice{ + RPreimage: b.preimage[:], + Memo: "test", + ValueMsat: 10_000_000, + Blind: true, + }) - edges := []*forwardingEdge{ - getForwardingEdge(b.ht, b.ht.Bob, bobChan.ChanId), - getForwardingEdge(b.ht, b.carol, carolChan.ChanId), - } + // 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.ht.Bob.PubKey[:]) - davePk, err := btcec.ParsePubKey(b.dave.PubKey[:]) - require.NoError(b.ht, err, "dave pubkey") - - return b.createBlindedRoute(edges, davePk, 50) + return payReq.BlindedPaths[0] } // cleanup tears down all channels created by the test and cancels the top @@ -399,41 +411,12 @@ func (b *blindedForwardTest) cleanup() { // //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, - ), - } + 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. + // 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, @@ -480,63 +463,48 @@ func (b *blindedForwardTest) sendBlindedPayment(ctx context.Context, 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) { +// 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[:]) - 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") - } + sendReq := &routerrpc.SendToRouteRequest{ + PaymentHash: hash[:], + Route: route, } - 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 @@ -632,234 +600,55 @@ func setupFourHopNetwork(ht *lntest.HarnessTest, } } -// 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: 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) { +// testForwardAndReceiveBlindedRoute tests lnd's ability to create a blinded +// payment path, forward payments in a blinded route and receive the payment. +func testForwardAndReceiveBlindedRoute(ht *lntest.HarnessTest) { ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() - route := testCase.setup(ctx) - blindedRoute := testCase.createRouteToBlinded(10_000_000, route) + // 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) + blindedPaymentPath := testCase.buildBlindedPath() - // 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() + // Construct a full route from Alice to Dave using the blinded payment + // path. + route := testCase.createRouteToBlinded(10_000_000, blindedPaymentPath) - // 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[:]) + // Let Alice send to the constructed route and assert that the payment + // succeeds. + testCase.sendToRoute(route, true) } -// 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. +// 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() - 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 @@ -869,61 +658,29 @@ func testReceiverBlindedError(ht *lntest.HarnessTest) { func testRelayingBlindedError(ht *lntest.HarnessTest) { ctx, testCase := newBlindedForwardTest(ht) 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 // 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) -} + // Subscribe to Carol's HTLC events so that we can observe the payment + // coming in. + carolEvents := testCase.carol.RPC.SubscribeHtlcEvents() -// 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) { + // Once subscribed, the first event will be UNKNOWN. + ht.AssertHtlcEventType(carolEvents, routerrpc.HtlcEvent_UNKNOWN) - 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 - // 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, + // Make sure that the HTLC did in fact reach Carol and fail there. + ht.AssertHtlcEvents( + carolEvents, 0, 0, 0, 1, routerrpc.HtlcEvent_FORWARD, ) } @@ -934,35 +691,52 @@ func sendAndResumeBlindedPayment(ctx context.Context, ht *lntest.HarnessTest, func testIntroductionNodeError(ht *lntest.HarnessTest) { ctx, testCase := newBlindedForwardTest(ht) 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 // 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) + // Subscribe to Bob's HTLC events so that we can observe the payment + // 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 // introduction node. 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{ "--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) + // Assert that this fails. + testCase.sendToRoute(route, false) } // 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. 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 + // 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 + // Note that we send a larger amount here, so it'll be worthwhile for // the sweeper to claim. - route := testCase.setup(ctx) - blindedRoute := testCase.createRouteToBlinded(50_000_000, route) + testCase.setupNetwork(ctx, true) + blindedPaymentPath := testCase.buildBlindedPath() + blindedRoute := testCase.createRouteToBlinded( + 50_000_000, blindedPaymentPath, + ) // Once our interceptor is set up, we can send the blinded payment. cancelPmt := testCase.sendBlindedPayment(ctx, blindedRoute) diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go index 4a5d04274..5ef2ec3df 100644 --- a/lntest/harness_assertion.go +++ b/lntest/harness_assertion.go @@ -2218,12 +2218,12 @@ func (h *HarnessTest) AssertFeeReport(hn *node.HarnessNode, // // TODO(yy): needs refactor to reduce its complexity. func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient, - fwdCount, fwdFailCount, settleCount int, + fwdCount, fwdFailCount, settleCount, linkFailCount int, 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) // It's either the userType or the unknown type. @@ -2256,6 +2256,9 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient, settles++ } + case *routerrpc.HtlcEvent_LinkFailEvent: + linkFails++ + default: require.Fail(h, "assert event fail", "unexpected event: %T", event.Event) @@ -2266,6 +2269,7 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient, require.Equal(h, fwdFailCount, forwardFails, "num of forward fails mismatch") require.Equal(h, settleCount, settles, "num of settles mismatch") + require.Equal(h, linkFailCount, linkFails, "num of link fails mismatch") return events }