bolt4

Onion routing protocol, per BOLT #4 (docs.ppad.tech/bolt4).
git clone git://git.ppad.tech/bolt4.git
Log | Files | Refs | README | LICENSE

commit c43c7dfc3d673f48af2ce7e95b8677f297d942cd
parent 1694f71dc187752662c354d9f917b740037f5431
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:02:39 +0400

docs: readme, changelog and AGENTS.md for 0.1.0

Diffstat:
MAGENTS.md | 10++++++----
MCHANGELOG | 50+++++++++++++++++++++++++++++++++++++++++++++++++-
MREADME.md | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
3 files changed, 171 insertions(+), 47 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -1,10 +1,12 @@ # ppad-bolt4 -A pure Haskell implementation of BOLT4 (onion routing) from the -Lightning Network specification. +A pure Haskell implementation of BOLT #4 (onion routing) from the +Lightning Network specification, as of lightning/bolts@1aadb719 (copy +in `etc/04-onion-routing.md`). -The implementation consists of the ppad-bolt4 library, a test suite, -and benchmarks (for time and space). +The implementation consists of the ppad-bolt4 library (public API: +Lightning.Protocol.BOLT4; the submodules are internal), a test suite +(public API only), and benchmarks (for time and space). ## build diff --git a/CHANGELOG b/CHANGELOG @@ -1,4 +1,52 @@ # Changelog -- 0.0.1 (UNRELEASED) +- 0.1.0 (UNRELEASED) + * A substantial rewrite, with breaking changes throughout: + + * The API is exported from Lightning.Protocol.BOLT4 alone, and uses + snake_case names (e.g. construct, process, construct_error, + create_blinded_path). The Prim, Codec and Internal modules are + no longer exposed. + + * Fundamental types come from ppad-bolt1: points are Points, + amounts are MilliSatoshis, short channel ids and TLV streams are + bolt1's. allowed_features is a ppad-bolt9 FeatureVector. The + private BigSize, TLV and truncated-integer codecs are gone. + + * Secret keys (SecretKey) must be exactly 32 bytes encoding a valid + scalar. Secret keys, shared secrets and payment secrets are + abstract, with redacted Show instances; shared and payment + secrets compare in constant time. + + * process takes the path_key received with an onion, derives the + blinded private key, decrypts encrypted_recipient_data (using + path_key or current_path_key) and returns its contents and the + next path key. + + * Hop payloads and encrypted_data_tlv streams reject unknown even + types and keep unknown odd ones, so re-encoding is exact. + payment_metadata and total_amount_msat are typed fields. + + * Failures are a typed sum of every failure code in the spec, with + an escape hatch for unknown codes; invalid_realm is gone, and + required_channel_feature_missing, unknown_next_peer and + invalid_onion_blinding are new. + + * unwrap_error reports the hop whose HMAC matches even when its + failure message is malformed. wrap_error is total, and + construct_error fails only on an unencodable failure message. + + * Fixes onion construction for routes of three or more hops, the + framing and padding of failure messages, and decoding of payload + lengths past the end of hop_payloads, overlong TLV lengths and + unknown even TLV types. + + * The onion HMAC check is constant time. + + * Tests now cover the onion, error, route blinding and blinded + payment test vectors of BOLT #4, through the public API. + + * Adds NFData instances, and criterion and weigh benchmarks. + +- 0.0.1 (2026-04-18) * Initial release. diff --git a/README.md b/README.md @@ -4,9 +4,9 @@ ![](https://img.shields.io/badge/license-MIT-brightgreen) [![](https://img.shields.io/badge/haddock-bolt4-lightblue)](https://docs.ppad.tech/bolt4) -A pure Haskell implementation of [BOLT #4][bolt4] (onion routing) from -the Lightning Network protocol specification, including packet -construction and processing, error handling, and route blinding. +A pure Haskell implementation of Lightning Network onion routing, +[BOLT #4][bolt4]: onion construction and processing, returning errors, +and route blinding. ## Usage @@ -14,56 +14,128 @@ A sample GHCi session: ``` > :set -XOverloadedStrings - > - > -- construct an onion packet for a 3-hop route - > import qualified Crypto.Curve.Secp256k1 as Secp256k1 > import qualified Data.ByteString as BS - > import qualified Lightning.Protocol.BOLT4.Construct as Construct - > import qualified Lightning.Protocol.BOLT4.Process as Process - > import Lightning.Protocol.BOLT4.Types + > import qualified Lightning.Protocol.BOLT1 as BOLT1 + > import Lightning.Protocol.BOLT4 + > + > -- keys (use a fresh random session key for every onion) + > let Just session = secret_key (BS.replicate 32 0x41) + > let Just alice = secret_key (BS.replicate 32 0x42) + > let Just bob = secret_key (BS.replicate 32 0x43) > - > -- session key (32 bytes random, use CSPRNG in production) - > let sessionKey = BS.replicate 32 0x41 + > -- a two-hop route, paying bob through alice + > let Just amt = BOLT1.milli_satoshi 100000 + > let Just scid = BOLT1.short_channel_id 800000 1 0 + > let pay = empty_hop_payload { hp_amt_to_forward = Just amt } + > :{ + ghci| let to_alice = Hop (public_key alice) pay { + ghci| hp_outgoing_cltv_value = Just 800040 + ghci| , hp_short_channel_id = Just scid + ghci| } + ghci| to_bob = Hop (public_key bob) pay { + ghci| hp_outgoing_cltv_value = Just 800000 + ghci| } + ghci| :} + > let hash = BS.replicate 32 0xff + > let Right (onion, secrets) = construct session [to_alice, to_bob] hash + > BS.length (encode_onion_packet onion) + 1366 > - > -- node keys (in production, these come from the network) - > let Just hop1Sec = Secp256k1.parse_integer (BS.replicate 32 0x01) - > let Just hop1Pub = Secp256k1.mul Secp256k1._CURVE_G hop1Sec + > -- each hop peels off its layer + > let Right (Forward fwd) = process alice onion hash Nothing + > hp_short_channel_id (fwd_payload fwd) + Just (ShortChannelId 879609302220865536) + > let next = fwd_next_packet fwd + > let Right (Receive rcv) = process bob next hash Nothing + > hp_amt_to_forward (rcv_payload rcv) + Just (MilliSatoshi 100000) > - > -- build a minimal payload - > let payload = emptyHopPayload { hpAmtToForward = Just 1000 - > , hpOutgoingCltv = Just 144 } - > let hop = Construct.Hop hop1Pub payload + > -- bob fails the payment, alice wraps the error, and the sender + > -- attributes it to bob + > let failure = FailureMessage MppTimeout BOLT1.empty_tlv_stream + > let Right err = construct_error (rcv_shared_secret rcv) failure + > unwrap_error secrets (wrap_error (fwd_shared_secret fwd) err) + Attributed 1 (FailureMessage {fm_failure = MppTimeout, + fm_tlvs = TlvStream []}) > - > -- construct packet (returns packet + shared secrets for error handling) - > let assocData = BS.replicate 32 0x00 -- typically payment_hash - > let Right (packet, secrets) = Construct.construct sessionKey [hop] assocData + > -- bob hands out a blinded route to himself + > let Just seed = secret_key (BS.replicate 32 0x01) + > let mine = empty_blinded_hop_data { bhd_path_id = Just "invoice 1" } + > let Right path = create_blinded_path seed [(public_key bob, mine)] + > let [hop] = bp_hops path > - > -- process at receiving node - > let result = Process.process (BS.replicate 32 0x01) packet assocData - > case result of - > Right (Process.Receive info) -> print (riPayload info) - > _ -> putStrLn "not final hop" + > -- the sender reaches the route's introduction node through alice + > :{ + ghci| let blinded = Hop (bp_first_node_id path) pay { + ghci| hp_outgoing_cltv_value = Just 800000 + ghci| , hp_total_amount_msat = Just amt + ghci| , hp_encrypted_data = Just (bh_encrypted_data hop) + ghci| , hp_current_path_key = Just (bp_first_path_key path) + ghci| } + ghci| :} + > let Right (onion', _) = construct session [to_alice, blinded] hash + > let Right (Forward fwd') = process alice onion' hash Nothing + > let next' = fwd_next_packet fwd' + > let Right (Receive rcv') = process bob next' hash Nothing + > fmap (bhd_path_id . bi_data) (rcv_blinded rcv') + Just (Just "invoice 1") ``` -## Modules +Nodes further along a blinded route pass the `path_key` they received +(`bi_next_path_key` of the previous hop) as the last argument of +`process`, which then derives their blinded private key. -The library is organized into the following modules: +## Documentation -* `Lightning.Protocol.BOLT4.Construct` - onion packet construction -* `Lightning.Protocol.BOLT4.Process` - onion packet processing -* `Lightning.Protocol.BOLT4.Error` - failure message handling -* `Lightning.Protocol.BOLT4.Blinding` - route blinding support -* `Lightning.Protocol.BOLT4.Types` - core data types -* `Lightning.Protocol.BOLT4.Codec` - serialization (BigSize, TLV) -* `Lightning.Protocol.BOLT4.Prim` - cryptographic primitives +Haddocks (API documentation, etc.) are hosted at +[docs.ppad.tech/bolt4](https://docs.ppad.tech/bolt4). -## Documentation +## Specification + +This library implements BOLT #4 as of lightning/bolts commit +[1aadb719][spec] (2026-09-21); a copy lives in `etc/`. It is tested +against the `onion-test.json`, `onion-error-test.json`, +`route-blinding-test.json` and `blinded-payment-onion-test.json` +vectors, and the 'Returning Errors' trace. + +Fundamental types and the TLV format come from [ppad-bolt1][bolt1]; +`allowed_features` is a [ppad-bolt9][bolt9] feature vector. + +Not implemented: + +* Attributable failures (`option_attribution_data`): the + `attribution_data` of `update_fail_htlc` and `update_fulfill_htlc`, + and HTLC hold times. +* Onion messages (`onion_message_packet`, `onionmsg_tlv`). +* The 32 KiB limit on return packets: `construct_error` doesn't enforce + it, and `wrap_error` doesn't truncate longer packets. +* The origin node's dummy decryption rounds: `unwrap_error` stops at the + hop whose HMAC matches. +* The `blinded_path` wire encoding, and the computation of a blinded + route's total fees and CLTV delta. -Haddocks are hosted at [docs.ppad.tech/bolt4][hadoc]. +`process` enforces the onion decryption requirements and the path key +rules of route blinding; the reader requirements that depend on the +HTLC or the node's channels (required fields, amount and expiry checks, +`payment_relay` and `payment_constraints`, `allowed_features`, replayed +HMACs) are left to the caller. + +## Performance + +The aim is best-in-class performance for pure Haskell code. Run the +benchmarks with: + +``` +$ cabal bench +``` ## Security -This is a pre-release library that claims no security properties whatsoever. +This library has received no security audit. Secret keys, shared +secrets and payment secrets are abstract and kept out of `Show` output; +shared and payment secrets compare in constant time, and `process` +checks onion HMACs in constant time. Scalar multiplications involving +secrets use ppad-secp256k1's constant-time implementation. ## Development @@ -77,12 +149,14 @@ $ nix develop Then do e.g.: ``` -$ cabal build -$ cabal test -$ cabal bench +$ cabal repl ppad-bolt4 ``` +to get a REPL for the main library. + [bolt4]: https://github.com/lightning/bolts/blob/master/04-onion-routing.md -[hadoc]: https://docs.ppad.tech/bolt4 +[bolt1]: https://git.ppad.tech/bolt1 +[bolt9]: https://git.ppad.tech/bolt9 +[spec]: https://github.com/lightning/bolts/tree/1aadb719b4007c4cea0ba6e36b08c4fb53788dee [nixos]: https://nixos.org/ [flake]: https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html