Signed-off-by: Arija A. <ari@ari.lt>
This commit is contained in:
2025-10-23 20:36:41 +03:00
parent f6a80c9c4f
commit caf4ac7c06
107 changed files with 3327 additions and 2147 deletions

68
src/static/css/base.css Normal file
View File

@@ -0,0 +1,68 @@
:root {
color-scheme: dark;
}
*,
*::before,
*::after {
-webkit-box-sizing: border-box;
box-sizing: border-box;
word-wrap: break-word;
scroll-behavior: smooth;
}
code,
code *,
pre,
pre * {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
}
html {
height: 100%;
min-height: 100%;
}
body {
height: auto;
min-height: 100%;
padding: 2rem;
min-height: 100vh;
}
html,
body {
line-height: 1.5;
}
ol,
ul,
ol *,
ul * {
line-height: 1.8;
}
code {
white-space: pre-wrap !important;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
-webkit-transition: none !important;
-o-transition: none !important;
transition: none !important;
-webkit-animation: none !important;
animation: none !important;
-webkit-animation-play-state: paused !important;
animation-play-state: paused !important;
scroll-behavior: auto !important;
}
}

3
src/static/css/error.css Normal file
View File

@@ -0,0 +1,3 @@
article {
text-align: center;
}

BIN
src/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

353
src/static/js/pow.js Normal file
View File

