bolt9

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

commit 83eda90bde6eb40729825472d0e28f2025e13f9f
parent 7a479be882040996a46d0b26d99b0e87eba25bb7
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:32:47 +0400

docs: README, CHANGELOG and AGENTS.md for 0.1.0

The README example is checked against the new API, states the spec
commit implemented and lists what isn't implemented. Drops the false
claim of an LLVM flag from AGENTS.md.

Diffstat:
MAGENTS.md | 1-
MCHANGELOG | 29++++++++++++++++++++++++++++-
MREADME.md | 120+++++++++++++++++++++++++++++++++++++++++--------------------------------------
3 files changed, 90 insertions(+), 60 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -139,5 +139,4 @@ When planning work: The flake.nix follows ppad conventions: - Uses ppad-nixpkgs as base - Follows references to avoid duplication -- Supports LLVM backend via cabal flag - Provides devShell with ghc, cabal, cc, llvm diff --git a/CHANGELOG b/CHANGELOG @@ -1,4 +1,31 @@ # Changelog -- 0.0.1 (unreleased) +- 0.1.0 (UNRELEASED) + * Breaking: the API is redesigned and now lives entirely in + Lightning.Protocol.BOLT9, with snake_case names. The submodules, + the bit-index newtypes, the name-based feature lookups and the + duplicate bit operations are gone. + + * Breaking: features are a closed enumeration matching the BOLT #9 + table at lightning/bolts@1aadb719, which adds zero_fee_commitments, + option_splice and option_onion_messages_only_channels. + + * Breaking: contexts follow the spec. C- and C+ are no longer + contexts, and channel_type is validated as a BOLT #2 channel type + (the new ChannelType) rather than as a feature vector. + + * FeatureVector preserves received bytes exactly for re-encoding, + compares equal regardless of leading zero bytes, and gains union. + + * Fixes remote validation, which skipped the dependency check, let + bits above 65535 wrap onto known features, and applied the init + rules in every context. Local validation now rejects unknown bits, + and a dependency on an ASSUMED feature counts as met. + + * Fixes setting a feature, which could leave both bits of its pair + set. + + * Drops the dependency on containers. + +- 0.0.1 (2026-04-18) * Initial release. diff --git a/README.md b/README.md @@ -4,22 +4,11 @@ ![](https://img.shields.io/badge/license-MIT-brightgreen) [![](https://img.shields.io/badge/haddock-bolt9-lightblue)](https://docs.ppad.tech/bolt9) -A Haskell implementation of Lightning Network feature flags (BOLT #9), -providing type-safe feature vectors with validation and wire format -encoding. - -## 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: - -- **Even bits** (0, 2, 4, ...) indicate *required* support -- **Odd bits** (1, 3, 5, ...) indicate *optional* support - -For example, `basic_mpp` (multi-part payments) uses bits 16/17. A node -setting bit 16 requires peers to support it; bit 17 indicates optional -support. +A pure Haskell implementation of Lightning Network feature flags, per +[BOLT #9][bolt9]: an abstract feature vector type that preserves its +wire bytes, the table of known features with their contexts and +dependencies, sender- and receiver-side validation for each context, +and BOLT #2 channel types. ## Usage @@ -27,48 +16,39 @@ A sample GHCi session: ``` > :set -XOverloadedStrings - > import Lightning.Protocol.BOLT9 - > import Data.Maybe (fromJust) > - > -- Look up features by name - > let mpp = fromJust $ featureByName "basic_mpp" - > featureBaseBit mpp - 16 - > featureDependencies mpp - ["payment_secret"] + > -- import qualified + > import qualified Lightning.Protocol.BOLT9 as BOLT9 > - > -- Build a feature vector with optional basic_mpp support - > let fv = setFeature mpp False empty - > hasFeature mpp fv - Just False + > -- build a feature vector for an init message, with dependencies + > let opt = BOLT9.Optional + > let fv = BOLT9.set_feature_with_deps BOLT9.OptionZeroconf opt BOLT9.empty + > BOLT9.list_features fv + [(OptionScidAlias,Optional),(OptionZeroconf,Optional)] > - > -- Validation catches missing dependencies - > validateLocal Init fv - Left [MissingDependency "basic_mpp" "payment_secret"] - > - > -- Add the dependency - > let ps = fromJust $ featureByName "payment_secret" - > let fv' = setFeature ps False $ setFeature mpp False empty - > validateLocal Init fv' + > -- check it against the sender's rules, then encode it + > BOLT9.validate_local BOLT9.InitContext fv Right () + > BOLT9.render fv + "\b\128\NUL\NUL\NUL\NUL\NUL" > - > -- Render to wire format (compact, no leading zeros) - > render fv' - "\STX\128" - > - > -- Parse from wire format - > let fv'' = parse "\STX\128" - > listFeatures fv'' - [(Feature {featureName = "payment_secret", ...},False), - (Feature {featureName = "basic_mpp", ...},False)] + > -- a received vector keeps its exact bytes + > let theirs = BOLT9.parse "\NUL\STX\NUL\NUL" + > BOLT9.render theirs + "\NUL\STX\NUL\NUL" + > BOLT9.test_feature BOLT9.BasicMpp theirs + Just Optional > - > -- Remote validation: unknown optional bits are OK - > validateRemote Init (setBit 201 empty) + > -- check it against the receiver's rules for init + > BOLT9.validate_remote BOLT9.InitContext theirs Right () + > BOLT9.validate_remote BOLT9.InitContext (BOLT9.set_bit 100 theirs) + Left (UnknownBit 100 :| []) > - > -- But unknown required bits are rejected - > validateRemote Init (setBit 200 empty) - Left [UnknownRequiredBit 200] + > -- channel types follow BOLT #2 + > let ct = BOLT9.ChannelType BOLT9.BasicAnchors False True + > BOLT9.channel_type (BOLT9.channel_type_features ct) == Just ct + True ``` ## Documentation @@ -76,10 +56,36 @@ A sample GHCi session: Haddocks (API documentation, etc.) are hosted at [docs.ppad.tech/bolt9](https://docs.ppad.tech/bolt9). +## Specification + +The library implements BOLT #9 as of [lightning/bolts@1aadb719][spec] +(2026-09-21), a copy of which lives in `etc/09-features.md`, together +with the feature-vector rules of BOLTs #1, #2, #4, #7 and #11 for each +context. + +Not implemented: + +* Feature fields of BOLT #12 offers, invoice requests and invoices, + which the BOLT #9 table assigns no context. +* The BOLT #11 encoding of features as 5-bit groups. +* BOLT #1's notions of *offered* and *negotiated* features. +* BOLT #2's rule against `option_scid_alias` in the channel type of an + announced channel, which depends on `channel_flags`. +* BOLT #4's check that a payment uses only features in + `allowed_features`. +* The `C-` and `C+` presentation rules for `channel_announcement`. No + feature in the table uses them, or `C`. +* The receiving requirements specific to each feature, which BOLT #9 + defers to the linked sections of the other BOLTs. + +BOLT #2 defines `zero_fee_commitments` as a basic channel type, though +the BOLT #9 table doesn't list it in the `T` context; `ChannelType` +follows BOLT #2. + ## Performance -The aim is best-in-class performance for feature flag handling. -Benchmarks are available under `bench/` and can be run with: +The aim is best-in-class performance for pure Haskell code. Run the +benchmark suite with: ``` $ cabal bench @@ -87,7 +93,8 @@ $ cabal bench ## Security -This is a pre-release library and makes no claims about security whatsoever. +This is a pre-release library and makes no claims about security +whatsoever. ## Development @@ -106,10 +113,7 @@ $ cabal repl ppad-bolt9 to get a REPL for the main library. -## Attribution - -This library implements the Lightning Network BOLT #9 specification: -https://github.com/lightning/bolts/blob/master/09-features.md - +[bolt9]: https://github.com/lightning/bolts/blob/master/09-features.md +[spec]: https://github.com/lightning/bolts/blob/1aadb719b4007c4cea0ba6e36b08c4fb53788dee/09-features.md [nixos]: https://nixos.org/ [flake]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html