"disable": [
"mutable-globals"
],
- "noColors": true
+ "noColors": true,
+ "use": [
+ "abort=src/assembly/abort/abort"
+ ]
}
-}
+}
\ No newline at end of file
--- /dev/null
+//! 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)
+}
//! 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
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)
}
/** 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]
}
/** 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]
}
/** 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]
}
* 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)
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
// Clear signature output buffer if signing failed
if (result != 0) {
OUTPUT_SIGN.fill(0)
+ raise(ERROR_INVALID_PUBLIC_KEY)
}
- return result
}
/**
* @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)
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
INPUT_MSG.fill(0)
pub.fill(0)
sig.fill(0)
- return 0
}
/**
* 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)
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
h.fill(0)
pub.fill(0)
sig.fill(0)
- return 0
}
//! 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)
}
}
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]
}
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]
}
--- /dev/null
+//! 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))
+}
//! 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
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)
}
//@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
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
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
}