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