]> git.codecow.com Git - nano25519.git/commitdiff
Use custom abort handler.
authorChris Duncan <chris@zoso.dev>
Wed, 23 Sep 2026 20:19:45 +0000 (13:19 -0700)
committerChris Duncan <chris@zoso.dev>
Wed, 23 Sep 2026 20:19:45 +0000 (13:19 -0700)
asconfig.json
src/assembly/abort.ts [new file with mode: 0644]
src/assembly/errors.ts
src/assembly/index.ts
src/assembly/tests.ts
src/lib/errors.ts [new file with mode: 0644]
src/lib/sign.ts
src/lib/wasm.ts
test/node.mjs

index fa1b91708dd8d5b097cf44d48f921a605258ee69..480f0cbf68f8c5f06fdc5861a0d77f581cdd7af5 100644 (file)
@@ -21,6 +21,9 @@
                "disable": [
                        "mutable-globals"
                ],
-               "noColors": true
+               "noColors": true,
+               "use": [
+                       "abort=src/assembly/abort/abort"
+               ]
        }
-}
+}
\ No newline at end of file
diff --git a/src/assembly/abort.ts b/src/assembly/abort.ts
new file mode 100644 (file)
index 0000000..dd9a3da
--- /dev/null
@@ -0,0 +1,50 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { ERROR_UNKNOWN, } from './errors'
+
+/**
+ * The module's only import, installed as AssemblyScript's `abort` by
+ * `use: ["abort=src/assembly/abort/abort"]` in `asconfig.json`.
+ *
+ * It takes four integers and no pointers, so the host never reads wasm memory
+ * to report an error. The previous handler decoded a UTF-16 string out of
+ * linear memory, which is the pointer arithmetic this replaces.
+ */
+@external('env', 'abort')
+declare function env_abort (code: i32, line: i32, a: i32, b: i32): void
+
+let errorCode: i32 = ERROR_UNKNOWN
+let detailA: i32 = 0
+let detailB: i32 = 0
+
+/**
+ * Throws, reporting `code` and up to two context integers to the host.
+ *
+ * **Must stay `@inline`.** AssemblyScript fills in the line number at the
+ * `throw`, so inlining is what makes the host report the caller's line rather
+ * than this file's.
+ */
+//@ts-expect-error
+@inline
+export function raise (code: i32, a: i32 = 0, b: i32 = 0): void {
+       errorCode = code
+       detailA = a
+       detailB = b
+       throw new Error()
+}
+
+/**
+ * Reads the pending code, resets it, and hands it to the host. Resetting
+ * *before* the call matters: `env_abort` throws, so nothing after it runs, and
+ * a stale code would otherwise be attributed to the next uncoded abort.
+ */
+export function abort (message: string | null, fileName: string | null, line: u32, col: u32): void {
+       const code = errorCode
+       const a = detailA
+       const b = detailB
+       errorCode = ERROR_UNKNOWN
+       detailA = 0
+       detailB = 0
+       env_abort(code, i32(line), a, b)
+}
index c318d9ebcb88dec43c74983d7c3f1627a72b0ddb..ed4e6e59f704015f019d9abd694a6390d78b8103 100644 (file)
@@ -1,9 +1,46 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-export const errors = [
-    '',
-    'Invalid public key',
-    'Invalid message length',
-    'Invalid block count'
-] 
\ No newline at end of file
+/**
+ * Error codes reported to the host through `env.abort`. These values are a
+ * public contract: `src/lib/errors.ts` turns them back into message text, and
+ * they are re-exported as wasm globals so the host reads the numbers from the
+ * module rather than restating them.
+ *
+ * `ERROR_UNKNOWN` is the resting value. An abort that reports it came from a
+ * site that never called `raise` — the stub allocator running out of heap, or
+ * a `StaticArray` constructor rejecting its size. Both are init-time.
+ */
+export const ERROR_UNKNOWN: i32 = 0
+export const ERROR_INVALID_PUBLIC_KEY: i32 = 1
+export const ERROR_INVALID_MESSAGE_LENGTH: i32 = 2
+export const ERROR_INVALID_BLOCK_COUNT: i32 = 3
+/**
+ * Raised by the byte accessors. `a` is the buffer id below, `b` the offending
+ * index or value. The buffer id is what identifies the site: `line` cannot,
+ * because AssemblyScript fixes it at the `throw` inside `raise`, so every
+ * coded abort reports that one line. `line` is only meaningful for
+ * `ERROR_UNKNOWN`, where it points into the AssemblyScript runtime.
+ */
+export const ERROR_INDEX_OUT_OF_RANGE: i32 = 4
+export const ERROR_BYTE_OUT_OF_RANGE: i32 = 5
+
+export const BUFFER_INPUT_MSG: i32 = 0
+export const BUFFER_INPUT_PRV: i32 = 1
+export const BUFFER_INPUT_PUB: i32 = 2
+export const BUFFER_INPUT_SIG: i32 = 3
+export const BUFFER_OUTPUT_DERIVE: i32 = 4
+export const BUFFER_OUTPUT_SIGN: i32 = 5
+export const BUFFER_OUTPUT_VERIFY: i32 = 6
+
+/**
+ * `equalbytes` start-function guard. `a` is the vector index (0 `zeros`,
+ * 1 `ones`, 2 `spread`); `b` encodes the corruption the comparison failed to
+ * detect, and is 0 for `SELFTEST_XPASS`.
+ *
+ * - `SELFTEST_XFAIL_ONE`: `b` is `(position << 8) | mask`
+ * - `SELFTEST_XFAIL_PAIR`: `b` is `(position << 8) | position`, both `^= 0x80`
+ */
+export const ERROR_SELFTEST_XPASS: i32 = 6
+export const ERROR_SELFTEST_XFAIL_ONE: i32 = 7
+export const ERROR_SELFTEST_XFAIL_PAIR: i32 = 8
index d300801ddd0d95a0c4b7220039d08a795fde56a4..a51f8638add9e65445b581c2dd4db6b058a55438 100644 (file)
@@ -5,9 +5,13 @@ import { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIF
 import { crypto_derive, } from './crypto_derive'
 import { crypto_sign, } from './crypto_sign'
 import { crypto_verify_decodepubkey, crypto_verify_donna, crypto_verify_sodium, } from './crypto_verify'
+import { BUFFER_INPUT_MSG, BUFFER_INPUT_PRV, BUFFER_INPUT_PUB, BUFFER_INPUT_SIG, BUFFER_OUTPUT_DERIVE, BUFFER_OUTPUT_SIGN, BUFFER_OUTPUT_VERIFY, ERROR_INDEX_OUT_OF_RANGE, ERROR_BYTE_OUT_OF_RANGE, ERROR_INVALID_BLOCK_COUNT, ERROR_INVALID_MESSAGE_LENGTH, ERROR_INVALID_PUBLIC_KEY, ERROR_SELFTEST_XFAIL_ONE, ERROR_SELFTEST_XFAIL_PAIR, ERROR_SELFTEST_XPASS, ERROR_UNKNOWN, } from './errors'
+import { raise, } from './abort'
 import './tests'
 
 export { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIFY_BLOCKS, MAX_VERIFY_BLOCKS_BYTELENGTH, SIGNATURE_BYTELENGTH }
+export { BUFFER_INPUT_MSG, BUFFER_INPUT_PRV, BUFFER_INPUT_PUB, BUFFER_INPUT_SIG, BUFFER_OUTPUT_DERIVE, BUFFER_OUTPUT_SIGN, BUFFER_OUTPUT_VERIFY }
+export { ERROR_INDEX_OUT_OF_RANGE, ERROR_BYTE_OUT_OF_RANGE, ERROR_INVALID_BLOCK_COUNT, ERROR_INVALID_MESSAGE_LENGTH, ERROR_INVALID_PUBLIC_KEY, ERROR_SELFTEST_XFAIL_ONE, ERROR_SELFTEST_XFAIL_PAIR, ERROR_SELFTEST_XPASS, ERROR_UNKNOWN }
 
 // Static I/O buffers
 const INPUT_MSG = new StaticArray<u8>(MAX_MESSAGE_BYTELENGTH)
@@ -31,57 +35,53 @@ export function clearMemory (): void {
 }
 
 /** Sets a byte in the static message input buffer. */
-export function setInputMsgByte (index: i32, v0: i32): i32 {
+export function setInputMsgByte (index: i32, v0: i32): void {
        if (index < 0 || MAX_MESSAGE_BYTELENGTH <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_MSG, index)
        }
        if (v0 < 0 || 0xff < v0) {
-               return -2
+               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_MSG, v0)
        }
        INPUT_MSG[index] = u8(v0)
-       return 0
 }
 
 /** Sets a byte in the static private key input buffer (32 bytes). */
