]> git.codecow.com Git - nano25519.git/commitdiff
Update agent file.
authorChris Duncan <chris@codecow.com>
Fri, 28 Aug 2026 18:45:12 +0000 (11:45 -0700)
committerChris Duncan <chris@codecow.com>
Fri, 28 Aug 2026 18:45:12 +0000 (11:45 -0700)
AGENTS.md

index cbda5c7beead36e97a9cd9dfe03b21d82af7b7b2..e9a34f30de6b7970423346f8986eb788f589c340 100644 (file)
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -100,7 +100,7 @@ every pointer and byte length once at module load and re-exports them.
 
 | Buffer | Purpose |
 | --- | --- |
-| `INPUT_MSG` | 32 KiB. A message for `sign`/`verify`, or packed 96-byte records for `verify_blocks` |
+| `INPUT_MSG` | 64 KiB. A message for `sign`/`verify`, or packed 96-byte records for `verify_blocks` |
 | `INPUT_PRV`, `INPUT_PUB` | 32 bytes each |
 | `INPUT_SIG` | 64 B, one signature for single `verify` |
 | `OUTPUT_DERIVE` | 32 B, a public key |
@@ -116,12 +116,8 @@ byte per block, *and* at least 64 bytes for a signature. Only the first was ever
 written down, so cutting the batch cap to 32 made `sign` overrun it. If you ever
 merge them again, both constraints have to be stated together.
 
-`INPUT_MSG` still has this problem. It holds **either** a message for
-`sign`/`verify` (up to `MAX_MESSAGE_BYTELENGTH`) **or** a packed batch
-(`MAX_VERIFY_BLOCKS * SIGNEDBLOCK_BYTELENGTH` = 3,072 B). Only the first is
-named. `1 << 15` satisfies both with room to spare, but it was briefly `1 << 10`
-and a full batch overran it. **Before shrinking `MAX_MESSAGE_BYTELENGTH` or
-raising `MAX_VERIFY_BLOCKS`, check both roles still fit.**
+`INPUT_MSG` holds **either** a message for `sign`/`verify` **or** a packed
+batch, and both roles are now in one expression, so the check is automatic.
 
 Records in `INPUT_MSG` are **`signature` then `hash`** (64 B + 32 B), libsodium
 combined-mode order. Nothing validates this; both sides just have to agree.
@@ -129,25 +125,35 @@ combined-mode order. Nothing validates this; both sides just have to agree.
 The batch cap is `MAX_VERIFY_BLOCKS` (32); the wasm guard, the host guard and
 `OUTPUT_VERIFY` all read it, so they cannot drift.
 
-⚠️ **`MAX_MESSAGE_BYTELENGTH` is currently derived from the wrong constant:**
+`MAX_MESSAGE_BYTELENGTH` is a **deliberately chosen policy limit** — a cap on
+how large a message this package will sign at all, so oversized inputs stay out
+of scope. It sizes `INPUT_MSG`; the buffer does not constrain it. Its floor is
+one full page, raised to whatever a full batch needs if that is ever larger:
 
 ```ts
-export const MAX_VERIFY_BLOCKS: i32 = 32
-export const MAX_MESSAGE_BYTELENGTH: i32 = MAX_VERIFY_BLOCKS << 10   // 32,768
+const PAGE_BYTELENGTH: i32 = 1 << 16
+export const MAX_VERIFY_BLOCKS_BYTELENGTH: i32 = MAX_VERIFY_BLOCKS * SIGNEDBLOCK_BYTELENGTH
+export const MAX_MESSAGE_BYTELENGTH: i32 =
+       MAX_VERIFY_BLOCKS_BYTELENGTH > PAGE_BYTELENGTH ? MAX_VERIFY_BLOCKS_BYTELENGTH : PAGE_BYTELENGTH
 ```
 
