bolt9

Lightning feature flags, per BOLT #9.
git clone git://git.ppad.tech/bolt9.git
Log | Files | Refs | README | LICENSE

commit fdb0dd501abf29b75c0a93526c4a531f5f125dba
parent 78c23f1935dceff739ae99086e579b6bfe1afd00
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 16:05:17 +0400

Add Haddock documentation and examples to public API

Enhance the main BOLT9 module with comprehensive documentation:
- Add module overview explaining the library's purpose
- Include Quick Start section with working code examples
- Document bit numbering conventions
- Add section descriptions for all export groups

Add doctest examples throughout:
- Types: empty, set, member, requiredBit, optionalBit
- Features: featureByBit, featureByName
- Codec: setBit, clearBit, testBit, setFeature, hasFeature, listFeatures
- Validate: validateLocal, validateRemote

All submodules already have OPTIONS_HADDOCK prune.

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

Diffstat:
Mlib/Lightning/Protocol/BOLT9.hs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mlib/Lightning/Protocol/BOLT9/Codec.hs | 34++++++++++++++++++++++++++++++++++
Mlib/Lightning/Protocol/BOLT9/Features.hs | 12++++++++++++
Mlib/Lightning/Protocol/BOLT9/Types.hs | 27+++++++++++++++++++++++++--
Mlib/Lightning/Protocol/BOLT9/Validate.hs | 15+++++++++++++++
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