bolt7

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

commit 7ce163855ecf9ddc4fd69a0572d4310a9ca8545c
parent 9d7951bead7f00e5f4c1ad2067cb1f543b7af9d0
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 15:12:52 +0400

Phase 2: Complete Types module

Add ShortChannelId helpers:
- mkShortChannelId: construct from block/tx/output components
- formatScid: human-readable "539268x845x1" format

Add Bitcoin mainnet chain hash constant (mainnetChainHash).

Add Ord instance for NodeId (lexicographic comparison required by
spec for channel announcements where node_id_1 < node_id_2).

Add tests for new functionality:
- mkShortChannelId roundtrip
- formatScid formatting
- mainnetChainHash length
- NodeId ordering

All 31 tests pass.

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

Diffstat:
Mlib/Lightning/Protocol/BOLT7/Types.hs | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtest/Main.hs | 22++++++++++++++++++++++
2 files changed, 79 insertions(+), 3 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT7/Types.hs b/lib/Lightning/Protocol/BOLT7/Types.hs @@ -14,12 +14,15 @@ module Lightning.Protocol.BOLT7.Types ( ChainHash , chainHash , getChainHash + , mainnetChainHash , ShortChannelId , shortChannelId + , mkShortChannelId , getShortChannelId , scidBlockHeight , scidTxIndex , scidOutputIndex + , formatScid , ChannelId , channelId , getChannelId @@ -81,10 +84,10 @@ module Lightning.Protocol.BOLT7.Types ( ) where import Control.DeepSeq (NFData) -import Data.Bits (shiftL, (.|.)) +import Data.Bits (shiftL, shiftR, (.&.), (.|.)) import Data.ByteString (ByteString) import qualified Data.ByteString as BS -import Data.Word (Word16, Word32, Word64) +import Data.Word (Word8, Word16, Word32, Word64) import GHC.Generics (Generic) -- Constants ------------------------------------------------------------------- @@ -159,6 +162,18 @@ chainHash !bs | otherwise = Nothing {-# INLINE chainHash #-} +-- | Bitcoin mainnet chain hash (genesis block hash, little-endian). +-- +-- This is the double-SHA256 of the mainnet genesis block header, reversed +-- to little-endian byte order as used in the protocol. +mainnetChainHash :: ChainHash +mainnetChainHash = ChainHash $ BS.pack + [ 0x6f, 0xe2, 0x8c, 0x0a, 0xb6, 0xf1, 0xb3, 0x72 + , 0xc1, 0xa6, 0xa2, 0x46, 0xae, 0x63, 0xf7, 0x4f + , 0x93, 0x1e, 0x83, 0x65, 0xe1, 0x5a, 0x08, 0x9c + , 0x68, 0xd6, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00 + ] + -- | Short channel ID (8 bytes): block height (3) + tx index (3) + output (2). newtype ShortChannelId = ShortChannelId { getShortChannelId :: ByteString } deriving (Eq, Show, Generic) @@ -172,6 +187,29 @@ shortChannelId !bs | otherwise = Nothing {-# INLINE shortChannelId #-} +-- | Construct ShortChannelId from components. +-- +-- Block height and tx index are truncated to 24 bits. +-- +-- >>> mkShortChannelId 539268 845 1 +-- ShortChannelId {getShortChannelId = "\NUL\131\132\NUL\ETX-\NUL\SOH"} +mkShortChannelId + :: Word32 -- ^ Block height (24 bits) + -> Word32 -- ^ Transaction index (24 bits) + -> Word16 -- ^ Output index + -> ShortChannelId +mkShortChannelId !block !txIdx !outIdx = ShortChannelId $ BS.pack + [ fromIntegral ((block `shiftR` 16) .&. 0xff) :: Word8 + , fromIntegral ((block `shiftR` 8) .&. 0xff) + , fromIntegral (block .&. 0xff) + , fromIntegral ((txIdx `shiftR` 16) .&. 0xff) + , fromIntegral ((txIdx `shiftR` 8) .&. 0xff) + , fromIntegral (txIdx .&. 0xff) + , fromIntegral ((outIdx `shiftR` 8) .&. 0xff) + , fromIntegral (outIdx .&. 0xff) + ] +{-# INLINE mkShortChannelId #-} + -- | Extract block height from short channel ID (first 3 bytes, big-endian). scidBlockHeight :: ShortChannelId -> Word32 scidBlockHeight (ShortChannelId bs) = @@ -198,6 +236,19 @@ scidOutputIndex (ShortChannelId bs) = in (b6 `shiftL` 8) .|. b7 {-# INLINE scidOutputIndex #-} +-- | Format short channel ID as human-readable string. +-- +-- Uses the standard "block x tx x output" notation. +-- +-- >>> formatScid (mkShortChannelId 539268 845 1) +-- "539268x845x1" +formatScid :: ShortChannelId -> String +formatScid scid = + show (scidBlockHeight scid) ++ "x" ++ + show (scidTxIndex scid) ++ "x" ++ + show (scidOutputIndex scid) +{-# INLINE formatScid #-} + -- | Channel ID (32 bytes). newtype ChannelId = ChannelId { getChannelId :: ByteString } deriving (Eq, Show, Generic) @@ -240,8 +291,11 @@ point !bs {-# INLINE point #-} -- | Node ID (33 bytes, same as compressed public key). +-- +-- Has Ord instance for lexicographic comparison (required by spec for +-- channel announcements where node_id_1 < node_id_2). newtype NodeId = NodeId { getNodeId :: ByteString } - deriving (Eq, Show, Generic) + deriving (Eq, Ord, Show, Generic) instance NFData NodeId diff --git a/test/Main.hs b/test/Main.hs @@ -87,6 +87,17 @@ type_tests = testGroup "Types" [ let scid = fromJust $ shortChannelId (BS.pack [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]) scidOutputIndex scid @?= 0xdef0 + , testCase "mkShortChannelId roundtrip" $ do + let scid = mkShortChannelId 539268 845 1 + scidBlockHeight scid @?= 539268 + scidTxIndex scid @?= 845 + scidOutputIndex scid @?= 1 + , testCase "formatScid" $ do + let scid = mkShortChannelId 539268 845 1 + formatScid scid @?= "539268x845x1" + , testCase "formatScid zero values" $ do + let scid = mkShortChannelId 0 0 0 + formatScid scid @?= "0x0x0" ] , testGroup "Smart constructors" [ testCase "chainHash rejects wrong length" $ do @@ -102,6 +113,17 @@ type_tests = testGroup "Types" [ point (BS.replicate 32 0x00) @?= Nothing point (BS.replicate 34 0x00) @?= Nothing ] + , testGroup "Constants" [ + testCase "mainnetChainHash has correct length" $ do + BS.length (getChainHash mainnetChainHash) @?= 32 + ] + , testGroup "NodeId ordering" [ + testCase "NodeId Ord is lexicographic" $ do + let n1 = fromJust $ nodeId (BS.pack $ 0x02 : replicate 32 0x00) + n2 = fromJust $ nodeId (BS.pack $ 0x03 : replicate 32 0x00) + n1 < n2 @?= True + n2 < n1 @?= False + ] ] -- Channel Announcement Tests --------------------------------------------------