]> git.codecow.com Git - nano25519.git/commitdiff
Add agent file.
authorChris Duncan <chris@zoso.dev>
Wed, 26 Aug 2026 05:54:28 +0000 (22:54 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 26 Aug 2026 05:54:28 +0000 (22:54 -0700)
AGENTS.md [new file with mode: 0644]

diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644 (file)
index 0000000..056f8bc
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,133 @@
+<!--
+SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+# AGENTS.md
+
+Ed25519-BLAKE2b public key derivation, block signing, and signature
+verification for the Nano cryptocurrency. The cryptography is AssemblyScript
+compiled to WebAssembly; a thin TypeScript layer marshals data across the
+boundary. Everything is synchronous and single-threaded.
+
+## Commands
+
+```bash
+npm run build       # clean + compile + esbuild (dev)
+npm run build:prod  # clean + compile + esbuild (minified)
+npm run compile     # asc + tsc only, no bundle
+npm test            # build, then node ./test/node.mjs
+npm run test:prod   # build:prod, then the same suite
+npm run clean       # rm -rf {build,dist,types}
+```
+
+`build/`, `dist/`, and `types/` are generated and gitignored. Never edit them.
+The test suite imports from `dist/`, so **always build before testing** — a
+stale `dist/` will silently test the previous revision.
+
+## Layout
+
+| Path | What it is |
+| --- | --- |
+| `src/index.ts` | Public API and its overloads: `derive`, `sign`, `verify`, `verify_blocks` |
+| `src/lib/wasm.ts` | Instantiates the module; exports pointers, byte lengths, `normalize()`, `clear()` |
+| `src/lib/{derive,sign,verify}.ts` | Host-side marshalling, one file per primitive |
+| `src/assembly/index.ts` | Wasm entry points, static I/O buffers, exported pointers and byte-length globals |
+| `src/assembly/crypto_{derive,sign,verify}.ts` | RFC 8032-shaped primitives over BLAKE2b |
+| `src/assembly/{fe,ge,p,sc}.ts` | Field element, group element, point, and scalar arithmetic |
+| `src/assembly/{base,base2}.ts` | Precomputed base point tables as static data segments |
+| `test/node.mjs`, `test/vectors.mjs` | Suite and vectors |
+| `test/index.html` | Browser test and benchmark page |
+
+## Build constraints that break normal assumptions
+
+`asconfig.json` disables most of the AssemblyScript safety net. Read these
+before writing any assembly code — they invalidate habits that are correct in
+ordinary AssemblyScript:
+
+- **`runtime: "stub"`** — bump allocator, no collector. Any allocation on a
+  call path is a permanent leak. Call paths must allocate nothing; all state is
+  module-level `StaticArray` allocated once at init.
+- **`noAssert: true`, `uncheckedBehavior: "always"`** — bounds and null checks
+  are compiled out. An out-of-range index silently corrupts neighbouring
+  statics instead of trapping. Validate every untrusted length or count as the
+  **first** statement of an exported function, before any indexing.
+- **`disable: ["mutable-globals"]`** — a global with a non-constant initialiser
+  cannot be exported. Only compile-time constants survive as exported globals.
+- **`enable: ["simd"]`** — field elements are 12 `i32` limbs, not 10. The two
+  trailing limbs are padding; respect the stride.
+- **`initialMemory: 4`** — pins the module at 4 pages. Without it the stub
+  allocator doubles on growth (1→2→4→8). Re-derive this value when buffer
+  sizes change rather than leaving it stale.
+
+Memory growth *during* a call would detach every `Uint8Array` view the host
+holds, so keeping call paths allocation-free is a correctness requirement, not
+just a memory one.
+
+## Gotchas
+
+**Shift operands are masked to the operand width.** `x >> 8` where `x` is a
+`u8` is a no-op, not zero. This silently broke the constant-time helpers
+`equal()`, `negative()`, and `sodium_is_zero()`. Widen to `i32`/`u32` before
+shifting when porting libsodium idioms.
+
+**`throw new Error('literal')` does not allocate.** With exception handling
+disabled, AssemblyScript lowers it straight to `abort(msgPtr, filePtr, line,
+col)` against static string data — the wat shows `call $abort` and
+`unreachable`, no `__new`. Do **not** hoist it to a module-level
+`const ERROR = new Error()`: that costs 96 bytes of permanent heap, adds a dead
+store, and makes AssemblyScript pass a null message pointer, so the error text
+is lost before it reaches the host.
+
+**Scrub buffers on every path, unscoped.** Every exported wasm function must
+leave its input buffers fully zeroed when it returns, including the throw path.
+Use whole-buffer `fill(0)`, never a length-scoped fill: the buffer pointers are
+public exports, so a caller can write more bytes than the length it then
+declares, and a scoped fill leaves the rest behind. A 32 KiB fill costs ~0.1 µs
+— roughly 0.4% of one signature — so this is never worth optimising away.
+
+**Do not add JS-side suspension points.** The safety of hashing straight out of
+the shared message buffer rests on there being no `await`, no yield, and no
+callback into user code between the host writing the buffer and reading the
+result. The only wasm import is `env.abort`, which throws. Preserve both
+properties.
+
+## Benchmarking
+
+Micro-benchmarks here mislead badly without care. Warm each build ≥200
+iterations, interleave the builds being compared in a single process, and take
+best-of-N with N≥7. A short warmup with one timing pass once reported an 18.6%
+gain that was entirely JIT warmup; re-measured properly it was under 2%, with
+the sign of the delta flipping between runs. Treat anything under ~2% as noise.
+
+Useful scale anchors: a wasm call boundary is ~2 ns, a 32 KiB `memory.fill` is
+~0.1 µs, and one `verify` is ~85 µs. Buffer scrubbing and boundary crossing
+never show up on the clock; only the point arithmetic does.
+
+## Testing
+
+`node ./test/node.mjs` after a build. Current state is **6168 passing, 1
+failing**, and that one failure is expected.
+
+The failing case is `PROBLEM_VECTOR`: a live cemented Nano block from a
+small-order account (`nano_11a11…`, public key `0100…00`) that network
+consensus accepted but strict Ed25519 rejects. The test asserts `true` to match
+the ledger; `verify()` returns `false` to match the spec. Do not resolve this by
+weakening the small-order check in strict verification — the intended shape is a
+relaxed variant for Nano blocks alongside a strict one for general use.
+
+## Style
+
+- Tabs for indentation, no semicolons, single quotes.
+- A space between a function name and its parameter list: `export function derive (prv: ...)`.
+- Every file carries an SPDX header (REUSE); licence texts live in `LICENSES/`.
+  Original code is GPL-3.0-or-later; code ported from libsodium keeps its ISC
+  header and its original copyright line.
+- Use real mathematical symbols in comments (`≤`, `²⁵⁵`) — the codebase already
+  does, and it reads against the RFC.
+
+## Git
+
+Work happens on `next/<topic>` branches merged into `main`. Commit subjects are
+capitalised imperative sentences ending in a period, e.g. `Fix reversed
+parameters.` Do not commit or push unless asked.