packages feed

ppad-bolt7-0.0.1: lib/Lightning/Protocol/BOLT7/Codec.hs

{-# OPTIONS_HADDOCK prune #-}

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}

-- |
-- Module: Lightning.Protocol.BOLT7.Codec
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Encoding and decoding for BOLT #7 gossip messages.

module Lightning.Protocol.BOLT7.Codec (
  -- * Error types
    EncodeError(..)
  , DecodeError(..)

  -- * Channel announcement
  , encodeChannelAnnouncement
  , decodeChannelAnnouncement

  -- * Node announcement
  , encodeNodeAnnouncement
  , decodeNodeAnnouncement

  -- * Channel update
  , encodeChannelUpdate
  , decodeChannelUpdate

  -- * Announcement signatures
  , encodeAnnouncementSignatures
  , decodeAnnouncementSignatures

  -- * Query messages
  , encodeQueryShortChannelIds
  , decodeQueryShortChannelIds
  , encodeReplyShortChannelIdsEnd
  , decodeReplyShortChannelIdsEnd
  , encodeQueryChannelRange
  , decodeQueryChannelRange
  , encodeReplyChannelRange
  , decodeReplyChannelRange
  , encodeGossipTimestampFilter
  , decodeGossipTimestampFilter

  -- * Short channel ID encoding
  , encodeShortChannelIdList
  , decodeShortChannelIdList
  ) where

import Control.DeepSeq (NFData)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Word (Word8, Word16, Word32, Word64)
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT1 (unsafeTlvStream)
import qualified Lightning.Protocol.BOLT1.Prim as Prim
import qualified Lightning.Protocol.BOLT1.TLV as TLV
import Lightning.Protocol.BOLT7.Messages
import Lightning.Protocol.BOLT7.Types

-- Error types -----------------------------------------------------------------

-- | Encoding errors.
data EncodeError
  = EncodeLengthOverflow  -- ^ Field too large for u16 length prefix
  deriving (Eq, Show, Generic)

instance NFData EncodeError

-- | Decoding errors.
data DecodeError
  = DecodeInsufficientBytes          -- ^ Not enough bytes
  | DecodeInvalidSignature           -- ^ Invalid signature field
  | DecodeInvalidChainHash           -- ^ Invalid chain hash field
  | DecodeInvalidShortChannelId      -- ^ Invalid short channel ID field
  | DecodeInvalidChannelId           -- ^ Invalid channel ID field
  | DecodeInvalidNodeId              -- ^ Invalid node ID field
  | DecodeInvalidPoint               -- ^ Invalid point field
  | DecodeInvalidRgbColor            -- ^ Invalid RGB color field
  | DecodeInvalidAlias               -- ^ Invalid alias field
  | DecodeInvalidAddress             -- ^ Invalid address encoding
  | DecodeTlvError                   -- ^ TLV decoding error
  deriving (Eq, Show, Generic)

instance NFData DecodeError

-- Primitive helpers -----------------------------------------------------------

