feat: change difficulty from byte count to bits
This commit is contained in:
@@ -10,7 +10,7 @@ async function benchmark(cb: () => void | Promise<void>): Promise<number> {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [durationJs, setDurationJs] = useState<null | number>(null);
|
||||
const [durationJs, _setDurationJs] = useState<null | number>(null);
|
||||
const [durationWasm, setDurationWasm] = useState<null | number>(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);
|
||||
|
||||
@@ -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<wire.Challenge> {
|
||||
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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Challenge = readonly [Uint8Array, Uint8Array];
|
||||
export type WorkerRequest = {
|
||||
engine?: undefined | "js" | "wasm";
|
||||
challenges: ReadonlyArray<Challenge>;
|
||||
difficultyBits: number;
|
||||
};
|
||||
|
||||
export type WorkerResponse = {
|
||||
|
||||
@@ -10,29 +10,30 @@ async function solve(
|
||||
nonce: Uint8Array,
|
||||
target: Uint8Array,
|
||||
engine: undefined | "js" | "wasm",
|
||||
difficultyBits: number,
|
||||
): Promise<Uint8Array> {
|
||||
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<WorkerRequest>): Promise<void> {
|
||||
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<WorkerRequest>): Promise<void> {
|
||||
|
||||
const solutions: Array<Uint8Array> = [];
|
||||
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<WorkerRequest>): Promise<void> {
|
||||
postMessage(res);
|
||||
}
|
||||
|
||||
onerror = (m) => {
|
||||
console.error("pow-captcha: Failure in worker: ", m);
|
||||
};
|
||||
|
||||
onmessage = (m: MessageEvent<WorkerRequest>) => {
|
||||
console.log("onmessage", m);
|
||||
console.log("onmessage", m.data);
|
||||
processMessage(m).catch((err) => {
|
||||
console.error("pow-captcha: Failure in worker: ", err);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -8,11 +8,26 @@ import { arrayStartsWith, chunkArray } from "./utils";
|
||||
export async function solveJs(
|
||||
nonce: Uint8Array,
|
||||
target: Uint8Array,
|
||||
difficultyBits: number,
|
||||
): Promise<Uint8Array> {
|
||||
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,7 +40,12 @@ export async function solveJs(
|
||||
const hashArrayBuf = await crypto.subtle.digest("SHA-256", arr);
|
||||
const hash = new Uint8Array(hashArrayBuf);
|
||||
|
||||
if (arrayStartsWith(hash, target)) {
|
||||
if (arrayStartsWith(hash, targetWholeBytes)) {
|
||||
if (
|
||||
targetRest === null ||
|
||||
(hash[targetWholeBytes.length]! & targetRest[0]) ===
|
||||
targetRest[1]
|
||||
) {
|
||||
return new Uint8Array(
|
||||
solutionView.buffer,
|
||||
solutionView.byteOffset,
|
||||
@@ -34,11 +54,13 @@ export async function solveJs(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function solveChallenges(
|
||||
challenges: ReadonlyArray<readonly [Uint8Array, Uint8Array]>,
|
||||
engine?: "wasm" | "js",
|
||||
): Promise<Array<Uint8Array>> {
|
||||
export async function solveChallenges({
|
||||
challenges,
|
||||
engine,
|
||||
difficultyBits,
|
||||
}: WorkerRequest): Promise<Array<Uint8Array>> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -53,8 +53,6 @@ export function chunkArray<T>(
|
||||
// // input.length / numBuckets
|
||||
// const chunkSize = Math.ceil();
|
||||
|
||||
|
||||
|
||||
// return createArray(numBuckets, (bucket): Array<T> => {
|
||||
// const start = bucket * chunkSize;
|
||||
// return input.slice(start, start + chunkSize);
|
||||
|
||||
@@ -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<typeof challengeSchema>;
|
||||
|
||||
|
||||
@@ -7,18 +7,39 @@ 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 {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!();
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user