* @param {number[]} times - List of numbers, often timing durations.
* @returns Object with averaged values for the specified list.
*/
- function stats (times) {
+ function stats(times) {
if (times == null || times.length === 0) return null
const count = times.length
}
}
- function average (times, type) {
+ function average(times, type) {
const averages = stats(times)
const title = `${type} derive-sign-verify`
return {
}
}
- function random (length = 32) {
+ function random(length = 32) {
const bytes = crypto.getRandomValues(new Uint8Array(length))
return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('')
}
}
}
- async function sequenceDeriveSignVerify (api, blockHash, privateKey) {
+ async function sequenceDeriveSignVerify(api, blockHash, privateKey) {
let start = 0, end = 0, publicKey = null, signature = null, isValid = false
if (api === 'nano25519 (async)') {
start = performance.now()
return end - start
}
- export async function test (api, size, runs) {
+ export async function test(api, size, runs) {
if (typeof size !== 'number' || size < 1) {
size = 1
}
* @param {string} name
* @param {boolean} test
*/
- function check (name, test) {
+ function check(name, test) {
if (typeof test !== 'boolean') {
throw 'invalid test'
}
passes += +test
failures += +!test
- // test both strings and bytes as input
- for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE2B_VECTORS) {
- result = nano25519.derive(privateKey)
- test = result.toLowerCase() === publicKey
- check(`derive from ${privateKey}`, test)
- passes += +test
- failures += +!test
-
- result = nano25519.sign(message, privateKey + publicKey)
- test = result.toLowerCase() === signature.slice(0, 128)
- check(`sign message ${message}`, test)
- passes += +test
- failures += +!test
-
- result = nano25519.verify(signature.slice(0, 128), message, publicKey)
- test = result === true
- check(`verify signature ${signature.slice(0, 128)}`, test)
- passes += +test
- failures += +!test
-
- const privateKeyBytes = new Uint8Array(privateKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
- const publicKeyBytes = new Uint8Array(publicKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
- const secretKeyBytes = new Uint8Array([...privateKeyBytes, ...publicKeyBytes])
- const messageBytes = new Uint8Array(message.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
- const signatureBytes = new Uint8Array(signature.match(/.{2}/g)?.slice(0, 64).map(b => parseInt(b, 16)) ?? [])
-
- result = nano25519.derive(privateKeyBytes)
- test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey
- check(`derive from ${privateKey}`, test)
- passes += +test
- failures += +!test
-
- result = nano25519.sign(messageBytes, secretKeyBytes)
- test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128)
- check(`sign message ${message}`, test)
- passes += +test
- failures += +!test
-
- result = nano25519.verify(signatureBytes, messageBytes, publicKeyBytes)
- test = result === true
- check(`verify signature ${signature.slice(0, 128)}`, test)
- passes += +test
- failures += +!test
- }
+ // // test both strings and bytes as input
+ // for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE2B_VECTORS) {
+ // result = nano25519.derive(privateKey)
+ // test = result.toLowerCase() === publicKey
+ // check(`derive from ${privateKey}`, test)
+ // passes += +test
+ // failures += +!test
+
+ // result = nano25519.sign(message, privateKey + publicKey)
+ // test = result.toLowerCase() === signature.slice(0, 128)
+ // check(`sign message ${message}`, test)
+ // passes += +test
+ // failures += +!test
+
+ // result = nano25519.verify(signature.slice(0, 128), message, publicKey)
+ // test = result === true
+ // check(`verify signature ${signature.slice(0, 128)}`, test)
+ // passes += +test
+ // failures += +!test
+
+ // const privateKeyBytes = new Uint8Array(privateKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
+ // const publicKeyBytes = new Uint8Array(publicKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
+ // const secretKeyBytes = new Uint8Array([...privateKeyBytes, ...publicKeyBytes])
+ // const messageBytes = new Uint8Array(message.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? [])
+ // const signatureBytes = new Uint8Array(signature.match(/.{2}/g)?.slice(0, 64).map(b => parseInt(b, 16)) ?? [])
+
+ // result = nano25519.derive(privateKeyBytes)
+ // test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey
+ // check(`derive from ${privateKey}`, test)
+ // passes += +test
+ // failures += +!test
+
+ // result = nano25519.sign(messageBytes, secretKeyBytes)
+ // test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128)
+ // check(`sign message ${message}`, test)
+ // passes += +test
+ // failures += +!test
+
+ // result = nano25519.verify(signatureBytes, messageBytes, publicKeyBytes)
+ // test = result === true
+ // check(`verify signature ${signature.slice(0, 128)}`, test)
+ // passes += +test
+ // failures += +!test
+ // }
// XFAIL
try {
console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold')
}
- function startValidation (event) {
+ function startValidation(event) {
const api = document.getElementById('api')?.value
const signature = document.getElementById('signature')?.value
const blockHash = document.getElementById('blockHash')?.value
document.getElementById('signature').addEventListener('input', startValidation)
document.getElementById('publicKey').addEventListener('input', startValidation)
- function startTest (event) {
+ function startTest(event) {
event.target.disabled = true
const api = document.getElementById('api')
const size = document.getElementById('size')
//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
+import { UUID } from 'node:crypto'
import { MessagePort as NodeMessagePort, Worker as NodeWorker } from 'node:worker_threads'
//@ts-expect-error
import nano25519_wasm from '../../build/nano25519.wasm'
-
import { nano25519_init } from './nano25519'
+type Action = 'derive' | 'sign' | 'start' | 'stop' | 'verify'
+
type Data = {
- url: string
id: string
action: string
message?: string | ArrayBuffer
const nano25519_worker_init = ({ derive, sign, verify }: ReturnType<typeof nano25519_init>) => {
let isListening = false
let host: NodeMessagePort | null = null
- let client: string | undefined = globalThis.location?.href
/**
* Parses inbound data when nano25519 is started as a Web Worker. Only called
|| !('data' in message)
|| message.data == null
|| typeof message.data !== 'object'
- || !('url' in message.data)
- || typeof message.data.url !== 'string'
|| !('id' in message.data)
|| typeof message.data.id !== 'string'
|| !('action' in message.data)
|| typeof message.data.action !== 'string'
) return
let result: undefined | boolean | string | Uint8Array<ArrayBuffer>
- let url: undefined | string
let id: undefined | string
try {
- const data: Data = message.data as object & { url: string, id: string, action: string }
- { ({ url, id } = data) }
- if (url !== client) return
+ const data: Data = message.data as object & { id: string, action: string }
+ id = data.id
if (data.action === 'start') {
isListening = true
result = JSON.stringify(err)
}
} finally {
- BROWSER: postMessage({ url, id, result })
- NODE: host?.postMessage({ data: { url, id, result } })
+ const data = { id, result }
+ BROWSER: postMessage(data)
+ NODE: host?.postMessage({ data })
}
}
BROWSER: addEventListener('message', handleMessage)
NODE: {
if (host == null) {
import('node:worker_threads')
- .then(({ parentPort, threadId }): void => {
+ .then(({ parentPort }): void => {
host = parentPort
- client = threadId.toString()
host?.on('message', handleMessage)
})
}
* Host code for asynchronous Web Worker
*/
let isWorkerReady: boolean = false
-let isWorkerListening: boolean = false
+let starting: Promise<unknown> | undefined
let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
let worker: Worker | NodeWorker
let url: string
function init (): void {
try {
BROWSER: {
- if (url) URL.revokeObjectURL(url)
url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
worker = new Worker(url, { type: 'module' })
+ worker.onmessage = report
+ worker.onerror = reset
}
NODE: {
worker = new NodeWorker(nano25519_worker, {
stderr: false,
stdout: false
})
- url = worker.threadId.toString()
+ worker.on('message', report)
+ worker.on('error', reset)
}
console.log(`nano25519 initialized.`)
isWorkerReady = true
}
}
-// Reconstruct worker when errors occur
-function reset (): void {
- console.warn(`nano25519 encountered an error. Reinitializing...`)
- isWorkerReady = false
- worker.terminate()
- init()
+// Helper for environment-specific messaging to worker
+function post (id: UUID, data: Record<string, any> & Record<'action', Action>, transfer?: ArrayBuffer[]): void {
+ data.id = id
+ BROWSER: worker.postMessage(data, transfer)
+ NODE: worker.postMessage({ data }, transfer)
}
-function onresult (msg: { data: Record<string, unknown> }): void {
+// Parse and validate worker message
+function report (msg: { data: Record<string, unknown> }): void {
const { data } = msg
- if (data.url !== url) return
if (!('id' in data) || typeof data.id !== 'string') return
const executor = tasks.get(data.id)
if (executor == null) return
- tasks.delete(data.id)
const [ok, err] = executor
+ tasks.delete(data.id)
const { result } = data
console.log('received result from worker')
return ok(result)
}
+// Reconstruct worker when errors occur
+function reset (): void {
+ console.warn(`nano25519 encountered an error. Reinitializing...`)
+ isWorkerReady = false
+ worker.terminate()
+ init()
+}
+
// Check that the worker is running and listening before sending messages
-async function start (): Promise<void> {
- if (!isWorkerReady) init()
- if (!isWorkerListening) {
- return new Promise(async (resolve, reject): Promise<void> => {
- const onstarted = (msg: { data: Record<string, unknown> }): void => {
- if (msg.data.url !== url) return
- const { result } = msg.data
- if (result === 'started') {
- console.log('worker started successfully')
- BROWSER: { (worker as Worker).onmessage = onresult }
- NODE: { (worker as NodeWorker).on('message', onresult) }
- isWorkerListening = true
- resolve()
- } else {
- isWorkerListening = false
- reject()
- }
- }
- console.log(`starting worker`)
- const id = crypto.randomUUID()
- const data = { url, id, action: 'start' }
- BROWSER: {
- worker = worker as Worker
- worker.onerror = reject
- worker.onmessage = onstarted
- worker.postMessage(data)
- }
- NODE: {
- worker = worker as unknown as NodeWorker
- worker.on('error', reject)
- worker.on('message', onstarted)
- worker.postMessage({ data })
- }
- })
- }
+async function start (): Promise<unknown> {
+ return starting ??= new Promise((resolve, reject): void => {
+ if (!isWorkerReady) init()
+ const id = crypto.randomUUID()
+ tasks.set(id, [resolve, reject])
+ post(id, { action: 'start' })
+ })
}
// Send command and relevant data to nano25519 worker
-async function dispatch (data: Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
+async function dispatch (data: Record<'action', Action> & Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
const id = crypto.randomUUID()
const transfer: ArrayBuffer[] = []
for (let k of Object.keys(data)) {
}
}
console.log('sending data to worker')
- data.url = url
- data.id = id
return new Promise((resolve, reject) => {
tasks.set(id, [resolve, reject])
console.log('tasks', tasks)
- BROWSER: { (worker as Worker).postMessage(data, transfer) }
- NODE: { (worker as NodeWorker).postMessage({ data }, transfer) }
+ post(id, data, transfer)
})
}
// Request that the worker stop listening without terminating
async function stop (): Promise<void> {
- return new Promise((resolve, reject): void => {
- const onstop = (msg: { data: Record<string, unknown> }): void => {
- const { data } = msg
- if (data.url !== url) return
- const { result } = data
- if (result === 'stopped') {
- console.log('worker stopped successfully')
- isWorkerListening = false
- resolve()
- } else {
- reject(result)
- }
- }
- console.log(`stopping worker`)
- const id = crypto.randomUUID()
- BROWSER: {
- worker = worker as Worker
- worker.onerror = reject
- worker.onmessage = onstop
- worker.postMessage({ url, id, action: 'stop' })
- }
- NODE: {
- worker = worker as unknown as NodeWorker
- worker.on('error', reject)
- worker.on('message', onstop)
- worker.postMessage({ data: { url, id, action: 'stop' } })
- }
- })
+ console.log('stopping worker')
+ const result = await dispatch({ action: 'stop' })
+ if (result === 'stopped') {
+ console.log('worker stopped successfully')
+ } else if (typeof result === 'string') {
+ throw new Error(result)
+ } else {
+ throw new Error('unknown error while stopping worker')
+ }
}
export async function run (data: Record<'action', 'derive'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
try {
await start()
+ if (url) URL.revokeObjectURL(url)
} catch (err: any) {
throw new Error('Error initializing worker')
}