Clean up code. Reorganize files. Port stuff from other branches. + more
This turns the project into a monorepo using pnpm workspaces, dramatically simplifying the build process. It also fixes a lot of bugs and just generally makes the codebase a lot cleaner.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
@@ -14,6 +14,6 @@ This is the impost monorepo, containing the following packages:
|
||||
- `@impost/lib`: A library that can be used to generate, solve, and verify
|
||||
proofs.
|
||||
|
||||
It also contains a `solver` package, which is the PoW solver written in Zig,
|
||||
`@impost/lib` is built on top of, an example of how to use the solver in a
|
||||
nuxt 3 project.
|
||||
It also contains a `solver` package, which is the Zig PoW solver that
|
||||
`@impost/lib` is built on top of, an example of how to use the solver in
|
||||
example-app
|
||||
|
||||
@@ -1 +1 @@
|
||||
YAPTCHA_HMAC_SECRET=xxx # openssl rand -base64 32
|
||||
IMPOST_HMAC_SECRET=xxx # openssl rand -base64 32
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# YAPTCHA
|
||||
# IMPOST
|
||||
|
||||
Yet Another Pow capTCHA.
|
||||
|
||||
## What is this
|
||||
|
||||
YAPTCHA is a proof of work based challenge-response system that is designed to
|
||||
IMPOST is a proof of work based challenge-response system that is designed to
|
||||
ward off spam and abuse.
|
||||
|
||||
<!-- TODO: -->
|
||||
|
||||
@@ -1,411 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { type WorkerRequest, type SolutionMessage, WorkerResponseType, WorkerMessageType, ChallengeStrategy, type Challenge } from '~/types/pow';
|
||||
import WASMSolverUrl from "~/utils/solver.wasm?url";
|
||||
import ChallengeWorker from '~/utils/worker?worker';
|
||||
const { data: challengeData } = await useFetch('/api/pow/challenge');
|
||||
|
||||
let shared_atomics = new SharedArrayBuffer(12);
|
||||
|
||||
let workers: Worker[] = [];
|
||||
let workers_initialized = false;
|
||||
if (import.meta.client) {
|
||||
try {
|
||||
// const num_workers = 1;
|
||||
const num_workers = navigator.hardwareConcurrency;
|
||||
|
||||
for (let i = 0; i < num_workers; i++) {
|
||||
workers.push(new ChallengeWorker());
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to create worker:", error);
|
||||
}
|
||||
}
|
||||
|
||||
let autoSolve: Ref<boolean> = ref(false);
|
||||
watch(autoSolve, async () => {
|
||||
if (autoSolve.value) {
|
||||
while (autoSolve.value) {
|
||||
if (solving.value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
continue;
|
||||
}
|
||||
|
||||
await getChallenge();
|
||||
await solveChallenge();
|
||||
if (!autoSolve.value) {
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let total_solved: Ref<number> = ref(0);
|
||||
let total_solving_for: Ref<number> = ref(0);
|
||||
|
||||
function pluralize(value: number, string: string) {
|
||||
return value === 1 ? string : `${string}s`;
|
||||
}
|
||||
|
||||
let challenge_loading: Ref<boolean> = ref(false);
|
||||
let challenge_loading_indicator: Ref<string> = ref('');
|
||||
let challenge: Ref<Challenge | null> = ref(null);
|
||||
|
||||
let nonce: Ref<string | null> = ref(null);
|
||||
let solved: Ref<boolean> = ref(false);
|
||||
let solving_for: Ref<string> = ref('0');
|
||||
|
||||
const number_formatter = new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
// the hashrate of all the runs
|
||||
let hashrate_array: Ref<Array<number>> = ref([]);
|
||||
let hashrate = computed(() => {
|
||||
if (hashrate_array.value.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return hashrate_array.value.reduce((a, b) => a + b, 0) / hashrate_array.value.length;
|
||||
});
|
||||
|
||||
let solving = ref(false);
|
||||
let solveTime: Ref<number> = ref(0);
|
||||
|
||||
let solveTimeout: Ref<any | null> = ref(null);
|
||||
|
||||
let challenge_error: Ref<string | null> = ref(null);
|
||||
|
||||
let difficulty: Ref<number> = ref(0);
|
||||
|
||||
let { data } = await useFetch('/api/pow/difficulty');
|
||||
|
||||
if (data.value) {
|
||||
difficulty.value = data.value.difficulty;
|
||||
}
|
||||
|
||||
const MESSAGE_TIMEOUT = 3000;
|
||||
|
||||
async function getChallenge() {
|
||||
challenge_error.value = null;
|
||||
challenge_loading_indicator.value = '';
|
||||
challenge.value = null;
|
||||
nonce.value = null;
|
||||
challenge_loading.value = true;
|
||||
|
||||
const spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
let spinner_index = 0;
|
||||
const loading_interval = setInterval(() => {
|
||||
challenge_loading_indicator.value = spinners[spinner_index]!;
|
||||
spinner_index = (spinner_index + 1) % spinners.length;
|
||||
console.log(spinners[spinner_index]);
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
const new_challenge = await $fetch('/api/pow/challenge');
|
||||
challenge.value = new_challenge.challenge;
|
||||
} catch (error: any) {
|
||||
console.error("Failed to get challenge:", error);
|
||||
challenge_error.value = `Failed to get challenge: ${error.message}`;
|
||||
} finally {
|
||||
challenge_loading.value = false;
|
||||
clearInterval(loading_interval);
|
||||
}
|
||||
}
|
||||
|
||||
async function initWorkers() {
|
||||
if (workers_initialized) {
|
||||
throw createError("Workers already initialized");
|
||||
}
|
||||
|
||||
workers_initialized = true;
|
||||
|
||||
const module = await WebAssembly.compileStreaming(fetch(WASMSolverUrl));
|
||||
|
||||
const atomics_view = new Int32Array(shared_atomics);
|
||||
Atomics.store(atomics_view, 0, 0);
|
||||
Atomics.store(atomics_view, 1, 0);
|
||||
|
||||
console.debug(`Initializing ${workers.length} workers`);
|
||||
|
||||
let worker_promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < workers.length; i++) {
|
||||
const worker = workers[i]!;
|
||||
|
||||
worker_promises.push(new Promise<void>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
if (event.data.type === WorkerResponseType.Error) {
|
||||
console.error("Worker error:", event.data.error);
|
||||
reject(event.data.error);
|
||||
}
|
||||
|
||||
if (event.data.type === WorkerResponseType.Ok) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject(new Error("Unexpected message from worker"));
|
||||
};
|
||||
|
||||
const error_handler = (error: ErrorEvent) => {
|
||||
console.error("Worker error:", error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.addEventListener('message', message_handler);
|
||||
worker.addEventListener('error', error_handler);
|
||||
|
||||
worker.postMessage({
|
||||
type: WorkerMessageType.Init,
|
||||
module: module,
|
||||
sab: shared_atomics,
|
||||
} as WorkerRequest);
|
||||
}));
|
||||
}
|
||||
|
||||
const timeoutMs = 10 * 1000;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`Function timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
Promise.all(worker_promises),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
console.log("All workers initialized");
|
||||
}
|
||||
|
||||
async function getChallengeSolution(worker: Worker, request: { strategy: ChallengeStrategy.LeadingZeroes, target: string, difficulty: number } | { strategy: ChallengeStrategy.TargetNumber, target: string, salt: string }): Promise<SolutionMessage> {
|
||||
return new Promise<SolutionMessage>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
worker.removeEventListener('message', message_handler);
|
||||
worker.removeEventListener('error', error_handler);
|
||||
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
const error_handler = (error: ErrorEvent) => {
|
||||
worker.removeEventListener('error', error_handler);
|
||||
worker.removeEventListener('message', message_handler);
|
||||
console.error("Worker error:", error);
|
||||
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.addEventListener('message', message_handler);
|
||||
worker.addEventListener('error', error_handler);
|
||||
|
||||
switch (request.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
worker.postMessage({
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
target: request.target,
|
||||
difficulty: request.difficulty,
|
||||
} as WorkerRequest);
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
worker.postMessage({
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
target: request.target,
|
||||
salt: request.salt,
|
||||
} as WorkerRequest);
|
||||
break;
|
||||
}
|
||||
if (!challengeData) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Failed to fetch challenge',
|
||||
});
|
||||
}
|
||||
|
||||
async function solveChallenge() {
|
||||
if (!challenge.value?.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workers_initialized) {
|
||||
try {
|
||||
await initWorkers();
|
||||
} catch (error: any) {
|
||||
console.error("Failed to initialize workers:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const atomics_view = new Int32Array(shared_atomics);
|
||||
Atomics.store(atomics_view, 0, 0);
|
||||
Atomics.store(atomics_view, 1, -1);
|
||||
|
||||
solved.value = false;
|
||||
challenge_error.value = null;
|
||||
solving.value = true;
|
||||
solveTime.value = 0;
|
||||
solving_for.value = '0';
|
||||
|
||||
let startTime = performance.now();
|
||||
|
||||
let solving_for_interval = setInterval(() => {
|
||||
solving_for.value = ((performance.now() - startTime) / 1000).toFixed(1);
|
||||
}, 100);
|
||||
|
||||
function cleanup() {
|
||||
clearTimeout(solveTimeout.value);
|
||||
solveTimeout.value = setTimeout(() => {
|
||||
solveTime.value = 0;
|
||||
}, MESSAGE_TIMEOUT);
|
||||
|
||||
solved.value = false;
|
||||
solving.value = false;
|
||||
clearInterval(solving_for_interval);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
let request: { strategy: ChallengeStrategy.LeadingZeroes, target: string, difficulty: number } | { strategy: ChallengeStrategy.TargetNumber, target: string, salt: string };
|
||||
|
||||
switch (challenge.value.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
request = {
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
target: challenge.value.target,
|
||||
difficulty: challenge.value.difficulty,
|
||||
};
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
request = {
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
target: challenge.value.target,
|
||||
salt: challenge.value.salt,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
let worker_promises: Promise<SolutionMessage>[] = [];
|
||||
for (let worker of workers) {
|
||||
// dispatch to all workers, func is async so it will not block
|
||||
worker_promises.push(getChallengeSolution(worker, request));
|
||||
}
|
||||
|
||||
let solution = await Promise.race(worker_promises);
|
||||
|
||||
if (solution.type === WorkerResponseType.Error) {
|
||||
throw createError(solution.error);
|
||||
}
|
||||
|
||||
if (solution.type === WorkerResponseType.Ok) {
|
||||
throw createError("spurious solution");
|
||||
}
|
||||
|
||||
console.log(shared_atomics.slice(8, 12));
|
||||
|
||||
nonce.value = Atomics.load(atomics_view, 1).toString();
|
||||
|
||||
solveTime.value = Math.floor(performance.now() - startTime);
|
||||
total_solved.value += 1;
|
||||
total_solving_for.value += solveTime.value;
|
||||
// since nonce is the number of iterations we have completed, we can divide that by how long it took in second
|
||||
// to get H/s
|
||||
hashrate_array.value.push(+Atomics.load(atomics_view, 1) / (solveTime.value / 1000));
|
||||
clearTimeout(solveTimeout.value);
|
||||
|
||||
await $fetch('/api/pow/challenge', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
challenge: challenge.value.target,
|
||||
nonce: nonce.value,
|
||||
}
|
||||
});
|
||||
|
||||
solved.value = true;
|
||||
|
||||
solveTimeout.value = setTimeout(() => {
|
||||
solveTime.value = 0;
|
||||
solved.value = false;
|
||||
}, MESSAGE_TIMEOUT);
|
||||
|
||||
switch (challenge.value.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
console.debug("Solved challenge with difficulty", challenge.value.difficulty, "in " + solveTime.value + "ms");
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
console.debug("Solved challenge with salt", challenge.value.salt, "in " + solveTime.value + "ms");
|
||||
break;
|
||||
}
|
||||
|
||||
solving.value = false;
|
||||
clearInterval(solving_for_interval);
|
||||
} catch (error: any) {
|
||||
challenge_error.value = `Failed to solve challenge: ${error.message}`;
|
||||
console.error(error);
|
||||
cleanup();
|
||||
}
|
||||
function solved(ev: CustomEvent) {
|
||||
console.log("Impost Solved:", ev.detail.solution);
|
||||
}
|
||||
|
||||
async function setDifficulty(difficulty: number) {
|
||||
const response = await $fetch('/api/pow/difficulty', {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
difficulty,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
function formsubmit(ev: Event) {
|
||||
console.log("Submitted form");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between" v-if="hashrate_array.length !== 0">
|
||||
<span>Your average Hashrate: {{ number_formatter.format(hashrate) }} H/s</span>
|
||||
<span>You have solved {{ total_solved }} {{ pluralize(total_solved, "challenge") }} in
|
||||
{{ number_formatter.format(total_solving_for / 1000) }}s</span>
|
||||
<span>Your Hashrate on the last challenge: {{ number_formatter.format(hashrate_array.at(-1)!) }} H/s</span>
|
||||
<div class="flex justify-center items-center h-screen w-screen ">
|
||||
<form @submit="formsubmit" action="/"
|
||||
class="p-5 rounded-2xl bg-dark-9 border-coolGray-600 border flex flex-col gap-2">
|
||||
<impost-captcha name="impost" challengeUrl="/api/pow" auto="onsubmit"
|
||||
:challengejson="JSON.stringify(challengeData!.challenge)" @impost:solved="solved" />
|
||||
<input class="bg-blue-7 text-white font-semibold px-4 py-2.5 border-0 rounded-md" type="submit"
|
||||
value="Submit" />
|
||||
</form>
|
||||
</div>
|
||||
<p v-else>You have not solved any challenges yet</p>
|
||||
<p>Challenge: <span v-if="challenge_loading">{{ challenge_loading_indicator }}</span>
|
||||
<span v-else>{{ challenge }}</span>
|
||||
</p>
|
||||
<p>Nonce: {{ nonce }}</p>
|
||||
<button @click="getChallenge()" :disabled="solving || challenge_loading">
|
||||
Get Challenge
|
||||
</button>
|
||||
<button @click="solveChallenge()" :disabled="solving || challenge === null || nonce !== null">
|
||||
<span v-if="!solving">Solve Challenge</span>
|
||||
<span v-else>Solving challenge for {{ solving_for }}s...</span>
|
||||
</button>
|
||||
<div>
|
||||
<div v-if="solveTime && !challenge_error" class="min-h-[1rem]">
|
||||
<span v-if="solved">Challenge solved in {{ solveTime }}ms!</span>
|
||||
<span v-else>Validating solution...</span>
|
||||
</div>
|
||||
<div v-else-if="challenge_error">{{ challenge_error }}</div>
|
||||
<p v-else class="empty-p"><!-- Empty so there is no content shift when there is text or isnt --></p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row w-fit">
|
||||
<label for="autoSolve">Auto solve</label>
|
||||
<input class="w-min" type="checkbox" v-model="autoSolve" id="autoSolve"></input>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-fit">
|
||||
<label for="difficulty">Difficulty</label>
|
||||
<input class="w-min" type="number" min="1" max="64" v-model="difficulty" @change="setDifficulty(difficulty)"
|
||||
id="difficulty"></input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
min-width: 140px;
|
||||
padding: 0.25rem 0.375rem;
|
||||
}
|
||||
|
||||
.empty-p {
|
||||
margin: 0;
|
||||
|
||||
&::after {
|
||||
content: "-";
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
@@ -1,38 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const { data: challengeData } = await useFetch('/api/pow/challenge');
|
||||
|
||||
if (!challengeData) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: 'Failed to fetch challenge',
|
||||
});
|
||||
}
|
||||
|
||||
function solved(ev: CustomEvent) {
|
||||
console.log("Solved:", ev.detail.solution);
|
||||
}
|
||||
|
||||
function formsubmit(ev: Event) {
|
||||
console.log("Submitted form");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('impost:solved', (ev) => solved(ev as CustomEvent));
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('impost:solved', (ev) => solved(ev as CustomEvent));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center items-center h-screen w-screen ">
|
||||
<form @submit.prevent="formsubmit"
|
||||
class="p-5 rounded-2xl bg-dark-9 border-coolGray-600 border flex flex-col gap-2">
|
||||
<pow-captcha challengeUrl="/api/pow" auto="onsubmit"
|
||||
:challengejson="JSON.stringify(challengeData!.challenge)" @impost:solved="solved" />
|
||||
<input class="bg-blue-7 text-white font-semibold px-4 py-2.5 border-0 rounded-md" type="submit"
|
||||
value="Submit" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +0,0 @@
|
||||
const adjectives = ['swift', 'silent', 'hidden', 'clever', 'brave', 'sharp', 'shadow', 'crimson', 'bright', 'quiet', 'loud', 'happy', 'dark', 'evil', 'good', 'intelligent', 'lovely', 'mysterious', 'peaceful', 'powerful', 'pure', 'quiet', 'shiny', 'sleepy', 'strong', 'sweet', 'tall', 'warm', 'gentle', 'kind', 'nice', 'polite', 'rough', 'rude', 'scary', 'shy', 'silly', 'smart', 'strange', 'tough', 'ugly', 'vivid', 'wicked', 'wise', 'young', 'sleepy'];
|
||||
const nouns = ['fox', 'river', 'stone', 'cipher', 'link', 'comet', 'falcon', 'signal', 'anchor', 'spark', 'stone', 'comet', 'rocket', 'snake', 'snail', 'shark', 'elephant', 'cat', 'dog', 'whale', 'orca', 'cactus', 'flower', 'frog', 'toad', 'apple', 'strawberry', 'raspberry', 'lemon', 'bot', 'gopher', 'dinosaur', 'racoon', 'penguin', 'chameleon', 'atom', 'particle', 'witch', 'wizard', 'warlock', 'deer']
|
||||
|
||||
export function getWorkerName() {
|
||||
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
// This worker just sits on another thread and waits for message to solve
|
||||
// challenges so that we dont block the render thread
|
||||
|
||||
import {
|
||||
type WorkerRequest,
|
||||
type SolutionMessage,
|
||||
WorkerMessageType,
|
||||
WorkerResponseType,
|
||||
ChallengeStrategy,
|
||||
} from "~/types/pow";
|
||||
|
||||
const worker_name = getWorkerName();
|
||||
let solver: SolverModule | null = null;
|
||||
|
||||
let atomic_nonce: Int32Array | null = null;
|
||||
let atomic_solution: Int32Array | null = null;
|
||||
|
||||
async function loadWasmSolver(module: WebAssembly.Module) {
|
||||
if (atomic_nonce === null || atomic_solution === null) {
|
||||
throw createError("Atomics not initialized");
|
||||
}
|
||||
|
||||
console.debug(`[${worker_name}]: Loading WASM solver`);
|
||||
|
||||
solver = await WebAssembly.instantiate(module, {
|
||||
env: {
|
||||
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
__log: (ptr: number, len: number) => {
|
||||
const string_data = new Uint8Array(solver!.exports.memory.buffer, ptr, len);
|
||||
console.log(`[${worker_name}]: ${new TextDecoder().decode(string_data)}`);
|
||||
},
|
||||
}
|
||||
}) as unknown as SolverModule;
|
||||
console.debug(`[${worker_name}]: WASM solver loaded`);
|
||||
}
|
||||
|
||||
onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
if (event.data.type === WorkerMessageType.Init) {
|
||||
console.log(`[${worker_name}]: Initializing...`);
|
||||
|
||||
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
|
||||
atomic_solution = new Int32Array(event.data.sab, 4, 1);
|
||||
|
||||
try {
|
||||
await loadWasmSolver(event.data.module);
|
||||
} catch (error: any) {
|
||||
console.error(`[${worker_name}]: Failed to load WASM solver:`, error);
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: `Could not load WASM solver: ${error.message}`,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
console.error(`[${worker_name}]: Failed to load WASM solver`);
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to load WASM solver",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.Ok,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "WASM solver not loaded",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const { strategy } = event.data;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
let solution: number;
|
||||
let target: string = event.data.target;
|
||||
let target_bytes, target_ptr;
|
||||
let memory;
|
||||
switch (strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
const { difficulty } = event.data;
|
||||
console.debug(`[${worker_name}]: recieved ${strategy} challenge: ${target}, difficulty: ${difficulty}`);
|
||||
|
||||
target_bytes = encoder.encode(target);
|
||||
|
||||
target_ptr = solver.exports.malloc(target_bytes.length);
|
||||
if (target_ptr === 0 || target_ptr === null) {
|
||||
console.error(`[${worker_name}]: Failed to allocate memory for challenge string`);
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to allocate memory for challenge string",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
|
||||
solution = solver.exports.solve_leaading_zeroes_challenge(
|
||||
target_ptr,
|
||||
target.length,
|
||||
difficulty,
|
||||
);
|
||||
console.debug(`[${worker_name}]: WASM solver found nonce: ${solution}`);
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
const { salt } = event.data;
|
||||
console.debug(`[${worker_name}]: recieved ${strategy} challenge: ${target}, salt: ${salt}`);
|
||||
|
||||
const salt_bytes = encoder.encode(salt);
|
||||
target_bytes = encoder.encode(target);
|
||||
|
||||
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
||||
if (salt_ptr === 0 || salt_ptr === null) {
|
||||
console.error(`[${worker_name}]: Failed to allocate memory for salt string`);
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to allocate memory for salt string",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
target_ptr = solver.exports.malloc(target_bytes.length);
|
||||
if (target_ptr === 0 || target_ptr === null) {
|
||||
console.error(`[${worker_name}]: Failed to allocate memory for target string`);
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to allocate memory for target string",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
memory = new Uint8Array(solver.exports.memory.buffer);
|
||||
memory.set(salt_bytes, salt_ptr);
|
||||
memory.set(target_bytes, target_ptr);
|
||||
|
||||
solution = solver.exports.solve_target_number_challenge(
|
||||
target_ptr,
|
||||
target_bytes.length,
|
||||
salt_ptr,
|
||||
salt_bytes.length,
|
||||
);
|
||||
console.debug(`[${worker_name}]: WASM solver found nonce: ${solution}`);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// we are just assuming that if its less than -1, its the min i32
|
||||
if (solution < 0) {
|
||||
return postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "failed to solve challenge",
|
||||
} as SolutionMessage);
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.Solution,
|
||||
nonce: solution === -1 ? null : solution.toString()
|
||||
} as SolutionMessage);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
strategy = "target_number"
|
||||
|
||||
[leading_zeroes]
|
||||
difficulty = 4
|
||||
difficulty = 2
|
||||
|
||||
[target_number]
|
||||
max_number = 10000
|
||||
max_number = 192
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
},
|
||||
modules: ['@unocss/nuxt', ['nuxt-ssr-lit', { litElementPrefix: 'pow-' }]],
|
||||
modules: ['@unocss/nuxt', ['nuxt-ssr-lit', { litElementPrefix: 'impost-' }]],
|
||||
|
||||
nitro: {
|
||||
moduleSideEffects: ["@impost/widget"],
|
||||
@@ -28,17 +28,9 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
imports: {
|
||||
transform: {
|
||||
// only necessary in the monorepo since vite is going out to the
|
||||
// source of the widget and transforming it and clobbering it.
|
||||
exclude: [/impost/],
|
||||
}
|
||||
},
|
||||
|
||||
vue: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.includes('pow-'),
|
||||
isCustomElement: (tag) => tag.startsWith('impost-'),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
1455
example-app/package-lock.json
generated
1455
example-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "hello-nuxt",
|
||||
"name": "example-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -9,15 +9,13 @@
|
||||
"preview": "nuxt preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.1.1",
|
||||
"@impost/lib": "workspace:*",
|
||||
"@impost/widget": "workspace:*",
|
||||
"js-toml": "^1.0.2",
|
||||
"lit-element": "^4.2.1",
|
||||
"lit-html": "^3.3.1",
|
||||
"nuxt": "latest",
|
||||
"nuxt-ssr-lit": "1.6.32",
|
||||
"zod": "^4.1.12",
|
||||
"@impost/lib": "file:../packages/lib",
|
||||
"@impost/widget": "file:../packages/widget"
|
||||
"vue": "^3.5.25",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.0",
|
||||
|
||||
@@ -13,26 +13,29 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readValidatedBody(event, challengeSchema.safeParse);
|
||||
|
||||
if (!body.success) {
|
||||
const errors = z.treeifyError(body.error);
|
||||
const error_message = Object.entries(errors.errors).map(([key, value]) => `${key}: ${value}`).join('\n');
|
||||
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Validation failed'
|
||||
statusMessage: error_message
|
||||
})
|
||||
}
|
||||
|
||||
let target = body.data.challenge;
|
||||
let nonce = body.data.nonce;
|
||||
let { challenge, nonce } = body.data;
|
||||
|
||||
const outstanding_challenge = outstandingChallenges.get(body.data.challenge)!;
|
||||
// always delete the challenge on a solve attempt
|
||||
clearTimeout(outstanding_challenge.timeout);
|
||||
outstandingChallenges.delete(challenge);
|
||||
|
||||
// check if the challenge is valid
|
||||
let challenge_valid = await validate_challenge(outstandingChallenges.get(target)!.challenge, {
|
||||
challenge: target,
|
||||
nonce: nonce
|
||||
let challenge_valid = await validate_challenge(outstandingChallenges.get(challenge)!.challenge, {
|
||||
challenge,
|
||||
nonce,
|
||||
});
|
||||
|
||||
if (challenge_valid) {
|
||||
// clear the challenge
|
||||
clearTimeout(outstandingChallenges.get(target)!.timeout);
|
||||
outstandingChallenges.delete(target);
|
||||
|
||||
return {
|
||||
message: 'Challenge solved'
|
||||
};
|
||||
|
||||
26
justfile
26
justfile
@@ -1,26 +0,0 @@
|
||||
build: build-widget
|
||||
|
||||
wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz"
|
||||
zig-build-args := "--release=fast"
|
||||
|
||||
npm-runner := "npm"
|
||||
|
||||
[working-directory: "solver"]
|
||||
build-wasm:
|
||||
zig build {{zig-build-args}}
|
||||
wasm-opt {{wasm-opt-args}} --enable-simd -o zig-out/bin/solver.wasm zig-out/bin/solver.wasm
|
||||
# The server does not support simd, so we disable it here
|
||||
wasm-opt {{wasm-opt-args}} --disable-simd -o zig-out/bin/validator.wasm zig-out/bin/validator.wasm
|
||||
|
||||
[working-directory: "packages/lib"]
|
||||
build-lib: build-wasm
|
||||
{{npm-runner}} install
|
||||
{{npm-runner}} link
|
||||
{{npm-runner}} run build
|
||||
|
||||
[working-directory: "packages/widget"]
|
||||
build-widget: build-lib
|
||||
{{npm-runner}} install
|
||||
{{npm-runner}} link @impost/lib
|
||||
{{npm-runner}} link
|
||||
{{npm-runner}} run build
|
||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "impost-monorepo",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm build:all",
|
||||
"build:all": "pnpm build:solver && pnpm build:lib && pnpm build:widget",
|
||||
"build:solver": "pnpm --filter solver build",
|
||||
"build:lib": "pnpm --filter lib build",
|
||||
"build:widget": "pnpm --filter widget build",
|
||||
"dev:example": "pnpm --filter example-app dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.21.0+sha512.da3337267e400fdd3d479a6c68079ac6db01d8ca4f67572083e722775a796788a7a9956613749e000fac20d424b594f7a791a5f4e2e13581c5ef947f26968a40"
|
||||
}
|
||||
3
packages/lib/package-lock.json
generated
3
packages/lib/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -36,4 +36,4 @@
|
||||
"dependencies": {
|
||||
"uuidv7": "^1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||
import WASMSolverUrl from '../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||
import { ChallengeStrategy, type Challenge, type ChallengeLeadingZeroes, type ChallengeTargetNumber } from '.';
|
||||
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"malloc": (byte_count: number) => number | null;
|
||||
@@ -17,6 +18,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<WebAssembly.Module> {
|
||||
@@ -29,7 +31,18 @@ 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 {
|
||||
export function solve_challenge(solver: SolverModule, challenge: Challenge): number {
|
||||
switch (challenge.strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
return solve_leaading_zeroes_challenge(solver, challenge);
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
return solve_target_number_challenge(solver, challenge);
|
||||
default:
|
||||
throw new Error("Invalid challenge strategy");
|
||||
}
|
||||
}
|
||||
|
||||
function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: ChallengeLeadingZeroes): number {
|
||||
const { salt, difficulty } = challenge;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
@@ -56,7 +69,7 @@ export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge:
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function solve_target_number_challenge(solver: SolverModule, challenge: { salt: string, target: string }): number {
|
||||
function solve_target_number_challenge(solver: SolverModule, challenge: ChallengeTargetNumber): number {
|
||||
const { salt, target } = challenge;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChallengeAlgorithm, ChallengeStrategy, type Challenge } from '.';
|
||||
import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||
import { ChallengeAlgorithm, ChallengeStrategy, type Challenge, type ChallengeLeadingZeroes, type ChallengeTargetNumber } from '.';
|
||||
import WASMValidatorUrl from '../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||
|
||||
type WasmExports = Record<string, Function> & {
|
||||
"malloc": (byte_count: number) => number | null;
|
||||
@@ -18,11 +18,11 @@ 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<Challenge> {
|
||||
async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise<ChallengeLeadingZeroes> {
|
||||
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 = {
|
||||
let challenge: ChallengeLeadingZeroes = {
|
||||
algorithm: ChallengeAlgorithm.Argon2id,
|
||||
strategy: ChallengeStrategy.LeadingZeroes,
|
||||
salt,
|
||||
@@ -32,7 +32,7 @@ async function generate_leading_zeroes_challenge(parameters: Object, difficulty:
|
||||
return challenge;
|
||||
}
|
||||
|
||||
async function generate_target_number_challenge(parameters: Object, max_number: number): Promise<Challenge | null> {
|
||||
async function generate_target_number_challenge(parameters: Object, max_number: number): Promise<ChallengeTargetNumber | null> {
|
||||
// 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;
|
||||
@@ -68,7 +68,7 @@ async function generate_target_number_challenge(parameters: Object, max_number:
|
||||
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 = {
|
||||
let challenge: ChallengeTargetNumber = {
|
||||
algorithm: ChallengeAlgorithm.Argon2id,
|
||||
strategy: ChallengeStrategy.TargetNumber,
|
||||
salt,
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineConfig({
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
sourcemap: true
|
||||
minify: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -2,17 +2,13 @@ const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const wasm_target = b.resolveTargetQuery(.{
|
||||
.cpu_arch = .wasm32,
|
||||
.os_tag = .freestanding,
|
||||
.cpu_features_add = std.Target.wasm.featureSet(&.{ .bulk_memory, .bulk_memory_opt, .simd128, .tail_call }),
|
||||
});
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// solver
|
||||
const solver_mod = b.addModule("solver", .{
|
||||
.root_source_file = b.path("src/solver.zig"),
|
||||
.optimize = optimize,
|
||||
.target = wasm_target,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const solver_exe = b.addExecutable(.{
|
||||
@@ -20,10 +16,12 @@ pub fn build(b: *std.Build) void {
|
||||
.root_module = solver_mod,
|
||||
});
|
||||
|
||||
solver_exe.entry = .disabled;
|
||||
solver_exe.rdynamic = true;
|
||||
solver_exe.lto = .full;
|
||||
solver_exe.link_gc_sections = true;
|
||||
if (target.result.cpu.arch == .wasm32) {
|
||||
solver_exe.entry = .disabled;
|
||||
solver_exe.rdynamic = true;
|
||||
solver_exe.link_gc_sections = true;
|
||||
solver_exe.lto = .full;
|
||||
}
|
||||
|
||||
b.installArtifact(solver_exe);
|
||||
|
||||
@@ -31,16 +29,18 @@ pub fn build(b: *std.Build) void {
|
||||
const validator_mod = b.addModule("validator", .{
|
||||
.root_source_file = b.path("src/validator.zig"),
|
||||
.optimize = optimize,
|
||||
.target = wasm_target,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
const validator_exe = b.addLibrary(.{
|
||||
const validator_exe = b.addExecutable(.{
|
||||
.name = "validator",
|
||||
.root_module = validator_mod,
|
||||
});
|
||||
|
||||
validator_exe.entry = .disabled;
|
||||
validator_exe.rdynamic = true;
|
||||
if (target.result.cpu.arch == .wasm32) {
|
||||
validator_exe.entry = .disabled;
|
||||
validator_exe.rdynamic = true;
|
||||
}
|
||||
|
||||
b.installArtifact(validator_exe);
|
||||
}
|
||||
11
packages/solver/package.json
Normal file
11
packages/solver/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "solver",
|
||||
"description": "Zig WASM POW solver, not an actual node package, just using this so that pnpm will build it for me",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm run build:wasm && pnpm run minify",
|
||||
"build:wasm": "zig build --release=fast -Dtarget=wasm32-freestanding -Dcpu=generic+bulk_memory+bulk_memory_opt+simd128+tail_call",
|
||||
"minify": "wasm-opt --strip-debug --strip-dwarf -O4 -o zig-out/bin/solver.wasm zig-out/bin/solver.wasm && wasm-opt --strip-debug --strip-dwarf -O4 -o zig-out/bin/validator.wasm zig-out/bin/validator.wasm"
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
var argon2_params = std.crypto.pwhash.argon2.Params{
|
||||
.t = 4, // time cost
|
||||
.m = 256, // memory cost (in KiB)
|
||||
.t = 3, // time cost
|
||||
.m = 8192, // memory cost (in KiB)
|
||||
.p = 1, // parallelism (this doesnt do anything because we are targeting wasm, and we do multithreading differently anyways)
|
||||
};
|
||||
|
||||
@@ -11,21 +11,8 @@ const dk_len: usize = 32; // 16 or 32 byte key
|
||||
var derived: [dk_len]u8 = undefined;
|
||||
var buffer_hash_hex: [64]u8 = undefined;
|
||||
|
||||
fn bytesToHex(bytes: []const u8, output: []u8) void {
|
||||
const hex_chars = "0123456789abcdef";
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) : (i += 1) {
|
||||
output[i * 2] = hex_chars[(bytes[i] >> 4)];
|
||||
output[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash(allocator: Allocator, challenge: []const u8, nonce: []const u8) ![]u8 {
|
||||
try std.crypto.pwhash.argon2.kdf(allocator, &derived, nonce, challenge, argon2_params, .argon2id);
|
||||
|
||||
var hash_bytes: [32]u8 = undefined;
|
||||
std.crypto.hash.sha2.Sha256.hash(&derived, @ptrCast(hash_bytes[0..].ptr), .{});
|
||||
|
||||
bytesToHex(&hash_bytes, &buffer_hash_hex);
|
||||
return buffer_hash_hex[0..];
|
||||
return derived[0..];
|
||||
}
|
||||
@@ -1,23 +1,42 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
const hasher = @import("hasher.zig");
|
||||
const argon2 = @import("argon2.zig");
|
||||
|
||||
// var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
// var allocator = gpa.allocator();
|
||||
|
||||
var allocator = std.heap.wasm_allocator;
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var allocator = gpa.allocator();
|
||||
|
||||
extern fn __get_solution() i32;
|
||||
extern fn __set_solution(value: i32) void;
|
||||
extern fn __cmpxchg_solution(old: i32, new: i32) i32;
|
||||
extern fn __fetch_add_nonce(value: i32) i32;
|
||||
extern fn __log(str_ptr: usize, str_len: usize) void;
|
||||
|
||||
// fn log(comptime fmt: []const u8, args: anytype) void {
|
||||
// const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
|
||||
// __log(@intFromPtr(formatted.ptr), formatted.len);
|
||||
// allocator.free(formatted);
|
||||
// }
|
||||
fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void {
|
||||
if (comptime builtin.target.cpu.arch != .wasm32) {
|
||||
std.log.defaultLog(level, scope, fmt, args);
|
||||
return;
|
||||
}
|
||||
|
||||
const log_level_str = switch (level) {
|
||||
.err => "Error: ",
|
||||
.warn => "Warning: ",
|
||||
.info => "Info: ",
|
||||
.debug => "Debug: ",
|
||||
};
|
||||
|
||||
const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
|
||||
const log_str = std.fmt.allocPrint(allocator, "{s}{s}", .{ log_level_str, formatted }) catch return;
|
||||
allocator.free(formatted);
|
||||
__log(@intFromPtr(log_str.ptr), log_str.len);
|
||||
allocator.free(log_str);
|
||||
}
|
||||
|
||||
pub const std_options: std.Options = .{ .logFn = log };
|
||||
var hex_encoder = utils.HexEncoder{};
|
||||
|
||||
export fn malloc(byte_count: usize) ?*u8 {
|
||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||
@@ -31,42 +50,21 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||
}
|
||||
}
|
||||
|
||||
const SolveError = enum(u32) {
|
||||
InvalidDifficulty = 1,
|
||||
InvalidNonce = 2,
|
||||
NoSolution = 3,
|
||||
OutOfMemory = 4,
|
||||
};
|
||||
|
||||
var solve_error: ?SolveError = null;
|
||||
export fn get_solve_error() u32 {
|
||||
if (solve_error) |err| {
|
||||
return @intFromEnum(err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// returns nonce on success, -1 on failure
|
||||
// to get the error, call get_solve_error
|
||||
export fn solve_leaading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, difficulty: u32) i32 {
|
||||
solve_error = null;
|
||||
|
||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||
|
||||
if (difficulty < 1 or difficulty > 64) {
|
||||
solve_error = SolveError.InvalidDifficulty;
|
||||
std.log.err("Invalid difficulty for leading zeroes\n", .{});
|
||||
return -1;
|
||||
}
|
||||
|
||||
var target_prefix_buffer: [64]u8 = @splat('0');
|
||||
const target_prefix = target_prefix_buffer[0..difficulty];
|
||||
|
||||
const max_nonce_iterations: u64 = 1_000_000_000;
|
||||
|
||||
// 64 + 9 digits for nonce since the max nonce is 999_999_999 (not 1 billion since nonce < max_nonce_iterations)
|
||||
var input_buffer: []u8 = allocator.alloc(u8, challenge_len + 9) catch {
|
||||
// log("Failed to allocate memory for challenge\n", .{});
|
||||
solve_error = SolveError.OutOfMemory;
|
||||
std.log.err("Failed to allocate memory for challenge\n", .{});
|
||||
return -1;
|
||||
};
|
||||
// dont leak memory :pepega:
|
||||
@@ -83,34 +81,34 @@ export fn solve_leaading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: u
|
||||
}
|
||||
|
||||
const nonce_str = std.fmt.bufPrint(input_buffer[challenge_len..], "{d}", .{nonce}) catch {
|
||||
solve_error = SolveError.InvalidNonce;
|
||||
std.log.err("Failed to allocate memory for nonce\n", .{});
|
||||
return -1;
|
||||
};
|
||||
|
||||
const hash_hex_slice = hasher.hash(allocator, input_buffer[0..challenge_len], input_buffer[challenge_len .. challenge_len + nonce_str.len]) catch {
|
||||
solve_error = SolveError.OutOfMemory;
|
||||
const argon2_key = argon2.hash(allocator, input_buffer[0..challenge_len], input_buffer[challenge_len .. challenge_len + nonce_str.len]) catch {
|
||||
std.log.err("Failed to hash argon2 key\n", .{});
|
||||
return -1;
|
||||
};
|
||||
|
||||
if (std.mem.startsWith(u8, hash_hex_slice, target_prefix)) {
|
||||
// Found a solution!
|
||||
if (__cmpxchg_solution(-1, nonce) == -1) {
|
||||
// we found a solution, and we are the first to do so
|
||||
return nonce;
|
||||
} else {
|
||||
// we found a solution, but we are not the first to do so
|
||||
return 0;
|
||||
}
|
||||
_ = hex_encoder.encode(argon2_key);
|
||||
if (!hex_encoder.countZeroes(difficulty)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a solution!
|
||||
if (__cmpxchg_solution(-1, nonce) == -1) {
|
||||
// we found a solution, and we are the first to do so
|
||||
return nonce;
|
||||
} else {
|
||||
// we found a solution, but we are not the first to do so
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
solve_error = SolveError.NoSolution;
|
||||
return -1;
|
||||
}
|
||||
|
||||
export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, salt_ptr: [*]u8, salt_len: usize) i32 {
|
||||
solve_error = null;
|
||||
|
||||
const target_slice = target_ptr[0..target_len];
|
||||
const salt_slice = salt_ptr[0..salt_len];
|
||||
|
||||
@@ -119,7 +117,6 @@ export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, sa
|
||||
const max_digits = std.math.log10(max_nonce_iterations);
|
||||
|
||||
var input_buffer: []u8 = allocator.alloc(u8, salt_len + max_digits) catch {
|
||||
solve_error = SolveError.OutOfMemory;
|
||||
return -1;
|
||||
};
|
||||
defer allocator.free(input_buffer);
|
||||
@@ -133,16 +130,16 @@ export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, sa
|
||||
}
|
||||
|
||||
const nonce_str = std.fmt.bufPrint(input_buffer[salt_len..], "{d}", .{nonce}) catch {
|
||||
solve_error = SolveError.InvalidNonce;
|
||||
return -1;
|
||||
};
|
||||
|
||||
const hash_hex_slice = hasher.hash(allocator, input_buffer[0..salt_len], input_buffer[salt_len .. salt_len + nonce_str.len]) catch {
|
||||
solve_error = SolveError.OutOfMemory;
|
||||
const argon2_key = argon2.hash(allocator, input_buffer[0..salt_len], input_buffer[salt_len .. salt_len + nonce_str.len]) catch {
|
||||
return -1;
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, target_slice, hash_hex_slice)) {
|
||||
const hex_slice = hex_encoder.encode(argon2_key);
|
||||
|
||||
if (std.mem.eql(u8, target_slice, hex_slice)) {
|
||||
// Found a solution!
|
||||
if (__cmpxchg_solution(-1, nonce) == -1) {
|
||||
// we found a solution, and we are the first to do so
|
||||
@@ -154,6 +151,5 @@ export fn solve_target_number_challenge(target_ptr: [*]u8, target_len: usize, sa
|
||||
}
|
||||
}
|
||||
|
||||
solve_error = SolveError.NoSolution;
|
||||
return -1;
|
||||
}
|
||||
43
packages/solver/src/utils.zig
Normal file
43
packages/solver/src/utils.zig
Normal file
@@ -0,0 +1,43 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const HexEncoder = struct {
|
||||
scratch: [64]u8 = undefined,
|
||||
scratch_set: bool = false,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn encode(self: *Self, bytes: []const u8) []u8 {
|
||||
self.scratch_set = true;
|
||||
|
||||
const hex_chars = "0123456789abcdef";
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) : (i += 1) {
|
||||
self.scratch[i * 2] = hex_chars[(bytes[i] >> 4)];
|
||||
self.scratch[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
|
||||
}
|
||||
|
||||
return &self.scratch;
|
||||
}
|
||||
|
||||
// counts the number of leading hexidecimal zeroes in the scratch buffer
|
||||
// which is set by encode
|
||||
pub fn countZeroes(self: *Self, zeroes: usize) bool {
|
||||
if (!self.scratch_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (zeroes > 64 or zeroes == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < zeroes) : (i += 1) {
|
||||
if (self.scratch[i] != '0') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,13 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const hasher = @import("hasher.zig");
|
||||
const argon2 = @import("argon2.zig");
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
var allocator = std.heap.wasm_allocator;
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
var allocator = gpa.allocator();
|
||||
|
||||
var hex_encoder = utils.HexEncoder{};
|
||||
|
||||
export fn malloc(byte_count: usize) ?*u8 {
|
||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||
@@ -17,15 +21,6 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||
}
|
||||
}
|
||||
|
||||
fn bytesToHex(bytes: []const u8, buf: []u8) void {
|
||||
const hex_chars = "0123456789abcdef";
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) : (i += 1) {
|
||||
buf[i * 2] = hex_chars[(bytes[i] >> 4)];
|
||||
buf[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
export fn validate_leading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, difficulty: u32) i32 {
|
||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||
const nonce_slice = nonce_ptr[0..nonce_len];
|
||||
@@ -34,12 +29,10 @@ export fn validate_leading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len:
|
||||
return -1;
|
||||
}
|
||||
|
||||
var target_prefix_buffer: [64]u8 = @splat('0');
|
||||
const target_prefix = target_prefix_buffer[0..difficulty];
|
||||
const argon2_key = argon2.hash(allocator, challenge_slice, nonce_slice) catch return -2;
|
||||
|
||||
const hash_hex_slice = hasher.hash(allocator, challenge_slice, nonce_slice) catch return -2;
|
||||
|
||||
if (!std.mem.startsWith(u8, hash_hex_slice, target_prefix)) {
|
||||
_ = hex_encoder.encode(argon2_key);
|
||||
if (!hex_encoder.countZeroes(difficulty)) {
|
||||
return -3;
|
||||
}
|
||||
|
||||
@@ -51,9 +44,10 @@ export fn validate_target_number_challenge(target_ptr: [*]u8, target_len: usize,
|
||||
const salt_slice = salt_ptr[0..salt_len];
|
||||
const nonce_slice = nonce_ptr[0..nonce_len];
|
||||
|
||||
const hash_hex_slice = hasher.hash(allocator, salt_slice, nonce_slice) catch return -2;
|
||||
const argon2_key = argon2.hash(allocator, salt_slice, nonce_slice) catch return -2;
|
||||
const hex_slice = hex_encoder.encode(argon2_key);
|
||||
|
||||
if (!std.mem.eql(u8, target_slice, hash_hex_slice)) {
|
||||
if (!std.mem.eql(u8, target_slice, hex_slice)) {
|
||||
return -3;
|
||||
}
|
||||
|
||||
@@ -63,12 +57,13 @@ export fn validate_target_number_challenge(target_ptr: [*]u8, target_len: usize,
|
||||
export fn hash(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize) u64 {
|
||||
const challenge = challenge_ptr[0..challenge_len];
|
||||
const nonce = nonce_ptr[0..nonce_len];
|
||||
const hash_slice = hasher.hash(allocator, challenge, nonce) catch return 0;
|
||||
const argon2_key = argon2.hash(allocator, challenge, nonce) catch return 0;
|
||||
const hex_slice = hex_encoder.encode(argon2_key);
|
||||
|
||||
// bs to get the compiler to not whine about hash_slice.len being a u5 annd thus cannot be shifted by 32
|
||||
var ret: u64 = hash_slice.len;
|
||||
var ret: u64 = hex_slice.len;
|
||||
ret <<= 32;
|
||||
ret |= @intFromPtr(hash_slice.ptr);
|
||||
ret |= @intFromPtr(hex_slice.ptr);
|
||||
return ret;
|
||||
}
|
||||
|
||||
12
packages/widget/package-lock.json
generated
12
packages/widget/package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "@impost/widget",
|
||||
"version": "0.1.0",
|
||||
"license": "BSL-1.0",
|
||||
"dependencies": {
|
||||
"comlink": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.24",
|
||||
"lit": "^3.1.2",
|
||||
@@ -1433,7 +1436,6 @@
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -1655,6 +1657,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comlink": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz",
|
||||
"integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||
@@ -2148,7 +2155,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2422,7 +2428,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -2461,7 +2466,6 @@
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"lit-element": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@impost/lib": "workspace:*",
|
||||
"@types/node": "^20.11.24",
|
||||
"lit": "^3.1.2",
|
||||
"lit-element": "^3.1.2",
|
||||
@@ -30,6 +31,7 @@
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-dts": "^4.5.4"
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"comlink": "^4.4.2"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { LitElement, html, css, isServer, type PropertyValues } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { type ChallengeSolveRequest, type SolutionMessage, WorkerMessageType, type WorkerRequest, WorkerResponseType } from './types/worker';
|
||||
import { type Challenge, ChallengeStrategy } from '@impost/lib';
|
||||
import { type Challenge } from '@impost/lib';
|
||||
import { get_wasm_module } from '@impost/lib/solver';
|
||||
import * as Comlink from 'comlink';
|
||||
|
||||
import ChallengeWorker from './solver-worker?worker&inline';
|
||||
import SolverWorker from './solver-worker?worker&inline';
|
||||
|
||||
@customElement('pow-captcha')
|
||||
export class PowCaptcha extends LitElement {
|
||||
type SolverWorkerAPI = Comlink.Remote<typeof import("./solver-worker")>;
|
||||
|
||||
@customElement('impost-captcha')
|
||||
export class ImpostCaptcha extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@@ -63,13 +65,13 @@ export class PowCaptcha extends LitElement {
|
||||
.impost-footer div a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
/// ================================================
|
||||
/// Configuration
|
||||
/// ================================================
|
||||
|
||||
|
||||
// one of: "load", "focus", "submit", "off"
|
||||
@property({ type: String })
|
||||
auto: "onload" | "onfocus" | "onsubmit" | "off" = "off";
|
||||
|
||||
@@ -79,24 +81,28 @@ export class PowCaptcha extends LitElement {
|
||||
@property({ type: String })
|
||||
challengejson: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
showHashrate: boolean = false;
|
||||
|
||||
/// ================================================
|
||||
/// Internals
|
||||
/// ================================================
|
||||
|
||||
// needed to allow for multiple widgets on the same page if you wanted to
|
||||
// do that for some reason
|
||||
uid: string = Math.floor(Math.random() * 100000).toString();
|
||||
private uid: string = Math.floor(Math.random() * 100000).toString();
|
||||
|
||||
private _internals: ElementInternals | null = null;
|
||||
static formAssociated = true;
|
||||
|
||||
@state()
|
||||
private solution: string = '';
|
||||
|
||||
@state()
|
||||
private errorMessage: string = '';
|
||||
private solution: string | null = null;
|
||||
|
||||
@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;
|
||||
@@ -104,18 +110,23 @@ export class PowCaptcha extends LitElement {
|
||||
@state()
|
||||
private hashRate: number = 0;
|
||||
|
||||
|
||||
// stores the nonce and solution atomics
|
||||
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
||||
|
||||
private solverWorkers: Worker[] | null = null;
|
||||
private solverWorkers: SolverWorkerAPI[] | null = null;
|
||||
private nativeWorkers: Worker[] | null = null;
|
||||
|
||||
private solveStartTime: number | null = null;
|
||||
private hashRateInterval: number | null = null;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._internals = this.attachInternals();
|
||||
this.fetchChallenge();
|
||||
|
||||
console.log(this._internals.form);
|
||||
|
||||
this.initWorkers();
|
||||
|
||||
switch (this.auto) {
|
||||
@@ -127,7 +138,23 @@ export class PowCaptcha extends LitElement {
|
||||
break;
|
||||
case 'onsubmit':
|
||||
if (this.parentElement?.nodeName === 'FORM') {
|
||||
this.parentElement.addEventListener('submit', () => this.solveChallenge());
|
||||
this.parentElement.addEventListener('submit', async (ev) => {
|
||||
if (this.status === 'solved') {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
await this.solveChallenge();
|
||||
|
||||
const form = this.parentElement as HTMLFormElement;
|
||||
|
||||
if (form.requestSubmit) {
|
||||
form.requestSubmit();
|
||||
} else {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -146,9 +173,10 @@ export class PowCaptcha extends LitElement {
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
|
||||
for (const worker of this.solverWorkers || []) {
|
||||
for (const worker of this.nativeWorkers || []) {
|
||||
worker.terminate();
|
||||
this.solverWorkers = null;
|
||||
this.nativeWorkers = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +185,6 @@ export class PowCaptcha extends LitElement {
|
||||
}
|
||||
|
||||
async fetchChallenge() {
|
||||
this.errorMessage = '';
|
||||
if (this.challengeData !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -168,6 +195,7 @@ export class PowCaptcha extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// challenge data must be provided by the user when using SSR
|
||||
if (isServer) {
|
||||
return;
|
||||
}
|
||||
@@ -179,16 +207,21 @@ export class PowCaptcha extends LitElement {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching challenge:', error);
|
||||
this.errorMessage = 'Failed to fetch challenge. Please try again.';
|
||||
this.status = 'error';
|
||||
});
|
||||
}
|
||||
|
||||
async initWorkers() {
|
||||
this.solverWorkers = [];
|
||||
this.nativeWorkers = [];
|
||||
|
||||
const num_workers = navigator.hardwareConcurrency;
|
||||
for (let i = 0; i < num_workers; i++) {
|
||||
this.solverWorkers.push(new ChallengeWorker());
|
||||
const nativeWorker = new SolverWorker();
|
||||
const comlinkWorker = Comlink.wrap<SolverWorkerAPI>(nativeWorker);
|
||||
|
||||
this.nativeWorkers.push(nativeWorker);
|
||||
this.solverWorkers.push(comlinkWorker);
|
||||
}
|
||||
|
||||
const atomics_view = new Int32Array(this.sab);
|
||||
@@ -198,106 +231,53 @@ export class PowCaptcha extends LitElement {
|
||||
let wasm_module = await get_wasm_module();
|
||||
let worker_promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < this.solverWorkers.length; i++) {
|
||||
console.log('Worker', i);
|
||||
const worker = this.solverWorkers[i]!;
|
||||
|
||||
worker_promises.push(new Promise<void>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
console.log('Worker', i, 'got message', event.data);
|
||||
|
||||
if (event.data.type === WorkerResponseType.Error) {
|
||||
console.error("Worker error:", event.data.error);
|
||||
reject(event.data.error);
|
||||
}
|
||||
|
||||
if (event.data.type === WorkerResponseType.InitOk) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
reject(new Error("Unexpected message from worker"));
|
||||
};
|
||||
|
||||
const error_handler = (error: ErrorEvent) => {
|
||||
console.error("Worker error:", error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.addEventListener('message', message_handler);
|
||||
worker.addEventListener('error', error_handler);
|
||||
|
||||
worker.postMessage({
|
||||
type: WorkerMessageType.Init,
|
||||
module: wasm_module,
|
||||
sab: this.sab,
|
||||
} as WorkerRequest);
|
||||
}));
|
||||
const solver = this.solverWorkers[i]!;
|
||||
worker_promises.push(solver.init(wasm_module, this.sab)); // Direct call to exposed `init` method
|
||||
}
|
||||
|
||||
const timeoutMs = 10 * 1000;
|
||||
let timeout: number;
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
this.errorMessage = 'Failed to initialize workers in time. Please refresh the page.';
|
||||
reject(new Error(`Function timed out after ${timeoutMs}ms`));
|
||||
reject(new Error(`Function timeout after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
await Promise.race([
|
||||
Promise.allSettled(worker_promises).then(() => {
|
||||
Promise.allSettled(worker_promises).then(results => {
|
||||
const failedInits = results.filter(r => r.status === 'rejected');
|
||||
if (failedInits.length > 0) {
|
||||
console.error('Some workers failed to initialize:', failedInits);
|
||||
// we might want to collect all errors, and if every
|
||||
// worker fails, we can throw, but carry on if only some
|
||||
// fail. For now, we'll just throw if any fail.
|
||||
|
||||
throw new Error("One or more workers failed to initialize.");
|
||||
}
|
||||
|
||||
console.log('All workers initialized');
|
||||
return;
|
||||
}),
|
||||
timeoutPromise,
|
||||
]).then(() => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
}
|
||||
|
||||
async issueChallengeToWorker(worker: Worker, request: ChallengeSolveRequest): Promise<SolutionMessage> {
|
||||
return new Promise<SolutionMessage>((resolve, reject) => {
|
||||
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||
worker.removeEventListener('message', message_handler);
|
||||
worker.removeEventListener('error', error_handler);
|
||||
|
||||
resolve(event.data);
|
||||
};
|
||||
|
||||
const error_handler = (error: ErrorEvent) => {
|
||||
worker.removeEventListener('error', error_handler);
|
||||
worker.removeEventListener('message', message_handler);
|
||||
console.error("Worker error:", error);
|
||||
|
||||
reject(error);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}).catch(error => {
|
||||
clearTimeout(timeout);
|
||||
console.error("Worker initialization failed:", error);
|
||||
this.status = 'error';
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
async solveChallenge() {
|
||||
if (!this.challengeData || this.solverWorkers === null) {
|
||||
this.errorMessage = 'Captcha is not ready. Please wait or refresh.';
|
||||
// in all normal cases, this should be impossible
|
||||
this.status = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.solution !== '') {
|
||||
if (this.solution !== null || this.status !== 'unsolved') {
|
||||
// do not solve twice
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,45 +286,23 @@ export class PowCaptcha extends LitElement {
|
||||
const nonce = this.getCurrentWorkingNonce();
|
||||
|
||||
this.hashRate = (nonce / ((performance.now() - this.solveStartTime!) / 1000));
|
||||
console.log(this.hashRate);
|
||||
}, 250);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solve', {
|
||||
detail: {
|
||||
solution: this.solution,
|
||||
},
|
||||
detail: { challenge: this.challengeData, }, // empty solution
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}));
|
||||
|
||||
console.log(this.challengeData);
|
||||
|
||||
this.isSolving = true;
|
||||
this.errorMessage = '';
|
||||
this.solution = '';
|
||||
this.status = 'solving';
|
||||
this.solution = null;
|
||||
|
||||
const atomics_view = new Int32Array(this.sab);
|
||||
// reset atomics
|
||||
Atomics.store(atomics_view, 0, 0);
|
||||
Atomics.store(atomics_view, 1, -1);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
console.log('Sending challenge to workers...');
|
||||
// TODO: the first response is not always the solution, due to cmpxchg
|
||||
// blocking, some workers may block on the read, and as soon as they
|
||||
@@ -352,40 +310,42 @@ export class PowCaptcha extends LitElement {
|
||||
//
|
||||
// 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<SolutionMessage>[] = [];
|
||||
for (let worker of this.solverWorkers) {
|
||||
// dispatch to all workers, func is async so it will not block
|
||||
worker_promises.push(this.issueChallengeToWorker(worker, request));
|
||||
let worker_promises: Promise<string>[] = [];
|
||||
for (let solver of this.solverWorkers) {
|
||||
worker_promises.push(solver.solve_challenge(this.challengeData as Challenge));
|
||||
}
|
||||
|
||||
let solution = await Promise.race(worker_promises);
|
||||
try {
|
||||
await Promise.race(worker_promises);
|
||||
|
||||
if (solution.type === WorkerResponseType.Error) {
|
||||
this.errorMessage = solution.error;
|
||||
return;
|
||||
// The true solution is stored in the SharedArrayBuffer.
|
||||
this.solution = String(Atomics.load(atomics_view, 1));
|
||||
this.status = 'solved';
|
||||
} catch (error: any) {
|
||||
console.error("Captcha solving failed:", error);
|
||||
this.status = 'error';
|
||||
} finally {
|
||||
if (this.hashRateInterval !== null) {
|
||||
clearInterval(this.hashRateInterval);
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (solution.type !== WorkerResponseType.Solution) {
|
||||
this.errorMessage = "Something went wrong, please try again later.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.solution = Atomics.load(atomics_view, 1).toString();
|
||||
this.isSolving = false;
|
||||
this.solved = true;
|
||||
|
||||
if (this.hashRateInterval !== null) {
|
||||
clearInterval(this.hashRateInterval);
|
||||
this.hashRateInterval = null;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||
detail: {
|
||||
if (this.status === 'solved') {
|
||||
this._internals!.setFormValue(JSON.stringify({
|
||||
challenge: this.challengeData.salt,
|
||||
solution: this.solution,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}))
|
||||
}));
|
||||
|
||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||
detail: {
|
||||
challenge: this.challengeData,
|
||||
solution: this.solution,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
solvePreventDefault(event: Event) {
|
||||
@@ -400,12 +360,6 @@ export class PowCaptcha extends LitElement {
|
||||
this.challengejson = '';
|
||||
}
|
||||
|
||||
if (this.errorMessage) {
|
||||
return html`
|
||||
<div class="error-message">${this.errorMessage}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.challengeData === null) {
|
||||
return html`
|
||||
<div class="loading-message">Loading captcha challenge...</div>
|
||||
@@ -416,9 +370,9 @@ export class PowCaptcha extends LitElement {
|
||||
<div class="impost-widget">
|
||||
<div class="impost-main">
|
||||
<div class="impost-checkbox">
|
||||
${!this.isSolving ? html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.solved}>
|
||||
` : html`
|
||||
${this.status !== 'solving' ? html`${this.status === 'error' ? html`<svg class="impost-error-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 14q-.425 0-.712-.288T11 13V6q0-.425.288-.712T12 5t.713.288T13 6v7q0 .425-.288.713T12 14m0 5q-.425 0-.712-.288T11 18t.288-.712T12 17t.713.288T13 18t-.288.713T12 19"/></svg>` : html`
|
||||
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
|
||||
`}` : html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||
<g stroke="currentColor">
|
||||
@@ -431,11 +385,11 @@ export class PowCaptcha extends LitElement {
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
<label for="impost-checkbox-${this.uid}">${this.solved ? 'Verified' : html`${this.isSolving ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
<label for="impost-checkbox-${this.uid}">${this.status === 'error' ? 'Something went wrong' : this.status === 'solved' ? 'Verified' : html`${this.status === 'solving' ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||
</div>
|
||||
|
||||
<div class="impost-footer">
|
||||
${this.isSolving && this.hashRate > 0 ? html`
|
||||
${this.showHashrate && this.status === 'solving' && this.hashRate > 0 ? html`
|
||||
<div>
|
||||
<span>H/s:</span>
|
||||
<span>${this.hashRate.toFixed(2)}</span>
|
||||
@@ -449,8 +403,9 @@ export class PowCaptcha extends LitElement {
|
||||
|
||||
<input
|
||||
type = "text"
|
||||
id = "impost-solution"
|
||||
class="hidden"
|
||||
name = "impost-solution"
|
||||
id = "impost-solution-${this.uid}"
|
||||
style="display: none;"
|
||||
.value = ${this.solution}
|
||||
/>
|
||||
`;
|
||||
@@ -1 +1 @@
|
||||
export * from './pow-captcha';
|
||||
export * from './captcha';
|
||||
@@ -1,91 +1,64 @@
|
||||
// This worker just sits on another thread and waits for message to solve
|
||||
// challenges so that we dont block the render thread
|
||||
|
||||
import {
|
||||
type WorkerRequest,
|
||||
type SolutionMessage,
|
||||
WorkerMessageType,
|
||||
WorkerResponseType,
|
||||
} from "./types/worker";
|
||||
|
||||
import { ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
import { type SolverModule, init_solver, solve_leaading_zeroes_challenge, solve_target_number_challenge } from '@impost/lib/solver';
|
||||
import { type Challenge } from "@impost/lib";
|
||||
import { type SolverModule, init_solver, solve_challenge as libimpost_solve_challenge } from '@impost/lib/solver';
|
||||
import * as Comlink from 'comlink';
|
||||
|
||||
let solver: SolverModule | null = null;
|
||||
|
||||
let atomic_nonce: Int32Array | null = null;
|
||||
let atomic_solution: Int32Array | null = null;
|
||||
|
||||
onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
if (event.data.type === WorkerMessageType.Init) {
|
||||
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
|
||||
atomic_solution = new Int32Array(event.data.sab, 4, 1);
|
||||
let module = event.data.module;
|
||||
/**
|
||||
* Initializes the worker.
|
||||
* Must be called before any solve functions.
|
||||
* @param module The solver web assembly module.
|
||||
* @param sab The SharedArrayBuffer for nonce and solution.
|
||||
*/
|
||||
export async function init(module: WebAssembly.Module, sab: SharedArrayBuffer): Promise<void> {
|
||||
atomic_nonce = new Int32Array(sab, 0, 1);
|
||||
atomic_solution = new Int32Array(sab, 4, 1);
|
||||
|
||||
try {
|
||||
solver = await init_solver({
|
||||
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
}, module);
|
||||
} catch (error: any) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: `Could not load WASM solver: ${error.message}`,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "Failed to load WASM solver",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.InitOk,
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
try {
|
||||
solver = await init_solver({
|
||||
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||
__log: (str_ptr: number, str_len: number) => console.log(new TextDecoder().decode(new Uint8Array(solver!.exports.memory.buffer, str_ptr, str_len))),
|
||||
}, module);
|
||||
} catch (error: any) {
|
||||
console.error("Worker: Failed to initialize WASM solver", error);
|
||||
throw new Error(`Could not load WASM solver in worker: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!solver) {
|
||||
postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "WASM solver not loaded",
|
||||
} as SolutionMessage);
|
||||
return;
|
||||
throw new Error("Worker: Failed to load WASM solver.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves a given challenge.
|
||||
* @param challenge The challenge data.
|
||||
* @returns The nonce solution on success, or throws an error.
|
||||
*/
|
||||
export async function solve_challenge(challenge: Challenge): Promise<string> {
|
||||
if (!solver || !atomic_nonce || !atomic_solution) {
|
||||
throw new Error("WASM solver or atomics not initialized in worker. Call init() first.");
|
||||
}
|
||||
|
||||
if (atomic_nonce === null || atomic_solution === null) {
|
||||
throw new Error("Atomics not initialized");
|
||||
console.log("Worker: Solving challenge...");
|
||||
|
||||
const solutionNonce = libimpost_solve_challenge(solver, challenge);
|
||||
|
||||
if (solutionNonce < 0) {
|
||||
throw new Error(`Worker: Failed to solve challenge. Internal code: ${solutionNonce}`);
|
||||
}
|
||||
|
||||
const { strategy } = event.data;
|
||||
const finalSolution = Atomics.load(atomic_solution, 0);
|
||||
return finalSolution.toString();
|
||||
}
|
||||
|
||||
let solution: number;
|
||||
switch (strategy) {
|
||||
case ChallengeStrategy.LeadingZeroes:
|
||||
solution = solve_leaading_zeroes_challenge(solver, event.data)
|
||||
break;
|
||||
case ChallengeStrategy.TargetNumber:
|
||||
solution = solve_target_number_challenge(solver, event.data)
|
||||
break;
|
||||
}
|
||||
|
||||
// we are just assuming that if its less than -1, its the min i32
|
||||
if (solution < 0) {
|
||||
return postMessage({
|
||||
type: WorkerResponseType.Error,
|
||||
error: "failed to solve challenge",
|
||||
} as SolutionMessage);
|
||||
}
|
||||
|
||||
postMessage({
|
||||
type: WorkerResponseType.Solution,
|
||||
nonce: solution === -1 ? null : solution.toString()
|
||||
} as SolutionMessage);
|
||||
};
|
||||
// * Do not forget to expose functions we want to use on the main thread
|
||||
Comlink.expose({ solve_challenge, init });
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { ChallengeStrategy } from "@impost/lib";
|
||||
|
||||
export enum WorkerMessageType {
|
||||
Init = "init",
|
||||
Challenge = "challenge",
|
||||
}
|
||||
|
||||
interface WorkerInitRequest {
|
||||
type: WorkerMessageType.Init;
|
||||
module: WebAssembly.Module;
|
||||
|
||||
sab: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
interface ChallengeLeadingZeroesSolveRequest {
|
||||
strategy: ChallengeStrategy.LeadingZeroes;
|
||||
salt: string;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
interface ChallengeTargetNumberSolveRequest {
|
||||
strategy: ChallengeStrategy.TargetNumber;
|
||||
target: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest {
|
||||
type: WorkerMessageType.Challenge;
|
||||
}
|
||||
|
||||
export type ChallengeSolveRequest = ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest;
|
||||
type WorkerChallengeSolveRequest = WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest;
|
||||
|
||||
export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest;
|
||||
|
||||
export enum WorkerResponseType {
|
||||
Error = "error",
|
||||
InitOk = "init_ok",
|
||||
Solution = "solution",
|
||||
}
|
||||
|
||||
interface ErrorMessageResponse {
|
||||
type: WorkerResponseType.Error;
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface SolutionMessageResponse {
|
||||
type: WorkerResponseType.Solution;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
interface InitOkMessageResponse {
|
||||
type: WorkerResponseType.InitOk;
|
||||
}
|
||||
|
||||
export type SolutionMessage = ErrorMessageResponse | SolutionMessageResponse | InitOkMessageResponse;
|
||||
2
packages/widget/src/vite-env.d.ts
vendored
2
packages/widget/src/vite-env.d.ts
vendored
@@ -1 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
@@ -17,13 +17,10 @@ export default defineConfig({
|
||||
dir: 'dist',
|
||||
},
|
||||
},
|
||||
// WARN: this setting has caused issues for me in the past, but now it
|
||||
// seems to work fine. I'm not sure why, but minifying the bundle saves
|
||||
// 5KB, and it doesnt *cause* issues, so I'm leaving it on.
|
||||
minify: true,
|
||||
minify: true
|
||||
},
|
||||
|
||||
plugins: [
|
||||
dts(),
|
||||
dts()
|
||||
],
|
||||
});
|
||||
8648
pnpm-lock.yaml
generated
Normal file
8648
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
pnpm-workspace.yaml
Normal file
10
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
- packages/*
|
||||
- example-app/
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- core-js-pure
|
||||
- esbuild
|
||||
|
||||
shamefully-hoist: true
|
||||
@@ -1,7 +0,0 @@
|
||||
// A PoW algorithm based on google's kCTF scheme
|
||||
// https://google.github.io/kctf/
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const math = std.math;
|
||||
Reference in New Issue
Block a user