]> git.codecow.com Git - nano25519.git/commitdiff
Refactor input copying from host to WASM to batch bytes and vastly improve performanc...
authorChris Duncan <chris@codecow.com>
Fri, 25 Sep 2026 16:33:43 +0000 (09:33 -0700)
committerChris Duncan <chris@codecow.com>
Fri, 25 Sep 2026 16:33:43 +0000 (09:33 -0700)
src/assembly/index.ts
src/lib/derive.ts
src/lib/errors.ts
src/lib/sign.ts
src/lib/verify.ts
src/lib/wasm.ts

index 1da0d8aa3421f53251902b5112180d0ecbb93146..b82b2d010db06e89102860ce73884dd274e724dc 100644 (file)
@@ -21,6 +21,23 @@ const OUTPUT_DERIVE = new StaticArray<u8>(KEY_BYTELENGTH)
 const OUTPUT_SIGN = new StaticArray<u8>(SIGNATURE_BYTELENGTH)
 const OUTPUT_VERIFY = new StaticArray<u8>(MAX_VERIFY_BLOCKS)
 
+/**
+ * Sets 32 bytes at a time in a buffer memory address specified by `p`.
+ *
+ * A host-to-module crossing costs roughly the same regardless of payload width,
+ * so batching 32 bytes is much more efficient than writing individual bytes.
+ */
+function set32Bytes (p: usize, v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+       store<u32>(p, v0)
+       store<u32>(p + 4, v1)
+       store<u32>(p + 8, v2)
+       store<u32>(p + 12, v3)
+       store<u32>(p + 16, v4)
+       store<u32>(p + 20, v5)
+       store<u32>(p + 24, v6)
+       store<u32>(p + 28, v7)
+}
+
 /** Clears all input and output buffers. */
 export function clearMemory (): void {
        INPUT_MSG.fill(0)
@@ -32,65 +49,36 @@ export function clearMemory (): void {
        OUTPUT_VERIFY.fill(0)
 }
 
-/** Sets a byte in the static message input buffer. */
-export function setInputMsgByte (index: i32, v0: i32): void {
-       if (index < 0 || MAX_MESSAGE_BYTELENGTH <= index) {
+/**
+ * Sets 32 bytes in the static message input buffer. Index range check accounts
+ * for a full batch width. The host code must pack the input bytes in
+ * little-endian order since WASM is little-endian by definition.
+ */
+export function setInputMsg (index: i32, v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+       if (index < 0 || MAX_MESSAGE_BYTELENGTH - 32 < index) {
                raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_MSG, index)
        }
-       if (v0 < 0 || 0xff < v0) {
-               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_MSG, v0)
-       }
-       INPUT_MSG[index] = u8(v0)
+       const p: usize = changetype<usize>(INPUT_MSG) + index
+       set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
 }
 
-/** Sets 4 bytes in the static message input buffer. */
-export function setInputMsg32 (index: i32, v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
-       if (index < 0 || MAX_MESSAGE_BYTELENGTH <= index) {
-               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_MSG, index)
-       }
-       const v = [v0, v1, v2, v3, v4, v5, v6, v7]
-       for (let i = 0; i < 8; i++) {
-               if (v[i] < 0 || 0xffffffff < v[i]) {
-                       raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_MSG, v[i])
-               }
-               INPUT_MSG[index + (i << 2)] = u8((v[i] >>> 24) & 255)
-               INPUT_MSG[index + (i << 2) + 1] = u8((v[i] >>> 16) & 255)
-               INPUT_MSG[index + (i << 2) + 2] = u8((v[i] >>> 8) & 255)
-               INPUT_MSG[index + (i << 2) + 3] = u8(v[i] & 255)
-       }
+/** Sets all 32 bytes in the static private key input buffer. */
+export function setInputPrv (v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+       const p: usize = changetype<usize>(INPUT_PRV)
+       set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
 }
 
-/** Sets a byte in the static private key input buffer (32 bytes). */
-export function setInputPrvByte (index: i32, value: i32): void {
-       if (index < 0 || KEY_BYTELENGTH <= index) {
-               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PRV, index)
-       }
-       if (value < 0 || 0xff < value) {
-               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PRV, value)
-       }
-       INPUT_PRV[index] = u8(value)
+/** Sets all 32 bytes of the static public key input buffer. */
+export function setInputPub (v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32): void {
+       const p: usize = changetype<usize>(INPUT_PUB)
+       set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
 }
 
