// 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; }