Skip to content

Commit 3bf2f15

Browse files
itest: verify switchrpc server enforces send then track
We prevent the rpc server from allowing onion dispatches for attempt IDs which have already been tracked by rpc clients. This helps protect the client from leaking a duplicate onion attempt. NOTE: This is not the only method for solving this issue! The issue could be addressed via careful client side programming which accounts for the uncertainty and async nature of dispatching onions to a remote process via RPC. This would require some lnd ChannelRouter changes for how we intend to use these RPCs though.
1 parent f69c058 commit 3bf2f15

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,10 @@ var allTestCases = []*lntest.TestCase{
670670
Name: "send onion twice",
671671
TestFunc: testSendOnionTwice,
672672
},
673+
{
674+
Name: "send then track",
675+
TestFunc: testTrackThenSend,
676+
},
673677
{
674678
Name: "track onion",
675679
TestFunc: testTrackOnion,

itest/lnd_sendonion_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,102 @@ func testSendOnionTwice(ht *lntest.HarnessTest) {
273273
require.Equal(ht, resp.ErrorMessage, htlcswitch.ErrDuplicateAdd.Error())
274274
}
275275

276+
func testTrackThenSend(ht *lntest.HarnessTest) {
277+
alice := ht.NewNodeWithCoins("Alice", nil)
278+
bob := ht.NewNodeWithCoins("Bob", nil)
279+
carol := ht.NewNode("Carol", nil)
280+
dave := ht.NewNode("Dave", nil)
281+
282+
ht.EnsureConnected(alice, bob)
283+
ht.EnsureConnected(bob, carol)
284+
ht.EnsureConnected(carol, dave)
285+
286+
const chanAmt = btcutil.Amount(100000)
287+
288+
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
289+
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
290+
291+
chanPointAliceBob := ht.OpenChannel(alice, bob, lntest.OpenChannelParams{Amt: chanAmt})
292+
defer ht.CloseChannel(alice, chanPointAliceBob)
293+
294+
chanPointBobCarol := ht.OpenChannel(bob, carol, lntest.OpenChannelParams{Amt: chanAmt})
295+
defer ht.CloseChannel(bob, chanPointBobCarol)
296+
297+
chanPointCarolDave := ht.OpenChannel(carol, dave, lntest.OpenChannelParams{Amt: chanAmt})
298+
defer ht.CloseChannel(carol, chanPointCarolDave)
299+
300+
ht.AssertChannelInGraph(alice, chanPointBobCarol)
301+
ht.AssertChannelInGraph(alice, chanPointCarolDave)
302+
303+
const paymentAmt = 10000
304+
305+
// Request an invoice from Dave.
306+
_, rHashes, invoices := ht.CreatePayReqs(dave, paymentAmt, 1)
307+
paymentHash := rHashes[0]
308+
309+
// Query for routes to Dave.
310+
routesReq := &lnrpc.QueryRoutesRequest{
311+
PubKey: dave.PubKeyStr,
312+
Amt: paymentAmt,
313+
}
314+
routes := alice.RPC.QueryRoutes(routesReq)
315+
route := routes.Routes[0]
316+
finalHop := route.Hops[len(route.Hops)-1]
317+
finalHop.MppRecord = &lnrpc.MPPRecord{
318+
PaymentAddr: invoices[0].PaymentAddr,
319+
TotalAmtMsat: int64(lnwire.NewMSatFromSatoshis(paymentAmt)),
320+
}
321+
322+
// Build the onion.
323+
onionReq := &switchrpc.BuildOnionRequest{
324+
Route: route,
325+
PaymentHash: paymentHash,
326+
}
327+
onionResp := alice.RPC.BuildOnion(onionReq)
328+
329+
// Simulate a scenario in which the request to track an onion for a
330+
// given attempt ID is processed by the remote server *before* the
331+
// request to send the onion.
332+
333+
// Track the payment first. We expect to receive notice that no HTLC
334+
// by this attempt ID is in-flight or known about.
335+
trackReq := &switchrpc.TrackOnionRequest{
336+
AttemptId: 1,
337+
PaymentHash: paymentHash,
338+
SessionKey: onionResp.SessionKey,
339+
HopPubkeys: onionResp.HopPubkeys,
340+
}
341+
trackResp := alice.RPC.TrackOnion(trackReq)
342+
require.False(ht, trackResp.Success, "expected failure on onion track")
343+
require.Equal(ht, trackResp.ErrorCode,
344+
switchrpc.ErrorCode_ERROR_CODE_PAYMENT_ID_NOT_FOUND,
345+
"unexpected error code")
346+
require.Equal(ht, trackResp.ErrorMessage,
347+
htlcswitch.ErrPaymentIDNotFound.Error())
348+
349+
// Now send the onion for the first time. This should fail as our server
350+
// enforces "send then track" ordering in order to protect rpc client
351+
// from duplicate onion attempts.
352+
//
353+
// NOTE(calvin): Undesired attempt duplication can also be prevented by
354+
// careful programming on the rpc client side, but handling this on the
355+
// rpc server allows us to avoid changing lnd ChannelRouter code.
356+
sendReq := &switchrpc.SendOnionRequest{
357+
FirstHopPubkey: bob.PubKey[:],
358+
Amount: route.TotalAmtMsat,
359+
Timelock: route.TotalTimeLock,
360+
PaymentHash: paymentHash,
361+
OnionBlob: onionResp.OnionBlob,
362+
AttemptId: 1,
363+
}
364+
resp := alice.RPC.SendOnion(sendReq)
365+
require.False(ht, resp.Success, "expected failure on onion send")
366+
require.Equal(ht, resp.ErrorCode,
367+
switchrpc.ErrorCode_ERROR_CODE_DUPLICATE_HTLC,
368+
"unexpected error code")
369+
require.Equal(ht, resp.ErrorMessage, htlcswitch.ErrDuplicateAdd.Error())
370+
}
371+
276372
func testTrackOnion(ht *lntest.HarnessTest) {
277373
// Create a four-node context consisting of Alice, Bob and two new
278374
// nodes: Carol and Dave. This will provide a 4 node, 3 channel topology.

0 commit comments

Comments
 (0)