-export function setInputPrvByte (index: i32, value: i32): i32 {
+export function setInputPrvByte (index: i32, value: i32): void {
        if (index < 0 || KEY_BYTELENGTH <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PRV, index)
        }
        if (value < 0 || 0xff < value) {
-               return -2
+               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PRV, value)
        }
        INPUT_PRV[index] = u8(value)
-       return 0
 }
 
 /** Sets a byte in the static public key input buffer (32 bytes). */
-export function setInputPubByte (index: i32, value: i32): i32 {
+export function setInputPubByte (index: i32, value: i32): void {
        if (index < 0 || KEY_BYTELENGTH <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PUB, index)
        }
        if (value < 0 || 0xff < value) {
-               return -2
+               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PUB, value)
        }
        INPUT_PUB[index] = u8(value)
-       return 0
 }
 
 /** Sets a byte in the static signature input buffer (64 bytes). */
-export function setInputSigByte (index: i32, value: i32): i32 {
+export function setInputSigByte (index: i32, value: i32): void {
        if (index < 0 || SIGNATURE_BYTELENGTH <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_SIG, index)
        }
        if (value < 0 || 0xff < value) {
-               return -2
+               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_SIG, value)
        }
        INPUT_SIG[index] = u8(value)
-       return 0
 }
 
 /** Gets a byte from the derive() static output buffer (32 bytes). */
 export function getOutputDeriveByte (index: i32): i32 {
        if (index < 0 || KEY_BYTELENGTH <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_OUTPUT_DERIVE, index)
        }
        return OUTPUT_DERIVE[index]
 }
