UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

164 lines (147 loc) 7.78 kB
/** * # crypto/sha256 — the one portable, synchronous sha256 the runtime hashes with (kestrel-alw.17) * * Content-addressing is woven through the whole grade path as **pure, synchronous** functions: * {@link ../grade/receipt.ts createReceipt}, {@link ../blotter/project.ts project}, the {@link ../ledger * ledger}'s `deriveRunId`, and the sim driver's bus hash all take a sha256 of a canonical string and must * return it inline — no `await`. Bun's `Bun.CryptoHasher` gives exactly that (a synchronous `.digest`), but * it is a **Bun-only** global: it does not exist in a Cloudflare Worker isolate, which is where the grade * path must also run (ADR-0006, EPIC kestrel-markets-alw). WebCrypto (`crypto.subtle.digest`) IS present in * a Worker but is **async**, and threading `await` through every content-address would break the * pure-function doctrine the determinism check relies on (RUNTIME §0). * * The resolution of that sync-vs-async tension is this module: ONE synchronous `sha256(text)` that is * portable across environments. It uses Bun's native hasher when the `Bun` global is present (the fast path * under `bun test` / the CLI) and a self-contained pure-JS SHA-256 otherwise (the Worker isolate). Both code * paths produce **byte-identical** output — a determinism guarantee locked down by a parity test that runs * BOTH implementations over the same inputs ({@link sha256Pure} is exported so the Worker path is directly * testable even under Bun). No wall clock, no RNG. */ /** The narrow shape of Bun's native `CryptoHasher` this module uses: a fluent `update` (returns the * hasher so `.update(…).digest(…)` chains) and a hex `digest`. */ interface BunCryptoHasher { update(input: string): BunCryptoHasher; digest(enc: "hex"): string; } /** Bun's native `CryptoHasher` when the `Bun` global exists (fast path); `undefined` in a Worker isolate. */ const BUN_CRYPTO_HASHER: undefined | (new (algo: string) => BunCryptoHasher) = (globalThis as { Bun?: { CryptoHasher?: new (algo: string) => BunCryptoHasher } }).Bun?.CryptoHasher; // ───────────────────────────────────────────────────────────────────────────── // Pure-JS SHA-256 (the Worker-portable path) // ───────────────────────────────────────────────────────────────────────────── /** The SHA-256 round constants (first 32 bits of the fractional parts of the cube roots of the first 64 * primes) — FIPS 180-4. */ 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, ]); /** 32-bit right rotate. */ function rotr(x: number, n: number): number { return (x >>> n) | (x << (32 - n)); } /** * Pure-JS SHA-256 over the UTF-8 bytes of `text` → a 64-char lowercase hex digest (FIPS 180-4). This is the * Worker-portable path (no `Bun` global, no async `crypto.subtle`). Exported so the parity test can exercise * it directly even under Bun, proving it matches the native hasher byte-for-byte. */ export function sha256Pure(text: string): string { const msg = new TextEncoder().encode(text); const bitLen = msg.length * 8; // Pad: append 0x80, then zeros up to 56 mod 64, then the 64-bit big-endian bit length. const withPad = new Uint8Array(((msg.length + 8) >> 6) * 64 + 64); withPad.set(msg); withPad[msg.length] = 0x80; // 64-bit length; JS bit ops are 32-bit, so write the two 32-bit halves. const hi = Math.floor(bitLen / 0x100000000); const lo = bitLen >>> 0; const lenOff = withPad.length - 8; withPad[lenOff] = (hi >>> 24) & 0xff; withPad[lenOff + 1] = (hi >>> 16) & 0xff; withPad[lenOff + 2] = (hi >>> 8) & 0xff; withPad[lenOff + 3] = hi & 0xff; withPad[lenOff + 4] = (lo >>> 24) & 0xff; withPad[lenOff + 5] = (lo >>> 16) & 0xff; withPad[lenOff + 6] = (lo >>> 8) & 0xff; withPad[lenOff + 7] = lo & 0xff; // Initial hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes). 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 off = 0; off < withPad.length; off += 64) { for (let i = 0; i < 16; i++) { const j = off + i * 4; w[i] = ((withPad[j]! << 24) | (withPad[j + 1]! << 16) | (withPad[j + 2]! << 8) | withPad[j + 3]!) >>> 0; } for (let i = 16; i < 64; i++) { const s0 = rotr(w[i - 15]!, 7) ^ rotr(w[i - 15]!, 18) ^ (w[i - 15]! >>> 3); const s1 = rotr(w[i - 2]!, 17) ^ rotr(w[i - 2]!, 19) ^ (w[i - 2]! >>> 10); w[i] = (w[i - 16]! + s0 + w[i - 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 i = 0; i < 64; i++) { const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25); const ch = (e & f) ^ (~e & g); const t1 = (h + S1 + ch + K[i]! + w[i]!) >>> 0; const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22); const maj = (a & b) ^ (a & c) ^ (b & c); const t2 = (S0 + maj) >>> 0; h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 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; } return ( hex32(h0) + hex32(h1) + hex32(h2) + hex32(h3) + hex32(h4) + hex32(h5) + hex32(h6) + hex32(h7) ); } /** A 32-bit word as 8 lowercase hex chars. */ function hex32(x: number): string { return (x >>> 0).toString(16).padStart(8, "0"); } // ───────────────────────────────────────────────────────────────────────────── // The one portable sha256 the runtime hashes with // ───────────────────────────────────────────────────────────────────────────── /** * The portable, synchronous sha256 of a UTF-8 string → 64-char lowercase hex. Uses Bun's native * `CryptoHasher` when the `Bun` global is present (fast path under `bun test` / the CLI) and the pure-JS * {@link sha256Pure} otherwise (a Cloudflare Worker isolate). Both paths are byte-identical (parity test). * Deterministic — no wall clock, no RNG. */ export function sha256(text: string): string { if (BUN_CRYPTO_HASHER !== undefined) { return new BUN_CRYPTO_HASHER("sha256").update(text).digest("hex"); } return sha256Pure(text); }