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:
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