Skip to content

Commit

Permalink
itest: add track_onion test
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinrzachman authored and starius committed Oct 29, 2024
1 parent 9793cd6 commit 29cef78
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 0 deletions.
4 changes: 4 additions & 0 deletions itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,4 +706,8 @@ var allTestCases = []*lntest.TestCase{
Name: "send onion",
TestFunc: testSendOnion,
},
{
Name: "track onion",
TestFunc: testTrackOnion,
},
}
178 changes: 178 additions & 0 deletions itest/lnd_sendonion_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package itest

import (
"context"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
Expand All @@ -10,6 +15,10 @@ import (
"github.com/stretchr/testify/require"
)

// const (
// defaultTimeout = 30 * time.Second
// )

func testSendOnion(ht *lntest.HarnessTest) {
// Create a four-node context consisting of Alice, Bob and two new
// nodes: Carol and Dave. This provides a 4 node, 3 channel topology.
Expand Down Expand Up @@ -140,4 +149,173 @@ func testSendOnion(ht *lntest.HarnessTest) {

// The invoice should show as settled for Dave.
ht.AssertInvoiceSettled(dave, invoices[0].PaymentAddr)

// TODO(calvin): Other things to check:
// - Error conditions/handling (server handles with decryptor or caller
// handles encrypted error blobs from server)
// - That we successfully convert pubkey --> channel when there are
// multiple channels, some of which can carry the payment and other
// which cannot.
// - Send the same onion again. Send the same onion again but mark it
// with a different attempt ID.
//
// If we send again, our node does forward the onion but the first hop
// considers it a replayed onion.
// 2024-05-01 15:54:18.364 [ERR] HSWC: unable to process onion packet: sphinx packet replay attempted
// 2024-05-01 15:54:18.364 [ERR] HSWC: ChannelLink(a680b373941e2e056e7b98007cc8cee933331e28981474b34d4275bb94cd17fe:0): unable to decode onion hop iterator: InvalidOnionVersion
// 2024-05-01 15:54:18.364 [DBG] PEER: Peer(0352f454dd5e09cd3e979cbace6fc6727cfa9a1eaa878a452ce63b221f51771a74): Sending UpdateFailMalformedHTLC(chan_id=fe17cd94bb75424db3741498281e3333e9cec87c00987b6e052e1e9473b380a6, id=1, fail_code=InvalidOnionVersion) to 0352f454dd5e09cd3e979cbace6fc6727cfa9a1eaa878a452ce63b221f51771a74@127.0.0.1:63567
// If we randomize the payment hash, first hop says bad HMAC.
//
// - Send different onion but with same attempt ID.
}

func testTrackOnion(ht *lntest.HarnessTest) {
// Create a four-node context consisting of Alice, Bob and two new
// nodes: Carol and Dave. This will provide a 4 node, 3 channel topology.
// Alice will make a channel with Bob, and Bob with Carol, and Carol
// with Dave such that we arrive at the network topology:
// Alice -> Bob -> Carol -> Dave
alice, bob := ht.Alice, ht.Bob
carol := ht.NewNode("carol", nil)
dave := ht.NewNode("dave", nil)

// Connect nodes to ensure propagation of channels.
ht.EnsureConnected(alice, bob)
ht.EnsureConnected(bob, carol)
ht.EnsureConnected(carol, dave)

const chanAmt = btcutil.Amount(100000)

// Open a channel with 100k satoshis between Alice and Bob with Alice
// being the sole funder of the channel.
chanPointAlice := ht.OpenChannel(
alice, bob, lntest.OpenChannelParams{Amt: chanAmt},
)
defer ht.CloseChannel(alice, chanPointAlice)

// We'll create Dave and establish a channel between Bob and Carol.
ht.FundCoins(btcutil.SatoshiPerBitcoin, dave)
chanPointBob := ht.OpenChannel(
bob, carol, lntest.OpenChannelParams{Amt: chanAmt},
)
defer ht.CloseChannel(bob, chanPointBob)

// Next, we'll create Carol and establish a channel to from her to Dave.
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
chanPointCarol := ht.OpenChannel(
carol, dave, lntest.OpenChannelParams{Amt: chanAmt},
)
defer ht.CloseChannel(carol, chanPointCarol)

// Make sure Alice knows the channel between Bob and Carol.
ht.AssertTopologyChannelOpen(alice, chanPointBob)
ht.AssertTopologyChannelOpen(alice, chanPointCarol)

const paymentAmt = 10000

// Query for routes to pay from Alice to Dave.
routesReq := &lnrpc.QueryRoutesRequest{
PubKey: dave.PubKeyStr,
Amt: paymentAmt,
}
routes := alice.RPC.QueryRoutes(routesReq)
route := routes.Routes[0]

finalHop := route.Hops[len(route.Hops)-1]
finalHop.MppRecord = &lnrpc.MPPRecord{
PaymentAddr: ht.Random32Bytes(),
TotalAmtMsat: int64(lnwire.NewMSatFromSatoshis(paymentAmt)),
}

// Build the onion to use for our payment.
paymentHash := ht.Random32Bytes()
onionReq := &routerrpc.BuildOnionRequest{
Route: route,
PaymentHash: paymentHash,
}
onionResp := alice.RPC.BuildOnion(onionReq)

// Dispatch a payment via SendOnion.
firstHop := bob.PubKey
sendReq := &routerrpc.SendOnionRequest{
FirstHopPubkey: firstHop[:],
Amount: route.TotalAmtMsat,
Timelock: route.TotalTimeLock,
PaymentHash: paymentHash,
OnionBlob: onionResp.OnionBlob,
AttemptId: 1,
}

resp := alice.RPC.SendOnion(sendReq)
require.True(ht, resp.Success, "expected successful onion send")
require.Empty(ht, resp.ErrorMessage, "unexpected failure to send onion")

serverErrorStr := ""
clientErrorStr := ""

// Track the payment providing all necessary information to delegate
// error decryption to the server.
//
// NOTE(calvin): We expect this to fail as Dave is not expecting payment.
ctxt, _ := context.WithTimeout(context.Background(), defaultTimeout)
trackReq := &routerrpc.TrackOnionRequest{
AttemptId: 1,
PaymentHash: paymentHash,
SessionKey: onionResp.SessionKey,
HopPubkeys: onionResp.HopPubkeys,
}
trackResp, err := alice.RPC.Router.TrackOnion(ctxt, trackReq)
require.Nil(ht, err, "unexpected onion tracking error")
require.NotEmpty(ht, trackResp.ErrorMessage,
"expected onion tracking error")

serverErrorStr = trackResp.ErrorMessage

// Now we'll track the same payment attempt, but we'll specify that
// we want to handle the error decryption ourselves client side.
trackReq = &routerrpc.TrackOnionRequest{
AttemptId: 1,
PaymentHash: paymentHash,
}
trackResp, err = alice.RPC.Router.TrackOnion(ctxt, trackReq)
require.Nil(ht, err, "unexpected onion tracking error")
require.NotNil(ht, trackResp.EncryptedError, "expected encrypted error")

// Decrypt and inspect the error from the TrackOnion RPC response.
sessionKey, _ := btcec.PrivKeyFromBytes(onionResp.SessionKey)
var pubKeys []*btcec.PublicKey
for _, keyBytes := range onionResp.HopPubkeys {
pubKey, err := btcec.ParsePubKey(keyBytes)
if err != nil {
ht.Fatalf("Failed to parse public key: %v", err)
}
pubKeys = append(pubKeys, pubKey)
}

// Construct the circuit to create the error decryptor
circuit := reconstructCircuit(sessionKey, pubKeys)
errorDecryptor := &htlcswitch.SphinxErrorDecrypter{
OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
}

// Simulate an RPC client decrypting the onion error.
encryptedError := lnwire.OpaqueReason(trackResp.EncryptedError)
forwardingError, err := errorDecryptor.DecryptError(encryptedError)
require.Nil(ht, err, "unable to decrypt error")

clientErrorStr = forwardingError.Error()

serverFwdErr, err := htlcswitch.ParseForwardingError(serverErrorStr)
require.Nil(ht, err, "expected to parse forwarding error from server")
require.Equal(ht, serverFwdErr.Error(), clientErrorStr, "expect error "+
"message to match whether handled by client or server")
}

func reconstructCircuit(sessionKey *btcec.PrivateKey,
pubKeys []*btcec.PublicKey) *sphinx.Circuit {

return &sphinx.Circuit{
SessionKey: sessionKey,
PaymentPath: pubKeys,
}
}

0 comments on commit 29cef78

Please sign in to comment.