]> git.codecow.com Git - nano25519.git/commitdiff
Abandon conditional word copy function selection in favor of reliability.
authorChris Duncan <chris@codecow.com>
Sat, 26 Sep 2026 09:12:27 +0000 (02:12 -0700)
committerChris Duncan <chris@codecow.com>
Sat, 26 Sep 2026 09:12:27 +0000 (02:12 -0700)
src/lib/primordials.ts
src/lib/wasm.ts

index 4849056fab70d765d7c70a3b15e2094168948963..5af4e945f1301e72a4572d77b6b10f61cf6b8233 100644 (file)
@@ -1,8 +1,6 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { Nano25519RangeError, Nano25519TypeError } from "./errors"
-
 /**
  * Builtins captured once, when this module is evaluated, so that nothing on a
  * call path resolves a global or a prototype method that hostile code could
@@ -43,14 +41,11 @@ import { Nano25519RangeError, Nano25519TypeError } from "./errors"
 
 /** Type alias for legibility. */
 export type Bytes = Uint8Array<ArrayBuffer>
-export type Words = Uint32Array<ArrayBuffer>
 export type View = DataView<ArrayBuffer>
 export const isArray = Array.isArray
-export const isLE = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1
 
 /** `TypedArray` constructors, bound before anything can replace the globals. */
 const Bytes = Uint8Array
-const Words = Uint32Array
 const TypedArray = Object.getPrototypeOf(Bytes.prototype)
 const View = DataView
 
@@ -126,28 +121,24 @@ export function isBytes (a: unknown): a is Bytes {
        return getTag.call(a) === 'Uint8Array' && isArrayBuffer(getBuffer.call(a))
 }
 
-/** Captured version of `DataView` reusing input offset and length. */
-export function view (bytes: Bytes): View {
-       return new View(getBuffer.call(bytes) as ArrayBuffer, getByteOffset.call(bytes), getByteLength.call(bytes))
-}
-
 /**
- * Captured version of `Uint32Array`. Enables reading multiple bytes in a single
+ * 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.
  *
  * 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 words (bytes: Bytes): Words {
+export function view (bytes: Bytes): View {
        if (!isBytes(bytes)) {
-               throw new Nano25519TypeError('Error creating Uint32Array', { cause: bytes })
+               throw new TypeError('Error creating DataView', { cause: bytes })
        }
        const offset = getByteOffset.call(bytes)
        if (offset !== 0) {
-               throw new Nano25519RangeError('Unexpected byte offset', { cause: offset })
+               throw new RangeError('Unexpected byte offset', { cause: offset })
        }
-       return new Words(getBuffer.call(bytes) as ArrayBuffer, 0, getByteLength.call(bytes) >> 2)
+       return new View(getBuffer.call(bytes) as ArrayBuffer, offset, getByteLength.call(bytes))
 }
 
 const LOWER = '0123456789abcdef'
index 0f44b83ee764375688343693d2a3efe0b96b5cfc..54cf3bf9cd086cb541574baf39890b8b559c3157 100644 (file)
@@ -119,71 +119,27 @@ function setInputMsgEnd (m: Bytes, i: number): void {
  * remaining bytes, fewer than 32, go through `setInputMsgBytes`, which handles
  * the zero padding.
  */
-function setInputMsgBigEndian (m: View, i: number, p: number): void {
-       p <<= 2
-       exports.setInputMsg(i,
-               m.getUint32(p, true),
-               m.getUint32(p + 1, true),
-               m.getUint32(p + 2, true),
-               m.getUint32(p + 3, true),
-               m.getUint32(p + 4, true),
-               m.getUint32(p + 5, true),
-               m.getUint32(p + 6, true),
-               m.getUint32(p + 7, true)
-       )
-}
-
-/**
- * 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
- * written in one pass rather than looped over an intermediate array because
- * allocating costs more than the copy itself.
- *
- * Whole 32-byte chunks are read a word at a time through a `Uint32Array` view.
- * A `Uint32Array` is laid out little-endian on every platform that can run
- * WASM, so a word read is already in the order the module's `store<u32>` wants
- * and needs no shifting — measured over 64 KiB, that takes the loop from ~45 µs
- * to ~13 µs, which is the cost of the 2048 boundary crossings alone. The
- * remaining bytes, fewer than 32, go through `setInputMsgBytes`, which handles
- * the zero padding.
- *
- * Widening the crossing to 64 bytes per call was measured at ~12 µs, under the
- * ~2% noise floor for a whole `sign` or `verify`, so the ABI stays at 32.
- */
-function setInputMsgLittleEndian (m: Words, i: number, p: number): void {
-       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]
-       )
-}
-
 function setInputMsg (message: Bytes): void {
        const mlen = byteLength(message)
        const mlentrunc = mlen & ~31
-       const m = setInputMsgView(message)
-       for (let i = 0, p = 0; i < mlentrunc; i += 32, p += 8) {
-               setInputMsgEndian(m as (Words & View), i, p)
+       const m = view(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)
+               )
        }
        if (mlentrunc < mlen) {
                setInputMsgEnd(message, mlentrunc)
        }
 }
 
-/**
- * Selected once, at load, so the hot loop carries no per-chunk branch. The
- * word path is wrong on a big-endian platform and there is no such WASM host,
- * but the check costs one probe at load and removes the silent-wrong-answer
- * failure mode entirely.
- */
-const setInputMsgEndian = isLE ? setInputMsgLittleEndian : setInputMsgBigEndian
-const setInputMsgView = isLE ? words : view
-
 /**
  * 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