ppad-bolt1-0.0.1: lib/Lightning/Protocol/BOLT1/Codec.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
-- |
-- Module: Lightning.Protocol.BOLT1.Codec
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Message encoding and decoding for BOLT #1.
module Lightning.Protocol.BOLT1.Codec (
-- * Encoding errors
EncodeError(..)
-- * Message encoding
, encodeInit
, encodeError
, encodeWarning
, encodePing
, encodePong
, encodePeerStorage
, encodePeerStorageRetrieval
, encodeMessage
, encodeEnvelope
-- * Decoding errors
, DecodeError(..)
-- * Message decoding
, decodeInit
, decodeError
, decodeWarning
, decodePing
, decodePong
, decodePeerStorage
, decodePeerStorageRetrieval
, decodeMessage
, decodeEnvelope
, decodeEnvelopeWith
) where
import Control.DeepSeq (NFData)
import Control.Monad (when, unless)
import qualified Data.ByteString as BS
import Data.Word (Word16, Word64)
import GHC.Generics (Generic)
import Lightning.Protocol.BOLT1.Prim
import Lightning.Protocol.BOLT1.TLV
import Lightning.Protocol.BOLT1.Message
-- Encoding errors -------------------------------------------------------------
-- | Encoding errors.
data EncodeError
= EncodeLengthOverflow -- ^ Field length exceeds u16 max (65535 bytes)
| EncodeMessageTooLarge -- ^ Total message size exceeds 65535 bytes
deriving stock (Eq, Show, Generic)
instance NFData EncodeError
-- Message encoding ------------------------------------------------------------
-- | Encode an Init message payload.
encodeInit :: Init -> Either EncodeError BS.ByteString
encodeInit (Init gf feat tlvs) = do
gfLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength gf)
featLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength feat)
Right $ mconcat
[ gfLen
, gf
, featLen
, feat
, encodeTlvStream (encodeInitTlvs tlvs)
]
-- | Encode an Error message payload.
encodeError :: Error -> Either EncodeError BS.ByteString
encodeError (Error cid dat) = do
datLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength dat)
Right $ mconcat [unChannelId cid, datLen, dat]
-- | Encode a Warning message payload.
encodeWarning :: Warning -> Either EncodeError BS.ByteString
encodeWarning (Warning cid dat) = do
datLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength dat)
Right $ mconcat [unChannelId cid, datLen, dat]
-- | Encode a Ping message payload.
encodePing :: Ping -> Either EncodeError BS.ByteString
encodePing (Ping numPong ignored) = do
ignoredLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength ignored)
Right $ mconcat [encodeU16 numPong, ignoredLen, ignored]
-- | Encode a Pong message payload.
encodePong :: Pong -> Either EncodeError BS.ByteString
encodePong (Pong ignored) = do
ignoredLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength ignored)
Right $ mconcat [ignoredLen, ignored]
-- | Encode a PeerStorage message payload.
encodePeerStorage :: PeerStorage -> Either EncodeError BS.ByteString
encodePeerStorage (PeerStorage blob) = do
blobLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength blob)
Right $ mconcat [blobLen, blob]
-- | Encode a PeerStorageRetrieval message payload.
encodePeerStorageRetrieval
:: PeerStorageRetrieval -> Either EncodeError BS.ByteString
encodePeerStorageRetrieval (PeerStorageRetrieval blob) = do
blobLen <- maybe (Left EncodeLengthOverflow) Right (encodeLength blob)
Right $ mconcat [blobLen, blob]
-- | Encode a message to its payload bytes.
--
-- Checks that the payload does not exceed 65533 bytes (the maximum
-- possible given the 2-byte type field and 65535-byte message limit).
encodeMessage :: Message -> Either EncodeError BS.ByteString
encodeMessage msg = do
payload <- case msg of
MsgInitVal m -> encodeInit m
MsgErrorVal m -> encodeError m
MsgWarningVal m -> encodeWarning m
MsgPingVal m -> encodePing m
MsgPongVal m -> encodePong m
MsgPeerStorageVal m -> encodePeerStorage m
MsgPeerStorageRetrievalVal m -> encodePeerStorageRetrieval m
-- Payload must leave room for 2-byte type (max 65533 bytes)
when (BS.length payload > 65533) $
Left EncodeMessageTooLarge
Right payload
-- | Encode a message as a complete envelope (type + payload + extension).
--
-- Per BOLT #1, the total message size must not exceed 65535 bytes.
encodeEnvelope :: Message -> Maybe TlvStream -> Either EncodeError BS.ByteString
encodeEnvelope msg mext = do
payload <- encodeMessage msg
let !typeBytes = encodeU16 (msgTypeWord (messageType msg))
!extBytes = maybe BS.empty encodeTlvStream mext
!result = mconcat [typeBytes, payload, extBytes]
-- Per BOLT #1: message size must fit in 2 bytes (max 65535)
when (BS.length result > 65535) $
Left EncodeMessageTooLarge
Right result
-- Decoding errors -------------------------------------------------------------
-- | Decoding errors.
data DecodeError
= DecodeInsufficientBytes
| DecodeInvalidLength
| DecodeUnknownEvenType !Word16
| DecodeUnknownOddType !Word16
| DecodeTlvError !TlvError
| DecodeInvalidChannelId
| DecodeInvalidExtension !TlvError
deriving stock (Eq, Show, Generic)
instance NFData DecodeError
-- Message decoding ------------------------------------------------------------
-- | Decode an Init message from payload bytes.
--
-- Returns the decoded message and any remaining bytes.
decodeInit :: BS.ByteString -> Either DecodeError (Init, BS.ByteString)
decodeInit !bs = do
(gfLen, rest1) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 bs)
unless (BS.length rest1 >= fromIntegral gfLen) $
Left DecodeInsufficientBytes
let !gf = BS.take (fromIntegral gfLen) rest1
!rest2 = BS.drop (fromIntegral gfLen) rest1
(fLen, rest3) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 rest2)
unless (BS.length rest3 >= fromIntegral fLen) $
Left DecodeInsufficientBytes
let !feat = BS.take (fromIntegral fLen) rest3
!rest4 = BS.drop (fromIntegral fLen) rest3
-- Parse optional TLV stream (consumes all remaining bytes for init)
tlvs <- if BS.null rest4
then Right (unsafeTlvStream [])
else either (Left . DecodeTlvError) Right (decodeTlvStream rest4)
initTlvList <- either (Left . DecodeTlvError) Right
(parseInitTlvs tlvs)
-- Init consumes all bytes (TLVs are part of init, not extensions)
Right (Init gf feat initTlvList, BS.empty)
-- | Decode an Error message from payload bytes.
decodeError :: BS.ByteString -> Either DecodeError (Error, BS.ByteString)
decodeError !bs = do
unless (BS.length bs >= 32) $ Left DecodeInsufficientBytes
let !cidBytes = BS.take 32 bs
!rest1 = BS.drop 32 bs
cid <- maybe (Left DecodeInvalidChannelId) Right (channelId cidBytes)
(dLen, rest2) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 rest1)
unless (BS.length rest2 >= fromIntegral dLen) $
Left DecodeInsufficientBytes
let !dat = BS.take (fromIntegral dLen) rest2
!rest3 = BS.drop (fromIntegral dLen) rest2
Right (Error cid dat, rest3)
-- | Decode a Warning message from payload bytes.
decodeWarning :: BS.ByteString -> Either DecodeError (Warning, BS.ByteString)
decodeWarning !bs = do
unless (BS.length bs >= 32) $ Left DecodeInsufficientBytes
let !cidBytes = BS.take 32 bs
!rest1 = BS.drop 32 bs
cid <- maybe (Left DecodeInvalidChannelId) Right (channelId cidBytes)
(dLen, rest2) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 rest1)
unless (BS.length rest2 >= fromIntegral dLen) $
Left DecodeInsufficientBytes
let !dat = BS.take (fromIntegral dLen) rest2
!rest3 = BS.drop (fromIntegral dLen) rest2
Right (Warning cid dat, rest3)
-- | Decode a Ping message from payload bytes.
decodePing :: BS.ByteString -> Either DecodeError (Ping, BS.ByteString)
decodePing !bs = do
(numPong, rest1) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 bs)
(bLen, rest2) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 rest1)
unless (BS.length rest2 >= fromIntegral bLen) $
Left DecodeInsufficientBytes
let !ignored = BS.take (fromIntegral bLen) rest2
!rest3 = BS.drop (fromIntegral bLen) rest2
Right (Ping numPong ignored, rest3)
-- | Decode a Pong message from payload bytes.
decodePong :: BS.ByteString -> Either DecodeError (Pong, BS.ByteString)
decodePong !bs = do
(bLen, rest1) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 bs)
unless (BS.length rest1 >= fromIntegral bLen) $
Left DecodeInsufficientBytes
let !ignored = BS.take (fromIntegral bLen) rest1
!rest2 = BS.drop (fromIntegral bLen) rest1
Right (Pong ignored, rest2)
-- | Decode a PeerStorage message from payload bytes.
decodePeerStorage
:: BS.ByteString -> Either DecodeError (PeerStorage, BS.ByteString)
decodePeerStorage !bs = do
(bLen, rest1) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 bs)
unless (BS.length rest1 >= fromIntegral bLen) $
Left DecodeInsufficientBytes
let !blob = BS.take (fromIntegral bLen) rest1
!rest2 = BS.drop (fromIntegral bLen) rest1
Right (PeerStorage blob, rest2)
-- | Decode a PeerStorageRetrieval message from payload bytes.
decodePeerStorageRetrieval
:: BS.ByteString
-> Either DecodeError (PeerStorageRetrieval, BS.ByteString)
decodePeerStorageRetrieval !bs = do
(bLen, rest1) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 bs)
unless (BS.length rest1 >= fromIntegral bLen) $
Left DecodeInsufficientBytes
let !blob = BS.take (fromIntegral bLen) rest1
!rest2 = BS.drop (fromIntegral bLen) rest1
Right (PeerStorageRetrieval blob, rest2)
-- | Decode a message from its type and payload.
--
-- Returns the decoded message and any remaining bytes (for extensions).
-- For unknown types, returns an appropriate error.
decodeMessage
:: MsgType -> BS.ByteString -> Either DecodeError (Message, BS.ByteString)
decodeMessage MsgInit bs = do
(m, rest) <- decodeInit bs
Right (MsgInitVal m, rest)
decodeMessage MsgError bs = do
(m, rest) <- decodeError bs
Right (MsgErrorVal m, rest)
decodeMessage MsgWarning bs = do
(m, rest) <- decodeWarning bs
Right (MsgWarningVal m, rest)
decodeMessage MsgPing bs = do
(m, rest) <- decodePing bs
Right (MsgPingVal m, rest)
decodeMessage MsgPong bs = do
(m, rest) <- decodePong bs
Right (MsgPongVal m, rest)
decodeMessage MsgPeerStorage bs = do
(m, rest) <- decodePeerStorage bs
Right (MsgPeerStorageVal m, rest)
decodeMessage MsgPeerStorageRet bs = do
(m, rest) <- decodePeerStorageRetrieval bs
Right (MsgPeerStorageRetrievalVal m, rest)
decodeMessage (MsgUnknown w) _
| even w = Left (DecodeUnknownEvenType w)
| otherwise = Left (DecodeUnknownOddType w)
-- | Decode a complete envelope (type + payload + optional extension).
--
-- Per BOLT #1:
-- - Unknown odd message types are ignored (returns Nothing for message)
-- - Unknown even message types cause connection close (returns error)
-- - Invalid extension TLV causes connection close (returns error)
--
-- This uses the default policy of treating all extension TLV types as
-- unknown. Use 'decodeEnvelopeWith' for configurable extension handling.
--
-- Returns the decoded message (if known) and any extension TLVs.
decodeEnvelope
:: BS.ByteString
-> Either DecodeError (Maybe Message, Maybe TlvStream)
decodeEnvelope = decodeEnvelopeWith (const False)
-- | Decode a complete envelope with configurable extension TLV handling.
--
-- The predicate determines which extension TLV types are "known" and
-- should be preserved. Unknown even types cause failure; unknown odd
-- types are skipped.
--
-- Use @decodeEnvelopeWith (const False)@ to reject all even extension
-- types (the default behavior of 'decodeEnvelope').
--
-- Use @decodeEnvelopeWith (const True)@ to accept all extension types.
decodeEnvelopeWith
:: (Word64 -> Bool) -- ^ Predicate: is this extension TLV type known?
-> BS.ByteString
-> Either DecodeError (Maybe Message, Maybe TlvStream)
decodeEnvelopeWith isKnownExt !bs = do
(typeWord, rest1) <- maybe (Left DecodeInsufficientBytes) Right
(decodeU16 bs)
let !msgType = parseMsgType typeWord
case msgType of
MsgUnknown w
| even w -> Left (DecodeUnknownEvenType w)
| otherwise -> Right (Nothing, Nothing) -- Ignore unknown odd types
_ -> do
(msg, rest2) <- decodeMessage msgType rest1
-- Parse any remaining bytes as extension TLV
ext <- if BS.null rest2
then Right Nothing
else case decodeTlvStreamWith isKnownExt rest2 of
Left e -> Left (DecodeInvalidExtension e)
Right s -> Right (Just s)
Right (Just msg, ext)