@@ -89,7 +89,7 @@ export function getOutputDeriveByte (index: i32): i32 {
 /** Gets a byte from the sign() static output buffer (64 bytes). */
 export function getOutputSignByte (index: i32): i32 {
        if (index < 0 || SIGNATURE_BYTELENGTH <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_OUTPUT_SIGN, index)
        }
        return OUTPUT_SIGN[index]
 }
@@ -97,7 +97,7 @@ export function getOutputSignByte (index: i32): i32 {
 /** Gets a byte from the verify() static output buffer (64 bytes). */
 export function getOutputVerifyByte (index: i32): i32 {
        if (index < 0 || MAX_VERIFY_BLOCKS <= index) {
-               return -1
+               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_OUTPUT_VERIFY, index)
        }
        return OUTPUT_VERIFY[index]
 }
@@ -149,7 +149,7 @@ export function derive (): void {
  * from the message and written to the output buffer.
  * @param {i32} mlen Byte length of message to be signed, up to 65536
  */
-export function sign (mlen: i32): i32 {
+export function sign (mlen: i32): void {
        // Clear output buffer so errors do not retain stale prior data
        OUTPUT_SIGN.fill(0)
 
@@ -163,7 +163,7 @@ export function sign (mlen: i32): i32 {
                INPUT_PRV.fill(0)
                INPUT_PUB.fill(0)
                INPUT_MSG.fill(0)
-               return -2
+               raise(ERROR_INVALID_MESSAGE_LENGTH, mlen, MAX_MESSAGE_BYTELENGTH)
        }
 
        // Copy input buffers to local parameters, then clear input buffer
@@ -181,8 +181,8 @@ export function sign (mlen: i32): i32 {
        // Clear signature output buffer if signing failed
        if (result != 0) {
                OUTPUT_SIGN.fill(0)
+               raise(ERROR_INVALID_PUBLIC_KEY)
        }
-       return result
 }
 
 /**
@@ -191,7 +191,7 @@ export function sign (mlen: i32): i32 {
  * @param {i32} mlen Byte length of message that was signed, up to 65536
  * @returns {boolean} True if message was signed by public key's private key
  */
-export function verify (mlen: i32): i32 {
+export function verify (mlen: i32): void {
        // Clear output buffer so errors do not retain stale prior data
        OUTPUT_VERIFY.fill(0)
 
@@ -203,7 +203,7 @@ export function verify (mlen: i32): i32 {
        if (mlen < 0 || mlen > MAX_MESSAGE_BYTELENGTH) {
                INPUT_MSG.fill(0)
                INPUT_PUB.fill(0)
-               return -2
+               raise(ERROR_INVALID_MESSAGE_LENGTH, mlen, MAX_MESSAGE_BYTELENGTH)
        }
 
        // Copy input buffer to local parameters, then clear input buffer
@@ -219,7 +219,6 @@ export function verify (mlen: i32): i32 {
        INPUT_MSG.fill(0)
        pub.fill(0)
        sig.fill(0)
-       return 0
 }
 
 /**
@@ -239,7 +238,7 @@ export function verify (mlen: i32): i32 {
  * to the output buffer in the same order they were received.
  * @param {i32} count Number of signatures to verify, up to 64
  */
-export function verify_blocks (count: i32): i32 {
+export function verify_blocks (count: i32): void {
        // Clear output buffer so errors do not retain stale prior data
        OUTPUT_VERIFY.fill(0)
 
@@ -252,7 +251,7 @@ export function verify_blocks (count: i32): i32 {
        if (count < 1 || MAX_VERIFY_BLOCKS < count) {
                INPUT_MSG.fill(0)
                INPUT_PUB.fill(0)
-               return -3
+               raise(ERROR_INVALID_BLOCK_COUNT, count, MAX_VERIFY_BLOCKS)
        }
 
        // Copy public key input buffer to local parameter, then clear input buffer
@@ -278,5 +277,4 @@ export function verify_blocks (count: i32): i32 {
        h.fill(0)
        pub.fill(0)
        sig.fill(0)
-       return 0
 }
index 34691bc9eca5f0326ef270d46cbe0400603fd563..ed4d5120d3bfee828dc838f9d746e00de16b565e 100644 (file)
@@ -1,25 +1,34 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
+import { raise, } from './abort'
 import { KEY_BYTELENGTH } from './constants'
+import { ERROR_SELFTEST_XFAIL_ONE, ERROR_SELFTEST_XFAIL_PAIR, ERROR_SELFTEST_XPASS, } from './errors'
 import { equalbytes } from './utils'
 
 /**
  * Tests run on initialization to ensure build is correct.
+ *
+ * Failures report a code and two integers rather than a message: building the
+ * message here would pull String#join and the whole itoa path into the module
+ * for a path that runs once and then traps. The host reconstructs the text
+ * from `vector` and `detail` — see `src/lib/errors.ts`.
  */
-function err (fname: string, vector: string, expected: string, actual: string): void {
-       throw new Error(`${fname} build error from "${vector}": expected ${expected}; actual ${actual}`)
+function xpass_equalbytes (vector: i32, expected: StaticArray<u8>, actual: StaticArray<u8>): void {
+       if (!equalbytes(expected, actual, KEY_BYTELENGTH)) {
+               raise(ERROR_SELFTEST_XPASS, vector, 0)
+       }
 }
 
-function xpass_equalbytes (vector: string, expected: StaticArray<u8>, actual: StaticArray<u8>): void {
-       if (!equalbytes(expected, actual, KEY_BYTELENGTH)) {
-               err('equalbytes', vector, `pass [${expected}]`, `fail [${actual}]`)
+function xfail_equalbytes_one (vector: i32, position: i32, mask: i32, expected: StaticArray<u8>, actual: StaticArray<u8>): void {
+       if (equalbytes(expected, actual, KEY_BYTELENGTH)) {
+               raise(ERROR_SELFTEST_XFAIL_ONE, vector, (position << 8) | mask)
        }
 }
 
-function xfail_equalbytes (vector: string, expected: StaticArray<u8>, actual: StaticArray<u8>): void {
+function xfail_equalbytes_pair (vector: i32, i: i32, j: i32, expected: StaticArray<u8>, actual: StaticArray<u8>): void {
        if (equalbytes(expected, actual, KEY_BYTELENGTH)) {
-               err('equalbytes', vector, `fail [${expected}]`, `pass [${actual}]`)
+               raise(ERROR_SELFTEST_XFAIL_PAIR, vector, (i << 8) | j)
        }
 }
 
@@ -37,23 +46,22 @@ for (let i = 0; i < KEY_BYTELENGTH; i++) {
        spread[i] = u8(i * 37 + 11)
 }
 
-const vectors = ['zeros', 'ones', 'spread',]
+// Vector index is the error detail: 0 zeros, 1 ones, 2 spread
 const expecteds = [zeros, ones, spread,]
 
 const actual = new StaticArray<u8>(KEY_BYTELENGTH)
 
-for (let v = 0; v < vectors.length; v++) {
-       const vector = vectors[v]
+for (let v = 0; v < expecteds.length; v++) {
        const expected = expecteds[v]
        memory.copy(changetype<usize>(actual), changetype<usize>(expected), KEY_BYTELENGTH)
-       xpass_equalbytes(vector, expected, actual)
+       xpass_equalbytes(v, expected, actual)
 
        for (let i = 0; i < KEY_BYTELENGTH; i++) {
 
                // verify any individual bit difference fails
                for (let j = 1; j < 256; j++) {
                        actual[i] ^= u8(j)
-                       xfail_equalbytes(vector, expected, actual)
+                       xfail_equalbytes_one(v, i, j, expected, actual)
                        actual[i] = expected[i]
                }
 
@@ -62,7 +70,7 @@ for (let v = 0; v < vectors.length; v++) {
                for (let j = i + 1; j < KEY_BYTELENGTH; j++) {
                        actual[i] ^= 128
                        actual[j] ^= 128
-                       xfail_equalbytes(vector, expected, actual)
+                       xfail_equalbytes_pair(v, i, j, expected, actual)
                        actual[i] = expected[i]
                        actual[j] = expected[j]
                }
diff --git a/src/lib/errors.ts b/src/lib/errors.ts
new file mode 100644 (file)
index 0000000..9ebeb1a
--- /dev/null
@@ -0,0 +1,118 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+import { Nano25519WasmError } from './primordials'
+
+/**
+ * Message text for the codes in `src/assembly/errors.ts`. The numbers are the
+ * contract between the two files; `wasm.ts` asserts after instantiation that
+ * every `ERROR_*` global the module exports still matches the key used here,
+ * so the two cannot drift silently.
+ *
+ * The table is keyed by number rather than read from the module because the
+ * handler has to exist *before* instantiation: the `equalbytes` guard runs in
+ * the start function, so it can abort while `exports` is still undefined.
+ */
+export const ERROR_UNKNOWN = 0
+export const ERROR_INVALID_PUBLIC_KEY = 1
+export const ERROR_INVALID_MESSAGE_LENGTH = 2
+export const ERROR_INVALID_BLOCK_COUNT = 3
+export const ERROR_INDEX_OUT_OF_RANGE = 4
+export const ERROR_BYTE_OUT_OF_RANGE = 5
+export const ERROR_SELFTEST_XPASS = 6
+export const ERROR_SELFTEST_XFAIL_ONE = 7
+export const ERROR_SELFTEST_XFAIL_PAIR = 8
+
+const VECTORS = ['zeros', 'ones', 'spread']
+
+/** Buffer ids from `src/assembly/errors.ts`, in value order. */
+const BUFFERS = ['INPUT_MSG', 'INPUT_PRV', 'INPUT_PUB', 'INPUT_SIG', 'OUTPUT_DERIVE', 'OUTPUT_SIGN', 'OUTPUT_VERIFY']
+
+/**
+ * Source file per code. `env.abort` no longer receives a file pointer, so the
+ * code carries that instead — which holds only while **each code is raised
+ * from exactly one file**. Reusing a code across files makes this lie.
+ */
+const FILES: Record<number, string> = {
+       [ERROR_INVALID_PUBLIC_KEY]: 'src/assembly/index.ts',
+       [ERROR_INVALID_MESSAGE_LENGTH]: 'src/assembly/index.ts',
+       [ERROR_INVALID_BLOCK_COUNT]: 'src/assembly/index.ts',
+       [ERROR_INDEX_OUT_OF_RANGE]: 'src/assembly/index.ts',
+       [ERROR_BYTE_OUT_OF_RANGE]: 'src/assembly/index.ts',
+       [ERROR_SELFTEST_XPASS]: 'src/assembly/tests.ts',
+       [ERROR_SELFTEST_XFAIL_ONE]: 'src/assembly/tests.ts',
+       [ERROR_SELFTEST_XFAIL_PAIR]: 'src/assembly/tests.ts'
+}
+
+/**
+ * Rebuilds a self-test vector. These are the same three definitions as
+ * `src/assembly/tests.ts`; keeping them here is what lets the host report the
+ * failing bytes without the module ever formatting a string.
+ */
+function vector (index: number, length: number): number[] {
+       const bytes: number[] = []
+       for (let i = 0; i < length; i++) {
+               bytes[i] = index === 0 ? 0 : index === 1 ? 255 : (i * 37 + 11) & 0xff
+       }
+       return bytes
+}
+
+function selftest (code: number, v: number, detail: number, length: number): string {
+       const name = VECTORS[v] ?? `vector ${v}`
+       const expected = vector(v, length)
+       const actual = vector(v, length)
+       if (code === ERROR_SELFTEST_XFAIL_ONE) {
+               const position = (detail >> 8) & 0xff
+               const mask = detail & 0xff
+               actual[position] ^= mask
+               return `equalbytes build error from "${name}": expected fail [${expected}]; actual pass [${actual}] (byte ${position} ^ 0x${mask.toString(16).padStart(2, '0')})`
+       }
+       if (code === ERROR_SELFTEST_XFAIL_PAIR) {
+               const i = (detail >> 8) & 0xff
+               const j = detail & 0xff
+               actual[i] ^= 0x80
+               actual[j] ^= 0x80
+               return `equalbytes build error from "${name}": expected fail [${expected}]; actual pass [${actual}] (bytes ${i} and ${j} ^ 0x80)`
+       }
+       return `equalbytes build error from "${name}": expected pass [${expected}]; actual fail`
+}
+
+/** Turns the four integers `env.abort` reports into a message. */
+export function describe (code: number, line: number, a: number, b: number): string {
+       let text: string
+       switch (code) {
+               case ERROR_INVALID_PUBLIC_KEY:
+                       text = 'Invalid public key'
+                       break
+               case ERROR_INVALID_MESSAGE_LENGTH:
+                       text = `Invalid message length: ${a} is not in [0, ${b}]`
+                       break
+               case ERROR_INVALID_BLOCK_COUNT:
+                       text = `Invalid block count: ${a} is not in [1, ${b}]`
+                       break
+               case ERROR_INDEX_OUT_OF_RANGE:
+                       text = `Buffer index out of range for ${BUFFERS[a] ?? `buffer ${a}`}: ${b}`
+                       break
+               case ERROR_BYTE_OUT_OF_RANGE:
+                       text = `Byte value out of range for ${BUFFERS[a] ?? `buffer ${a}`}: ${b} is not in [0, 255]`
+                       break
+               case ERROR_SELFTEST_XPASS:
+               case ERROR_SELFTEST_XFAIL_ONE:
+               case ERROR_SELFTEST_XFAIL_PAIR:
+                       text = selftest(code, a, b, 32)
+                       break
+               default:
+                       text = `Unrecognized wasm abort (code ${code}, a ${a}, b ${b})`
+       }
+       // `line` only locates an ERROR_UNKNOWN abort. Every coded site reports the
+       // `throw` inside `raise`, because AssemblyScript fixes the line before
+       // inlining — so for coded errors the code and its details are the location.
+       return code === ERROR_UNKNOWN
+               ? `${text}, row ${line}`
+               : `${text}, ${FILES[code] ?? 'unknown file'}`
+}
+
+/** The module's only import. Takes four integers and never reads wasm memory. */
+export function abort (code: number, line: number, a: number, b: number): never {
+       throw new Nano25519WasmError(describe(code, line, a, b))
+}
index b9e5a04f348e134b19a911b6340e2d0f737d6a6b..b5d069bb0b5238716b69268826b8700e456cc160 100644 (file)
@@ -1,7 +1,7 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { allocate, byteLength, bytesToHex, copy, fill, isBytes, Nano25519WasmError, normalize } from './primordials'
+import { allocate, byteLength, bytesToHex, copy, fill, isBytes, normalize } from './primordials'
 import { clearMemory, constants, MemoryBuffers, Mutex, sign as wasm_sign } from './wasm'
 
 const { KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, SIGNATURE_BYTELENGTH, } = constants
@@ -25,10 +25,7 @@ export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown):
                for (let i = 0; i < KEY_BYTELENGTH; i++) {
                        MemoryBuffers.INPUT_PUB(i)(publicKey[i])
                }
-               const result = wasm_sign(message.byteLength)
-               if (result !== 0) {
-                       throw new Nano25519WasmError('Invalid public key')
-               }
+               wasm_sign(message.byteLength)
                for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
                        signature[i] = MemoryBuffers.OUTPUT_SIGN(i)
                }
index cb62d4bc23ec034009b32f1f4b004f34ff75b6f9..7fd6c7c2ac2a9da92de56dd27bd0ce1da422ebd8 100644 (file)
@@ -4,17 +4,18 @@
 //@ts-expect-error
 import nano25519_wasm from '../../build/nano25519.wasm'
 import { Nano25519TypeError, copy } from './primordials'
+import * as errors from './errors'
 
 type Exports = WebAssembly.Instance['exports'] & {
        clearMemory: () => void
        derive: () => void
-       sign: (mlen: number) => number
-       verify: (mlen: number) => number
-       verify_blocks: (count: number) => number
-       setInputMsgByte: (index: number, value: number) => number
-       setInputPrvByte: (index: number, value: number) => number
-       setInputPubByte: (index: number, value: number) => number
-       setInputSigByte: (index: number, value: number) => number
+       sign: (mlen: number) => void
+       verify: (mlen: number) => void
+       verify_blocks: (count: number) => void
+       setInputMsgByte: (index: number, value: number) => void
+       setInputPrvByte: (index: number, value: number) => void
+       setInputPubByte: (index: number, value: number) => void
+       setInputSigByte: (index: number, value: number) => void
        getOutputDeriveByte: (index: number) => number
        getOutputSignByte: (index: number) => number
        getOutputVerifyByte: (index: number) => number
@@ -37,9 +38,24 @@ export const Mutex = {
 const wasm = copy(nano25519_wasm)
 const module = new WebAssembly.Module(wasm)
 
-const instance = new WebAssembly.Instance(module, { env: { abort: () => { } } })
+const instance = new WebAssembly.Instance(module, { env: { abort: errors.abort } })
 const exports = instance.exports as Exports
 
+/**
+ * The error codes are a contract between `src/assembly/errors.ts` and
+ * `src/lib/errors.ts`. The host table cannot be read from the module, because
+ * the handler has to exist before instantiation, so it is checked against the
+ * module here instead. A mismatch means one side was renumbered alone.
+ */
+for (const name of Object.keys(errors)) {
+       if (!name.startsWith('ERROR_')) continue
+       const global = exports[name] as WebAssembly.Global | undefined
+       if (global == null) throw new Nano25519TypeError(`Missing wasm error code ${name}`)
+       if (global.value !== (errors as Record<string, unknown>)[name]) {
+               throw new Nano25519TypeError(`Error code ${name} is ${global.value} in wasm, ${(errors as Record<string, unknown>)[name]} in host`)
+       }
+}
+
 const BLOCKHASH_BYTELENGTH = exports.BLOCKHASH_BYTELENGTH.value
 const KEY_BYTELENGTH = exports.KEY_BYTELENGTH.value
 const SIGNATURE_BYTELENGTH = exports.SIGNATURE_BYTELENGTH.value
index cc82c925bc7789ea58a7f820da94ef75be66e96e..e22a5a9e2e43c4f91dc432c4387bbd6bb064a4b5 100644 (file)
@@ -164,7 +164,7 @@ try {
        result = false
 } catch (err) {
        if (err != null && typeof err === 'object' && 'message' in err && typeof err.message === 'string') {
-               result = err.constructor.name === 'Nano25519WasmError' && err.message === 'Invalid public key'
+               result = err.constructor.name === 'Nano25519WasmError' && err.message === 'Invalid public key, src/assembly/index.ts'
        } else {
                result = false
        }