]> git.codecow.com Git - nano25519.git/commitdiff
Fix buffer lengths and loop iterators.
authorChris Duncan <chris@zoso.dev>
Wed, 26 Aug 2026 08:15:33 +0000 (01:15 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 26 Aug 2026 08:15:33 +0000 (01:15 -0700)
AGENTS.md
src/assembly/index.ts
src/lib/verify.ts

index 056f8bcb5eed27b446e5cba45c9c0a128dc8d05e..6dc9825f51f4d53dc91d3f1820dd947b5d3a0d2f 100644 (file)
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,6 +10,19 @@ 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.
 
+```javascript
+derive(prv[, out])          // 32-byte private key -> 32-byte public key
+sign(msg, prv, pub[, out])  // 32-byte block hash  -> 64-byte signature
+verify(sig, msg, pub)       // -> boolean
+verify_blocks(pub, blocks)  // { hash, signature }[], up to 256 -> boolean[]
+```
+
+Inputs are `Uint8Array` or hex strings, and the output type follows the input
+type. `derive` and `sign` take an optional preallocated output buffer as a
+trailing argument. `verify_blocks` is the Nano account-chain case — many block
+hashes against one public key — and is under active construction; check the
+living context document before relying on it.
+
 ## Commands
 
 ```bash
@@ -39,6 +52,24 @@ stale `dist/` will silently test the previous revision.
 | `test/node.mjs`, `test/vectors.mjs` | Suite and vectors |
 | `test/index.html` | Browser test and benchmark page |
 
+## The wasm ABI
+
+Nothing crosses the boundary as arguments. The host writes into fixed static
+buffers, calls an export that takes only a length or a count, and reads results
+back out of `OUTPUT_BUFFER`. `src/lib/wasm.ts` resolves every pointer and byte
+length once at module load and re-exports them.
+
+| Buffer | Purpose |
+| --- | --- |
+| `MESSAGE_BUFFER` | 32 KiB. A message for `sign`/`verify`, or packed 96-byte `hash \|\| signature` records for `verify_blocks` |
+| `PRV_BUFFER`, `PUB_BUFFER` | 32 bytes each |
+| `SIGNATURE_BUFFER` | one signature for single `verify` |
+| `OUTPUT_BUFFER` | public key, signature, or one result byte per verified block |
+
+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.
+
 ## Build constraints that break normal assumptions
 
 `asconfig.json` disables most of the AssemblyScript safety net. Read these
@@ -86,6 +117,23 @@ 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.
 
+**Counts, lengths, and end offsets are not interchangeable, and confusing them
+is silent here.** This project has produced the same bug three separate times: a
+byte length passed where an element count was wanted, and a length passed where
+an end offset was wanted. With bounds checks compiled out on the wasm side and
+typed-array writes silently discarded on the JS side, every instance compiled,
+ran, and returned plausible answers. When touching either side of the boundary,
+name the quantity in the variable (`…_count`, `…_bytes`, `…_end`) and check each
+call against it:
+
+- `Uint8Array.prototype.fill(value, start, end)` — the third argument is an
+  **end offset**, not a length. `fill(0, PTR, LEN)` is a silent no-op whenever
+  `PTR >= LEN`.
+- `memory.copy(dest, src, n)` — `n` is **bytes**. For an array of one-byte
+  results, that is the element count; for anything wider it is not.
+- A loop bound of `SIG_LEN` or `KEY_LEN` where the intent was "per item" reads
+  the right number of bytes for the wrong reason.
+
 **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
@@ -109,12 +157,19 @@ never show up on the clock; only the point arithmetic does.
 `node ./test/node.mjs` after a build. Current state is **6168 passing, 1
 failing**, and that one failure is expected.
 
+`verify_blocks` has **no vector coverage yet** — a green suite says nothing
+about it. Exercise it directly, and at batch sizes past 64 and past 256, where
+the interesting boundaries are.
+
 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.
+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;
+`crypto_verify_relaxed` and `crypto_verify_strict` both exist, but the relaxed
+one is still byte-identical to the strict one, so the split does not yet buy
+anything.
 
 ## Style
 
index 27aff69aa7d50bfd8ae7180d2d944dbf96cac42b..ce0b2880afc277210412861ebac44b98a42ed9b6 100644 (file)
@@ -13,7 +13,7 @@ export const MESSAGE_BUFFER_BYTELENGTH: i32 = BLOCKHASH_BYTELENGTH << 10
 export const PRV_BUFFER_BYTELENGTH: i32 = KEY_BYTELENGTH
 export const PUB_BUFFER_BYTELENGTH: i32 = KEY_BYTELENGTH
 export const OUTPUT_BUFFER_BYTELENGTH: i32 = 1 << 10
-export const SIGNATURE_BUFFER_BYTELENGTH: i32 = SIGNATURE_BYTELENGTH << 10
+export const SIGNATURE_BUFFER_BYTELENGTH: i32 = SIGNATURE_BYTELENGTH
 
 // Static I/O buffers
 const MESSAGE_BUFFER = new StaticArray<u8>(MESSAGE_BUFFER_BYTELENGTH)
@@ -27,7 +27,7 @@ export function getMessagePointer (): usize {
        return changetype<usize>(MESSAGE_BUFFER)
 }
 
-/** Returns the pointer to the static output buffer (512 bytes). */
+/** Returns the pointer to the static output buffer (256 bytes). */
 export function getOutputPointer (): usize {
        return changetype<usize>(OUTPUT_BUFFER)
 }
@@ -160,9 +160,6 @@ export function verify (mlen: i32): void {
        // Clear output buffer of prior data, then copy local result to output buffer
        OUTPUT_BUFFER.fill(0)
        OUTPUT_BUFFER[0] = u8(verified)
-
-       // Clear local result
-       sign_sig.fill(0)
 }
 
 
@@ -216,5 +213,5 @@ export function verify_blocks (count: i32): void {
 
        // Clear output buffer of prior data, then copy local result to output buffer
        OUTPUT_BUFFER.fill(0)
-       memory.copy(changetype<usize>(OUTPUT_BUFFER), changetype<usize>(verify_blocks_out), count * 32)
+       memory.copy(changetype<usize>(OUTPUT_BUFFER), changetype<usize>(verify_blocks_out), count)
 }
index a0d53c92fb3e14c0c4c9f2538dc029d876004abd..fb51cf9dc95ae6346851ed4005de4b3c20c7478d 100644 (file)
@@ -30,6 +30,12 @@ export function verify (sig: unknown, msg: unknown, pub: unknown): boolean {
 export function verify_blocks (pub: unknown, data: unknown): boolean[] {
        const blocks: unknown[] = Array.isArray(data) ? data : [data]
        const count = blocks.length
+       if (count < 1) {
+               throw new RangeError('Must pass at least one block', { cause: data })
+       }
+       if (256 < count) {
+               throw new RangeError('Bulk Nano block verification must be no more than 256 blocks', { cause: data })
+       }
        const verified = new Uint8Array(count)
        let buffer = new Uint8Array(exports.memory.buffer)
        try {
@@ -54,7 +60,7 @@ export function verify_blocks (pub: unknown, data: unknown): boolean[] {
                        p += SIG_LEN
                }
                exports.verify_blocks(blocks.length)
-               for (let i = 0; i < SIG_LEN; i++) {
+               for (let i = 0; i < count; i++) {
                        verified[i] = buffer[OUT_PTR + i]
                }
                return [...verified].map(v => v === 0)