-/** Sets a byte in the static public key input buffer (32 bytes). */
-export function setInputPubByte (index: i32, value: i32): void {
-       if (index < 0 || KEY_BYTELENGTH <= index) {
-               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PUB, index)
-       }
-       if (value < 0 || 0xff < value) {
-               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PUB, value)
-       }
-       INPUT_PUB[index] = u8(value)
-}
-
-/** Sets a byte in the static signature input buffer (64 bytes). */
-export function setInputSigByte (index: i32, value: i32): void {
-       if (index < 0 || SIGNATURE_BYTELENGTH <= index) {
-               raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_SIG, index)
-       }
-       if (value < 0 || 0xff < value) {
-               raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_SIG, value)
-       }
-       INPUT_SIG[index] = u8(value)
+/** Sets all 64 bytes of the static signature input buffer. */
+export function setInputSig (v0: u32, v1: u32, v2: u32, v3: u32, v4: u32, v5: u32, v6: u32, v7: u32, v8: u32, v9: u32, v10: u32, v11: u32, v12: u32, v13: u32, v14: u32, v15: u32): void {
+       const p: usize = changetype<usize>(INPUT_SIG)
+       set32Bytes(p, v0, v1, v2, v3, v4, v5, v6, v7)
+       set32Bytes(p + 32, v8, v9, v10, v11, v12, v13, v14, v15)
 }
 
 /** Gets a byte from the derive() static output buffer (32 bytes). */
index 44569228dcd71ea2ac6503414bd4d6bd46abf5c8..160fa9b986a5aa9654caf96b95f36d29ab04ebaf 100644 (file)
@@ -2,7 +2,7 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { allocate, byteLength, bytesToHex, copy, fill, isBytes, normalize } from './primordials'
-import { MemoryBuffers, Mutex, clearMemory, constants, derive as wasm_derive, } from './wasm'
+import { clearMemory, constants, getOutput, Mutex, setInput, derive as wasm_derive, } from './wasm'
 
 const { KEY_BYTELENGTH, } = constants
 
