Once again a weird place to commit, I have already done a lot of work, but I am just bad at using git, okay.
411 lines
13 KiB
Vue
411 lines
13 KiB
Vue
<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> |