commit 564b7b53f6dfc7830c389793758fae9118c78b9f
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 25 Jan 2026 15:42:49 +0400
Initial project skeleton for ppad-bolt9
Feature flags library per BOLT #9 specification.
Project structure:
- lib/Lightning/Protocol/BOLT9.hs - main library module
- test/Main.hs - tasty test skeleton
- bench/Main.hs, bench/Weight.hs - criterion/weigh benchmarks
- etc/09-features.md - cached BOLT9 specification
- flake.nix - nix flake for dependency/build management
- ppad-bolt9.cabal - cabal package definition
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
11 files changed, 523 insertions(+), 0 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -0,0 +1,143 @@
+# ppad-bolt9
+
+Haskell implementation of BOLT #9 (Lightning Network feature flags).
+
+Specification: https://github.com/lightning/bolts/blob/master/09-features.md
+
+## Project Structure
+
+- `lib/` - library source (Lightning.Protocol.BOLT9)
+- `test/` - tests (tasty + tasty-hunit)
+- `bench/` - benchmarks (criterion for timing, weigh for allocations)
+- `etc/` - cached BOLT specification
+- `flake.nix` - nix flake for dependency and build management
+- `ppad-bolt9.cabal` - cabal package definition
+- `CLAUDE.md` / `AGENTS.md` - keep these in sync
+
+## Build and Test
+
+Enter devshell and use cabal:
+
+```
+nix develop
+cabal build
+cabal test
+cabal bench
+```
+
+Do not use stack. All dependency and build management via nix.
+
+## Dependencies
+
+### ppad libraries (use freely)
+
+Use ppad libraries (github.com/ppad-tech, git.ppad.tech) liberally.
+
+### External libraries
+
+Use only minimal external dependencies. Prefer GHC's core/boot libraries
+(base, bytestring, primitive, etc.).
+
+**Ask for explicit confirmation before adding any library outside of:**
+- GHC boot/core libraries
+- ppad-* libraries
+- Test dependencies (tasty, QuickCheck, etc. for test-suite only)
+- Benchmark dependencies (criterion, weigh for benchmark only)
+
+## Code Style
+
+### Performance
+
+- Use strictness annotations (BangPatterns) liberally
+- Prefer UNPACK for strict record fields
+- Use MagicHash, UnboxedTuples, GHC.Exts for hot paths
+- Do not rely on UNBOX pragmas; implement primitives directly with
+ MagicHash and GHC.Exts when needed
+- Use INLINE pragmas for small functions
+- Refer to ppad-sha256 and ppad-fixed for low-level patterns
+
+### Type safety
+
+- Encode invariants into the type system
+- Use newtypes liberally
+- Use ADTs to make illegal states unrepresentable
+- Prefer smart constructors that validate inputs
+
+### Safety
+
+- Never use partial Prelude functions (head, tail, !!, etc.)
+- Avoid brittle partials in tests too (e.g., unchecked indexing). Prefer
+ bounds checks or total helpers even in test code.
+- Avoid non-exhaustive pattern matches and unsafe behavior; use total
+ helpers and make all constructors explicit.
+- Use Maybe/Either for fallible operations
+- Validate all inputs at system boundaries
+
+### Formatting
+
+- Keep lines under 80 characters
+- Use Haskell2010
+- Module header with copyright, license, maintainer
+- OPTIONS_HADDOCK prune for public modules
+- Haddock examples for exported functions
+
+## Testing
+
+Use tasty to wrap all tests:
+- tasty-hunit for unit tests with known vectors
+- tasty-quickcheck for property-based tests
+- Source test vectors from specifications (RFC, BOLT spec, Wycheproof, etc.)
+
+Property tests should enforce invariants that can't be encoded in types.
+
+## Benchmarking
+
+Always maintain benchmark suites:
+- `bench/Main.hs` - criterion for wall-time benchmarks
+- `bench/Weight.hs` - weigh for allocation tracking
+
+Define NFData instances for types that need benchmarking.
+
+## Git Workflow
+
+- Feature branches for development; commit freely there
+- Logical, atomic commits on feature branches
+- Master should be mostly merge commits
+- Merge to master with `--no-ff` after validation
+- Always build and test before creating a merge commit
+- Write detailed merge commit messages summarising changes
+
+### Worktree flow (for planned work)
+
+When starting work on an implementation plan:
+
+```
+git worktree add ./impl-<desc> -b impl/<desc> master
+# work in that worktree
+# merge to master when complete
+git worktree remove ./impl-<desc>
+```
+
+### Commits
+
+- Higher-level descriptions in merge commits
+- Never update git config
+- Never use destructive git commands (push --force, hard reset) without
+ explicit request
+- Never skip hooks unless explicitly requested
+
+## Planning
+
+When planning work:
+- Highlight which steps can be done independently
+- Consider forking subagents for concurrent work on independent steps
+- Write implementation plans to `plans/IMPL<n>.md` if the project uses
+ this convention
+
+## Flake Structure
+
+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
@@ -0,0 +1,4 @@
+# Changelog
+
+- 0.0.1 (unreleased)
+ * Initial release.
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2025 Jared Tobin
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
@@ -0,0 +1,57 @@
+# ppad-bolt9
+
+A Haskell implementation of Lightning Network feature flags (BOLT #9).
+
+## Usage
+
+A sample GHCi session:
+
+```
+ > import Lightning.Protocol.BOLT9
+ > -- TODO
+```
+
+## Documentation
+
+Haddocks (API documentation, etc.) are hosted at
+[docs.ppad.tech/bolt9](https://docs.ppad.tech/bolt9).
+
+## Performance
+
+The aim is best-in-class performance for feature flag handling.
+Benchmarks are available under `bench/` and can be run with:
+
+```
+$ cabal bench
+```
+
+## Security
+
+This library targets safe, validated handling of Lightning BOLT #9
+feature flags. If you discover any vulnerabilities, please disclose
+them via security@ppad.tech.
+
+## Development
+
+You'll require [Nix][nixos] with [flake][flake] support enabled. Enter a
+development shell with:
+
+```
+$ nix develop
+```
+
+Then do e.g.:
+
+```
+$ 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
+
+[nixos]: https://nixos.org/
+[flake]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html
diff --git a/bench/Main.hs b/bench/Main.hs
@@ -0,0 +1,8 @@
+module Main where
+
+import Criterion.Main
+
+main :: IO ()
+main = defaultMain [
+ -- TODO
+ ]
diff --git a/bench/Weight.hs b/bench/Weight.hs
@@ -0,0 +1,8 @@
+module Main where
+
+import Weigh
+
+main :: IO ()
+main = mainWith $ do
+ pure ()
+ -- TODO
diff --git a/etc/09-features.md b/etc/09-features.md
@@ -0,0 +1,113 @@
+# BOLT #9: Assigned Feature Flags
+
+This document tracks the assignment of `features` flags in the `init`
+message ([BOLT #1](01-messaging.md)), as well as `features` fields in
+the `channel_announcement` and `node_announcement` messages ([BOLT
+#7](07-routing-gossip.md)). The flags are tracked separately, since
+new flags will likely be added over time.
+
+Some features were introduced and became so widespread they are `ASSUMED` to be present by all nodes, and can be safely ignored (and the semantics are only defined in prior revisions of this spec).
+
+Flags are numbered from the least-significant bit, at bit 0 (i.e. 0x1,
+an _even_ bit). They are generally assigned in pairs so that features
+can be introduced as optional (_odd_ bits) and later upgraded to be compulsory
+(_even_ bits), which will be refused by outdated nodes:
+see [BOLT #1: The `init` Message](01-messaging.md#the-init-message).
+
+Some features don't make sense on a per-channels or per-node basis, so
+each feature defines how it is presented in those contexts. Some
+features may be required for opening a channel, but not a requirement
+for use of the channel, so the presentation of those features depends
+on the feature itself.
+
+The Context column decodes as follows:
+
+* `I`: presented in the `init` message.
+* `N`: presented in the `node_announcement` messages
+* `C`: presented in the `channel_announcement` message.
+* `C-`: presented in the `channel_announcement` message, but always odd (optional).
+* `C+`: presented in the `channel_announcement` message, but always even (required).
+* `9`: presented in [BOLT 11](11-payment-encoding.md) invoices.
+* `B`: presented in the `allowed_features` field of a blinded path.
+* `T`: used in the `channel_type` field [when opening channels](02-peer-protocol.md#the-open_channel-message).
+
+| Bits | Name | Description | Context | Dependencies | Link |
+|-------|-----------------------------------|-----------------------------------------------------------|----------|-----------------------------|-----------------------------------------------------------------------|
+| 0/1 | `option_data_loss_protect` | ASSUMED | | | |
+| 4/5 | `option_upfront_shutdown_script` | Commits to a shutdown scriptpubkey when opening channel | IN | | [BOLT #2][bolt02-open] |
+| 6/7 | `gossip_queries` | Peer has useful gossip to share | | | |
+| 8/9 | `var_onion_optin` | ASSUMED | | | |
+| 10/11 | `gossip_queries_ex` | Gossip queries can include additional information | IN | | [BOLT #7][bolt07-query] |
+| 12/13 | `option_static_remotekey` | ASSUMED | | | |
+| 14/15 | `payment_secret` | ASSUMED | | | [Routing Onion Specification][bolt04] |
+| 16/17 | `basic_mpp` | Node can receive basic multi-part payments | IN9 | `payment_secret` | [BOLT #4][bolt04-mpp] |
+| 18/19 | `option_support_large_channel` | Can create large channels | IN | | [BOLT #2](02-peer-protocol.md#the-open_channel-message) |
+| 22/23 | `option_anchors` | Anchor commitment type with zero fee HTLC transactions | INT | | [BOLT #3][bolt03-htlc-tx], [lightning-dev][ml-sighash-single-harmful] |
+| 24/25 | `option_route_blinding` | Node supports blinded paths | IN9 | | [BOLT #4][bolt04-route-blinding] |
+| 26/27 | `option_shutdown_anysegwit` | Future segwit versions allowed in `shutdown` | IN | | [BOLT #2][bolt02-shutdown] |
+| 28/29 | `option_dual_fund` | Use v2 of channel open, enables dual funding | IN | | [BOLT #2](02-peer-protocol.md) |
+| 34/35 | `option_quiesce` | Support for `stfu` message | IN | | [BOLT #2][bolt02-quiescence] |
+| 36/37 | `option_attribution_data` | Can generate/relay attribution data in `update_fail_htlc` and `update_fulfill_htlc` | IN9 | | [BOLT #4][bolt04-attribution-data] |
+| 38/39 | `option_onion_messages` | Can forward onion messages | IN | | [BOLT #7](04-onion-routing.md#onion-messages) |
+| 42/43 | `option_provide_storage` | Can store other nodes' encrypted backup data | IN | | [BOLT #1](01-messaging.md#peer-storage) |
+| 44/45 | `option_channel_type` | ASSUMED | | | |
+| 46/47 | `option_scid_alias` | Supply channel aliases for routing | INT | | [BOLT #2][bolt02-channel-ready] |
+| 48/49 | `option_payment_metadata` | Payment metadata in tlv record | 9 | | [BOLT #11](11-payment-encoding.md#tagged-fields) |
+| 50/51 | `option_zeroconf` | Understands zeroconf channel types | INT | `option_scid_alias` | [BOLT #2][bolt02-channel-ready] |
+| 60/61 | `option_simple_close` | Simplified closing negotiation | IN | `option_shutdown_anysegwit` | [BOLT #2][bolt02-simple-close] |
+
+## Requirements
+
+The origin node:
+ * If it supports a feature above, SHOULD set the corresponding odd
+ bit in all feature fields indicated by the Context column unless
+ indicated that it must set the even feature bit instead.
+ * If it requires a feature above, MUST set the corresponding even
+ feature bit in all feature fields indicated by the Context column,
+ unless indicated that it must set the odd feature bit instead.
+ * MUST NOT set feature bits it does not support.
+ * MUST NOT set feature bits in fields not specified by the table above.
+ * MUST NOT set both the optional and mandatory bits.
+ * MUST set all transitive feature dependencies.
+ * MUST support:
+ * `var_onion_optin`
+
+The receiving node:
+ * if both the optional and the mandatory feature bits in a pair are set,
+ the feature should be treated as mandatory.
+
+The requirements for receiving specific bits are defined in the linked sections in the table above.
+The requirements for feature bits that are not defined
+above can be found in [BOLT #1: The `init` Message](01-messaging.md#the-init-message).
+
+## Rationale
+
+Note that for feature flags which are available in both the `node_announcement`
+and [BOLT 11](11-payment-encoding.md) invoice contexts, the features as set in
+the [BOLT 11](11-payment-encoding.md) invoice should override those set in the
+`node_announcement`. This keeps things consistent with the unknown features
+behavior as specified in [BOLT 7](07-routing-gossip.md#the-node_announcement-message).
+
+The origin must set all transitive feature dependencies in order to create a
+well-formed feature vector. By validating all known dependencies up front, this
+simplifies logic gated on a single feature bit; the feature's dependencies are
+known to be set, and do not need to be validated at every feature gate.
+
+
+<br>
+This work is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
+
+[bolt02-retransmit]: 02-peer-protocol.md#message-retransmission
+[bolt02-open]: 02-peer-protocol.md#the-open_channel-message
+[bolt02-simple-close]: 02-peer-protocol.md#closing-negotiation-closing_complete-and-closing_sig
+[bolt03-htlc-tx]: 03-transactions.md#htlc-timeout-and-htlc-success-transactions
+[bolt02-shutdown]: 02-peer-protocol.md#closing-initiation-shutdown
+[bolt02-quiescence]: 02-peer-protocol.md#channel-quiescence
+[bolt02-channel-ready]: 02-peer-protocol.md#the-channel_ready-message
+[bolt04-attribution-data]: 04-onion-routing.md#returning-errors
+[bolt07-sync]: 07-routing-gossip.md#initial-sync
+[bolt07-query]: 07-routing-gossip.md#query-messages
+[bolt04-mpp]: 04-onion-routing.md#basic-multi-part-payments
+[bolt04-route-blinding]: 04-onion-routing.md#route-blinding
+[bolt04-attributable-errors]: 04-onion-routing.md
+[ml-sighash-single-harmful]: https://lists.linuxfoundation.org/pipermail/lightning-dev/2020-September/002796.html
diff --git a/flake.nix b/flake.nix
@@ -0,0 +1,58 @@
+{
+ description = "A Haskell implementation of BOLT #9.";
+
+ inputs = {
+ ppad-nixpkgs = {
+ type = "git";
+ url = "git://git.ppad.tech/nixpkgs.git";
+ ref = "master";
+ };
+ flake-utils.follows = "ppad-nixpkgs/flake-utils";
+ nixpkgs.follows = "ppad-nixpkgs/nixpkgs";
+ };
+
+ outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ lib = "ppad-bolt9";
+
+ pkgs = import nixpkgs { inherit system; };
+ hlib = pkgs.haskell.lib;
+ llvm = pkgs.llvmPackages_19.llvm;
+ clang = pkgs.llvmPackages_19.clang;
+
+ hpkgs = pkgs.haskell.packages.ghc910.extend (new: old: {
+ ${lib} = new.callCabal2nix lib ./. { };
+ });
+
+ cc = pkgs.stdenv.cc;
+ ghc = hpkgs.ghc;
+ cabal = hpkgs.cabal-install;
+ in
+ {
+ packages.default = hpkgs.${lib};
+
+ packages.haddock = hpkgs.${lib}.doc;
+
+ devShells.default = hpkgs.shellFor {
+ packages = p: [
+ (hlib.doBenchmark p.${lib})
+ ];
+
+ buildInputs = [
+ cabal
+ cc
+ llvm
+ ];
+
+ shellHook = ''
+ PS1="[${lib}] \w$ "
+ echo "entering ${system} shell, using"
+ echo "cc: $(${cc}/bin/cc --version)"
+ echo "ghc: $(${ghc}/bin/ghc --version)"
+ echo "cabal: $(${cabal}/bin/cabal --version)"
+ '';
+ };
+ }
+ );
+}
diff --git a/lib/Lightning/Protocol/BOLT9.hs b/lib/Lightning/Protocol/BOLT9.hs
@@ -0,0 +1,21 @@
+{-# OPTIONS_HADDOCK prune #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+-- |
+-- Module: Lightning.Protocol.BOLT9
+-- Copyright: (c) 2025 Jared Tobin
+-- License: MIT
+-- Maintainer: Jared Tobin <jared@ppad.tech>
+--
+-- Feature flags for the Lightning Network, per
+-- [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md).
+
+module Lightning.Protocol.BOLT9 (
+ -- * Feature flags
+ -- TODO
+ ) where
+
+import Control.DeepSeq (NFData)
+import GHC.Generics (Generic)
+
diff --git a/ppad-bolt9.cabal b/ppad-bolt9.cabal
@@ -0,0 +1,80 @@
+cabal-version: 3.0
+name: ppad-bolt9
+version: 0.0.1
+synopsis: Feature flags per BOLT #9
+license: MIT
+license-file: LICENSE
+author: Jared Tobin
+maintainer: jared@ppad.tech
+category: Cryptography
+build-type: Simple
+tested-with: GHC == 9.10.3
+extra-doc-files: CHANGELOG
+description:
+ Feature flags, per
+ [BOLT #9](https://github.com/lightning/bolts/blob/master/09-features.md).
+
+source-repository head
+ type: git
+ location: git.ppad.tech/bolt9.git
+
+library
+ default-language: Haskell2010
+ hs-source-dirs: lib
+ ghc-options:
+ -Wall
+ exposed-modules:
+ Lightning.Protocol.BOLT9
+ build-depends:
+ base >= 4.9 && < 5
+ , bytestring >= 0.9 && < 0.13
+ , deepseq >= 1.4 && < 1.6
+
+test-suite bolt9-tests
+ type: exitcode-stdio-1.0
+ default-language: Haskell2010
+ hs-source-dirs: test
+ main-is: Main.hs
+
+ ghc-options:
+ -rtsopts -Wall -O2
+
+ build-depends:
+ base
+ , bytestring
+ , ppad-bolt9
+ , tasty
+ , tasty-hunit
+ , tasty-quickcheck
+
+benchmark bolt9-bench
+ type: exitcode-stdio-1.0
+ default-language: Haskell2010
+ hs-source-dirs: bench
+ main-is: Main.hs
+
+ ghc-options:
+ -rtsopts -O2 -Wall -fno-warn-orphans
+
+ build-depends:
+ base
+ , bytestring
+ , criterion
+ , deepseq
+ , ppad-bolt9
+
+benchmark bolt9-weigh
+ type: exitcode-stdio-1.0
+ default-language: Haskell2010
+ hs-source-dirs: bench
+ main-is: Weight.hs
+
+ ghc-options:
+ -rtsopts -O2 -Wall -fno-warn-orphans
+
+ build-depends:
+ base
+ , bytestring
+ , deepseq
+ , ppad-bolt9
+ , weigh
diff --git a/test/Main.hs b/test/Main.hs
@@ -0,0 +1,11 @@
+module Main where
+
+import Test.Tasty
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "ppad-bolt9" [
+ -- TODO
+ ]