commit 8e4f8f54e8877fce725267ff84718053c300f400
parent c8c6467cc48353cf21b4d42434bf129380dc5a88
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:53:20 +0400
merge fix/onion-spec
Fix the spec violations and safety issues that broke BOLT4 interop,
and assert the upstream bolt04 test vectors.
- construct: the filler for each hop now starts at 1300 minus the
previous filler length in that hop's rho stream (it always started
at 1300). Onions for routes of three or more hops were rejected by
every receiver, including this library's own process.
- Failure messages use the spec framing (u16 failure_code || data ||
optional TLV stream) without the extra u16 length; the decoder
splits data by code, ignores trailing bytes and keeps the remainder
of unknown codes as data. Return packets are padded so that
failure_len + pad_len is 256 (it was 220).
- process: payload_length is compared in Word64 and must leave 1300
bytes for the next hop (it could run into the zero-extension
stream, and lengths >= 2^63 wrapped to negative); lengths below two
are rejected.
- TLV decoding: record lengths are compared in Word64 (2^64 - 1
wrapped to -1 and was accepted). Unknown even types now fail in hop
payloads and blinded hop data; hop payload types 16 and 18 are
still accepted, untyped, in hpUnknownTlvs.
- Safety: generateStream returns Maybe instead of an all-zero stream
on failure, encryptHopData returns Maybe instead of calling error,
construct rejects hop payloads under 2 bytes (new InvalidHopPayload
error), and nextEphemeral checks its secret key is 32 bytes before
unsafe_roll32.
API changes: generateStream, encryptHopData, constructError and
wrapError now return Maybe; Construct.Error gains InvalidHopPayload.
Tests (148, up from 105): onion-test.json (construct byte for byte;
process at every hop), onion-error-test.json, the 'Returning Errors'
trace in 04-onion-routing.md (every intermediate packet),
route-blinding-test.json, blinded-payment-onion-test.json (construct
and every forwarded onion), construct-then-process roundtrips for 1
to 20 hops, and negative tests for the decoders and process. Vectors
live in test/Vectors.hs; the test suite now depends on ppad-sha256.
Diffstat:
10 files changed, 2245 insertions(+), 167 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT4/Blinding.hs b/lib/Lightning/Protocol/BOLT4/Blinding.hs
@@ -134,20 +134,24 @@ deriveBlindedNodeId (SharedSecret !ss) !nodePub = do
--
-- @e_{i+1} = SHA256(E_i || ss_i) * e_i@
-- @E_{i+1} = SHA256(E_i || ss_i) * E_i@
+--
+-- Returns 'Nothing' if the secret key is not 32 bytes.
nextEphemeral
:: BS.ByteString -- ^ e_i (32-byte secret key)
-> Secp256k1.Projective -- ^ E_i
-> SharedSecret -- ^ ss_i
-> Maybe (BS.ByteString, Secp256k1.Projective) -- ^ (e_{i+1}, E_{i+1})
-nextEphemeral !secKey !pubKey (SharedSecret !ss) = do
- let !pubBytes = Secp256k1.serialize_point pubKey
- !blindingFactor = SHA256.hash (pubBytes <> ss)
- bfInt <- Secp256k1.roll32 blindingFactor
- -- Compute e_{i+1} = e_i * blindingFactor (mod q)
- let !newSecKey = mulSecKey secKey blindingFactor
- -- Compute E_{i+1} = E_i * blindingFactor
- newPubKey <- Secp256k1.mul pubKey bfInt
- pure (newSecKey, newPubKey)
+nextEphemeral !secKey !pubKey (SharedSecret !ss)
+ | BS.length secKey /= 32 = Nothing
+ | otherwise = do
+ let !pubBytes = Secp256k1.serialize_point pubKey
+ !blindingFactor = SHA256.hash (pubBytes <> ss)
+ bfInt <- Secp256k1.roll32 blindingFactor
+ -- Compute e_{i+1} = e_i * blindingFactor (mod q)
+ let !newSecKey = mulSecKey secKey blindingFactor
+ -- Compute E_{i+1} = E_i * blindingFactor
+ newPubKey <- Secp256k1.mul pubKey bfInt
+ pure (newSecKey, newPubKey)
{-# INLINE nextEphemeral #-}
-- | Compute blinding factor for next path key (public key only).
@@ -167,13 +171,16 @@ nextPathKey !pubKey (SharedSecret !ss) = do
-- | Encrypt hop data with ChaCha20-Poly1305.
--
-- Uses rho key and 12-byte zero nonce, empty AAD.
-encryptHopData :: DerivedKey -> BlindedHopData -> BS.ByteString
+--
+-- Returns 'Nothing' if encryption fails (e.g. the key is not 32
+-- bytes).
+encryptHopData :: DerivedKey -> BlindedHopData -> Maybe BS.ByteString
encryptHopData (DerivedKey !rho) !hopData =
let !plaintext = encodeBlindedHopData hopData
!nonce = BS.replicate 12 0
in case AEAD.encrypt BS.empty rho nonce plaintext of
- Left e -> error $ "encryptHopData: unexpected AEAD error: " ++ show e
- Right (!ciphertext, !mac) -> ciphertext <> mac
+ Left _ -> Nothing
+ Right (!ciphertext, !mac) -> Just (ciphertext <> mac)
{-# INLINE encryptHopData #-}
-- | Decrypt hop data with ChaCha20-Poly1305.
@@ -209,6 +216,8 @@ encodeBlindedHopData !bhd = encodeTlvStream (buildTlvs bhd)
{-# INLINE encodeBlindedHopData #-}
-- | Decode TLV stream to BlindedHopData.
+--
+-- Fails on unknown even TLV types; unknown odd types are skipped.
decodeBlindedHopData :: BS.ByteString -> Maybe BlindedHopData
decodeBlindedHopData !bs = do
tlvs <- decodeTlvStream bs
@@ -238,7 +247,8 @@ parseBlindedHopData = go emptyHopData
pc <- decodePaymentConstraints val
go bhd { bhdPaymentConstraints = Just pc } rest
14 -> go bhd { bhdAllowedFeatures = Just val } rest
- _ -> go bhd rest -- Skip unknown TLVs
+ _ | even typ -> Nothing
+ | otherwise -> go bhd rest
-- PaymentRelay encoding/decoding --------------------------------------------
@@ -338,8 +348,9 @@ processHops !eKey !ePub ((nodePub, hopData) : rest) !idx = do
blindedId <- maybe (Left (InvalidNodeKey idx)) Right
(deriveBlindedNodeId ss nodePub)
-- encrypted_i = encryptHopData(rho_i, data_i)
- let !encData = encryptHopData rho hopData
- !hop = BlindedHop blindedId encData
+ encData <- maybe (Left (InvalidNodeKey idx)) Right
+ (encryptHopData rho hopData)
+ let !hop = BlindedHop blindedId encData
-- (e_{i+1}, E_{i+1}) = nextEphemeral(e_i, E_i, ss_i)
(nextE, nextEPub) <- maybe (Left (InvalidNodeKey idx)) Right
(nextEphemeral eKey ePub ss)
diff --git a/lib/Lightning/Protocol/BOLT4/Codec.hs b/lib/Lightning/Protocol/BOLT4/Codec.hs
@@ -50,6 +50,7 @@ import Data.Bits (shiftL)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as B
import qualified Data.ByteString.Lazy as BL
+import Data.Maybe (fromMaybe)
import Data.Word (Word16, Word32, Word64)
import Lightning.Protocol.BOLT4.Types
@@ -124,11 +125,12 @@ decodeTlv :: BS.ByteString -> Maybe (TlvRecord, BS.ByteString)
decodeTlv !bs = do
(typ, r1) <- decodeBigSize bs
(len, r2) <- decodeBigSize r1
- let !len' = fromIntegral len
- if BS.length r2 < len'
+ -- compare in Word64 so that large lengths cannot wrap
+ if len > fromIntegral (BS.length r2)
then Nothing
else do
- let !val = BS.take len' r2
+ let !len' = fromIntegral len
+ !val = BS.take len' r2
!rest = BS.drop len' r2
Just (TlvRecord typ val, rest)
{-# INLINE decodeTlv #-}
@@ -198,6 +200,10 @@ encodeHopPayload !hp = encodeTlvStream (buildTlvs hp)
in amt' ++ cltv' ++ sci' ++ pd' ++ ed' ++ cpk' ++ unk
-- | Decode HopPayload from bytes.
+--
+-- Fails on unknown even TLV types. Types 16 (payment_metadata) and 18
+-- (total_amount_msat) are accepted and kept, with unknown odd types,
+-- in 'hpUnknownTlvs'.
decodeHopPayload :: BS.ByteString -> Maybe HopPayload
decodeHopPayload !bs = do
tlvs <- decodeTlvStream bs
@@ -226,7 +232,10 @@ parseHopPayload = go emptyHop
go hp { hpPaymentData = Just pd } rest
10 -> go hp { hpEncryptedData = Just val } rest
12 -> go hp { hpCurrentPathKey = Just val } rest
- _ -> go hp { hpUnknownTlvs = TlvRecord typ val : hpUnknownTlvs hp } rest
+ _ | even typ && typ /= 16 && typ /= 18 -> Nothing
+ | otherwise ->
+ go hp { hpUnknownTlvs = TlvRecord typ val : hpUnknownTlvs hp }
+ rest
-- ShortChannelId -----------------------------------------------------------
@@ -254,28 +263,81 @@ decodeShortChannelId !bs
-- Failure messages ---------------------------------------------------------
-- | Encode failure message.
+--
+-- Format: u16 failure_code || data || optional TLV stream. The data
+-- must already be laid out as the failure code requires.
encodeFailureMessage :: FailureMessage -> BS.ByteString
encodeFailureMessage (FailureMessage (FailureCode !code) !dat !tlvs) =
toStrict $
B.word16BE code <>
- B.word16BE (fromIntegral (BS.length dat)) <>
B.byteString dat <>
B.byteString (encodeTlvStream tlvs)
{-# INLINE encodeFailureMessage #-}
-- | Decode failure message.
+--
+-- For known failure codes the data is split off according to the
+-- code's layout, and any remaining bytes are parsed as a TLV stream.
+-- Remaining bytes that are not a valid TLV stream are ignored. For
+-- unknown failure codes all bytes after the code are returned as data.
+--
+-- Returns 'Nothing' if the input is shorter than the failure code or
+-- than the data its code requires.
decodeFailureMessage :: BS.ByteString -> Maybe FailureMessage
decodeFailureMessage !bs = do
- if BS.length bs < 4 then Nothing else do
- let !code = word16BE (BS.take 2 bs)
- !dlen = fromIntegral (word16BE (BS.take 2 (BS.drop 2 bs)))
- if BS.length bs < 4 + dlen then Nothing else do
- let !dat = BS.take dlen (BS.drop 4 bs)
- !tlvBytes = BS.drop (4 + dlen) bs
- tlvs <- if BS.null tlvBytes
- then Just []
- else decodeTlvStream tlvBytes
- Just (FailureMessage (FailureCode code) dat tlvs)
+ (hi, r0) <- BS.uncons bs
+ (lo, rest) <- BS.uncons r0
+ let !code = FailureCode (fromIntegral hi `shiftL` 8 + fromIntegral lo)
+ dlen <- failureDataLen code rest
+ let (!dat, !extra) = BS.splitAt dlen rest
+ !tlvs = if BS.null extra
+ then []
+ else fromMaybe [] (decodeTlvStream extra)
+ Just (FailureMessage code dat tlvs)
+
+-- | Length of the data that a failure code requires, given the bytes
+-- following the code.
+failureDataLen :: FailureCode -> BS.ByteString -> Maybe Int
+failureDataLen !code !bs = case code of
+ InvalidRealm -> fixed 0
+ TemporaryNodeFailure -> fixed 0
+ PermanentNodeFailure -> fixed 0
+ RequiredNodeFeatureMissing -> fixed 0
+ InvalidOnionVersion -> fixed 32
+ InvalidOnionHmac -> fixed 32
+ InvalidOnionKey -> fixed 32
+ TemporaryChannelFailure -> update 0
+ PermanentChannelFailure -> fixed 0
+ FailureCode 0x4009 -> fixed 0 -- req. channel feature
+ FailureCode 0x400A -> fixed 0 -- unknown_next_peer
+ AmountBelowMinimum -> update 8
+ FeeInsufficient -> update 8
+ IncorrectCltvExpiry -> update 4
+ ExpiryTooSoon -> update 0
+ IncorrectOrUnknownPaymentDetails -> fixed 12
+ FinalIncorrectCltvExpiry -> fixed 4
+ FinalIncorrectHtlcAmount -> fixed 8
+ ChannelDisabled -> update 2
+ ExpiryTooFar -> fixed 0
+ InvalidOnionPayload -> do
+ (_, r) <- decodeBigSize bs
+ fixed (BS.length bs - BS.length r + 2)
+ MppTimeout -> fixed 0
+ FailureCode 0xC018 -> fixed 32 -- invalid_onion_blinding
+ _ -> Just (BS.length bs)
+ where
+ fixed :: Int -> Maybe Int
+ fixed !n
+ | BS.length bs >= n = Just n
+ | otherwise = Nothing
+
+ -- n bytes of fields, then u16 len and a len-byte channel_update
+ update :: Int -> Maybe Int
+ update !n = do
+ (hi, r) <- BS.uncons (BS.drop n bs)
+ (lo, _) <- BS.uncons r
+ let !len = fromIntegral hi * 256 + fromIntegral lo :: Int
+ fixed (n + 2 + len)
-- Helper functions ---------------------------------------------------------
diff --git a/lib/Lightning/Protocol/BOLT4/Construct.hs b/lib/Lightning/Protocol/BOLT4/Construct.hs
@@ -40,6 +40,7 @@ data Error
| TooManyHops
| PayloadTooLarge !Int
| InvalidHopPubKey !Int
+ | InvalidHopPayload !Int -- ^ hop index of a payload under 2 bytes
deriving (Eq, Show)
-- | Maximum number of hops in a route.
@@ -74,27 +75,33 @@ construct !sessionKey !hops !assocData
let hopPubKeys = map hopPubKey hops
(secrets, _) <- computeAllSecrets sessionKey ephPub hopPubKeys
- -- Validate payload sizes
+ -- Validate payload sizes (receivers reject lengths 0 and 1)
let payloadBytes = map (encodeHopPayload . hopPayload) hops
payloadSizes = map payloadShiftSize payloadBytes
totalSize = sum payloadSizes
+ case [ i | (i, p) <- zip [0 ..] payloadBytes, BS.length p < 2 ] of
+ i : _ -> Left (InvalidHopPayload i)
+ [] -> pure ()
if totalSize > hopPayloadsSize
then Left (PayloadTooLarge totalSize)
else do
- -- Generate filler using secrets for all but final hop
+ -- Generate each hop's rho stream (2 * 1300 bytes)
+ rhoStreams <- traverse rhoStream (zip [0 ..] secrets)
+
+ -- Generate filler using streams for all but final hop
let numHops = length hops
- secretsExceptFinal = take (numHops - 1) secrets
+ streamsExceptFinal = take (numHops - 1) rhoStreams
sizesExceptFinal = take (numHops - 1) payloadSizes
- filler = generateFiller secretsExceptFinal sizesExceptFinal
+ filler = generateFiller streamsExceptFinal sizesExceptFinal
-- Initialize hop_payloads with deterministic padding
- let DerivedKey padKey = derivePad (SharedSecret sessionKey)
- initialPayloads = generateStream (DerivedKey padKey)
- hopPayloadsSize
+ initialPayloads <- maybe (Left InvalidSessionKey) Right
+ (generateStream (derivePad (SharedSecret sessionKey))
+ hopPayloadsSize)
-- Wrap payloads in reverse order (final hop first)
- let (finalPayloads, finalHmac) = wrapAllHops
- secrets payloadBytes filler assocData initialPayloads
+ let (finalPayloads, finalHmac) = wrapAllHops secrets
+ rhoStreams payloadBytes filler assocData initialPayloads
-- Build the final packet
let ephPubBytes = Secp256k1.serialize_point ephPub
@@ -106,6 +113,9 @@ construct !sessionKey !hops !assocData
}
Right (packet, secrets)
+ where
+ rhoStream (i, ss) = maybe (Left (InvalidHopPubKey i)) Right
+ (generateStream (deriveRho ss) (2 * hopPayloadsSize))
-- | Compute the total shift size for a payload.
payloadShiftSize :: BS.ByteString -> Int
@@ -134,20 +144,22 @@ computeAllSecrets !initSec !initPub = go initSec initPub 0 []
(blindPubKey ephPub bf)
go newEphSec newEphPub (idx + 1) (ss : acc) rest
--- | Generate filler bytes.
-generateFiller :: [SharedSecret] -> [Int] -> BS.ByteString
-generateFiller !secrets !sizes = go BS.empty secrets sizes
+-- | Generate filler bytes from the hops' 2600-byte rho streams.
+--
+-- At each hop the filler, extended by that hop's shift size, is XORed
+-- with the hop's rho stream starting at 1300 minus the length of the
+-- filler before extension.
+generateFiller :: [BS.ByteString] -> [Int] -> BS.ByteString
+generateFiller !streams !sizes = go BS.empty streams sizes
where
go !filler [] [] = filler
- go !filler (ss:sss) (sz:szs) =
+ go !filler (stream:sts) (sz:szs) =
let !extended = filler <> BS.replicate sz 0
- !rhoKey = deriveRho ss
- !stream = generateStream rhoKey (2 * hopPayloadsSize)
- !streamOffset = hopPayloadsSize
+ !streamOffset = hopPayloadsSize - BS.length filler
!streamPart = BS.take (BS.length extended)
(BS.drop streamOffset stream)
!newFiller = xorBytes extended streamPart
- in go newFiller sss szs
+ in go newFiller sts szs
go !filler _ _ = filler
{-# INLINE generateFiller #-}
@@ -155,26 +167,28 @@ generateFiller !secrets !sizes = go BS.empty secrets sizes
wrapAllHops
:: [SharedSecret]
-> [BS.ByteString]
+ -> [BS.ByteString]
-> BS.ByteString
-> BS.ByteString
-> BS.ByteString
-> (BS.ByteString, BS.ByteString)
-wrapAllHops !secrets !payloads !filler !assocData !initPayloads =
- let !paired = reverse (zip secrets payloads)
+wrapAllHops !secrets !streams !payloads !filler !assocData !initPayloads =
+ let !paired = reverse (zip3 secrets streams payloads)
!numHops = length paired
!initHmac = BS.replicate hmacSize 0
in go numHops initPayloads initHmac paired
where
go !_ !hpBuf !hmac [] = (hpBuf, hmac)
- go !remaining !hpBuf !hmac ((ss, payload):rest) =
+ go !remaining !hpBuf !hmac ((ss, stream, payload):rest) =
let !isLastHop = remaining ==
length (reverse (zip secrets payloads))
(!newPayloads, !newHmac) =
- wrapHop ss payload hmac hpBuf
+ wrapHop ss stream payload hmac hpBuf
assocData filler isLastHop
in go (remaining - 1) newPayloads newHmac rest
--- | Wrap a single hop's payload.
+-- | Wrap a single hop's payload, given its shared secret and rho
+-- stream.
wrapHop
:: SharedSecret
-> BS.ByteString
@@ -182,17 +196,16 @@ wrapHop
-> BS.ByteString
-> BS.ByteString
-> BS.ByteString
+ -> BS.ByteString
-> Bool
-> (BS.ByteString, BS.ByteString)
-wrapHop !ss !payload !hmac !hpBuf !assocData !filler !isFinalHop =
+wrapHop !ss !stream !payload !hmac !hpBuf !assocData !filler !isFinalHop =
let !payloadLen = BS.length payload
!lenBytes = encodeBigSize (fromIntegral payloadLen)
!shiftSize = BS.length lenBytes + payloadLen + hmacSize
!shifted = BS.take (hopPayloadsSize - shiftSize) hpBuf
!prepended = lenBytes <> payload <> hmac <> shifted
- !rhoKey = deriveRho ss
- !stream = generateStream rhoKey hopPayloadsSize
- !obfuscated = xorBytes prepended stream
+ !obfuscated = xorBytes prepended (BS.take hopPayloadsSize stream)
!withFiller = if isFinalHop && not (BS.null filler)
then applyFiller obfuscated filler
else obfuscated
diff --git a/lib/Lightning/Protocol/BOLT4/Error.hs b/lib/Lightning/Protocol/BOLT4/Error.hs
@@ -52,7 +52,9 @@ data AttributionResult
-- ^ Could not attribute to any hop
deriving (Eq, Show)
--- | Minimum error packet size (256 bytes per spec).
+-- | Minimum of @failure_len + pad_len@ in a return packet (256 bytes
+-- per spec). A return packet is therefore at least 32 + 2 + 256 + 2
+-- = 292 bytes.
minErrorPacketSize :: Int
minErrorPacketSize = 256
{-# INLINE minErrorPacketSize #-}
@@ -63,29 +65,31 @@ minErrorPacketSize = 256
--
-- Takes the shared secret (from processing) and failure message,
-- and wraps it for return to origin.
+--
+-- Returns 'Nothing' if the obfuscation stream cannot be generated.
constructError
:: SharedSecret -- ^ from packet processing
-> FailureMessage -- ^ failure details
- -> ErrorPacket
-constructError !ss !failure =
+ -> Maybe ErrorPacket
+constructError !ss !failure = do
let !um = deriveUm ss
!ammag = deriveAmmag ss
!inner = buildErrorMessage um failure
- !obfuscated = obfuscateError ammag inner
- in ErrorPacket obfuscated
+ ErrorPacket <$> obfuscateError ammag inner
{-# INLINE constructError #-}
-- | Wrap an existing error packet for forwarding back.
--
-- Each intermediate node wraps the error with its own layer.
+--
+-- Returns 'Nothing' if the obfuscation stream cannot be generated.
wrapError
:: SharedSecret -- ^ this node's shared secret
-> ErrorPacket -- ^ error from downstream
- -> ErrorPacket
+ -> Maybe ErrorPacket
wrapError !ss (ErrorPacket !packet) =
let !ammag = deriveAmmag ss
- !wrapped = obfuscateError ammag packet
- in ErrorPacket wrapped
+ in ErrorPacket <$> obfuscateError ammag packet
{-# INLINE wrapError #-}
-- Error unwrapping -----------------------------------------------------------
@@ -107,19 +111,23 @@ unwrapError secrets (ErrorPacket !initialPacket) = go 0 initialPacket secrets
go !idx !packet (ss:rest) =
let !ammag = deriveAmmag ss
!um = deriveUm ss
- !deobfuscated = deobfuscateError ammag packet
- in if verifyErrorHmac um deobfuscated
- then case parseErrorMessage (BS.drop 32 deobfuscated) of
- Just msg -> Attributed idx msg
- Nothing -> UnknownOrigin deobfuscated
- else go (idx + 1) deobfuscated rest
+ in case deobfuscateError ammag packet of
+ Nothing -> UnknownOrigin packet
+ Just deobfuscated
+ | verifyErrorHmac um deobfuscated ->
+ case parseErrorMessage (BS.drop 32 deobfuscated) of
+ Just msg -> Attributed idx msg
+ Nothing -> UnknownOrigin deobfuscated
+ | otherwise -> go (idx + 1) deobfuscated rest
-- Internal functions ---------------------------------------------------------
-- | Build the inner error message structure.
--
-- Format: HMAC (32) || len (2) || message || pad_len (2) || padding
--- Total must be >= 256 bytes.
+--
+-- The padding brings len + pad_len up to 256, as the spec recommends;
+-- longer messages are not padded.
buildErrorMessage
:: DerivedKey -- ^ um key
-> FailureMessage -- ^ failure to encode
@@ -127,9 +135,7 @@ buildErrorMessage
buildErrorMessage (DerivedKey !umKey) !failure =
let !encoded = encodeFailureMessage failure
!msgLen = BS.length encoded
- -- Total payload: len(2) + msg + pad_len(2) + padding = 256 - 32 = 224
- -- padding = 224 - 2 - msgLen - 2 = 220 - msgLen
- !padLen = max 0 (minErrorPacketSize - 32 - 2 - msgLen - 2)
+ !padLen = max 0 (minErrorPacketSize - msgLen)
!padding = BS.replicate padLen 0
-- Build: len || message || pad_len || padding
!payload = toStrict $
@@ -146,21 +152,21 @@ buildErrorMessage (DerivedKey !umKey) !failure =
--
-- XORs the entire packet with pseudo-random stream.
obfuscateError
- :: DerivedKey -- ^ ammag key
- -> BS.ByteString -- ^ error packet
- -> BS.ByteString -- ^ obfuscated packet
-obfuscateError !ammag !packet =
- let !stream = generateStream ammag (BS.length packet)
- in xorBytes packet stream
+ :: DerivedKey -- ^ ammag key
+ -> BS.ByteString -- ^ error packet
+ -> Maybe BS.ByteString -- ^ obfuscated packet
+obfuscateError !ammag !packet = do
+ stream <- generateStream ammag (BS.length packet)
+ pure $! xorBytes packet stream
{-# INLINE obfuscateError #-}
-- | Remove one layer of obfuscation from error packet.
--
-- XOR is its own inverse, so same as obfuscation.
deobfuscateError
- :: DerivedKey -- ^ ammag key
- -> BS.ByteString -- ^ obfuscated packet
- -> BS.ByteString -- ^ deobfuscated packet
+ :: DerivedKey -- ^ ammag key
+ -> BS.ByteString -- ^ obfuscated packet
+ -> Maybe BS.ByteString -- ^ deobfuscated packet
deobfuscateError = obfuscateError
{-# INLINE deobfuscateError #-}
diff --git a/lib/Lightning/Protocol/BOLT4/Prim.hs b/lib/Lightning/Protocol/BOLT4/Prim.hs
@@ -183,14 +183,16 @@ blindSecKey !secBs (BlindingFactor !bf)
--
-- Uses derived key as ChaCha20 key, 96-bit zero nonce, counter=0.
-- Encrypts zeros to produce keystream.
+--
+-- Returns 'Nothing' if ChaCha20 fails (e.g. the key is not 32 bytes).
generateStream
:: DerivedKey -- ^ rho or ammag key
-> Int -- ^ desired length
- -> BS.ByteString
+ -> Maybe BS.ByteString
generateStream (DerivedKey !key) !len =
let !nonce = BS.replicate 12 0
!zeros = BS.replicate len 0
- in either (const (BS.replicate len 0)) id
+ in either (const Nothing) Just
(ChaCha.cipher key (0 :: Word32) nonce zeros)
{-# INLINE generateStream #-}
diff --git a/lib/Lightning/Protocol/BOLT4/Process.hs b/lib/Lightning/Protocol/BOLT4/Process.hs
@@ -71,8 +71,10 @@ process !secKey !packet !assocData = do
else pure ()
-- Step 6: Decrypt hop payloads
- let !decrypted = decryptPayloads rhoKey
- (unHopPayloads (opHopPayloads packet))
+ decrypted <- case decryptPayloads rhoKey
+ (unHopPayloads (opHopPayloads packet)) of
+ Nothing -> Left InvalidEphemeralKey
+ Just d -> Right d
-- Step 7: Extract payload
(payloadBytes, nextHmac, remaining) <-
@@ -126,13 +128,14 @@ parseEphemeralKey !packet =
decryptPayloads
:: DerivedKey -- ^ rho key
-> BS.ByteString -- ^ hop_payloads (1300 bytes)
- -> BS.ByteString -- ^ decrypted (2600 bytes, first 1300 useful)
-decryptPayloads !rhoKey !payloads =
+ -> Maybe BS.ByteString
+ -- ^ decrypted (2600 bytes, first 1300 useful)
+decryptPayloads !rhoKey !payloads = do
let !streamLen = 2 * hopPayloadsSize -- 2600 bytes
- !stream = generateStream rhoKey streamLen
- -- Extend payloads with zeros for the shift operation
- !extended = payloads <> BS.replicate hopPayloadsSize 0
- in xorBytes stream extended
+ stream <- generateStream rhoKey streamLen
+ -- Extend payloads with zeros for the shift operation
+ let !extended = payloads <> BS.replicate hopPayloadsSize 0
+ pure $! xorBytes stream extended
{-# INLINE decryptPayloads #-}
-- | XOR two bytestrings of equal length.
@@ -144,6 +147,10 @@ xorBytes !a !b = BS.pack (BS.zipWith xor a b)
--
-- Parses BigSize length prefix, extracts payload bytes and
-- next HMAC.
+--
+-- The length prefix, payload and next HMAC must fit in the 1300-byte
+-- hop_payloads region, so that at least 1300 bytes remain for the
+-- next hop.
extractPayload
:: BS.ByteString
-> Either RejectReason
@@ -151,19 +158,22 @@ extractPayload
-- ^ (payload_bytes, next_hmac, remaining_hop_payloads)
extractPayload !decrypted = do
-- Parse length prefix
- (len, afterLen) <- case decodeBigSize decrypted of
+ (len64, afterLen) <- case decodeBigSize decrypted of
Nothing -> Left (InvalidPayload "invalid length prefix")
- Just (l, r) -> Right (fromIntegral l :: Int, r)
-
- -- Validate length
- if len > BS.length afterLen
- then Left (InvalidPayload "payload length exceeds buffer")
- else if len == 0
- then Left (InvalidPayload "zero-length payload")
+ Just r -> Right r
+
+ -- Validate length (in Word64, before converting to Int)
+ let !prefixLen = BS.length decrypted - BS.length afterLen
+ !maxLen = hopPayloadsSize - prefixLen - hmacSize
+ if len64 > fromIntegral maxLen
+ then Left (InvalidPayload "payload length exceeds hop_payloads")
+ else if len64 < 2
+ then Left (InvalidPayload "payload length below 2")
else pure ()
-- Extract payload bytes
- let !payloadBytes = BS.take len afterLen
+ let !len = fromIntegral len64 :: Int
+ !payloadBytes = BS.take len afterLen
!afterPayload = BS.drop len afterLen
-- Extract next HMAC (32 bytes)
diff --git a/lib/Lightning/Protocol/BOLT4/Types.hs b/lib/Lightning/Protocol/BOLT4/Types.hs
@@ -163,7 +163,7 @@ data HopPayload = HopPayload
, hpPaymentData :: !(Maybe PaymentData) -- ^ TLV type 8
, hpEncryptedData :: !(Maybe BS.ByteString) -- ^ TLV type 10
, hpCurrentPathKey :: !(Maybe BS.ByteString) -- ^ TLV type 12
- , hpUnknownTlvs :: ![TlvRecord] -- ^ Unknown types
+ , hpUnknownTlvs :: ![TlvRecord] -- ^ 16, 18 and unknown odd
} deriving (Eq, Show, Generic)
-- | Payment data for final hop (TLV type 8).
diff --git a/ppad-bolt4.cabal b/ppad-bolt4.cabal
@@ -48,6 +48,7 @@ test-suite bolt4-tests
default-language: Haskell2010
hs-source-dirs: test
main-is: Main.hs
+ other-modules: Vectors
ghc-options:
-rtsopts -Wall -O2
build-depends:
@@ -56,6 +57,7 @@ test-suite bolt4-tests
, ppad-base16
, ppad-bolt4
, ppad-secp256k1
+ , ppad-sha256
, tasty
, tasty-hunit
, tasty-quickcheck
diff --git a/test/Main.hs b/test/Main.hs
@@ -2,10 +2,14 @@
module Main where
+import Control.Monad (foldM)
import Data.Bits (xor)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as B16
import qualified Crypto.Curve.Secp256k1 as Secp256k1
+import qualified Crypto.Hash.SHA256 as SHA256
+import Data.List (nub, sort)
+import Data.Maybe (isJust)
import Data.Word (Word8, Word16, Word32)
import Lightning.Protocol.BOLT4.Blinding
import Lightning.Protocol.BOLT4.Codec
@@ -18,12 +22,28 @@ import Lightning.Protocol.BOLT4.Types
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
+import qualified Vectors as V
-- | Demand a Just value in IO, failing the test on Nothing.
demand :: String -> Maybe a -> IO a
demand _ (Just a) = pure a
demand msg Nothing = assertFailure msg
+-- | Decode hex, failing the test on invalid input.
+unhex :: BS.ByteString -> IO BS.ByteString
+unhex = demand "invalid hex" . B16.decode
+
+-- | Decode a hex-encoded public key, failing the test if invalid.
+unhexPoint :: BS.ByteString -> IO Secp256k1.Projective
+unhexPoint h = unhex h >>= demand "invalid point" . Secp256k1.parse_point
+
+-- | Strip a BigSize length prefix, checking that it is exact.
+unprefix :: BS.ByteString -> IO BS.ByteString
+unprefix bs = do
+ (len, body) <- demand "invalid length prefix" (decodeBigSize bs)
+ assertEqual "length prefix" (fromIntegral len) (BS.length body)
+ pure body
+
-- | Construct a ShortChannelId, failing if invalid.
assertScid :: Word32 -> Word32 -> Word16
-> IO ShortChannelId
@@ -46,6 +66,7 @@ main = defaultMain $ testGroup "ppad-bolt4" [
]
, testGroup "TLV" [
tlvTests
+ , tlvRejectionTests
]
, testGroup "ShortChannelId" [
sciTests
@@ -73,6 +94,16 @@ main = defaultMain $ testGroup "ppad-bolt4" [
, blindingCreatePathTests
, blindingProcessHopTests
]
+ , testGroup "spec vectors" [
+ onionVectorTests
+ , errorVectorTests
+ , traceVectorTests
+ , routeBlindingTests
+ , blindedPaymentTests
+ ]
+ , testGroup "multi-hop" [
+ multiHopTests
+ ]
]
-- BigSize tests ------------------------------------------------------------
@@ -179,6 +210,37 @@ tlvTests = testGroup "encoding/decoding" [
decoded @?= Just []
]
+tlvRejectionTests :: TestTree
+tlvRejectionTests = testGroup "rejection" [
+ testCase "length of 2^64 - 1" $ do
+ let huge = BS.pack (0xff : replicate 8 0xff)
+ decodeTlv (BS.singleton 0x01 <> huge <> BS.pack [0xaa, 0xbb])
+ @?= Nothing
+ decodeTlvStream (BS.singleton 0x01 <> huge <> BS.pack [0x03, 0x00])
+ @?= Nothing
+ decodeHopPayload
+ (BS.singleton 0x02 <> huge <> BS.pack [0x04, 0x01, 0x07])
+ @?= Nothing
+ , testCase "hop payload: unknown even type" $
+ decodeHopPayload (BS.pack [0x02, 0x01, 0x05, 0x0e, 0x01, 0x07])
+ @?= Nothing
+ , testCase "hop payload: unknown odd type is kept" $
+ fmap hpUnknownTlvs
+ (decodeHopPayload (BS.pack [0x02, 0x01, 0x05, 0x0f, 0x01, 0x07]))
+ @?= Just [TlvRecord 15 (BS.singleton 0x07)]
+ , testCase "hop payload: types 16 and 18 are accepted" $
+ fmap hpUnknownTlvs
+ (decodeHopPayload (BS.pack [0x10, 0x01, 0x01, 0x12, 0x01, 0x02]))
+ @?= Just [ TlvRecord 16 (BS.singleton 0x01)
+ , TlvRecord 18 (BS.singleton 0x02) ]
+ , testCase "blinded hop data: unknown even type" $
+ decodeBlindedHopData (BS.pack [0x10, 0x01, 0x00]) @?= Nothing
+ , testCase "blinded hop data: unknown odd type is skipped" $
+ decodeBlindedHopData (BS.pack [0x06, 0x01, 0x2a, 0xfd, 0x02, 0x31,
+ 0x01, 0x00])
+ @?= Just emptyHopData { bhdPathId = Just (BS.singleton 0x2a) }
+ ]
+
-- ShortChannelId tests -----------------------------------------------------
sciTests :: TestTree
@@ -345,17 +407,17 @@ testGenerateStream :: TestTree
testGenerateStream = testGroup "generateStream" [
testCase "produces correct length" $ do
let dk = DerivedKey (BS.replicate 32 0)
- stream = generateStream dk 100
- BS.length stream @?= 100
+ fmap BS.length (generateStream dk 100) @?= Just 100
, testCase "1300-byte stream for hop_payloads" $ do
let dk = DerivedKey (BS.replicate 32 0x42)
- stream = generateStream dk 1300
- BS.length stream @?= 1300
+ fmap BS.length (generateStream dk 1300) @?= Just 1300
, testCase "deterministic output" $ do
let dk = DerivedKey (BS.replicate 32 0x55)
stream1 = generateStream dk 64
stream2 = generateStream dk 64
stream1 @?= stream2
+ , testCase "fails on a key that is not 32 bytes" $
+ generateStream (DerivedKey "short") 16 @?= Nothing
]
testHmacOperations :: TestTree
@@ -452,8 +514,20 @@ testConstructErrorCases = testGroup "error cases" [
case result of
Left TooManyHops -> return ()
_ -> assertFailure "Expected TooManyHops"
+ , testCase "rejects an empty hop payload" $ do
+ pub <- demand "parse_point" $
+ Secp256k1.parse_point (fromHex hop0PubKeyHex)
+ let emptyPayload = HopPayload Nothing Nothing Nothing
+ Nothing Nothing Nothing []
+ hops = [Hop pub minimalPayload, Hop pub emptyPayload]
+ construct sessionKey hops "" @?= Left (InvalidHopPayload 1)
]
+-- | A small, valid hop payload.
+minimalPayload :: HopPayload
+minimalPayload =
+ HopPayload (Just 1000) (Just 500000) Nothing Nothing Nothing Nothing []
+
testSharedSecretComputation :: TestTree
testSharedSecretComputation =
testCase "computes correct shared secrets (BOLT4 spec)" $ do
@@ -467,13 +541,11 @@ testSharedSecretComputation =
Secp256k1.parse_point (fromHex hop3PubKeyHex)
pub4 <- demand "parse_point" $
Secp256k1.parse_point (fromHex hop4PubKeyHex)
- let emptyPayload = HopPayload Nothing Nothing Nothing
- Nothing Nothing Nothing []
- hops = [ Hop pub0 emptyPayload
- , Hop pub1 emptyPayload
- , Hop pub2 emptyPayload
- , Hop pub3 emptyPayload
- , Hop pub4 emptyPayload
+ let hops = [ Hop pub0 minimalPayload
+ , Hop pub1 minimalPayload
+ , Hop pub2 minimalPayload
+ , Hop pub3 minimalPayload
+ , Hop pub4 minimalPayload
]
result = construct sessionKey hops ""
case result of
@@ -497,10 +569,8 @@ testPacketStructure =
Secp256k1.parse_point (fromHex hop0PubKeyHex)
pub1 <- demand "parse_point" $
Secp256k1.parse_point (fromHex hop1PubKeyHex)
- let emptyPayload = HopPayload Nothing Nothing Nothing
- Nothing Nothing Nothing []
- hops = [Hop pub0 emptyPayload,
- Hop pub1 emptyPayload]
+ let hops = [Hop pub0 minimalPayload,
+ Hop pub1 minimalPayload]
result = construct sessionKey hops ""
case result of
Left err ->
@@ -555,6 +625,7 @@ processTests = testGroup "packet processing" [
, testEphemeralKeyValidation
, testHmacValidation
, testProcessBasic
+ , testPayloadLength
]
testVersionValidation :: TestTree
@@ -685,9 +756,9 @@ testProcessBasic = testGroup "basic processing" [
rawPayloads = payloadWithHmac <> padding
ss <- demand "computeSharedSecret" $
computeSharedSecret sessionKey hop0PubKey
- let rhoKey = deriveRho ss
- muKey = deriveMu ss
- stream = generateStream rhoKey 1300
+ stream <- demand "generateStream" $
+ generateStream (deriveRho ss) 1300
+ let muKey = deriveMu ss
encryptedPayloads =
BS.pack (BS.zipWith xor rawPayloads stream)
correctHmac =
@@ -711,6 +782,68 @@ testProcessBasic = testGroup "basic processing" [
assertFailure "expected Receive, got Forward"
]
+-- | Encrypt 1300 bytes of plaintext hop_payloads into a packet for
+-- the node with secret key 'sessionKey' (using its own public key
+-- as the ephemeral key), with empty associated data.
+mkPacket :: BS.ByteString -> IO OnionPacket
+mkPacket plain = do
+ pub <- demand "derive_pub"
+ (Secp256k1.roll32 sessionKey >>= Secp256k1.derive_pub)
+ ss <- demand "computeSharedSecret" (computeSharedSecret sessionKey pub)
+ stream <- demand "generateStream" $
+ generateStream (deriveRho ss) hopPayloadsSize
+ let enc = BS.pack (BS.zipWith xor plain stream)
+ mac = computeHmac (deriveMu ss) enc BS.empty
+ hp <- demand "hopPayloads" (hopPayloads enc)
+ hm <- demand "hmac32" (hmac32 mac)
+ pure (OnionPacket versionByte (Secp256k1.serialize_point pub) hp hm)
+
+-- | Pad (or truncate) plaintext hop_payloads to 1300 bytes.
+pad1300 :: BS.ByteString -> BS.ByteString
+pad1300 bs = BS.take hopPayloadsSize (bs <> BS.replicate hopPayloadsSize 0)
+
+assertInvalidPayload :: Either RejectReason ProcessResult -> Assertion
+assertInvalidPayload r = case r of
+ Left (InvalidPayload _) -> pure ()
+ Left e -> assertFailure ("expected InvalidPayload, got " ++ show e)
+ Right _ -> assertFailure "expected rejection, got success"
+
+testPayloadLength :: TestTree
+testPayloadLength = testGroup "payload_length bounds" [
+ testCase "rejects payload_length below 2" $ do
+ pkt <- mkPacket (pad1300 (BS.pack [0x01, 0x02]))
+ assertInvalidPayload (process sessionKey pkt BS.empty)
+
+ , testCase "rejects payload_length past hop_payloads" $ do
+ -- payload_length 2000, covered by an odd TLV (type 257, length
+ -- 1994) that runs into the zero-extension stream
+ let prefix = BS.pack [0xfd, 0x07, 0xd0, 0xfd, 0x01, 0x01,
+ 0xfd, 0x07, 0xca]
+ pkt <- mkPacket (pad1300 prefix)
+ assertInvalidPayload (process sessionKey pkt BS.empty)
+
+ , testCase "rejects payload_length of 2^64 - 1" $ do
+ pkt <- mkPacket (pad1300 (BS.pack (0xff : replicate 8 0xff)))
+ assertInvalidPayload (process sessionKey pkt BS.empty)
+
+ , testCase "rejects payload one byte too long" $ do
+ -- 3-byte prefix + 1266-byte payload + 32-byte HMAC = 1301
+ let tlv = BS.pack [0x01, 0xfd, 0x04, 0xee] <> BS.replicate 1262 0
+ pkt <- mkPacket (pad1300 (encodeBigSize 1266 <> tlv))
+ assertInvalidPayload (process sessionKey pkt BS.empty)
+
+ , testCase "accepts the largest payload that fits" $ do
+ -- 3-byte prefix + 1265-byte payload + 32-byte HMAC = 1300
+ let val = BS.replicate 1261 0
+ tlv = BS.pack [0x01, 0xfd, 0x04, 0xed] <> val
+ pkt <- mkPacket (pad1300 (encodeBigSize 1265 <> tlv))
+ case process sessionKey pkt BS.empty of
+ Right (Receive ri) ->
+ hpUnknownTlvs (riPayload ri) @?= [TlvRecord 1 val]
+ Right (Forward _) -> assertFailure "expected Receive"
+ Left e -> assertFailure ("expected success, got " ++ show e)
+ ]
+
-- Error tests -------------------------------------------------------------
errorTests :: TestTree
@@ -720,6 +853,7 @@ errorTests = testGroup "error handling" [
, testMultiHopWrapping
, testErrorAttribution
, testFailureMessageParsing
+ , testFailureMessageFraming
]
-- Shared secrets for testing (deterministic)
@@ -735,24 +869,74 @@ testSecret3 = SharedSecret (BS.replicate 32 0x33)
testSecret4 :: SharedSecret
testSecret4 = SharedSecret (BS.replicate 32 0x44)
--- Simple failure message for testing
+-- Simple failure message for testing (htlc_msat 1000, height 800000)
testFailure :: FailureMessage
-testFailure =
- FailureMessage IncorrectOrUnknownPaymentDetails BS.empty []
+testFailure = FailureMessage IncorrectOrUnknownPaymentDetails
+ (BS.pack [0, 0, 0, 0, 0, 0, 0x03, 0xe8, 0, 0x0c, 0x35, 0x00]) []
+
+-- | Minimal well-formed data for a failure code.
+failureData :: FailureCode -> BS.ByteString
+failureData c
+ | c `elem` [InvalidOnionVersion, InvalidOnionHmac, InvalidOnionKey] =
+ BS.replicate 32 0xab
+ | c `elem` [TemporaryChannelFailure, ExpiryTooSoon] = BS.pack [0, 0]
+ | c `elem` [AmountBelowMinimum, FeeInsufficient] =
+ BS.replicate 8 0x01 <> BS.pack [0, 0]
+ | c == IncorrectCltvExpiry = BS.replicate 4 0x01 <> BS.pack [0, 0]
+ | c == ChannelDisabled = BS.pack [0, 1, 0, 0]
+ | c == IncorrectOrUnknownPaymentDetails = BS.replicate 12 0x01
+ | c == FinalIncorrectCltvExpiry = BS.replicate 4 0x01
+ | c == FinalIncorrectHtlcAmount = BS.replicate 8 0x01
+ | c == InvalidOnionPayload = BS.pack [0x02, 0x00, 0x05]
+ | otherwise = BS.empty
+
+-- | Read a big-endian u16 at an offset, failing the test if short.
+u16At :: Int -> BS.ByteString -> IO Int
+u16At off bs = case BS.unpack (BS.take 2 (BS.drop off bs)) of
+ [hi, lo] -> pure (fromIntegral hi * 256 + fromIntegral lo)
+ _ -> assertFailure "u16At: input too short"
+
+-- | 'constructError', failing the test on Nothing.
+mkError :: SharedSecret -> FailureMessage -> IO ErrorPacket
+mkError ss fm = demand "constructError" (constructError ss fm)
+
+-- | 'wrapError', failing the test on Nothing.
+wrapErr :: SharedSecret -> ErrorPacket -> IO ErrorPacket
+wrapErr ss pkt = demand "wrapError" (wrapError ss pkt)
+
+-- | Strip the failing node's obfuscation from a return packet and
+-- return (failure_len, pad_len, packet length).
+returnPacketLengths :: SharedSecret -> ErrorPacket -> IO (Int, Int, Int)
+returnPacketLengths ss pkt = do
+ ErrorPacket raw <- wrapErr ss pkt
+ failureLen <- u16At 32 raw
+ padLen <- u16At (34 + failureLen) raw
+ pure (failureLen, padLen, BS.length raw)
testErrorConstruction :: TestTree
-testErrorConstruction =
- testCase "error packet construction" $ do
- let errPacket = constructError testSecret1 testFailure
- ErrorPacket bs = errPacket
- assertBool "error packet >= 256 bytes"
- (BS.length bs >= minErrorPacketSize)
+testErrorConstruction = testGroup "error packet construction" [
+ testCase "short message is padded to 256" $ do
+ let failure = FailureMessage TemporaryNodeFailure BS.empty []
+ errPacket <- mkError testSecret1 failure
+ (fl, pl, size) <- returnPacketLengths testSecret1 errPacket
+ fl @?= 2
+ fl + pl @?= minErrorPacketSize
+ size @?= 32 + 2 + fl + 2 + pl
+ , testCase "long message is not truncated or padded" $ do
+ let failure = FailureMessage TemporaryNodeFailure BS.empty
+ [TlvRecord 1 (BS.replicate 300 0x00)]
+ errPacket <- mkError testSecret1 failure
+ (fl, pl, size) <- returnPacketLengths testSecret1 errPacket
+ fl @?= 2 + 1 + 3 + 300
+ pl @?= 0
+ size @?= 32 + 2 + fl + 2
+ ]
testErrorRoundtrip :: TestTree
testErrorRoundtrip =
testCase "construct and unwrap roundtrip" $ do
- let errPacket = constructError testSecret1 testFailure
- result = unwrapError [testSecret1] errPacket
+ errPacket <- mkError testSecret1 testFailure
+ let result = unwrapError [testSecret1] errPacket
case result of
Attributed idx msg -> do
idx @?= 0
@@ -766,10 +950,10 @@ testMultiHopWrapping = testGroup "multi-hop wrapping" [
testCase "3-hop route, error from hop 2 (final)" $ do
let secrets =
[testSecret1, testSecret2, testSecret3]
- err0 = constructError testSecret3 testFailure
- err1 = wrapError testSecret2 err0
- err2 = wrapError testSecret1 err1
- result = unwrapError secrets err2
+ err0 <- mkError testSecret3 testFailure
+ err1 <- wrapErr testSecret2 err0
+ err2 <- wrapErr testSecret1 err1
+ let result = unwrapError secrets err2
case result of
Attributed idx msg -> do
idx @?= 2
@@ -781,9 +965,9 @@ testMultiHopWrapping = testGroup "multi-hop wrapping" [
, testCase "4-hop route, error from hop 1" $ do
let secrets = [testSecret1, testSecret2,
testSecret3, testSecret4]
- err0 = constructError testSecret2 testFailure
- err1 = wrapError testSecret1 err0
- result = unwrapError secrets err1
+ err0 <- mkError testSecret2 testFailure
+ err1 <- wrapErr testSecret1 err0
+ let result = unwrapError secrets err1
case result of
Attributed idx msg -> do
idx @?= 1
@@ -795,8 +979,8 @@ testMultiHopWrapping = testGroup "multi-hop wrapping" [
, testCase "4-hop route, error from hop 0 (first)" $ do
let secrets = [testSecret1, testSecret2,
testSecret3, testSecret4]
- err0 = constructError testSecret1 testFailure
- result = unwrapError secrets err0
+ err0 <- mkError testSecret1 testFailure
+ let result = unwrapError secrets err0
case result of
Attributed idx msg -> do
idx @?= 0
@@ -809,8 +993,8 @@ testMultiHopWrapping = testGroup "multi-hop wrapping" [
testErrorAttribution :: TestTree
testErrorAttribution = testGroup "error attribution" [
testCase "wrong secrets gives UnknownOrigin" $ do
- let err = constructError testSecret1 testFailure
- wrongSecrets = [testSecret2, testSecret3]
+ err <- mkError testSecret1 testFailure
+ let wrongSecrets = [testSecret2, testSecret3]
result = unwrapError wrongSecrets err
case result of
UnknownOrigin _ -> return ()
@@ -819,8 +1003,8 @@ testErrorAttribution = testGroup "error attribution" [
"Expected UnknownOrigin with wrong secrets"
, testCase "empty secrets gives UnknownOrigin" $ do
- let err = constructError testSecret1 testFailure
- result = unwrapError [] err
+ err <- mkError testSecret1 testFailure
+ let result = unwrapError [] err
case result of
UnknownOrigin _ -> return ()
Attributed _ _ ->
@@ -834,9 +1018,9 @@ testErrorAttribution = testGroup "error attribution" [
, (InvalidOnionHmac, testSecret3)
]
mapM_ (\(code, secret) -> do
- let failure = FailureMessage code BS.empty []
- err = constructError secret failure
- result = unwrapError [secret] err
+ let failure = FailureMessage code (failureData code) []
+ err <- mkError secret failure
+ let result = unwrapError [secret] err
case result of
Attributed 0 msg -> fmCode msg @?= code
_ -> assertFailure $
@@ -848,11 +1032,11 @@ testFailureMessageParsing :: TestTree
testFailureMessageParsing =
testGroup "failure message parsing" [
testCase "code with data" $ do
- let failData = BS.replicate 10 0xAB
+ let failData = BS.pack [0, 0, 0, 0, 0, 0, 0x03, 0xe8, 0, 0]
failure =
FailureMessage AmountBelowMinimum failData []
- err = constructError testSecret1 failure
- result = unwrapError [testSecret1] err
+ err <- mkError testSecret1 failure
+ let result = unwrapError [testSecret1] err
case result of
Attributed 0 msg -> do
fmCode msg @?= AmountBelowMinimum
@@ -869,9 +1053,9 @@ testFailureMessageParsing =
, IncorrectOrUnknownPaymentDetails
]
mapM_ (\code -> do
- let failure = FailureMessage code BS.empty []
- err = constructError testSecret1 failure
- result = unwrapError [testSecret1] err
+ let failure = FailureMessage code (failureData code) []
+ err <- mkError testSecret1 failure
+ let result = unwrapError [testSecret1] err
case result of
Attributed 0 msg -> fmCode msg @?= code
_ -> assertFailure $
@@ -879,6 +1063,41 @@ testFailureMessageParsing =
) codes
]
+testFailureMessageFraming :: TestTree
+testFailureMessageFraming = testGroup "failure message framing" [
+ testCase "no length after the failure code" $ do
+ encodeFailureMessage
+ (FailureMessage TemporaryNodeFailure BS.empty [])
+ @?= BS.pack [0x20, 0x02]
+ encodeFailureMessage testFailure
+ @?= BS.pack [ 0x40, 0x0f, 0, 0, 0, 0, 0, 0, 0x03, 0xe8
+ , 0, 0x0c, 0x35, 0x00 ]
+ , testCase "decodes spec-framed messages" $ do
+ decodeFailureMessage (BS.pack [0x20, 0x02])
+ @?= Just (FailureMessage TemporaryNodeFailure BS.empty [])
+ decodeFailureMessage (encodeFailureMessage testFailure)
+ @?= Just testFailure
+ , testCase "splits data from the TLV stream by code" $ do
+ let fm = FailureMessage FinalIncorrectHtlcAmount
+ (BS.replicate 8 0x07) [TlvRecord 3 (BS.pack [1, 2])]
+ decodeFailureMessage (encodeFailureMessage fm) @?= Just fm
+ , testCase "channel_update length is honoured" $ do
+ let fm = FailureMessage TemporaryChannelFailure
+ (BS.pack [0, 3, 0xaa, 0xbb, 0xcc]) []
+ decodeFailureMessage (encodeFailureMessage fm) @?= Just fm
+ , testCase "ignores extra bytes" $
+ decodeFailureMessage (BS.pack [0x20, 0x02, 0xff])
+ @?= Just (FailureMessage TemporaryNodeFailure BS.empty [])
+ , testCase "unknown code keeps the remainder as data" $
+ decodeFailureMessage (BS.pack [0x20, 0x63, 0x01, 0x02])
+ @?= Just (FailureMessage (FailureCode 0x2063)
+ (BS.pack [0x01, 0x02]) [])
+ , testCase "rejects truncated data" $ do
+ decodeFailureMessage (BS.pack [0x40, 0x0f, 0, 0, 0]) @?= Nothing
+ decodeFailureMessage (BS.pack [0x10, 0x07, 0, 2, 0xaa]) @?= Nothing
+ decodeFailureMessage (BS.pack [0x20]) @?= Nothing
+ ]
+
-- Blinding tests -----------------------------------------------------------
-- Test data setup
@@ -1038,6 +1257,12 @@ blindingEphemeralKeyTests =
testSeed testSeedPubKey ss2
assertBool "results should differ"
(result1 /= result2)
+
+ , testCase "nextEphemeral rejects a secret not of 32 bytes" $ do
+ nextEphemeral (BS.singleton 0x07) testSeedPubKey testSharedSecretBS
+ @?= Nothing
+ nextEphemeral (BS.cons 0x00 testSeed) testSeedPubKey
+ testSharedSecretBS @?= Nothing
]
-- 3. TLV Encoding/Decoding Tests -----------------------------------------
@@ -1103,30 +1328,33 @@ blindingEncryptionTests =
testGroup "encryption/decryption" [
testCase "roundtrip: encrypt then decrypt" $ do
let rho = deriveBlindingRho testSharedSecretBS
- encrypted = encryptHopData rho sampleHopData
- decrypted = decryptHopData rho encrypted
- decrypted @?= Just sampleHopData
+ encrypted <- demand "encryptHopData" $
+ encryptHopData rho sampleHopData
+ decryptHopData rho encrypted @?= Just sampleHopData
, testCase "roundtrip: empty hop data" $ do
let rho = deriveBlindingRho testSharedSecretBS
- encrypted = encryptHopData rho emptyHopData
- decrypted = decryptHopData rho encrypted
- decrypted @?= Just emptyHopData
+ encrypted <- demand "encryptHopData" $
+ encryptHopData rho emptyHopData
+ decryptHopData rho encrypted @?= Just emptyHopData
, testCase "decryption with wrong key fails" $ do
let rho1 = deriveBlindingRho testSharedSecretBS
rho2 = deriveBlindingRho
(SharedSecret (BS.replicate 32 0xFF))
- encrypted = encryptHopData rho1 sampleHopData
- decrypted = decryptHopData rho2 encrypted
+ encrypted <- demand "encryptHopData" $
+ encryptHopData rho1 sampleHopData
assertBool "decryption should fail or produce garbage"
- (decrypted /= Just sampleHopData)
+ (decryptHopData rho2 encrypted /= Just sampleHopData)
, testCase "encrypt is deterministic" $ do
let rho = deriveBlindingRho testSharedSecretBS
encrypted1 = encryptHopData rho sampleHopData
encrypted2 = encryptHopData rho sampleHopData
encrypted1 @?= encrypted2
+
+ , testCase "encrypt fails on a key that is not 32 bytes" $
+ encryptHopData (DerivedKey "short") sampleHopData @?= Nothing
]
-- 5. createBlindedPath Tests ----------------------------------------------
@@ -1408,14 +1636,658 @@ genFailureMessage = do
[ InvalidRealm
, TemporaryNodeFailure
, PermanentNodeFailure
+ , RequiredNodeFeatureMissing
+ , InvalidOnionVersion
, InvalidOnionHmac
+ , InvalidOnionKey
, TemporaryChannelFailure
- , IncorrectOrUnknownPaymentDetails
+ , PermanentChannelFailure
, AmountBelowMinimum
, FeeInsufficient
+ , IncorrectCltvExpiry
, ExpiryTooSoon
+ , IncorrectOrUnknownPaymentDetails
+ , FinalIncorrectCltvExpiry
+ , FinalIncorrectHtlcAmount
+ , ChannelDisabled
+ , ExpiryTooFar
+ , InvalidOnionPayload
, MppTimeout
]
- dlen <- choose (0, 100 :: Int)
- dat <- BS.pack <$> vectorOf dlen arbitrary
- pure (FailureMessage code dat [])
+ dat <- genFailureData code
+ ntlv <- choose (0, 3 :: Int)
+ types <- (nub . sort) <$> vectorOf ntlv arbitrary
+ tlvs <- mapM (\t -> do
+ len <- choose (0, 20 :: Int)
+ TlvRecord t . BS.pack <$> vectorOf len arbitrary) types
+ pure (FailureMessage code dat tlvs)
+
+-- | Random well-formed data for a failure code.
+genFailureData :: FailureCode -> Gen BS.ByteString
+genFailureData c
+ | c `elem` [InvalidOnionVersion, InvalidOnionHmac, InvalidOnionKey] =
+ bytes 32
+ | c `elem` [TemporaryChannelFailure, ExpiryTooSoon] = update 0
+ | c `elem` [AmountBelowMinimum, FeeInsufficient] = update 8
+ | c == IncorrectCltvExpiry = update 4
+ | c == ChannelDisabled = update 2
+ | c == IncorrectOrUnknownPaymentDetails = bytes 12
+ | c == FinalIncorrectCltvExpiry = bytes 4
+ | c == FinalIncorrectHtlcAmount = bytes 8
+ | c == InvalidOnionPayload = do
+ typ <- arbitrary
+ off <- bytes 2
+ pure (encodeBigSize typ <> off)
+ | otherwise = pure BS.empty
+ where
+ bytes n = BS.pack <$> vectorOf n arbitrary
+ update n = do
+ fields <- bytes n
+ len <- choose (0, 300 :: Int)
+ cu <- bytes len
+ pure $ fields
+ <> BS.pack [fromIntegral (len `div` 256), fromIntegral len]
+ <> cu
+
+-- Spec vectors: onion-test.json --------------------------------------------
+
+-- | Shared secrets for the onion-test.json route (identical to those
+-- listed in onion-error-test.json).
+onionSharedSecrets :: IO [BS.ByteString]
+onionSharedSecrets = mapM unhex
+ [ V.errSharedSecret0, V.errSharedSecret1, V.errSharedSecret2
+ , V.errSharedSecret3, V.errSharedSecret4 ]
+
+-- | (node key, node pubkey, payload without length prefix) per hop.
+onionVectorHops
+ :: IO [(BS.ByteString, Secp256k1.Projective, BS.ByteString)]
+onionVectorHops = mapM hop
+ [ (V.onionNodeKey0, V.onionPubKey0, V.onionPayload0)
+ , (V.onionNodeKey1, V.onionPubKey1, V.onionPayload1)
+ , (V.onionNodeKey2, V.onionPubKey2, V.onionPayload2)
+ , (V.onionNodeKey3, V.onionPubKey3, V.onionPayload3)
+ , (V.onionNodeKey4, V.onionPubKey4, V.onionPayload4)
+ ]
+ where
+ hop (k, p, pl) = do
+ key <- unhex k
+ pub <- unhexPoint p
+ body <- unhex pl >>= unprefix
+ pure (key, pub, body)
+
+-- | Next ephemeral key per spec: SHA256(E || ss) * E.
+nextEphemeralKey
+ :: Secp256k1.Projective -> BS.ByteString -> Maybe Secp256k1.Projective
+nextEphemeralKey e ss = do
+ tweak <- Secp256k1.roll32
+ (SHA256.hash (Secp256k1.serialize_point e <> ss))
+ Secp256k1.mul e tweak
+
+-- | Process an onion hop by hop, checking each hop's payload and
+-- shared secret, and that each forwarded packet is a well-formed
+-- 1366-byte packet carrying the expected ephemeral key.
+peelOnion
+ :: BS.ByteString
+ -> [(BS.ByteString, BS.ByteString, BS.ByteString)]
+ -- ^ (node key, expected payload, expected shared secret)
+ -> Secp256k1.Projective
+ -- ^ expected ephemeral key of the packet
+ -> OnionPacket
+ -> Assertion
+peelOnion _ [] _ _ = assertFailure "no hops"
+peelOnion ad ((key, body, ss) : rest) eph pkt = do
+ opVersion pkt @?= versionByte
+ opEphemeralKey pkt @?= Secp256k1.serialize_point eph
+ case (process key pkt ad, rest) of
+ (Left e, _) -> assertFailure ("process failed: " ++ show e)
+ (Right (Receive ri), []) -> do
+ encodeHopPayload (riPayload ri) @?= body
+ riSharedSecret ri @?= SharedSecret ss
+ (Right (Forward fi), _ : _) -> do
+ encodeHopPayload (fiPayload fi) @?= body
+ fiSharedSecret fi @?= SharedSecret ss
+ let next = fiNextPacket fi
+ bytes = encodeOnionPacket next
+ BS.length bytes @?= onionPacketSize
+ decodeOnionPacket bytes @?= Just next
+ eph' <- demand "next ephemeral key" (nextEphemeralKey eph ss)
+ peelOnion ad rest eph' next
+ (Right (Receive _), _ : _) ->
+ assertFailure "expected Forward, got Receive"
+ (Right (Forward _), []) ->
+ assertFailure "expected Receive, got Forward"
+
+onionVectorTests :: TestTree
+onionVectorTests = testGroup "onion-test.json" [
+ testCase "construct reproduces the spec onion" $ do
+ hops <- onionVectorHops
+ sk <- unhex V.onionSessionKey
+ ad <- unhex V.onionAssocData
+ expected <- unhex V.onionPacket
+ sss <- onionSharedSecrets
+ route <- mapM (\(_, pub, body) -> do
+ hp <- demand "decodeHopPayload" (decodeHopPayload body)
+ encodeHopPayload hp @?= body
+ pure (Hop pub hp)) hops
+ case construct sk route ad of
+ Left e -> assertFailure ("construct failed: " ++ show e)
+ Right (pkt, secrets) -> do
+ encodeOnionPacket pkt @?= expected
+ secrets @?= map SharedSecret sss
+
+ , testCase "process peels the spec onion at every hop" $ do
+ hops <- onionVectorHops
+ sk <- unhex V.onionSessionKey
+ ad <- unhex V.onionAssocData
+ sss <- onionSharedSecrets
+ pkt <- unhex V.onionPacket >>=
+ demand "decodeOnionPacket" . decodeOnionPacket
+ e0 <- demand "session pubkey"
+ (Secp256k1.roll32 sk >>= Secp256k1.derive_pub)
+ let expect = zipWith (\(k, _, body) ss -> (k, body, ss)) hops sss
+ peelOnion ad expect e0 pkt
+ ]
+
+-- Multi-hop construct/process ----------------------------------------------
+
+-- | Construct an onion over a route of (node key, payload) pairs, then
+-- process it at every hop, checking payloads and shared secrets.
+roundtripRoute :: BS.ByteString -> [(BS.ByteString, HopPayload)] -> Bool
+roundtripRoute sk route = case mapM mkHop route of
+ Nothing -> False
+ Just hops -> case construct sk hops ad of
+ Left _ -> False
+ Right (pkt, secrets) ->
+ length secrets == length route && go (zip route secrets) pkt
+ where
+ ad = BS.replicate 32 0x42
+ mkHop (k, hp) = do
+ pub <- Secp256k1.roll32 k >>= Secp256k1.derive_pub
+ pure (Hop pub hp)
+ go [] _ = False
+ go [((k, hp), ss)] pkt = case process k pkt ad of
+ Right (Receive ri) -> riPayload ri == hp && riSharedSecret ri == ss
+ _ -> False
+ go (((k, hp), ss) : rest) pkt = case process k pkt ad of
+ Right (Forward fi) ->
+ fiPayload fi == hp
+ && fiSharedSecret fi == ss
+ && BS.length (encodeOnionPacket (fiNextPacket fi))
+ == onionPacketSize
+ && go rest (fiNextPacket fi)
+ _ -> False
+
+multiHopTests :: TestTree
+multiHopTests = testGroup "construct then process" [
+ testCase "routes of 1 to 20 hops" $
+ mapM_ (\n -> do
+ route <- mapM (\i -> do
+ scid <- assertScid (700000 + i) i 1
+ let hp = HopPayload
+ { hpAmtToForward = Just (1000 * fromIntegral i)
+ , hpOutgoingCltv = Just (500000 + i)
+ , hpShortChannelId = Just scid
+ , hpPaymentData = Nothing
+ , hpEncryptedData = Nothing
+ , hpCurrentPathKey = Nothing
+ , hpUnknownTlvs = []
+ }
+ pure (BS.replicate 32 (fromIntegral i), hp)) [1 .. n]
+ assertBool ("route of " ++ show n ++ " hops")
+ (roundtripRoute sessionKey route)) [1 .. 20 :: Word32]
+
+ , testProperty "random routes of 1 to 20 hops" $
+ forAll (choose (1, 20)) $ \n ->
+ forAll genSecKey $ \sk ->
+ forAll (vectorOf n genSecKey) $ \keys ->
+ forAll (vectorOf n (genRoutePayload n)) $ \pls ->
+ roundtripRoute sk (zip keys pls)
+ ]
+
+-- | A valid 32-byte secret key.
+genSecKey :: Gen BS.ByteString
+genSecKey = (BS.pack <$> vectorOf 32 arbitrary) `suchThat` valid
+ where
+ valid k = isJust (Secp256k1.roll32 k >>= Secp256k1.derive_pub)
+
+-- | A hop payload whose shift size fits n hops in 1300 bytes.
+--
+-- The fixed fields take at most 26 bytes; an odd TLV of type 65537
+-- adds at most 8 bytes of framing plus its value. With a 3-byte
+-- length prefix and 32-byte HMAC a hop then shifts at most 69 bytes
+-- plus the value length.
+genRoutePayload :: Int -> Gen HopPayload
+genRoutePayload n = do
+ amt <- arbitrary
+ cltv <- arbitrary
+ scid <- oneof [pure Nothing, genScid]
+ let budget = hopPayloadsSize `div` n - 69
+ extra <- if budget < 0
+ then pure []
+ else do
+ len <- choose (0, budget)
+ blob <- BS.pack <$> vectorOf len arbitrary
+ pure [TlvRecord 65537 blob]
+ pure HopPayload
+ { hpAmtToForward = Just amt
+ , hpOutgoingCltv = Just cltv
+ , hpShortChannelId = scid
+ , hpPaymentData = Nothing
+ , hpEncryptedData = Nothing
+ , hpCurrentPathKey = Nothing
+ , hpUnknownTlvs = extra
+ }
+ where
+ genScid :: Gen (Maybe ShortChannelId)
+ genScid = do
+ bh <- choose (0, 0xFFFFFF)
+ ti <- choose (0, 0xFFFFFF)
+ oi <- arbitrary
+ pure (shortChannelId bh ti oi)
+
+-- Spec vectors: onion-error-test.json --------------------------------------
+
+-- | Shared secrets for hops 0 to 4, in route order.
+errorSharedSecrets :: IO [SharedSecret]
+errorSharedSecrets = map SharedSecret <$> onionSharedSecrets
+
+-- | The failing node's (hop 4) shared secret, and those of the hops
+-- that wrap its return packet, from hop 3 back to hop 0.
+errorWrappers :: IO (SharedSecret, [SharedSecret])
+errorWrappers = do
+ sss <- errorSharedSecrets
+ case reverse sss of
+ ss4 : rest -> pure (ss4, rest)
+ [] -> assertFailure "no shared secrets"
+
+errorVectorTests :: TestTree
+errorVectorTests = testGroup "onion-error-test.json" [
+ testCase "ammag and um keys" $ do
+ sss <- errorSharedSecrets
+ ammags <- mapM unhex
+ [ V.errAmmagKey0, V.errAmmagKey1, V.errAmmagKey2
+ , V.errAmmagKey3, V.errAmmagKey4 ]
+ map deriveAmmag sss @?= map DerivedKey ammags
+ (ss4, _) <- errorWrappers
+ um4 <- unhex V.errUmKey4
+ deriveUm ss4 @?= DerivedKey um4
+
+ , testCase "failing node builds the spec return packet" $ do
+ (ss4, _) <- errorWrappers
+ msg <- unhex V.errFailureMessage
+ payload <- unhex V.errPayload4
+ fm <- demand "decodeFailureMessage" (decodeFailureMessage msg)
+ fm @?= FailureMessage TemporaryNodeFailure BS.empty []
+ encodeFailureMessage fm @?= msg
+ -- removing hop 4's own obfuscation leaves hmac || payload
+ ErrorPacket raw <- mkError ss4 fm >>= wrapErr ss4
+ BS.drop 32 raw @?= payload
+
+ , testCase "wrapping at each hop yields the spec packet" $ do
+ (ss4, wrappers) <- errorWrappers
+ expected <- unhex V.errPacket
+ let fm = FailureMessage TemporaryNodeFailure BS.empty []
+ initial <- mkError ss4 fm
+ final <- foldM (flip wrapErr) initial wrappers
+ final @?= ErrorPacket expected
+
+ , testCase "origin attributes the spec packet to hop 4" $ do
+ sss <- errorSharedSecrets
+ pkt <- unhex V.errPacket
+ unwrapError sss (ErrorPacket pkt)
+ @?= Attributed 4 (FailureMessage TemporaryNodeFailure BS.empty [])
+ ]
+
+-- Spec vectors: 'Returning Errors' trace -----------------------------------
+
+-- | incorrect_or_unknown_payment_details with htlc_msat 100, height
+-- 800000 and a 300-byte TLV of type 34001.
+traceFailure :: FailureMessage
+traceFailure = FailureMessage IncorrectOrUnknownPaymentDetails
+ (BS.pack [0, 0, 0, 0, 0, 0, 0, 100, 0, 0x0c, 0x35, 0x00])
+ [TlvRecord 34001 (BS.replicate 300 0x80)]
+
+traceVectorTests :: TestTree
+traceVectorTests = testGroup "returning errors trace" [
+ testCase "failure message encoding" $ do
+ -- the trace lists failuremsg || pad_len || pad
+ encoded <- unhex V.traceFailureMessage
+ raw <- unhex V.traceRawPacket
+ let msg = encodeFailureMessage traceFailure
+ BS.take (BS.length msg) encoded @?= msg
+ decodeFailureMessage msg @?= Just traceFailure
+ failureLen <- u16At 32 raw
+ failureLen @?= BS.length msg
+ BS.take failureLen (BS.drop 34 raw) @?= msg
+
+ , testCase "each hop's packet matches the trace" $ do
+ (ss4, wrappers) <- errorWrappers
+ raw <- unhex V.traceRawPacket
+ expected <- mapM unhex
+ [ V.tracePacket4, V.tracePacket3, V.tracePacket2
+ , V.tracePacket1, V.tracePacket0 ]
+ packets <- demand "wrapError" $ sequence $ drop 1 $
+ scanl (\p ss -> p >>= wrapError ss) (Just (ErrorPacket raw))
+ (ss4 : wrappers)
+ packets @?= map ErrorPacket expected
+
+ , testCase "origin attributes the trace packet to hop 4" $ do
+ sss <- errorSharedSecrets
+ pkt <- unhex V.tracePacket0
+ unwrapError sss (ErrorPacket pkt) @?= Attributed 4 traceFailure
+ ]
+
+-- Spec vectors: route-blinding-test.json -----------------------------------
+
+-- | Blinded private key HMAC256("blinded_node_id", ss) * k, where
+-- ss = ECDH(k, path_key). The library has no function for this, so
+-- it is assembled from exported primitives.
+blindedPrivKey
+ :: BS.ByteString -> Secp256k1.Projective -> Maybe BS.ByteString
+blindedPrivKey k pathKey = do
+ SharedSecret ss <- computeSharedSecret k pathKey
+ let SHA256.MAC tweak = SHA256.hmac "blinded_node_id" ss
+ blindSecKey k (BlindingFactor tweak)
+
+rbBobData :: IO BlindedHopData
+rbBobData = do
+ scid <- assertScid 0 0 1729
+ pure emptyHopData
+ { bhdPadding = Just (BS.replicate 26 0)
+ , bhdShortChannelId = Just scid
+ , bhdPaymentRelay = Just (PaymentRelay 36 150 10000)
+ , bhdPaymentConstraints = Just (PaymentConstraints 748005 1500)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+rbCarolData :: IO BlindedHopData
+rbCarolData = do
+ scid <- assertScid 0 0 1105
+ override <- unhex V.rbDavePathKey
+ pure emptyHopData
+ { bhdShortChannelId = Just scid
+ , bhdNextPathKeyOverride = Just override
+ , bhdPaymentRelay = Just (PaymentRelay 48 100 500)
+ , bhdPaymentConstraints = Just (PaymentConstraints 747969 1500)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+rbDaveData :: IO BlindedHopData
+rbDaveData = do
+ scid <- assertScid 0 0 561
+ pure emptyHopData
+ { bhdPadding = Just (BS.replicate 35 0)
+ , bhdShortChannelId = Just scid
+ , bhdPaymentRelay = Just (PaymentRelay 144 250 0)
+ , bhdPaymentConstraints = Just (PaymentConstraints 747921 1500)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+rbEveData :: IO BlindedHopData
+rbEveData = pure emptyHopData
+ { bhdPadding = Just (BS.replicate 26 0)
+ , bhdPathId = Just (BS.pack [0xde, 0xad, 0xbe, 0xef])
+ , bhdPaymentConstraints = Just (PaymentConstraints 747777 1500)
+ , bhdAllowedFeatures = Just (BS.cons 0x02 (BS.replicate 14 0))
+ }
+
+-- | A hop of route-blinding-test.json. 'rbUnknown' holds the unknown
+-- odd TLVs that the hop's data carries in addition to 'rbData'.
+data RbHop = RbHop
+ { rbData :: !BlindedHopData
+ , rbUnknown :: ![TlvRecord]
+ , rbNodeId :: !Secp256k1.Projective
+ , rbTlvs :: !BS.ByteString
+ , rbPathPrivKey :: !BS.ByteString
+ , rbPathKey :: !Secp256k1.Projective
+ , rbSharedSecret :: !BS.ByteString
+ , rbRho :: !BS.ByteString
+ , rbEncrypted :: !BS.ByteString
+ , rbBlindedNodeId :: !BS.ByteString
+ , rbNodeKey :: !BS.ByteString
+ , rbBlindedKey :: !BS.ByteString
+ , rbNextPathKey :: !Secp256k1.Projective
+ }
+
+rbHops :: IO (RbHop, RbHop, RbHop, RbHop)
+rbHops = do
+ bob <- hop rbBobData [TlvRecord 561 (BS.pack [0x12, 0x34, 0x56])]
+ ( V.rbBobNodeId, V.rbBobTlvs, V.rbBobPathPrivKey, V.rbBobPathKey
+ , V.rbBobSharedSecret, V.rbBobRho, V.rbBobEncryptedData
+ , V.rbBobBlindedNodeId )
+ (V.rbBobNodeKey, V.rbBobBlindedPrivKey, V.rbBobNextPathKey)
+ carol <- hop rbCarolData []
+ ( V.rbCarolNodeId, V.rbCarolTlvs, V.rbCarolPathPrivKey
+ , V.rbCarolPathKey, V.rbCarolSharedSecret, V.rbCarolRho
+ , V.rbCarolEncryptedData, V.rbCarolBlindedNodeId )
+ (V.rbCarolNodeKey, V.rbCarolBlindedPrivKey, V.rbCarolNextPathKey)
+ dave <- hop rbDaveData []
+ ( V.rbDaveNodeId, V.rbDaveTlvs, V.rbDavePathPrivKey, V.rbDavePathKey
+ , V.rbDaveSharedSecret, V.rbDaveRho, V.rbDaveEncryptedData
+ , V.rbDaveBlindedNodeId )
+ (V.rbDaveNodeKey, V.rbDaveBlindedPrivKey, V.rbDaveNextPathKey)
+ eve <- hop rbEveData [TlvRecord 65535 (BS.pack [0x06, 0xc1])]
+ ( V.rbEveNodeId, V.rbEveTlvs, V.rbEvePathPrivKey, V.rbEvePathKey
+ , V.rbEveSharedSecret, V.rbEveRho, V.rbEveEncryptedData
+ , V.rbEveBlindedNodeId )
+ (V.rbEveNodeKey, V.rbEveBlindedPrivKey, V.rbEveNextPathKey)
+ pure (bob, carol, dave, eve)
+ where
+ hop mkData unknown (nid, tlvs, ppk, pk, ss, rho, enc, bid)
+ (nk, bk, npk) =
+ RbHop <$> mkData <*> pure unknown <*> unhexPoint nid
+ <*> unhex tlvs <*> unhex ppk <*> unhexPoint pk <*> unhex ss
+ <*> unhex rho <*> unhex enc <*> unhex bid <*> unhex nk
+ <*> unhex bk <*> unhexPoint npk
+
+routeBlindingTests :: TestTree
+routeBlindingTests = testGroup "route-blinding-test.json" [
+ testCase "encrypted_data_tlv encoding" $ do
+ (bob, carol, dave, eve) <- rbHops
+ mapM_ (\h -> do
+ -- unknown odd TLVs are the last records in the vectors
+ encodeBlindedHopData (rbData h) <> encodeTlvStream (rbUnknown h)
+ @?= rbTlvs h
+ decodeBlindedHopData (rbTlvs h) @?= Just (rbData h))
+ [bob, carol, dave, eve]
+
+ , testCase "per-hop shared secret and rho" $ do
+ (bob, carol, dave, eve) <- rbHops
+ mapM_ (\h -> do
+ ss <- demand "computeSharedSecret" $
+ computeSharedSecret (rbPathPrivKey h) (rbNodeId h)
+ ss @?= SharedSecret (rbSharedSecret h)
+ deriveBlindingRho ss @?= DerivedKey (rbRho h)
+ deriveBlindedNodeId ss (rbNodeId h)
+ @?= Just (rbBlindedNodeId h))
+ [bob, carol, dave, eve]
+
+ , testCase "nextEphemeral yields the next path key pair" $ do
+ (bob, carol, dave, eve) <- rbHops
+ mapM_ (\(h, nxt) -> do
+ let ss = SharedSecret (rbSharedSecret h)
+ nextEphemeral (rbPathPrivKey h) (rbPathKey h) ss
+ @?= Just (rbPathPrivKey nxt, rbPathKey nxt))
+ [(bob, carol), (dave, eve)]
+
+ , testCase "createBlindedPath (Bob -> Carol)" $ do
+ (bob, carol, _, _) <- rbHops
+ path <- either (assertFailure . show) pure $
+ createBlindedPath (rbPathPrivKey bob)
+ [(rbNodeId bob, rbData bob), (rbNodeId carol, rbData carol)]
+ bpIntroductionNode path @?= rbNodeId bob
+ bpBlindingKey path @?= rbPathKey bob
+ case bpBlindedHops path of
+ [b, c] -> do
+ bhBlindedNodeId b @?= rbBlindedNodeId bob
+ bhBlindedNodeId c @?= rbBlindedNodeId carol
+ -- Bob's data in the vector carries an unknown TLV (561)
+ -- that BlindedHopData cannot represent, so only Carol's
+ -- encrypted_data can be reproduced.
+ bhEncryptedData c @?= rbEncrypted carol
+ _ -> assertFailure "expected 2 blinded hops"
+
+ , testCase "createBlindedPath (Dave -> Eve)" $ do
+ (_, _, dave, eve) <- rbHops
+ path <- either (assertFailure . show) pure $
+ createBlindedPath (rbPathPrivKey dave)
+ [(rbNodeId dave, rbData dave), (rbNodeId eve, rbData eve)]
+ bpIntroductionNode path @?= rbNodeId dave
+ bpBlindingKey path @?= rbPathKey dave
+ case bpBlindedHops path of
+ [d, e] -> do
+ bhBlindedNodeId d @?= rbBlindedNodeId dave
+ bhBlindedNodeId e @?= rbBlindedNodeId eve
+ -- Eve's data carries an unknown TLV (65535); see above.
+ bhEncryptedData d @?= rbEncrypted dave
+ _ -> assertFailure "expected 2 blinded hops"
+
+ , testCase "unblinding at each hop" $ do
+ (bob, carol, dave, eve) <- rbHops
+ mapM_ (\h -> do
+ case processBlindedHop (rbNodeKey h) (rbPathKey h)
+ (rbEncrypted h) of
+ Left e -> assertFailure ("processBlindedHop: " ++ show e)
+ Right (dat, npk) -> do
+ dat @?= rbData h
+ npk @?= rbNextPathKey h
+ blindedPrivKey (rbNodeKey h) (rbPathKey h)
+ @?= Just (rbBlindedKey h))
+ [bob, carol, dave, eve]
+ ]
+
+-- Spec vectors: blinded-payment-onion-test.json ----------------------------
+
+bpBobData :: IO BlindedHopData
+bpBobData = do
+ scid <- assertScid 0 0 1
+ pure emptyHopData
+ { bhdPadding = Just (BS.replicate 32 0)
+ , bhdShortChannelId = Just scid
+ , bhdPaymentRelay = Just (PaymentRelay 50 0 10000)
+ , bhdPaymentConstraints = Just (PaymentConstraints 750150 50)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+bpCarolData :: IO BlindedHopData
+bpCarolData = do
+ scid <- assertScid 0 0 2
+ override <- unhex V.rbDavePathKey
+ pure emptyHopData
+ { bhdShortChannelId = Just scid
+ , bhdNextPathKeyOverride = Just override
+ , bhdPaymentRelay = Just (PaymentRelay 75 150 100)
+ , bhdPaymentConstraints = Just (PaymentConstraints 750100 50)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+bpDaveData :: IO BlindedHopData
+bpDaveData = do
+ scid <- assertScid 0 0 3
+ pure emptyHopData
+ { bhdPadding = Just (BS.replicate 34 0)
+ , bhdShortChannelId = Just scid
+ , bhdPaymentRelay = Just (PaymentRelay 25 100 0)
+ , bhdPaymentConstraints = Just (PaymentConstraints 750025 50)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+bpEveData :: IO BlindedHopData
+bpEveData = do
+ pathId <- unhex "c9cf92f45ade68345bc20ae672e2012f4af487ed4415"
+ pure emptyHopData
+ { bhdPadding = Just (BS.replicate 28 0)
+ , bhdPathId = Just pathId
+ , bhdPaymentConstraints = Just (PaymentConstraints 750000 50)
+ , bhdAllowedFeatures = Just BS.empty
+ }
+
+-- | Per hop: (key the onion is encrypted to, onion received, payload
+-- without length prefix).
+bpRoute :: IO [(BS.ByteString, Secp256k1.Projective, BS.ByteString,
+ BS.ByteString)]
+bpRoute = do
+ alice <- unhex V.onionNodeKey0
+ bob <- unhex V.rbBobNodeKey
+ -- Carol, Dave and Eve decrypt with their blinded private keys
+ carol <- unhex V.rbCarolBlindedPrivKey
+ dave <- unhex V.rbDaveBlindedPrivKey
+ eve <- unhex V.rbEveBlindedPrivKey
+ mapM (\(k, pub, onion, pl) -> do
+ p <- unhexPoint pub
+ o <- unhex onion
+ body <- unhex pl >>= unprefix
+ pure (k, p, o, body))
+ [ (alice, V.bpAlicePubKey, V.bpAliceOnion, V.bpAlicePayload)
+ , (bob, V.bpBobPubKey, V.bpBobOnion, V.bpBobPayload)
+ , (carol, V.bpCarolPubKey, V.bpCarolOnion, V.bpCarolPayload)
+ , (dave, V.bpDavePubKey, V.bpDaveOnion, V.bpDavePayload)
+ , (eve, V.bpEvePubKey, V.bpEveOnion, V.bpEvePayload)
+ ]
+
+blindedPaymentTests :: TestTree
+blindedPaymentTests = testGroup "blinded-payment-onion-test.json" [
+ testCase "construct reproduces the spec onion" $ do
+ route <- bpRoute
+ sk <- unhex V.bpSessionKey
+ ad <- unhex V.bpAssocData
+ hops <- mapM (\(_, pub, _, body) -> do
+ hp <- demand "decodeHopPayload" (decodeHopPayload body)
+ encodeHopPayload hp @?= body
+ pure (Hop pub hp)) route
+ expected <- unhex V.bpAliceOnion
+ case construct sk hops ad of
+ Left e -> assertFailure ("construct failed: " ++ show e)
+ Right (pkt, _) -> encodeOnionPacket pkt @?= expected
+
+ , testCase "each hop forwards the spec onion" $ do
+ route <- bpRoute
+ ad <- unhex V.bpAssocData
+ let go [] = assertFailure "no hops"
+ go ((k, _, onion, body) : rest) = do
+ pkt <- demand "decodeOnionPacket" (decodeOnionPacket onion)
+ case (process k pkt ad, rest) of
+ (Left e, _) -> assertFailure ("process failed: " ++ show e)
+ (Right (Forward fi), (_, _, next, _) : _) -> do
+ encodeHopPayload (fiPayload fi) @?= body
+ encodeOnionPacket (fiNextPacket fi) @?= next
+ go rest
+ (Right (Receive ri), []) ->
+ encodeHopPayload (riPayload ri) @?= body
+ (Right _, _) -> assertFailure "unexpected result"
+ go route
+
+ , testCase "blinded hops decrypt their data and next path key" $ do
+ bobKey <- unhex V.rbBobNodeKey
+ carolKey <- unhex V.rbCarolNodeKey
+ daveKey <- unhex V.rbDaveNodeKey
+ eveKey <- unhex V.rbEveNodeKey
+ firstPathKey <- unhexPoint V.rbBobPathKey
+ bobNext <- unhexPoint V.bpBobNextPathKey
+ carolNext <- unhexPoint V.bpCarolNextPathKey
+ daveNext <- unhexPoint V.bpDaveNextPathKey
+ eveNext <- unhexPoint V.bpEveNextPathKey
+ bobBody <- unhex V.bpBobPayload >>= unprefix
+ bobHp <- demand "decodeHopPayload" (decodeHopPayload bobBody)
+ hpCurrentPathKey bobHp @?= Just (Secp256k1.serialize_point firstPathKey)
+ let encOf pl = do
+ body <- unhex pl >>= unprefix
+ hp <- demand "decodeHopPayload" (decodeHopPayload body)
+ demand "encrypted_recipient_data" (hpEncryptedData hp)
+ bobData <- bpBobData
+ carolData <- bpCarolData
+ daveData <- bpDaveData
+ eveData <- bpEveData
+ hops <- sequence
+ [ (,,,,) bobKey firstPathKey <$> encOf V.bpBobPayload
+ <*> pure bobData <*> pure bobNext
+ , (,,,,) carolKey bobNext <$> encOf V.bpCarolPayload
+ <*> pure carolData <*> pure carolNext
+ , (,,,,) daveKey carolNext <$> encOf V.bpDavePayload
+ <*> pure daveData <*> pure daveNext
+ , (,,,,) eveKey daveNext <$> encOf V.bpEvePayload
+ <*> pure eveData <*> pure eveNext
+ ]
+ mapM_ (\(k, pk, enc, dat, npk) ->
+ processBlindedHop k pk enc @?= Right (dat, npk)) hops
+ ]
diff --git a/test/Vectors.hs b/test/Vectors.hs
@@ -0,0 +1,1100 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+-- |
+-- Module: Vectors
+-- Copyright: (c) 2025 Jared Tobin
+-- License: MIT
+-- Maintainer: Jared Tobin <jared@ppad.tech>
+--
+-- BOLT4 test vectors, hex-encoded.
+--
+-- Taken from the bolt04/*.json files in lightning/bolts, and from the
+-- 'Returning Errors' trace in 04-onion-routing.md.
+
+module Vectors where
+
+import qualified Data.ByteString as BS
+
+-- onion-test.json ---------------------------------------------------------
+
+-- | Session key.
+onionSessionKey :: BS.ByteString
+onionSessionKey =
+ "4141414141414141414141414141414141414141414141414141414141414141"
+
+-- | Associated data.
+onionAssocData :: BS.ByteString
+onionAssocData =
+ "4242424242424242424242424242424242424242424242424242424242424242"
+
+-- | Hop 0 public key.
+onionPubKey0 :: BS.ByteString
+onionPubKey0 =
+ "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"
+
+-- | Hop 0 payload, with its length prefix.
+onionPayload0 :: BS.ByteString
+onionPayload0 = "1202023a98040205dc06080000000000000001"
+
+-- | Hop 1 public key.
+onionPubKey1 :: BS.ByteString
+onionPubKey1 =
+ "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"
+
+-- | Hop 1 payload, with its length prefix.
+onionPayload1 :: BS.ByteString
+onionPayload1 = BS.concat
+ [ "52020236b00402057806080000000000000002fd02013c010203040506070809"
+ , "0a0b0c0d0e0f0102030405060708090a0b0c0d0e0f0102030405060708090a0b"
+ , "0c0d0e0f0102030405060708090a0b0c0d0e0f"
+ ]
+
+-- | Hop 2 public key.
+onionPubKey2 :: BS.ByteString
+onionPubKey2 =
+ "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"
+
+-- | Hop 2 payload, with its length prefix.
+onionPayload2 :: BS.ByteString
+onionPayload2 = "12020230d4040204e206080000000000000003"
+
+-- | Hop 3 public key.
+onionPubKey3 :: BS.ByteString
+onionPubKey3 =
+ "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"
+
+-- | Hop 3 payload, with its length prefix.
+onionPayload3 :: BS.ByteString
+onionPayload3 = "1202022710040203e806080000000000000004"
+
+-- | Hop 4 public key.
+onionPubKey4 :: BS.ByteString
+onionPubKey4 =
+ "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"
+
+-- | Hop 4 payload, with its length prefix.
+onionPayload4 :: BS.ByteString
+onionPayload4 = BS.concat
+ [ "fd011002022710040203e8082224a33562c54507a9334e79f0dc4f17d407e6d7"
+ , "c61f0e2f3d0d38599502f617042710fd012de02a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ , "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"
+ ]
+
+-- | The resulting onion packet.
+onionPacket :: BS.ByteString
+onionPacket = BS.concat
+ [ "0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368"
+ , "6619f7f3416a5aa36dc7eeb3ec6d421e9615471ab870a33ac07fa5d5a51df0a8"
+ , "823aabe3fea3f90d387529d4f72837f9e687230371ccd8d263072206dbed0234"
+ , "f6505e21e282abd8c0e4f5b9ff8042800bbab065036eadd0149b37f27dde6647"
+ , "25a49866e052e809d2b0198ab9610faa656bbf4ec516763a59f8f42c171b1791"
+ , "66ba38958d4f51b39b3e98706e2d14a2dafd6a5df808093abfca5aeaaca16ede"
+ , "d5db7d21fb0294dd1a163edf0fb445d5c8d7d688d6dd9c541762bf5a5123bf99"
+ , "39d957fe648416e88f1b0928bfa034982b22548e1a4d922690eecf546275afb2"
+ , "33acf4323974680779f1a964cfe687456035cc0fba8a5428430b390f0057b6d1"
+ , "fe9a8875bfa89693eeb838ce59f09d207a503ee6f6299c92d6361bc335fcbf9b"
+ , "5cd44747aadce2ce6069cfdc3d671daef9f8ae590cf93d957c9e873e9a1bc62d"
+ , "9640dc8fc39c14902d49a1c80239b6c5b7fd91d05878cbf5ffc7db2569f47c43"
+ , "d6c0d27c438abff276e87364deb8858a37e5a62c446af95d8b786eaf0b5fcf78"
+ , "d98b41496794f8dcaac4eef34b2acfb94c7e8c32a9e9866a8fa0b6f2a06f00a1"
+ , "ccde569f97eec05c803ba7500acc96691d8898d73d8e6a47b8f43c3d5de74458"
+ , "d20eda61474c426359677001fbd75a74d7d5db6cb4feb83122f133206203e4e2"
+ , "d293f838bf8c8b3a29acb321315100b87e80e0edb272ee80fda944e3fb6084ed"
+ , "4d7f7c7d21c69d9da43d31a90b70693f9b0cc3eac74c11ab8ff655905688916c"
+ , "fa4ef0bd04135f2e50b7c689a21d04e8e981e74c6058188b9b1f9dfc3eec6838"
+ , "e9ffbcf22ce738d8a177c19318dffef090cee67e12de1a3e2a39f61247547ba5"
+ , "257489cbc11d7d91ed34617fcc42f7a9da2e3cf31a94a210a1018143173913c3"
+ , "8f60e62b24bf0d7518f38b5bab3e6a1f8aeb35e31d6442c8abb5178efc892d2e"
+ , "787d79c6ad9e2fc271792983fa9955ac4d1d84a36c024071bc6e431b625519d5"
+ , "56af38185601f70e29035ea6a09c8b676c9d88cf7e05e0f17098b584c4168735"
+ , "940263f940033a220f40be4c85344128b14beb9e75696db37014107801a59b13"
+ , "e89cd9d2258c169d523be6d31552c44c82ff4bb18ec9f099f3bf0e5b1bb2ba9a"
+ , "87d7e26f98d294927b600b5529c47e04d98956677cbcee8fa2b60f49776d8b8c"
+ , "367465b7c626da53700684fb6c918ead0eab8360e4f60edd25b4f43816a75ecf"
+ , "70f909301825b512469f8389d79402311d8aecb7b3ef8599e79485a4388d8774"
+ , "4d899f7c47ee644361e17040a7958c8911be6f463ab6a9b2afacd688ec55ef51"
+ , "7b38f1339efc54487232798bb25522ff4572ff68567fe830f92f7b8113efce3e"
+ , "98c3fffbaedce4fd8b50e41da97c0c08e423a72689cc68e68f752a5e3a9003e6"
+ , "4e35c957ca2e1c48bb6f64b05f56b70b575ad2f278d57850a7ad568c24a4d32a"
+ , "3d74b29f03dc125488bc7c637da582357f40b0a52d16b3b40bb2c2315d03360b"
+ , "c24209e20972c200566bcf3bbe5c5b0aedd83132a8a4d5b4242ba370b6d67d9b"
+ , "67eb01052d132c7866b9cb502e44796d9d356e4e3cb47cc527322cd24976fe7c"
+ , "9257a2864151a38e568ef7a79f10d6ef27cc04ce382347a2488b1f404fdbf407"
+ , "fe1ca1c9d0d5649e34800e25e18951c98cae9f43555eef65fee1ea8f15828807"
+ , "366c3b612cd5753bf9fb8fced08855f742cddd6f765f74254f03186683d646e6"
+ , "f09ac2805586c7cf11998357cafc5df3f285329366f475130c928b2dceba4aa3"
+ , "83758e7a9d20705c4bb9db619e2992f608a1ba65db254bb389468741d0502e25"
+ , "88aeb54390ac600c19af5c8e61383fc1bebe0029e4474051e4ef908828db9cca"
+ , "13277ef65db3fd47ccc2179126aaefb627719f421e20"
+ ]
+
+-- | Hop 0 node private key.
+onionNodeKey0 :: BS.ByteString
+onionNodeKey0 =
+ "4141414141414141414141414141414141414141414141414141414141414141"
+
+-- | Hop 1 node private key.
+onionNodeKey1 :: BS.ByteString
+onionNodeKey1 =
+ "4242424242424242424242424242424242424242424242424242424242424242"
+
+-- | Hop 2 node private key.
+onionNodeKey2 :: BS.ByteString
+onionNodeKey2 =
+ "4343434343434343434343434343434343434343434343434343434343434343"
+
+-- | Hop 3 node private key.
+onionNodeKey3 :: BS.ByteString
+onionNodeKey3 =
+ "4444444444444444444444444444444444444444444444444444444444444444"
+
+-- | Hop 4 node private key.
+onionNodeKey4 :: BS.ByteString
+onionNodeKey4 =
+ "4545454545454545454545454545454545454545454545454545454545454545"
+
+-- onion-error-test.json ---------------------------------------------------
+
+-- | Failure message.
+errFailureMessage :: BS.ByteString
+errFailureMessage = "2002"
+
+-- | Hop 0 shared secret.
+errSharedSecret0 :: BS.ByteString
+errSharedSecret0 =
+ "53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66"
+
+-- | Hop 0 ammag key.
+errAmmagKey0 :: BS.ByteString
+errAmmagKey0 =
+ "3761ba4d3e726d8abb16cba5950ee976b84937b61b7ad09e741724d7dee12eb5"
+
+-- | Hop 1 shared secret.
+errSharedSecret1 :: BS.ByteString
+errSharedSecret1 =
+ "a6519e98832a0b179f62123b3567c106db99ee37bef036e783263602f3488fae"
+
+-- | Hop 1 ammag key.
+errAmmagKey1 :: BS.ByteString
+errAmmagKey1 =
+ "59ee5867c5c151daa31e36ee42530f429c433836286e63744f2020b980302564"
+
+-- | Hop 2 shared secret.
+errSharedSecret2 :: BS.ByteString
+errSharedSecret2 =
+ "3a6b412548762f0dbccce5c7ae7bb8147d1caf9b5471c34120b30bc9c04891cc"
+
+-- | Hop 2 ammag key.
+errAmmagKey2 :: BS.ByteString
+errAmmagKey2 =
+ "1bf08df8628d452141d56adfd1b25c1530d7921c23cecfc749ac03a9b694b0d3"
+
+-- | Hop 3 shared secret.
+errSharedSecret3 :: BS.ByteString
+errSharedSecret3 =
+ "21e13c2d7cfe7e18836df50872466117a295783ab8aab0e7ecc8c725503ad02d"
+
+-- | Hop 3 ammag key.
+errAmmagKey3 :: BS.ByteString
+errAmmagKey3 =
+ "cd9ac0e09064f039fa43a31dea05f5fe5f6443d40a98be4071af4a9d704be5ad"
+
+-- | Hop 4 shared secret.
+errSharedSecret4 :: BS.ByteString
+errSharedSecret4 =
+ "b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328"
+
+-- | Hop 4 ammag key.
+errAmmagKey4 :: BS.ByteString
+errAmmagKey4 =
+ "2f36bb8822e1f0d04c27b7d8bb7d7dd586e032a3218b8d414afbba6f169a4d68"
+
+-- | Hop 4 um key.
+errUmKey4 :: BS.ByteString
+errUmKey4 = "4da7f2923edce6c2d85987d1d9fa6d88023e6c3a9c3d20f07d3b10b61a78d646"
+
+-- | Hop 4 return packet, without its HMAC.
+errPayload4 :: BS.ByteString
+errPayload4 = BS.concat
+ [ "0002200200fe0000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "00000000"
+ ]
+
+-- | The return packet received by the origin.
+errPacket :: BS.ByteString
+errPacket = BS.concat
+ [ "9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed"
+ , "3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8"
+ , "bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d9"
+ , "90f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc"
+ , "28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7f"
+ , "bb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79"
+ , "f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf8"
+ , "84064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c1"
+ , "18f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448"
+ , "b604d12d"
+ ]
+
+-- BOLT4 'Returning Errors' trace ------------------------------------------
+
+-- | Encoded failure message, followed by pad_len and pad.
+traceFailureMessage :: BS.ByteString
+traceFailureMessage = BS.concat
+ [ "400f0000000000000064000c3500fd84d1fd012c808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "02c0000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000"
+ ]
+
+-- | Return packet before obfuscation.
+traceRawPacket :: BS.ByteString
+traceRawPacket = BS.concat
+ [ "fda7e11974f78ca6cc456f2d17ae54463664696e93842548245dd2a2c513a626"
+ , "0140400f0000000000000064000c3500fd84d1fd012c80808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "8080808080808080808080808080808080808080808080808080808080808080"
+ , "808002c000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000000000000000000000000000000000000000000000000000000000"
+ , "00000000"
+ ]
+
+-- | Return packet as sent by node 4.
+tracePacket4 :: BS.ByteString
+tracePacket4 = BS.concat
+ [ "146e94a9086dbbed6a0ab6932d00c118a7195dbf69b7d7a12b0e6956fc54b5e0"
+ , "a989f165b5f12fd45edd73a5b0c48630ff5be69500d3d82a29c0803f0a0679a6"
+ , "a073c33a6fb8250090a3152eba3f11a85184fa87b67f1b0354d6f48e3b342e33"
+ , "2a17b7710f342f342a87cf32eccdf0afc2160808d58abb5e5840d2c760c538e6"
+ , "3a6f841970f97d2e6fe5b8739dc45e2f7f5f532f227bcc2988ab0f9cc6d3f129"
+ , "09cd5842c37bc8c7608475a5ebbe10626d5ecc1f3388ad5f645167b44a4d166f"
+ , "87863fe34918cea25c18059b4c4d9cb414b59f6bc50c1cea749c80c43e2344f5"
+ , "d23159122ed4ab9722503b212016470d9610b46c35dbeebaf2e342e09770b383"
+ , "92a803bc9d2e7c8d6d384ffcbeb74943fe3f64afb2a543a6683c7db3088441c5"
+ , "31eeb4647518cb41992f8954f1269fb969630944928c2d2b45593731b5da0c4e"
+ , "70d04a0a57afe4af42e99912fbb4f8883a5ecb9cb29b883cb6bfa0f4db2279ff"
+ , "8c6d2b56a232f55ba28fe7dfa70a9ab0433a085388f25cce8d53de6a2fbd7546"
+ , "377d6ede9027ad173ba1f95767461a3689ef405ab608a21086165c64b02c1782"
+ , "b04a6dba2361a7784603069124e12f2f6dcb1ec7612a4fbf94c0e14631a2bef6"
+ , "190c3d5f35e0c4b32aa85201f449d830fd8f782ec758b0910428e3ec3ca1dba3"
+ , "b6c7d89f69e1ee1b9df3dfbbf6d361e1463886b38d52e8f43b73a3bd48c6f36f"
+ , "5897f514b93364a31d49d1d506340b1315883d425cb36f4ea553430d538fd6f3"
+ , "596d4afc518db2f317dd051abc0d4bfb0a7870c3db70f19fe78d6604bbf088fc"
+ , "b4613f54e67b038277fedcd9680eb97bdffc3be1ab2cbcbafd625b8a7ac34d8c"
+ , "190f98d3064ecd3b95b8895157c6a37f31ef4de094b2cb9dbf8ff1f419ba0eca"
+ , "cb1bb13df0253b826bec2ccca1e745dd3b3e7cc6277ce284d649e7b828572773"
+ , "5ff4ef6cca6c18e2714f4e2a1ac67b25213d3bb49763b3b94e7ebf72507b71fb"
+ , "2fe0329666477ee7cb7ebd6b88ad5add8b217188b1ca0fa13de1ec09cc674346"
+ , "875105be6e0e0d6c8928eb0df23c39a639e04e4aedf535c4e093f08b2c905a14"
+ , "f25c0c0fe47a5a1535ab9eae0d9d67bdd79de13a08d59ee05385c7ea4af1ad32"
+ , "48e61dd22f8990e9e99897d653dd7b1b1433a6d464ea9f74e377f2d8ce99ba7d"
+ , "bc753297644234d25ecb5bd528e2e2082824681299ac30c05354baaa9c3967d8"
+ , "6d7c07736f87fc0f63e5036d47235d7ae12178ced3ae36ee5919c093a02579e4"
+ , "fc9edad2c446c656c790704bfc8e2c491a42500aa1d75c8d4921ce29b753f883"
+ , "e17c79b09ea324f1f32ddf1f3284cd70e847b09d90f6718c42e5c94484cc9cbb"
+ , "0df659d255630a3f5a27e7d5dd14fa6b974d1719aa98f01a20fb4b7b1c77b42d"
+ , "57fab3c724339d459ee4a1c6b5d3bd4e08624c786a257872acc9ad3ff62222f2"
+ , "265a658d9f2a007229a5293b67ec91c84c4b4407c228434bad8a815ca9b256c7"
+ , "76bd2c9f"
+ ]
+
+-- | Return packet as sent by node 3.
+tracePacket3 :: BS.ByteString
+tracePacket3 = BS.concat
+ [ "7512354d6a26781d25e65539772ba049b7ed7c530bf75ab7ef80cf974b978a07"
+ , "a1c3dabc61940011585323f70fa98cfa1d4c868da30b1f751e44a72d9b3f7980"
+ , "9c8c51c9f0843daa8fe83587844fedeacb7348362003b31922cbb4d6169b2087"
+ , "b6f8d192d9cfe5363254cd1fde24641bde9e422f170c3eb146f194c48a459ae2"
+ , "889d706dc654235fa9dd20307ea54091d09970bf956c067a3bcc05af03c41e01"
+ , "af949a131533778bf6ee3b546caf2eabe9d53d0fb2e8cc952b7e0f5326a69ed2"
+ , "e58e088729a1d85971c6b2e129a5643f3ac43da031e655b27081f10543262cf9"
+ , "d72d6f64d5d96387ac0d43da3e3a03da0c309af121dcf3e99192efa754eab696"
+ , "0c256ffd4c546208e292e0ab9894e3605db098dc16b40f17c320aa4a0e42fc8b"
+ , "105c22f08c9bc6537182c24e32062c6cd6d7ec7062a0c2c2ecdae1588c82185c"
+ , "dc61d874ee916a7873ac54cddf929354f307e870011704a0e9fbc5c7802d6140"
+ , "134028aca0e78a7e2f3d9e5c7e49e20c3a56b624bfea51196ec9e88e4e56be38"
+ , "ff56031369f45f1e03be826d44a182f270c153ee0d9f8cf9f1f4132f33974e37"
+ , "c7887d5b857365c873cb218cbf20d4be3abdb2a2011b14add0a5672e01e58454"
+ , "21cf6dd6faca1f2f443757aae575c53ab797c2227ecdab03882bbbf4599318ce"
+ , "fafa72fa0c9a0f5a51d13c9d0e5d25bfcfb0154ed25895260a9df8743ac18871"
+ , "4a3f16960e6e2ff663c08bffda41743d50960ea2f28cda0bc3bd4a180e297b5b"
+ , "41c700b674cb31d99c7f2a1445e121e772984abff2bbe3f42d757ceeda3d03fb"
+ , "1ffe710aecabda21d738b1f4620e757e57b123dbc3c4aa5d9617dfa72f4a12d7"
+ , "88ca596af14bea583f502f16fdc13a5e739afb0715424af2767049f6b9aa107f"
+ , "69c5da0e85f6d8c5e46507e14616d5d0b797c3dea8b74a1b12d4e47ba7f57f09"
+ , "d515f6c7314543f78b5e85329d50c5f96ee2f55bbe0df742b4003b24ccbd4598"
+ , "a64413ee4807dc7f2a9c0b92424e4ae1b418a3cdf02ea4da5c3b12139348aa70"
+ , "22cc8272a3a1714ee3e4ae111cffd1bdfd62c503c80bdf27b2feaea0d5ab8fe0"
+ , "0f9cec66e570b00fd24b4a2ed9a5f6384f148a4d6325110a41ca5659ebc5b987"
+ , "21d298a52819b6fb150f273383f1c5754d320be428941922da790e17f482989c"
+ , "365c078f7f3ae100965e1b38c052041165295157e1a7c5b7a57671b842d4d85a"
+ , "7d971323ad1f45e17a16c4656d889fc75c12fc3d8033f598306196e29571e414"
+ , "281c5da19c12605f48347ad5b4648e371757cbe1c40adb93052af1d6110cfbf6"
+ , "11af5c8fc682b7e2ade3bfca8b5c7717d19fc9f97964ba6025aebbc91a6671e2"
+ , "59949dcf40984342118de1f6b514a7786bd4f6598ffbe1604cef476b2a4cb134"
+ , "3db608aca09d1d38fc23e98ee9c65e7f6023a8d1e61fd4f34f753454bd8e858c"
+ , "8ad6be6403edc599c220e03ca917db765980ac781e758179cd93983e9c1e769e"
+ , "4241d47c"
+ ]
+
+-- | Return packet as sent by node 2.
+tracePacket2 :: BS.ByteString
+tracePacket2 = BS.concat
+ [ "145bc1c63058f7204abbd2320d422e69fb1b3801a14312f81e5e29e6b5f4774c"
+ , "fed8a25241d3dfb7466e749c1b3261559e49090853612e07bd669dfb5f4c5416"
+ , "2fa504138dabd6ebcf0db8017840c35f12a2cfb84f89cc7c8959a6d51815b1d2"
+ , "c5136cedec2e4106bb5f2af9a21bd0a02c40b44ded6e6a90a145850614fb1b0e"
+ , "ef2a03389f3f2693bc8a755630fc81fff1d87a147052863a71ad5aebe8770537"
+ , "f333e07d841761ec448257f948540d8f26b1d5b66f86e073746106dfdbb86ac9"
+ , "475acf59d95ece037fba360670d924dce53aaa74262711e62a8fc9eb70cd8618"
+ , "fbedae22853d3053c7f10b1a6f75369d7f73c419baa7dbf9f1fc5895362dcc8b"
+ , "6bd60cca4943ef7143956c91992119bccbe1666a20b7de8a2ff30a46112b53a6"
+ , "bb79b763903ecbd1f1f74952fb1d8eb0950c504df31fe702679c23b463f82a92"
+ , "1a2c931500ab08e686cffb2d87258d254fb17843959cccd265a57ba26c740f0f"
+ , "231bb76df932b50c12c10be90174b37d454a3f8b284c849e86578a6182c4a7b2"
+ , "e47dd57d44730a1be9fec4ad07287a397e28dce4fda57e9cdfdb2eb5afdf0d38"
+ , "ef19d982341d18d07a556bb16c1416f480a396f278373b8fd9897023a4ac506e"
+ , "65cf4c306377730f9c8ca63cf47565240b59c4861e52f1dab84d938e96fb3182"
+ , "0064d534aca05fd3d2600834fe4caea98f2a748eb8f200af77bd9fbf46141952"
+ , "b9ddda66ef0ebea17ea1e7bb5bce65b6e71554c56dd0d4e14f4cf74c77a15077"
+ , "6bf31e7419756c71e7421dc22efe9cf01de9e19fc8808d5b525431b944400db1"
+ , "21a77994518d6025711cb25a18774068bba7faaa16d8f65c91bec87688483331"
+ , "56dcb4a08dfbbd9fef392da3e4de13d4d74e83a7d6e46cfe530ee7a6f711e2ca"
+ , "f8ad5461ba8177b2ef0a518baf9058ff9156e6aa7b08d938bd8d1485a787809d"
+ , "7b4c8aed97be880708470cd2b2cdf8e2f13428cc4b04ef1f2acbc9562f3693b9"
+ , "48d0aa94b0e6113cafa684f8e4a67dc431dfb835726874bef1de36f273f52ee6"
+ , "94ec46b0700f77f8538067642a552968e866a72a3f2031ad116663ac17b172b4"
+ , "46c5bc705b84777363a9a3fdc6443c07b2f4ef58858122168d4ebbaee920cefc"
+ , "312e1cea870ed6e15eec046ab2073bbf08b0a3366f55cfc6ad4681a12ab09465"
+ , "34e7b6f90ea8992d530ec3daa6b523b3cf03101c60cadd914f30dec932c1ef43"
+ , "41b5a8efac3c921e203574cfe0f1f83433fddb8ccfd273f7c3cab7bc27efe3bb"
+ , "61fdccd5146f1185364b9b621e7fb2b74b51f5ee6be72ab6ff46a6359dc2c855"
+ , "e61469724c1dbeb273df9d2e1c1fb74891239c0019dc12d5c7535f7238f963b7"
+ , "61d7102b585372cf021b64c4fc85bfb3161e59d2e298bba44cfd34d6859d9dba"
+ , "9dc6271e5047d525468c814f2ae438474b0a977273036da1a2292f88fcfb8957"
+ , "4a6bdca1185b40f8aa54026d5926725f99ef028da1be892e3586361efe15f4a1"
+ , "48ff1bc9"
+ ]
+
+-- | Return packet as sent by node 1.
+tracePacket1 :: BS.ByteString
+tracePacket1 = BS.concat
+ [ "1b4b09a935ce7af95b336baae307f2b400e3a7e808d9b4cf421cc4b3955620ac"
+ , "b69dcdb656128dae8857adbd4e6b37fbb1be9c1f2f02e61e9e59a630c4c77cf3"
+ , "83cb37b07413aa4de2f2fbf5b40ae40a91a8f4c6d74aeacef1bb1be4ecbc26ec"
+ , "2c824d2bc45db4b9098e732a769788f1cff3f5b41b0d25c132d40dc5ad045ef0"
+ , "043b15332ca3c5a09de2cdb17455a0f82a8f20da08346282823dab062cdbd211"
+ , "1e238528141d69de13de6d83994fbc711e3e269df63a12d3a4177c5c149150eb"
+ , "4dc2f589cd8acabcddba14dec3b0dada12d663b36176cd3c257c5460bab93981"
+ , "ad99f58660efa9b31d7e63b39915329695b3fa60e0a3bdb93e7e29a54ca6a8f3"
+ , "60d3848866198f9c3da3ba958e7730847fe1e6478ce8597848d3412b4ae48b06"
+ , "e05ba9a104e648f6eaf183226b5f63ed2e68f77f7e38711b393766a6fab7921b"
+ , "03eba82b5d7cb78e34dc961948d6161eadd7cf5d95d9c56df2ff5faa6ccf85ea"
+ , "cdc9ff2fc3abafe41c365a5bd14fd486d6b5e2f24199319e7813e02e798877ff"
+ , "e31a70ae2398d9e31b9e3727e6c1a3c0d995c67d37bb6e72e9660aaaa9232670"
+ , "f382add2edd468927e3303b6142672546997fe105583e7c5a3c4c2b599731308"
+ , "b5416e6c9a3f3ba55b181ad0439d3535356108b059f2cb8742eed7a58d4eba9f"
+ , "e79eaa77c34b12aff1abdaea93197aabd0e74cb271269ca464b3b06aef1d6573"
+ , "df5e1224179616036b368677f26479376681b772d3760e871d99efd34cca5cd6"
+ , "beca95190d967da820b21e5bec60082ea46d776b0517488c84f26d12873912d1"
+ , "f68fafd67bcf4c298e43cfa754959780682a2db0f75f95f0598c0d04fd014c50"
+ , "e4beb86a9e37d95f2bba7e5065ae052dc306555bca203d104c44a538b438c976"
+ , "2de299e1c4ad30d5b4a6460a76484661fc907682af202cd69b9a4473813b2fdc"
+ , "1142f1403a49b7e69a650b7cde9ff133997dcc6d43f049ecac5fce097a21e2bc"
+ , "e49c810346426585e3a5a18569b4cddd5ff6bdec66d0b69fcbc5ab3b137b34cc"
+ , "8aefb8b850a764df0e685c81c326611d901c392a519866e132bbb73234f6a358"
+ , "ba284fbafb21aa3605cacbaf9d0c901390a98b7a7dac9d4f0b405f7291c88b2f"
+ , "f45874241c90ac6c5fc895a440453c344d3a365cb929f9c91b9e39cb98b14244"
+ , "4aae03a6ae8284c77eb04b0a163813d4c21883df3c0f398f47bf127b5525f222"
+ , "107a2d8fe55289f0cfd3f4bbad6c5387b0594ef8a966afc9e804ccaf75fe39f3"
+ , "5c6446f7ee076d433f2f8a44dba1515acc78e589fa8c71b0a006fe14feebd51d"
+ , "0e0aa4e51110d16759eee86192eee90b34432130f387e0ccd2ee71023f1f641c"
+ , "ddb571c690107e08f592039fe36d81336a421e89378f351e633932a2f5f697d2"
+ , "5b620ffb8e84bb6478e9bd229bf3b164b48d754ae97bd23f319e3c56b3bcdaae"
+ , "b3bd7fc02ec02066b324cb72a09b6b43dec1097f49d69d3c138ce6f1a6402898"
+ , "baf7568c"
+ ]
+
+-- | Return packet as sent by node 0.
+tracePacket0 :: BS.ByteString
+tracePacket0 = BS.concat
+ [ "2dd2f49c1f5af0fcad371d96e8cddbdcd5096dc309c1d4e110f955926506b3c0"
+ , "3b44c192896f45610741c85ed4074212537e0c118d472ff3a559ae244acd9d78"
+ , "3c65977765c5d4e00b723d00f12475aafaafff7b31c1be5a589e6e25f8da2959"
+ , "107206dd42bbcb43438129ce6cce2b6b4ae63edc76b876136ca5ea6cd1c6a04c"
+ , "a86eca143d15e53ccdc9e23953e49dc2f87bb11e5238cd6536e57387225b8fff"
+ , "3bf5f3e686fd08458ffe0211b87d64770db9353500af9b122828a006da754cf9"
+ , "79738b4374e146ea79dd93656170b89c98c5f2299d6e9c0410c826c721950c78"
+ , "0486cd6d5b7130380d7eaff994a8503a8fef3270ce94889fe996da66ed121741"
+ , "987010f785494415ca991b2e8b39ef2df6bde98efd2aec7d251b2772485194c8"
+ , "368451ad49c2354f9d30d95367bde316fec6cbdddc7dc0d25e99d3075e13d3de"
+ , "0822669861dafcd29de74eac48b64411987285491f98d78584d0c2a163b7221e"
+ , "a796f9e8671b2bb91e38ef5e18aaf32c6c02f2fb690358872a1ed28166172631"
+ , "a82c2568d23238017188ebbd48944a147f6cdb3690d5f88e51371cb70adf1fa0"
+ , "2afe4ed8b581afc8bcc5104922843a55d52acde09bc9d2b71a663e178788280f"
+ , "3c3eae127d21b0b95777976b3eb17be40a702c244d0e5f833ff49dae6403ff44"
+ , "b131e66df8b88e33ab0a58e379f2c34bf5113c66b9ea8241fc7aa2b1fa53cf4e"
+ , "d3cdd91d407730c66fb039ef3a36d4050dde37d34e80bcfe02a48a6b14ae2822"
+ , "7b1627b5ad07608a7763a531f2ffc96dff850e8c583461831b19feffc783bc1b"
+ , "eab6301f647e9617d14c92c4b1d63f5147ccda56a35df8ca4806b8884c4aa3c3"
+ , "cc6a174fdc2232404822569c01aba686c1df5eecc059ba97e9688c8b16b70f0d"
+ , "24eacfdba15db1c71f72af1b2af85bd168f0b0800483f115eeccd9b02adf03bd"
+ , "d4a88eab03e43ce342877af2b61f9d3d85497cd1c6b96674f3d4f07f635bb26a"
+ , "dd1e36835e321d70263b1c04234e222124dad30ffb9f2a138e3ef453442df1af"
+ , "7e566890aedee568093aa922dd62db188aa8361c55503f8e2c2e6ba93de744b5"
+ , "5c15260f15ec8e69bb01048ca1fa7bbbd26975bde80930a5b95054688a0ea73a"
+ , "f0353cc84b997626a987cc06a517e18f91e02908829d4f4efc011b9867bd9bfe"
+ , "04c5f94e4b9261d30cc39982eb7b250f12aee2a4cce0484ff34eebba89bc6e35"
+ , "bd48d3968e4ca2d77527212017e202141900152f2fd8af0ac3aa456aae13276a"
+ , "13b9b9492a9a636e18244654b3245f07b20eb76b8e1cea8c55e5427f08a63a16"
+ , "b0a633af67c8e48ef8e53519041c9138176eb14b8782c6c2ee76146b8490b979"
+ , "78ee73cd0104e12f483be5a4af414404618e9f6633c55dda6f22252cb793d3d1"
+ , "6fae4f0e1431434e7acc8fa2c009d4f6e345ade172313d558a4e61b4377e31b8"
+ , "ed4e28f7cd13a7fe3f72a409bc3bdabfe0ba47a6d861e21f64d2fac706dab18b"
+ , "3e546df4"
+ ]
+
+-- route-blinding-test.json ----------------------------------------------
+
+-- | Bob's node id.
+rbBobNodeId :: BS.ByteString
+rbBobNodeId =
+ "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"
+
+-- | Bob's encoded encrypted_data_tlv.
+rbBobTlvs :: BS.ByteString
+rbBobTlvs = BS.concat
+ [ "011a000000000000000000000000000000000000000000000000000002080000"
+ , "0000000006c10a0800240000009627100c06000b69e505dc0e00fd0231031234"
+ , "56"
+ ]
+
+-- | Path private key at Bob (the session key for the first hop of a path).
+rbBobPathPrivKey :: BS.ByteString
+rbBobPathPrivKey =
+ "0202020202020202020202020202020202020202020202020202020202020202"
+
+-- | Path key at Bob.
+rbBobPathKey :: BS.ByteString
+rbBobPathKey =
+ "024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766"
+
+-- | Shared secret at Bob.
+rbBobSharedSecret :: BS.ByteString
+rbBobSharedSecret =
+ "76771bab0cc3d0de6e6f60147fd7c9c7249a5ced3d0612bdfaeec3b15452229d"
+
+-- | rho key at Bob.
+rbBobRho :: BS.ByteString
+rbBobRho = "ba217b23c0978d84c4a19be8a9ff64bc1b40ed0d7ecf59521567a5b3a9a1dd48"
+
+-- | Bob's encrypted_data.
+rbBobEncryptedData :: BS.ByteString
+rbBobEncryptedData = BS.concat
+ [ "cd4100ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c49982088b4"
+ , "9f2e70b99287fdee0aa58aa39913ab405813b999f66783aa2fe637b3cda91ffc"
+ , "0913c30324e2c6ce327e045183e4bffecb"
+ ]
+
+-- | Bob's blinded node id.
+rbBobBlindedNodeId :: BS.ByteString
+rbBobBlindedNodeId =
+ "03da173ad2aee2f701f17e59fbd16cb708906d69838a5f088e8123fb36e89a2c25"
+
+-- | Carol's node id.
+rbCarolNodeId :: BS.ByteString
+rbCarolNodeId =
+ "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"
+
+-- | Carol's encoded encrypted_data_tlv.
+rbCarolTlvs :: BS.ByteString
+rbCarolTlvs = BS.concat
+ [ "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e18"
+ , "34604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e"
+ , "00"
+ ]
+
+-- | Path private key at Carol (the session key for the first hop of a path).
+rbCarolPathPrivKey :: BS.ByteString
+rbCarolPathPrivKey =
+ "0a2aa791ac81265c139237b2b84564f6000b1d4d0e68d4b9cc97c5536c9b61c1"
+
+-- | Path key at Carol.
+rbCarolPathKey :: BS.ByteString
+rbCarolPathKey =
+ "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0"
+
+-- | Shared secret at Carol.
+rbCarolSharedSecret :: BS.ByteString
+rbCarolSharedSecret =
+ "dc91516ec6b530a3d641c01f29b36ed4dc29a74e063258278c0eeed50313d9b8"
+
+-- | rho key at Carol.
+rbCarolRho :: BS.ByteString
+rbCarolRho = "d1e62bae1a8e169da08e6204997b60b1a7971e0f246814c648125c35660f5416"
+
+-- | Carol's encrypted_data.
+rbCarolEncryptedData :: BS.ByteString
+rbCarolEncryptedData = BS.concat
+ [ "cc0f16524fd7f8bb0b1d8d40ad71709ef140174c76faa574cac401bb8992fef7"
+ , "6c4d004aa485dd599ed1cf2715f57ff62da5aaec5d7b10d59b04d8a9d77e472b"
+ , "9b3ecc2179334e411be22fa4c02b467c7e"
+ ]
+
+-- | Carol's blinded node id.
+rbCarolBlindedNodeId :: BS.ByteString
+rbCarolBlindedNodeId =
+ "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7"
+
+-- | Dave's node id.
+rbDaveNodeId :: BS.ByteString
+rbDaveNodeId =
+ "032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"
+
+-- | Dave's encoded encrypted_data_tlv.
+rbDaveTlvs :: BS.ByteString
+rbDaveTlvs = BS.concat
+ [ "0123000000000000000000000000000000000000000000000000000000000000"
+ , "0000000000020800000000000002310a060090000000fa0c06000b699105dc0e"
+ , "00"
+ ]
+
+-- | Path private key at Dave (the session key for the first hop of a path).
+rbDavePathPrivKey :: BS.ByteString
+rbDavePathPrivKey =
+ "0101010101010101010101010101010101010101010101010101010101010101"
+
+-- | Path key at Dave.
+rbDavePathKey :: BS.ByteString
+rbDavePathKey =
+ "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"
+
+-- | Shared secret at Dave.
+rbDaveSharedSecret :: BS.ByteString
+rbDaveSharedSecret =
+ "dc46f3d1d99a536300f17bc0512376cc24b9502c5d30144674bfaa4b923d9057"
+
+-- | rho key at Dave.
+rbDaveRho :: BS.ByteString
+rbDaveRho = "393aa55d35c9e207a8f28180b81628a31dff558c84959cdc73130f8c321d6a06"
+
+-- | Dave's encrypted_data.
+rbDaveEncryptedData :: BS.ByteString
+rbDaveEncryptedData = BS.concat
+ [ "0fa0a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f679ee8"
+ , "db3e561d9c43815fd4bcebf6f58c546da0cd8a9bf5cebd0d554802f6c0255e28"
+ , "e4a27343f761fe518cd897463187991105"
+ ]
+
+-- | Dave's blinded node id.
+rbDaveBlindedNodeId :: BS.ByteString
+rbDaveBlindedNodeId =
+ "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf"
+
+-- | Eve's node id.
+rbEveNodeId :: BS.ByteString
+rbEveNodeId =
+ "02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145"
+
+-- | Eve's encoded encrypted_data_tlv.
+rbEveTlvs :: BS.ByteString
+rbEveTlvs = BS.concat
+ [ "011a00000000000000000000000000000000000000000000000000000604dead"
+ , "beef0c06000b690105dc0e0f020000000000000000000000000000fdffff0206"
+ , "c1"
+ ]
+
+-- | Path private key at Eve (the session key for the first hop of a path).
+rbEvePathPrivKey :: BS.ByteString
+rbEvePathPrivKey =
+ "62e8bcd6b5f7affe29bec4f0515aab2eebd1ce848f4746a9638aa14e3024fb1b"
+
+-- | Path key at Eve.
+rbEvePathKey :: BS.ByteString
+rbEvePathKey =
+ "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a"
+
+-- | Shared secret at Eve.
+rbEveSharedSecret :: BS.ByteString
+rbEveSharedSecret =
+ "352a706b194c2b6d0a04ba1f617383fb816dc5f8f9ac0b60dd19c9ae3b517289"
+
+-- | rho key at Eve.
+rbEveRho :: BS.ByteString
+rbEveRho = "719d0307340b1c68b79865111f0de6e97b093a30bc603cebd1beb9eef116f2d8"
+
+-- | Eve's encrypted_data.
+rbEveEncryptedData :: BS.ByteString
+rbEveEncryptedData = BS.concat
+ [ "da1a7e5f7881219884beae6ae68971de73bab4c3055d9865b1afb60724a2e4d3"
+ , "f0489ad884f7f3f77149209f0df51efd6b276294a02e3949c7254fbc8b5cab58"
+ , "212d9a78983e1cf86fe218b30c4ca8f6d8"
+ ]
+
+-- | Eve's blinded node id.
+rbEveBlindedNodeId :: BS.ByteString
+rbEveBlindedNodeId =
+ "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae"
+
+-- | Bob's node private key.
+rbBobNodeKey :: BS.ByteString
+rbBobNodeKey =
+ "4242424242424242424242424242424242424242424242424242424242424242"
+
+-- | Bob's blinded private key.
+rbBobBlindedPrivKey :: BS.ByteString
+rbBobBlindedPrivKey =
+ "d12fec0332c3e9d224789a17ebd93595f37d37bd8ef8bd3d2e6ce50acb9e554f"
+
+-- | Next path key after Bob (override if present).
+rbBobNextPathKey :: BS.ByteString
+rbBobNextPathKey =
+ "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0"
+
+-- | Carol's node private key.
+rbCarolNodeKey :: BS.ByteString
+rbCarolNodeKey =
+ "4343434343434343434343434343434343434343434343434343434343434343"
+
+-- | Carol's blinded private key.
+rbCarolBlindedPrivKey :: BS.ByteString
+rbCarolBlindedPrivKey =
+ "bfa697fbbc8bbc43ca076e6dd60d306038a32af216b9dc6fc4e59e5ae28823c1"
+
+-- | Next path key after Carol (override if present).
+rbCarolNextPathKey :: BS.ByteString
+rbCarolNextPathKey =
+ "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"
+
+-- | Dave's node private key.
+rbDaveNodeKey :: BS.ByteString
+rbDaveNodeKey =
+ "4444444444444444444444444444444444444444444444444444444444444444"
+
+-- | Dave's blinded private key.
+rbDaveBlindedPrivKey :: BS.ByteString
+rbDaveBlindedPrivKey =
+ "cebc115c7fce4c295dc396dea6c79115b289b8ceeceea2ed61cf31428d88fc4e"
+
+-- | Next path key after Dave (override if present).
+rbDaveNextPathKey :: BS.ByteString
+rbDaveNextPathKey =
+ "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a"
+
+-- | Eve's node private key.
+rbEveNodeKey :: BS.ByteString
+rbEveNodeKey =
+ "4545454545454545454545454545454545454545454545454545454545454545"
+
+-- | Eve's blinded private key.
+rbEveBlindedPrivKey :: BS.ByteString
+rbEveBlindedPrivKey =
+ "ff4e07da8d92838bedd019ce532eb990ed73b574e54a67862a1df81b40c0d2af"
+
+-- | Next path key after Eve (override if present).
+rbEveNextPathKey :: BS.ByteString
+rbEveNextPathKey =
+ "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589"
+
+-- blinded-payment-onion-test.json ---------------------------------------
+
+-- | Session key.
+bpSessionKey :: BS.ByteString
+bpSessionKey =
+ "0303030303030303030303030303030303030303030303030303030303030303"
+
+-- | Associated data.
+bpAssocData :: BS.ByteString
+bpAssocData =
+ "4242424242424242424242424242424242424242424242424242424242424242"
+
+-- | Public key the onion is encrypted to at Alice.
+bpAlicePubKey :: BS.ByteString
+bpAlicePubKey =
+ "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"
+
+-- | Alice's payload, with its length prefix.
+bpAlicePayload :: BS.ByteString
+bpAlicePayload = "14020301ae2d04030b6e5e0608000000000000000a"
+
+-- | Public key the onion is encrypted to at Bob.
+bpBobPubKey :: BS.ByteString
+bpBobPubKey =
+ "0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"
+
+-- | Bob's payload, with its length prefix.
+bpBobPayload :: BS.ByteString
+bpBobPayload = BS.concat
+ [ "740a4fcd7b00ff9c09ed28102b210ac73aa12d63e90852cebc496c49f57c499a"
+ , "2888b49f2e72b19446f7e60a818aa2938d8c625415b992b8928a7321edb8f7ce"
+ , "a40de362bed082ad51acc6156dca5532fb680c21024d4b6cd1361032ca9bd2ae"
+ , "b9d900aa4d45d9ead80ac9423374c451a7254d0766"
+ ]
+
+-- | Public key the onion is encrypted to at Carol.
+bpCarolPubKey :: BS.ByteString
+bpCarolPubKey =
+ "02e466727716f044290abf91a14a6d90e87487da160c2a3cbd0d465d7a78eb83a7"
+
+-- | Carol's payload, with its length prefix.
+bpCarolPayload :: BS.ByteString
+bpCarolPayload = BS.concat
+ [ "510a4fcc0f16524fd7f8bb0f4e8d40ad71709ef140174c76faa574cac401bb89"
+ , "92fef76c4d004aa485dd599ed1cf2715f570f656a5aaecaf1ee8dc9d0fa1d424"
+ , "759be1932a8f29fac08bc2d2a1ed7159f28b"
+ ]
+
+-- | Public key the onion is encrypted to at Dave.
+bpDavePubKey :: BS.ByteString
+bpDavePubKey =
+ "036861b366f284f0a11738ffbf7eda46241a8977592878fe3175ae1d1e4754eccf"
+
+-- | Dave's payload, with its length prefix.
+bpDavePayload :: BS.ByteString
+bpDavePayload = BS.concat
+ [ "510a4f0fa1a72cff3b64a3d6e1e4903cf8c8b0a17144aeb249dcb86561adee1f"
+ , "679ee8db3e561d9e49895fd4bcebf6f58d6f61a6d41a9bf5aa4b045343785663"
+ , "2e8255c351873143ddf2bb2b0832b091e1b4"
+ ]
+
+-- | Public key the onion is encrypted to at Eve.
+bpEvePubKey :: BS.ByteString
+bpEvePubKey =
+ "021982a48086cb8984427d3727fe35a03d396b234f0701f5249daa12e8105c8dae"
+
+-- | Eve's payload, with its length prefix.
+bpEvePayload :: BS.ByteString
+bpEvePayload = BS.concat
+ [ "6002030186a004030b6dc80a4fda1c7e5f7881219884beae6ae68971de73bab4"
+ , "c3055d9865b1afb60722a63c688768042ade22f2c22f5724767d171fd221d3e5"
+ , "79e43b354cc72e3ef146ada91a892d95fc48662f5b158add0af457da12030249"
+ , "f0"
+ ]
+
+-- | Onion received by Alice.
+bpAliceOnion :: BS.ByteString
+bpAliceOnion = BS.concat
+ [ "0002531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1f"
+ , "e337dadf610256c6ab518495dce9cdedf9391e21a71dada75be905267ba82f32"
+ , "6c0513dda706908cfee834996700f881b2aed106585d61a2690de4ebe5d56ad2"
+ , "013b520af2a3c49316bc590ee83e8c31b1eb11ff766dad27ca993326b1ed582f"
+ , "b451a2ad87fbf6601134c6341c4a2deb6850e25a355be68dbb6923dc89444fdd"
+ , "74a0f700433b667bda345926099f5547b07e97ad903e8a01566a78ae17736623"
+ , "9e793dac719de805565b6d0a1d290e273f705cfc56873f8b5e28225f7ded7a1d"
+ , "4ceffae63f91e477be8c917c786435976102a924ba4ba3de6150c829ce01c254"
+ , "28f2f5d05ef023be7d590ecdf6603730db3948f80ca1ed3d85227e64ef77200b"
+ , "9b557f427b6e1073cfa0e63e4485441768b98ab11ba8104a6cee1d7af7bb5ee9"
+ , "c05cf9cf4718901e92e09dfe5cb3af336a953072391c1e91fc2f4b92e124b38e"
+ , "0c6d17ef6ba7bbe93f02046975bb01b7f766fcfc5a755af11a90cc7eb3505986"
+ , "b56e07a7855534d03b79f0dfbfe645b0d6d4185c038771fd25b800aa26b2ed2e"
+ , "30b1e713659468618a2fea04fcd0473284598f76b11b0d159d343bc9711d3bea"
+ , "8d561547bcc8fff12317c0e7b1ee75bcb8082d762b6417f99d0f71ff7c060f6b"
+ , "564ad6827edaffa72eefcc4ce633a8da8d41c19d8f6aebd8878869eb518ccc16"
+ , "dccae6a94c690957598ce0295c1c46af5d7a2f0955b5400526bfd1430f554562"
+ , "614b5d00feff3946427be520dee629b76b6a9c2b1da6701c8ca628a69d6d40e2"
+ , "0dd69d6e879d7a052d9c16f544b49738c7ff3cdd0613e9ed00ead7707702d1a6"
+ , "a0b88de1927a50c36beb78f4ff81e3dd97b706307596eebb363d418a891e1cb4"
+ , "589ce86ce81cdc0e1473d7a7dd5f6bb6e147c1f7c46fa879b4512c25704da6cd"
+ , "bb3c123a72e3585dc07b3e5cbe7fecf3a08426eee8c70ddc46ebf98b0bcb14a0"
+ , "8c469cb5cfb6702acc0befd17640fa60244eca491280a95fbbc5833d26e4be70"
+ , "fcf798b55e06eb9fcb156942dcf108236f32a5a6c605687ba4f037eddbb1834d"
+ , "cbcd5293a0b66c621346ca5d893d239c26619b24c71f25cecc275e1ab24436ac"
+ , "01c80c0006fab2d95e82e3a0c3ea02d08ec5b24eb39205c49f4b549dcab7a889"
+ , "62336c4624716902f4e08f2b23cfd324f18405d66e9da3627ac34a6873ba2238"
+ , "386313af20d5a13bbd507fdc73015a17e3bd38fae1145f7f70d7cb8c5e1cdf9c"
+ , "f06d1246592a25d56ec2ae44cd7f75aa7f5f4a2b2ee49a41a26be4fab3f3f2ce"
+ , "b7b08510c5e2b7255326e4c417325b333cafe96dde1314a15dd6779a7d5a8a40"
+ , "622260041e936247eec8ec39ca29a1e18161db37497bdd4447a7d5ef3b8d22a2"
+ , "acd7f486b152bb66d3a15afc41dc9245a8d75e1d33704d4471e417ccc8d31645"
+ , "fdd647a2c191692675cf97664951d6ce98237d78b0962ad1433b5a3e49ddddbf"
+ , "57a391b14dcce00b4d7efe5cbb1e78f30d5ef53d66c381a45e275d2dcf6be559"
+ , "acb3c42494a9a2156eb8dcf03dd92b2ebaa697ea628fa0f75f125e4a7daa10f8"
+ , "dcf56ebaf7814557708c75580fad2bbb33e66ad7a4788a7aaac792aaae76138d"
+ , "7ff09df6a1a1920ddcf22e5e7007b15171b51ff81799355232ce39f7d5ceeaf7"
+ , "04255d790041d6390a69f42816cba641ec81faa3d7c0fdec59dfe4ca41f31a69"
+ , "2eaffc66b083995d86c575aea4514a3e09e8b3a1fa4d1591a2505f253ad0b6bf"
+ , "d9d87f063d2be414d3a427c0506a88ac5bdbef9b50d73bce876f85c196dca435"
+ , "e210e1d6713695b529ddda3350fb5065a6a8288abd265380917bac8ebbc7d5ce"
+ , "d564587471dddf90c22ce6dbadea7e7a6723438d4cf6ac6dae27d033a8cadd77"
+ , "ab262e8defb33445ddb2056ec364c7629c33745e2338"
+ ]
+
+-- | Onion received by Bob.
+bpBobOnion :: BS.ByteString
+bpBobOnion = BS.concat
+ [ "000280caa47c2a0ea677f6a77529e46caa04212153a8d5f829bee1e7339b17e2"
+ , "e2a9a3461d10472364a4ff12344beb6df96fb0c38ec47d1e956ddff5a665190f"
+ , "cca5ed02c3a3903fd8bbd4a4b95b197867c378b67b08f0624cfe80734ba51286"
+ , "9c0fa22099beb1f6f1ea325b07ce7449736d7ffad79178b428d8ea2d7bc6578f"
+ , "12dbd788ef933f3b5ba352797c41f6786c3820c96726acf8bddf2cfa5d9c617d"
+ , "2b0bd5ab7b93f7964c98f44cf47db8422f47d11100236a29579f1cafcd38bd97"
+ , "9814e1d2bf6d625edf50e1e21bfaf6268e3180dd7aafd3892da281c6dd53c1c3"
+ , "66d0fdaf670b6ad84a38d6e8a3f4a80d132d686fd3b7443bc2250023bdb93031"
+ , "90f74c9220481cf99da30b5ec2bdb5a49028f5014e3eaeaa48429a0c78ebd3bb"
+ , "7c7d582c22b7d547cd269f0c4490373a81bf92687e73dac2075b4bda189ce0be"
+ , "225f5f510655e37a6e724a1415bede0a076b92a882cc2a82878ba67aaedf7145"
+ , "4eb42b7f8638df8e21d5f708006e5112e2dc0a4afbcfed9f2c7959be812853ca"
+ , "8e313fbc99a0f38f1ee4479c96ccb836632b0808401db159bd2637f7a6640132"
+ , "41e4664e994a0a9a3940115a702c60381e66d291e1ade1be2802e1226e311e32"
+ , "01a7c9682b6bc4354caff3d439adb1dfee53ad3fb3dd5e169d64796853bb3231"
+ , "29f41213b166a7cac00f728c3e33bd7e59aa2ac0d1341cdb1532b507a0f446e5"
+ , "1022a882ac16405442347b70f78c9b6e122f8e70096a4fae4c0405db5b869e0b"
+ , "7b59b09519c4dbf4d4980483906e837da0bee93f668ffaad37d6a4764211a02f"
+ , "95ad2dc2d942c198796741c20a3baf8efb5a53bd9c1a0148318d60a97d0013ab"
+ , "63269097ea295d62c1426d064f0b31c02e74a348ee0509998e701069f5a1e0c1"
+ , "086aed38d2ec87da69fb57a992d88ace3b4a16b0960f5a94936e2e684a9926cf"
+ , "4f911969a2a5d31fed0c7616d30197848253170e51274278873b11f3f5cc1b04"
+ , "b14aa5812524e4d86cbf08306c2aa671288324d7a009b2be533b1d7d0ce6defe"
+ , "eb630b86a9655f1e6424fcb559ed67457c115fba0d0719374802ea68fab299fd"
+ , "3f273be86fa3d2e7456020db2f47c6ec16c21ce6ec65de495e20af1941a5dcd6"
+ , "5d910c1cb93f22e1318c173c645c81aed681c9704a8a541ac3d6ff604f46d026"
+ , "0468acbfec1b771b9eb8cd49a2124468dae786571895a569aae18438eaee6343"
+ , "ab2634823119fa2439634645d12e3b4a748b9cc0398b8416a834eb5d9e5cf619"
+ , "bbfaba4894d1c574c738caf530d0862f4cc75eb52bd3921d2d9edb09940edb1e"
+ , "3776423b0046d870ccdcc5d61f72e0440b97a93eeef21fb246a779d339be301a"
+ , "5971400749d6cc9911dfbf9de8ae86fac83c860fdd0e2bfa40af37c99d50e50f"
+ , "d6e5ae86597a201112ed404042b55e132f243dec481a2adc1d5e4b71e1efdea8"
+ , "06ea900b2907ce877742d5ecf700ff3640f737863d0dd7207e462ee8d0e17d52"
+ , "047a88ae7446f419560d23968bf64957949e36953155b0ac2511c66be2890b40"
+ , "36329a21e132efb635297a64431899e0c351e50c6682c9b4d79b5d122466d02c"
+ , "d84f206369417d9c194a9349d3c631d72eb7857a9cd542906fc02ad6cdcf9bcf"
+ , "25ace3d826b6623fa5164351e14d3f0de5c8445a2ba3aae26595d0e31c3e307c"
+ , "1d56d4274f61f056145c1b8d6880872b9b10a8bfa4a923cad2edbcf5c50eba48"
+ , "936ed2bcc0be60eb721a74b46704aaae5ad24e2797852195dfacbb30a777d33b"
+ , "63d4dc4f35cfbe5e88fd1944c55a54fd53581446ea061ad29f4671da819ad748"
+ , "8c5dfc700f5f7a1b2af0d6a6e9d9ffc570a6d3209614ab4dc43728f3f0cd7eb4"
+ , "ce36ccd98936bbcbd32627384434bd01e9c0f93b2a5173fba184685e19b9af78"
+ , "afe876aa4e4b4242382b293133771d95a2bd83fa9c62"
+ ]
+
+-- | Next path key after Bob.
+bpBobNextPathKey :: BS.ByteString
+bpBobNextPathKey =
+ "034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0"
+
+-- | Onion received by Carol.
+bpCarolOnion :: BS.ByteString
+bpCarolOnion = BS.concat
+ [ "000288b48876fb0dc0d7375233ccaf2910dc0dc81ba52e5a7906f00d75e0d58d"
+ , "bd4bb7c2714870529410735f0951e72cbe981e2e167c0d8f3de33a36e39e7846"
+ , "5aea2acad1e23c78b6fd342d63e37d214c912b4a0be344618f779138edc1b42a"
+ , "5ca3218ca2fea4be427f6cd0d387160db2bf6c2ba8e82941c8cf3626bd6bed71"
+ , "87f633012ef49df38f6b12963cb639e9eed1b9d269dcebcbd0b25287aa536ec8"
+ , "5e7320b02e193122199a745ccbaaebd37f5d4b71f52f9b50feeb793eeef56924"
+ , "a046bc5e7003f6253e0284a8d3fe2e42c3564050f1e753cd32cc258ac0ffa6e0"
+ , "5eecad5ba1286f78252e60dd884a65405ab673a85ba52adfa65c1086d4bb37ba"
+ , "2e0848adb2b04379775ad798492b14e8997f30ffa9cf5d432bdf5b246fce008f"
+ , "d876399beed827db58195f4f6192f6ff4ec63cb17fdcb497cb7aec26846a71dd"
+ , "8dca02fc3bb14dd7231a4d62a981bec54b71eb20331096dfa214a0ff4489ee96"
+ , "db663826ae8c850e9f06baa52a47b8eb576363f97e742aab2dc616acc6e74588"
+ , "e1d2ac16694febc90abaf5b1c684163c0e615a68d32633f01934adc8c6bf91fa"
+ , "3fd7aad033b7596d60402494e45e2c1632c40f7bfbd88a81a896a1d28ed6338c"
+ , "83e1eeaa467945d59998eb456c95f94bf1892e8f326ec2d5e0196b7073f106fe"
+ , "bc6ab8ca5bcc23f77ffc819bc1b5debce418ccc7d8391bbf33bceee6110beba1"
+ , "70121bd99f54c956e64970bdab31227b03ee0ea3f01fbd9bd74015f6f82d04fa"
+ , "b072e8f85f4370d09f41ee3e48eb959767bd989abb4eea42c4daa0437a7f747d"
+ , "7f9b70eb87b9f9b0b6f283b8205912601a432999b8869fd9fe5bad3572edac24"
+ , "da7184f9298f21ff60923db277264d29c846dd2f228f6fc53b6b60364237de64"
+ , "773f803f174ed10229c374f603ccc5fd3a62cb413ffe6f5630dc646bb33f231b"
+ , "2350537ec39e5d3f2fe1a1cb019ed0b18ad14019cad27afcca8ad70387ca1103"
+ , "94c0432774f1aa1fa404b2e086c84a55388d3bd102501c78ef925cce89d76fa0"
+ , "4c3f20f2d1f0ce507ac8b37b7913e3949ba12bbc5a4f6bac37c2415622d365bc"
+ , "8b83709a28e3d46f3850c89a3ff4d027fef6e3e4ce5c6c85f663c7eaec3c9730"
+ , "106fb82f53249a905533cfabee812aae51965b24b42f7ab471967bc8e73354e6"
+ , "9141ee26a1f03684d5fb9c256a34de8257210e0390dd3962db521ae0a3bdab28"
+ , "300610ab2a634b699e5f092da5a061609ef6414bd805c8171f54ad6f285fb64c"
+ , "e0becca0b61188badcf8ef21190dad629e3fb3e89f55ebba829919540ebf5f8a"
+ , "e4283836d3c9133c1ca3365f6b9394916730411650686e0c2ab9c53b6cda9efd"
+ , "d5cfcb53ba9b6962bb6aa49d0a83a87460b60a9c7d2643ee99afe652883795f1"
+ , "4014ec5df61b1e30c041c1fa6487f3c82f1ded5f83ffbef5017e197b7fb77be3"
+ , "b36e284a15e57d45bf9316dcaf97eb78ee4642b731ba05c5063bce1333fab4af"
+ , "6da97c80a96ee599b4df823efbedc250c0abba9783da7ddf2414b2a4774ff288"
+ , "0a7dc6791103e18b8631e39743cf9e87aed71700daa5dc72fdae520324741f92"
+ , "ea3d510ff555dea5e45f15cda87272d4559a12d4777680acb06993840e3c748d"
+ , "a82c16cae556015fb2acd0335da11a3388575394048ab71199793ab706abc9d6"
+ , "8add2075d79a5cc0f779845ee8b98951be61fd293d6c15b9d4653935bf17cf50"
+ , "bd31f8b79e60dba0e7fd6864754fd94262485a4f65e7eb3e1922f51b1a4dd2b4"
+ , "fd2c20d94d1213fbe90bd603dfc7e15176382e3ce0f43f980d44d23bf3c57f54"
+ , "a15f42c171a8f2511e28ac178c6f01396e50397a57ffb09c5e6c315bd3ae7983"
+ , "577c1a0386c6d5d9a2223438e321b0fedfdee58fa452d57dc11a256834bb49ac"
+ , "9deeec88e4bf563c7340f44a240caec941c7e50f09cf"
+ ]
+
+-- | Next path key after Carol.
+bpCarolNextPathKey :: BS.ByteString
+bpCarolNextPathKey =
+ "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"
+
+-- | Onion received by Dave.
+bpDaveOnion :: BS.ByteString
+bpDaveOnion = BS.concat
+ [ "0003f25471c0f2ff549a7fd7859100306bb6c294c209f34c77f782897f184b96"
+ , "7c498efc246bdb8e060a6d1cf8dd0d4d732e33311fb96c9e9f1274005fa3d08b"
+ , "41704a1b7224c6300a7caead7baa0a8263eba2e0de6956ee8e4a1958264f47e4"
+ , "cf20d194eb576f5bd249ee4fece563f80fd76dc3eaca8f956188406d83195752"
+ , "b5c90c4b2a5e7ac3a8d5c62b17b551aff48ef6842a7e9326832c9a4a2fd41501"
+ , "1150a9e71beb901fd9747bac8add1c694b612730dc86b5b19a0bbbc675947a95"
+ , "3316e3303d7b30c182f94def9206671edac9a3ec3e52d28fc28247a1c73ab751"
+ , "bf61c82c3950f617e758f79bd0ba294defb20466eaf1e801462046baad3aec3e"
+ , "5b8868a7b037f23d73a47a7e74c77107334f37388cff863e452820c61d89728f"
+ , "a75c84bc7cdfc06dcdd1911f5f803353926d073efd65251380e174913aae0331"
+ , "8ea5b6f0ec83998c55ab99bef62803ea2da9f6d1ea892b90efc4f8ffb685a520"
+ , "1a781da2e6ac5923645638c9709ae32171a00c0cd3d8c7eedfb06b4eedc7d3e5"
+ , "66987e2e3805a038f21d78ded5d6c7137a5e8e592f3180ee4d5f4e1289176f67"
+ , "fc38690d0958bc82e240b72b10577f340f1e14b8633f0b6d9729ff4618be2a97"
+ , "2400a015a871ba33be70335f652a8d70f2bd32421d6ac2af781d667dad787d6a"
+ , "ef4505a15d046579e46eebe757444cffca6d0610f0dd36a7ce57af969bd0c3f7"
+ , "006298ef406a25f689daf58f875d44d2423ebf195b503f11c37c506ea6abe50a"
+ , "463f7bb5e9b964604d832724de768513f6b38bf71715e1feea8a6e86797788d4"
+ , "87146891564919da1372016ed8f08c7fcbff66a4a65a3d0fcd8e3daac6eba41f"
+ , "5d65ef2d8075364a9e78b3a273549f6eac4abb72e912e237990367e0d43e8994"
+ , "5f8ac3907be5a6c662139485a50cb5ce3f0ba08586c39f6c515368ec3f91b722"
+ , "95f1b7a73a9df322ae9a45d363d6c616be3300083764cbdee31221f25a318f09"
+ , "5feacb09f957c96db30fccca47a0215b576c3ed925a0bad05d6400abe318c11f"
+ , "36628c387a4ee38832182cd44b3cd48e5422c1f1e3b57218dfe72c611f541512"
+ , "7720e60f6e2400607e61841b76de1704bcbeb0daf1377ccb2253916de2b6d490"
+ , "bb71ba0a44fea2e94f2423d723934557d5905e01b2b80232a884e258d46dc92e"
+ , "a11e0818d0ece5b914f02049866e151801ab8c9aea155479b354dc91151fb9ba"
+ , "43277458f9760dd859faaa139e3b9ab36a1dbc36a93ef2c90598b20cb30ef3c4"
+ , "f23a2d6178b4d1da668fb328a25d84d30a132d9f2a6a988cbe2e5c2be01cb6db"
+ , "4b4725a50d6cdacf5fb083e7d650a25bec1407fbc047d26076c7596429a29606"
+ , "ad527e97ef0824ad6c1b05831a3e5b71c63a528918a3301cdd4061fc1fcce3da"
+ , "601961f2602a2b002ac8404125c2d52666263858a923e197efcda873c32d8689"
+ , "7352e4f2264ad6a1b48acc0fe78ff55cb442cb2bb5fa2880810e1d00aa024705"
+ , "7fb80b7ed36cf9647af41b44ee4a63ee2d6f652526404572520a7d2d9dcde4e6"
+ , "2df0c3be89f8471550594cdd16a51a9cacc58729c092c68506162fe65edc2314"
+ , "055d389f724ced189d826a546b5c4d08a43d977b3cf033de5760b71a7cc38ee5"
+ , "851592031aafb467a89b3b6c7ed67b15d44c48d6baedce3e95e08ec7c55038f3"
+ , "eba90ccb900895734f0fb7efe54961ce493369cc56416898a9bed7c2482871c1"
+ , "5a7f1eb5ed17c33657fc31333539c2dfb59461af09e7049228113b5c9feea5a6"
+ , "e9959c18c51b19c90995afb9c76f2c0c820964cd7989c993a73925818a656c6a"
+ , "18dcd1a1e3782b2eae06dd5a41250ec2d1c203626ab9920c1673339eff04b1eb"
+ , "0cab85ef5909f571f9b83cdf21697c9f5cfa1c76e7bca955510e2126b3bb989a"
+ , "4ac21cf948f965e48bc363d2997437797b4f770e8b65"
+ ]
+
+-- | Next path key after Dave.
+bpDaveNextPathKey :: BS.ByteString
+bpDaveNextPathKey =
+ "03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a"
+
+-- | Onion received by Eve.
+bpEveOnion :: BS.ByteString
+bpEveOnion = BS.concat
+ [ "0002ef43c4dfe9aee14248c445406ac7980474ce106c504d9025f57963739130"
+ , "adfd06eb26201baee8866af2d1b7a7ab26595349dad002af0590193aaa8f400a"
+ , "b394f5994ec831aeeecb64421c566e3556cbdd7e7e50deb1fc49fd5e007308ab"
+ , "6494415514abff978899623f9b6065ca1e243bb78170118e8b8c8b53b750b59c"
+ , "c1ec017d167adbb3aabab7c2d84fbf94f5d827239f4c2b9d2c3cfe68fe5641f2"
+ , "5e386202a4b6edff2a71e700229df7230c8ca31bd5588f04799e9640c9c20a47"
+ , "cba713f3cc5ad3202e14bb520880f2a8409d8e7835cae21b48a651c2d47fe6af"
+ , "785889ab98f1416f6e4ad67a66ae681e9a8828bad3f9b6890221c4a7ec80531d"
+ , "6b63eb30843f613ce644795bc8bcee60e8f7b36f3fd04de762f103c52efaf36a"
+ , "2f3bbbaac482d6271dc4180c10bcc076c04d06ea7fd8fb6a647e0e10523b05da"
+ , "2d89e4139fb55c2315cd01bdcbd57587fef8442d7ff5620630fd2d2e79739d90"
+ , "be811bf2cba60415d6cba2cea14ba1859f3122cd905c4e12e3e2a1ab6fab54b2"
+ , "ec40e434626e2d3c3195c02c82a8bd64d226c2328ac72ca12197d9908eaf5433"
+ , "3717448ce6ed73adc0ac05e2ee1d735131d87918beb8995993dc8f63fe10f2c8"
+ , "eba2be7ab8bb44d9f78f59ef3e4c180bd75e4eef2381450c6f0480d543997305"
+ , "f1d07815993b5aca8d88d474966d9abec93bb069a16aa2da75b87f94576e01d0"
+ , "8a17d3e0e3d0370f010733a7d7affb12cdf94c259a62607fce71003535c47273"
+ , "05de5ff7bba3840922844b3a45f62c29715fccf440517ef121450f6962396fba"
+ , "9b07036d085582405dcae6ee95964b66bc7c85b8d02d90091500db3cebf6de58"
+ , "4f86b7b55335a8c9aa26381b00747f055cc458a2cadfccf9c29702bf941447be"
+ , "aca6583cca09492a57d4b03b2ca00dbaf41dfd6a9b249381626a7debe475735a"
+ , "7e39e77a363eccf14669046f656cc09ad448da8d8b545e6a604f46dc481786d0"
+ , "9a94c63cf23f49ba367d2929466364dbce2a8ffce3dadf8f4cef8a56e1fefa1a"
+ , "3304a953fe83018e57d8a95694b02d994fea2630a9a3d5f1e2f6d6142d503ec4"
+ , "152871f7122d7e566a03261f554639e7a759e0e73846f71d5cace37d91336fc9"
+ , "ca9396bf64ca2cf45fa2db779b3b5c63b04f1c0c1fb79fdfcf5a82b0202df934"
+ , "ae1720a7ce1e047cbec3f82737b50168c974f4623cacce87e3f5bd5232caca79"
+ , "56d28ffedcf11ac5998662c5f6b13c6126584ca2e894d3fcbad4d130bbe22e88"
+ , "a135e0020cdd43853e0b3af3800e9544854d211e873cf68ab683578d501d69ec"
+ , "5dc7fce42ac436d58243880c1b88227b0681c6c9dd8a8ad0793202b15ab63b78"
+ , "7b748e258da3e68d0e649fc4ac081a71de8adbc891c113d5f722686b6ac4ed9e"
+ , "3cc247bc4a4643416f480627e9de20f7307f434a499f5c6951c2e8b3ff51d455"
+ , "bf65ceb5ee3dee47b968ac2642e13d8a68f903b73627c2e75788fecca5836371"
+ , "a908eea4f1ea44db2315bc185f77e478efeaaa4da2da13fe7aeaa79ed1d04876"
+ , "a8b2b7b333c5de8c4c9a50274c2eb7b9bd2a3630c57173174781fc9785235f83"
+ , "0cefa1c82080eaffdef257f18eedc9ddfd25a696a11a3dc56cd836be72f5f4a2"
+ , "cbb6316d5d3b1ad91a7ec7d877f28d2c29a5525b0b24362699281b0e3b48f38c"
+ , "af1085045fe9089f9e6fb29e4b47aa4cecf68c9bf72073469bd9beeea5e88bfe"
+ , "554cb6a81231149ba7fe7784c154fd8b0f9179ecdf1e9fd5c2939ec1ab16df9c"
+ , "be9359101ebce933d4f65d3f66f87afaecfe9c046b52f4878b6c430329df7bd8"
+ , "79fba8864fcbd9b782bf545734699b9b5a66b466dcedc0c9368803b5b0f12329"
+ , "50cef398ad3e057a5db964bd3e5c8a5717b30b41601a4f11ad63afe404cb6f1e"
+ , "8ea5fd7a8e085b65ca5136146febf4d47928dcc9a9e0"
+ ]
+
+-- | Next path key after Eve.
+bpEveNextPathKey :: BS.ByteString
+bpEveNextPathKey =
+ "038fc6859a402b96ce4998c537c823d6ab94d1598fca02c788ba5dd79fbae83589"