bolt9

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

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:
AAGENTS.md | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACHANGELOG | 4++++
ALICENSE | 20++++++++++++++++++++
AREADME.md | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abench/Main.hs | 8++++++++
Abench/Weight.hs | 8++++++++
Aetc/09-features.md | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aflake.nix | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/Lightning/Protocol/BOLT9.hs | 21+++++++++++++++++++++
Appad-bolt9.cabal | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/Main.hs | 11+++++++++++
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. + +![Creative Commons License](https://i.creativecommons.org/l/by/4.0/88x31.png "License CC-BY") +<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 + ]