Downloads: packaging/ota/publish-public.sh publishes the current installers, the HiveOS package and the two signed manifests into dl/public/ with no token in any URL, writes the four /public/ aliases as vercel.json rewrites and an unsigned index for the site; publish-manifest.sh --public and ship-app.mjs --public run it on every release (dry run and self-test cover it). Nothing removed from the token folders. Site: the miner and wallet buttons link the public aliases and show the version and size from the index, read at build time (site/downloads.json is the offline snapshot); TESTNET_OPEN in build.mjs drops the "Public testnet: not yet open" line on the go; the HiveOS Flight Sheet install line on the miner page; /faucet page. HiveOS: igneum-hive-0.3.8.tar.gz from the 0.3.8 node (2b6d23ef, PC build job) and the zig-built Linux workers. Faucet: site/api/faucet.mjs (10 IGN per address and per IP per day, Neon table faucet_grants, EIP-1559 transfer signed by site/lib/eth.mjs with no dependencies: keccak, RLP, secp256k1 with RFC 6979), FAUCET_KEY and FAUCET_RPC from the Vercel env only; 15 unit tests with a fake database and node, run in CI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
165 lines
8.9 KiB
JavaScript
165 lines
8.9 KiB
JavaScript
// Ethereum signing with no dependencies, for the faucet function (site/api/faucet.mjs). The site project installs
|
|
// nothing at build time (its install command is `echo skip`), so viem is not an option there; this is the minimum:
|
|
// keccak-256, RLP, secp256k1 ECDSA with RFC 6979 nonces, the EIP-1559 envelope and the address of a key.
|
|
// Checked against the live devnet node (eth_sendRawTransaction accepts the transaction and the receipt's `from` is
|
|
// this key's address) and against known keccak vectors in site/api/faucet.test.mjs.
|
|
import { createHmac } from 'node:crypto';
|
|
|
|
// ---- keccak-256 (the original Keccak padding 0x01..0x80, not SHA-3's 0x06) ------------------------------------------
|
|
const RC = [
|
|
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n,
|
|
0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
|
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n,
|
|
0x000000000000800an, 0x800000008000000an, 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
|
];
|
|
// rotation offsets r[x + 5y]
|
|
const ROT = [0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14];
|
|
const M64 = (1n << 64n) - 1n;
|
|
const rotl = (v, n) => n === 0 ? v : (((v << BigInt(n)) | (v >> BigInt(64 - n))) & M64);
|
|
function keccakF(A) {
|
|
const C = new Array(5), D = new Array(5), B = new Array(25);
|
|
for (let round = 0; round < 24; round++) {
|
|
for (let x = 0; x < 5; x++) C[x] = A[x] ^ A[x + 5] ^ A[x + 10] ^ A[x + 15] ^ A[x + 20];
|
|
for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rotl(C[(x + 1) % 5], 1);
|
|
for (let i = 0; i < 25; i++) A[i] ^= D[i % 5];
|
|
for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl(A[x + 5 * y], ROT[x + 5 * y]);
|
|
for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) A[x + 5 * y] = B[x + 5 * y] ^ ((~B[(x + 1) % 5 + 5 * y] & M64) & B[(x + 2) % 5 + 5 * y]);
|
|
A[0] ^= RC[round];
|
|
}
|
|
}
|
|
export function keccak256(bytes) {
|
|
const rate = 136;
|
|
const padded = new Uint8Array(Math.ceil((bytes.length + 1) / rate) * rate);
|
|
padded.set(bytes); padded[bytes.length] ^= 0x01; padded[padded.length - 1] ^= 0x80;
|
|
const A = new Array(25).fill(0n);
|
|
for (let off = 0; off < padded.length; off += rate) {
|
|
for (let i = 0; i < rate / 8; i++) {
|
|
let lane = 0n;
|
|
for (let b = 7; b >= 0; b--) lane = (lane << 8n) | BigInt(padded[off + i * 8 + b]);
|
|
A[i] ^= lane;
|
|
}
|
|
keccakF(A);
|
|
}
|
|
const out = new Uint8Array(32);
|
|
for (let i = 0; i < 4; i++) { let lane = A[i]; for (let b = 0; b < 8; b++) { out[i * 8 + b] = Number(lane & 0xffn); lane >>= 8n; } }
|
|
return out;
|
|
}
|
|
|
|
// ---- bytes and hex --------------------------------------------------------------------------------------------------
|
|
export const hex = bytes => '0x' + Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
export function unhex(s) {
|
|
const h = String(s).replace(/^0x/i, '');
|
|
if (h.length % 2 || /[^0-9a-f]/i.test(h)) throw new Error(`bad hex ${s}`);
|
|
const out = new Uint8Array(h.length / 2);
|
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
return out;
|
|
}
|
|
const bigToBytes = (v, len) => { const h = v.toString(16).padStart(len * 2, '0'); return unhex(h); };
|
|
const bytesToBig = b => { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; };
|
|
const concat = (...parts) => { const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; };
|
|
const utf8 = s => new TextEncoder().encode(s);
|
|
|
|
// ---- RLP -----------------------------------------------------------------------------------------------------------
|
|
// an item is a Uint8Array, a bigint or number (minimal big-endian, 0 = empty), a hex string, or an array of items
|
|
function toBytes(item) {
|
|
if (item instanceof Uint8Array) return item;
|
|
if (typeof item === 'bigint' || typeof item === 'number') { const v = BigInt(item); if (v < 0n) throw new Error('negative'); if (v === 0n) return new Uint8Array(0); let h = v.toString(16); if (h.length % 2) h = '0' + h; return unhex(h); }
|
|
if (typeof item === 'string') return unhex(item);
|
|
throw new Error('rlp: unsupported item');
|
|
}
|
|
function rlpLength(len, offset) {
|
|
if (len < 56) return new Uint8Array([offset + len]);
|
|
const l = toBytes(len);
|
|
return concat(new Uint8Array([offset + 55 + l.length]), l);
|
|
}
|
|
export function rlp(item) {
|
|
if (Array.isArray(item)) { const body = concat(...item.map(rlp)); return concat(rlpLength(body.length, 0xc0), body); }
|
|
const b = toBytes(item);
|
|
if (b.length === 1 && b[0] < 0x80) return b;
|
|
return concat(rlpLength(b.length, 0x80), b);
|
|
}
|
|
|
|
// ---- secp256k1 ------------------------------------------------------------------------------------------------------
|
|
const P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
|
|
export const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
const G = [0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n];
|
|
const mod = (a, m) => { const r = a % m; return r < 0n ? r + m : r; };
|
|
function inv(a, m) { // extended Euclid
|
|
let lm = 1n, hm = 0n, low = mod(a, m), high = m;
|
|
if (low === 0n) throw new Error('no inverse of 0');
|
|
while (low > 1n) { const r = high / low; [lm, hm] = [hm - lm * r, lm]; [low, high] = [high - low * r, low]; }
|
|
return mod(lm, m);
|
|
}
|
|
function pointAdd(a, b) {
|
|
if (!a) return b; if (!b) return a;
|
|
const [x1, y1] = a, [x2, y2] = b;
|
|
if (x1 === x2) { if (mod(y1 + y2, P) === 0n) return null; return pointDouble(a); }
|
|
const l = mod((y2 - y1) * inv(x2 - x1, P), P);
|
|
const x3 = mod(l * l - x1 - x2, P);
|
|
return [x3, mod(l * (x1 - x3) - y1, P)];
|
|
}
|
|
function pointDouble(a) {
|
|
const [x, y] = a;
|
|
const l = mod(3n * x * x * inv(2n * y, P), P);
|
|
const x3 = mod(l * l - 2n * x, P);
|
|
return [x3, mod(l * (x - x3) - y, P)];
|
|
}
|
|
function pointMul(k, point = G) {
|
|
let r = null, q = point;
|
|
for (let e = mod(k, N); e > 0n; e >>= 1n) { if (e & 1n) r = pointAdd(r, q); q = pointDouble(q); }
|
|
return r;
|
|
}
|
|
export function publicKey(priv) { // uncompressed, 64 bytes (x || y), no 0x04 prefix
|
|
const d = bytesToBig(unhex(priv));
|
|
if (d <= 0n || d >= N) throw new Error('private key out of range');
|
|
const [x, y] = pointMul(d);
|
|
return concat(bigToBytes(x, 32), bigToBytes(y, 32));
|
|
}
|
|
export function addressOf(priv) { return hex(keccak256(publicKey(priv)).slice(12)); }
|
|
|
|
// RFC 6979 nonce for secp256k1 with HMAC-SHA256 (deterministic: the same key and hash give the same signature)
|
|
function rfc6979(privBytes, hash) {
|
|
const hmac = (k, ...msgs) => { const h = createHmac('sha256', k); for (const m of msgs) h.update(m); return new Uint8Array(h.digest()); };
|
|
let v = new Uint8Array(32).fill(1), k = new Uint8Array(32);
|
|
const x = privBytes, h1 = bigToBytes(mod(bytesToBig(hash), N), 32);
|
|
k = hmac(k, v, new Uint8Array([0]), x, h1); v = hmac(k, v);
|
|
k = hmac(k, v, new Uint8Array([1]), x, h1); v = hmac(k, v);
|
|
for (;;) {
|
|
v = hmac(k, v);
|
|
const t = bytesToBig(v);
|
|
if (t >= 1n && t < N) return t;
|
|
k = hmac(k, v, new Uint8Array([0])); v = hmac(k, v);
|
|
}
|
|
}
|
|
// returns {r, s, v} with low s and the recovery id v (0 or 1)
|
|
export function sign(hash, priv) {
|
|
const privBytes = unhex(priv); const d = bytesToBig(privBytes);
|
|
if (d <= 0n || d >= N) throw new Error('private key out of range');
|
|
const z = mod(bytesToBig(hash), N);
|
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
const k = rfc6979(privBytes, attempt ? keccak256(concat(hash, new Uint8Array([attempt]))) : hash);
|
|
const R = pointMul(k);
|
|
const r = mod(R[0], N);
|
|
if (r === 0n) continue;
|
|
let s = mod(inv(k, N) * (z + r * d), N);
|
|
if (s === 0n) continue;
|
|
let v = Number(R[1] & 1n);
|
|
if (s > N / 2n) { s = N - s; v ^= 1; }
|
|
return { r, s, v };
|
|
}
|
|
throw new Error('no signature');
|
|
}
|
|
|
|
// ---- the EIP-1559 transaction ------------------------------------------------------------------------------------------
|
|
// fields: chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to (hex), value, data (hex, default 0x)
|
|
export function signTransaction(tx, priv) {
|
|
const fields = [BigInt(tx.chainId), BigInt(tx.nonce), BigInt(tx.maxPriorityFeePerGas), BigInt(tx.maxFeePerGas), BigInt(tx.gas), unhex(tx.to), BigInt(tx.value), unhex(tx.data || '0x'), []];
|
|
const signingHash = keccak256(concat(new Uint8Array([2]), rlp(fields)));
|
|
const { r, s, v } = sign(signingHash, priv);
|
|
const raw = concat(new Uint8Array([2]), rlp([...fields, BigInt(v), r, s]));
|
|
return { raw: hex(raw), hash: hex(keccak256(raw)), v, r, s };
|
|
}
|
|
|
|
export const toWei = ign => BigInt(Math.round(Number(ign) * 1e6)) * 10n ** 12n; // whole-number IGN or up to 6 decimals
|
|
export const isAddress = s => typeof s === 'string' && /^0x[0-9a-fA-F]{40}$/.test(s);
|
|
export { utf8, bytesToBig, bigToBytes };
|