Proper PoW, unified verification WS etc

This commit is contained in:
Leo Vasanko
2025-12-07 04:12:11 +00:00
parent 9485cbd201
commit 4bb64863f9
8 changed files with 1014 additions and 179 deletions
+46
View File
@@ -0,0 +1,46 @@
import { solvePoW, verifyPoW, nonceToNumber } from './pow.js'
const TRIALS = 20
const REQUIRED_BITS = 18
async function test() {
console.log(`Running ${TRIALS} trials with ${REQUIRED_BITS} required zero bits...\n`)
const times = []
const iterations = []
for (let trial = 1; trial <= TRIALS; trial++) {
const challenge = crypto.getRandomValues(new Uint8Array(8))
const start = performance.now()
let lastIterations = 0
const nonce = await solvePoW(challenge, REQUIRED_BITS, {
onProgress: (n) => { lastIterations = n }
})
const elapsed = performance.now() - start
const valid = await verifyPoW(challenge, nonce, REQUIRED_BITS)
const nonceVal = nonceToNumber(nonce)
times.push(elapsed)
iterations.push(Number(nonceVal))
console.log(`Trial ${trial.toString().padStart(2)}: ${(elapsed / 1000).toFixed(3)}s, nonce=${nonceVal}, valid=${valid}`)
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length
const avgIter = iterations.reduce((a, b) => a + b, 0) / iterations.length
const minTime = Math.min(...times)
const maxTime = Math.max(...times)
console.log('\n--- Summary ---')
console.log(`Trials: ${TRIALS}`)
console.log(`Required bits: ${REQUIRED_BITS}`)
console.log(`Avg time: ${(avgTime / 1000).toFixed(3)}s`)
console.log(`Min time: ${(minTime / 1000).toFixed(3)}s`)
console.log(`Max time: ${(maxTime / 1000).toFixed(3)}s`)
console.log(`Avg iterations: ${Math.round(avgIter)}`)
console.log(`Expected iterations: ~${Math.pow(2, REQUIRED_BITS)}`)
}
test()
+142
View File
@@ -0,0 +1,142 @@
/**
* Proof of Work utility using SHA-512
*
* The PoW challenge requires finding a nonce such that
* SHA-512(challenge || nonce) has a specified number of leading zero bits.
*/
/**
* Solve a Proof of Work challenge
*
* @param {Uint8Array|ArrayBuffer} challenge - 8-byte server-provided challenge
* @param {number} requiredZeroBits - Number of leading zero bits required (1-32)
* @param {object} [options] - Optional parameters
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the operation
* @param {function} [options.onProgress] - Callback for progress updates (iterations count)
* @returns {Promise<Uint8Array>} The successful 8-byte nonce (little-endian)
* @throws {Error} If challenge is invalid or operation is aborted
*/
export async function solvePoW(challenge, requiredZeroBits, options = {}) {
const { signal, onProgress } = options
const startTime = performance.now()
// Validate inputs
const challengeBytes = challenge instanceof ArrayBuffer
? new Uint8Array(challenge)
: challenge
if (!(challengeBytes instanceof Uint8Array) || challengeBytes.length !== 8) {
throw new Error('Challenge must be exactly 8 bytes')
}
if (requiredZeroBits < 1 || requiredZeroBits > 32) {
throw new Error('Required zero bits must be between 1 and 32')
}
// Prepare the buffer: challenge (8 bytes) || nonce (8 bytes)
const data = new Uint8Array(16)
data.set(challengeBytes, 0)
// Nonce area data[8..15] starts at zero
// Precalculate mask and byte count for the required zero bits
const fullZeroBytes = requiredZeroBits >>> 3
const remainingBits = requiredZeroBits & 7
const partialMask = remainingBits ? (0xFF << (8 - remainingBits)) & 0xFF : 0
while (true) {
// Increment 64-bit little-endian nonce
for (let i = 8; i < 16; i++) if (++data[i] !== 256) break
// Calculate SHA-512 hash and verify leading zero bits
const hash = new Uint8Array(await crypto.subtle.digest('SHA-512', data))
if (hash[0]) continue // Quick check - most hashes fail here
// Yield to UI periodically and check abort signal
if (!(hash[1] & 0x0F)) {
await new Promise(r => setTimeout(r, 0))
if (signal?.aborted) throw new DOMException('PoW operation aborted', 'AbortError')
}
// Check all full zero bytes
let valid = true
for (let i = 1; i < fullZeroBytes; i++) {
if (hash[i]) { valid = false; break }
}
if (!valid) continue
if (partialMask && (hash[fullZeroBytes] & partialMask)) continue
break
}
const iterations = Number(nonceToNumber(data.slice(8, 16)))
const elapsed = (performance.now() - startTime) / 1000
const iterPerSec = Math.round(iterations / elapsed)
console.log(`PoW solved in ${elapsed.toFixed(1)}s (${(iterPerSec / 1e3).toFixed(0)} k-it/s)`)
return data.slice(8, 16)
}
/**
* Verify a PoW solution
*
* @param {Uint8Array|ArrayBuffer} challenge - 8-byte server-provided challenge
* @param {Uint8Array|ArrayBuffer} nonce - 8-byte client-provided nonce
* @param {number} requiredZeroBits - Number of leading zero bits required
* @returns {Promise<boolean>} True if the solution is valid
*/
export async function verifyPoW(challenge, nonce, requiredZeroBits) {
const challengeBytes = challenge instanceof ArrayBuffer
? new Uint8Array(challenge)
: challenge
const nonceBytes = nonce instanceof ArrayBuffer
? new Uint8Array(nonce)
: nonce
if (challengeBytes.length !== 8 || nonceBytes.length !== 8) {
return false
}
const data = new Uint8Array(16)
data.set(challengeBytes, 0)
data.set(nonceBytes, 8)
const hashBuffer = await crypto.subtle.digest('SHA-512', data)
const hash = new Uint8Array(hashBuffer)
// Check leading zero bits using same logic as solvePoW
const fullZeroBytes = requiredZeroBits >>> 3
const remainingBits = requiredZeroBits & 7
const partialMask = remainingBits ? (0xFF << (8 - remainingBits)) & 0xFF : 0
for (let i = 0; i < fullZeroBytes; i++) {
if (hash[i] !== 0) return false
}
if (partialMask && (hash[fullZeroBytes] & partialMask) !== 0) {
return false
}
return true
}
/**
* Convert a nonce Uint8Array to a BigInt (little-endian)
* @param {Uint8Array} nonce - 8-byte little-endian nonce
* @returns {bigint} The nonce as a BigInt
*/
export function nonceToNumber(nonce) {
let value = 0n
for (let i = 7; i >= 0; i--) {
value = (value << 8n) | BigInt(nonce[i])
}
return value
}
/**
* Convert a BigInt to a nonce Uint8Array (little-endian)
* @param {bigint} value - The value to convert
* @returns {Uint8Array} 8-byte little-endian representation
*/
export function numberToNonce(value) {
const nonce = new Uint8Array(8)
let v = BigInt(value)
for (let i = 0; i < 8; i++) {
nonce[i] = Number(v & 0xFFn)
v >>= 8n
}
return nonce
}
File diff suppressed because one or more lines are too long