From 887afdd9964feedf11c10fc2e2e525e6ba185109 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Tue, 25 Aug 2026 22:54:28 -0700 Subject: [PATCH] Add agent file. --- AGENTS.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..056f8bc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,133 @@ + + +# 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/` 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. -- 2.52.0