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
| `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
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
`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
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)
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)
}
// 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)
}
// 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)
}