bolt4

Onion routing protocol, per BOLT #4 (docs.ppad.tech/bolt4).
git clone git://git.ppad.tech/bolt4.git
Log | Files | Refs | README | LICENSE

commit 419819555d052d16174db8e74cf9dafef4819ae0
parent 17e6d5d6664c778f215a9e4e9906c3512f66c0de
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:29:47 +0400

test: add onion-error-test.json and error trace vectors

From onion-error-test.json: check the ammag and um keys, that the
failing node builds the spec return packet, that wrapping at each hop
yields the spec packet and that the origin attributes it to hop 4.

From the 'Returning Errors' trace in 04-onion-routing.md: check the
failure message encoding (with its TLV extension), every intermediate
packet, and attribution at the origin.

Diffstat:
Mtest/Main.hs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/Vectors.hs | 2+-
2 files changed, 92 insertions(+), 1 deletion(-)

diff --git a/test/Main.hs b/test/Main.hs @@ -94,6 +94,8 @@ main = defaultMain $ testGroup "ppad-bolt4" [ ] , testGroup "spec vectors" [ onionVectorTests + , errorVectorTests + , traceVectorTests ] , testGroup "multi-hop" [ multiHopTests @@ -1760,3 +1762,92 @@ genRoutePayload n = do 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 + let ErrorPacket raw = wrapError ss4 (constructError ss4 fm) + 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 [] + final = foldl (flip wrapError) (constructError ss4 fm) 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 ] + let packets = drop 1 $ + scanl (flip wrapError) (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 + ] diff --git a/test/Vectors.hs b/test/Vectors.hs @@ -250,7 +250,7 @@ errPacket = BS.concat -- BOLT4 'Returning Errors' trace ------------------------------------------ --- | Encoded failure message. +-- | Encoded failure message, followed by pad_len and pad. traceFailureMessage :: BS.ByteString traceFailureMessage = BS.concat [ "400f0000000000000064000c3500fd84d1fd012c808080808080808080808080"