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 7e9196bd1f726947eea0aebd2179454fffa62304
parent dd4a5625a853a465d07feba5e6e5fee47c7b0776
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:43:48 +0400

lib: reject hop payloads under 2 bytes in construct

An all-Nothing HopPayload encodes to zero bytes. construct accepted
it and produced an onion that every receiver must reject (the spec
reserves payload lengths 0 and 1). Fail with the new
InvalidHopPayload error, carrying the hop index, instead.

Construct tests that used empty payloads now use a minimal one.

Diffstat:
Mlib/Lightning/Protocol/BOLT4/Construct.hs | 6+++++-
Mtest/Main.hs | 30+++++++++++++++++++-----------
2 files changed, 24 insertions(+), 12 deletions(-)

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,10 +75,13 @@ 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 diff --git a/test/Main.hs b/test/Main.hs @@ -512,8 +512,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 @@ -527,13 +539,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 @@ -557,10 +567,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 ->