bolt8

Encrypted and authenticated transport, per BOLT #8.
git clone git://git.ppad.tech/bolt8.git
Log | Files | Refs | README | LICENSE

commit 991f84308a8597540ddbceb608b189ddfab42353
parent a2adc22ba86ac58835d283a9b1fdabcf0ad8ce31
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 09:45:50 +0400

meta: docs

Diffstat:
MAGENTS.md | 2++
MCLAUDE.md | 2++
Aplans/ARCH1.md | 28++++++++++++++++++++++++++++
Aplans/ARCH2.md | 18++++++++++++++++++
Aplans/ARCH3.md | 24++++++++++++++++++++++++
Aplans/ARCH4.md | 28++++++++++++++++++++++++++++
Aplans/IMPL1.md | 23+++++++++++++++++++++++
Aplans/IMPL2.md | 6++++++
Aplans/REVIEW-c3040f683.md | 11+++++++++++
Aplans/REVIEW-ebf75f4de8031a369868aba3a94ef89b6a25422b.md | 11+++++++++++
10 files changed, 153 insertions(+), 0 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -66,6 +66,8 @@ Use only minimal external dependencies. Prefer GHC's core/boot libraries ### 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. - Use Maybe/Either for fallible operations - Validate all inputs at system boundaries diff --git a/CLAUDE.md b/CLAUDE.md @@ -66,6 +66,8 @@ Use only minimal external dependencies. Prefer GHC's core/boot libraries ### 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. - Use Maybe/Either for fallible operations - Validate all inputs at system boundaries diff --git a/plans/ARCH1.md b/plans/ARCH1.md @@ -0,0 +1,28 @@ +# ARCH1: Packet framing for decrypt + +## Goal +Define how the API should handle stream framing and trailing bytes when +receiving BOLT8 packets. + +## Context +BOLT8 frames are sent on a stream as: +- encrypted length (2 bytes) + MAC (16 bytes) == 18 bytes total +- encrypted body (len bytes) + MAC (16 bytes) + +A receiver typically reads 18 bytes, decrypts length, then reads the +next len+16 bytes. If a read returns more than one frame, the caller +must retain the remainder for the next decrypt. + +## Decision points +- Keep strict packet API (reject trailing bytes), or +- Provide a framing helper that returns (plaintext, remainder, session), + leaving existing decrypt unchanged. + +## Constraints +- Preserve BOLT8 wire semantics. +- Avoid partial functions. +- Keep changes minimal and compatible where possible. + +## Expected outcome +A clear API contract for decrypt framing in downstream callers, with +explicit behavior on trailing bytes. diff --git a/plans/ARCH2.md b/plans/ARCH2.md @@ -0,0 +1,18 @@ +# ARCH2: Document HKDF invariant + +## Goal +Document why mix_key cannot hit the Nothing case from HKDF.derive. + +## Context +mix_key uses HKDF.derive hmac ck mempty 64 ikm and currently calls +error on Nothing. The Nothing case occurs when the requested output +length exceeds 255 * hashlen. For SHA256, hashlen is 32, so the limit +is 8160 bytes. The requested length is 64. + +## Decision +Keep the error, but document the invariant in a short comment so future +readers understand why the case is impossible. + +## Expected outcome +A local comment near mix_key explaining the bound and why error is safe +in this context. diff --git a/plans/ARCH3.md b/plans/ARCH3.md @@ -0,0 +1,24 @@ +# ARCH3: Doc fixes and negative tests + +## Goal +Correct key-rotation documentation and add negative tests for +validation paths. + +## Context +- Docs say rotate every 500 messages; code rotates at 1000. +- Tests only cover positive BOLT8 vectors. + +## Changes +- Update Haddock comments to match implementation or spec. +- Add tests for: + - Invalid lengths (short/extra). + - Invalid version byte. + - Invalid MAC. + +## Constraints +- Use tasty + tasty-hunit. +- Use spec-aligned values and local helpers. +- No new dependencies. + +## Expected outcome +Docs reflect actual behavior, and failures are covered by tests. diff --git a/plans/ARCH4.md b/plans/ARCH4.md @@ -0,0 +1,28 @@ +# ARCH4: Recoverable partial framing + +## Goal +Distinguish "need more bytes" from malformed packets when decrypting +from a stream buffer. + +## Context +Current decrypt_frame returns InvalidLength when the buffer does not +contain a full frame. In streaming reads, this can be a normal condition +rather than an error. BOLT8 framing requires incremental parsing of a +length field and then the encrypted body. + +## Decision points +- Introduce a new result type (e.g., FrameResult) that can be: + - NeedMore Int (minimum bytes required), + - FrameOk plaintext remainder session, + - FrameError Error. +- Alternatively, add a new function decrypt_frame_partial with a + dedicated error type while keeping decrypt_frame strict. + +## Constraints +- Keep existing API behavior stable if possible. +- Avoid partial functions. +- Make the partial/need-more case explicit and non-exceptional. + +## Expected outcome +A clear API for streaming callers to handle partial buffers without +conflating them with invalid frames. diff --git a/plans/IMPL1.md b/plans/IMPL1.md @@ -0,0 +1,23 @@ +# IMPL1: Packet framing for decrypt + +## Steps +1) Decide API shape: + - Option A: Make decrypt strict and require exact packet length. + - Option B: Add decrypt_frame returning remainder, keep decrypt strict + or unchanged. +2) If Option A: + - Add length check that the buffer equals 18 + len + 16. + - Return InvalidLength on trailing bytes. +3) If Option B: + - Implement decrypt_frame :: Session -> ByteString + -> Either Error (ByteString, ByteString, Session). + - decrypt_frame consumes one frame and returns the remainder. + - Keep existing decrypt strict or make it a wrapper over + decrypt_frame that rejects remainder. +4) Add tests for framing behavior: + - Trailing bytes rejected for strict decrypt. + - decrypt_frame returns the correct remainder. + +## Notes +- Align docstrings with the chosen behavior. +- No new dependencies. diff --git a/plans/IMPL2.md b/plans/IMPL2.md @@ -0,0 +1,6 @@ +# IMPL2: Document HKDF invariant + +## Steps +1) Add a short comment near mix_key describing the HKDF length bound. +2) Ensure the comment is under 80 columns and ASCII. +3) No behavior changes. diff --git a/plans/REVIEW-c3040f683.md b/plans/REVIEW-c3040f683.md @@ -0,0 +1,11 @@ +# Review c3040f683 + +## Findings +- Low: Doc mismatch between encrypt and decrypt about key-rotation cadence. + encrypt says "every 500 messages" while decrypt/decrypt_frame say 1000. + This is misleading for callers reading Haddocks. See + lib/Lightning/Protocol/BOLT8.hs:503. + +## Notes +- If you want a recoverable/partial state ("need more bytes") rather than + InvalidLength, that needs an API/ADT change; see ARCH/IMPL proposal. diff --git a/plans/REVIEW-ebf75f4de8031a369868aba3a94ef89b6a25422b.md b/plans/REVIEW-ebf75f4de8031a369868aba3a94ef89b6a25422b.md @@ -0,0 +1,11 @@ +# Review ebf75f4de8031a369868aba3a94ef89b6a25422b + +## Findings +- Low: test helper `flip_byte` uses `BS.index` without bounds checking, + so a bad index will crash the test rather than fail an assertion. + It is safe with the current constants (msg2 len 50, msg3 len 66), but + brittle if framing changes. See test/Main.hs:443. + +## Notes +- Doc fixes for key-rotation now align with nonce=1000. +- Negative tests look correct and target the intended failure modes.