Skip to content

Commit e1b56dc

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 7ea2f0c commit e1b56dc

File tree

2 files changed

+99
-0
lines changed

2 files changed

+99
-0
lines changed

itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,10 @@ var allTestCases = []*lntest.TestCase{
707707
Name: "send onion twice",
708708
TestFunc: testSendOnionTwice,
709709
},
710+
{
711+
Name: "send then track",
712+
TestFunc: testTrackThenSend,
713+
},
710714
{
711715
Name: "track onion",
712716
TestFunc: testTrackOnion,

itest/lnd_sendonion_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,101 @@ func testSendOnionTwice(ht *lntest.HarnessTest) {
271271
require.Equal(ht, resp.ErrorMessage, htlcswitch.ErrDuplicateAdd.Error())
272272
}
273273

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

0 commit comments

Comments
 (0)