commit 991f84308a8597540ddbceb608b189ddfab42353
parent a2adc22ba86ac58835d283a9b1fdabcf0ad8ce31
Author: Jared Tobin <jared@jtobin.io>
Date: Sun, 25 Jan 2026 09:45:50 +0400
meta: docs
Diffstat:
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.