Implement kCTF strategy
This implementation is pretty scuffed, but its more exploratory than anything else.
This commit is contained in:
@@ -1,411 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type WorkerRequest, type SolutionMessage, WorkerResponseType, WorkerMessageType, ChallengeStrategy, type Challenge } from '~/types/pow';
|
const { data: challengeData } = await useFetch('/api/pow/challenge');
|
||||||
import WASMSolverUrl from "~/utils/solver.wasm?url";
|
|
||||||
import ChallengeWorker from '~/utils/worker?worker';
|
|
||||||
|
|
||||||
let shared_atomics = new SharedArrayBuffer(12);
|
if (!challengeData) {
|
||||||
|
throw createError({
|
||||||
let workers: Worker[] = [];
|
statusCode: 500,
|
||||||
let workers_initialized = false;
|
message: 'Failed to fetch challenge',
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function solveChallenge() {
|
function solved(ev: CustomEvent) {
|
||||||
if (!challenge.value?.target) {
|
console.log("Solved:", ev.detail.solution);
|
||||||
return;
|
// $fetch('/api/pow/challenge', {
|
||||||
|
// method: 'POST',
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// challenge: ev.detail.challenge,
|
||||||
|
// solution: ev.detail.solution,
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workers_initialized) {
|
function formsubmit(ev: Event) {
|
||||||
try {
|
console.log("Submitted form");
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setDifficulty(difficulty: number) {
|
|
||||||
const response = await $fetch('/api/pow/difficulty', {
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
|
||||||
difficulty,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between" v-if="hashrate_array.length !== 0">
|
<div class="flex justify-center items-center h-screen w-screen ">
|
||||||
<span>Your average Hashrate: {{ number_formatter.format(hashrate) }} H/s</span>
|
<form @submit.prevent="formsubmit"
|
||||||
<span>You have solved {{ total_solved }} {{ pluralize(total_solved, "challenge") }} in
|
class="p-5 rounded-2xl bg-dark-9 border-coolGray-600 border flex flex-col gap-2">
|
||||||
{{ number_formatter.format(total_solving_for / 1000) }}s</span>
|
<pow-captcha challengeUrl="/api/pow" auto="onsubmit"
|
||||||
<span>Your Hashrate on the last challenge: {{ number_formatter.format(hashrate_array.at(-1)!) }} H/s</span>
|
:challengejson="JSON.stringify(challengeData!.challenge)" @impost:solved="solved" />
|
||||||
</div>
|
<input class="bg-blue-7 text-white font-semibold px-4 py-2.5 border-0 rounded-md" type="submit"
|
||||||
<p v-else>You have not solved any challenges yet</p>
|
value="Submit" />
|
||||||
<p>Challenge: <span v-if="challenge_loading">{{ challenge_loading_indicator }}</span>
|
</form>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
button {
|
|
||||||
min-width: 140px;
|
|
||||||
padding: 0.25rem 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-p {
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "-";
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
411
example-app/app/pages/index.vue.old
Normal file
411
example-app/app/pages/index.vue.old
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
<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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setDifficulty(difficulty: number) {
|
||||||
|
const response = await $fetch('/api/pow/difficulty', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
difficulty,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
@@ -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,7 +1,10 @@
|
|||||||
strategy = "target_number"
|
strategy = "kctf"
|
||||||
|
|
||||||
[leading_zeroes]
|
[leading_zeroes]
|
||||||
difficulty = 4
|
difficulty = 4
|
||||||
|
|
||||||
[target_number]
|
[target_number]
|
||||||
max_number = 10000
|
max_number = 10000
|
||||||
|
|
||||||
|
[kctf]
|
||||||
|
difficulty = 100
|
||||||
|
|||||||
7
example-app/package-lock.json
generated
7
example-app/package-lock.json
generated
@@ -6,8 +6,8 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "hello-nuxt",
|
"name": "hello-nuxt",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@impost/lib": "file:../packages/lib",
|
"@impost/lib": "^0.1.0",
|
||||||
"@impost/widget": "file:../packages/widget",
|
"@impost/widget": "^0.1.0",
|
||||||
"@lit/reactive-element": "^2.1.1",
|
"@lit/reactive-element": "^2.1.1",
|
||||||
"js-toml": "^1.0.2",
|
"js-toml": "^1.0.2",
|
||||||
"lit-element": "^4.2.1",
|
"lit-element": "^4.2.1",
|
||||||
@@ -41,6 +41,9 @@
|
|||||||
"name": "@impost/lib",
|
"name": "@impost/lib",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "BSL-1.0",
|
"license": "BSL-1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"uuidv7": "^1.0.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"oxc-minify": "^0.97.0",
|
"oxc-minify": "^0.97.0",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
"nuxt": "latest",
|
"nuxt": "latest",
|
||||||
"nuxt-ssr-lit": "1.6.32",
|
"nuxt-ssr-lit": "1.6.32",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.12",
|
||||||
"@impost/lib": "file:../packages/lib",
|
"@impost/lib": "^0.1.0",
|
||||||
"@impost/widget": "file:../packages/widget"
|
"@impost/widget": "^0.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
|
|||||||
@@ -6,19 +6,28 @@ import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow
|
|||||||
|
|
||||||
export default defineEventHandler(async () => {
|
export default defineEventHandler(async () => {
|
||||||
let challenge_config;
|
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) {
|
switch (config.strategy) {
|
||||||
case ChallengeStrategy.LeadingZeroes:
|
case ChallengeStrategy.kCTF:
|
||||||
challenge_config = {
|
challenge_config = {
|
||||||
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
||||||
strategy: config.strategy,
|
strategy: config.strategy,
|
||||||
difficulty: config.leading_zeroes?.difficulty!,
|
difficulty: config.kctf.difficulty,
|
||||||
};
|
|
||||||
break;
|
|
||||||
case ChallengeStrategy.TargetNumber:
|
|
||||||
challenge_config = {
|
|
||||||
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
|
||||||
strategy: config.strategy,
|
|
||||||
max_number: config.target_number.max_number,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -31,10 +40,10 @@ export default defineEventHandler(async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
outstandingChallenges.set(challenge.salt, {
|
outstandingChallenges.set(challenge.challenge, {
|
||||||
challenge, timeout: setTimeout(() => {
|
challenge, timeout: setTimeout(() => {
|
||||||
console.log("Challenge timed out:", challenge.salt);
|
console.log("Challenge timed out:", challenge.challenge);
|
||||||
outstandingChallenges.delete(challenge.salt);
|
outstandingChallenges.delete(challenge.challenge);
|
||||||
}, CHALLENGE_TIMEOUT_MS)
|
}, CHALLENGE_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import * as z from 'zod';
|
|||||||
import { outstandingChallenges } from '~~/server/utils/pow';
|
import { outstandingChallenges } from '~~/server/utils/pow';
|
||||||
|
|
||||||
const challengeSchema = z.object({
|
const challengeSchema = z.object({
|
||||||
challenge: z.string(),
|
challenge: z.string().startsWith("s."),
|
||||||
nonce: z.string()
|
solution: z.string().startsWith("s.")
|
||||||
})
|
})
|
||||||
|
|
||||||
// post handler that takes in the challenge, and the nonce
|
// post handler that takes in the challenge, and the nonce
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
console.log(await readBody(event));
|
||||||
const body = await readValidatedBody(event, challengeSchema.safeParse);
|
const body = await readValidatedBody(event, challengeSchema.safeParse);
|
||||||
|
|
||||||
if (!body.success) {
|
if (!body.success) {
|
||||||
@@ -19,19 +20,25 @@ export default defineEventHandler(async (event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = body.data.challenge;
|
let { challenge, solution } = body.data;
|
||||||
let nonce = body.data.nonce;
|
|
||||||
|
const outstanding_challenge = outstandingChallenges.get(challenge);
|
||||||
|
if (outstanding_challenge === undefined) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Challenge not found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// check if the challenge is valid
|
// check if the challenge is valid
|
||||||
let challenge_valid = await validate_challenge(outstandingChallenges.get(target)!.challenge, {
|
const challenge_valid = await validate_challenge(outstanding_challenge.challenge, solution);
|
||||||
challenge: target,
|
|
||||||
nonce: nonce
|
console.log("CHALLENGE VALID", challenge_valid);
|
||||||
});
|
|
||||||
|
|
||||||
if (challenge_valid) {
|
if (challenge_valid) {
|
||||||
// clear the challenge
|
// clear the challenge
|
||||||
clearTimeout(outstandingChallenges.get(target)!.timeout);
|
clearTimeout(outstandingChallenges.get(challenge)!.timeout);
|
||||||
outstandingChallenges.delete(target);
|
outstandingChallenges.delete(challenge);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: 'Challenge solved'
|
message: 'Challenge solved'
|
||||||
|
|||||||
@@ -3,26 +3,31 @@ import { load } from 'js-toml';
|
|||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { ChallengeStrategy } from "@impost/lib";
|
import { ChallengeStrategy } from "@impost/lib";
|
||||||
|
|
||||||
const LeadingZeroesSchema = z.object({
|
// const LeadingZeroesSchema = z.object({
|
||||||
strategy: z.literal(ChallengeStrategy.LeadingZeroes),
|
// strategy: z.literal(ChallengeStrategy.LeadingZeroes),
|
||||||
leading_zeroes: z.object({
|
// leading_zeroes: z.object({
|
||||||
difficulty: z.number().int().min(1).max(64),
|
// 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<typeof Config>;
|
export type Config = z.infer<typeof Config>;
|
||||||
|
|
||||||
export const Config = z.discriminatedUnion('strategy', [
|
export const Config = z.discriminatedUnion('strategy', [
|
||||||
LeadingZeroesSchema,
|
kCTFSchema,
|
||||||
TargetNumberSchema,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export let config: Config;
|
export let config: Config;
|
||||||
|
|||||||
10
justfile
10
justfile
@@ -1,10 +1,14 @@
|
|||||||
build: build-widget
|
|
||||||
|
|
||||||
wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz"
|
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"
|
npm-runner := "npm"
|
||||||
|
|
||||||
|
[working-directory: "example-app"]
|
||||||
|
playground: build
|
||||||
|
{{npm-runner}} run dev
|
||||||
|
|
||||||
|
build: build-widget
|
||||||
|
|
||||||
[working-directory: "solver"]
|
[working-directory: "solver"]
|
||||||
build-wasm:
|
build-wasm:
|
||||||
zig build {{zig-build-args}}
|
zig build {{zig-build-args}}
|
||||||
|
|||||||
3
packages/lib/package-lock.json
generated
3
packages/lib/package-lock.json
generated
@@ -2058,7 +2058,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2332,7 +2331,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2383,7 +2381,6 @@
|
|||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|||||||
@@ -1,28 +1,44 @@
|
|||||||
|
import { UUID } from "uuidv7";
|
||||||
|
|
||||||
export enum ChallengeAlgorithm {
|
export enum ChallengeAlgorithm {
|
||||||
Argon2id = "argon2id",
|
Argon2id = "argon2id",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChallengeStrategy {
|
export enum ChallengeStrategy {
|
||||||
|
kCTF = "kctf",
|
||||||
LeadingZeroes = "leading_zeroes",
|
LeadingZeroes = "leading_zeroes",
|
||||||
TargetNumber = "target_number",
|
TargetNumber = "target_number",
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this case, the client will repeatedly hash a number with has until it
|
// In this case, the client will repeatedly hash a number with has until it
|
||||||
// finds a hash thaat starts with *difficulty* leading zeroes
|
// finds a hash thaat starts with *difficulty* leading zeroes
|
||||||
export interface ChallengeLeadingZeroes {
|
// export interface ChallengeLeadingZeroes {
|
||||||
algorithm: ChallengeAlgorithm;
|
// algorithm: ChallengeAlgorithm;
|
||||||
strategy: ChallengeStrategy.LeadingZeroes;
|
// strategy: ChallengeStrategy.LeadingZeroes;
|
||||||
salt: string; // random string
|
// 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;
|
difficulty: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this case, the server generates a random number, and the client will hash
|
export interface ChallengekCTF {
|
||||||
// the salt (a random string) + a random number until it finds a hash that is equal to challenge
|
strategy: ChallengeStrategy.kCTF;
|
||||||
export interface ChallengeTargetNumber {
|
challenge: string;
|
||||||
algorithm: ChallengeAlgorithm;
|
|
||||||
strategy: ChallengeStrategy.TargetNumber;
|
|
||||||
salt: string; // random string
|
|
||||||
target: string; // hash of salt + random number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Challenge = ChallengeLeadingZeroes | ChallengeTargetNumber;
|
export type InnerChallenge = InnerChallengekCTF;
|
||||||
|
|
||||||
|
export type Challenge = ChallengekCTF;
|
||||||
@@ -3,8 +3,10 @@ import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
|
|||||||
type WasmExports = Record<string, Function> & {
|
type WasmExports = Record<string, Function> & {
|
||||||
"malloc": (byte_count: number) => number | null;
|
"malloc": (byte_count: number) => number | null;
|
||||||
"free": (ptr: number | null, byte_count: number) => void;
|
"free": (ptr: number | null, byte_count: number) => void;
|
||||||
"solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: 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_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;
|
"memory": WebAssembly.Memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ export type SolverEnv = {
|
|||||||
__set_solution: (value: number) => void;
|
__set_solution: (value: number) => void;
|
||||||
__cmpxchg_solution: (expected: number, replacement: number) => number;
|
__cmpxchg_solution: (expected: number, replacement: number) => number;
|
||||||
__fetch_add_nonce: (value: 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> {
|
export async function get_wasm_module(): Promise<WebAssembly.Module> {
|
||||||
@@ -29,64 +32,35 @@ export async function init_solver(env: SolverEnv, module: WebAssembly.Module): P
|
|||||||
}) as unknown as SolverModule;
|
}) as unknown as SolverModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: { salt: string, difficulty: number }): number {
|
export function solve(solver: SolverModule, challenge: string): string {
|
||||||
const { salt, difficulty } = challenge;
|
console.log(challenge);
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
const challenge_buf = encoder.encode(challenge);
|
||||||
|
|
||||||
const salt_bytes = encoder.encode(salt);
|
const challenge_ptr = solver.exports.malloc(challenge_buf.length);
|
||||||
|
if (challenge_ptr === 0 || challenge_ptr === null) {
|
||||||
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
|
||||||
if (salt_ptr === 0 || salt_ptr === null) {
|
|
||||||
throw new Error("Failed to allocate memory for challenge string");
|
throw new Error("Failed to allocate memory for challenge string");
|
||||||
}
|
}
|
||||||
|
|
||||||
const memory = new Uint8Array(solver.exports.memory.buffer);
|
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(
|
const ret = solver.exports.solve(challenge_ptr, challenge_buf.length);
|
||||||
salt_ptr,
|
|
||||||
salt_bytes.length,
|
|
||||||
difficulty,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ret < 0) {
|
console.log("RET", ret);
|
||||||
|
|
||||||
|
if (ret <= 0) {
|
||||||
throw new Error("Failed to solve challenge");
|
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));
|
||||||
|
|
||||||
export function solve_target_number_challenge(solver: SolverModule, challenge: { salt: string, target: string }): number {
|
console.log("SOLUTION", solution);
|
||||||
const { salt, target } = challenge;
|
console.log("LENGTH", length);
|
||||||
const encoder = new TextEncoder();
|
|
||||||
|
solver.exports.free(ret, 2 + length);
|
||||||
const salt_bytes = encoder.encode(salt);
|
|
||||||
const target_bytes = encoder.encode(target);
|
return solution;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@@ -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 WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||||
|
import { uuidv7obj } from 'uuidv7';
|
||||||
|
|
||||||
type WasmExports = Record<string, Function> & {
|
type WasmExports = Record<string, Function> & {
|
||||||
"malloc": (byte_count: number) => number | null;
|
"malloc": (byte_count: number) => number | null;
|
||||||
"free": (ptr: number | null, byte_count: number) => void;
|
"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": (challenge_ptr: number, challenge_len: number, solution_ptr: number, solution_len: number) => boolean;
|
||||||
"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;
|
|
||||||
"memory": WebAssembly.Memory;
|
"memory": WebAssembly.Memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,159 +13,92 @@ export interface ValidatorModule extends WebAssembly.Instance {
|
|||||||
exports: WasmExports;
|
exports: WasmExports;
|
||||||
}
|
}
|
||||||
|
|
||||||
function array_to_base64(buffer: ArrayBuffer): string {
|
function array_to_base64(buffer: ArrayBufferLike): string {
|
||||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
for (var i = 0; i < bytes.byteLength; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise<Challenge> {
|
export interface kCTFChallengeConfig {
|
||||||
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<Challenge | 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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LeadingZeroesChallengeConfig {
|
|
||||||
parameters: Object;
|
parameters: Object;
|
||||||
strategy: ChallengeStrategy.LeadingZeroes;
|
strategy: ChallengeStrategy.kCTF;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TargetNumberChallengeConfig {
|
export type ChallengeConfig = kCTFChallengeConfig;
|
||||||
parameters: Object;
|
|
||||||
strategy: ChallengeStrategy.TargetNumber;
|
const VERSION = "s";
|
||||||
max_number: number;
|
|
||||||
|
async function encode_challenge(challenge: InnerChallenge, parameters: Object = {}): Promise<string> {
|
||||||
|
if (challenge.strategy !== ChallengeStrategy.kCTF) {
|
||||||
|
throw new Error("Unsupported challenge strategy");
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChallengeConfig = LeadingZeroesChallengeConfig | TargetNumberChallengeConfig;
|
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<Challenge | null> {
|
export async function generate_challenge(config: ChallengeConfig): Promise<Challenge | null> {
|
||||||
let challenge: Challenge | null = null;
|
if (config.difficulty < 1) {
|
||||||
switch (config.strategy) {
|
throw new Error("Difficulty must be at least 1");
|
||||||
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 (challenge === null) {
|
const challenge: InnerChallenge = {
|
||||||
return null;
|
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<boolean> {
|
export async function validate_challenge(challenge: Challenge, challenge_solution: string): Promise<boolean> {
|
||||||
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule
|
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 encoder = new TextEncoder();
|
||||||
|
const challenge_buf = encoder.encode(challenge.challenge);
|
||||||
|
const solution_buf = encoder.encode(challenge_solution);
|
||||||
|
|
||||||
let err;
|
const challenge_ptr = validator.exports.malloc(challenge_buf.length);
|
||||||
let memory;
|
const solution_ptr = validator.exports.malloc(solution_buf.length);
|
||||||
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);
|
|
||||||
|
|
||||||
target_ptr = validator.exports.malloc(challenge_solution.challenge.length);
|
if (challenge_ptr === 0 || challenge_ptr === null || solution_ptr === 0 || solution_ptr === null) {
|
||||||
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");
|
console.error("Failed to allocate memory for challenge string");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
memory = new Uint8Array(validator.exports.memory.buffer);
|
const memory = new Uint8Array(validator.exports.memory.buffer);
|
||||||
memory.set(target_bytes, target_ptr);
|
memory.set(challenge_buf, challenge_ptr);
|
||||||
memory.set(nonce_bytes, nonce_ptr);
|
memory.set(solution_buf, solution_ptr);
|
||||||
|
|
||||||
err = validator.exports.validate_leading_zeroes_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, challenge.difficulty);
|
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);
|
||||||
|
|
||||||
validator.exports.free(target_ptr, target_bytes.length);
|
return is_valid;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return err === 0;
|
|
||||||
}
|
}
|
||||||
4
packages/widget/package-lock.json
generated
4
packages/widget/package-lock.json
generated
@@ -1433,7 +1433,6 @@
|
|||||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -2148,7 +2147,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2422,7 +2420,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2461,7 +2458,6 @@
|
|||||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export class PowCaptcha extends LitElement {
|
|||||||
background-color: var(--impost-widget-background-color, #070408);
|
background-color: var(--impost-widget-background-color, #070408);
|
||||||
border-radius: var(--impost-widget-border-radius, 8px);
|
border-radius: var(--impost-widget-border-radius, 8px);
|
||||||
}
|
}
|
||||||
|
.impost-error-icon {
|
||||||
|
color: var(--impost-widget-error-icon-color, #FF8117);
|
||||||
|
}
|
||||||
.impost-main {
|
.impost-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -86,32 +89,20 @@ export class PowCaptcha extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
private solution: string = '';
|
private solution: string = '';
|
||||||
|
|
||||||
@state()
|
|
||||||
private errorMessage: string = '';
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private challengeData: Challenge | null = null;
|
private challengeData: Challenge | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private solved: boolean = false;
|
private status: 'unsolved' | 'solving' | 'solved' | 'error' = 'unsolved';
|
||||||
|
|
||||||
@state()
|
|
||||||
private isSolving: boolean = false;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private disabled: boolean = true;
|
private disabled: boolean = true;
|
||||||
|
|
||||||
@state()
|
|
||||||
private hashRate: number = 0;
|
|
||||||
|
|
||||||
// stores the nonce and solution atomics
|
// stores the nonce and solution atomics
|
||||||
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
||||||
|
|
||||||
private solverWorkers: Worker[] | null = null;
|
private solverWorkers: Worker[] | null = null;
|
||||||
|
|
||||||
private solveStartTime: number | null = null;
|
|
||||||
private hashRateInterval: number | null = null;
|
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.fetchChallenge();
|
this.fetchChallenge();
|
||||||
@@ -141,10 +132,6 @@ export class PowCaptcha extends LitElement {
|
|||||||
|
|
||||||
override disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
if (this.hashRateInterval !== null) {
|
|
||||||
clearInterval(this.hashRateInterval);
|
|
||||||
this.hashRateInterval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const worker of this.solverWorkers || []) {
|
for (const worker of this.solverWorkers || []) {
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
@@ -157,7 +144,6 @@ export class PowCaptcha extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchChallenge() {
|
async fetchChallenge() {
|
||||||
this.errorMessage = '';
|
|
||||||
if (this.challengeData !== null) {
|
if (this.challengeData !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,14 +165,14 @@ export class PowCaptcha extends LitElement {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error fetching challenge:', error);
|
console.error('Error fetching challenge:', error);
|
||||||
this.errorMessage = 'Failed to fetch challenge. Please try again.';
|
console.error('Failed to fetch challenge');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async initWorkers() {
|
async initWorkers() {
|
||||||
this.solverWorkers = [];
|
this.solverWorkers = [];
|
||||||
|
|
||||||
const num_workers = navigator.hardwareConcurrency;
|
const num_workers = 1;
|
||||||
for (let i = 0; i < num_workers; i++) {
|
for (let i = 0; i < num_workers; i++) {
|
||||||
this.solverWorkers.push(new ChallengeWorker());
|
this.solverWorkers.push(new ChallengeWorker());
|
||||||
}
|
}
|
||||||
@@ -237,7 +223,8 @@ export class PowCaptcha extends LitElement {
|
|||||||
let timeout: number;
|
let timeout: number;
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
timeout = setTimeout(() => {
|
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`));
|
reject(new Error(`Function timed out after ${timeoutMs}ms`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
});
|
});
|
||||||
@@ -272,19 +259,27 @@ export class PowCaptcha extends LitElement {
|
|||||||
worker.addEventListener('message', message_handler);
|
worker.addEventListener('message', message_handler);
|
||||||
worker.addEventListener('error', error_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) {
|
switch (request.strategy) {
|
||||||
case ChallengeStrategy.LeadingZeroes:
|
case ChallengeStrategy.kCTF:
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
strategy: ChallengeStrategy.LeadingZeroes,
|
strategy: ChallengeStrategy.kCTF,
|
||||||
salt: request.salt,
|
challenge: request.challenge,
|
||||||
difficulty: request.difficulty,
|
|
||||||
} as WorkerRequest);
|
|
||||||
break;
|
|
||||||
case ChallengeStrategy.TargetNumber:
|
|
||||||
worker.postMessage({
|
|
||||||
strategy: ChallengeStrategy.TargetNumber,
|
|
||||||
target: request.target,
|
|
||||||
salt: request.salt,
|
|
||||||
} as WorkerRequest);
|
} as WorkerRequest);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -293,7 +288,8 @@ export class PowCaptcha extends LitElement {
|
|||||||
|
|
||||||
async solveChallenge() {
|
async solveChallenge() {
|
||||||
if (!this.challengeData || this.solverWorkers === null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,25 +297,14 @@ export class PowCaptcha extends LitElement {
|
|||||||
return;
|
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', {
|
this.dispatchEvent(new CustomEvent('impost:solve', {
|
||||||
detail: {
|
|
||||||
solution: this.solution,
|
|
||||||
},
|
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
console.log(this.challengeData);
|
console.log(this.challengeData);
|
||||||
|
|
||||||
this.isSolving = true;
|
this.status = 'solving';
|
||||||
this.errorMessage = '';
|
|
||||||
this.solution = '';
|
this.solution = '';
|
||||||
|
|
||||||
const atomics_view = new Int32Array(this.sab);
|
const atomics_view = new Int32Array(this.sab);
|
||||||
@@ -328,19 +313,27 @@ export class PowCaptcha extends LitElement {
|
|||||||
|
|
||||||
let request: ChallengeSolveRequest;
|
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) {
|
switch (this.challengeData.strategy) {
|
||||||
case ChallengeStrategy.LeadingZeroes:
|
case ChallengeStrategy.kCTF:
|
||||||
request = {
|
request = {
|
||||||
strategy: ChallengeStrategy.LeadingZeroes,
|
strategy: ChallengeStrategy.kCTF,
|
||||||
salt: this.challengeData.salt,
|
challenge: this.challengeData.challenge,
|
||||||
difficulty: this.challengeData.difficulty,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case ChallengeStrategy.TargetNumber:
|
|
||||||
request = {
|
|
||||||
strategy: ChallengeStrategy.TargetNumber,
|
|
||||||
target: this.challengeData.target,
|
|
||||||
salt: this.challengeData.salt,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -350,7 +343,7 @@ export class PowCaptcha extends LitElement {
|
|||||||
// blocking, some workers may block on the read, and as soon as they
|
// blocking, some workers may block on the read, and as soon as they
|
||||||
// unblock, they return 0 since the challenge is already solved.
|
// 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.
|
// errors out, we only error out if all workers have errored out.
|
||||||
let worker_promises: Promise<SolutionMessage>[] = [];
|
let worker_promises: Promise<SolutionMessage>[] = [];
|
||||||
for (let worker of this.solverWorkers) {
|
for (let worker of this.solverWorkers) {
|
||||||
@@ -361,31 +354,44 @@ export class PowCaptcha extends LitElement {
|
|||||||
let solution = await Promise.race(worker_promises);
|
let solution = await Promise.race(worker_promises);
|
||||||
|
|
||||||
if (solution.type === WorkerResponseType.Error) {
|
if (solution.type === WorkerResponseType.Error) {
|
||||||
this.errorMessage = solution.error;
|
console.error("Worker error:", solution.error);
|
||||||
|
this.status = 'error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solution.type !== WorkerResponseType.Solution) {
|
if (solution.type !== WorkerResponseType.Solution) {
|
||||||
this.errorMessage = "Something went wrong, please try again later.";
|
console.error("Worker sent spurious message");
|
||||||
|
this.status = 'error';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.solution = Atomics.load(atomics_view, 1).toString();
|
// TODO: configure if we should fetch or not
|
||||||
this.isSolving = false;
|
try {
|
||||||
this.solved = true;
|
await fetch(`${this.challengeUrl}/challenge`, {
|
||||||
|
method: 'POST',
|
||||||
if (this.hashRateInterval !== null) {
|
body: JSON.stringify({
|
||||||
clearInterval(this.hashRateInterval);
|
challenge: this.challengeData.challenge,
|
||||||
this.hashRateInterval = null;
|
solution: solution.solution,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.status = 'solved';
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('impost:solved', {
|
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||||
detail: {
|
detail: {
|
||||||
solution: this.solution,
|
challenge: this.challengeData.challenge,
|
||||||
|
solution: solution.solution,
|
||||||
},
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
}))
|
}))
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to submit solution');
|
||||||
|
this.status = 'error';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
solvePreventDefault(event: Event) {
|
solvePreventDefault(event: Event) {
|
||||||
@@ -400,12 +406,6 @@ export class PowCaptcha extends LitElement {
|
|||||||
this.challengejson = '';
|
this.challengejson = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.errorMessage) {
|
|
||||||
return html`
|
|
||||||
<div class="error-message">${this.errorMessage}</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.challengeData === null) {
|
if (this.challengeData === null) {
|
||||||
return html`
|
return html`
|
||||||
<div class="loading-message">Loading captcha challenge...</div>
|
<div class="loading-message">Loading captcha challenge...</div>
|
||||||
@@ -416,9 +416,9 @@ export class PowCaptcha extends LitElement {
|
|||||||
<div class="impost-widget">
|
<div class="impost-widget">
|
||||||
<div class="impost-main">
|
<div class="impost-main">
|
||||||
<div class="impost-checkbox">
|
<div class="impost-checkbox">
|
||||||
${!this.isSolving ? 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.solved}>
|
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.status === 'solved'}>
|
||||||
` : html`
|
`}` : html`
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
<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 -->
|
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||||
<g stroke="currentColor">
|
<g stroke="currentColor">
|
||||||
@@ -431,16 +431,10 @@ export class PowCaptcha extends LitElement {
|
|||||||
</svg>
|
</svg>
|
||||||
`}
|
`}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="impost-footer">
|
<div class="impost-footer">
|
||||||
${this.isSolving && this.hashRate > 0 ? html`
|
|
||||||
<div>
|
|
||||||
<span>H/s:</span>
|
|
||||||
<span>${this.hashRate.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
` : ``}
|
|
||||||
<div id="provider-link">
|
<div id="provider-link">
|
||||||
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
|
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
WorkerResponseType,
|
WorkerResponseType,
|
||||||
} from "./types/worker";
|
} from "./types/worker";
|
||||||
|
|
||||||
import { ChallengeStrategy } from "@impost/lib";
|
import { type SolverModule, init_solver, solve } from '@impost/lib/solver';
|
||||||
|
|
||||||
import { type SolverModule, init_solver, solve_leaading_zeroes_challenge, solve_target_number_challenge } from '@impost/lib/solver';
|
|
||||||
|
|
||||||
let solver: SolverModule | null = null;
|
let solver: SolverModule | null = null;
|
||||||
|
|
||||||
@@ -29,6 +27,7 @@ onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
|||||||
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||||
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||||
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
__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);
|
}, module);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
postMessage({
|
postMessage({
|
||||||
@@ -60,32 +59,19 @@ onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (atomic_nonce === null || atomic_solution === null) {
|
let solution: string;
|
||||||
throw new Error("Atomics not initialized");
|
try {
|
||||||
}
|
solution = solve(solver, event.data.challenge);
|
||||||
|
} catch (error: any) {
|
||||||
const { strategy } = event.data;
|
postMessage({
|
||||||
|
|
||||||
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,
|
type: WorkerResponseType.Error,
|
||||||
error: "failed to solve challenge",
|
error: `Failed to solve challenge: ${error.message}`,
|
||||||
} as SolutionMessage);
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
postMessage({
|
postMessage({
|
||||||
type: WorkerResponseType.Solution,
|
type: WorkerResponseType.Solution,
|
||||||
nonce: solution === -1 ? null : solution.toString()
|
solution,
|
||||||
} as SolutionMessage);
|
} as SolutionMessage);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,28 +12,37 @@ interface WorkerInitRequest {
|
|||||||
sab: SharedArrayBuffer;
|
sab: SharedArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChallengeLeadingZeroesSolveRequest {
|
// interface ChallengeLeadingZeroesSolveRequest {
|
||||||
strategy: ChallengeStrategy.LeadingZeroes;
|
// strategy: ChallengeStrategy.LeadingZeroes;
|
||||||
salt: string;
|
// salt: string;
|
||||||
difficulty: number;
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
interface ChallengekCTFSolveRequest {
|
||||||
|
strategy: ChallengeStrategy.kCTF;
|
||||||
|
challenge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
interface WorkerChallengekCTFSolveRequest extends ChallengekCTFSolveRequest {
|
||||||
type: WorkerMessageType.Challenge;
|
type: WorkerMessageType.Challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChallengeTargetNumberSolveRequest {
|
export type ChallengeSolveRequest = ChallengekCTFSolveRequest;
|
||||||
strategy: ChallengeStrategy.TargetNumber;
|
type WorkerChallengeSolveRequest = WorkerChallengekCTFSolveRequest;
|
||||||
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 type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest;
|
||||||
|
|
||||||
@@ -50,7 +59,7 @@ interface ErrorMessageResponse {
|
|||||||
|
|
||||||
interface SolutionMessageResponse {
|
interface SolutionMessageResponse {
|
||||||
type: WorkerResponseType.Solution;
|
type: WorkerResponseType.Solution;
|
||||||
nonce: string;
|
solution: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InitOkMessageResponse {
|
interface InitOkMessageResponse {
|
||||||
|
|||||||
@@ -2,17 +2,13 @@ const std = @import("std");
|
|||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
const wasm_target = b.resolveTargetQuery(.{
|
const target = b.standardTargetOptions(.{});
|
||||||
.cpu_arch = .wasm32,
|
|
||||||
.os_tag = .freestanding,
|
|
||||||
.cpu_features_add = std.Target.wasm.featureSet(&.{ .bulk_memory, .bulk_memory_opt, .simd128, .tail_call }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// solver
|
// solver
|
||||||
const solver_mod = b.addModule("solver", .{
|
const solver_mod = b.addModule("solver", .{
|
||||||
.root_source_file = b.path("src/solver.zig"),
|
.root_source_file = b.path("src/solver.zig"),
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.target = wasm_target,
|
.target = target,
|
||||||
});
|
});
|
||||||
|
|
||||||
const solver_exe = b.addExecutable(.{
|
const solver_exe = b.addExecutable(.{
|
||||||
@@ -20,10 +16,12 @@ pub fn build(b: *std.Build) void {
|
|||||||
.root_module = solver_mod,
|
.root_module = solver_mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (target.result.cpu.arch == .wasm32) {
|
||||||
solver_exe.entry = .disabled;
|
solver_exe.entry = .disabled;
|
||||||
solver_exe.rdynamic = true;
|
solver_exe.rdynamic = true;
|
||||||
solver_exe.lto = .full;
|
|
||||||
solver_exe.link_gc_sections = true;
|
solver_exe.link_gc_sections = true;
|
||||||
|
solver_exe.lto = .full;
|
||||||
|
}
|
||||||
|
|
||||||
b.installArtifact(solver_exe);
|
b.installArtifact(solver_exe);
|
||||||
|
|
||||||
@@ -31,16 +29,18 @@ pub fn build(b: *std.Build) void {
|
|||||||
const validator_mod = b.addModule("validator", .{
|
const validator_mod = b.addModule("validator", .{
|
||||||
.root_source_file = b.path("src/validator.zig"),
|
.root_source_file = b.path("src/validator.zig"),
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.target = wasm_target,
|
.target = target,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validator_exe = b.addLibrary(.{
|
const validator_exe = b.addExecutable(.{
|
||||||
.name = "validator",
|
.name = "validator",
|
||||||
.root_module = validator_mod,
|
.root_module = validator_mod,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (target.result.cpu.arch == .wasm32) {
|
||||||
validator_exe.entry = .disabled;
|
validator_exe.entry = .disabled;
|
||||||
validator_exe.rdynamic = true;
|
validator_exe.rdynamic = true;
|
||||||
|
}
|
||||||
|
|
||||||
b.installArtifact(validator_exe);
|
b.installArtifact(validator_exe);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,211 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const math = std.math;
|
const math = std.math;
|
||||||
|
const Int = math.big.int.Managed;
|
||||||
|
|
||||||
|
var managed_one: ?Int = null;
|
||||||
|
|
||||||
|
fn get_bit(n: *Int, idx: usize) !bool {
|
||||||
|
if (n.len() < idx / @typeInfo(usize).int.bits) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var foo = try n.clone();
|
||||||
|
defer foo.deinit();
|
||||||
|
|
||||||
|
try foo.shiftRight(n, idx);
|
||||||
|
try foo.bitAnd(&foo, &managed_one.?);
|
||||||
|
return foo.eql(managed_one.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn square_mod(n: *Int) !void {
|
||||||
|
const allocator = n.allocator;
|
||||||
|
try n.sqr(n);
|
||||||
|
|
||||||
|
var high = try Int.init(allocator);
|
||||||
|
defer high.deinit();
|
||||||
|
try high.shiftRight(n, 1279); // high = n >> 1279
|
||||||
|
|
||||||
|
var mask = try Int.init(allocator);
|
||||||
|
defer mask.deinit();
|
||||||
|
|
||||||
|
if (managed_one == null) {
|
||||||
|
managed_one = try Int.init(allocator);
|
||||||
|
try managed_one.?.set(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try mask.set(1);
|
||||||
|
try mask.shiftLeft(&mask, 1279);
|
||||||
|
try mask.sub(&mask, &managed_one.?);
|
||||||
|
|
||||||
|
try n.bitAnd(n, &mask);
|
||||||
|
|
||||||
|
try n.add(n, &high);
|
||||||
|
|
||||||
|
if (try get_bit(n, 1279)) {
|
||||||
|
// clear bit 1279
|
||||||
|
var power_of_2 = try Int.init(allocator);
|
||||||
|
defer power_of_2.deinit();
|
||||||
|
try power_of_2.set(1);
|
||||||
|
try power_of_2.shiftLeft(&power_of_2, 1279);
|
||||||
|
try n.sub(n, &power_of_2);
|
||||||
|
|
||||||
|
// *n += 1;
|
||||||
|
try n.add(n, &managed_one.?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Version = "s";
|
||||||
|
|
||||||
|
pub const Challenge = struct {
|
||||||
|
difficulty: ?u32,
|
||||||
|
salt: std.math.big.int.Managed,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn destroy(self: *Self, allocator: Allocator) void {
|
||||||
|
self.salt.deinit();
|
||||||
|
allocator.destroy(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(self: *Self, allocator: Allocator) ![]u8 {
|
||||||
|
const solution_base64_len = std.base64.standard.Encoder.calcSize(self.salt.len() * @sizeOf(usize));
|
||||||
|
const dest = try allocator.alloc(u8, solution_base64_len);
|
||||||
|
defer allocator.free(dest);
|
||||||
|
@memset(dest, 0);
|
||||||
|
|
||||||
|
const limbs_u8_buffer: []u8 = std.mem.sliceAsBytes(self.salt.limbs[0..self.salt.len()]);
|
||||||
|
const base64_str = std.base64.standard.Encoder.encode(dest, limbs_u8_buffer);
|
||||||
|
|
||||||
|
return try std.fmt.allocPrint(allocator, "{s}", .{base64_str});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn decode(allocator: Allocator, challenge: []const u8) !*Challenge {
|
||||||
|
var parts = std.mem.splitAny(u8, challenge, ".");
|
||||||
|
if (parts.next()) |part| {
|
||||||
|
if (!std.mem.eql(u8, part, Version)) {
|
||||||
|
return error.InvalidChallenge;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error.InvalidChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
var difficulty: ?u32 = null;
|
||||||
|
var next_part = parts.next() orelse return error.InvalidChallenge;
|
||||||
|
if (parts.peek()) |_| {
|
||||||
|
// must be <version>.<difficulty>.<salt>
|
||||||
|
const difficulty_bytes = try allocator.alloc(u8, try std.base64.standard.Decoder.calcSizeForSlice(next_part));
|
||||||
|
defer allocator.free(difficulty_bytes);
|
||||||
|
|
||||||
|
try std.base64.standard.Decoder.decode(difficulty_bytes, next_part);
|
||||||
|
|
||||||
|
std.log.info("Decoded difficulty bytes: {any}\n", .{difficulty_bytes});
|
||||||
|
|
||||||
|
var difficulty_array: [4]u8 = .{0} ** 4;
|
||||||
|
if (difficulty_bytes.len > 4) {
|
||||||
|
const split_idx = difficulty_bytes.len - 4;
|
||||||
|
for (difficulty_bytes[0..split_idx]) |byte| {
|
||||||
|
if (byte != 0) return error.DifficultyTooLarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@memcpy(&difficulty_array, difficulty_bytes[split_idx..]);
|
||||||
|
difficulty = std.mem.readInt(u32, &difficulty_array, .big);
|
||||||
|
} else {
|
||||||
|
const start_idx = 4 - difficulty_bytes.len;
|
||||||
|
@memcpy(&difficulty_array, difficulty_bytes[start_idx..]);
|
||||||
|
difficulty = std.mem.readInt(u32, &difficulty_array, .big);
|
||||||
|
}
|
||||||
|
|
||||||
|
next_part = parts.next() orelse return error.InvalidChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
var salt = try std.math.big.int.Managed.init(allocator);
|
||||||
|
errdefer salt.deinit();
|
||||||
|
|
||||||
|
const salt_str = next_part;
|
||||||
|
const salt_bytes_len = try std.base64.standard.Decoder.calcSizeForSlice(salt_str);
|
||||||
|
|
||||||
|
std.log.info("salt_bytes_len: {d}\n", .{salt_bytes_len});
|
||||||
|
|
||||||
|
const salt_bytes = try allocator.alloc(u8, salt_bytes_len);
|
||||||
|
defer allocator.free(salt_bytes);
|
||||||
|
|
||||||
|
try std.base64.standard.Decoder.decode(salt_bytes, salt_str);
|
||||||
|
|
||||||
|
std.log.info("decoded salt: {any}\n", .{salt_bytes});
|
||||||
|
|
||||||
|
const usize_salt_bytes: []align(1) usize = std.mem.bytesAsSlice(usize, salt_bytes);
|
||||||
|
// TODO: the bytes are being read in as little endian, but need to be read in as big endian
|
||||||
|
std.log.info("usize_salt_bytes: {any}\n", .{usize_salt_bytes});
|
||||||
|
try salt.ensureCapacity(usize_salt_bytes.len);
|
||||||
|
@memcpy(salt.limbs[0..usize_salt_bytes.len], usize_salt_bytes);
|
||||||
|
salt.setLen(usize_salt_bytes.len);
|
||||||
|
|
||||||
|
const challenge_ptr = try allocator.create(Challenge);
|
||||||
|
errdefer challenge_ptr.destroy(allocator);
|
||||||
|
|
||||||
|
challenge_ptr.* = Challenge{
|
||||||
|
.difficulty = difficulty,
|
||||||
|
.salt = salt,
|
||||||
|
};
|
||||||
|
|
||||||
|
return challenge_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn solve(allocator: Allocator, challenge: *Challenge) ![]u8 {
|
||||||
|
if (challenge.difficulty == null) {
|
||||||
|
return error.InvalidChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..challenge.difficulty.?) |_| {
|
||||||
|
std.log.info("Solving challenge with difficulty {d}\n", .{challenge.difficulty.?});
|
||||||
|
for (0..1277) |_| {
|
||||||
|
try square_mod(&challenge.salt);
|
||||||
|
}
|
||||||
|
try challenge.salt.bitXor(&challenge.salt, &managed_one.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
std.log.info("solved challenge: {any}\n", .{challenge});
|
||||||
|
|
||||||
|
return try challenge.encode(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(allocator: Allocator, challenge: *Challenge, solution: *Challenge) !bool {
|
||||||
|
std.log.info("{d}", .{challenge.difficulty.?});
|
||||||
|
std.log.info("{any} vs {any}\n", .{ challenge, solution });
|
||||||
|
|
||||||
|
if (challenge.difficulty == null) {
|
||||||
|
return error.InvalidChallenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managed_one == null) {
|
||||||
|
managed_one = try Int.init(allocator);
|
||||||
|
try managed_one.?.set(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0..challenge.difficulty.?) |_| {
|
||||||
|
try solution.salt.bitXor(&solution.salt, &managed_one.?);
|
||||||
|
|
||||||
|
try square_mod(&solution.salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
std.log.info("{any} vs {any}\n", .{ challenge, solution });
|
||||||
|
|
||||||
|
if (challenge.salt.eql(solution.salt)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var foo = try std.math.big.int.Managed.initSet(allocator, 2);
|
||||||
|
defer foo.deinit();
|
||||||
|
try foo.pow(&foo, 1279);
|
||||||
|
try foo.sub(&foo, &managed_one.?);
|
||||||
|
try foo.sub(&foo, &challenge.salt);
|
||||||
|
|
||||||
|
if (foo.eql(solution.salt)) {
|
||||||
|
std.log.info("challenge solved!\n", .{});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const hasher = @import("hasher.zig");
|
const kCTF = @import("kctf.zig");
|
||||||
|
|
||||||
// var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
// var allocator = gpa.allocator();
|
var allocator = gpa.allocator();
|
||||||
|
|
||||||
var allocator = std.heap.wasm_allocator;
|
extern fn __log(str_ptr: usize, str_len: usize) void;
|
||||||
|
|
||||||
extern fn __get_solution() i32;
|
fn log(comptime level: std.log.Level, comptime scope: @TypeOf(.EnumLiteral), comptime fmt: []const u8, args: anytype) void {
|
||||||
extern fn __set_solution(value: i32) void;
|
if (comptime builtin.target.cpu.arch != .wasm32) {
|
||||||
extern fn __cmpxchg_solution(old: i32, new: i32) i32;
|
std.log.defaultLog(level, scope, fmt, args);
|
||||||
extern fn __fetch_add_nonce(value: i32) i32;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// fn log(comptime fmt: []const u8, args: anytype) void {
|
const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
|
||||||
// const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
|
__log(@intFromPtr(formatted.ptr), formatted.len);
|
||||||
// __log(@intFromPtr(formatted.ptr), formatted.len);
|
allocator.free(formatted);
|
||||||
// allocator.free(formatted);
|
}
|
||||||
// }
|
|
||||||
|
pub const std_options: std.Options = .{ .logFn = log };
|
||||||
|
|
||||||
export fn malloc(byte_count: usize) ?*u8 {
|
export fn malloc(byte_count: usize) ?*u8 {
|
||||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||||
@@ -31,129 +34,55 @@ export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SolveError = enum(u32) {
|
// value_ptr is a string to the entire encoded challenge string (e.g. "s.xxxxxxxxx.xxxxxxx")
|
||||||
InvalidDifficulty = 1,
|
export fn solve(value_ptr: [*]u8, value_len: usize) usize {
|
||||||
InvalidNonce = 2,
|
const challenge_slice = value_ptr[0..value_len];
|
||||||
NoSolution = 3,
|
|
||||||
OutOfMemory = 4,
|
std.log.info("Solve called with challenge {s}\n", .{challenge_slice});
|
||||||
|
|
||||||
|
const challenge = kCTF.decode(allocator, challenge_slice) catch |err| {
|
||||||
|
std.log.info("Error decoding challenge: {s}\n", .{@errorName(err)});
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
defer challenge.destroy(allocator);
|
||||||
|
|
||||||
|
std.log.info("decoded challenge {any}\n", .{challenge});
|
||||||
|
const solution = kCTF.solve(allocator, challenge) catch |err| {
|
||||||
|
std.log.info("Error solving challenge: {s}\n", .{@errorName(err)});
|
||||||
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
var solve_error: ?SolveError = null;
|
std.log.info("Solution: {s}\n", .{solution});
|
||||||
export fn get_solve_error() u32 {
|
|
||||||
if (solve_error) |err| {
|
const output_ptr = allocator.alloc(u8, solution.len + 4) catch return 0;
|
||||||
return @intFromEnum(err);
|
|
||||||
}
|
var output_slice = output_ptr[0 .. solution.len + 4];
|
||||||
|
if (output_slice.len - 2 > std.math.maxInt(u16)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
const output_len: u16 = @intCast(output_slice.len - 2);
|
||||||
|
// convert to little endian
|
||||||
|
output_slice[0] = @intCast(output_len & 0xFF); // LSB
|
||||||
|
output_slice[1] = @intCast(output_len >> 8); // MSB
|
||||||
|
|
||||||
// returns nonce on success, -1 on failure
|
@memcpy(output_slice[2..4], "s.");
|
||||||
// to get the error, call get_solve_error
|
@memcpy(output_slice[4 .. 4 + solution.len], solution);
|
||||||
export fn solve_leaading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, difficulty: u32) i32 {
|
allocator.free(solution);
|
||||||
solve_error = null;
|
|
||||||
|
|
||||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
return @intFromPtr(output_ptr.ptr);
|
||||||
|
|
||||||
if (difficulty < 1 or difficulty > 64) {
|
|
||||||
solve_error = SolveError.InvalidDifficulty;
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var target_prefix_buffer: [64]u8 = @splat('0');
|
pub fn main() anyerror!void {
|
||||||
const target_prefix = target_prefix_buffer[0..difficulty];
|
if (comptime builtin.cpu.arch == .wasm32) return;
|
||||||
|
|
||||||
const max_nonce_iterations: u64 = 1_000_000_000;
|
var args = try std.process.argsAlloc(allocator);
|
||||||
|
if (args.len < 2) {
|
||||||
// 64 + 9 digits for nonce since the max nonce is 999_999_999 (not 1 billion since nonce < max_nonce_iterations)
|
std.log.err("Usage: zig run src/kctf.zig <challenge>", .{});
|
||||||
var input_buffer: []u8 = allocator.alloc(u8, challenge_len + 9) catch {
|
return;
|
||||||
// log("Failed to allocate memory for challenge\n", .{});
|
|
||||||
solve_error = SolveError.OutOfMemory;
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
// dont leak memory :pepega:
|
|
||||||
defer allocator.free(input_buffer);
|
|
||||||
|
|
||||||
@memcpy(input_buffer[0..challenge_len], challenge_slice);
|
|
||||||
|
|
||||||
var nonce = __fetch_add_nonce(1);
|
|
||||||
|
|
||||||
while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) {
|
|
||||||
if (__get_solution() != -1) {
|
|
||||||
// solution has already been found, no point in continuing
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonce_str = std.fmt.bufPrint(input_buffer[challenge_len..], "{d}", .{nonce}) catch {
|
const challenge = try kCTF.decode(allocator, args[1]);
|
||||||
solve_error = SolveError.InvalidNonce;
|
const solution = try kCTF.solve(allocator, challenge);
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hash_hex_slice = hasher.hash(allocator, input_buffer[0..challenge_len], input_buffer[challenge_len .. challenge_len + nonce_str.len]) catch {
|
std.log.info("Solution: {s}", .{solution});
|
||||||
solve_error = SolveError.OutOfMemory;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
// TODO: take in max number
|
|
||||||
const max_nonce_iterations: usize = 1_000_000_000;
|
|
||||||
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);
|
|
||||||
|
|
||||||
@memcpy(input_buffer[0..salt_len], salt_slice);
|
|
||||||
|
|
||||||
var nonce = __fetch_add_nonce(1);
|
|
||||||
while (nonce < max_nonce_iterations) : (nonce = __fetch_add_nonce(1)) {
|
|
||||||
if (__get_solution() != -1) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (std.mem.eql(u8, target_slice, hash_hex_slice)) {
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const hasher = @import("hasher.zig");
|
const kCTF = @import("kctf.zig");
|
||||||
|
|
||||||
var allocator = std.heap.wasm_allocator;
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
|
var allocator = gpa.allocator();
|
||||||
|
|
||||||
|
extern fn __log(str_ptr: usize, str_len: usize) void;
|
||||||
|
|
||||||
|
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 formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
|
||||||
|
__log(@intFromPtr(formatted.ptr), formatted.len);
|
||||||
|
allocator.free(formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const std_options: std.Options = .{ .logFn = log };
|
||||||
|
|
||||||
export fn malloc(byte_count: usize) ?*u8 {
|
export fn malloc(byte_count: usize) ?*u8 {
|
||||||
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||||
@@ -26,56 +43,51 @@ fn bytesToHex(bytes: []const u8, buf: []u8) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn validate_leading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, difficulty: u32) i32 {
|
// challenge_ptr should look like s.<difficulty>.<challenge>
|
||||||
const challenge_slice = challenge_ptr[0..challenge_len];
|
// solution_ptr should look like s.<solved_hash>
|
||||||
const nonce_slice = nonce_ptr[0..nonce_len];
|
export fn validate(challenge_ptr: [*]u8, challenge_len: usize, solution_ptr: [*]u8, solution_len: usize) bool {
|
||||||
|
const challenge_buf = challenge_ptr[0..challenge_len];
|
||||||
|
const solution_buf = solution_ptr[0..solution_len];
|
||||||
|
|
||||||
if (difficulty < 1 or difficulty > 64) {
|
std.log.info("Validate called with challenge {s} and solution {s}\n", .{ challenge_buf, solution_buf });
|
||||||
return -1;
|
|
||||||
|
const challenge = kCTF.decode(allocator, challenge_buf) catch return false;
|
||||||
|
std.log.info("decoded challenge {any}\n", .{challenge});
|
||||||
|
const solution = kCTF.decode(allocator, solution_buf) catch return false;
|
||||||
|
defer {
|
||||||
|
challenge.destroy(allocator);
|
||||||
|
solution.destroy(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
var target_prefix_buffer: [64]u8 = @splat('0');
|
std.log.info("decoded challenge and solution\n", .{});
|
||||||
const target_prefix = target_prefix_buffer[0..difficulty];
|
|
||||||
|
|
||||||
const hash_hex_slice = hasher.hash(allocator, challenge_slice, nonce_slice) catch return -2;
|
const is_valid = kCTF.check(allocator, challenge, solution) catch return false;
|
||||||
|
|
||||||
if (!std.mem.startsWith(u8, hash_hex_slice, target_prefix)) {
|
return is_valid;
|
||||||
return -3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
pub fn main() anyerror!void {
|
||||||
|
if (comptime builtin.cpu.arch == .wasm32) return;
|
||||||
|
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
if (args.len < 3) {
|
||||||
|
std.log.err("Usage: zig run src/validator.zig <challenge> <solution>", .{});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn validate_target_number_challenge(target_ptr: [*]u8, target_len: usize, nonce_ptr: [*]u8, nonce_len: usize, salt_ptr: [*]u8, salt_len: usize) i32 {
|
const challenge = try kCTF.decode(allocator, args[1]);
|
||||||
const target_slice = target_ptr[0..target_len];
|
defer challenge.destroy(allocator);
|
||||||
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 solution = try kCTF.decode(allocator, args[2]);
|
||||||
|
defer solution.destroy(allocator);
|
||||||
|
|
||||||
if (!std.mem.eql(u8, target_slice, hash_hex_slice)) {
|
std.log.info("Challenge: {any}\n", .{challenge});
|
||||||
return -3;
|
std.log.info("Solution: {any}\n", .{solution});
|
||||||
|
|
||||||
|
const is_valid = kCTF.check(allocator, challenge, solution) catch |err| {
|
||||||
|
std.log.info("Error checking challenge: {s}\n", .{@errorName(err)});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
std.log.info("Is valid: {}\n", .{is_valid});
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
ret <<= 32;
|
|
||||||
ret |= @intFromPtr(hash_slice.ptr);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn main() void {
|
|
||||||
// const challenge = "4d7220e22a1ea588fea60000ab8874194e4c6ffd71077adbae915826c73dbf48";
|
|
||||||
// const nonce = "4302";
|
|
||||||
// const difficulty = 3;
|
|
||||||
|
|
||||||
// std.log.info("{d}", .{validate_challenge(@constCast(challenge[0..].ptr), challenge.len, @constCast(nonce[0..].ptr), nonce.len, difficulty)});
|
|
||||||
// }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user