From 02353e18ab8aa0f23bc4a2650f29b816df973092 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sat, 26 Sep 2026 20:00:36 -0700 Subject: [PATCH] Revert to array-based word copy with endianness check since hardened getUint32 is way to slow getting half speed. --- src/lib/primordials.ts | 25 +++++++++++++++---------- src/lib/wasm.ts | 36 ++++++++++++++++++++++-------------- test/node.mjs | 5 +++++ 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/lib/primordials.ts b/src/lib/primordials.ts index 2d44108..101ff48 100644 --- a/src/lib/primordials.ts +++ b/src/lib/primordials.ts @@ -41,13 +41,12 @@ /** Type alias for legibility. */ export type Bytes = Uint8Array -export type View = DataView -export const isArray = Array.isArray +export type Words = Uint32Array /** `TypedArray` constructors, bound before anything can replace the globals. */ const Bytes = Uint8Array +const Words = Uint32Array const TypedArray = Object.getPrototypeOf(Bytes.prototype) -const View = DataView const bytesSet = Function.prototype.call.bind(Bytes.prototype.set) const bytesFill = Function.prototype.call.bind(Bytes.prototype.fill) @@ -112,6 +111,9 @@ export function fill (target: Uint8Array, value: number, start?: number, end?: n bytesFill(target, value, start, end) } +/** Captured version of `Array.isArray(value)`. */ +export const isArray = Array.isArray + /** * True only for a genuine `Uint8Array` backed by a non-shared `ArrayBuffer`. * Reads internal slot `[[TypedArrayName]]`, so a subclass overriding @@ -121,24 +123,27 @@ export function isBytes (a: unknown): a is Bytes { return getTag(a) === 'Uint8Array' && isArrayBuffer(getBuffer(a)) } +/** Checks system endianness. Returns true if little-endian, else false. */ +export const isLE = new Bytes(getBuffer(new Words([1])))[0] === 1 + /** - * Captured version of `DataView` reusing input offset and length. - * Enables reading multiple bytes in a single - * load which is more efficient than reading and combining individual bytes - * with bitshifts. The caller is responsible for managing endianness. + * Captured version of `Uint32Array` reusing input offset and length. + * Enables reading multiple bytes in a single load which is more efficient than + * reading and combining individual bytes with bitshifts. The caller is + * responsible for managing endianness. * * Every boundary-crossing buffer comes from here or `normalize()`, so the * offset is always 0 in practice; the offset check is a guard against mishap. */ -export function view (bytes: Bytes): View { +export function words (bytes: Bytes): Words { if (!isBytes(bytes)) { - throw new TypeError('Error creating DataView', { cause: bytes }) + throw new TypeError('Error creating Uint32Array', { cause: bytes }) } const offset = getByteOffset(bytes) if (offset !== 0) { throw new RangeError('Unexpected byte offset', { cause: offset }) } - return new View(getBuffer(bytes) as ArrayBuffer, offset, getByteLength(bytes)) + return new Words(getBuffer(bytes) as ArrayBuffer, offset, getByteLength(bytes) >> 2) } const LOWER = '0123456789abcdef' diff --git a/src/lib/wasm.ts b/src/lib/wasm.ts index 15d4ee3..595540e 100644 --- a/src/lib/wasm.ts +++ b/src/lib/wasm.ts @@ -4,7 +4,7 @@ //@ts-expect-error import nano25519_wasm from '../../build/nano25519.wasm' import { Nano25519MutexError, Nano25519TypeError, abort, errorCodes, } from './errors' -import { Bytes, View, byteLength, copy, view, } from './primordials' +import { Bytes, Words, byteLength, copy, isLE, words } from './primordials' type Exports = WebAssembly.Instance['exports'] & { clearMemory: () => void @@ -92,7 +92,7 @@ const MAX_MESSAGE_BYTELENGTH = exports.MAX_MESSAGE_BYTELENGTH.value * This is the path for partial words. It runs for the short final chunk of any * message and ensures little-endian byte order and zero-padded filler. */ -function setInputMsgEnd (m: Bytes, i: number): void { +function setInputMsgShift (m: Bytes, i: number): void { exports.setInputMsg(i, (m[i] ?? 0) | ((m[i + 1] ?? 0) << 8) | ((m[i + 2] ?? 0) << 16) | ((m[i + 3] ?? 0) << 24), (m[i + 4] ?? 0) | ((m[i + 5] ?? 0) << 8) | ((m[i + 6] ?? 0) << 16) | ((m[i + 7] ?? 0) << 24), @@ -105,6 +105,22 @@ function setInputMsgEnd (m: Bytes, i: number): void { ) } +function setInputMsgWords (m: Words, i: number): void { + const p = i >> 2 + exports.setInputMsg(i, + m[p], + m[p + 1], + m[p + 2], + m[p + 3], + m[p + 4], + m[p + 5], + m[p + 6], + m[p + 7] + ) +} + +const setInputMsgEndian = isLE ? setInputMsgWords : setInputMsgShift + /** * Copies the bytes of a message for signing, or signature verification, into * the static message input buffer of the WASM module. The entire input is @@ -122,21 +138,13 @@ function setInputMsgEnd (m: Bytes, i: number): void { function setInputMsg (message: Bytes): void { const mlen = byteLength(message) const mlentrunc = mlen & ~31 - const m = view(message) + const m = words(message) for (let i = 0; i < mlentrunc; i += 32) { - exports.setInputMsg(i, - m.getUint32(i, true), - m.getUint32(i + 4, true), - m.getUint32(i + 8, true), - m.getUint32(i + 12, true), - m.getUint32(i + 16, true), - m.getUint32(i + 20, true), - m.getUint32(i + 24, true), - m.getUint32(i + 28, true) - ) + //@ts-expect-error + setInputMsgEndian(m, i) } if (mlentrunc < mlen) { - setInputMsgEnd(message, mlentrunc) + setInputMsgShift(message, mlentrunc) } } diff --git a/test/node.mjs b/test/node.mjs index 519d936..b37e459 100644 --- a/test/node.mjs +++ b/test/node.mjs @@ -656,6 +656,11 @@ const patchedBlocks = [{ hash: NANO_ORG_VECTOR.blockHashBytes, signature: NANO_O const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype) /** @type {[string, (() => () => void)][]} */ const patches = [ + ['Function.prototype.call', () => { + const real = Function.prototype.call + Function.prototype.call = () => { throw new Error('patched') } + return () => { Function.prototype.call = real } + }], ['Uint8Array.prototype.slice', () => { const real = Uint8Array.prototype.slice Uint8Array.prototype.slice = () => { throw new Error('patched') } -- 2.52.0