-This is right only because both happen to be 32. A batch needs
-`MAX_VERIFY_BLOCKS * SIGNEDBLOCK_BYTELENGTH` = 3,072 B, not 32 KiB. Change the
-cap and the **maximum signable message length changes with it**: at 16 it halves
-to 16 KiB (a silent API regression), at 64 it doubles and pushes the module from
-2 pages to 4. If you touch `MAX_VERIFY_BLOCKS`, check what happened to
-`MAX_MESSAGE_BYTELENGTH` and to `memory.size()`.
+Raising the *contract* rather than sizing the buffer is what makes this safe:
+the host already validates message length against `MAX_MESSAGE_BYTELENGTH`, so
+that one guard now covers batch packing too. **If you change `MAX_VERIFY_BLOCKS`,
+check `memory.size()`** — the message limit follows it once a batch exceeds a
+page, and pages are the expensive unit here.
 
-Exported byte-length globals (`KEY_BYTELENGTH`, `MESSAGE_BUFFER_BYTELENGTH`, …)
-are the single source of truth for these sizes — read them rather than
-hardcoding, on both sides of the boundary. `test/index.html` currently
-hardcodes the cap as `32` in two places and is the one file known to violate
-this — it tracked the last cap change by hand.
+These sizes are a **public API**: `nano25519.constants` carries
+`BLOCKHASH_BYTELENGTH`, `KEY_BYTELENGTH`, `SIGNATURE_BYTELENGTH`,
+`MAX_VERIFY_BLOCKS`, `MAX_VERIFY_BLOCKS_BYTELENGTH` and
+`MAX_MESSAGE_BYTELENGTH`. Read them rather than hardcoding, on both sides of the
+boundary and in consumers. Buffer **pointers** are grouped as `pointers` inside
+`src/lib` and deliberately not exported from the package — the buffer-scrub
+guarantee is a property of the module, not a promise about callers.
+
+`test/index.html` still hardcodes the cap as `32` in two places and is the one
+file known to violate this; `constants.MAX_VERIFY_BLOCKS` is now available to
+it.
 
 ## Build constraints that break normal assumptions
 
@@ -169,11 +175,12 @@ ordinary AssemblyScript:
   arithmetic over other constants is fine and still const-folds.
 - **`enable: ["simd"]`** — field elements are 12 `i32` limbs, not 10. The two
   trailing limbs are padding; respect the stride.
-- **`initialMemory: 2`** — pins the module at 2 pages (131,072 B); heap at rest
-  is 103,456 B. Without the setting the stub allocator doubles on growth
-  (1→2→4→8), so page counts are always powers of two. Re-derive this whenever a
-  buffer is resized rather than leaving it stale; it has been wrong in both
-  directions already.
+- **`initialMemory: 4`** — pins the module at 4 pages (262,144 B); heap at rest
+  is 135,824 B. Only the stub allocator's *growth* doubles (1→2→4→8); the
+  setting itself takes any integer, so it need not be a power of two — **3 pages
+  currently fit**, with 60,784 B spare. Re-derive this whenever a buffer is
+  resized rather than leaving it stale or rounding it up; it has been wrong in
+  both directions twice.
 
 Memory growth *during* a call would detach every `Uint8Array` view the host
 holds, so keeping call paths allocation-free is a correctness requirement, not
@@ -285,10 +292,22 @@ show up on the clock; only the point arithmetic does.
 
 ## Testing
 
-`node ./test/node.mjs` after a build. Current state is **6178 passing, 0
+`node ./test/node.mjs` after a build. Current state is **6177 passing, 0
 failing**. There is no longer an expected failure — if anything fails, it is a
 regression.
 
+`equalbytes` additionally self-tests in the module's **start function** (top-level
+code in `utils.ts`), flipping all 32 byte positions. A regression there makes the
+module fail to instantiate, so the package becomes unimportable rather than
+silently weakening signature verification. Keep that pattern for anything whose
+failure mode is "quietly accepts too much".
+
+⚠️ **An abort during instantiation loses its message.** The `env.abort` handler
+reads `exports.memory`, but `exports` is still in its temporal dead zone while
+the start function runs, so a fired self-test reports `Cannot access 'memory'
+before initialization` rather than its own text. If the module refuses to load
+with that error, suspect an init-time assertion, not a binding problem.
+
 `verify_blocks` now has vector coverage — a single block, a full 32-block
 batch, both range errors, and the three block-shape `TypeError`s. Coverage still
 runs thin in the middle: its bugs have twice been invisible below a boundary