Initial commit
Once again a weird place to commit, I have already done a lot of work, but I am just bad at using git, okay.
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"minimap.background": "#00000000",
|
||||||
|
"scrollbar.shadow": "#00000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
.vscode/tasks.json
vendored
Normal file
10
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "Build",
|
||||||
|
"command": "just build",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
23
LICENSE
Normal file
23
LICENSE
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Boost Software License - Version 1.0 - August 17th, 2003
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
do so, all subject to the following:
|
||||||
|
|
||||||
|
The copyright notices in the Software and this entire statement, including
|
||||||
|
the above license grant, this restriction and the following disclaimer,
|
||||||
|
must be included in all copies of the Software, in whole or in part, and
|
||||||
|
all derivative works of the Software, unless such copies or derivative
|
||||||
|
works are solely in the form of machine-executable object code generated by
|
||||||
|
a source language processor.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
7
NOTES.md
Normal file
7
NOTES.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Notes
|
||||||
|
|
||||||
|
Ideas:
|
||||||
|
|
||||||
|
- plugable solvers: users can implement their own solvers and use them with the
|
||||||
|
widget. This would allow easily changing the PoW algorithm, or even using
|
||||||
|
different PoW algorithms for different challenges.
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Impost
|
||||||
|
|
||||||
|
Impost /ˈimˌpōst/ _noun_ a tax or compulsory payment
|
||||||
|
|
||||||
|
Impost is a PoW anti-spam solution, or for short, a PoW captcha. Instead of
|
||||||
|
spying on your users and using heavy captchas, Impost uses PoW to impose a cost
|
||||||
|
on sending requests. To a single user, this is a negligable few seconds, but at
|
||||||
|
scale, it can be a significant deterrent to spam.
|
||||||
|
|
||||||
|
This is the impost monorepo, containing the following packages:
|
||||||
|
|
||||||
|
- `@impost/widget`: A web component that can be used to embed an Impost widget
|
||||||
|
in your website.
|
||||||
|
- `@impost/lib`: A library that can be used to generate, solve, and verify
|
||||||
|
proofs.
|
||||||
|
|
||||||
|
It also contains a `solver` package, which is the PoW solver written in Zig,
|
||||||
|
`@impost/lib` is built on top of, an example of how to use the solver in a
|
||||||
|
nuxt 3 project.
|
||||||
1
example-app/.example.env
Normal file
1
example-app/.example.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
YAPTCHA_HMAC_SECRET=xxx # openssl rand -base64 32
|
||||||
5
example-app/.gitignore
vendored
Normal file
5
example-app/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.output/
|
||||||
|
.nuxt/
|
||||||
|
.env
|
||||||
|
*.wasm
|
||||||
6
example-app/.vscode/settings.json
vendored
Normal file
6
example-app/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"minimap.background": "#00000000",
|
||||||
|
"scrollbar.shadow": "#00000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
50
example-app/README.md
Normal file
50
example-app/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# YAPTCHA
|
||||||
|
|
||||||
|
Yet Another Pow capTCHA.
|
||||||
|
|
||||||
|
## What is this
|
||||||
|
|
||||||
|
YAPTCHA is a proof of work based challenge-response system that is designed to
|
||||||
|
ward off spam and abuse.
|
||||||
|
|
||||||
|
<!-- TODO: -->
|
||||||
|
|
||||||
|
## Basic configuration notes
|
||||||
|
|
||||||
|
Leading zeroes takes in a difficulty from 1 to 64, this indicates the number of
|
||||||
|
leading hexidecimal zeroes that are required for a problem to be solved. The
|
||||||
|
probability for a digit to be a zero is 1/16^$D$ where $D$ is the difficulty.
|
||||||
|
|
||||||
|
The following chart is provided for a baseline of the theoretical expected
|
||||||
|
average iterations it would take to solve challenge of difficulty $D$.
|
||||||
|
|
||||||
|
| $D$ | expected average iterations |
|
||||||
|
| --- | --------------------------- |
|
||||||
|
| 1 | 16 |
|
||||||
|
| 2 | 256 |
|
||||||
|
| 3 | 4,096 |
|
||||||
|
| 4 | 65,536 |
|
||||||
|
| 5 | 1,048,576 |
|
||||||
|
| 6 | 16,777,216 |
|
||||||
|
| 7 | 268,435,456 |
|
||||||
|
| 8 | 4,294,967,296 |
|
||||||
|
|
||||||
|
Target Number takes in a max number of iterations, $M$, to be solved. The
|
||||||
|
probability that a solution is solved will be 1/targetNumber. This provides a
|
||||||
|
more preciese way of determining the difficulty of a challenge. With leading
|
||||||
|
zeroes, there is only a theoretical chance that a solution is found within a
|
||||||
|
certain number of iterations. With target number, there is a gurantee that a
|
||||||
|
solution will be found in, at most, $M$ iterations.
|
||||||
|
|
||||||
|
## Interesting links and blogs I used while writing this
|
||||||
|
|
||||||
|
- [Anubis](https://github.com/TecharoHQ/anubis) for inspiration. (once again, I
|
||||||
|
have some strong opinions on Anubis, but thats is still neithehrere nor there)
|
||||||
|
- ["Proof of Mutex: Outspeeding Anubis with Valid PoW" by yumechi](https://yumechi.jp/en/blog/2025/proof-of-mutex-outspeeding-anubis-with-valid-pow/)
|
||||||
|
- https://www.arkoselabs.com/blog/proof-of-work-invisible-security-visible-results/
|
||||||
|
for giving me hope that PoW is not a fruitless endeavor for captchas.
|
||||||
|
- [ALTCHA](https://github.com/altcha-org/altcha) purely to see how another
|
||||||
|
option in the space works. (I have some choice words about the implementation
|
||||||
|
of ALTCHA, but I digress. **No code from ALTCHA was used in this project.**)
|
||||||
|
- [The monero project](https://www.getmonero.org/) for its list of algorithms
|
||||||
|
they use in xmrig, and some inspiration.
|
||||||
17
example-app/app/app.vue
Normal file
17
example-app/app/app.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: #090508;
|
||||||
|
color-scheme: dark;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
411
example-app/app/pages/index.vue
Normal file
411
example-app/app/pages/index.vue
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>
|
||||||
38
example-app/app/pages/widget.vue
Normal file
38
example-app/app/pages/widget.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<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>
|
||||||
3
example-app/app/plugins/pow-captcha.ts
Normal file
3
example-app/app/plugins/pow-captcha.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import '@impost/widget';
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => { });
|
||||||
6
example-app/app/utils/worker-name.ts
Normal file
6
example-app/app/utils/worker-name.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
const adjectives = ['swift', 'silent', 'hidden', 'clever', 'brave', 'sharp', 'shadow', 'crimson', 'bright', 'quiet', 'loud', 'happy', 'dark', 'evil', 'good', 'intelligent', 'lovely', 'mysterious', 'peaceful', 'powerful', 'pure', 'quiet', 'shiny', 'sleepy', 'strong', 'sweet', 'tall', 'warm', 'gentle', 'kind', 'nice', 'polite', 'rough', 'rude', 'scary', 'shy', 'silly', 'smart', 'strange', 'tough', 'ugly', 'vivid', 'wicked', 'wise', 'young', 'sleepy'];
|
||||||
|
const nouns = ['fox', 'river', 'stone', 'cipher', 'link', 'comet', 'falcon', 'signal', 'anchor', 'spark', 'stone', 'comet', 'rocket', 'snake', 'snail', 'shark', 'elephant', 'cat', 'dog', 'whale', 'orca', 'cactus', 'flower', 'frog', 'toad', 'apple', 'strawberry', 'raspberry', 'lemon', 'bot', 'gopher', 'dinosaur', 'racoon', 'penguin', 'chameleon', 'atom', 'particle', 'witch', 'wizard', 'warlock', 'deer']
|
||||||
|
|
||||||
|
export function getWorkerName() {
|
||||||
|
return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${nouns[Math.floor(Math.random() * nouns.length)]}`;
|
||||||
|
}
|
||||||
170
example-app/app/utils/worker.ts
Normal file
170
example-app/app/utils/worker.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// This worker just sits on another thread and waits for message to solve
|
||||||
|
// challenges so that we dont block the render thread
|
||||||
|
|
||||||
|
import {
|
||||||
|
type WorkerRequest,
|
||||||
|
type SolutionMessage,
|
||||||
|
WorkerMessageType,
|
||||||
|
WorkerResponseType,
|
||||||
|
ChallengeStrategy,
|
||||||
|
} from "~/types/pow";
|
||||||
|
|
||||||
|
const worker_name = getWorkerName();
|
||||||
|
let solver: SolverModule | null = null;
|
||||||
|
|
||||||
|
let atomic_nonce: Int32Array | null = null;
|
||||||
|
let atomic_solution: Int32Array | null = null;
|
||||||
|
|
||||||
|
async function loadWasmSolver(module: WebAssembly.Module) {
|
||||||
|
if (atomic_nonce === null || atomic_solution === null) {
|
||||||
|
throw createError("Atomics not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`[${worker_name}]: Loading WASM solver`);
|
||||||
|
|
||||||
|
solver = await WebAssembly.instantiate(module, {
|
||||||
|
env: {
|
||||||
|
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||||
|
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||||
|
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||||
|
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||||
|
__log: (ptr: number, len: number) => {
|
||||||
|
const string_data = new Uint8Array(solver!.exports.memory.buffer, ptr, len);
|
||||||
|
console.log(`[${worker_name}]: ${new TextDecoder().decode(string_data)}`);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}) as unknown as SolverModule;
|
||||||
|
console.debug(`[${worker_name}]: WASM solver loaded`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||||
|
if (event.data.type === WorkerMessageType.Init) {
|
||||||
|
console.log(`[${worker_name}]: Initializing...`);
|
||||||
|
|
||||||
|
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
|
||||||
|
atomic_solution = new Int32Array(event.data.sab, 4, 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadWasmSolver(event.data.module);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[${worker_name}]: Failed to load WASM solver:`, error);
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: `Could not load WASM solver: ${error.message}`,
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!solver) {
|
||||||
|
console.error(`[${worker_name}]: Failed to load WASM solver`);
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "Failed to load WASM solver",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Ok,
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!solver) {
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "WASM solver not loaded",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { strategy } = event.data;
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
let solution: number;
|
||||||
|
let target: string = event.data.target;
|
||||||
|
let target_bytes, target_ptr;
|
||||||
|
let memory;
|
||||||
|
switch (strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
const { difficulty } = event.data;
|
||||||
|
console.debug(`[${worker_name}]: recieved ${strategy} challenge: ${target}, difficulty: ${difficulty}`);
|
||||||
|
|
||||||
|
target_bytes = encoder.encode(target);
|
||||||
|
|
||||||
|
target_ptr = solver.exports.malloc(target_bytes.length);
|
||||||
|
if (target_ptr === 0 || target_ptr === null) {
|
||||||
|
console.error(`[${worker_name}]: Failed to allocate memory for challenge string`);
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "Failed to allocate memory for challenge string",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memory = new Uint8Array(solver.exports.memory.buffer);
|
||||||
|
memory.set(target_bytes, target_ptr);
|
||||||
|
|
||||||
|
solution = solver.exports.solve_leaading_zeroes_challenge(
|
||||||
|
target_ptr,
|
||||||
|
target.length,
|
||||||
|
difficulty,
|
||||||
|
);
|
||||||
|
console.debug(`[${worker_name}]: WASM solver found nonce: ${solution}`);
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
const { salt } = event.data;
|
||||||
|
console.debug(`[${worker_name}]: recieved ${strategy} challenge: ${target}, salt: ${salt}`);
|
||||||
|
|
||||||
|
const salt_bytes = encoder.encode(salt);
|
||||||
|
target_bytes = encoder.encode(target);
|
||||||
|
|
||||||
|
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
||||||
|
if (salt_ptr === 0 || salt_ptr === null) {
|
||||||
|
console.error(`[${worker_name}]: Failed to allocate memory for salt string`);
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "Failed to allocate memory for salt string",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target_ptr = solver.exports.malloc(target_bytes.length);
|
||||||
|
if (target_ptr === 0 || target_ptr === null) {
|
||||||
|
console.error(`[${worker_name}]: Failed to allocate memory for target string`);
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "Failed to allocate memory for target string",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
memory = new Uint8Array(solver.exports.memory.buffer);
|
||||||
|
memory.set(salt_bytes, salt_ptr);
|
||||||
|
memory.set(target_bytes, target_ptr);
|
||||||
|
|
||||||
|
solution = solver.exports.solve_target_number_challenge(
|
||||||
|
target_ptr,
|
||||||
|
target_bytes.length,
|
||||||
|
salt_ptr,
|
||||||
|
salt_bytes.length,
|
||||||
|
);
|
||||||
|
console.debug(`[${worker_name}]: WASM solver found nonce: ${solution}`);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are just assuming that if its less than -1, its the min i32
|
||||||
|
if (solution < 0) {
|
||||||
|
return postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "failed to solve challenge",
|
||||||
|
} as SolutionMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Solution,
|
||||||
|
nonce: solution === -1 ? null : solution.toString()
|
||||||
|
} as SolutionMessage);
|
||||||
|
};
|
||||||
2283
example-app/bun.lock
Normal file
2283
example-app/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
7
example-app/config.toml
Normal file
7
example-app/config.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
strategy = "target_number"
|
||||||
|
|
||||||
|
[leading_zeroes]
|
||||||
|
difficulty = 4
|
||||||
|
|
||||||
|
[target_number]
|
||||||
|
max_number = 10000
|
||||||
57
example-app/nuxt.config.ts
Normal file
57
example-app/nuxt.config.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { defineNuxtConfig } from "nuxt/config";
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
ssr: true,
|
||||||
|
devServer: {
|
||||||
|
port: 3000,
|
||||||
|
host: '0.0.0.0'
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
headers: {
|
||||||
|
'strict-transport-security': 'max-age=63072000; includeSubDomains; preload',
|
||||||
|
'x-frame-options': 'DENY',
|
||||||
|
'x-xss-protection': '1; mode=block',
|
||||||
|
'cross-origin-opener-policy': 'same-origin',
|
||||||
|
'cross-origin-embedder-policy': 'require-corp',
|
||||||
|
'access-control-allow-origin': '*',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modules: ['@unocss/nuxt', ['nuxt-ssr-lit', { litElementPrefix: 'pow-' }]],
|
||||||
|
|
||||||
|
nitro: {
|
||||||
|
moduleSideEffects: ["@impost/widget"],
|
||||||
|
experimental: {
|
||||||
|
wasm: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
imports: {
|
||||||
|
transform: {
|
||||||
|
// only necessary in the monorepo since vite is going out to the
|
||||||
|
// source of the widget and transforming it and clobbering it.
|
||||||
|
exclude: [/impost/],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
vue: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: (tag) => tag.includes('pow-'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// typescript: {
|
||||||
|
// tsConfig: {
|
||||||
|
// compilerOptions: {
|
||||||
|
// paths: {
|
||||||
|
// "~/*": ["./app/*"],
|
||||||
|
// "~~/*": ["./*"],
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
compatibilityDate: '2025-11-05',
|
||||||
|
});
|
||||||
8722
example-app/package-lock.json
generated
Normal file
8722
example-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
example-app/package.json
Normal file
27
example-app/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "hello-nuxt",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@lit/reactive-element": "^2.1.1",
|
||||||
|
"js-toml": "^1.0.2",
|
||||||
|
"lit-element": "^4.2.1",
|
||||||
|
"lit-html": "^3.3.1",
|
||||||
|
"nuxt": "latest",
|
||||||
|
"nuxt-ssr-lit": "1.6.32",
|
||||||
|
"zod": "^4.1.12",
|
||||||
|
"@impost/lib": "file:../packages/lib",
|
||||||
|
"@impost/widget": "file:../packages/widget"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
|
"@unocss/nuxt": "^66.5.4",
|
||||||
|
"unocss": "^66.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
example-app/server/api/pow/challenge.get.ts
Normal file
44
example-app/server/api/pow/challenge.get.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
import { config } from '~~/server/utils/config';
|
||||||
|
import { generate_challenge } from '@impost/lib/validator';
|
||||||
|
import { ChallengeStrategy } from '@impost/lib';
|
||||||
|
import { CHALLENGE_TIMEOUT_MS, outstandingChallenges } from '~~/server/utils/pow';
|
||||||
|
|
||||||
|
export default defineEventHandler(async () => {
|
||||||
|
let challenge_config;
|
||||||
|
switch (config.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
challenge_config = {
|
||||||
|
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
||||||
|
strategy: config.strategy,
|
||||||
|
difficulty: config.leading_zeroes?.difficulty!,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
challenge_config = {
|
||||||
|
parameters: { expires_at: CHALLENGE_TIMEOUT_MS },
|
||||||
|
strategy: config.strategy,
|
||||||
|
max_number: config.target_number.max_number,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let challenge = await generate_challenge(challenge_config);
|
||||||
|
if (challenge === null) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
statusMessage: 'Failed to generate challenge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
outstandingChallenges.set(challenge.salt, {
|
||||||
|
challenge, timeout: setTimeout(() => {
|
||||||
|
console.log("Challenge timed out:", challenge.salt);
|
||||||
|
outstandingChallenges.delete(challenge.salt);
|
||||||
|
}, CHALLENGE_TIMEOUT_MS)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
challenge,
|
||||||
|
}
|
||||||
|
})
|
||||||
45
example-app/server/api/pow/challenge.post.ts
Normal file
45
example-app/server/api/pow/challenge.post.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
import { validate_challenge } from '@impost/lib/validator';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { outstandingChallenges } from '~~/server/utils/pow';
|
||||||
|
|
||||||
|
const challengeSchema = z.object({
|
||||||
|
challenge: z.string(),
|
||||||
|
nonce: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
// post handler that takes in the challenge, and the nonce
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readValidatedBody(event, challengeSchema.safeParse);
|
||||||
|
|
||||||
|
if (!body.success) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Validation failed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = body.data.challenge;
|
||||||
|
let nonce = body.data.nonce;
|
||||||
|
|
||||||
|
// check if the challenge is valid
|
||||||
|
let challenge_valid = await validate_challenge(outstandingChallenges.get(target)!.challenge, {
|
||||||
|
challenge: target,
|
||||||
|
nonce: nonce
|
||||||
|
});
|
||||||
|
|
||||||
|
if (challenge_valid) {
|
||||||
|
// clear the challenge
|
||||||
|
clearTimeout(outstandingChallenges.get(target)!.timeout);
|
||||||
|
outstandingChallenges.delete(target);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Challenge solved'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Challenge is not valid'
|
||||||
|
})
|
||||||
|
})
|
||||||
19
example-app/server/api/pow/difficulty.get.ts
Normal file
19
example-app/server/api/pow/difficulty.get.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
import { ChallengeStrategy } from '@impost/lib';
|
||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
let difficulty: number;
|
||||||
|
|
||||||
|
switch (config.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
difficulty = config.leading_zeroes.difficulty!;
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
difficulty = config.target_number.max_number!;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
difficulty
|
||||||
|
}
|
||||||
|
})
|
||||||
35
example-app/server/api/pow/difficulty.put.ts
Normal file
35
example-app/server/api/pow/difficulty.put.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
import { ChallengeStrategy } from '@impost/lib';
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
let difficulty = body.difficulty;
|
||||||
|
|
||||||
|
switch (config.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
if (!difficulty || difficulty < 1 || difficulty > 64) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config.leading_zeroes.difficulty = difficulty;
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
if (!difficulty || difficulty < 1 || difficulty > 100_000_000) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: 'Invalid request',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config.target_number.max_number = difficulty;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Challenge difficulty set'
|
||||||
|
};
|
||||||
|
});
|
||||||
10
example-app/server/middleware/secure-context.ts
Normal file
10
example-app/server/middleware/secure-context.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineEventHandler, setHeader } from 'h3'; // H3 is the underlying HTTP framework in Nuxt 3
|
||||||
|
|
||||||
|
export default defineEventHandler((event) => {
|
||||||
|
setHeader(event, 'Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
||||||
|
setHeader(event, 'X-Frame-Options', 'DENY');
|
||||||
|
setHeader(event, 'X-XSS-Protection', '1; mode=block');
|
||||||
|
setHeader(event, 'Cross-Origin-Opener-Policy', 'same-origin');
|
||||||
|
setHeader(event, 'Cross-Origin-Embedder-Policy', 'require-corp');
|
||||||
|
setHeader(event, 'access-control-allow-origin', '*');
|
||||||
|
});
|
||||||
42
example-app/server/utils/config.ts
Normal file
42
example-app/server/utils/config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { load } from 'js-toml';
|
||||||
|
import z from 'zod';
|
||||||
|
import { ChallengeStrategy } from "@impost/lib";
|
||||||
|
|
||||||
|
const LeadingZeroesSchema = z.object({
|
||||||
|
strategy: z.literal(ChallengeStrategy.LeadingZeroes),
|
||||||
|
leading_zeroes: z.object({
|
||||||
|
difficulty: z.number().int().min(1).max(64),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const 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 const Config = z.discriminatedUnion('strategy', [
|
||||||
|
LeadingZeroesSchema,
|
||||||
|
TargetNumberSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export let config: Config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = Config.parse(load(readFileSync('./config.toml', 'utf-8')));
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
console.error("Failed to parse config:");
|
||||||
|
for (const issue of error.issues) {
|
||||||
|
console.error(issue.message, issue.path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Failed to parse config:", error);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
6
example-app/server/utils/pow.ts
Normal file
6
example-app/server/utils/pow.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Challenge } from "@impost/lib";
|
||||||
|
|
||||||
|
export const outstandingChallenges = new Map<string, { challenge: Challenge, timeout: NodeJS.Timeout }>();
|
||||||
|
|
||||||
|
// 1 hour
|
||||||
|
export const CHALLENGE_TIMEOUT_MS = 3600000;
|
||||||
17
example-app/tsconfig.json
Normal file
17
example-app/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.server.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.shared.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./.nuxt/tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
example-app/uno.config.ts
Normal file
11
example-app/uno.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig, presetMini, presetIcons, transformerDirectives } from 'unocss'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetMini(),
|
||||||
|
presetIcons(),
|
||||||
|
],
|
||||||
|
transformers: [
|
||||||
|
transformerDirectives(),
|
||||||
|
]
|
||||||
|
})
|
||||||
26
justfile
Normal file
26
justfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
build: build-widget
|
||||||
|
|
||||||
|
wasm-opt-args := "--strip-debug --strip-dwarf --enable-tail-call --enable-bulk-memory -Oz"
|
||||||
|
zig-build-args := "--release=fast"
|
||||||
|
|
||||||
|
npm-runner := "npm"
|
||||||
|
|
||||||
|
[working-directory: "solver"]
|
||||||
|
build-wasm:
|
||||||
|
zig build {{zig-build-args}}
|
||||||
|
wasm-opt {{wasm-opt-args}} --enable-simd -o zig-out/bin/solver.wasm zig-out/bin/solver.wasm
|
||||||
|
# The server does not support simd, so we disable it here
|
||||||
|
wasm-opt {{wasm-opt-args}} --disable-simd -o zig-out/bin/validator.wasm zig-out/bin/validator.wasm
|
||||||
|
|
||||||
|
[working-directory: "packages/lib"]
|
||||||
|
build-lib: build-wasm
|
||||||
|
{{npm-runner}} install
|
||||||
|
{{npm-runner}} link
|
||||||
|
{{npm-runner}} run build
|
||||||
|
|
||||||
|
[working-directory: "packages/widget"]
|
||||||
|
build-widget: build-lib
|
||||||
|
{{npm-runner}} install
|
||||||
|
{{npm-runner}} link @impost/lib
|
||||||
|
{{npm-runner}} link
|
||||||
|
{{npm-runner}} run build
|
||||||
2
packages/lib/.gitignore
vendored
Normal file
2
packages/lib/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
60
packages/lib/README.md
Normal file
60
packages/lib/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# @impost/lib
|
||||||
|
|
||||||
|
This package contains the types and WASM code for the impost. It is useful for
|
||||||
|
building other packages that want to use the WASM code, a client, or a server.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
validate_challenge,
|
||||||
|
generate_target_number_challenge,
|
||||||
|
} from "@impost/lib/validator";
|
||||||
|
import {
|
||||||
|
init_solver,
|
||||||
|
get_wasm_module,
|
||||||
|
solve_target_number_challenge,
|
||||||
|
} from "@impost/lib/solver";
|
||||||
|
import { type Challenge, ChallengeStrategy } from "@impost/lib";
|
||||||
|
|
||||||
|
const challenge = await generate_target_number_challenge(
|
||||||
|
// timeout in ms
|
||||||
|
{
|
||||||
|
// impost automatically recognizes this parameter, its also recommended to
|
||||||
|
// add a timestamp to prevent replay or pre-computation attacks
|
||||||
|
expires_at: Date.now() + 3600,
|
||||||
|
}
|
||||||
|
// max number
|
||||||
|
1_000
|
||||||
|
);
|
||||||
|
|
||||||
|
if (challenge === null) {
|
||||||
|
throw new Error("Failed to generate challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
let solver = await init_solver(
|
||||||
|
{
|
||||||
|
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||||
|
__set_solution: (value: number) =>
|
||||||
|
Atomics.store(atomic_solution!, 0, value),
|
||||||
|
__cmpxchg_solution: (expected: number, replacement: number) =>
|
||||||
|
Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||||
|
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||||
|
},
|
||||||
|
await get_wasm_module()
|
||||||
|
);
|
||||||
|
|
||||||
|
const solution = await solve_target_number_challenge(solver, {
|
||||||
|
salt: challenge.salt,
|
||||||
|
target: challenge.target,
|
||||||
|
});
|
||||||
|
|
||||||
|
const is_valid = await validate_challenge(challenge, {
|
||||||
|
challenge: challenge.target,
|
||||||
|
nonce: solution,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is_valid) {
|
||||||
|
console.log("Challenge solved!", solution);
|
||||||
|
}
|
||||||
|
```
|
||||||
2498
packages/lib/package-lock.json
generated
Normal file
2498
packages/lib/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
packages/lib/package.json
Normal file
39
packages/lib/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@impost/lib",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"license": "BSL-1.0",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./solver": {
|
||||||
|
"types": "./dist/solver.d.ts",
|
||||||
|
"import": "./dist/solver.js"
|
||||||
|
},
|
||||||
|
"./validator": {
|
||||||
|
"types": "./dist/validator.d.ts",
|
||||||
|
"import": "./dist/validator.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"watch": "tsc && vite build --watch",
|
||||||
|
"dev": "vite",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"oxc-minify": "^0.97.0",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "npm:rolldown-vite@latest",
|
||||||
|
"vite-plugin-dts": "^4.5.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuidv7": "^1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/lib/src/index.ts
Normal file
28
packages/lib/src/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export enum ChallengeAlgorithm {
|
||||||
|
Argon2id = "argon2id",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChallengeStrategy {
|
||||||
|
LeadingZeroes = "leading_zeroes",
|
||||||
|
TargetNumber = "target_number",
|
||||||
|
}
|
||||||
|
|
||||||
|
// In this case, the client will repeatedly hash a number with has until it
|
||||||
|
// finds a hash thaat starts with *difficulty* leading zeroes
|
||||||
|
export interface ChallengeLeadingZeroes {
|
||||||
|
algorithm: ChallengeAlgorithm;
|
||||||
|
strategy: ChallengeStrategy.LeadingZeroes;
|
||||||
|
salt: string; // random string
|
||||||
|
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 type Challenge = ChallengeLeadingZeroes | ChallengeTargetNumber;
|
||||||
92
packages/lib/src/solver.ts
Normal file
92
packages/lib/src/solver.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import WASMSolverUrl from '../../../solver/zig-out/bin/solver.wasm?url&inline';
|
||||||
|
|
||||||
|
type WasmExports = Record<string, Function> & {
|
||||||
|
"malloc": (byte_count: number) => number | null;
|
||||||
|
"free": (ptr: number | null, byte_count: number) => void;
|
||||||
|
"solve_leaading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, difficulty: number) => number;
|
||||||
|
"solve_target_number_challenge": (challenge_ptr: number, challenge_len: number, target_ptr: number, target_len: number) => number;
|
||||||
|
"memory": WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SolverModule extends WebAssembly.Instance {
|
||||||
|
exports: WasmExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SolverEnv = {
|
||||||
|
__get_solution: () => number;
|
||||||
|
__set_solution: (value: number) => void;
|
||||||
|
__cmpxchg_solution: (expected: number, replacement: number) => number;
|
||||||
|
__fetch_add_nonce: (value: number) => number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function get_wasm_module(): Promise<WebAssembly.Module> {
|
||||||
|
return WebAssembly.compileStreaming(fetch(WASMSolverUrl));;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function init_solver(env: SolverEnv, module: WebAssembly.Module): Promise<SolverModule> {
|
||||||
|
return await WebAssembly.instantiate(module, {
|
||||||
|
env,
|
||||||
|
}) as unknown as SolverModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function solve_leaading_zeroes_challenge(solver: SolverModule, challenge: { salt: string, difficulty: number }): number {
|
||||||
|
const { salt, difficulty } = challenge;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const salt_bytes = encoder.encode(salt);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
const memory = new Uint8Array(solver.exports.memory.buffer);
|
||||||
|
memory.set(salt_bytes, salt_ptr);
|
||||||
|
|
||||||
|
const ret = solver.exports.solve_leaading_zeroes_challenge(
|
||||||
|
salt_ptr,
|
||||||
|
salt_bytes.length,
|
||||||
|
difficulty,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
throw new Error("Failed to solve challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function solve_target_number_challenge(solver: SolverModule, challenge: { salt: string, target: string }): number {
|
||||||
|
const { salt, target } = challenge;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
const salt_bytes = encoder.encode(salt);
|
||||||
|
const target_bytes = encoder.encode(target);
|
||||||
|
|
||||||
|
const salt_ptr = solver.exports.malloc(salt_bytes.length);
|
||||||
|
if (salt_ptr === 0 || salt_ptr === null) {
|
||||||
|
throw new Error("Failed to allocate memory for salt string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const target_ptr = solver.exports.malloc(target_bytes.length);
|
||||||
|
if (target_ptr === 0 || target_ptr === null) {
|
||||||
|
throw new Error("Failed to allocate memory for target string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const memory = new Uint8Array(solver.exports.memory.buffer);
|
||||||
|
memory.set(salt_bytes, salt_ptr);
|
||||||
|
memory.set(target_bytes, target_ptr);
|
||||||
|
|
||||||
|
const ret = solver.exports.solve_target_number_challenge(
|
||||||
|
target_ptr,
|
||||||
|
target_bytes.length,
|
||||||
|
salt_ptr,
|
||||||
|
salt_bytes.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
throw new Error("Failed to solve challenge");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
172
packages/lib/src/validator.ts
Normal file
172
packages/lib/src/validator.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { ChallengeAlgorithm, ChallengeStrategy, type Challenge } from '.';
|
||||||
|
import WASMValidatorUrl from '../../../solver/zig-out/bin/validator.wasm?url&inline';
|
||||||
|
|
||||||
|
type WasmExports = Record<string, Function> & {
|
||||||
|
"malloc": (byte_count: number) => number | null;
|
||||||
|
"free": (ptr: number | null, byte_count: number) => void;
|
||||||
|
"validate_leading_zeroes_challenge": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number, difficulty: number) => number;
|
||||||
|
"validate_target_number_challenge": (target_ptr: number, target_len: number, nonce_ptr: number, nonce_len: number, salt_ptr: number, salt_len: number) => number;
|
||||||
|
"hash": (challenge_ptr: number, challenge_len: number, nonce_ptr: number, nonce_len: number) => bigint;
|
||||||
|
"memory": WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidatorModule extends WebAssembly.Instance {
|
||||||
|
exports: WasmExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function array_to_base64(buffer: ArrayBuffer): string {
|
||||||
|
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate_leading_zeroes_challenge(parameters: Object, difficulty: number): Promise<Challenge> {
|
||||||
|
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;
|
||||||
|
strategy: ChallengeStrategy.LeadingZeroes;
|
||||||
|
difficulty: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TargetNumberChallengeConfig {
|
||||||
|
parameters: Object;
|
||||||
|
strategy: ChallengeStrategy.TargetNumber;
|
||||||
|
max_number: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChallengeConfig = LeadingZeroesChallengeConfig | TargetNumberChallengeConfig;
|
||||||
|
|
||||||
|
export async function generate_challenge(config: ChallengeConfig): Promise<Challenge | null> {
|
||||||
|
let challenge: Challenge | null = null;
|
||||||
|
switch (config.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
challenge = await generate_leading_zeroes_challenge(config.parameters, config.difficulty);
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
challenge = await generate_target_number_challenge(config.parameters, config.max_number);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (challenge === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validate_challenge(challenge: Challenge, challenge_solution: { challenge: string, nonce: string }): Promise<boolean> {
|
||||||
|
const validator = (await WebAssembly.instantiateStreaming(fetch(WASMValidatorUrl))).instance as unknown as ValidatorModule
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
let err;
|
||||||
|
let memory;
|
||||||
|
let nonce_bytes, nonce_ptr;
|
||||||
|
let target_bytes, target_ptr;
|
||||||
|
switch (challenge.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
target_bytes = encoder.encode(challenge_solution.challenge);
|
||||||
|
nonce_bytes = encoder.encode(challenge_solution.nonce);
|
||||||
|
|
||||||
|
target_ptr = validator.exports.malloc(challenge_solution.challenge.length);
|
||||||
|
nonce_ptr = validator.exports.malloc(challenge_solution.nonce.length);
|
||||||
|
|
||||||
|
if (target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
|
||||||
|
console.error("Failed to allocate memory for challenge string");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memory = new Uint8Array(validator.exports.memory.buffer);
|
||||||
|
memory.set(target_bytes, target_ptr);
|
||||||
|
memory.set(nonce_bytes, nonce_ptr);
|
||||||
|
|
||||||
|
err = validator.exports.validate_leading_zeroes_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, challenge.difficulty);
|
||||||
|
|
||||||
|
validator.exports.free(target_ptr, target_bytes.length);
|
||||||
|
validator.exports.free(nonce_ptr, nonce_bytes.length);
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
target_bytes = encoder.encode(challenge.target);
|
||||||
|
const salt_bytes = encoder.encode(challenge.salt);
|
||||||
|
nonce_bytes = encoder.encode(challenge_solution.nonce);
|
||||||
|
|
||||||
|
const salt_ptr = validator.exports.malloc(salt_bytes.length);
|
||||||
|
target_ptr = validator.exports.malloc(target_bytes.length);
|
||||||
|
nonce_ptr = validator.exports.malloc(nonce_bytes.length);
|
||||||
|
|
||||||
|
if (salt_ptr === 0 || salt_ptr === null || target_ptr === 0 || target_ptr === null || nonce_ptr === 0 || nonce_ptr === null) {
|
||||||
|
console.error("Failed to allocate memory for challenge string");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memory = new Uint8Array(validator.exports.memory.buffer);
|
||||||
|
memory.set(salt_bytes, salt_ptr);
|
||||||
|
memory.set(target_bytes, target_ptr);
|
||||||
|
memory.set(nonce_bytes, nonce_ptr);
|
||||||
|
|
||||||
|
err = validator.exports.validate_target_number_challenge(target_ptr, target_bytes.length, nonce_ptr, nonce_bytes.length, salt_ptr, salt_bytes.length);
|
||||||
|
|
||||||
|
validator.exports.free(salt_ptr, salt_bytes.length);
|
||||||
|
validator.exports.free(target_ptr, target_bytes.length);
|
||||||
|
validator.exports.free(nonce_ptr, nonce_bytes.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return err === 0;
|
||||||
|
}
|
||||||
1
packages/lib/src/vite-env.d.ts
vendored
Normal file
1
packages/lib/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
27
packages/lib/tsconfig.json
Normal file
27
packages/lib/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
29
packages/lib/vite.config.ts
Normal file
29
packages/lib/vite.config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: 'es2022',
|
||||||
|
rollupOptions: {
|
||||||
|
input: [
|
||||||
|
resolve(__dirname, 'src/index.ts'),
|
||||||
|
resolve(__dirname, 'src/solver.ts'),
|
||||||
|
resolve(__dirname, 'src/validator.ts'),
|
||||||
|
],
|
||||||
|
preserveEntrySignatures: "strict",
|
||||||
|
output: {
|
||||||
|
dir: 'dist',
|
||||||
|
entryFileNames: '[name].js',
|
||||||
|
preserveModules: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [dts()]
|
||||||
|
});
|
||||||
2
packages/widget/.gitignore
vendored
Normal file
2
packages/widget/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
12
packages/widget/index.html
Normal file
12
packages/widget/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>vite-project</title>
|
||||||
|
<script type="module" src="/src/pow-captcha.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<pow-captcha />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2576
packages/widget/package-lock.json
generated
Normal file
2576
packages/widget/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
packages/widget/package.json
Normal file
35
packages/widget/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@impost/widget",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"license": "BSL-1.0",
|
||||||
|
"module": "./dist/impost.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/impost.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"watch": "tsc && vite build --watch",
|
||||||
|
"dev": "vite",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"lit": "^3.1.2",
|
||||||
|
"lit-element": "^3.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.11.24",
|
||||||
|
"lit": "^3.1.2",
|
||||||
|
"lit-element": "^3.1.2",
|
||||||
|
"oxc-minify": "^0.97.0",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "npm:rolldown-vite@latest",
|
||||||
|
"vite-plugin-dts": "^4.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/widget/src/entry.ts
Normal file
1
packages/widget/src/entry.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './pow-captcha';
|
||||||
458
packages/widget/src/pow-captcha.ts
Normal file
458
packages/widget/src/pow-captcha.ts
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
import { LitElement, html, css, isServer, type PropertyValues } from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
import { type ChallengeSolveRequest, type SolutionMessage, WorkerMessageType, type WorkerRequest, WorkerResponseType } from './types/worker';
|
||||||
|
import { type Challenge, ChallengeStrategy } from '@impost/lib';
|
||||||
|
import { get_wasm_module } from '@impost/lib/solver';
|
||||||
|
|
||||||
|
import ChallengeWorker from './solver-worker?worker&inline';
|
||||||
|
|
||||||
|
@customElement('pow-captcha')
|
||||||
|
export class PowCaptcha extends LitElement {
|
||||||
|
static override styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: var(--impost-widget-width, 250px);
|
||||||
|
}
|
||||||
|
.impost-widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--impost-widget-border-color, #505050);
|
||||||
|
font-family: sans-serif;
|
||||||
|
background-color: var(--impost-widget-background-color, #070408);
|
||||||
|
border-radius: var(--impost-widget-border-radius, 8px);
|
||||||
|
}
|
||||||
|
.impost-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
.impost-main span {
|
||||||
|
height: min-content;
|
||||||
|
}
|
||||||
|
.impost-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
color: var(--impost-widget-checkbox-color, #6e6e6eff);
|
||||||
|
}
|
||||||
|
.impost-checkbox input {
|
||||||
|
height: 80%;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.impost-footer {
|
||||||
|
display: flex;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--impost-widget-footer-color, #a2a2a2);
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
.impost-footer div {
|
||||||
|
height: min-content;
|
||||||
|
}
|
||||||
|
.impost-footer #provider-link {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.impost-footer div a {
|
||||||
|
color: var(--impost-widget-footer-color, #a2a2a2);
|
||||||
|
}
|
||||||
|
.impost-footer div a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
// one of: "load", "focus", "submit", "off"
|
||||||
|
@property({ type: String })
|
||||||
|
auto: "onload" | "onfocus" | "onsubmit" | "off" = "off";
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
challengeUrl: string = 'http://localhost:3000/api/pow';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
challengejson: string = '';
|
||||||
|
|
||||||
|
// needed to allow for multiple widgets on the same page if you wanted to
|
||||||
|
// do that for some reason
|
||||||
|
uid: string = Math.floor(Math.random() * 100000).toString();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private solution: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private errorMessage: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private challengeData: Challenge | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private solved: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private isSolving: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private disabled: boolean = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private hashRate: number = 0;
|
||||||
|
|
||||||
|
// stores the nonce and solution atomics
|
||||||
|
private sab: SharedArrayBuffer = new SharedArrayBuffer(8);
|
||||||
|
|
||||||
|
private solverWorkers: Worker[] | null = null;
|
||||||
|
|
||||||
|
private solveStartTime: number | null = null;
|
||||||
|
private hashRateInterval: number | null = null;
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.fetchChallenge();
|
||||||
|
|
||||||
|
this.initWorkers();
|
||||||
|
|
||||||
|
switch (this.auto) {
|
||||||
|
case 'onload':
|
||||||
|
this.solveChallenge();
|
||||||
|
break;
|
||||||
|
case 'onfocus':
|
||||||
|
this.addEventListener('focus', () => this.solveChallenge());
|
||||||
|
break;
|
||||||
|
case 'onsubmit':
|
||||||
|
if (this.parentElement?.nodeName === 'FORM') {
|
||||||
|
this.parentElement.addEventListener('submit', () => this.solveChallenge());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(_changedProperties);
|
||||||
|
|
||||||
|
this.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this.hashRateInterval !== null) {
|
||||||
|
clearInterval(this.hashRateInterval);
|
||||||
|
this.hashRateInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const worker of this.solverWorkers || []) {
|
||||||
|
worker.terminate();
|
||||||
|
this.solverWorkers = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentWorkingNonce() {
|
||||||
|
return Atomics.load(new Uint32Array(this.sab), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchChallenge() {
|
||||||
|
this.errorMessage = '';
|
||||||
|
if (this.challengeData !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.challengejson !== '' && this.challengeData === null) {
|
||||||
|
this.challengeData = JSON.parse(this.challengejson);
|
||||||
|
this.challengejson = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isServer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(`${this.challengeUrl}/challenge`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
this.challengeData = data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching challenge:', error);
|
||||||
|
this.errorMessage = 'Failed to fetch challenge. Please try again.';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initWorkers() {
|
||||||
|
this.solverWorkers = [];
|
||||||
|
|
||||||
|
const num_workers = navigator.hardwareConcurrency;
|
||||||
|
for (let i = 0; i < num_workers; i++) {
|
||||||
|
this.solverWorkers.push(new ChallengeWorker());
|
||||||
|
}
|
||||||
|
|
||||||
|
const atomics_view = new Int32Array(this.sab);
|
||||||
|
Atomics.store(atomics_view, 0, 0);
|
||||||
|
Atomics.store(atomics_view, 1, 0);
|
||||||
|
|
||||||
|
let wasm_module = await get_wasm_module();
|
||||||
|
let worker_promises: Promise<void>[] = [];
|
||||||
|
for (let i = 0; i < this.solverWorkers.length; i++) {
|
||||||
|
console.log('Worker', i);
|
||||||
|
const worker = this.solverWorkers[i]!;
|
||||||
|
|
||||||
|
worker_promises.push(new Promise<void>((resolve, reject) => {
|
||||||
|
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||||
|
console.log('Worker', i, 'got message', event.data);
|
||||||
|
|
||||||
|
if (event.data.type === WorkerResponseType.Error) {
|
||||||
|
console.error("Worker error:", event.data.error);
|
||||||
|
reject(event.data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.type === WorkerResponseType.InitOk) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error("Unexpected message from worker"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const error_handler = (error: ErrorEvent) => {
|
||||||
|
console.error("Worker error:", error);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.addEventListener('message', message_handler);
|
||||||
|
worker.addEventListener('error', error_handler);
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
type: WorkerMessageType.Init,
|
||||||
|
module: wasm_module,
|
||||||
|
sab: this.sab,
|
||||||
|
} as WorkerRequest);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutMs = 10 * 1000;
|
||||||
|
let timeout: number;
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
this.errorMessage = 'Failed to initialize workers in time. Please refresh the page.';
|
||||||
|
reject(new Error(`Function timed out after ${timeoutMs}ms`));
|
||||||
|
}, timeoutMs);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.race([
|
||||||
|
Promise.allSettled(worker_promises).then(() => {
|
||||||
|
console.log('All workers initialized');
|
||||||
|
}),
|
||||||
|
timeoutPromise,
|
||||||
|
]).then(() => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async issueChallengeToWorker(worker: Worker, request: ChallengeSolveRequest): Promise<SolutionMessage> {
|
||||||
|
return new Promise<SolutionMessage>((resolve, reject) => {
|
||||||
|
const message_handler = (event: MessageEvent<SolutionMessage>) => {
|
||||||
|
worker.removeEventListener('message', message_handler);
|
||||||
|
worker.removeEventListener('error', error_handler);
|
||||||
|
|
||||||
|
resolve(event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error_handler = (error: ErrorEvent) => {
|
||||||
|
worker.removeEventListener('error', error_handler);
|
||||||
|
worker.removeEventListener('message', message_handler);
|
||||||
|
console.error("Worker error:", error);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.addEventListener('message', message_handler);
|
||||||
|
worker.addEventListener('error', error_handler);
|
||||||
|
|
||||||
|
switch (request.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
worker.postMessage({
|
||||||
|
strategy: ChallengeStrategy.LeadingZeroes,
|
||||||
|
salt: request.salt,
|
||||||
|
difficulty: request.difficulty,
|
||||||
|
} as WorkerRequest);
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
worker.postMessage({
|
||||||
|
strategy: ChallengeStrategy.TargetNumber,
|
||||||
|
target: request.target,
|
||||||
|
salt: request.salt,
|
||||||
|
} as WorkerRequest);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async solveChallenge() {
|
||||||
|
if (!this.challengeData || this.solverWorkers === null) {
|
||||||
|
this.errorMessage = 'Captcha is not ready. Please wait or refresh.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.solution !== '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.solveStartTime = performance.now();
|
||||||
|
this.hashRateInterval = setInterval(async () => {
|
||||||
|
const nonce = this.getCurrentWorkingNonce();
|
||||||
|
|
||||||
|
this.hashRate = (nonce / ((performance.now() - this.solveStartTime!) / 1000));
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('impost:solve', {
|
||||||
|
detail: {
|
||||||
|
solution: this.solution,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log(this.challengeData);
|
||||||
|
|
||||||
|
this.isSolving = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
this.solution = '';
|
||||||
|
|
||||||
|
const atomics_view = new Int32Array(this.sab);
|
||||||
|
Atomics.store(atomics_view, 0, 0);
|
||||||
|
Atomics.store(atomics_view, 1, -1);
|
||||||
|
|
||||||
|
let request: ChallengeSolveRequest;
|
||||||
|
|
||||||
|
switch (this.challengeData.strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
request = {
|
||||||
|
strategy: ChallengeStrategy.LeadingZeroes,
|
||||||
|
salt: this.challengeData.salt,
|
||||||
|
difficulty: this.challengeData.difficulty,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
request = {
|
||||||
|
strategy: ChallengeStrategy.TargetNumber,
|
||||||
|
target: this.challengeData.target,
|
||||||
|
salt: this.challengeData.salt,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Sending challenge to workers...');
|
||||||
|
// TODO: the first response is not always the solution, due to cmpxchg
|
||||||
|
// blocking, some workers may block on the read, and as soon as they
|
||||||
|
// unblock, they return 0 since the challenge is already solved.
|
||||||
|
//
|
||||||
|
// We need to do a better job of tracking solvers, so if one worker
|
||||||
|
// errors out, we only error out if all workers have errored out.
|
||||||
|
let worker_promises: Promise<SolutionMessage>[] = [];
|
||||||
|
for (let worker of this.solverWorkers) {
|
||||||
|
// dispatch to all workers, func is async so it will not block
|
||||||
|
worker_promises.push(this.issueChallengeToWorker(worker, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
let solution = await Promise.race(worker_promises);
|
||||||
|
|
||||||
|
if (solution.type === WorkerResponseType.Error) {
|
||||||
|
this.errorMessage = solution.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (solution.type !== WorkerResponseType.Solution) {
|
||||||
|
this.errorMessage = "Something went wrong, please try again later.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.solution = Atomics.load(atomics_view, 1).toString();
|
||||||
|
this.isSolving = false;
|
||||||
|
this.solved = true;
|
||||||
|
|
||||||
|
if (this.hashRateInterval !== null) {
|
||||||
|
clearInterval(this.hashRateInterval);
|
||||||
|
this.hashRateInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent('impost:solved', {
|
||||||
|
detail: {
|
||||||
|
solution: this.solution,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
solvePreventDefault(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.solveChallenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
if (this.challengejson !== '' && this.challengeData === null) {
|
||||||
|
this.challengeData = JSON.parse(this.challengejson);
|
||||||
|
this.challengejson = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.errorMessage) {
|
||||||
|
return html`
|
||||||
|
<div class="error-message">${this.errorMessage}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.challengeData === null) {
|
||||||
|
return html`
|
||||||
|
<div class="loading-message">Loading captcha challenge...</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="impost-widget">
|
||||||
|
<div class="impost-main">
|
||||||
|
<div class="impost-checkbox">
|
||||||
|
${!this.isSolving ? html`
|
||||||
|
<input type="checkbox" id="impost-checkbox-${this.uid}" @click=${this.solvePreventDefault} @change=${this.solvePreventDefault} ?disabled=${this.disabled} ?checked=${this.solved}>
|
||||||
|
` : html`
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
|
||||||
|
<!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
||||||
|
<g stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="9.5" fill="none" stroke-linecap="round" stroke-width="3">
|
||||||
|
<animate attributeName="stroke-dasharray" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0 150;42 150;42 150;42 150"/>
|
||||||
|
<animate attributeName="stroke-dashoffset" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0;-16;-59;-59"/>
|
||||||
|
</circle>
|
||||||
|
<animateTransform attributeName="transform" dur="2s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<label for="impost-checkbox-${this.uid}">${this.solved ? 'Verified' : html`${this.isSolving ? 'Verifying...' : 'I am not a robot'}`}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
Protected by <a href="https://github.com/impost/pow-captcha" target="_blank">Impost</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type = "text"
|
||||||
|
id = "impost-solution"
|
||||||
|
class="hidden"
|
||||||
|
.value = ${this.solution}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
packages/widget/src/solver-worker.ts
Normal file
91
packages/widget/src/solver-worker.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// This worker just sits on another thread and waits for message to solve
|
||||||
|
// challenges so that we dont block the render thread
|
||||||
|
|
||||||
|
import {
|
||||||
|
type WorkerRequest,
|
||||||
|
type SolutionMessage,
|
||||||
|
WorkerMessageType,
|
||||||
|
WorkerResponseType,
|
||||||
|
} from "./types/worker";
|
||||||
|
|
||||||
|
import { ChallengeStrategy } from "@impost/lib";
|
||||||
|
|
||||||
|
import { type SolverModule, init_solver, solve_leaading_zeroes_challenge, solve_target_number_challenge } from '@impost/lib/solver';
|
||||||
|
|
||||||
|
let solver: SolverModule | null = null;
|
||||||
|
|
||||||
|
let atomic_nonce: Int32Array | null = null;
|
||||||
|
let atomic_solution: Int32Array | null = null;
|
||||||
|
|
||||||
|
onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||||
|
if (event.data.type === WorkerMessageType.Init) {
|
||||||
|
atomic_nonce = new Int32Array(event.data.sab, 0, 1);
|
||||||
|
atomic_solution = new Int32Array(event.data.sab, 4, 1);
|
||||||
|
let module = event.data.module;
|
||||||
|
|
||||||
|
try {
|
||||||
|
solver = await init_solver({
|
||||||
|
__get_solution: () => Atomics.load(atomic_solution!, 0),
|
||||||
|
__set_solution: (value: number) => Atomics.store(atomic_solution!, 0, value),
|
||||||
|
__cmpxchg_solution: (expected: number, replacement: number) => Atomics.compareExchange(atomic_solution!, 0, expected, replacement),
|
||||||
|
__fetch_add_nonce: (value: number) => Atomics.add(atomic_nonce!, 0, value),
|
||||||
|
}, module);
|
||||||
|
} catch (error: any) {
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: `Could not load WASM solver: ${error.message}`,
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!solver) {
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "Failed to load WASM solver",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.InitOk,
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!solver) {
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "WASM solver not loaded",
|
||||||
|
} as SolutionMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atomic_nonce === null || atomic_solution === null) {
|
||||||
|
throw new Error("Atomics not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { strategy } = event.data;
|
||||||
|
|
||||||
|
let solution: number;
|
||||||
|
switch (strategy) {
|
||||||
|
case ChallengeStrategy.LeadingZeroes:
|
||||||
|
solution = solve_leaading_zeroes_challenge(solver, event.data)
|
||||||
|
break;
|
||||||
|
case ChallengeStrategy.TargetNumber:
|
||||||
|
solution = solve_target_number_challenge(solver, event.data)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are just assuming that if its less than -1, its the min i32
|
||||||
|
if (solution < 0) {
|
||||||
|
return postMessage({
|
||||||
|
type: WorkerResponseType.Error,
|
||||||
|
error: "failed to solve challenge",
|
||||||
|
} as SolutionMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
type: WorkerResponseType.Solution,
|
||||||
|
nonce: solution === -1 ? null : solution.toString()
|
||||||
|
} as SolutionMessage);
|
||||||
|
};
|
||||||
60
packages/widget/src/types/worker.ts
Normal file
60
packages/widget/src/types/worker.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { ChallengeStrategy } from "@impost/lib";
|
||||||
|
|
||||||
|
export enum WorkerMessageType {
|
||||||
|
Init = "init",
|
||||||
|
Challenge = "challenge",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkerInitRequest {
|
||||||
|
type: WorkerMessageType.Init;
|
||||||
|
module: WebAssembly.Module;
|
||||||
|
|
||||||
|
sab: SharedArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChallengeLeadingZeroesSolveRequest {
|
||||||
|
strategy: ChallengeStrategy.LeadingZeroes;
|
||||||
|
salt: string;
|
||||||
|
difficulty: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkerChallengeLeadingZeroesSolveRequest extends ChallengeLeadingZeroesSolveRequest {
|
||||||
|
type: WorkerMessageType.Challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChallengeTargetNumberSolveRequest {
|
||||||
|
strategy: ChallengeStrategy.TargetNumber;
|
||||||
|
target: string;
|
||||||
|
salt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkerChallengeTargetNumberSolveRequest extends ChallengeTargetNumberSolveRequest {
|
||||||
|
type: WorkerMessageType.Challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChallengeSolveRequest = ChallengeLeadingZeroesSolveRequest | ChallengeTargetNumberSolveRequest;
|
||||||
|
type WorkerChallengeSolveRequest = WorkerChallengeLeadingZeroesSolveRequest | WorkerChallengeTargetNumberSolveRequest;
|
||||||
|
|
||||||
|
export type WorkerRequest = WorkerInitRequest | WorkerChallengeSolveRequest;
|
||||||
|
|
||||||
|
export enum WorkerResponseType {
|
||||||
|
Error = "error",
|
||||||
|
InitOk = "init_ok",
|
||||||
|
Solution = "solution",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorMessageResponse {
|
||||||
|
type: WorkerResponseType.Error;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SolutionMessageResponse {
|
||||||
|
type: WorkerResponseType.Solution;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InitOkMessageResponse {
|
||||||
|
type: WorkerResponseType.InitOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SolutionMessage = ErrorMessageResponse | SolutionMessageResponse | InitOkMessageResponse;
|
||||||
1
packages/widget/src/vite-env.d.ts
vendored
Normal file
1
packages/widget/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
27
packages/widget/tsconfig.json
Normal file
27
packages/widget/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
29
packages/widget/vite.config.ts
Normal file
29
packages/widget/vite.config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { resolve } from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: 'es2022',
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'src/entry.ts'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: 'impost',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [/^lit/, /^@lit\//],
|
||||||
|
output: {
|
||||||
|
preserveModules: false,
|
||||||
|
dir: 'dist',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// WARN: this setting has caused issues for me in the past, but now it
|
||||||
|
// seems to work fine. I'm not sure why, but minifying the bundle saves
|
||||||
|
// 5KB, and it doesnt *cause* issues, so I'm leaving it on.
|
||||||
|
minify: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
dts(),
|
||||||
|
],
|
||||||
|
});
|
||||||
2
solver/.gitignore
vendored
Normal file
2
solver/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.zig-cache/
|
||||||
|
zig-out/
|
||||||
46
solver/build.zig
Normal file
46
solver/build.zig
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
const wasm_target = b.resolveTargetQuery(.{
|
||||||
|
.cpu_arch = .wasm32,
|
||||||
|
.os_tag = .freestanding,
|
||||||
|
.cpu_features_add = std.Target.wasm.featureSet(&.{ .bulk_memory, .bulk_memory_opt, .simd128, .tail_call }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// solver
|
||||||
|
const solver_mod = b.addModule("solver", .{
|
||||||
|
.root_source_file = b.path("src/solver.zig"),
|
||||||
|
.optimize = optimize,
|
||||||
|
.target = wasm_target,
|
||||||
|
});
|
||||||
|
|
||||||
|
const solver_exe = b.addExecutable(.{
|
||||||
|
.name = "solver",
|
||||||
|
.root_module = solver_mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
solver_exe.entry = .disabled;
|
||||||
|
solver_exe.rdynamic = true;
|
||||||
|
solver_exe.lto = .full;
|
||||||
|
solver_exe.link_gc_sections = true;
|
||||||
|
|
||||||
|
b.installArtifact(solver_exe);
|
||||||
|
|
||||||
|
// validator
|
||||||
|
const validator_mod = b.addModule("validator", .{
|
||||||
|
.root_source_file = b.path("src/validator.zig"),
|
||||||
|
.optimize = optimize,
|
||||||
|
.target = wasm_target,
|
||||||
|
});
|
||||||
|
|
||||||
|
const validator_exe = b.addLibrary(.{
|
||||||
|
.name = "validator",
|
||||||
|
.root_module = validator_mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
validator_exe.entry = .disabled;
|
||||||
|
validator_exe.rdynamic = true;
|
||||||
|
|
||||||
|
b.installArtifact(validator_exe);
|
||||||
|
}
|
||||||
31
solver/src/hasher.zig
Normal file
31
solver/src/hasher.zig
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
var argon2_params = std.crypto.pwhash.argon2.Params{
|
||||||
|
.t = 4, // time cost
|
||||||
|
.m = 256, // memory cost (in KiB)
|
||||||
|
.p = 1, // parallelism (this doesnt do anything because we are targeting wasm, and we do multithreading differently anyways)
|
||||||
|
};
|
||||||
|
|
||||||
|
const dk_len: usize = 32; // 16 or 32 byte key
|
||||||
|
var derived: [dk_len]u8 = undefined;
|
||||||
|
var buffer_hash_hex: [64]u8 = undefined;
|
||||||
|
|
||||||
|
fn bytesToHex(bytes: []const u8, output: []u8) void {
|
||||||
|
const hex_chars = "0123456789abcdef";
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < bytes.len) : (i += 1) {
|
||||||
|
output[i * 2] = hex_chars[(bytes[i] >> 4)];
|
||||||
|
output[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(allocator: Allocator, challenge: []const u8, nonce: []const u8) ![]u8 {
|
||||||
|
try std.crypto.pwhash.argon2.kdf(allocator, &derived, nonce, challenge, argon2_params, .argon2id);
|
||||||
|
|
||||||
|
var hash_bytes: [32]u8 = undefined;
|
||||||
|
std.crypto.hash.sha2.Sha256.hash(&derived, @ptrCast(hash_bytes[0..].ptr), .{});
|
||||||
|
|
||||||
|
bytesToHex(&hash_bytes, &buffer_hash_hex);
|
||||||
|
return buffer_hash_hex[0..];
|
||||||
|
}
|
||||||
7
solver/src/kctf.zig
Normal file
7
solver/src/kctf.zig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// A PoW algorithm based on google's kCTF scheme
|
||||||
|
// https://google.github.io/kctf/
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const math = std.math;
|
||||||
159
solver/src/solver.zig
Normal file
159
solver/src/solver.zig
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const hasher = @import("hasher.zig");
|
||||||
|
|
||||||
|
// var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
|
// var allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var allocator = std.heap.wasm_allocator;
|
||||||
|
|
||||||
|
extern fn __get_solution() i32;
|
||||||
|
extern fn __set_solution(value: i32) void;
|
||||||
|
extern fn __cmpxchg_solution(old: i32, new: i32) i32;
|
||||||
|
extern fn __fetch_add_nonce(value: i32) i32;
|
||||||
|
|
||||||
|
// fn log(comptime fmt: []const u8, args: anytype) void {
|
||||||
|
// const formatted = std.fmt.allocPrint(allocator, fmt, args) catch return;
|
||||||
|
// __log(@intFromPtr(formatted.ptr), formatted.len);
|
||||||
|
// allocator.free(formatted);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export fn malloc(byte_count: usize) ?*u8 {
|
||||||
|
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||||
|
return @ptrCast(ptr.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||||
|
if (ptr) |p| {
|
||||||
|
const cast_ptr: [*]u8 = @ptrCast(p);
|
||||||
|
allocator.free(cast_ptr[0..byte_count]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SolveError = enum(u32) {
|
||||||
|
InvalidDifficulty = 1,
|
||||||
|
InvalidNonce = 2,
|
||||||
|
NoSolution = 3,
|
||||||
|
OutOfMemory = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
var solve_error: ?SolveError = null;
|
||||||
|
export fn get_solve_error() u32 {
|
||||||
|
if (solve_error) |err| {
|
||||||
|
return @intFromEnum(err);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns nonce on success, -1 on failure
|
||||||
|
// to get the error, call get_solve_error
|
||||||
|
export fn solve_leaading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, difficulty: u32) i32 {
|
||||||
|
solve_error = null;
|
||||||
|
|
||||||
|
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||||
|
|
||||||
|
if (difficulty < 1 or difficulty > 64) {
|
||||||
|
solve_error = SolveError.InvalidDifficulty;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target_prefix_buffer: [64]u8 = @splat('0');
|
||||||
|
const target_prefix = target_prefix_buffer[0..difficulty];
|
||||||
|
|
||||||
|
const max_nonce_iterations: u64 = 1_000_000_000;
|
||||||
|
|
||||||
|
// 64 + 9 digits for nonce since the max nonce is 999_999_999 (not 1 billion since nonce < max_nonce_iterations)
|
||||||
|
var input_buffer: []u8 = allocator.alloc(u8, challenge_len + 9) catch {
|
||||||
|
// log("Failed to allocate memory for challenge\n", .{});
|
||||||
|
solve_error = SolveError.OutOfMemory;
|
||||||
|
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 {
|
||||||
|
solve_error = SolveError.InvalidNonce;
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hash_hex_slice = hasher.hash(allocator, input_buffer[0..challenge_len], input_buffer[challenge_len .. challenge_len + nonce_str.len]) catch {
|
||||||
|
solve_error = SolveError.OutOfMemory;
|
||||||
|
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;
|
||||||
|
}
|
||||||
81
solver/src/validator.zig
Normal file
81
solver/src/validator.zig
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const hasher = @import("hasher.zig");
|
||||||
|
|
||||||
|
var allocator = std.heap.wasm_allocator;
|
||||||
|
|
||||||
|
export fn malloc(byte_count: usize) ?*u8 {
|
||||||
|
const ptr = allocator.alloc(u8, byte_count) catch return null;
|
||||||
|
return @ptrCast(ptr.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn free(ptr: ?*anyopaque, byte_count: usize) void {
|
||||||
|
if (ptr) |p| {
|
||||||
|
const cast_ptr: [*]u8 = @ptrCast(p);
|
||||||
|
allocator.free(cast_ptr[0..byte_count]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytesToHex(bytes: []const u8, buf: []u8) void {
|
||||||
|
const hex_chars = "0123456789abcdef";
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < bytes.len) : (i += 1) {
|
||||||
|
buf[i * 2] = hex_chars[(bytes[i] >> 4)];
|
||||||
|
buf[i * 2 + 1] = hex_chars[bytes[i] & 0x0F];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export fn validate_leading_zeroes_challenge(challenge_ptr: [*]u8, challenge_len: usize, nonce_ptr: [*]u8, nonce_len: usize, difficulty: u32) i32 {
|
||||||
|
const challenge_slice = challenge_ptr[0..challenge_len];
|
||||||
|
const nonce_slice = nonce_ptr[0..nonce_len];
|
||||||
|
|
||||||
|
if (difficulty < 1 or difficulty > 64) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target_prefix_buffer: [64]u8 = @splat('0');
|
||||||
|
const target_prefix = target_prefix_buffer[0..difficulty];
|
||||||
|
|
||||||
|
const hash_hex_slice = hasher.hash(allocator, challenge_slice, nonce_slice) catch return -2;
|
||||||
|
|
||||||
|
if (!std.mem.startsWith(u8, hash_hex_slice, target_prefix)) {
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 target_slice = target_ptr[0..target_len];
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!std.mem.eql(u8, target_slice, hash_hex_slice)) {
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
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