bolt1

Base Lightning protocol, per BOLT #1.
git clone git://git.ppad.tech/bolt1.git
Log | Files | Refs | README | LICENSE

commit 41855291dc1943189ade89d7d519c002f32cd721
parent 56d27f6673b4a125218f55ff1ee0d2d74889183b
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 10:29:31 +0400

fix: reject unknown even TLV types in extensions

Per BOLT #1, unknown even TLV types must cause failure. The previous
implementation used decodeTlvStreamRaw for extensions, which accepts
all types without enforcing the even/odd rule.

Now decodeEnvelope uses decodeTlvStreamWith (const False), which:
- Rejects unknown even types (fails per spec)
- Skips unknown odd types (ignores per spec)

Also updates decodeTlvStreamRaw docstring to clarify it does NOT
enforce the BOLT #1 unknown-even-type rule.

Tests updated to verify:
- Unknown even types in extensions cause DecodeInvalidExtension error
- Unknown odd types in extensions are skipped (not returned)
- Mixed even/odd extensions fail on the even type

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mlib/Lightning/Protocol/BOLT1.hs | 8+++++---
Mtest/Main.hs | 50++++++++++++++++++++++++++++++--------------------
2 files changed, 35 insertions(+), 23 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT1.hs b/lib/Lightning/Protocol/BOLT1.hs @@ -250,8 +250,9 @@ instance NFData TlvError -- - Types must be strictly increasing -- - Lengths must not exceed bounds -- --- All records are returned regardless of type. Use this for parsing --- extension TLVs or when you want to handle type validation separately. +-- All records are returned regardless of type. Note: this does NOT +-- enforce the BOLT #1 unknown-even-type rule. Use 'decodeTlvStreamWith' +-- with an appropriate predicate for spec-compliant parsing. decodeTlvStreamRaw :: BS.ByteString -> Either TlvError TlvStream decodeTlvStreamRaw = go Nothing [] where @@ -748,9 +749,10 @@ decodeEnvelope !bs = do _ -> do (msg, rest2) <- decodeMessage msgType rest1 -- Parse any remaining bytes as extension TLV + -- Per BOLT #1: unknown even types must fail, unknown odd are ignored ext <- if BS.null rest2 then Right Nothing - else case decodeTlvStreamRaw rest2 of + else case decodeTlvStreamWith (const False) rest2 of Left e -> Left (DecodeInvalidExtension e) Right s -> Right (Just s) Right (Just msg, ext) diff --git a/test/Main.hs b/test/Main.hs @@ -326,46 +326,55 @@ envelope_tests = testGroup "Envelope" [ extension_tests :: TestTree extension_tests = testGroup "Extension TLV" [ - testCase "encode envelope with extension" $ do + testCase "encode envelope with extension (odd type)" $ do let msg = MsgPingVal (Ping 10 "") - ext = TlvStream [TlvRecord 100 "extension data"] + ext = TlvStream [TlvRecord 101 "extension data"] -- odd type case encodeEnvelope msg (Just ext) of Left e -> assertFailure $ "encode failed: " ++ show e Right encoded -> do -- Should contain message + extension assertBool "encoded should be longer" (BS.length encoded > 6) - , testCase "decode envelope with extension roundtrip" $ do + , testCase "decode envelope with odd extension - skipped per BOLT#1" $ do + -- Per BOLT #1: unknown odd types are ignored (skipped) let msg = MsgPingVal (Ping 10 "") - ext = TlvStream [TlvRecord 101 "ext"] + ext = TlvStream [TlvRecord 101 "ext"] -- odd type case encodeEnvelope msg (Just ext) of Left e -> assertFailure $ "encode failed: " ++ show e Right encoded -> case decodeEnvelope encoded of - Right (Just decoded, Just decodedExt) -> do + Right (Just decoded, Just (TlvStream [])) -> do + -- Extension is empty because unknown odd types are skipped decoded @?= msg - length (unTlvStream decodedExt) @?= 1 other -> assertFailure $ "unexpected: " ++ show other - , testCase "decode envelope extension is parsed" $ do - -- Manually construct ping + extension TLV + , testCase "decode envelope with unknown even extension fails" $ do + -- Per BOLT #1: unknown even types must cause failure let pingPayload = mconcat [encodeU16 10, encodeU16 0] -- numPong=10, len=0 - extTlv = mconcat [encodeBigSize 200, encodeBigSize 3, "abc"] + extTlv = mconcat [encodeBigSize 100, encodeBigSize 3, "abc"] -- even! envelope = encodeU16 18 <> pingPayload <> extTlv -- type 18 = ping case decodeEnvelope envelope of - Right (Just (MsgPingVal ping), Just (TlvStream [r])) -> do - pingNumPongBytes ping @?= 10 - tlvType r @?= 200 - tlvValue r @?= "abc" - other -> assertFailure $ "unexpected: " ++ show other + Left (DecodeInvalidExtension (TlvUnknownEvenType 100)) -> pure () + other -> assertFailure $ "expected unknown even error: " ++ show other , testCase "decode envelope with invalid extension fails" $ do -- Ping + invalid TLV (non-strictly-increasing) let pingPayload = mconcat [encodeU16 10, encodeU16 0] badTlv = mconcat [ - encodeBigSize 100, encodeBigSize 1, "a" - , encodeBigSize 50, encodeBigSize 1, "b" -- 50 < 100, invalid + encodeBigSize 101, encodeBigSize 1, "a" -- odd types for this test + , encodeBigSize 51, encodeBigSize 1, "b" -- 51 < 101, invalid ] envelope = encodeU16 18 <> pingPayload <> badTlv case decodeEnvelope envelope of Left (DecodeInvalidExtension TlvNotStrictlyIncreasing) -> pure () other -> assertFailure $ "expected invalid extension: " ++ show other + , testCase "unknown even in extension fails even with odd types present" $ do + -- Mixed odd and even - should fail on the even type + let pingPayload = mconcat [encodeU16 10, encodeU16 0] + extTlv = mconcat [ + encodeBigSize 101, encodeBigSize 1, "a" -- odd, would be skipped + , encodeBigSize 200, encodeBigSize 1, "b" -- even, must fail + ] + envelope = encodeU16 18 <> pingPayload <> extTlv + case decodeEnvelope envelope of + Left (DecodeInvalidExtension (TlvUnknownEvenType 200)) -> pure () + other -> assertFailure $ "expected unknown even error: " ++ show other ] -- Bounds checking tests ------------------------------------------------------- @@ -450,15 +459,16 @@ property_tests = testGroup "Properties" [ Right (MsgErrorVal decoded, rest) -> decoded == msg && BS.null rest _ -> False - , testProperty "Envelope with extension roundtrip" $ \bs -> + , testProperty "Envelope with odd extension (skipped per BOLT#1)" $ \bs -> + -- Unknown odd types in extensions are skipped per BOLT #1 let msg = MsgPingVal (Ping 42 "") extData = BS.pack (take 100 bs) - ext = TlvStream [TlvRecord 101 extData] + ext = TlvStream [TlvRecord 101 extData] -- odd type, will be skipped in case encodeEnvelope msg (Just ext) of Left _ -> False Right encoded -> case decodeEnvelope encoded of - Right (Just decoded, Just (TlvStream [r])) -> - decoded == msg && tlvType r == 101 && tlvValue r == extData + -- Extension should be empty (odd types skipped) + Right (Just decoded, Just (TlvStream [])) -> decoded == msg _ -> False ]