Zero dependencies, no account, no API. Every command takes --rpc, so it runs against a node you operate and never needs ours. That is the point: an exposure measurement you cannot re-run is an opinion with a logo on it. Ships with two self-checks meant to be run by you, not just by us. The second, proba-vocabular.mjs, exists because of a real defect found on 2026-08-16: the scanner emits RED / YELLOW / GREEN, two downstream files each wrote their own copy of that list, and both wrote AMBER, so a hybrid contract, exactly a client who has already started migrating, was reported as unmeasurable. Both sides were self-consistent, so nothing we had could see it. The vocabulary now has one exported source, and the test walks the whole path with a negative control that makes it able to fail. The same scanner returns RED about our own contracts. See VERIFY-US.md.
143 lines
4.8 KiB
JavaScript
143 lines
4.8 KiB
JavaScript
// keccak.js, a compact, zero-dependency keccak256 (Ethereum's Keccak, 0x01
|
|
// domain padding, NOT FIPS-202 SHA3). Pure Node built-ins, BigInt lanes.
|
|
//
|
|
// This exists so the migration SDK can derive CREATE2 account addresses and
|
|
// EIP-55 checksums with no runtime dependencies, matching the create-aere-pqc
|
|
// tool's "zero runtime dependencies" style. Correctness is proven two ways in
|
|
// selftest.js: the empty-string vector, and a real on-chain CREATE2 address.
|
|
|
|
const MASK64 = (1n << 64n) - 1n;
|
|
|
|
const ROUND_CONSTANTS = [
|
|
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
|
|
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
|
|
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
|
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
|
|
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
|
|
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
|
];
|
|
|
|
// Rho rotation offsets, flat-indexed by (x + 5*y).
|
|
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,
|
|
];
|
|
|
|
function rotl64(x, n) {
|
|
const b = BigInt(n) % 64n;
|
|
if (b === 0n) return x & MASK64;
|
|
return ((x << b) | (x >> (64n - b))) & MASK64;
|
|
}
|
|
|
|
function keccakF(state) {
|
|
for (let round = 0; round < 24; round++) {
|
|
// theta
|
|
const C = new Array(5);
|
|
for (let x = 0; x < 5; x++) {
|
|
C[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
|
|
}
|
|
const D = new Array(5);
|
|
for (let x = 0; x < 5; x++) {
|
|
D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1n);
|
|
}
|
|
for (let x = 0; x < 5; x++) {
|
|
for (let y = 0; y < 5; y++) state[x + 5 * y] ^= D[x];
|
|
}
|
|
// rho + pi
|
|
const B = new Array(25).fill(0n);
|
|
for (let x = 0; x < 5; x++) {
|
|
for (let y = 0; y < 5; y++) {
|
|
B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(state[x + 5 * y], ROT[x + 5 * y]);
|
|
}
|
|
}
|
|
// chi
|
|
for (let x = 0; x < 5; x++) {
|
|
for (let y = 0; y < 5; y++) {
|
|
state[x + 5 * y] = B[x + 5 * y] ^ ((~B[((x + 1) % 5) + 5 * y] & MASK64) & B[((x + 2) % 5) + 5 * y]);
|
|
}
|
|
}
|
|
// iota
|
|
state[0] ^= ROUND_CONSTANTS[round];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* keccak256 over a Uint8Array, returning a 32-byte Uint8Array.
|
|
* @param {Uint8Array} bytes
|
|
* @returns {Uint8Array}
|
|
*/
|
|
export function keccak256(bytes) {
|
|
const RATE = 136; // 1088-bit rate for keccak256
|
|
const state = new Array(25).fill(0n);
|
|
|
|
// pad10*1 with Ethereum/Keccak domain byte 0x01
|
|
const padLen = RATE - (bytes.length % RATE);
|
|
const padded = new Uint8Array(bytes.length + padLen);
|
|
padded.set(bytes, 0);
|
|
padded[bytes.length] |= 0x01;
|
|
padded[padded.length - 1] |= 0x80;
|
|
|
|
for (let off = 0; off < padded.length; off += RATE) {
|
|
for (let i = 0; i < RATE; i++) {
|
|
const lane = i >> 3;
|
|
const shift = BigInt((i & 7) * 8);
|
|
state[lane] ^= BigInt(padded[off + i]) << shift;
|
|
}
|
|
keccakF(state);
|
|
}
|
|
|
|
const out = new Uint8Array(32);
|
|
for (let i = 0; i < 32; i++) {
|
|
const lane = i >> 3;
|
|
const shift = BigInt((i & 7) * 8);
|
|
out[i] = Number((state[lane] >> shift) & 0xffn);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ── small hex helpers (no dependency on ethers) ──────────────────────────────
|
|
|
|
/** Convert a hex string (with or without 0x) to a Uint8Array. */
|
|
export function hexToBytes(hex) {
|
|
let h = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
|
|
if (h.length % 2 !== 0) h = '0' + h;
|
|
const out = new Uint8Array(h.length / 2);
|
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.substr(i * 2, 2), 16);
|
|
return out;
|
|
}
|
|
|
|
/** Convert a Uint8Array to a 0x-prefixed hex string. */
|
|
export function bytesToHex(bytes) {
|
|
let s = '0x';
|
|
for (const b of bytes) s += b.toString(16).padStart(2, '0');
|
|
return s;
|
|
}
|
|
|
|
/** keccak256 of a hex string input, returning a 0x-prefixed 32-byte hex string. */
|
|
export function keccak256Hex(hexInput) {
|
|
return bytesToHex(keccak256(hexToBytes(hexInput)));
|
|
}
|
|
|
|
/**
|
|
* EIP-55 mixed-case checksum of a 20-byte address hex string.
|
|
* @param {string} address 0x-prefixed 20-byte address (any case)
|
|
* @returns {string} checksummed address
|
|
*/
|
|
export function toChecksumAddress(address) {
|
|
const addr = (address.startsWith('0x') ? address.slice(2) : address).toLowerCase();
|
|
const hashBytes = keccak256(new TextEncoder().encode(addr));
|
|
// hex of the hash, one nibble per address char
|
|
let hashHex = '';
|
|
for (const b of hashBytes) hashHex += b.toString(16).padStart(2, '0');
|
|
let out = '0x';
|
|
for (let i = 0; i < addr.length; i++) {
|
|
const c = addr[i];
|
|
if (c >= '0' && c <= '9') out += c;
|
|
else out += parseInt(hashHex[i], 16) >= 8 ? c.toUpperCase() : c;
|
|
}
|
|
return out;
|
|
}
|