diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go index 556aece84..430b001c6 100644 --- a/itest/lnd_route_blinding_test.go +++ b/itest/lnd_route_blinding_test.go @@ -1,9 +1,11 @@ package itest import ( + "bytes" "context" "crypto/sha256" "encoding/hex" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" @@ -13,6 +15,7 @@ import ( "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" @@ -323,6 +326,10 @@ type blindedForwardTest struct { dave *node.HarnessNode channels []*lnrpc.ChannelPoint + carolInterceptor routerrpc.Router_HtlcInterceptorClient + + preimage [32]byte + // cancel will cancel the test's top level context. cancel func() } @@ -333,16 +340,28 @@ func newBlindedForwardTest(ht *lntest.HarnessTest) (context.Context, ctx, cancel := context.WithCancel(context.Background()) return ctx, &blindedForwardTest{ - ht: ht, - cancel: cancel, + ht: ht, + cancel: cancel, + preimage: [32]byte{1, 2, 3}, } } // setup spins up additional nodes needed for our test and creates a four hop // network for testing blinded forwarding and returns a blinded route from -// Bob -> Carol -> Dave, with Bob acting as the introduction point. -func (b *blindedForwardTest) setup() *routing.BlindedPayment { - b.carol = b.ht.NewNode("Carol", nil) +// Bob -> Carol -> Dave, with Bob acting as the introduction point and an +// interceptor on Carol's node to manage HTLCs (as Dave does not yet support +// receiving). +func (b *blindedForwardTest) setup( + ctx context.Context) *routing.BlindedPayment { + + b.carol = b.ht.NewNode("Carol", []string{ + "requireinterceptor", + }) + + var err error + b.carolInterceptor, err = b.carol.RPC.Router.HtlcInterceptor(ctx) + require.NoError(b.ht, err, "interceptor") + b.dave = b.ht.NewNode("Dave", nil) b.channels = setupFourHopNetwork(b.ht, b.carol, b.dave) @@ -429,6 +448,82 @@ func (b *blindedForwardTest) createRouteToBlinded(paymentAmt int64, return resp.Routes[0] } +// sendBlindedPayment dispatches a payment to the route provided. +func (b *blindedForwardTest) sendBlindedPayment(ctx context.Context, + route *lnrpc.Route) { + + hash := sha256.Sum256(b.preimage[:]) + sendReq := &routerrpc.SendToRouteRequest{ + PaymentHash: hash[:], + Route: route, + } + + // Dispatch in a goroutine because this call is blocking - we assume + // that we'll have assertions that this payment is sent by the caller. + go func() { + b.ht.Alice.RPC.SendToRouteV2(sendReq) + }() +} + +// interceptFinalHop launches a goroutine to intercept Carol's htlcs and +// returns a closure that can be used to resolve intercepted htlcs. +// +//nolint:lll +func (b *blindedForwardTest) interceptFinalHop() func(routerrpc.ResolveHoldForwardAction) { + hash := sha256.Sum256(b.preimage[:]) + htlcReceived := make(chan *routerrpc.ForwardHtlcInterceptRequest) + + // Launch a goroutine which will receive from the interceptor and pipe + // it into our request channel. + go func() { + forward, err := b.carolInterceptor.Recv() + if err != nil { + b.ht.Fatalf("intercept receive failed: %v", err) + } + + if !bytes.Equal(forward.PaymentHash, hash[:]) { + b.ht.Fatalf("unexpected payment hash: %v", hash) + } + + select { + case htlcReceived <- forward: + + case <-time.After(lntest.DefaultTimeout): + b.ht.Fatal("timeout waiting to send intercepted htlc") + } + }() + + // Create a closure that will wait for the intercept request and + // resolve the HTLC with the appropriate action. + resolve := func(action routerrpc.ResolveHoldForwardAction) { + select { + case forward := <-htlcReceived: + resp := &routerrpc.ForwardHtlcInterceptResponse{ + IncomingCircuitKey: forward.IncomingCircuitKey, + } + + switch action { + case routerrpc.ResolveHoldForwardAction_FAIL: + resp.Action = routerrpc.ResolveHoldForwardAction_FAIL + + case routerrpc.ResolveHoldForwardAction_SETTLE: + resp.Action = routerrpc.ResolveHoldForwardAction_SETTLE + resp.Preimage = b.preimage[:] + + case routerrpc.ResolveHoldForwardAction_RESUME: + resp.Action = routerrpc.ResolveHoldForwardAction_RESUME + } + + require.NoError(b.ht, b.carolInterceptor.Send(resp)) + + case <-time.After(lntest.DefaultTimeout): + b.ht.Fatal("timeout waiting for htlc intercept") + } + } + + return resolve +} + // setupFourHopNetwork creates a network with the following topology and // liquidity: // Alice (100k)----- Bob (100k) ----- Carol (100k) ----- Dave @@ -654,9 +749,42 @@ func getForwardingEdge(ht *lntest.HarnessTest, // testForwardBlindedRoute tests lnd's ability to forward payments in a blinded // route. func testForwardBlindedRoute(ht *lntest.HarnessTest) { - _, testCase := newBlindedForwardTest(ht) + ctx, testCase := newBlindedForwardTest(ht) defer testCase.cleanup() - route := testCase.setup() - testCase.createRouteToBlinded(100_000, route) + route := testCase.setup(ctx) + blindedRoute := testCase.createRouteToBlinded(10_000_000, route) + + // Receiving via blinded routes is not yet supported, so Dave won't be + // able to process the payment. + // + // We have an interceptor at our disposal that will catch htlcs as they + // are forwarded (ie, it won't intercept a HTLC that dave is receiving, + // since no forwarding occurs). We initiate this interceptor with + // Carol, so that we can catch it and settle on the outgoing link to + // Dave. Once we hit the outgoing link, we know that we successfully + // parsed the htlc, so this is an acceptable compromise. + // Assert that our interceptor has exited without an error. + resolveHTLC := testCase.interceptFinalHop() + + // Once our interceptor is set up, we can send the blinded payment. + testCase.sendBlindedPayment(ctx, blindedRoute) + + // Wait for the HTLC to be active on Alice's channel. + hash := sha256.Sum256(testCase.preimage[:]) + ht.AssertOutgoingHTLCActive(ht.Alice, testCase.channels[0], hash[:]) + ht.AssertOutgoingHTLCActive(ht.Bob, testCase.channels[1], hash[:]) + + // Intercept and settle the HTLC. + resolveHTLC(routerrpc.ResolveHoldForwardAction_SETTLE) + + // Wait for the HTLC to reflect as settled for Alice. + preimage, err := lntypes.MakePreimage(testCase.preimage[:]) + require.NoError(ht, err) + ht.AssertPaymentStatus(ht.Alice, preimage, lnrpc.Payment_SUCCEEDED) + + // Assert that the HTLC has settled before test cleanup runs so that + // we can cooperatively close all channels. + ht.AssertHLTCNotActive(ht.Bob, testCase.channels[1], hash[:]) + ht.AssertHLTCNotActive(ht.Alice, testCase.channels[0], hash[:]) }