-- | Decode u8.
decodeU8 :: ByteString -> Either DecodeError (Word8, ByteString)
decodeU8 bs
  | BS.null bs = Left DecodeInsufficientBytes
  | otherwise = Right (BS.index bs 0, BS.drop 1 bs)
{-# INLINE decodeU8 #-}

-- | Decode u16 (big-endian).
decodeU16 :: ByteString -> Either DecodeError (Word16, ByteString)
decodeU16 bs = case Prim.decodeU16 bs of
  Nothing -> Left DecodeInsufficientBytes
  Just r  -> Right r
{-# INLINE decodeU16 #-}

-- | Decode u32 (big-endian).
decodeU32 :: ByteString -> Either DecodeError (Word32, ByteString)
decodeU32 bs = case Prim.decodeU32 bs of
  Nothing -> Left DecodeInsufficientBytes
  Just r  -> Right r
{-# INLINE decodeU32 #-}

-- | Decode u64 (big-endian).
decodeU64 :: ByteString -> Either DecodeError (Word64, ByteString)
decodeU64 bs = case Prim.decodeU64 bs of
  Nothing -> Left DecodeInsufficientBytes
  Just r  -> Right r
{-# INLINE decodeU64 #-}

-- | Decode fixed-length bytes.
decodeBytes :: Int -> ByteString -> Either DecodeError (ByteString, ByteString)
decodeBytes n bs
  | BS.length bs < n = Left DecodeInsufficientBytes
  | otherwise = Right (BS.splitAt n bs)
{-# INLINE decodeBytes #-}

-- | Decode length-prefixed bytes (u16 prefix).
decodeLenPrefixed :: ByteString
                  -> Either DecodeError (ByteString, ByteString)
decodeLenPrefixed bs = do
  (len, rest) <- decodeU16 bs
  let n = fromIntegral len
  if BS.length rest < n
    then Left DecodeInsufficientBytes
    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
{-# INLINE encodeLenPrefixed #-}

-- | Decode fixed-length validated type.
decodeFixed :: Int -> DecodeError -> (ByteString -> Maybe a)
            -> ByteString -> Either DecodeError (a, ByteString)
decodeFixed len err mkVal bs = do
  (bytes, rest) <- decodeBytes len bs
  case mkVal bytes of
    Nothing -> Left err
    Just v  -> Right (v, rest)
{-# INLINE decodeFixed #-}

-- Type-specific decoders ------------------------------------------------------

-- | Decode Signature (64 bytes).
decodeSignature :: ByteString -> Either DecodeError (Signature, ByteString)
decodeSignature = decodeFixed signatureLen DecodeInvalidSignature signature
{-# INLINE decodeSignature #-}

-- | Decode ChainHash (32 bytes).
decodeChainHash :: ByteString -> Either DecodeError (ChainHash, ByteString)
decodeChainHash = decodeFixed chainHashLen DecodeInvalidChainHash chainHash
{-# INLINE decodeChainHash #-}

-- | Decode ShortChannelId (8 bytes).
decodeShortChannelId :: ByteString
                     -> Either DecodeError (ShortChannelId, ByteString)
decodeShortChannelId =
  decodeFixed shortChannelIdLen DecodeInvalidShortChannelId shortChannelId
{-# INLINE decodeShortChannelId #-}

-- | Decode ChannelId (32 bytes).
decodeChannelId :: ByteString -> Either DecodeError (ChannelId, ByteString)
decodeChannelId = decodeFixed channelIdLen DecodeInvalidChannelId channelId
{-# INLINE decodeChannelId #-}

-- | Decode NodeId (33 bytes).
decodeNodeId :: ByteString -> Either DecodeError (NodeId, ByteString)
decodeNodeId = decodeFixed nodeIdLen DecodeInvalidNodeId nodeId
{-# INLINE decodeNodeId #-}

-- | Decode Point (33 bytes).
decodePoint :: ByteString -> Either DecodeError (Point, ByteString)
decodePoint = decodeFixed pointLen DecodeInvalidPoint point
{-# INLINE decodePoint #-}

-- | Decode RgbColor (3 bytes).
decodeRgbColor :: ByteString -> Either DecodeError (RgbColor, ByteString)
decodeRgbColor = decodeFixed rgbColorLen DecodeInvalidRgbColor rgbColor
{-# INLINE decodeRgbColor #-}

-- | Decode Alias (32 bytes).
decodeAlias :: ByteString -> Either DecodeError (Alias, ByteString)
decodeAlias = decodeFixed aliasLen DecodeInvalidAlias alias
{-# INLINE decodeAlias #-}

-- | Decode FeatureBits (length-prefixed).
decodeFeatureBits :: ByteString -> Either DecodeError (FeatureBits, ByteString)
decodeFeatureBits bs = do
  (bytes, rest) <- decodeLenPrefixed bs
  Right (featureBits bytes, rest)
{-# INLINE decodeFeatureBits #-}

-- | Decode addresses list (length-prefixed).
decodeAddresses :: ByteString -> Either DecodeError ([Address], ByteString)
decodeAddresses bs = do
  (addrData, rest) <- decodeLenPrefixed bs
  addrs <- parseAddrs addrData
  Right (addrs, rest)
  where
    parseAddrs :: ByteString -> Either DecodeError [Address]
    parseAddrs !d
      | BS.null d = Right []
      | otherwise = do
          (addr, d') <- parseOneAddr d
          addrs <- parseAddrs d'
          Right (addr : addrs)

    parseOneAddr :: ByteString -> Either DecodeError (Address, ByteString)
    parseOneAddr d = do
      (typ, d1) <- decodeU8 d
      case typ of
        1 -> do  -- IPv4
          (addrBytes, d2) <- decodeBytes ipv4AddrLen d1
          (port, d3) <- decodeU16 d2
          case ipv4Addr addrBytes of
            Nothing -> Left DecodeInvalidAddress
            Just a  -> Right (AddrIPv4 a port, d3)
        2 -> do  -- IPv6
          (addrBytes, d2) <- decodeBytes ipv6AddrLen d1
          (port, d3) <- decodeU16 d2
          case ipv6Addr addrBytes of
            Nothing -> Left DecodeInvalidAddress
            Just a  -> Right (AddrIPv6 a port, d3)
        4 -> do  -- Tor v3
          (addrBytes, d2) <- decodeBytes torV3AddrLen d1
          (port, d3) <- decodeU16 d2
          case torV3Addr addrBytes of
            Nothing -> Left DecodeInvalidAddress
            Just a  -> Right (AddrTorV3 a port, d3)
        5 -> do  -- DNS hostname
          (hostLen, d2) <- decodeU8 d1
          (hostBytes, d3) <- decodeBytes (fromIntegral hostLen) d2
          (port, d4) <- decodeU16 d3
          Right (AddrDNS hostBytes port, d4)
        _ -> Left DecodeInvalidAddress  -- Unknown address type

-- Channel announcement --------------------------------------------------------

-- | Encode channel_announcement message.
encodeChannelAnnouncement :: ChannelAnnouncement -> ByteString
encodeChannelAnnouncement msg = mconcat
  [ getSignature (channelAnnNodeSig1 msg)
  , getSignature (channelAnnNodeSig2 msg)
  , getSignature (channelAnnBitcoinSig1 msg)
  , getSignature (channelAnnBitcoinSig2 msg)
  , encodeLenPrefixed (getFeatureBits (channelAnnFeatures msg))
  , getChainHash (channelAnnChainHash msg)
  , getShortChannelId (channelAnnShortChanId msg)
  , getNodeId (channelAnnNodeId1 msg)
  , getNodeId (channelAnnNodeId2 msg)
  , getPoint (channelAnnBitcoinKey1 msg)
  , getPoint (channelAnnBitcoinKey2 msg)
  ]

-- | Decode channel_announcement message.
decodeChannelAnnouncement :: ByteString
                          -> Either DecodeError (ChannelAnnouncement, ByteString)
decodeChannelAnnouncement bs = do
  (nodeSig1, bs1)    <- decodeSignature bs
  (nodeSig2, bs2)    <- decodeSignature bs1
  (btcSig1, bs3)     <- decodeSignature bs2
  (btcSig2, bs4)     <- decodeSignature bs3
  (features, bs5)    <- decodeFeatureBits bs4
  (chainH, bs6)      <- decodeChainHash bs5
  (scid, bs7)        <- decodeShortChannelId bs6
  (nid1, bs8)        <- decodeNodeId bs7
  (nid2, bs9)        <- decodeNodeId bs8
  (btcKey1, bs10)    <- decodePoint bs9
  (btcKey2, rest)    <- decodePoint bs10
  let msg = ChannelAnnouncement
        { channelAnnNodeSig1    = nodeSig1
        , channelAnnNodeSig2    = nodeSig2
        , channelAnnBitcoinSig1 = btcSig1
        , channelAnnBitcoinSig2 = btcSig2
        , channelAnnFeatures    = features
        , channelAnnChainHash   = chainH
        , channelAnnShortChanId = scid
        , channelAnnNodeId1     = nid1
        , channelAnnNodeId2     = nid2
        , channelAnnBitcoinKey1 = btcKey1
        , channelAnnBitcoinKey2 = btcKey2
        }
  Right (msg, rest)

-- Node announcement -----------------------------------------------------------

-- | Encode node_announcement message.
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
      [ getSignature (nodeAnnSignature msg)
      , encodeLenPrefixed features
      , Prim.encodeU32 (nodeAnnTimestamp msg)
      , getNodeId (nodeAnnNodeId msg)
      , getRgbColor (nodeAnnRgbColor msg)
      , getAlias (nodeAnnAlias msg)
      , encodeLenPrefixed addrData
      ]

-- | Encode address list.
encodeAddresses :: [Address] -> Either EncodeError ByteString
encodeAddresses addrs = Right $ mconcat (map encodeAddress addrs)
  where
    encodeAddress :: Address -> ByteString
    encodeAddress (AddrIPv4 a port) = mconcat
      [ BS.singleton 1
      , getIPv4Addr a
      , Prim.encodeU16 port
      ]
    encodeAddress (AddrIPv6 a port) = mconcat
      [ BS.singleton 2
      , getIPv6Addr a
      , Prim.encodeU16 port
      ]
    encodeAddress (AddrTorV3 a port) = mconcat
      [ BS.singleton 4
      , getTorV3Addr a
      , Prim.encodeU16 port
      ]
    encodeAddress (AddrDNS host port) = mconcat
      [ BS.singleton 5
      , BS.singleton (fromIntegral $ BS.length host)
      , host
      , Prim.encodeU16 port
      ]

-- | Decode node_announcement message.
decodeNodeAnnouncement :: ByteString
                       -> Either DecodeError (NodeAnnouncement, ByteString)
decodeNodeAnnouncement bs = do
  (sig, bs1)       <- decodeSignature bs
  (features, bs2)  <- decodeFeatureBits bs1
  (timestamp, bs3) <- decodeU32 bs2
  (nid, bs4)       <- decodeNodeId bs3
  (color, bs5)     <- decodeRgbColor bs4
  (al, bs6)        <- decodeAlias bs5
  (addrs, rest)    <- decodeAddresses bs6
  let msg = NodeAnnouncement
        { nodeAnnSignature = sig
        , nodeAnnFeatures  = features
        , nodeAnnTimestamp = timestamp
        , nodeAnnNodeId    = nid
        , nodeAnnRgbColor  = color
        , nodeAnnAlias     = al
        , nodeAnnAddresses = addrs
        }
  Right (msg, rest)

-- Channel update --------------------------------------------------------------

-- | Encode channel_update message.
encodeChannelUpdate :: ChannelUpdate -> ByteString
encodeChannelUpdate msg = mconcat
  [ getSignature (chanUpdateSignature msg)
  , getChainHash (chanUpdateChainHash msg)
  , getShortChannelId (chanUpdateShortChanId msg)
  , Prim.encodeU32 (chanUpdateTimestamp msg)
  , BS.singleton (encodeMessageFlags (chanUpdateMsgFlags msg))
  , BS.singleton (encodeChannelFlags (chanUpdateChanFlags msg))
  , Prim.encodeU16 (getCltvExpiryDelta (chanUpdateCltvExpDelta msg))
  , Prim.encodeU64 (getHtlcMinimumMsat (chanUpdateHtlcMinMsat msg))
  , Prim.encodeU32 (getFeeBaseMsat (chanUpdateFeeBaseMsat msg))
  , Prim.encodeU32 (getFeeProportionalMillionths (chanUpdateFeeProportional msg))
  , case chanUpdateHtlcMaxMsat msg of
      Nothing -> BS.empty
      Just m  -> Prim.encodeU64 (getHtlcMaximumMsat m)
  ]

-- | Decode channel_update message.
decodeChannelUpdate :: ByteString
                    -> Either DecodeError (ChannelUpdate, ByteString)
decodeChannelUpdate bs = do
  (sig, bs1)         <- decodeSignature bs
  (chainH, bs2)      <- decodeChainHash bs1
  (scid, bs3)        <- decodeShortChannelId bs2
  (timestamp, bs4)   <- decodeU32 bs3
  (msgFlagsRaw, bs5) <- decodeU8 bs4
  (chanFlagsRaw, bs6) <- decodeU8 bs5
  (cltvDelta, bs7)   <- decodeU16 bs6
  (htlcMin, bs8)     <- decodeU64 bs7
  (feeBase, bs9)     <- decodeU32 bs8
  (feeProp, bs10)    <- decodeU32 bs9
  let msgFlags' = decodeMessageFlags msgFlagsRaw
      chanFlags' = decodeChannelFlags chanFlagsRaw
  -- htlc_maximum_msat is present if message_flags bit 0 is set
  (htlcMax, rest) <- if mfHtlcMaxPresent msgFlags'
    then do
      (m, r) <- decodeU64 bs10
      Right (Just (HtlcMaximumMsat m), r)
    else Right (Nothing, bs10)
  let msg = ChannelUpdate
        { chanUpdateSignature       = sig
        , chanUpdateChainHash       = chainH
        , chanUpdateShortChanId     = scid
        , chanUpdateTimestamp       = timestamp
        , chanUpdateMsgFlags        = msgFlags'
        , chanUpdateChanFlags       = chanFlags'
        , chanUpdateCltvExpDelta    = CltvExpiryDelta cltvDelta
        , chanUpdateHtlcMinMsat     = HtlcMinimumMsat htlcMin
        , chanUpdateFeeBaseMsat     = FeeBaseMsat feeBase
        , chanUpdateFeeProportional = FeeProportionalMillionths feeProp
        , chanUpdateHtlcMaxMsat     = htlcMax
        }
  Right (msg, rest)

-- Announcement signatures -----------------------------------------------------

-- | Encode announcement_signatures message.
encodeAnnouncementSignatures :: AnnouncementSignatures -> ByteString
encodeAnnouncementSignatures msg = mconcat
  [ getChannelId (annSigChannelId msg)
  , getShortChannelId (annSigShortChanId msg)
  , getSignature (annSigNodeSig msg)
  , getSignature (annSigBitcoinSig msg)
  ]

-- | Decode announcement_signatures message.
decodeAnnouncementSignatures :: ByteString
                             -> Either DecodeError
                                  (AnnouncementSignatures, ByteString)
decodeAnnouncementSignatures bs = do
  (cid, bs1)     <- decodeChannelId bs
  (scid, bs2)    <- decodeShortChannelId bs1
  (nodeSig, bs3) <- decodeSignature bs2
  (btcSig, rest) <- decodeSignature bs3
  let msg = AnnouncementSignatures
        { annSigChannelId   = cid
        , annSigShortChanId = scid
        , annSigNodeSig     = nodeSig
        , annSigBitcoinSig  = btcSig
        }
  Right (msg, rest)

-- Query messages --------------------------------------------------------------

-- | Encode query_short_channel_ids message.
encodeQueryShortChannelIds :: QueryShortChannelIds
                           -> Either EncodeError ByteString
encodeQueryShortChannelIds msg = do
  let scidData = queryScidsData msg
  if BS.length scidData > 65535
    then Left EncodeLengthOverflow
    else Right $ mconcat
      [ getChainHash (queryScidsChainHash msg)
      , encodeLenPrefixed scidData
      , TLV.encodeTlvStream (queryScidsTlvs msg)
      ]

-- | Decode query_short_channel_ids message.
decodeQueryShortChannelIds :: ByteString
                           -> Either DecodeError
                                (QueryShortChannelIds, ByteString)
decodeQueryShortChannelIds bs = do
  (chainH, bs1)   <- decodeChainHash bs
  (scidData, bs2) <- decodeLenPrefixed bs1
  let tlvs = case TLV.decodeTlvStreamRaw bs2 of
        Left _  -> unsafeTlvStream []
        Right t -> t
  let msg = QueryShortChannelIds
        { queryScidsChainHash = chainH
        , queryScidsData      = scidData
        , queryScidsTlvs      = tlvs
        }
  Right (msg, BS.empty)

-- | Encode reply_short_channel_ids_end message.
encodeReplyShortChannelIdsEnd :: ReplyShortChannelIdsEnd -> ByteString
encodeReplyShortChannelIdsEnd msg = mconcat
  [ getChainHash (replyScidsChainHash msg)
  , BS.singleton (replyScidsFullInfo msg)
  ]

-- | Decode reply_short_channel_ids_end message.
decodeReplyShortChannelIdsEnd :: ByteString
                              -> Either DecodeError
                                   (ReplyShortChannelIdsEnd, ByteString)
decodeReplyShortChannelIdsEnd bs = do
  (chainH, bs1)    <- decodeChainHash bs
  (fullInfo, rest) <- decodeU8 bs1
  let msg = ReplyShortChannelIdsEnd
        { replyScidsChainHash = chainH
        , replyScidsFullInfo  = fullInfo
        }
  Right (msg, rest)

-- | Encode query_channel_range message.
encodeQueryChannelRange :: QueryChannelRange -> ByteString
encodeQueryChannelRange msg = mconcat
  [ getChainHash (queryRangeChainHash msg)
  , Prim.encodeU32 (queryRangeFirstBlock msg)
  , Prim.encodeU32 (queryRangeNumBlocks msg)
  , TLV.encodeTlvStream (queryRangeTlvs msg)
  ]

-- | Decode query_channel_range message.
decodeQueryChannelRange :: ByteString
                        -> Either DecodeError (QueryChannelRange, ByteString)
decodeQueryChannelRange bs = do
  (chainH, bs1)     <- decodeChainHash bs
  (firstBlock, bs2) <- decodeU32 bs1
  (numBlocks, bs3)  <- decodeU32 bs2
  let tlvs = case TLV.decodeTlvStreamRaw bs3 of
        Left _  -> unsafeTlvStream []
        Right t -> t
  let msg = QueryChannelRange
        { queryRangeChainHash  = chainH
        , queryRangeFirstBlock = firstBlock
        , queryRangeNumBlocks  = numBlocks
        , queryRangeTlvs       = tlvs
        }
  Right (msg, BS.empty)

-- | Encode reply_channel_range message.
encodeReplyChannelRange :: ReplyChannelRange -> Either EncodeError ByteString
encodeReplyChannelRange msg = do
  let rangeData = replyRangeData msg
  if BS.length rangeData > 65535
    then Left EncodeLengthOverflow
    else Right $ mconcat
      [ getChainHash (replyRangeChainHash msg)
      , Prim.encodeU32 (replyRangeFirstBlock msg)
      , Prim.encodeU32 (replyRangeNumBlocks msg)
      , BS.singleton (replyRangeSyncComplete msg)
      , encodeLenPrefixed rangeData
      , TLV.encodeTlvStream (replyRangeTlvs msg)
      ]

-- | Decode reply_channel_range message.
decodeReplyChannelRange :: ByteString
                        -> Either DecodeError (ReplyChannelRange, ByteString)
decodeReplyChannelRange bs = do
  (chainH, bs1)       <- decodeChainHash bs
  (firstBlock, bs2)   <- decodeU32 bs1
  (numBlocks, bs3)    <- decodeU32 bs2
  (syncComplete, bs4) <- decodeU8 bs3
  (rangeData, bs5)    <- decodeLenPrefixed bs4
  let tlvs = case TLV.decodeTlvStreamRaw bs5 of
        Left _  -> unsafeTlvStream []
        Right t -> t
  let msg = ReplyChannelRange
        { replyRangeChainHash    = chainH
        , replyRangeFirstBlock   = firstBlock
        , replyRangeNumBlocks    = numBlocks
        , replyRangeSyncComplete = syncComplete
        , replyRangeData         = rangeData
        , replyRangeTlvs         = tlvs
        }
  Right (msg, BS.empty)

-- | Encode gossip_timestamp_filter message.
encodeGossipTimestampFilter :: GossipTimestampFilter -> ByteString
encodeGossipTimestampFilter msg = mconcat
  [ getChainHash (gossipFilterChainHash msg)
  , Prim.encodeU32 (gossipFilterFirstTimestamp msg)
  , Prim.encodeU32 (gossipFilterTimestampRange msg)
  ]

-- | Decode gossip_timestamp_filter message.
decodeGossipTimestampFilter :: ByteString
                            -> Either DecodeError
                                 (GossipTimestampFilter, ByteString)
decodeGossipTimestampFilter bs = do
  (chainH, bs1)   <- decodeChainHash bs
  (firstTs, bs2)  <- decodeU32 bs1
  (tsRange, rest) <- decodeU32 bs2
  let msg = GossipTimestampFilter
        { gossipFilterChainHash      = chainH
        , gossipFilterFirstTimestamp = firstTs
        , gossipFilterTimestampRange = tsRange
        }
  Right (msg, rest)

-- Short channel ID list encoding -----------------------------------------------

-- | Encode a list of short channel IDs as concatenated 8-byte values.
--
-- This produces encoded_short_ids data with encoding type 0 (uncompressed).
-- The first byte is the encoding type (0), followed by the concatenated SCIDs.
--
-- Note: This does NOT sort the SCIDs. The caller should ensure they are in
-- ascending order if that's required by the protocol context.
encodeShortChannelIdList :: [ShortChannelId] -> ByteString
encodeShortChannelIdList scids = BS.cons 0 $
  mconcat (map getShortChannelId scids)
{-# INLINE encodeShortChannelIdList #-}

-- | Decode a list of short channel IDs from encoded_short_ids data.
--
-- Supports encoding type 0 (uncompressed). Other encoding types will fail.
decodeShortChannelIdList :: ByteString
                         -> Either DecodeError [ShortChannelId]
decodeShortChannelIdList bs
  | BS.null bs = Left DecodeInsufficientBytes
  | otherwise = do
      let encType = BS.index bs 0
          payload = BS.drop 1 bs
      case encType of
        0 -> decodeUncompressedScids payload
        _ -> Left DecodeInvalidShortChannelId  -- Unsupported encoding type
  where
    decodeUncompressedScids :: ByteString -> Either DecodeError [ShortChannelId]
    decodeUncompressedScids !d
      | BS.null d = Right []
      | BS.length d < shortChannelIdLen = Left DecodeInsufficientBytes
      | otherwise = do
          let (scidBytes, rest) = BS.splitAt shortChannelIdLen d
          case shortChannelId scidBytes of
            Nothing -> Left DecodeInvalidShortChannelId
            Just scid -> do
              scids <- decodeUncompressedScids rest
              Right (scid : scids)
{-# INLINE decodeShortChannelIdList #-}