@@ -0,0 +1,353 @@
/**
* @licstart The following is the entire license notice for the JavaScript
* code in this file.
*
* Copyright (C) 2025 Arija A. <ari@ari.lt>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice for the JavaScript code
* in this file.
*/
"use strict";
const pow_text_encoder = new TextEncoder();
/**
* Minimal pure JavaScript SHA-256 implementation.
*/
function pow_sha256_js(msg_uint8) {
const K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
function right_rotate(value, amount) {
return (value >>> amount) | (value << (32 - amount));
}
const msg_length = msg_uint8.length;
const bit_length = msg_length * 8;
/* Padding */
const padded_length =
((msg_length + 9 + 63) >> 6) << 6; /* multiple of 64 */
const padded = new Uint8Array(padded_length);
padded.set(msg_uint8);
padded[msg_length] = 0x80;
const dv = new DataView(padded.buffer);
/* Append 64-bit big-endian length */
dv.setUint32(
padded_length - 8,
Math.floor(bit_length / 0x100000000),
false,
);
dv.setUint32(padded_length - 4, bit_length & 0xffffffff, false);
/* Initial hash values */
let h0 = 0x6a09e667;
let h1 = 0xbb67ae85;
let h2 = 0x3c6ef372;
let h3 = 0xa54ff53a;
let h4 = 0x510e527f;
let h5 = 0x9b05688c;
let h6 = 0x1f83d9ab;
let h7 = 0x5be0cd19;
const w = new Uint32Array(64);
for (let i = 0; i < padded_length; i += 64) {
for (let t = 0; t < 16; ++t) {
w[t] = dv.getUint32(i + t * 4, false);
}
for (let t = 16; t < 64; ++t) {
const s0 =
right_rotate(w[t - 15], 7) ^
right_rotate(w[t - 15], 18) ^
(w[t - 15] >>> 3);
const s1 =
right_rotate(w[t - 2], 17) ^
right_rotate(w[t - 2], 19) ^
(w[t - 2] >>> 10);
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
}
let a = h0;
let b = h1;
let c = h2;
let d = h3;
let e = h4;
let f = h5;
let g = h6;
let h = h7;
for (let t = 0; t < 64; ++t) {
const S1 =
right_rotate(e, 6) ^ right_rotate(e, 11) ^ right_rotate(e, 25);
const ch = (e & f) ^ (~e & g);
const temp1 = (h + S1 + ch + K[t] + w[t]) >>> 0;
const S0 =
right_rotate(a, 2) ^ right_rotate(a, 13) ^ right_rotate(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const temp2 = (S0 + maj) >>> 0;
h = g;
g = f;
f = e;
e = (d + temp1) >>> 0;
d = c;
c = b;
b = a;
a = (temp1 + temp2) >>> 0;
}
h0 = (h0 + a) >>> 0;
h1 = (h1 + b) >>> 0;
h2 = (h2 + c) >>> 0;
h3 = (h3 + d) >>> 0;
h4 = (h4 + e) >>> 0;
h5 = (h5 + f) >>> 0;
h6 = (h6 + g) >>> 0;
h7 = (h7 + h) >>> 0;
}
const hash = new Uint8Array(32);
const hash_view = new DataView(hash.buffer);
hash_view.setUint32(0, h0, false);
hash_view.setUint32(4, h1, false);
hash_view.setUint32(8, h2, false);
hash_view.setUint32(12, h3, false);
hash_view.setUint32(16, h4, false);
hash_view.setUint32(20, h5, false);
hash_view.setUint32(24, h6, false);
hash_view.setUint32(28, h7, false);
return hash;
}
/**
* Compute SHA-256 hash of input Uint8Array.
* Uses native crypto.subtle.digest if available, otherwise falls back to pure JS implementation.
*/
async function pow_sha256(message_buffer) {
if (
typeof crypto === "object" &&
crypto.subtle &&
typeof crypto.subtle.digest === "function"
) {
return await crypto.subtle.digest("SHA-256", message_buffer);
} else {
return pow_sha256_js(message_buffer).buffer;
}
}
/**
* Encode a string to Uint8Array using TextEncoder.
*/
function pow_str_to_u8(str) {
return pow_text_encoder.encode(str);
}
/**
* Check if the hash buffer meets the difficulty.
* Difficulty is number of leading zero hex characters (nibbles).
* This function checks bytes directly for better performance.
*/
function pow_meets_difficulty_bytes(hash_buffer, difficulty) {
const hash_bytes = new Uint8Array(hash_buffer);
const zero_bytes = Math.floor(difficulty / 2);
const half_nibble = difficulty % 2;
for (let idx = 0; idx < zero_bytes; ++idx) {
if (hash_bytes[idx] !== 0) return false;
}
if (half_nibble) {
if ((hash_bytes[zero_bytes] & 0xf0) !== 0) return false;
}
return true;
}
/**
* JS Proof-of-Work
*/
async function js_proof_of_work(button, difficulty, nonce) {
let tries = 0;
let solution = 0;
const nonce_str = nonce.toString();
const batch_size = 150000;
while (true) {
for (let idx = 0; idx < batch_size; ++idx) {
const message = nonce_str + solution.toString();
const message_buffer = pow_str_to_u8(message);
const hash_buffer = await pow_sha256(message_buffer);
if (pow_meets_difficulty_bytes(hash_buffer, difficulty)) {
button.value = `Computed PoW after ${tries + idx} tries`;
return solution.toString();
}
++solution;
}
tries += batch_size;
button.value = `Computing PoW (${tries} tries)`;
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
/**
* WASM Proof-of-Work
*/
async function wasm_proof_of_work(button, difficulty, nonce) {
let pow_found = false;
let pow_solution = null;
const wasm_path = "/static/js/wasm/pow.wasm";
const imports = {
global: {
pow_update: (tries) => {
button.value = `Computing PoW (${tries} tries)`;
},
pow_finish: (tries, solution) => {
button.value = `Computed PoW after ${tries} tries`;
pow_solution = solution;
pow_found = true;
},
},
};
let response = await fetch(wasm_path);
if (!response.ok) {
console.warn(`Failed to fetch WASM ${wasm_path}`);
return null;
}
let wasm_module;
if (
"instantiateStreaming" in WebAssembly &&
typeof WebAssembly.instantiateStreaming === "function"
) {
wasm_module = await WebAssembly.instantiateStreaming(response, imports);
} else {
const bytes = await response.arrayBuffer();
wasm_module = await WebAssembly.instantiate(bytes, imports);
}
const { pow_init, pow_solve_batch, memory } = wasm_module.instance.exports;
const encoder = new TextEncoder();
const nonce_bytes = encoder.encode(nonce);
const memory_view = new Uint8Array(memory.buffer);
memory_view.set(nonce_bytes, 0);
pow_init(0, nonce_bytes.length, difficulty);
while (!pow_found) {
const result = pow_solve_batch();
if (result === 1) {
// Solution found, pow_finish callback fired
break;
} else if (result === -1) {
console.error("WASM PoW (initialisation) error.");
return null;
}
// Yield to UI between batches
await new Promise((resolve) => setTimeout(resolve, 0));
}
return pow_solution === null ? null : pow_solution.toString();
}
/**
* Perform proof of work by finding a solution such that
* SHA-256(nonce + solution) hash has leading zeros per difficulty.
*/
async function proof_of_work(button, difficulty, nonce) {
button.value = "Computing PoW (0 tries)";
await new Promise((resolve) => setTimeout(resolve, 0));
if ("WebAssembly" in window && typeof WebAssembly === "object") {
const sol = await wasm_proof_of_work(button, difficulty, nonce);
if (sol !== null) {
return sol;
} else {
console.warn(
"WebAssembly present but failed to compute PoW using web assembly. Falling back to JavaScript.",
);
}
}
return await js_proof_of_work(button, difficulty, nonce);
}
/**
* Wrapper to handle form submission and disable button during PoW.
*/
async function proof_of_work_powform(form, button, difficulty, nonce) {
const was_disabled = button.disabled;
button.disabled = true;
button.setAttribute("aria-label", "Proof of Work is computing...");
try {
form.elements["pow_solution"].value = await proof_of_work(
button,
difficulty,
nonce,
);
} finally {
button.disabled = was_disabled;
button.setAttribute("aria-label", "Submitting...");
}
}
/**
* Wrapper to handle form submission and disable button during PoW on page load.
*/
function proof_of_work_powform_onload(form_id, button_id, difficulty, nonce) {
const form = document.getElementById(form_id);
const form_submit = document.getElementById(button_id);
form.addEventListener("submit", async (evt) => {
evt.preventDefault();
form_submit.disabled = true;
await proof_of_work_powform(form, form_submit, difficulty, nonce);
form_submit.value = "Submitting...";
form.submit();
});
}

View File

@@ -0,0 +1,19 @@
---
BasedOnStyle: LLVM
IndentWidth: 4
SortIncludes: false
AlignConsecutiveAssignments: true
AlignConsecutiveBitFields: true
AlignConsecutiveMacros: true
AlignEscapedNewlines: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortEnumsOnASingleLine: true
AllowShortFunctionsOnASingleLine: true
AllowShortLambdasOnASingleLine: true
BinPackParameters: false
IndentCaseBlocks: true
IndentCaseLabels: true
IndentExternBlock: true
IndentGotoLabels: true
---

6
src/static/js/wasm/compile.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
set -xeu
clang --target=wasm32 -s -O3 -flto=full -fno-trapping-math -funroll-loops -ffast-math -fno-math-errno -fomit-frame-pointer -fstrict-aliasing -fvisibility-inlines-hidden -fvisibility=hidden -std=c99 -Werror -Wpedantic -pedantic-errors -pedantic -nostdlib -Wl,--no-entry -Wl,--export=pow_init -Wl,--export=pow_solve_batch -o pow.wasm pow.c
chmod 600 pow.wasm

203
src/static/js/wasm/pow.c Normal file
View File

@@ -0,0 +1,203 @@
#include <stdint.h>
#include <stddef.h>
#define BATCH_SIZE 150000
#define MAX_MSG_LEN 256
#define NONCE_MAX_LEN 128
/* Global state */
static char solution_str[32];
static uint8_t hash_output[32];
static uint8_t message_buffer[MAX_MSG_LEN];
static uint32_t nonce_length = 0;
static uint32_t difficulty = 0;
static uint64_t current_solution = 0;
static uint64_t tries = 0;
static uint32_t zero_bytes = 0;
static uint32_t half_nibble = 0;
__attribute__((__import_module__("global"), __import_name__("pow_update"))) void
pow_update(uint64_t tries);
__attribute__((__import_module__("global"), __import_name__("pow_finish"))) void
pow_finish(uint64_t tries, uint64_t solution);
static const uint32_t K[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
};
#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define EP0(x) (ROTR(x, 2) ^ ROTR(x, 13) ^ ROTR(x, 22))
#define EP1(x) (ROTR(x, 6) ^ ROTR(x, 11) ^ ROTR(x, 25))
#define SIG0(x) (ROTR(x, 7) ^ ROTR(x, 18) ^ ((x) >> 3))
#define SIG1(x) (ROTR(x, 17) ^ ROTR(x, 19) ^ ((x) >> 10))
static inline void g_sha256_hash(uint32_t message_length) {
uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
const uint64_t bit_length = (uint64_t)message_length * 8;
const uint32_t padded_length = ((message_length + 9 + 63) / 64) * 64;
static uint8_t padded_msg[128];
for (uint32_t idx = 0; idx < message_length; ++idx) {
padded_msg[idx] = message_buffer[idx];
}
padded_msg[message_length] = 0x80;
for (uint32_t idx = message_length + 1; idx < padded_length - 8; ++idx) {
padded_msg[idx] = 0;
}
for (int idx = 0; idx < 8; ++idx) {
padded_msg[padded_length - 1 - idx] =
(uint8_t)(bit_length >> (8 * idx));
}
for (uint32_t chunk = 0; chunk < padded_length; chunk += 64) {
uint32_t w[64];
for (int idx = 0; idx < 16; ++idx) {
w[idx] = ((uint32_t)padded_msg[chunk + idx * 4] << 24) |
((uint32_t)padded_msg[chunk + idx * 4 + 1] << 16) |
((uint32_t)padded_msg[chunk + idx * 4 + 2] << 8) |
((uint32_t)padded_msg[chunk + idx * 4 + 3]);
}
for (int idx = 16; idx < 64; ++idx) {
w[idx] =
SIG1(w[idx - 2]) + w[idx - 7] + SIG0(w[idx - 15]) + w[idx - 16];
}
uint32_t a = h[0], b = h[1], c = h[2], d = h[3];
uint32_t e = h[4], f = h[5], g = h[6], h_val = h[7];
for (int idx = 0; idx < 64; ++idx) {
const uint32_t t1 = h_val + EP1(e) + CH(e, f, g) + K[idx] + w[idx];
const uint32_t t2 = EP0(a) + MAJ(a, b, c);
h_val = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;
}
h[0] += a;
h[1] += b;
h[2] += c;
h[3] += d;
h[4] += e;
h[5] += f;
h[6] += g;
h[7] += h_val;
}
for (int idx = 0; idx < 8; ++idx) {
hash_output[idx * 4] = (h[idx] >> 24) & 0xFF;
hash_output[idx * 4 + 1] = (h[idx] >> 16) & 0xFF;
hash_output[idx * 4 + 2] = (h[idx] >> 8) & 0xFF;
hash_output[idx * 4 + 3] = h[idx] & 0xFF;
}
}
static inline int uint64_to_str(uint64_t number, char *str) {
if (number == 0) {
str[0] = '0';
str[1] = '\0';
return 1;
}
int length = 0;
uint64_t temp = number;
while (temp > 0) {
++length;
temp /= 10;
}
str[length] = '\0';
for (int idx = length - 1; idx >= 0; --idx) {
str[idx] = '0' + (number % 10);
number /= 10;
}
return length;
}
static inline int g_meets_difficulty(void) {
for (uint32_t idx = 0; idx < zero_bytes; ++idx) {
if (hash_output[idx] != 0) {
return 0;
}
}
if (half_nibble && (hash_output[zero_bytes] & 0xF0) != 0) {
return 0;
}
return 1;
}
__attribute__((export_name("pow_init"))) void
pow_init(uintptr_t nonce_ptr, uint32_t _nonce_length, uint32_t _difficulty) {
const uint8_t *nonce = (const uint8_t *)nonce_ptr;
nonce_length =
(_nonce_length > NONCE_MAX_LEN) ? NONCE_MAX_LEN : _nonce_length;
difficulty = _difficulty;
zero_bytes = _difficulty / 2;
half_nibble = _difficulty % 2;
current_solution = 0;
tries = 0;
for (uint32_t idx = 0; idx < nonce_length; ++idx) {
message_buffer[idx] = nonce[idx];
}
}
__attribute__((export_name("pow_solve_batch"))) int pow_solve_batch(void) {
if (nonce_length == 0) {
return 0;
}
for (uint32_t batch_idx = 0; batch_idx < BATCH_SIZE; batch_idx++) {
const int sol_len = uint64_to_str(current_solution, solution_str);
const int message_length = nonce_length + sol_len;
if (message_length > MAX_MSG_LEN) {
return -1;
}
for (int idx = 0; idx < sol_len; ++idx) {
message_buffer[nonce_length + idx] = (uint8_t)solution_str[idx];
}
g_sha256_hash(message_length);
if (g_meets_difficulty()) {
pow_finish(tries + batch_idx, current_solution);
return 1;
}
++current_solution;
}
tries += BATCH_SIZE;
pow_update(tries);
return 0;
}