bolt7

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

commit 04eb598fb74908ec1e9a941e0f701d1215acc0a7
parent a090604b82f24d72328a4e58b47a2e912fb93d2e
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 15:04:34 +0400

Phase 1: Fix skeleton build errors

Fix bolt1 API usage:
- Use unsafeTlvStream [] instead of TlvStream []
- Use correct Prim.encodeU16/decodeU16 API (not u16/word16)
- Fix decodeTlvStreamRaw pattern matching (returns Either, not tuple)

Add NFData instances for EncodeError and DecodeError to support
criterion/weigh benchmarks.

Remove unused imports (Word64, Word8, Base16, TlvStream).

Library, tests, and benchmarks all build cleanly.
All 26 tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mbench/Main.hs | 4++--
Mbench/Weight.hs | 1-
Aflake.lock | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/Lightning/Protocol/BOLT7/Codec.hs | 139++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mlib/Lightning/Protocol/BOLT7/Messages.hs | 2+-
Mtest/Main.hs | 7+++----
6 files changed, 227 insertions(+), 77 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -13,7 +13,7 @@ module Main where import Criterion.Main import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT1 (TlvStream(..)) +import Lightning.Protocol.BOLT1 (TlvStream, unsafeTlvStream) import Lightning.Protocol.BOLT7 -- Test data construction ------------------------------------------------------ @@ -93,7 +93,7 @@ testAlias = case alias zeroBytes32 of -- | Empty TLV stream. emptyTlvs :: TlvStream -emptyTlvs = TlvStream [] +emptyTlvs = unsafeTlvStream [] {-# NOINLINE emptyTlvs #-} -- | Empty feature bits. diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -12,7 +12,6 @@ module Main where import qualified Data.ByteString as BS -import Lightning.Protocol.BOLT1 (TlvStream(..)) import Lightning.Protocol.BOLT7 import Weigh diff --git a/flake.lock b/flake.lock @@ -0,0 +1,151 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766840161, + "narHash": "sha256-Ss/LHpJJsng8vz1Pe33RSGIWUOcqM1fjrehjUkdrWio=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3edc4a30ed3903fdf6f90c837f961fa6b49582d1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "ppad-base16": { + "inputs": { + "flake-utils": [ + "ppad-bolt1", + "ppad-base16", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-bolt1", + "ppad-base16", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-bolt1", + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1766934151, + "narHash": "sha256-BUFpuLfrGXE2xi3Wa9TYCEhhRhFp175Ghxnr0JRbG2I=", + "ref": "master", + "rev": "58dfb7922401a60d5de76825fcd5f6ecbcd7afe0", + "revCount": 26, + "type": "git", + "url": "git://git.ppad.tech/base16.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/base16.git" + } + }, + "ppad-bolt1": { + "inputs": { + "flake-utils": [ + "ppad-bolt1", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-bolt1", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-base16": "ppad-base16", + "ppad-nixpkgs": [ + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1769338476, + "narHash": "sha256-GX7raydVjsGq6qklkel8sMOX2XtSZnuGNGpsXwmWnnI=", + "path": "/Users/jtobin/src/ppad/bolt1", + "type": "path" + }, + "original": { + "path": "/Users/jtobin/src/ppad/bolt1", + "type": "path" + } + }, + "ppad-nixpkgs": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1766932084, + "narHash": "sha256-GvVsbTfW+B7IQ9K/QP2xcXJAm1lhBin1jYZWNjOzT+o=", + "ref": "master", + "rev": "353e61763b959b960a55321a85423501e3e9ed7a", + "revCount": 2, + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + } + }, + "root": { + "inputs": { + "flake-utils": [ + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-bolt1": "ppad-bolt1", + "ppad-nixpkgs": "ppad-nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/lib/Lightning/Protocol/BOLT7/Codec.hs b/lib/Lightning/Protocol/BOLT7/Codec.hs @@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DeriveGeneric #-} -- | -- Module: Lightning.Protocol.BOLT7.Codec @@ -42,11 +43,13 @@ module Lightning.Protocol.BOLT7.Codec ( , decodeGossipTimestampFilter ) where +import Control.DeepSeq (NFData) import Data.Bits ((.&.)) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import Data.Word (Word8, Word16, Word32, Word64) -import Lightning.Protocol.BOLT1 (TlvStream(..)) +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 @@ -57,7 +60,9 @@ import Lightning.Protocol.BOLT7.Types -- | Encoding errors. data EncodeError = EncodeLengthOverflow -- ^ Field too large for u16 length prefix - deriving (Eq, Show) + deriving (Eq, Show, Generic) + +instance NFData EncodeError -- | Decoding errors. data DecodeError @@ -72,7 +77,9 @@ data DecodeError | DecodeInvalidAlias -- ^ Invalid alias field | DecodeInvalidAddress -- ^ Invalid address encoding | DecodeTlvError -- ^ TLV decoding error - deriving (Eq, Show) + deriving (Eq, Show, Generic) + +instance NFData DecodeError -- Primitive helpers ----------------------------------------------------------- @@ -85,29 +92,23 @@ decodeU8 bs -- | Decode u16 (big-endian). decodeU16 :: ByteString -> Either DecodeError (Word16, ByteString) -decodeU16 bs - | BS.length bs < 2 = Left DecodeInsufficientBytes - | otherwise = - let (bytes, rest) = BS.splitAt 2 bs - in Right (Prim.word16 bytes, rest) +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 - | BS.length bs < 4 = Left DecodeInsufficientBytes - | otherwise = - let (bytes, rest) = BS.splitAt 4 bs - in Right (Prim.word32 bytes, rest) +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 - | BS.length bs < 8 = Left DecodeInsufficientBytes - | otherwise = - let (bytes, rest) = BS.splitAt 8 bs - in Right (Prim.word64 bytes, rest) +decodeU64 bs = case Prim.decodeU64 bs of + Nothing -> Left DecodeInsufficientBytes + Just r -> Right r {-# INLINE decodeU64 #-} -- | Decode fixed-length bytes. @@ -222,8 +223,8 @@ decodeAddresses bs = do | BS.null d = Right [] | otherwise = do (addr, d') <- parseOneAddr d - rest <- parseAddrs d' - Right (addr : rest) + addrs <- parseAddrs d' + Right (addr : addrs) parseOneAddr :: ByteString -> Either DecodeError (Address, ByteString) parseOneAddr d = do @@ -263,7 +264,7 @@ encodeChannelAnnouncement msg = mconcat , getSignature (channelAnnNodeSig2 msg) , getSignature (channelAnnBitcoinSig1 msg) , getSignature (channelAnnBitcoinSig2 msg) - , Prim.u16 (fromIntegral $ BS.length features) + , Prim.encodeU16 (fromIntegral $ BS.length features) , features , getChainHash (channelAnnChainHash msg) , getShortChannelId (channelAnnShortChanId msg) @@ -286,8 +287,8 @@ decodeChannelAnnouncement bs = do (features, bs5) <- decodeFeatureBits bs4 (chainH, bs6) <- decodeChainHash bs5 (scid, bs7) <- decodeShortChannelId bs6 - (nodeId1, bs8) <- decodeNodeId bs7 - (nodeId2, bs9) <- decodeNodeId bs8 + (nid1, bs8) <- decodeNodeId bs7 + (nid2, bs9) <- decodeNodeId bs8 (btcKey1, bs10) <- decodePoint bs9 (btcKey2, rest) <- decodePoint bs10 let msg = ChannelAnnouncement @@ -298,8 +299,8 @@ decodeChannelAnnouncement bs = do , channelAnnFeatures = features , channelAnnChainHash = chainH , channelAnnShortChanId = scid - , channelAnnNodeId1 = nodeId1 - , channelAnnNodeId2 = nodeId2 + , channelAnnNodeId1 = nid1 + , channelAnnNodeId2 = nid2 , channelAnnBitcoinKey1 = btcKey1 , channelAnnBitcoinKey2 = btcKey2 } @@ -316,13 +317,13 @@ encodeNodeAnnouncement msg = do then Left EncodeLengthOverflow else Right $ mconcat [ getSignature (nodeAnnSignature msg) - , Prim.u16 (fromIntegral $ BS.length features) + , Prim.encodeU16 (fromIntegral $ BS.length features) , features - , Prim.u32 (nodeAnnTimestamp msg) + , Prim.encodeU32 (nodeAnnTimestamp msg) , getNodeId (nodeAnnNodeId msg) , getRgbColor (nodeAnnRgbColor msg) , getAlias (nodeAnnAlias msg) - , Prim.u16 (fromIntegral $ BS.length addrData) + , Prim.encodeU16 (fromIntegral $ BS.length addrData) , addrData ] @@ -334,23 +335,23 @@ encodeAddresses addrs = Right $ mconcat (map encodeAddress addrs) encodeAddress (AddrIPv4 a port) = mconcat [ BS.singleton 1 , getIPv4Addr a - , Prim.u16 port + , Prim.encodeU16 port ] encodeAddress (AddrIPv6 a port) = mconcat [ BS.singleton 2 , getIPv6Addr a - , Prim.u16 port + , Prim.encodeU16 port ] encodeAddress (AddrTorV3 a port) = mconcat [ BS.singleton 4 , getTorV3Addr a - , Prim.u16 port + , Prim.encodeU16 port ] encodeAddress (AddrDNS host port) = mconcat [ BS.singleton 5 , BS.singleton (fromIntegral $ BS.length host) , host - , Prim.u16 port + , Prim.encodeU16 port ] -- | Decode node_announcement message. @@ -383,16 +384,16 @@ encodeChannelUpdate msg = mconcat [ getSignature (chanUpdateSignature msg) , getChainHash (chanUpdateChainHash msg) , getShortChannelId (chanUpdateShortChanId msg) - , Prim.u32 (chanUpdateTimestamp msg) + , Prim.encodeU32 (chanUpdateTimestamp msg) , BS.singleton (chanUpdateMsgFlags msg) , BS.singleton (chanUpdateChanFlags msg) - , Prim.u16 (chanUpdateCltvExpDelta msg) - , Prim.u64 (chanUpdateHtlcMinMsat msg) - , Prim.u32 (chanUpdateFeeBaseMsat msg) - , Prim.u32 (chanUpdateFeeProportional msg) + , Prim.encodeU16 (chanUpdateCltvExpDelta msg) + , Prim.encodeU64 (chanUpdateHtlcMinMsat msg) + , Prim.encodeU32 (chanUpdateFeeBaseMsat msg) + , Prim.encodeU32 (chanUpdateFeeProportional msg) , case chanUpdateHtlcMaxMsat msg of Nothing -> BS.empty - Just m -> Prim.u64 m + Just m -> Prim.encodeU64 m ] -- | Decode channel_update message. @@ -416,17 +417,17 @@ decodeChannelUpdate bs = do Right (Just m, r) else Right (Nothing, bs10) let msg = ChannelUpdate - { chanUpdateSignature = sig - , chanUpdateChainHash = chainH - , chanUpdateShortChanId = scid - , chanUpdateTimestamp = timestamp - , chanUpdateMsgFlags = msgFlags - , chanUpdateChanFlags = chanFlags - , chanUpdateCltvExpDelta = cltvDelta - , chanUpdateHtlcMinMsat = htlcMin - , chanUpdateFeeBaseMsat = feeBase + { chanUpdateSignature = sig + , chanUpdateChainHash = chainH + , chanUpdateShortChanId = scid + , chanUpdateTimestamp = timestamp + , chanUpdateMsgFlags = msgFlags + , chanUpdateChanFlags = chanFlags + , chanUpdateCltvExpDelta = cltvDelta + , chanUpdateHtlcMinMsat = htlcMin + , chanUpdateFeeBaseMsat = feeBase , chanUpdateFeeProportional = feeProp - , chanUpdateHtlcMaxMsat = htlcMax + , chanUpdateHtlcMaxMsat = htlcMax } Right (msg, rest) @@ -469,7 +470,7 @@ encodeQueryShortChannelIds msg = do then Left EncodeLengthOverflow else Right $ mconcat [ getChainHash (queryScidsChainHash msg) - , Prim.u16 (fromIntegral $ BS.length scidData) + , Prim.encodeU16 (fromIntegral $ BS.length scidData) , scidData , TLV.encodeTlvStream (queryScidsTlvs msg) ] @@ -482,8 +483,8 @@ decodeQueryShortChannelIds bs = do (chainH, bs1) <- decodeChainHash bs (scidData, bs2) <- decodeLenPrefixed bs1 let tlvs = case TLV.decodeTlvStreamRaw bs2 of - Left _ -> TlvStream [] - Right (t, _) -> t + Left _ -> unsafeTlvStream [] + Right t -> t let msg = QueryShortChannelIds { queryScidsChainHash = chainH , queryScidsData = scidData @@ -515,8 +516,8 @@ decodeReplyShortChannelIdsEnd bs = do encodeQueryChannelRange :: QueryChannelRange -> ByteString encodeQueryChannelRange msg = mconcat [ getChainHash (queryRangeChainHash msg) - , Prim.u32 (queryRangeFirstBlock msg) - , Prim.u32 (queryRangeNumBlocks msg) + , Prim.encodeU32 (queryRangeFirstBlock msg) + , Prim.encodeU32 (queryRangeNumBlocks msg) , TLV.encodeTlvStream (queryRangeTlvs msg) ] @@ -528,8 +529,8 @@ decodeQueryChannelRange bs = do (firstBlock, bs2) <- decodeU32 bs1 (numBlocks, bs3) <- decodeU32 bs2 let tlvs = case TLV.decodeTlvStreamRaw bs3 of - Left _ -> TlvStream [] - Right (t, _) -> t + Left _ -> unsafeTlvStream [] + Right t -> t let msg = QueryChannelRange { queryRangeChainHash = chainH , queryRangeFirstBlock = firstBlock @@ -546,10 +547,10 @@ encodeReplyChannelRange msg = do then Left EncodeLengthOverflow else Right $ mconcat [ getChainHash (replyRangeChainHash msg) - , Prim.u32 (replyRangeFirstBlock msg) - , Prim.u32 (replyRangeNumBlocks msg) + , Prim.encodeU32 (replyRangeFirstBlock msg) + , Prim.encodeU32 (replyRangeNumBlocks msg) , BS.singleton (replyRangeSyncComplete msg) - , Prim.u16 (fromIntegral $ BS.length rangeData) + , Prim.encodeU16 (fromIntegral $ BS.length rangeData) , rangeData , TLV.encodeTlvStream (replyRangeTlvs msg) ] @@ -558,14 +559,14 @@ encodeReplyChannelRange msg = do decodeReplyChannelRange :: ByteString -> Either DecodeError (ReplyChannelRange, ByteString) decodeReplyChannelRange bs = do - (chainH, bs1) <- decodeChainHash bs - (firstBlock, bs2) <- decodeU32 bs1 - (numBlocks, bs3) <- decodeU32 bs2 + (chainH, bs1) <- decodeChainHash bs + (firstBlock, bs2) <- decodeU32 bs1 + (numBlocks, bs3) <- decodeU32 bs2 (syncComplete, bs4) <- decodeU8 bs3 - (rangeData, bs5) <- decodeLenPrefixed bs4 + (rangeData, bs5) <- decodeLenPrefixed bs4 let tlvs = case TLV.decodeTlvStreamRaw bs5 of - Left _ -> TlvStream [] - Right (t, _) -> t + Left _ -> unsafeTlvStream [] + Right t -> t let msg = ReplyChannelRange { replyRangeChainHash = chainH , replyRangeFirstBlock = firstBlock @@ -580,8 +581,8 @@ decodeReplyChannelRange bs = do encodeGossipTimestampFilter :: GossipTimestampFilter -> ByteString encodeGossipTimestampFilter msg = mconcat [ getChainHash (gossipFilterChainHash msg) - , Prim.u32 (gossipFilterFirstTimestamp msg) - , Prim.u32 (gossipFilterTimestampRange msg) + , Prim.encodeU32 (gossipFilterFirstTimestamp msg) + , Prim.encodeU32 (gossipFilterTimestampRange msg) ] -- | Decode gossip_timestamp_filter message. @@ -589,9 +590,9 @@ decodeGossipTimestampFilter :: ByteString -> Either DecodeError (GossipTimestampFilter, ByteString) decodeGossipTimestampFilter bs = do - (chainH, bs1) <- decodeChainHash bs - (firstTs, bs2) <- decodeU32 bs1 - (tsRange, rest) <- decodeU32 bs2 + (chainH, bs1) <- decodeChainHash bs + (firstTs, bs2) <- decodeU32 bs1 + (tsRange, rest) <- decodeU32 bs2 let msg = GossipTimestampFilter { gossipFilterChainHash = chainH , gossipFilterFirstTimestamp = firstTs diff --git a/lib/Lightning/Protocol/BOLT7/Messages.hs b/lib/Lightning/Protocol/BOLT7/Messages.hs @@ -39,7 +39,7 @@ module Lightning.Protocol.BOLT7.Messages ( import Control.DeepSeq (NFData) import Data.ByteString (ByteString) -import Data.Word (Word8, Word16, Word32, Word64) +import Data.Word (Word8, Word16, Word32) import GHC.Generics (Generic) import Lightning.Protocol.BOLT1 (TlvStream) import Lightning.Protocol.BOLT7.Types diff --git a/test/Main.hs b/test/Main.hs @@ -3,10 +3,9 @@ module Main where import qualified Data.ByteString as BS -import qualified Data.ByteString.Base16 as B16 import Data.Maybe (fromJust) -import Data.Word (Word8, Word16, Word32) -import Lightning.Protocol.BOLT1 (TlvStream(..)) +import Data.Word (Word16, Word32) +import Lightning.Protocol.BOLT1 (TlvStream, unsafeTlvStream) import Lightning.Protocol.BOLT7 import Test.Tasty import Test.Tasty.HUnit @@ -64,7 +63,7 @@ testAlias = fromJust $ alias (BS.pack $ replicate 32 0x00) -- | Empty TLV stream for messages. emptyTlvs :: TlvStream -emptyTlvs = TlvStream [] +emptyTlvs = unsafeTlvStream [] -- | Empty feature bits. emptyFeatures :: FeatureBits