bolt1

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

commit f2dfffdce6dd1d0fb3135cf967ee23a466f26fac
parent 507af14c14be9fbaa27992357aa25e40c443daee
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:02:14 +0400

Merge fix/tlv-length: TLV length wrap, point validation

- decodeTlvStreamRaw and decodeTlvStreamWith compared a Word64 TLV
  length against the remaining input after converting it to Int, so
  lengths >= 2^63 wrapped negative and were accepted with an empty
  value. bolt2 and bolt7 inherit the fix through decodeTlvStreamRaw.
- point now rejects inputs without a 0x02/0x03 prefix, per the BOLT #1
  Appendix B invalid-point vector.
- Regression tests for both.

Diffstat:
Mlib/Lightning/Protocol/BOLT1/Prim.hs | 10+++++++---
Mlib/Lightning/Protocol/BOLT1/TLV.hs | 8++++----
Mtest/Main.hs | 24++++++++++++++++++++++++
3 files changed, 35 insertions(+), 7 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT1/Prim.hs b/lib/Lightning/Protocol/BOLT1/Prim.hs @@ -194,11 +194,15 @@ instance NFData Point -- | Construct a 'Point' from a 33-byte 'BS.ByteString'. -- --- Returns 'Nothing' if the input is not exactly 33 bytes. +-- Returns 'Nothing' if the input is not exactly 33 bytes, or does not +-- begin with a compressed-encoding prefix (0x02 or 0x03). The curve +-- equation is not checked. point :: BS.ByteString -> Maybe Point point !bs - | BS.length bs == 33 = Just (Point bs) - | otherwise = Nothing + | BS.length bs == 33 + , let !h = BS.index bs 0 + , h == 0x02 || h == 0x03 = Just (Point bs) + | otherwise = Nothing {-# INLINE point #-} -- | Extract the raw bytes from a 'Point'. diff --git a/lib/Lightning/Protocol/BOLT1/TLV.hs b/lib/Lightning/Protocol/BOLT1/TLV.hs @@ -136,8 +136,8 @@ decodeTlvStreamRaw = go Nothing [] Nothing -> pure () (len, rest2) <- maybe (Left TlvNonMinimalEncoding) Right (decodeBigSize rest1) - -- Length bounds check - when (fromIntegral len > BS.length rest2) $ + -- Length bounds check (in Word64, so huge lengths can't wrap) + when (len > fromIntegral (BS.length rest2)) $ Left TlvLengthExceedsBounds let !val = BS.take (fromIntegral len) rest2 !rest3 = BS.drop (fromIntegral len) rest2 @@ -172,8 +172,8 @@ decodeTlvStreamWith isKnown = go Nothing [] Nothing -> pure () (len, rest2) <- maybe (Left TlvNonMinimalEncoding) Right (decodeBigSize rest1) - -- Length bounds check - when (fromIntegral len > BS.length rest2) $ + -- Length bounds check (in Word64, so huge lengths can't wrap) + when (len > fromIntegral (BS.length rest2)) $ Left TlvLengthExceedsBounds let !val = BS.take (fromIntegral len) rest2 !rest3 = BS.drop (fromIntegral len) rest2 diff --git a/test/Main.hs b/test/Main.hs @@ -96,6 +96,18 @@ primitive_tests = testGroup "Primitives" [ decodeU32 (BS.pack [0x01, 0x02]) @?= Nothing , testCase "decodeU64 insufficient" $ decodeU64 (BS.pack [0x01, 0x02, 0x03, 0x04]) @?= Nothing + , testCase "point accepts 0x02 prefix" $ + fmap unPoint (point (BS.cons 0x02 (BS.replicate 32 0x01))) + @?= Just (BS.cons 0x02 (BS.replicate 32 0x01)) + , testCase "point accepts 0x03 prefix" $ + fmap unPoint (point (BS.cons 0x03 (BS.replicate 32 0x01))) + @?= Just (BS.cons 0x03 (BS.replicate 32 0x01)) + , testCase "point rejects 0x04 prefix (Appendix B)" $ + point (unhex ("043da092f6980e58d2c037173180e9a465476026ee50f9" + <> "6695963e8efe436f54eb")) + @?= Nothing + , testCase "point rejects wrong length" $ + point (BS.cons 0x02 (BS.replicate 31 0x01)) @?= Nothing ] -- Signed integer tests --------------------------------------------------------- @@ -281,6 +293,18 @@ tlv_tests = testGroup "TLV" [ ] , testCase "empty stream" $ decodeTlvStream "" @?= Right (unsafeTlvStream []) + , testCase "length 2^64-1 rejected (raw)" $ + decodeTlvStreamRaw (unhex "01ffffffffffffffffff") + @?= Left TlvLengthExceedsBounds + , testCase "length 2^63 rejected (raw)" $ + decodeTlvStreamRaw (unhex "01ff800000000000000067") + @?= Left TlvLengthExceedsBounds + , testCase "length 2^64-1 rejected (with predicate)" $ + decodeTlvStreamWith (const True) (unhex "01ffffffffffffffffff") + @?= Left TlvLengthExceedsBounds + , testCase "huge length not reparsed as later records" $ + decodeTlvStreamRaw (unhex "01ffffffffffffffffff6701aa") + @?= Left TlvLengthExceedsBounds , testCase "single record type 1" $ do let bs = mconcat [ encodeBigSize 1 -- type