@@ -14,12 +14,10 @@ export function derive (prv: unknown, out?: unknown): string | void {
        const publicKey = allocate(KEY_BYTELENGTH)
        Mutex.lock()
        try {
-               for (let i = 0; i < KEY_BYTELENGTH; i++) {
-                       MemoryBuffers.INPUT_PRV(i, privateKey[i])
-               }
+               setInput('prv', privateKey)
                wasm_derive()
                for (let i = 0; i < KEY_BYTELENGTH; i++) {
-                       publicKey[i] = MemoryBuffers.OUTPUT_DERIVE(i)
+                       publicKey[i] = getOutput('derive', i)
                }
                if (out != null) {
                        copy(out, publicKey)
index a74d7d6151cb4e6b1cb1c2f2812ab736e267181e..aaa10118a10766d25ba4b74a0520f8d1d5727c0c 100644 (file)
@@ -1,6 +1,7 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
+export class Nano25519MutexError extends RangeError { }
 export class Nano25519TypeError extends TypeError { }
 export class Nano25519WasmError extends Error { }
 
index 812a2d9e463a155820a7c6fa9e799773dfc41a28..85d071017945630cda261ff0e66d171cf207e09d 100644 (file)
@@ -2,7 +2,7 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { allocate, byteLength, bytesToHex, copy, fill, isBytes, normalize } from './primordials'
-import { MemoryBuffers, Mutex, clearMemory, constants, sign as wasm_sign } from './wasm'
+import { Mutex, clearMemory, constants, getOutput, setInput, sign as wasm_sign } from './wasm'
 
 const { KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, SIGNATURE_BYTELENGTH, } = constants
 
@@ -16,18 +16,12 @@ export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown):
        const signature = allocate(SIGNATURE_BYTELENGTH)
        Mutex.lock()
        try {
-               for (let i = 0; i < message.byteLength; i++) {
-                       MemoryBuffers.INPUT_MSG(i, message[i])
-               }
-               for (let i = 0; i < KEY_BYTELENGTH; i++) {
-                       MemoryBuffers.INPUT_PRV(i, privateKey[i])
-               }
-               for (let i = 0; i < KEY_BYTELENGTH; i++) {
-                       MemoryBuffers.INPUT_PUB(i, publicKey[i])
-               }
+               setInput('msg', message)
+               setInput('prv', privateKey)
+               setInput('pub', publicKey)
                wasm_sign(message.byteLength)
                for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
-                       signature[i] = MemoryBuffers.OUTPUT_SIGN(i)
+                       signature[i] = getOutput('sign', i)
                }
                if (out != null) {
                        copy(out, signature)
index b87d22656337cb92389399ffdf542c6f816c1679..1307276d099d89bdba387f9947ef4acce2cb36f9 100644 (file)
@@ -2,7 +2,7 @@
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
 import { isArray, normalize } from './primordials'
-import { MemoryBuffers, Mutex, clearMemory, constants, verify as wasm_verify, verify_blocks as wasm_verify_blocks, } from './wasm'
+import { Mutex, clearMemory, constants, getOutput, setInput, verify as wasm_verify, verify_blocks as wasm_verify_blocks, } from './wasm'
 
 const { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIFY_BLOCKS, SIGNATURE_BYTELENGTH, } = constants
 
@@ -12,21 +12,11 @@ export function verify (sig: unknown, msg: unknown, pub: unknown): boolean {
                const message = normalize('message', 0, MAX_MESSAGE_BYTELENGTH, msg)
                const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub)
                const signature = normalize('signature', SIGNATURE_BYTELENGTH, SIGNATURE_BYTELENGTH, sig)
-               for (let i = 0; i < message.byteLength; i += 32) {
-                       const u32: number[] = []
-                       for (let j = 0; j < 32; j += 4) {
-                               u32[j >> 2] = ((message[i + j] ?? 0) << 24) | ((message[i + j + 1] ?? 0) << 16) | ((message[i + j + 2] ?? 0) << 8) | (message[i + j + 3] ?? 0)
-                       }
-                       MemoryBuffers.INPUT_MSG_32(i, u32[0], u32[1], u32[2], u32[3], u32[4], u32[5], u32[6], u32[7])
-               }
-               for (let i = 0; i < KEY_BYTELENGTH; i++) {
-                       MemoryBuffers.INPUT_PUB(i, publicKey[i])
-               }
-               for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
-                       MemoryBuffers.INPUT_SIG(i, signature[i])
-               }
+               setInput('msg', message)
+               setInput('pub', publicKey)
+               setInput('sig', signature)
                wasm_verify(message.byteLength)
-               const verified = MemoryBuffers.OUTPUT_VERIFY(0)
+               const verified = getOutput('verify', 0)
                return verified === 1
        } finally {
                clearMemory()
@@ -46,10 +36,9 @@ export function verify_blocks (pub: unknown, data: unknown): boolean[] {
        Mutex.lock()
        try {
                const publicKey = normalize('public key', KEY_BYTELENGTH, KEY_BYTELENGTH, pub)
-               for (let i = 0; i < KEY_BYTELENGTH; i++) {
-                       MemoryBuffers.INPUT_PUB(i, publicKey[i])
-               }
-               for (let b = 0, p = 0; b < count; b++) {
+               setInput('pub', publicKey)
+               const message = new Uint8Array(count * (SIGNATURE_BYTELENGTH + BLOCKHASH_BYTELENGTH))
+               for (let b = 0; b < count; b++) {
                        const block = blocks[b]
                        if (block == null || typeof block !== 'object') {
                                throw new TypeError('Invalid block', { cause: block })
@@ -62,18 +51,14 @@ export function verify_blocks (pub: unknown, data: unknown): boolean[] {
                        }
                        const signature = normalize('signature', SIGNATURE_BYTELENGTH, SIGNATURE_BYTELENGTH, block.signature)
                        const hash = normalize('hash', BLOCKHASH_BYTELENGTH, BLOCKHASH_BYTELENGTH, block.hash)
-                       for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) {
-                               MemoryBuffers.INPUT_MSG(p + i, signature[i])
-                       }
-                       p += SIGNATURE_BYTELENGTH
-                       for (let i = 0; i < BLOCKHASH_BYTELENGTH; i++) {
-                               MemoryBuffers.INPUT_MSG(p + i, hash[i])
-                       }
-                       p += BLOCKHASH_BYTELENGTH
+                       const offset = b * 96
+                       message.set(signature, offset)
+                       message.set(hash, offset + SIGNATURE_BYTELENGTH)
                }
+               setInput('msg', message)
                wasm_verify_blocks(count)
                for (let i = 0; i < count; i++) {
-                       verified[i] = MemoryBuffers.OUTPUT_VERIFY(i) === 1
+                       verified[i] = getOutput('verify', i) === 1
                }
                return verified
        } finally {
index 64fb24dd2cabaac487240e6f0f3e7e57628f6b51..b4fe76075d21fd523850097651e02fb07732a5de 100644 (file)
@@ -3,8 +3,8 @@
 
 //@ts-expect-error
 import nano25519_wasm from '../../build/nano25519.wasm'
-import { Nano25519TypeError, abort, errorCodes } from './errors'
-import { copy } from './primordials'
+import { Nano25519MutexError, Nano25519TypeError, abort, errorCodes, } from './errors'
+import { copy, } from './primordials'
 
 type Exports = WebAssembly.Instance['exports'] & {
        clearMemory: () => void
@@ -12,11 +12,10 @@ type Exports = WebAssembly.Instance['exports'] & {
        sign: (mlen: number) => void
        verify: (mlen: number) => void
        verify_blocks: (count: number) => void
-       setInputMsgByte: (index: number, value: number) => void
-       setInputMsg32: (index: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
-       setInputPrvByte: (index: number, value: number) => void
-       setInputPubByte: (index: number, value: number) => void
-       setInputSigByte: (index: number, value: number) => void
+       setInputMsg: (index: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
+       setInputPrv: (v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
+       setInputPub: (v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) => void
+       setInputSig: (v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number, v8: number, v9: number, v10: number, v11: number, v12: number, v13: number, v14: number, v15: number) => void
        getOutputDeriveByte: (index: number) => number
        getOutputSignByte: (index: number) => number
        getOutputVerifyByte: (index: number) => number
@@ -83,6 +82,105 @@ const MAX_VERIFY_BLOCKS = exports.MAX_VERIFY_BLOCKS.value
 const MAX_VERIFY_BLOCKS_BYTELENGTH = exports.MAX_VERIFY_BLOCKS_BYTELENGTH.value
 const MAX_MESSAGE_BYTELENGTH = exports.MAX_MESSAGE_BYTELENGTH.value
 
+/**
+ * Copies the bytes of a message for signing, or signature verification, into
+ * the static message input buffer of the WASM module. Bytes are packed in
+ * little-endian order to align with the WASM spec. The entire input is written
+ * in one call rather than looped over an intermediate array because allocating
+ * costs more than the copy itself. Each byte must be protected with a null
+ * coalescing operator in order to fall back on a zero value since the message
+ * length can vary and not be a multiple of 32 bytes.
+ */
+function setInputMsg (message: Uint8Array): void {
+       const mlen = message.byteLength
+       for (let i = 0; i < mlen; i += 32) {
+               exports.setInputMsg(i,
+                       (message[i] ?? 0) | ((message[i + 1] ?? 0) << 8) | ((message[i + 2] ?? 0) << 16) | ((message[i + 3] ?? 0) << 24),
+                       (message[i + 4] ?? 0) | ((message[i + 5] ?? 0) << 8) | ((message[i + 6] ?? 0) << 16) | ((message[i + 7] ?? 0) << 24),
+                       (message[i + 8] ?? 0) | ((message[i + 9] ?? 0) << 8) | ((message[i + 10] ?? 0) << 16) | ((message[i + 11] ?? 0) << 24),
+                       (message[i + 12] ?? 0) | ((message[i + 13] ?? 0) << 8) | ((message[i + 14] ?? 0) << 16) | ((message[i + 15] ?? 0) << 24),
+                       (message[i + 16] ?? 0) | ((message[i + 17] ?? 0) << 8) | ((message[i + 18] ?? 0) << 16) | ((message[i + 19] ?? 0) << 24),
+                       (message[i + 20] ?? 0) | ((message[i + 21] ?? 0) << 8) | ((message[i + 22] ?? 0) << 16) | ((message[i + 23] ?? 0) << 24),
+                       (message[i + 24] ?? 0) | ((message[i + 25] ?? 0) << 8) | ((message[i + 26] ?? 0) << 16) | ((message[i + 27] ?? 0) << 24),
+                       (message[i + 28] ?? 0) | ((message[i + 29] ?? 0) << 8) | ((message[i + 30] ?? 0) << 16) | ((message[i + 31] ?? 0) << 24)
+               )
+       }
+}
+
+/**
+ * Copies the bytes of a private key into the static private key input buffer of
+ * the WASM module. Bytes are packed in little-endian order to align with the
+ * WASM spec. The entire input is written in one call rather than looped over an
+ * intermediate array because allocating costs more than the copy itself.
+ */
+function setInputPrv (prv: Uint8Array): void {
+       exports.setInputPrv(
+               prv[0] | (prv[1] << 8) | (prv[2] << 16) | (prv[3] << 24),
+               prv[4] | (prv[5] << 8) | (prv[6] << 16) | (prv[7] << 24),
+               prv[8] | (prv[9] << 8) | (prv[10] << 16) | (prv[11] << 24),
+               prv[12] | (prv[13] << 8) | (prv[14] << 16) | (prv[15] << 24),
+               prv[16] | (prv[17] << 8) | (prv[18] << 16) | (prv[19] << 24),
+               prv[20] | (prv[21] << 8) | (prv[22] << 16) | (prv[23] << 24),
+               prv[24] | (prv[25] << 8) | (prv[26] << 16) | (prv[27] << 24),
+               prv[28] | (prv[29] << 8) | (prv[30] << 16) | (prv[31] << 24)
+       )
+}
+
+/**
+ * Copies the bytes of a public key into the static public key input buffer of
+ * the WASM module. Bytes are packed in little-endian order to align with the
+ * WASM spec. The entire input is written in one call rather than looped over an
+ * intermediate array because allocating costs more than the copy itself.
+ */
+function setInputPub (pub: Uint8Array): void {
+       exports.setInputPub(
+               pub[0] | (pub[1] << 8) | (pub[2] << 16) | (pub[3] << 24),
+               pub[4] | (pub[5] << 8) | (pub[6] << 16) | (pub[7] << 24),
+               pub[8] | (pub[9] << 8) | (pub[10] << 16) | (pub[11] << 24),
+               pub[12] | (pub[13] << 8) | (pub[14] << 16) | (pub[15] << 24),
+               pub[16] | (pub[17] << 8) | (pub[18] << 16) | (pub[19] << 24),
+               pub[20] | (pub[21] << 8) | (pub[22] << 16) | (pub[23] << 24),
+               pub[24] | (pub[25] << 8) | (pub[26] << 16) | (pub[27] << 24),
+               pub[28] | (pub[29] << 8) | (pub[30] << 16) | (pub[31] << 24)
+       )
+}
+
+/**
+ * Copies the bytes of a detached signature into the static signature input
+ * buffer of the WASM module. Bytes are packed in little-endian order to align
+ * with the WASM spec. The entire input is written in one call rather than looped over an
+ * intermediate array because allocating costs more than the copy itself.
+ */
+function setInputSig (sig: Uint8Array): void {
+       exports.setInputSig(
+               sig[0] | (sig[1] << 8) | (sig[2] << 16) | (sig[3] << 24),
+               sig[4] | (sig[5] << 8) | (sig[6] << 16) | (sig[7] << 24),
+               sig[8] | (sig[9] << 8) | (sig[10] << 16) | (sig[11] << 24),
+               sig[12] | (sig[13] << 8) | (sig[14] << 16) | (sig[15] << 24),
+               sig[16] | (sig[17] << 8) | (sig[18] << 16) | (sig[19] << 24),
+               sig[20] | (sig[21] << 8) | (sig[22] << 16) | (sig[23] << 24),
+               sig[24] | (sig[25] << 8) | (sig[26] << 16) | (sig[27] << 24),
+               sig[28] | (sig[29] << 8) | (sig[30] << 16) | (sig[31] << 24),
+               sig[32] | (sig[33] << 8) | (sig[34] << 16) | (sig[35] << 24),
+               sig[36] | (sig[37] << 8) | (sig[38] << 16) | (sig[39] << 24),
+               sig[40] | (sig[41] << 8) | (sig[42] << 16) | (sig[43] << 24),
+               sig[44] | (sig[45] << 8) | (sig[46] << 16) | (sig[47] << 24),
+               sig[48] | (sig[49] << 8) | (sig[50] << 16) | (sig[51] << 24),
+               sig[52] | (sig[53] << 8) | (sig[54] << 16) | (sig[55] << 24),
+               sig[56] | (sig[57] << 8) | (sig[58] << 16) | (sig[59] << 24),
+               sig[60] | (sig[61] << 8) | (sig[62] << 16) | (sig[63] << 24)
+       )
+}
+
+/**
+ * Tells the WASM module to clear its own I/O buffers internally, and then
+ * releases the mutex lock.
+ */
+export function clearMemory (): void {
+       exports.clearMemory()
+       locked = false
+}
+
 export const constants = {
        BLOCKHASH_BYTELENGTH,
        KEY_BYTELENGTH,
@@ -92,47 +190,42 @@ export const constants = {
        MAX_MESSAGE_BYTELENGTH
 }
 
-export class MemoryBuffers {
-       static #check (): void {
-               if (!locked) throw new Nano25519TypeError('Mutex not acquired')
-       }
-       static get INPUT_MSG () {
-               this.#check()
-               return exports.setInputMsgByte
-       }
-       static get INPUT_MSG_32 () {
-               this.#check()
-               return exports.setInputMsg32
-       }
-       static get INPUT_PRV () {
-               this.#check()
-               return exports.setInputPrvByte
-       }
-       static get INPUT_PUB () {
-               this.#check()
-               return exports.setInputPubByte
-       }
-       static get INPUT_SIG () {
-               this.#check()
-               return exports.setInputSigByte
-       }
-       static get OUTPUT_DERIVE () {
-               this.#check()
-               return exports.getOutputDeriveByte
-       }
-       static get OUTPUT_SIGN () {
-               this.#check()
-               return exports.getOutputSignByte
-       }
-       static get OUTPUT_VERIFY () {
-               this.#check()
-               return exports.getOutputVerifyByte
+/**
+ * Copies the bytes of a specified input parameter into the associated static
+ * input buffer of the WASM module. Called internal functions must pack input
+ * bytes in little-endian order to align with the WASM spec.
+ *
+ * A mutex lock must first be acquired by called `Mutex.lock()`. The lock is
+ * only released when `clearMemory()` is called in order to prevent re-entry
+ * while WASM memory holds sensitive data.
+ */
+export function setInput (name: 'msg' | 'prv' | 'pub' | 'sig', input: Uint8Array): void {
+       if (!locked) throw new Nano25519MutexError('Mutex not acquired')
+       switch (name) {
+               case 'msg': return setInputMsg(input)
+               case 'prv': return setInputPrv(input)
+               case 'pub': return setInputPub(input)
+               case 'sig': return setInputSig(input)
+               default: throw new Nano25519TypeError('Invalid input name', { cause: name })
        }
 }
 
-export function clearMemory (): void {
-       exports.clearMemory()
-       locked = false
+/**
+ * Copies the bytes of a specified function result from the associated static
+ * output buffer of the WASM module.
+ *
+ * A mutex lock must first be acquired by called `Mutex.lock()`. The lock is
+ * only released when `clearMemory()` is called in order to prevent re-entry
+ * while WASM memory holds sensitive data.
+ */
+export function getOutput (name: 'derive' | 'sign' | 'verify', index: number): number {
+       if (!locked) throw new Nano25519MutexError('Mutex not acquired')
+       switch (name) {
+               case 'derive': return exports.getOutputDeriveByte(index)
+               case 'sign': return exports.getOutputSignByte(index)
+               case 'verify': return exports.getOutputVerifyByte(index)
+               default: throw new Nano25519TypeError('Invalid output name', { cause: name })
+       }
 }
 
 export const { derive, sign, verify, verify_blocks } = exports