commit ca374d4816e6f65baf08012e438e318ad102da42
parent 78c23f1935dceff739ae99086e579b6bfe1afd00
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 25 Jan 2026 16:06:30 +0400
Merge impl/api: public API documentation
Adds comprehensive Haddock documentation:
- Module overview with quick start guide
- Section headers for all export groups
- Doctest examples for key functions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
5 files changed, 143 insertions(+), 4 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT9.hs b/lib/Lightning/Protocol/BOLT9.hs
@@ -10,19 +10,68 @@
--
-- Feature flags for the Lightning Network, per
-- [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md).
+--
+-- == Overview
+--
+-- BOLT #9 defines feature flags that Lightning nodes advertise to indicate
+-- support for optional protocol features. Features are represented as bit
+-- positions in a variable-length bit vector, where even bits indicate
+-- required (compulsory) support and odd bits indicate optional support.
+--
+-- This library provides:
+--
+-- * Type-safe feature vectors with efficient bit manipulation
+-- * A complete table of known features from the BOLT #9 specification
+-- * Validation for both locally-created and remotely-received vectors
+-- * Context-aware validation (init, node_announcement, invoice, etc.)
+--
+-- == Quick Start
+--
+-- Create a feature vector and set some features:
+--
+-- >>> import Lightning.Protocol.BOLT9
+-- >>> let Just mpp = featureByName "basic_mpp"
+-- >>> let fv = setFeature mpp False empty -- optional support
+-- >>> hasFeature mpp fv
+-- Just False
+--
+-- Validate a feature vector for a specific context:
+--
+-- >>> validateLocal Init fv
+-- Left [MissingDependency "basic_mpp" "payment_secret"]
+--
+-- Fix by adding the dependency:
+--
+-- >>> let Just ps = featureByName "payment_secret"
+-- >>> let fv' = setFeature ps False (setFeature mpp False empty)
+-- >>> validateLocal Init fv'
+-- Right ()
+--
+-- == Bit Numbering
+--
+-- Features use paired bits: even bits (0, 2, 4, ...) indicate required
+-- support, while odd bits (1, 3, 5, ...) indicate optional support.
+-- For example, @basic_mpp@ uses bit 16 (required) and 17 (optional).
+--
+-- A node setting bit 16 requires all peers to support @basic_mpp@.
+-- A node setting bit 17 indicates optional support (peers without it
+-- may still connect).
module Lightning.Protocol.BOLT9 (
-- * Context
+ -- | Contexts specify where feature flags appear in the protocol.
Context(..)
, isChannelContext
, channelParity
-- * Bit indices
+ -- | Low-level bit index types for direct bit manipulation.
, BitIndex
, unBitIndex
, bitIndex
-- * Required/optional bits
+ -- | Type-safe wrappers ensuring correct parity.
, RequiredBit
, unRequiredBit
, requiredBit
@@ -34,6 +83,7 @@ module Lightning.Protocol.BOLT9 (
, optionalFromBitIndex
-- * Feature vectors
+ -- | The core feature vector type and basic operations.
, FeatureVector
, unFeatureVector
, FV.empty
@@ -43,26 +93,31 @@ module Lightning.Protocol.BOLT9 (
, member
-- * Known features
+ -- | The BOLT #9 feature table and lookup functions.
, Feature(..)
, featureByBit
, featureByName
, knownFeatures
-- * Parsing and rendering
+ -- | Wire format conversion.
, parse
, render
- -- * Codec bit operations
+ -- * Low-level bit operations
+ -- | Direct bit manipulation by index.
, Codec.setBit
, Codec.clearBit
, Codec.testBit
- -- * Codec feature operations
+ -- * Feature operations
+ -- | High-level operations using 'Feature' values.
, setFeature
, hasFeature
, listFeatures
-- * Validation
+ -- | Validate feature vectors for correctness.
, ValidationError(..)
, validateLocal
, validateRemote
diff --git a/lib/Lightning/Protocol/BOLT9/Codec.hs b/lib/Lightning/Protocol/BOLT9/Codec.hs
@@ -49,16 +49,27 @@ render = BS.dropWhile (== 0) . unFeatureVector
-- Bit operations -------------------------------------------------------------
-- | Set a bit by raw index.
+--
+-- >>> setBit 17 empty
+-- FeatureVector {unFeatureVector = "\STX"}
setBit :: Word16 -> FeatureVector -> FeatureVector
setBit !idx = set (bitIndex idx)
{-# INLINE setBit #-}
-- | Clear a bit by raw index.
+--
+-- >>> clearBit 17 (setBit 17 empty)
+-- FeatureVector {unFeatureVector = ""}
clearBit :: Word16 -> FeatureVector -> FeatureVector
clearBit !idx = clear (bitIndex idx)
{-# INLINE clearBit #-}
-- | Test if a bit is set.
+--
+-- >>> testBit 17 (setBit 17 empty)
+-- True
+-- >>> testBit 16 (setBit 17 empty)
+-- False
testBit :: Word16 -> FeatureVector -> Bool
testBit !idx = member (bitIndex idx)
{-# INLINE testBit #-}
@@ -69,6 +80,13 @@ testBit !idx = member (bitIndex idx)
--
-- If the Bool is True, sets the required (even) bit.
-- If the Bool is False, sets the optional (odd) bit.
+--
+-- >>> import Data.Maybe (fromJust)
+-- >>> let mpp = fromJust (featureByName "basic_mpp")
+-- >>> setFeature mpp False empty -- set optional bit (17)
+-- FeatureVector {unFeatureVector = "\STX"}
+-- >>> setFeature mpp True empty -- set required bit (16)
+-- FeatureVector {unFeatureVector = "\SOH"}
setFeature :: Feature -> Bool -> FeatureVector -> FeatureVector
setFeature !f !required = setBit targetBit
where
@@ -83,6 +101,15 @@ setFeature !f !required = setBit targetBit
-- * @Just True@ if the required (even) bit is set
-- * @Just False@ if the optional (odd) bit is set (and required is not)
-- * @Nothing@ if neither bit is set
+--
+-- >>> import Data.Maybe (fromJust)
+-- >>> let mpp = fromJust (featureByName "basic_mpp")
+-- >>> hasFeature mpp (setFeature mpp False empty)
+-- Just False
+-- >>> hasFeature mpp (setFeature mpp True empty)
+-- Just True
+-- >>> hasFeature mpp empty
+-- Nothing
hasFeature :: Feature -> FeatureVector -> Maybe Bool
hasFeature !f !fv
| testBit baseBit fv = Just True -- required
@@ -96,6 +123,13 @@ hasFeature !f !fv
--
-- Returns pairs of (Feature, Bool) where the Bool indicates if the
-- required (even) bit is set (True) or the optional (odd) bit (False).
+--
+-- >>> import Data.Maybe (fromJust)
+-- >>> let mpp = fromJust (featureByName "basic_mpp")
+-- >>> let ps = fromJust (featureByName "payment_secret")
+-- >>> let fv = setFeature mpp False (setFeature ps True empty)
+-- >>> map (\(f, r) -> (featureName f, r)) (listFeatures fv)
+-- [("payment_secret",True),("basic_mpp",False)]
listFeatures :: FeatureVector -> [(Feature, Bool)]
listFeatures !fv = foldr check [] knownFeatures
where
diff --git a/lib/Lightning/Protocol/BOLT9/Features.hs b/lib/Lightning/Protocol/BOLT9/Features.hs
@@ -81,11 +81,23 @@ knownFeatures = [
-- | Look up a feature by bit number.
--
-- Accepts either the even (compulsory) or odd (optional) bit of the pair.
+--
+-- >>> fmap featureName (featureByBit 16)
+-- Just "basic_mpp"
+-- >>> fmap featureName (featureByBit 17) -- odd bit also works
+-- Just "basic_mpp"
+-- >>> featureByBit 999
+-- Nothing
featureByBit :: Word16 -> Maybe Feature
featureByBit !bit =
let baseBit = bit - (bit `mod` 2) -- round down to even
in find (\f -> featureBaseBit f == baseBit) knownFeatures
-- | Look up a feature by its canonical name.
+--
+-- >>> fmap featureBaseBit (featureByName "basic_mpp")
+-- Just 16
+-- >>> featureByName "nonexistent"
+-- Nothing
featureByName :: String -> Maybe Feature
featureByName !name = find (\f -> featureName f == name) knownFeatures
diff --git a/lib/Lightning/Protocol/BOLT9/Types.hs b/lib/Lightning/Protocol/BOLT9/Types.hs
@@ -118,7 +118,12 @@ newtype RequiredBit = RequiredBit { unRequiredBit :: Word16 }
instance NFData RequiredBit
-- | Smart constructor for 'RequiredBit'. Returns 'Nothing' if the bit
--- index is odd.
+-- index is odd.
+--
+-- >>> requiredBit 16
+-- Just (RequiredBit {unRequiredBit = 16})
+-- >>> requiredBit 17
+-- Nothing
requiredBit :: Word16 -> Maybe RequiredBit
requiredBit !w
| w B..&. 1 == 0 = Just (RequiredBit w)
@@ -139,7 +144,12 @@ newtype OptionalBit = OptionalBit { unOptionalBit :: Word16 }
instance NFData OptionalBit
-- | Smart constructor for 'OptionalBit'. Returns 'Nothing' if the bit
--- index is even.
+-- index is even.
+--
+-- >>> optionalBit 17
+-- Just (OptionalBit {unOptionalBit = 17})
+-- >>> optionalBit 16
+-- Nothing
optionalBit :: Word16 -> Maybe OptionalBit
optionalBit !w
| w B..&. 1 == 1 = Just (OptionalBit w)
@@ -164,6 +174,9 @@ newtype FeatureVector = FeatureVector { unFeatureVector :: ByteString }
instance NFData FeatureVector
-- | The empty feature vector (no features set).
+--
+-- >>> empty
+-- FeatureVector {unFeatureVector = ""}
empty :: FeatureVector
empty = FeatureVector BS.empty
{-# INLINE empty #-}
@@ -174,6 +187,11 @@ fromByteString = FeatureVector
{-# INLINE fromByteString #-}
-- | Set a bit in the feature vector.
+--
+-- >>> set (bitIndex 0) empty
+-- FeatureVector {unFeatureVector = "\SOH"}
+-- >>> set (bitIndex 8) empty
+-- FeatureVector {unFeatureVector = "\SOH\NUL"}
set :: BitIndex -> FeatureVector -> FeatureVector
set (BitIndex idx) (FeatureVector bs) =
let byteIdx = fromIntegral idx `div` 8
@@ -211,6 +229,11 @@ clear (BitIndex idx) (FeatureVector bs)
{-# INLINE clear #-}
-- | Test if a bit is set in the feature vector.
+--
+-- >>> member (bitIndex 0) (set (bitIndex 0) empty)
+-- True
+-- >>> member (bitIndex 1) (set (bitIndex 0) empty)
+-- False
member :: BitIndex -> FeatureVector -> Bool
member (BitIndex idx) (FeatureVector bs)
| BS.null bs = False
diff --git a/lib/Lightning/Protocol/BOLT9/Validate.hs b/lib/Lightning/Protocol/BOLT9/Validate.hs
@@ -66,6 +66,15 @@ instance NFData ValidationError
-- * All set features are valid for the given context
-- * All dependencies of set features are also set
-- * C- context forces odd bits only, C+ forces even bits only
+--
+-- >>> import Data.Maybe (fromJust)
+-- >>> import Lightning.Protocol.BOLT9.Codec (setFeature)
+-- >>> let mpp = fromJust (featureByName "basic_mpp")
+-- >>> let ps = fromJust (featureByName "payment_secret")
+-- >>> validateLocal Init (setFeature mpp False empty)
+-- Left [MissingDependency "basic_mpp" "payment_secret"]
+-- >>> validateLocal Init (setFeature mpp False (setFeature ps False empty))
+-- Right ()
validateLocal :: Context -> FeatureVector -> Either [ValidationError] ()
validateLocal !ctx !fv =
let errs = bothBitsErrors fv
@@ -148,6 +157,12 @@ parityErrors !ctx !fv = case channelParity ctx of
-- * Unknown even (required) bits are errors
-- * If both bits of a pair are set, treat as required (not an error)
-- * Context restrictions still apply for known features
+--
+-- >>> import Lightning.Protocol.BOLT9.Codec (setBit)
+-- >>> validateRemote Init (setBit 999 empty) -- unknown odd bit: ok
+-- Right ()
+-- >>> validateRemote Init (setBit 998 empty) -- unknown even bit: error
+-- Left [UnknownRequiredBit 998]
validateRemote :: Context -> FeatureVector -> Either [ValidationError] ()
validateRemote !ctx !fv =
let errs = unknownRequiredErrors fv