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:
| M | AGENTS.md | | | 10 | ++++++---- |
| M | CHANGELOG | | | 50 | +++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | README.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://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