-
Your average Hashrate: {{ number_formatter.format(hashrate) }} H/s
-
You have solved {{ total_solved }} {{ pluralize(total_solved, "challenge") }} in
- {{ number_formatter.format(total_solving_for / 1000) }}s
-
Your Hashrate on the last challenge: {{ number_formatter.format(hashrate_array.at(-1)!) }} H/s
+
-
You have not solved any challenges yet
-
Challenge: {{ challenge_loading_indicator }}
- {{ challenge }}
-
-
Nonce: {{ nonce }}
-
- Get Challenge
-
-
- Solve Challenge
- Solving challenge for {{ solving_for }}s...
-
-
-
- Challenge solved in {{ solveTime }}ms!
- Validating solution...
-
-
{{ challenge_error }}
-
-
-
-
- Auto solve
-
-
-
-
- Difficulty
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/example-app/app/pages/index.vue.old b/example-app/app/pages/index.vue.old
new file mode 100644
index 0000000..e1609f6
--- /dev/null
+++ b/example-app/app/pages/index.vue.old
@@ -0,0 +1,411 @@
+
+
+
+
+ Your average Hashrate: {{ number_formatter.format(hashrate) }} H/s
+ You have solved {{ total_solved }} {{ pluralize(total_solved, "challenge") }} in
+ {{ number_formatter.format(total_solving_for / 1000) }}s
+ Your Hashrate on the last challenge: {{ number_formatter.format(hashrate_array.at(-1)!) }} H/s
+
+ You have not solved any challenges yet
+ Challenge: {{ challenge_loading_indicator }}
+ {{ challenge }}
+
+ Nonce: {{ nonce }}
+
+ Get Challenge
+
+
+ Solve Challenge
+ Solving challenge for {{ solving_for }}s...
+
+
+
+ Challenge solved in {{ solveTime }}ms!
+ Validating solution...
+
+
{{ challenge_error }}
+
+
+
+
+ Auto solve
+
+
+
+
+ Difficulty
+
+
+
+
+
\ No newline at end of file
diff --git a/example-app/app/pages/widget.vue b/example-app/app/pages/widget.vue
deleted file mode 100644
index af5a7e7..0000000
--- a/example-app/app/pages/widget.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/example-app/config.toml b/example-app/config.toml
index 175d496..e3e4da3 100644
--- a/example-app/config.toml
+++ b/example-app/config.toml
@@ -1,7 +1,10 @@
-strategy = "target_number"
+strategy = "kctf"
[leading_zeroes]
difficulty = 4
[target_number]
max_number = 10000
+
+[kctf]
+difficulty = 100
diff --git a/example-app/package-lock.json b/example-app/package-lock.json
index 133031d..c7e8194 100644
--- a/example-app/package-lock.json
+++ b/example-app/package-lock.json
@@ -6,8 +6,8 @@
"": {
"name": "hello-nuxt",
"dependencies": {
- "@impost/lib": "file:../packages/lib",
- "@impost/widget": "file:../packages/widget",
+ "@impost/lib": "^0.1.0",
+ "@impost/widget": "^0.1.0",
"@lit/reactive-element": "^2.1.1",
"js-toml": "^1.0.2",
"lit-element": "^4.2.1",
@@ -41,6 +41,9 @@
"name": "@impost/lib",
"version": "0.1.0",
"license": "BSL-1.0",
+ "dependencies": {
+ "uuidv7": "^1.0.2"
+ },
"devDependencies": {
"oxc-minify": "^0.97.0",
"tslib": "^2.6.2",
diff --git a/example-app/package.json b/example-app/package.json
index 2d301c0..89ed353 100644
--- a/example-app/package.json
+++ b/example-app/package.json
@@ -16,8 +16,8 @@
"nuxt": "latest",
"nuxt-ssr-lit": "1.6.32",
"zod": "^4.1.12",
- "@impost/lib": "file:../packages/lib",
- "@impost/widget": "file:../packages/widget"
+ "@impost/lib": "^0.1.0",
+ "@impost/widget": "^0.1.0"
},
"devDependencies": {
"@types/node": "^24.10.0",
diff --git a/example-app/server/api/pow/challenge.get.ts b/example-app/server/api/pow/challenge.get.ts
index 607cc42..cb0f522 100644
--- a/example-app/server/api/pow/challenge.get.ts
+++ b/example-app/server/api/pow/challenge.get.ts
@@ -6,19 +6,28 @@ import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow
export default defineEventHandler(async () => {
let challenge_config;
+ // switch (config.strategy) {
+ // case ChallengeStrategy.LeadingZeroes:
+ // challenge_config = {
+ // parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
+ // strategy: config.strategy,
+ // difficulty: config.leading_zeroes?.difficulty!,
+ // };
+ // break;
+ // case ChallengeStrategy.TargetNumber:
+ // challenge_config = {
+ // parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
+ // strategy: config.strategy,
+ // max_number: config.target_number.max_number,
+ // };
+ // break;
+ // }
switch (config.strategy) {
- case ChallengeStrategy.LeadingZeroes:
+ case ChallengeStrategy.kCTF:
challenge_config = {
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
strategy: config.strategy,
- difficulty: config.leading_zeroes?.difficulty!,
- };
- break;
- case ChallengeStrategy.TargetNumber:
- challenge_config = {
- parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
- strategy: config.strategy,
- max_number: config.target_number.max_number,
+ difficulty: config.kctf.difficulty,
};
break;
}
@@ -31,10 +40,10 @@ export default defineEventHandler(async () => {
});
}
- outstandingChallenges.set(challenge.salt, {
+ outstandingChallenges.set(challenge.challenge, {
challenge, timeout: setTimeout(() => {
- console.log("Challenge timed out:", challenge.salt);
- outstandingChallenges.delete(challenge.salt);
+ console.log("Challenge timed out:", challenge.challenge);
+ outstandingChallenges.delete(challenge.challenge);
}, CHALLENGE_TIMEOUT_MS)
});
diff --git a/example-app/server/api/pow/challenge.post.ts b/example-app/server/api/pow/challenge.post.ts
index e140f92..58d39ae 100644
--- a/example-app/server/api/pow/challenge.post.ts
+++ b/example-app/server/api/pow/challenge.post.ts
@@ -4,12 +4,13 @@ import * as z from 'zod';
import { outstandingChallenges } from '~~/server/utils/pow';
const challengeSchema = z.object({
- challenge: z.string(),
- nonce: z.string()
+ challenge: z.string().startsWith("s."),
+ solution: z.string().startsWith("s.")
})
// post handler that takes in the challenge, and the nonce
export default defineEventHandler(async (event) => {
+ console.log(await readBody(event));
const body = await readValidatedBody(event, challengeSchema.safeParse);
if (!body.success) {
@@ -19,19 +20,25 @@ export default defineEventHandler(async (event) => {
})
}
- let target = body.data.challenge;
- let nonce = body.data.nonce;
+ let { challenge, solution } = body.data;
+
+ const outstanding_challenge = outstandingChallenges.get(challenge);
+ if (outstanding_challenge === undefined) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'Challenge not found'
+ })
+ }
// check if the challenge is valid
- let challenge_valid = await validate_challenge(outstandingChallenges.get(target)!.challenge, {
- challenge: target,
- nonce: nonce
- });
+ const challenge_valid = await validate_challenge(outstanding_challenge.challenge, solution);
+
+ console.log("CHALLENGE VALID", challenge_valid);
if (challenge_valid) {
// clear the challenge
- clearTimeout(outstandingChallenges.get(target)!.timeout);
- outstandingChallenges.delete(target);
+ clearTimeout(outstandingChallenges.get(challenge)!.timeout);
+ outstandingChallenges.delete(challenge);
return {
message: 'Challenge solved'
diff --git a/example-app/server/utils/config.ts b/example-app/server/utils/config.ts
index 8bab744..e2ad38a 100644
--- a/example-app/server/utils/config.ts
+++ b/example-app/server/utils/config.ts
@@ -3,26 +3,31 @@ import { load } from 'js-toml';
import z from 'zod';
import { ChallengeStrategy } from "@impost/lib";
-const LeadingZeroesSchema = z.object({
- strategy: z.literal(ChallengeStrategy.LeadingZeroes),
- leading_zeroes: z.object({
- difficulty: z.number().int().min(1).max(64),
+// const LeadingZeroesSchema = z.object({
+// strategy: z.literal(ChallengeStrategy.LeadingZeroes),
+// leading_zeroes: z.object({
+// difficulty: z.number().int().min(1).max(64),
+// }),
+// });
+
+// const TargetNumberSchema = z.object({
+// strategy: z.literal(ChallengeStrategy.TargetNumber),
+// target_number: z.object({
+// max_number: z.number().int().min(1).max(100_000),
+// }),
+// });
+
+const kCTFSchema = z.object({
+ strategy: z.literal(ChallengeStrategy.kCTF),
+ kctf: z.object({
+ difficulty: z.number().int().min(1),
}),
});
-const TargetNumberSchema = z.object({
- strategy: z.literal(ChallengeStrategy.TargetNumber),
- target_number: z.object({
- max_number: z.number().int().min(1).max(100_000),
- }),
-});
-
-
export type Config = z.infer
;
export const Config = z.discriminatedUnion('strategy', [
- LeadingZeroesSchema,
- TargetNumberSchema,
+ kCTFSchema,
]);
export let config: Config;
diff --git a/justfile b/justfile
index 3d10f4c..c47db72 100644
--- a/justfile
+++ b/justfile
@@ -1,10 +1,14 @@
-build: build-widget
-
wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz"
-zig-build-args := "--release=fast"
+zig-build-args := "--release=fast -Dtarget=wasm32-freestanding -Dcpu=generic+bulk_memory+bulk_memory_opt+simd128+tail_call"
npm-runner := "npm"
+[working-directory: "example-app"]
+playground: build
+ {{npm-runner}} run dev
+
+build: build-widget
+
[working-directory: "solver"]
build-wasm:
zig build {{zig-build-args}}
diff --git a/packages/lib/package-lock.json b/packages/lib/package-lock.json
index d643549..e66168c 100644
--- a/packages/lib/package-lock.json
+++ b/packages/lib/package-lock.json
@@ -2058,7 +2058,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2332,7 +2331,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2383,7 +2381,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts
index 01081a7..9cfa23b 100644
--- a/packages/lib/src/index.ts
+++ b/packages/lib/src/index.ts
@@ -1,28 +1,44 @@
+import { UUID } from "uuidv7";
+
export enum ChallengeAlgorithm {
Argon2id = "argon2id",
}
export enum ChallengeStrategy {
+ kCTF = "kctf",
LeadingZeroes = "leading_zeroes",
TargetNumber = "target_number",
}
// In this case, the client will repeatedly hash a number with has until it
// finds a hash thaat starts with *difficulty* leading zeroes
-export interface ChallengeLeadingZeroes {
- algorithm: ChallengeAlgorithm;
- strategy: ChallengeStrategy.LeadingZeroes;
- salt: string; // random string
+// export interface ChallengeLeadingZeroes {
+// algorithm: ChallengeAlgorithm;
+// strategy: ChallengeStrategy.LeadingZeroes;
+// salt: string; // random string
+// difficulty: number;
+// }
+
+// // In this case, the server generates a random number, and the client will hash
+// // the salt (a random string) + a random number until it finds a hash that is equal to challenge
+// export interface ChallengeTargetNumber {
+// algorithm: ChallengeAlgorithm;
+// strategy: ChallengeStrategy.TargetNumber;
+// salt: string; // random string
+// target: string; // hash of salt + random number
+// }
+
+export interface InnerChallengekCTF {
+ strategy: ChallengeStrategy.kCTF;
+ salt: UUID; // UUIDv7
difficulty: number;
}
-// In this case, the server generates a random number, and the client will hash
-// the salt (a random string) + a random number until it finds a hash that is equal to challenge
-export interface ChallengeTargetNumber {
- algorithm: ChallengeAlgorithm;
- strategy: ChallengeStrategy.TargetNumber;
- salt: string; // random string
- target: string; // hash of salt + random number
+export interface ChallengekCTF {
+ strategy: ChallengeStrategy.kCTF;
+ challenge: string;
}
-export type Challenge = ChallengeLeadingZeroes | ChallengeTargetNumber;
\ No newline at end of file
+export type InnerChallenge = InnerChallengekCTF;
+
+export type Challenge = ChallengekCTF;
\ No newline at end of file
diff --git a/packages/lib/src/solver.ts b/packages/lib/src/solver.ts
index 7161566..0ba4144 100644
--- a/packages/lib/src/solver.ts
+++ b/packages/lib/src/solver.ts
@@ -3,8 +3,10 @@ import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
type WasmExports = Record & {
"malloc": (byte_count: number) => number | null;
"free": (ptr: number | null, byte_count: number) => void;
- "solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number;
- "solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len: number) => number;
+ // "solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number;
+ // "solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len:
+ // number) => number;
+ "solve": (value_ptr: number, value_len: number) => number,
"memory": WebAssembly.Memory;
}
@@ -17,6 +19,7 @@ export type SolverEnv = {
__set_solution: (value: number) => void;
__cmpxchg_solution: (expected: number, replacement: number) => number;
__fetch_add_nonce: (value: number) => number;
+ __log: (str_ptr: number, str_len: number) => void;
};
export async function get_wasm_module(): Promise {
@@ -29,64 +32,35 @@ export async function init_solver(env: SolverEnv, module: WebAssembly.Module): P
}) as unknown as SolverModule;
}
-export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: { salt: string, difficulty: number }): number {
- const { salt, difficulty } = challenge;
+export function solve(solver: SolverModule, challenge: string): string {
+ console.log(challenge);
+
const encoder = new TextEncoder();
+ const challenge_buf = encoder.encode(challenge);
- const salt_bytes = encoder.encode(salt);
-
- const salt_ptr = solver.exports.malloc(salt_bytes.length);
- if (salt_ptr === 0 || salt_ptr === null) {
+ const challenge_ptr = solver.exports.malloc(challenge_buf.length);
+ if (challenge_ptr === 0 || challenge_ptr === null) {
throw new Error("Failed to allocate memory for challenge string");
}
const memory = new Uint8Array(solver.exports.memory.buffer);
- memory.set(salt_bytes, salt_ptr);
+ memory.set(challenge_buf, challenge_ptr);
- const ret = solver.exports.solve_leaading_zeroes_challenge(
- salt_ptr,
- salt_bytes.length,
- difficulty,
- );
+ const ret = solver.exports.solve(challenge_ptr, challenge_buf.length);
- if (ret < 0) {
+ console.log("RET", ret);
+
+ if (ret <= 0) {
throw new Error("Failed to solve challenge");
}
- return ret;
+ const length = new DataView(solver.exports.memory.buffer, ret, 2).getUint16(0, true);
+ const solution = new TextDecoder().decode(solver.exports.memory.buffer.slice(ret + 2, ret + 2 + length));
+
+ console.log("SOLUTION", solution);
+ console.log("LENGTH", length);
+
+ solver.exports.free(ret, 2 + length);
+
+ return solution;
}
-
-export function solve_target_number_challenge(solver: SolverModule, challenge: { salt: string, target: string }): number {
- const { salt, target } = challenge;
- const encoder = new TextEncoder();
-
- const salt_bytes = encoder.encode(salt);
- const target_bytes = encoder.encode(target);
-
- const salt_ptr = solver.exports.malloc(salt_bytes.length);
- if (salt_ptr === 0 || salt_ptr === null) {
- throw new Error("Failed to allocate memory for salt string");
- }
-
- const target_ptr = solver.exports.malloc(target_bytes.length);
- if (target_ptr === 0 || target_ptr === null) {
- throw new Error("Failed to allocate memory for target string");
- }
-
- const memory = new Uint8Array(solver.exports.memory.buffer);
- memory.set(salt_bytes, salt_ptr);
- memory.set(target_bytes, target_ptr);
-
- const ret = solver.exports.solve_target_number_challenge(
- target_ptr,
- target_bytes.length,
- salt_ptr,
- salt_bytes.length,
- );
-
- if (ret < 0) {
- throw new Error("Failed to solve challenge");
- }
-
- return ret;
-}
\ No newline at end of file
diff --git a/packages/lib/src/validator.ts b/packages/lib/src/validator.ts
index 3c5185c..3498735 100644
--- a/packages/lib/src/validator.ts
+++ b/packages/lib/src/validator.ts
@@ -1,12 +1,11 @@
-import { ChallengeAlgorithm, ChallengeStrategy, type Challenge } from '.';
+import { ChallengeStrategy, type Challenge, type InnerChallenge } from '.';
import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
+import { uuidv7obj } from 'uuidv7';
type WasmExports = Record & {
"malloc": (byte_count: number) => number | null;
"free": (ptr: number | null, byte_count: number) => void;
- "validate_leading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number, difficulty: number) => number;
- "validate_target_number_challenge": (target_ptr: number, target_len: number, nonce_ptr: number, nonce_len: number, salt_ptr: number, salt_len: number) => number;
- "hash": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number) => bigint;
+ "validate": (challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number) => boolean;
"memory": WebAssembly.Memory;
}
@@ -14,159 +13,92 @@ export interface ValidatorModule extends WebAssembly.Instance {
exports: WasmExports;
}
-function array_to_base64(buffer: ArrayBuffer): string {
- return btoa(String.fromCharCode(...new Uint8Array(buffer)));
-}
-
-async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise {
- let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
- let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`;
-
- let challenge: Challenge = {
- algorithm: ChallengeAlgorithm.Argon2id,
- strategy: ChallengeStrategy.LeadingZeroes,
- salt,
- difficulty,
- };
-
- return challenge;
-}
-
-async function generate_target_number_challenge(parameters: Object, max_number: number): Promise {
- // in target number config, since we need to generate a target hash, we
- // need to hash the salt + nonce, so the client knows what the target is
- const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule;
-
- let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
- let salt = `${array_to_base64(crypto.getRandomValues(new Uint8Array(32)).buffer)}?${parameters_str}`;
- let random_number = new DataView(crypto.getRandomValues(new Uint8Array(4)).buffer).getUint32(0, true) % max_number;
-
- const encoder = new TextEncoder();
- const salt_bytes = encoder.encode(salt);
- const random_number_bytes = encoder.encode(random_number.toString());
-
- const salt_ptr = validator.exports.malloc(salt_bytes.length);
- const random_number_ptr = validator.exports.malloc(random_number_bytes.length);
-
- if (salt_ptr === 0 || salt_ptr === null || random_number_ptr === 0 || random_number_ptr === null) {
- console.error("Failed to allocate memory for challenge string");
- return null;
+function array_to_base64(buffer: ArrayBufferLike): string {
+ let binary = '';
+ const bytes = new Uint8Array(buffer);
+ for (var i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
}
-
- const memory = new Uint8Array(validator.exports.memory.buffer);
- memory.set(salt_bytes, salt_ptr);
- memory.set(random_number_bytes, random_number_ptr);
-
- let target_blob: bigint = validator.exports.hash(salt_ptr, salt_bytes.length, random_number_ptr, random_number_bytes.length);
- let target_ptr = Number(target_blob & BigInt(0xFFFFFFFF));
- let target_len = Number(target_blob >> BigInt(32));
-
- validator.exports.free(salt_ptr, salt_bytes.length);
- validator.exports.free(random_number_ptr, random_number_bytes.length);
-
- // do NOT use `memory` here, by this time it has almost definitely been resized and will cause errors to touch
- let target_slice = new Uint8Array(validator.exports.memory.buffer.slice(target_ptr, target_ptr + target_len));
- const target = new TextDecoder().decode(target_slice);
-
- let challenge: Challenge = {
- algorithm: ChallengeAlgorithm.Argon2id,
- strategy: ChallengeStrategy.TargetNumber,
- salt,
- target
- };
-
- return challenge;
+ return btoa(binary);
}
-export interface LeadingZeroesChallengeConfig {
+export interface kCTFChallengeConfig {
parameters: Object;
- strategy: ChallengeStrategy.LeadingZeroes;
+ strategy: ChallengeStrategy.kCTF;
difficulty: number;
}
-export interface TargetNumberChallengeConfig {
- parameters: Object;
- strategy: ChallengeStrategy.TargetNumber;
- max_number: number;
-}
+export type ChallengeConfig = kCTFChallengeConfig;
-export type ChallengeConfig = LeadingZeroesChallengeConfig | TargetNumberChallengeConfig;
+const VERSION = "s";
+
+async function encode_challenge(challenge: InnerChallenge, parameters: Object = {}): Promise {
+ if (challenge.strategy !== ChallengeStrategy.kCTF) {
+ throw new Error("Unsupported challenge strategy");
+ }
+
+ if (challenge.difficulty < 1) {
+ throw new Error("Difficulty must be at least 1");
+ }
+
+ const difficulty_buf = new Uint8Array(4);
+ const view = new DataView(difficulty_buf.buffer);
+ view.setUint32(0, challenge.difficulty, false);
+
+ const difficulty_str = array_to_base64(difficulty_buf.buffer);
+ const salt_str = array_to_base64(challenge.salt.bytes.buffer);
+
+ // the parameters str is expected to be sliced out of the challenge via the widget before it sends it to the wasm solver.
+ let parameters_str = Object.entries(parameters).map(([key, value]) => `${key}=${value}`).join("&");
+ if (parameters_str.length > 0) {
+ parameters_str = "?" + parameters_str;
+ }
+
+ return `${VERSION}.${difficulty_str}.${salt_str}${parameters_str}`;
+}
export async function generate_challenge(config: ChallengeConfig): Promise {
- let challenge: Challenge | null = null;
- switch (config.strategy) {
- case ChallengeStrategy.LeadingZeroes:
- challenge = await generate_leading_zeroes_challenge(config.parameters, config.difficulty);
- break;
- case ChallengeStrategy.TargetNumber:
- challenge = await generate_target_number_challenge(config.parameters, config.max_number);
- break;
+ if (config.difficulty < 1) {
+ throw new Error("Difficulty must be at least 1");
}
- if (challenge === null) {
- return null;
+ const challenge: InnerChallenge = {
+ strategy: ChallengeStrategy.kCTF,
+ salt: uuidv7obj(),
+ difficulty: config.difficulty,
}
- return challenge;
+ return {
+ strategy: ChallengeStrategy.kCTF,
+ challenge: await encode_challenge(challenge),
+ };
}
-export async function validate_challenge(challenge: Challenge, challenge_solution: { challenge: string, nonce: string }): Promise {
- const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule
-
+export async function validate_challenge(challenge: Challenge, challenge_solution: string): Promise {
+ const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl), {
+ env: {
+ __log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(validator.exports.memory.buffer, str_ptr, str_len))),
+ }
+ })).instance as unknown as ValidatorModule
const encoder = new TextEncoder();
+ const challenge_buf = encoder.encode(challenge.challenge);
+ const solution_buf = encoder.encode(challenge_solution);
- let err;
- let memory;
- let nonce_bytes, nonce_ptr;
- let target_bytes, target_ptr;
- switch (challenge.strategy) {
- case ChallengeStrategy.LeadingZeroes:
- target_bytes = encoder.encode(challenge_solution.challenge);
- nonce_bytes = encoder.encode(challenge_solution.nonce);
+ const challenge_ptr = validator.exports.malloc(challenge_buf.length);
+ const solution_ptr = validator.exports.malloc(solution_buf.length);
- target_ptr = validator.exports.malloc(challenge_solution.challenge.length);
- nonce_ptr = validator.exports.malloc(challenge_solution.nonce.length);
-
- if (target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
- console.error("Failed to allocate memory for challenge string");
- return false;
- }
-
- memory = new Uint8Array(validator.exports.memory.buffer);
- memory.set(target_bytes, target_ptr);
- memory.set(nonce_bytes, nonce_ptr);
-
- err = validator.exports.validate_leading_zeroes_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, challenge.difficulty);
-
- validator.exports.free(target_ptr, target_bytes.length);
- validator.exports.free(nonce_ptr, nonce_bytes.length);
- break;
- case ChallengeStrategy.TargetNumber:
- target_bytes = encoder.encode(challenge.target);
- const salt_bytes = encoder.encode(challenge.salt);
- nonce_bytes = encoder.encode(challenge_solution.nonce);
-
- const salt_ptr = validator.exports.malloc(salt_bytes.length);
- target_ptr = validator.exports.malloc(target_bytes.length);
- nonce_ptr = validator.exports.malloc(nonce_bytes.length);
-
- if (salt_ptr === 0 || salt_ptr === null || target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
- console.error("Failed to allocate memory for challenge string");
- return false;
- }
-
- memory = new Uint8Array(validator.exports.memory.buffer);
- memory.set(salt_bytes, salt_ptr);
- memory.set(target_bytes, target_ptr);
- memory.set(nonce_bytes, nonce_ptr);
-
- err = validator.exports.validate_target_number_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, salt_ptr, salt_bytes.length);
-
- validator.exports.free(salt_ptr, salt_bytes.length);
- validator.exports.free(target_ptr, target_bytes.length);
- validator.exports.free(nonce_ptr, nonce_bytes.length);
- break;
+ if (challenge_ptr === 0 || challenge_ptr === null || solution_ptr === 0 || solution_ptr === null) {
+ console.error("Failed to allocate memory for challenge string");
+ return false;
}
- return err === 0;
+ const memory = new Uint8Array(validator.exports.memory.buffer);
+ memory.set(challenge_buf, challenge_ptr);
+ memory.set(solution_buf, solution_ptr);
+
+ const is_valid = validator.exports.validate(challenge_ptr, challenge.challenge.length, solution_ptr, challenge_solution.length);
+ validator.exports.free(challenge_ptr, challenge.challenge.length);
+ validator.exports.free(solution_ptr, challenge_solution.length);
+
+ return is_valid;
}
\ No newline at end of file
diff --git a/packages/widget/package-lock.json b/packages/widget/package-lock.json
index ff2b26c..76bc129 100644
--- a/packages/widget/package-lock.json
+++ b/packages/widget/package-lock.json
@@ -1433,7 +1433,6 @@
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -2148,7 +2147,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2422,7 +2420,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2461,7 +2458,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
diff --git a/packages/widget/src/pow-captcha.ts b/packages/widget/src/pow-captcha.ts
index d441202..6f9c5d9 100644
--- a/packages/widget/src/pow-captcha.ts
+++ b/packages/widget/src/pow-captcha.ts
@@ -21,6 +21,9 @@ export class PowCaptcha extends LitElement {
background-color: var(--impost-widget-background-color, #070408);
border-radius: var(--impost-widget-border-radius, 8px);
}
+ .impost-error-icon {
+ color: var(--impost-widget-error-icon-color, #FF8117);
+ }
.impost-main {
display: flex;
align-items: center;
@@ -86,32 +89,20 @@ export class PowCaptcha extends LitElement {
@state()
private solution: string = '';
- @state()
- private errorMessage: string = '';
-
@state()
private challengeData: Challenge | null = null;
@state()
- private solved: boolean = false;
-
- @state()
- private isSolving: boolean = false;
+ private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved';
@state()
private disabled: boolean = true;
- @state()
- private hashRate: number = 0;
-
// stores the nonce and solution atomics
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
private solverWorkers: Worker[] | null = null;
- private solveStartTime: number | null = null;
- private hashRateInterval: number | null = null;
-
override connectedCallback() {
super.connectedCallback();
this.fetchChallenge();
@@ -141,10 +132,6 @@ export class PowCaptcha extends LitElement {
override disconnectedCallback() {
super.disconnectedCallback();
- if (this.hashRateInterval !== null) {
- clearInterval(this.hashRateInterval);
- this.hashRateInterval = null;
- }
for (const worker of this.solverWorkers || []) {
worker.terminate();
@@ -157,7 +144,6 @@ export class PowCaptcha extends LitElement {
}
async fetchChallenge() {
- this.errorMessage = '';
if (this.challengeData !== null) {
return;
}
@@ -179,14 +165,14 @@ export class PowCaptcha extends LitElement {
})
.catch(error => {
console.error('Error fetching challenge:', error);
- this.errorMessage = 'Failed to fetch challenge. Please try again.';
+ console.error('Failed to fetch challenge');
});
}
async initWorkers() {
this.solverWorkers = [];
- const num_workers = navigator.hardwareConcurrency;
+ const num_workers = 1;
for (let i = 0; i < num_workers; i++) {
this.solverWorkers.push(new ChallengeWorker());
}
@@ -237,7 +223,8 @@ export class PowCaptcha extends LitElement {
let timeout: number;
const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(() => {
- this.errorMessage = 'Failed to initialize workers in time. Please refresh the page.';
+ console.error('Failed to initialize workers in time');
+ this.status = 'error';
reject(new Error(`Function timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
@@ -272,19 +259,27 @@ export class PowCaptcha extends LitElement {
worker.addEventListener('message', message_handler);
worker.addEventListener('error', error_handler);
+ // switch (request.strategy) {
+ // case ChallengeStrategy.LeadingZeroes:
+ // worker.postMessage({
+ // strategy: ChallengeStrategy.LeadingZeroes,
+ // salt: request.salt,
+ // difficulty: request.difficulty,
+ // } as WorkerRequest);
+ // break;
+ // case ChallengeStrategy.TargetNumber:
+ // worker.postMessage({
+ // strategy: ChallengeStrategy.TargetNumber,
+ // target: request.target,
+ // salt: request.salt,
+ // } as WorkerRequest);
+ // break;
+ // }
switch (request.strategy) {
- case ChallengeStrategy.LeadingZeroes:
+ case ChallengeStrategy.kCTF:
worker.postMessage({
- strategy: ChallengeStrategy.LeadingZeroes,
- salt: request.salt,
- difficulty: request.difficulty,
- } as WorkerRequest);
- break;
- case ChallengeStrategy.TargetNumber:
- worker.postMessage({
- strategy: ChallengeStrategy.TargetNumber,
- target: request.target,
- salt: request.salt,
+ strategy: ChallengeStrategy.kCTF,
+ challenge: request.challenge,
} as WorkerRequest);
break;
}
@@ -293,7 +288,8 @@ export class PowCaptcha extends LitElement {
async solveChallenge() {
if (!this.challengeData || this.solverWorkers === null) {
- this.errorMessage = 'Captcha is not ready. Please wait or refresh.';
+ console.error('solveChallenge called before challenge is ready');
+ this.status = 'error';
return;
}
@@ -301,25 +297,14 @@ export class PowCaptcha extends LitElement {
return;
}
- this.solveStartTime = performance.now();
- this.hashRateInterval = setInterval(async () => {
- const nonce = this.getCurrentWorkingNonce();
-
- this.hashRate = (nonce / ((performance.now() - this.solveStartTime!) / 1000));
- }, 250);
-
this.dispatchEvent(new CustomEvent('impost:solve', {
- detail: {
- solution: this.solution,
- },
bubbles: true,
composed: true,
}))
console.log(this.challengeData);
- this.isSolving = true;
- this.errorMessage = '';
+ this.status = 'solving';
this.solution = '';
const atomics_view = new Int32Array(this.sab);
@@ -328,19 +313,27 @@ export class PowCaptcha extends LitElement {
let request: ChallengeSolveRequest;
+ // switch (this.challengeData.strategy) {
+ // case ChallengeStrategy.LeadingZeroes:
+ // request = {
+ // strategy: ChallengeStrategy.LeadingZeroes,
+ // salt: this.challengeData.salt,
+ // difficulty: this.challengeData.difficulty,
+ // };
+ // break;
+ // case ChallengeStrategy.TargetNumber:
+ // request = {
+ // strategy: ChallengeStrategy.TargetNumber,
+ // target: this.challengeData.target,
+ // salt: this.challengeData.salt,
+ // };
+ // break;
+ // }
switch (this.challengeData.strategy) {
- case ChallengeStrategy.LeadingZeroes:
+ case ChallengeStrategy.kCTF:
request = {
- strategy: ChallengeStrategy.LeadingZeroes,
- salt: this.challengeData.salt,
- difficulty: this.challengeData.difficulty,
- };
- break;
- case ChallengeStrategy.TargetNumber:
- request = {
- strategy: ChallengeStrategy.TargetNumber,
- target: this.challengeData.target,
- salt: this.challengeData.salt,
+ strategy: ChallengeStrategy.kCTF,
+ challenge: this.challengeData.challenge,
};
break;
}
@@ -350,7 +343,7 @@ export class PowCaptcha extends LitElement {
// blocking, some workers may block on the read, and as soon as they
// unblock, they return 0 since the challenge is already solved.
//
- // We need to do a better job of tracking solvers, so if one worker
+ // TODO: We need to do a better job of tracking solvers, so if one worker
// errors out, we only error out if all workers have errored out.
let worker_promises: Promise[] = [];
for (let worker of this.solverWorkers) {
@@ -361,31 +354,44 @@ export class PowCaptcha extends LitElement {
let solution = await Promise.race(worker_promises);
if (solution.type === WorkerResponseType.Error) {
- this.errorMessage = solution.error;
+ console.error("Worker error:", solution.error);
+ this.status = 'error';
return;
}
if (solution.type !== WorkerResponseType.Solution) {
- this.errorMessage = "Something went wrong, please try again later.";
+ console.error("Worker sent spurious message");
+ this.status = 'error';
return;
}
- this.solution = Atomics.load(atomics_view, 1).toString();
- this.isSolving = false;
- this.solved = true;
+ // TODO: configure if we should fetch or not
+ try {
+ await fetch(`${this.challengeUrl}/challenge`, {
+ method: 'POST',
+ body: JSON.stringify({
+ challenge: this.challengeData.challenge,
+ solution: solution.solution,
+ }),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
- if (this.hashRateInterval !== null) {
- clearInterval(this.hashRateInterval);
- this.hashRateInterval = null;
+ this.status = 'solved';
+
+ this.dispatchEvent(new CustomEvent('impost:solved', {
+ detail: {
+ challenge: this.challengeData.challenge,
+ solution: solution.solution,
+ },
+ bubbles: true,
+ composed: true,
+ }))
+ } catch {
+ console.error('Failed to submit solution');
+ this.status = 'error';
}
-
- this.dispatchEvent(new CustomEvent('impost:solved', {
- detail: {
- solution: this.solution,
- },
- bubbles: true,
- composed: true,
- }))
}
solvePreventDefault(event: Event) {
@@ -400,12 +406,6 @@ export class PowCaptcha extends LitElement {
this.challengejson = '';
}
- if (this.errorMessage) {
- return html`
- ${this.errorMessage}
- `;
- }
-
if (this.challengeData === null) {
return html`
Loading captcha challenge...
@@ -416,9 +416,9 @@ export class PowCaptcha extends LitElement {
-
${this.solved ? 'Verified' : html`${this.isSolving ? 'Verifying...' : 'I am not a robot'}`}
+
${this.status === 'error' ? 'Something went wrong' : this.status === 'solved' ? 'Verified' : html`${this.status === 'solving' ? 'Verifying...' : 'I am not a robot'}`}