diff --git a/pkgs/benchmark/src/App.tsx b/pkgs/benchmark/src/App.tsx index 89589e3..9b8fef7 100644 --- a/pkgs/benchmark/src/App.tsx +++ b/pkgs/benchmark/src/App.tsx @@ -10,7 +10,7 @@ async function benchmark(cb: () => void | Promise): Promise { } export default function App() { - const [durationJs, setDurationJs] = useState(null); + const [durationJs, _setDurationJs] = useState(null); const [durationWasm, setDurationWasm] = useState(null); const [runBenchmark, setRunBenchmark] = useState(false); @@ -26,8 +26,8 @@ export default function App() { async function init() { const challengesRaw = await powCaptcha.server.createChallengesRaw({ - difficulty: 2, - challengeCount: 64, + difficultyBits: 18, + challengeCount: 32, }); const challenges = challengesRaw.challenges.map( @@ -39,19 +39,21 @@ export default function App() { ); const durationWasm = await benchmark(async () => { - const solutions = await powCaptcha.solver.solveChallenges( + const solutions = await powCaptcha.solver.solveChallenges({ + difficultyBits: challengesRaw.difficultyBits, challenges, - "wasm", - ); + engine: "wasm", + }); console.log("wasm solutions", solutions); }); setDurationWasm(durationWasm); // const durationJs = await benchmark(async () => { - // const solutions = await powCaptcha.solver.solveChallenges( + // const solutions = await powCaptcha.solver.solveChallenges({ + // difficultyBits: challengesRaw.difficultyBits, // challenges, - // "js", - // ); + // engine: "js", + // }); // console.log("js solutions", solutions); // }); // setDurationJs(durationJs); diff --git a/pkgs/pow-captcha/src/server.ts b/pkgs/pow-captcha/src/server.ts index a00b49d..b2f3d41 100644 --- a/pkgs/pow-captcha/src/server.ts +++ b/pkgs/pow-captcha/src/server.ts @@ -3,24 +3,24 @@ import * as solver from "./solver"; import { createArray } from "./utils"; export type CreateChallengesOptions = { - /** @default 50 */ + /** @default 32 */ challengeCount?: number; /** @default 16 */ challengeLength?: number; - /** @default 2 */ - difficulty?: number; + /** @default 18 */ + difficultyBits?: number; }; export async function createChallengesRaw({ - challengeCount = 50, + challengeCount = 32, challengeLength = 16, - difficulty = 2, + difficultyBits = 18, }: CreateChallengesOptions): Promise { const challenges = createArray(challengeCount, (): wire.ChallengeEntry => { const challenge = new Uint8Array(challengeLength); crypto.getRandomValues(challenge); - const target = new Uint8Array(difficulty); + const target = new Uint8Array(Math.ceil(difficultyBits / 8)); crypto.getRandomValues(target); return [wire.serializeArray(challenge), wire.serializeArray(target)]; @@ -28,6 +28,7 @@ export async function createChallengesRaw({ return { magic: wire.CHALLENGE_MAGIC, + difficultyBits, challenges, }; } @@ -55,6 +56,7 @@ export async function redeemChallengeSolutionRaw( const isValid = await solver.verify( wire.deserializeArray(challenge[0]), wire.deserializeArray(challenge[1]), + challenges.difficultyBits, wire.deserializeArray(solution), ); if (!isValid) { diff --git a/pkgs/pow-captcha/src/solver-shared.ts b/pkgs/pow-captcha/src/solver-shared.ts index 95e0577..dc4088f 100644 --- a/pkgs/pow-captcha/src/solver-shared.ts +++ b/pkgs/pow-captcha/src/solver-shared.ts @@ -5,6 +5,7 @@ export type Challenge = readonly [Uint8Array, Uint8Array]; export type WorkerRequest = { engine?: undefined | "js" | "wasm"; challenges: ReadonlyArray; + difficultyBits: number; }; export type WorkerResponse = { diff --git a/pkgs/pow-captcha/src/solver-worker.ts b/pkgs/pow-captcha/src/solver-worker.ts index c414c12..f732060 100644 --- a/pkgs/pow-captcha/src/solver-worker.ts +++ b/pkgs/pow-captcha/src/solver-worker.ts @@ -10,29 +10,30 @@ async function solve( nonce: Uint8Array, target: Uint8Array, engine: undefined | "js" | "wasm", + difficultyBits: number, ): Promise { switch (engine) { case "js": - return await solver.solveJs(nonce, target); + return await solver.solveJs(nonce, target, difficultyBits); case "wasm": - return wasm.solve(nonce, target); + return wasm.solve(nonce, target, difficultyBits); case undefined: try { - return wasm.solve(nonce, target); + return wasm.solve(nonce, target, difficultyBits); } catch (err) { console.warn( "pow-captcha: Falling back to js solver. Error: ", err, ); - return await solver.solveJs(nonce, target); + return await solver.solveJs(nonce, target, difficultyBits); } } } async function processMessage(m: MessageEvent): Promise { - const { challenges, engine } = m.data; + const { challenges, engine, difficultyBits } = m.data; // const solutions = await Promise.all( // challenges.map(([nonce, target]) => solve(nonce, target, engine)), @@ -40,7 +41,7 @@ async function processMessage(m: MessageEvent): Promise { const solutions: Array = []; for (const [nonce, target] of challenges) { - const solution = await solve(nonce, target, engine); + const solution = await solve(nonce, target, engine, difficultyBits); solutions.push(solution); } @@ -50,8 +51,12 @@ async function processMessage(m: MessageEvent): Promise { postMessage(res); } +onerror = (m) => { + console.error("pow-captcha: Failure in worker: ", m); +}; + onmessage = (m: MessageEvent) => { - console.log("onmessage", m); + console.log("onmessage", m.data); processMessage(m).catch((err) => { console.error("pow-captcha: Failure in worker: ", err); }); diff --git a/pkgs/pow-captcha/src/solver.spec.ts b/pkgs/pow-captcha/src/solver.spec.ts index 6b2eca1..072d942 100644 --- a/pkgs/pow-captcha/src/solver.spec.ts +++ b/pkgs/pow-captcha/src/solver.spec.ts @@ -6,7 +6,8 @@ describe("solver", () => { expect( await solver.solveJs( new Uint8Array([1, 2]), - new Uint8Array([3, 4]), + new Uint8Array([3, 4, 5]), + 18, ), ).toStrictEqual(new Uint8Array([45, 176, 0, 0, 0, 0, 0, 0])); }); @@ -15,7 +16,8 @@ describe("solver", () => { expect( await solver.verify( new Uint8Array([1, 2]), - new Uint8Array([3, 4]), + new Uint8Array([3, 4, 5]), + 18, new Uint8Array([45, 176, 0, 0, 0, 0, 0, 0]), ), ).toStrictEqual(true); diff --git a/pkgs/pow-captcha/src/solver.ts b/pkgs/pow-captcha/src/solver.ts index 5f17507..67bd7a9 100644 --- a/pkgs/pow-captcha/src/solver.ts +++ b/pkgs/pow-captcha/src/solver.ts @@ -8,11 +8,26 @@ import { arrayStartsWith, chunkArray } from "./utils"; export async function solveJs( nonce: Uint8Array, target: Uint8Array, + difficultyBits: number, ): Promise { + if (target.length < Math.ceil(difficultyBits / 8)) { + throw new Error(`pow-captcha: target is smaller than difficultyBits`); + } + const arr = new Uint8Array(8 + nonce.byteLength); const solutionView = new DataView(arr.buffer, 0, 8); arr.set(nonce, 8); + const targetWholeBytes = target.slice(0, Math.floor(difficultyBits / 8)); + + let targetRest: null | [number, number] = null; + const targetRestBits = difficultyBits % 8; + if (targetRestBits !== 0) { + const mask = (0xff << (8 - targetRestBits)) & 0xff; + const rest = target[targetWholeBytes.length]! & mask; + targetRest = [mask, rest]; + } + for ( let i = BigInt(0); // eslint-disable-next-line no-constant-condition @@ -25,20 +40,27 @@ export async function solveJs( const hashArrayBuf = await crypto.subtle.digest("SHA-256", arr); const hash = new Uint8Array(hashArrayBuf); - if (arrayStartsWith(hash, target)) { - return new Uint8Array( - solutionView.buffer, - solutionView.byteOffset, - solutionView.byteLength, - ); + if (arrayStartsWith(hash, targetWholeBytes)) { + if ( + targetRest === null || + (hash[targetWholeBytes.length]! & targetRest[0]) === + targetRest[1] + ) { + return new Uint8Array( + solutionView.buffer, + solutionView.byteOffset, + solutionView.byteLength, + ); + } } } } -export async function solveChallenges( - challenges: ReadonlyArray, - engine?: "wasm" | "js", -): Promise> { +export async function solveChallenges({ + challenges, + engine, + difficultyBits, +}: WorkerRequest): Promise> { const workerChallenges = chunkArray( Math.floor(challenges.length / navigator.hardwareConcurrency), challenges, @@ -81,7 +103,7 @@ export async function solveChallenges( }, ); - const req: WorkerRequest = { challenges, engine }; + const req: WorkerRequest = { challenges, engine, difficultyBits }; worker.postMessage(req); return await resultPromise; @@ -96,8 +118,13 @@ export async function solveChallenges( export async function verify( nonce: Uint8Array, target: Uint8Array, + difficultyBits: number, solution: Uint8Array, ): Promise { + if (target.length < Math.ceil(difficultyBits / 8)) { + throw new Error(`pow-captcha: target is smaller than difficultyBits`); + } + const arr = new Uint8Array(solution.byteLength + nonce.byteLength); arr.set(solution); arr.set(nonce, solution.byteLength); @@ -105,5 +132,18 @@ export async function verify( const hashArrayBuf = await crypto.subtle.digest("SHA-256", arr); const hash = new Uint8Array(hashArrayBuf); - return arrayStartsWith(hash, target); + const targetWholeBytes = target.slice(0, Math.floor(difficultyBits / 8)); + + if (!arrayStartsWith(hash, targetWholeBytes)) { + return false; + } + + const targetRestBits = difficultyBits % 8; + if (targetRestBits === 0) { + return true; + } + + const mask = (0xff << (8 - targetRestBits)) & 0xff; + const rest = target[targetWholeBytes.length]! & mask; + return (hash[targetWholeBytes.length]! & mask) === rest; } diff --git a/pkgs/pow-captcha/src/utils.ts b/pkgs/pow-captcha/src/utils.ts index ad2de2a..f71393a 100644 --- a/pkgs/pow-captcha/src/utils.ts +++ b/pkgs/pow-captcha/src/utils.ts @@ -53,8 +53,6 @@ export function chunkArray( // // input.length / numBuckets // const chunkSize = Math.ceil(); - - // return createArray(numBuckets, (bucket): Array => { // const start = bucket * chunkSize; // return input.slice(start, start + chunkSize); diff --git a/pkgs/pow-captcha/src/wire.ts b/pkgs/pow-captcha/src/wire.ts index 54fc879..8d8b7a9 100644 --- a/pkgs/pow-captcha/src/wire.ts +++ b/pkgs/pow-captcha/src/wire.ts @@ -21,6 +21,7 @@ export const CHALLENGE_MAGIC = "2104f639-ba1b-48f3-9443-889128163f5a"; export const challengeSchema = z.object({ magic: z.literal(CHALLENGE_MAGIC), challenges: z.array(challengeEntrySchema), + difficultyBits: z.number(), }); export type Challenge = z.output; diff --git a/pkgs/solver-wasm/src/lib.rs b/pkgs/solver-wasm/src/lib.rs index cc4612a..77288e2 100644 --- a/pkgs/solver-wasm/src/lib.rs +++ b/pkgs/solver-wasm/src/lib.rs @@ -7,16 +7,37 @@ use sha2::{Digest, Sha256}; use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn solve(nonce: &[u8], target: &[u8]) -> Box<[u8]> { +pub fn solve(nonce: &[u8], target: &[u8], difficulty_bits: u32) -> Box<[u8]> { let mut buf = vec![0u8; 8 + nonce.len()]; buf[8..].copy_from_slice(nonce); + let target_whole_bytes = &target[0..(difficulty_bits as usize / 8)]; + + let target_rest = { + let rest_bits = difficulty_bits % 8; + match rest_bits { + 0 => None, + _ => { + let mask = 0xffu8.unbounded_shl(8 - rest_bits); + let rest = target[target_whole_bytes.len()] & mask; + Some((mask, rest)) + } + } + }; + for i in 0u64.. { let i_bytes = u64::to_le_bytes(i); buf[0..=7].copy_from_slice(&i_bytes); let hash = Sha256::digest(&buf); - if &hash[0..target.len()] == target { - return i_bytes.into(); + + if &hash[0..target_whole_bytes.len()] == target_whole_bytes { + let target_rest_ok = match target_rest { + None => true, + Some((mask, rest)) => (hash[target_whole_bytes.len()] & mask) == rest, + }; + if target_rest_ok { + return i_bytes.into(); + } } } @@ -28,7 +49,7 @@ mod tests { #[test] fn solve() { assert_eq!( - super::solve(&[1, 2], &[3, 4]).as_ref(), + super::solve(&[1, 2], &[3, 4, 5], 18).as_ref(), [45, 176, 0, 0, 0, 0, 0, 0] ); }