bolt7

Routing gossip protocol, per BOLT #7 (docs.ppad.tech/bolt7).
git clone git://git.ppad.tech/bolt7.git
Log | Files | Refs | README | LICENSE

commit 13606569e5aebb2c22f5c9c2b72be39442c343fc
parent dc0b9dd7a627c2b103b4ade084cd4d0492b33fa1
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:19:21 +0400

lib: reject u16 length overflow in encoders

encodeLenPrefixed truncated the length with fromIntegral, so
encodeChannelAnnouncement (features) and encodeNodeAnnouncement
(addresses) emitted a wrapped prefix and a malformed message for
fields over 65535 bytes.

encodeLenPrefixed now returns Either EncodeError and fails with
EncodeLengthOverflow. Every encoder with a u16 length field uses it,
replacing the ad-hoc checks. encodeChannelAnnouncement now returns
Either EncodeError ByteString.

Test and bench call sites are updated for the new type.

Diffstat:
Mbench/Main.hs | 5++++-
Mbench/Weight.hs | 5++++-
Mlib/Lightning/Protocol/BOLT7/Codec.hs | 116++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mtest/Main.hs | 26+++++++++++++++-----------
4 files changed, 88 insertions(+), 64 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -143,7 +143,10 @@ testChannelAnnouncement = ChannelAnnouncement -- | Encoded ChannelAnnouncement for decode benchmarks. encodedChannelAnnouncement :: BS.ByteString -encodedChannelAnnouncement = encodeChannelAnnouncement testChannelAnnouncement +encodedChannelAnnouncement = + case encodeChannelAnnouncement testChannelAnnouncement of + Right bs -> bs + Left _ -> error "encodedChannelAnnouncement: encode failed" {-# NOINLINE encodedChannelAnnouncement #-} -- | Test NodeAnnouncement message. diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -222,7 +222,10 @@ testChannelAnnouncement = mkChannelAnnouncement -- | Encoded ChannelAnnouncement for decode benchmarks. encodedChannelAnnouncement :: BS.ByteString -encodedChannelAnnouncement = encodeChannelAnnouncement testChannelAnnouncement +encodedChannelAnnouncement = + case encodeChannelAnnouncement testChannelAnnouncement of + Right bs -> bs + Left _ -> error "encodedChannelAnnouncement: encode failed" {-# NOINLINE encodedChannelAnnouncement #-} -- | Test NodeAnnouncement message. diff --git a/lib/Lightning/Protocol/BOLT7/Codec.hs b/lib/Lightning/Protocol/BOLT7/Codec.hs @@ -134,9 +134,14 @@ decodeLenPrefixed bs = do else Right (BS.splitAt n rest) {-# INLINE decodeLenPrefixed #-} --- | Encode with u16 length prefix. -encodeLenPrefixed :: ByteString -> ByteString -encodeLenPrefixed bs = Prim.encodeU16 (fromIntegral $ BS.length bs) <> bs +-- | Encode with u16 length prefix, failing if the length does not fit +-- in a u16. +encodeLenPrefixed :: ByteString -> Either EncodeError ByteString +encodeLenPrefixed bs + | len > 65535 = Left EncodeLengthOverflow + | otherwise = Right (Prim.encodeU16 (fromIntegral len) <> bs) + where + !len = BS.length bs {-# INLINE encodeLenPrefixed #-} -- | Decode fixed-length validated type. @@ -263,20 +268,26 @@ decodeAddresses bs = do -- Channel announcement -------------------------------------------------------- -- | Encode channel_announcement message. -encodeChannelAnnouncement :: ChannelAnnouncement -> ByteString -encodeChannelAnnouncement msg = mconcat - [ unSignature (channelAnnNodeSig1 msg) - , unSignature (channelAnnNodeSig2 msg) - , unSignature (channelAnnBitcoinSig1 msg) - , unSignature (channelAnnBitcoinSig2 msg) - , encodeLenPrefixed (getFeatureBits (channelAnnFeatures msg)) - , unChainHash (channelAnnChainHash msg) - , scidToBytes (channelAnnShortChanId msg) - , getNodeId (channelAnnNodeId1 msg) - , getNodeId (channelAnnNodeId2 msg) - , unPoint (channelAnnBitcoinKey1 msg) - , unPoint (channelAnnBitcoinKey2 msg) - ] +-- +-- Fails with 'EncodeLengthOverflow' if the feature bits exceed 65535 +-- bytes. +encodeChannelAnnouncement :: ChannelAnnouncement + -> Either EncodeError ByteString +encodeChannelAnnouncement msg = do + features <- encodeLenPrefixed (getFeatureBits (channelAnnFeatures msg)) + Right $ mconcat + [ unSignature (channelAnnNodeSig1 msg) + , unSignature (channelAnnNodeSig2 msg) + , unSignature (channelAnnBitcoinSig1 msg) + , unSignature (channelAnnBitcoinSig2 msg) + , features + , unChainHash (channelAnnChainHash msg) + , scidToBytes (channelAnnShortChanId msg) + , getNodeId (channelAnnNodeId1 msg) + , getNodeId (channelAnnNodeId2 msg) + , unPoint (channelAnnBitcoinKey1 msg) + , unPoint (channelAnnBitcoinKey2 msg) + ] -- | Decode channel_announcement message. decodeChannelAnnouncement :: ByteString @@ -311,21 +322,22 @@ decodeChannelAnnouncement bs = do -- Node announcement ----------------------------------------------------------- -- | Encode node_announcement message. +-- +-- Fails with 'EncodeLengthOverflow' if the feature bits or the +-- encoded addresses exceed 65535 bytes. encodeNodeAnnouncement :: NodeAnnouncement -> Either EncodeError ByteString encodeNodeAnnouncement msg = do - addrData <- encodeAddresses (nodeAnnAddresses msg) - let features = getFeatureBits (nodeAnnFeatures msg) - if BS.length features > 65535 - then Left EncodeLengthOverflow - else Right $ mconcat - [ unSignature (nodeAnnSignature msg) - , encodeLenPrefixed features - , Prim.encodeU32 (nodeAnnTimestamp msg) - , getNodeId (nodeAnnNodeId msg) - , getRgbColor (nodeAnnRgbColor msg) - , getAlias (nodeAnnAlias msg) - , encodeLenPrefixed addrData - ] + features <- encodeLenPrefixed (getFeatureBits (nodeAnnFeatures msg)) + addrs <- encodeLenPrefixed =<< encodeAddresses (nodeAnnAddresses msg) + Right $ mconcat + [ unSignature (nodeAnnSignature msg) + , features + , Prim.encodeU32 (nodeAnnTimestamp msg) + , getNodeId (nodeAnnNodeId msg) + , getRgbColor (nodeAnnRgbColor msg) + , getAlias (nodeAnnAlias msg) + , addrs + ] -- | Encode address list. encodeAddresses :: [Address] -> Either EncodeError ByteString @@ -471,17 +483,18 @@ decodeAnnouncementSignatures bs = do -- Query messages -------------------------------------------------------------- -- | Encode query_short_channel_ids message. +-- +-- Fails with 'EncodeLengthOverflow' if the encoded short_channel_ids +-- exceed 65535 bytes. encodeQueryShortChannelIds :: QueryShortChannelIds -> Either EncodeError ByteString encodeQueryShortChannelIds msg = do - let scidData = queryScidsData msg - if BS.length scidData > 65535 - then Left EncodeLengthOverflow - else Right $ mconcat - [ unChainHash (queryScidsChainHash msg) - , encodeLenPrefixed scidData - , TLV.encodeTlvStream (queryScidsTlvs msg) - ] + scidData <- encodeLenPrefixed (queryScidsData msg) + Right $ mconcat + [ unChainHash (queryScidsChainHash msg) + , scidData + , TLV.encodeTlvStream (queryScidsTlvs msg) + ] -- | Decode query_short_channel_ids message. -- @@ -553,22 +566,23 @@ decodeQueryChannelRange bs = do Right (msg, BS.empty) -- | Encode reply_channel_range message. +-- +-- Fails with 'EncodeLengthOverflow' if the encoded short_channel_ids +-- exceed 65535 bytes. encodeReplyChannelRange :: ReplyChannelRange -> Either EncodeError ByteString encodeReplyChannelRange msg = do - let rangeData = replyRangeData msg - if BS.length rangeData > 65535 - then Left EncodeLengthOverflow - else Right $ mconcat - [ unChainHash (replyRangeChainHash msg) - , Prim.encodeU32 - (getBlockHeight (replyRangeFirstBlock msg)) - , Prim.encodeU32 - (getBlockCount (replyRangeNumBlocks msg)) - , BS.singleton (replyRangeSyncComplete msg) - , encodeLenPrefixed rangeData - , TLV.encodeTlvStream (replyRangeTlvs msg) - ] + rangeData <- encodeLenPrefixed (replyRangeData msg) + Right $ mconcat + [ unChainHash (replyRangeChainHash msg) + , Prim.encodeU32 + (getBlockHeight (replyRangeFirstBlock msg)) + , Prim.encodeU32 + (getBlockCount (replyRangeNumBlocks msg)) + , BS.singleton (replyRangeSyncComplete msg) + , rangeData + , TLV.encodeTlvStream (replyRangeTlvs msg) + ] -- | Decode reply_channel_range message. -- diff --git a/test/Main.hs b/test/Main.hs @@ -193,10 +193,11 @@ channel_announcement_tests = testGroup "ChannelAnnouncement" [ , channelAnnBitcoinKey1 = testPoint , channelAnnBitcoinKey2 = testPoint } - encoded = encodeChannelAnnouncement msg - case decodeChannelAnnouncement encoded of - Right (decoded, _) -> decoded @?= msg - Left e -> assertFailure $ "decode failed: " ++ show e + case encodeChannelAnnouncement msg of + Left e -> assertFailure $ "encode failed: " ++ show e + Right encoded -> case decodeChannelAnnouncement encoded of + Right (decoded, _) -> decoded @?= msg + Left e -> assertFailure $ "decode failed: " ++ show e ] -- Node Announcement Tests ----------------------------------------------------- @@ -569,7 +570,7 @@ hash_tests = testGroup "Hash Functions" [ , testGroup "Signature Hashes" [ testCase "channelAnnouncementHash produces 32 bytes" $ do -- Create a minimal valid encoded message - let msg = encodeChannelAnnouncement ChannelAnnouncement + case encodeChannelAnnouncement ChannelAnnouncement { channelAnnNodeSig1 = testSignature , channelAnnNodeSig2 = testSignature , channelAnnBitcoinSig1 = testSignature @@ -581,9 +582,11 @@ hash_tests = testGroup "Hash Functions" [ , channelAnnNodeId2 = testNodeId2 , channelAnnBitcoinKey1 = testPoint , channelAnnBitcoinKey2 = testPoint - } - hashVal = channelAnnouncementHash msg - BS.length hashVal @?= 32 + } of + Left e -> assertFailure $ "encode failed: " ++ show e + Right msg -> do + let hashVal = channelAnnouncementHash msg + BS.length hashVal @?= 32 , testCase "nodeAnnouncementHash produces 32 bytes" $ do case encodeNodeAnnouncement NodeAnnouncement { nodeAnnSignature = testSignature @@ -833,10 +836,11 @@ propChannelAnnouncementRoundtrip = property $ do , channelAnnBitcoinKey1 = testPoint , channelAnnBitcoinKey2 = testPoint } - encoded = encodeChannelAnnouncement msg - case decodeChannelAnnouncement encoded of - Right (decoded, _) -> decoded == msg + case encodeChannelAnnouncement msg of Left _ -> False + Right encoded -> case decodeChannelAnnouncement encoded of + Right (decoded, _) -> decoded == msg + Left _ -> False -- Property: ChannelUpdate roundtrip propChannelUpdateRoundtrip :: Word32 -> Word16 -> Property