/* SPDX-License-Identifier: MIT — see /clients/LICENSE. Copy me freely. */
/* kaspulse.mjs — a tiny zero-dependency VERIFYING client for the kaspulse oracle.
   Works in Node 18+ and the browser (native fetch + BigInt). No npm deps, no keys.

     import { Kaspulse } from './kaspulse.mjs';
     const k = new Kaspulse('https://pulse.kascov.io');
     const feed = await k.feed('KAS/USD');
     const pin = loadPreviouslyAuthenticatedCommittee(); // not from this feed origin
     const r = k.verifyWithCommittee(feed, pin); // pinned threshold, both domains
     if (!r.ok) throw new Error(r.error || 'untrusted feed');
     const c = k.verifyCovenant(feed);  // the BOUND kaspulse/cov/v2 covenant preimage
     const px = k.checkedValue(feed);   // verified + fresh + not halted, or throws

   L1 KCC20 (Kaspa L1 AMM pools, priced through kascov) is first-class:

     const rows = await k.kcc20Feeds();          // the kind:"kcc20-pool" catalog rows
     const info = k.kcc20Info(feed);             // basis, last_trade_age_s, anchor_txid,
                                                 // unpriced_reason, gates, depth — flattened
     const r    = await k.reDeriveKcc20(feed);   // re-fetch kascov and demand the exact
                                                 // integer rational back (network call)
     const cen  = await k.kcc20Census();         // every market kaspulse REFUSED, with the reason

   Honest scope, in four parts. (1) verifyFeed checks the committee's
   signatures, the binding of the signed message to the JSON fields, and the
   safety flags (`halted`, `peg_ok`) — offline. It does NOT check FRESHNESS:
   nothing here compares `signed_ts` to a clock. That is checkedValue's job, and
   the sentence used to claim otherwise. (2) It does NOT re-fetch the exchanges and
   recompute the majors' median; verify exchange quotes separately if needed. (3)
   reDeriveKcc20 DOES re-derive an L1 price from source — but from kascov, so
   it is a cross-check, not independence; and it does not replay the admission
   gates. Both limits are spelled out at the KCC20 core block below. (4)
   verifyCovenant REBUILDS the bound `kaspulse/cov/v2` covenant preimage from
   the feed's own fields and demands the published bytes back exactly — the
   check that would have caught the unbound domain withdrawn on 2026-07-27.
   verifyFeed folds it in, so a feed whose covenant does not rebuild NEVER
   comes back ok:true.

   Both cores self-test at load — the crypto core against BIP340 official
   vector 0, a corrupted copy that must FAIL, and blake2b-256("abc"); the KCC20
   core against pinned mainnet tier fixtures. On any failure the matching entry
   point refuses rather than risk a fake ✓.

   CLI:  node kaspulse.mjs verify KAS/USD [base]
         node kaspulse.mjs kcc20 [base]      # re-derive every L1 KCC20 price

   Publishing to npm is a separate decision — this file is the whole client. */

// CRYPTO-CORE-BEGIN
// ── shared crypto core ──────────────────────────────────────────────────────
// The public JavaScript client and browser verifier share this exact block.
// Release checks require byte-for-byte parity; the implementation is
// dependency-free so either artifact can verify the published API responses.

// bytes / hex helpers (lowercase-hex canonical, tolerant of uppercase input)
function hexToBytes(hex) {
  if (typeof hex !== 'string' || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;
  const out = new Uint8Array(hex.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
  return out;
}
function bytesToHex(bytes) {
  let s = '';
  for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0');
  return s;
}
function asciiBytes(s) { return new TextEncoder().encode(s); }
function bytesToBig(bytes) {
  let v = 0n;
  for (let i = 0; i < bytes.length; i++) v = (v << 8n) | BigInt(bytes[i]);
  return v;
}
function concatBytes(...parts) {
  let n = 0;
  for (const p of parts) n += p.length;
  const out = new Uint8Array(n);
  let o = 0;
  for (const p of parts) { out.set(p, o); o += p.length; }
  return out;
}

// ── blake2b-256 — pure BigInt, unkeyed, 32-byte digest ──────────────────────
// (adapted from kascov's proven web/blake2b.js — same author, same vectors)
const B2B_IV = [
  0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n,
  0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n,
];
const B2B_SIGMA = [
  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
  [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
  [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
  [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
  [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
  [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
  [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
  [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
  [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
];
const B2B_M64 = (1n << 64n) - 1n;
function b2bRotr(x, n) { return ((x >> n) | (x << (64n - n))) & B2B_M64; }
function b2bCompress(h, block, t, last) {
  const m = new Array(16);
  for (let i = 0; i < 16; i++) {
    let w = 0n;
    for (let j = 7; j >= 0; j--) w = (w << 8n) | BigInt(block[i * 8 + j]);
    m[i] = w;
  }
  const v = h.concat(B2B_IV.slice());
  v[12] ^= BigInt(t) & B2B_M64;
  if (last) v[14] ^= B2B_M64;
  const G = (a, b, c, d, x, y) => {
    v[a] = (v[a] + v[b] + x) & B2B_M64; v[d] = b2bRotr(v[d] ^ v[a], 32n);
    v[c] = (v[c] + v[d]) & B2B_M64;     v[b] = b2bRotr(v[b] ^ v[c], 24n);
    v[a] = (v[a] + v[b] + y) & B2B_M64; v[d] = b2bRotr(v[d] ^ v[a], 16n);
    v[c] = (v[c] + v[d]) & B2B_M64;     v[b] = b2bRotr(v[b] ^ v[c], 63n);
  };
  for (let r = 0; r < 12; r++) {
    const s = B2B_SIGMA[r];
    G(0, 4, 8, 12, m[s[0]], m[s[1]]);  G(1, 5, 9, 13, m[s[2]], m[s[3]]);
    G(2, 6, 10, 14, m[s[4]], m[s[5]]); G(3, 7, 11, 15, m[s[6]], m[s[7]]);
    G(0, 5, 10, 15, m[s[8]], m[s[9]]); G(1, 6, 11, 12, m[s[10]], m[s[11]]);
    G(2, 7, 8, 13, m[s[12]], m[s[13]]); G(3, 4, 9, 14, m[s[14]], m[s[15]]);
  }
  for (let i = 0; i < 8; i++) h[i] = (h[i] ^ v[i] ^ v[i + 8]) & B2B_M64;
}
/* blake2b-256(bytes) -> Uint8Array(32); unkeyed, no salt/personalization */
function blake2b256(input) {
  const h = B2B_IV.slice();
  h[0] ^= 0x01010000n ^ 32n; // digest_length=32, fanout=1, depth=1
  let t = 0;
  let i = 0;
  // full blocks except the last (the final block is always compressed with last=true)
  while (input.length - i > 128) {
    t += 128;
    b2bCompress(h, input.subarray(i, i + 128), t, false);
    i += 128;
  }
  const block = new Uint8Array(128);
  block.set(input.subarray(i));
  t += input.length - i;
  b2bCompress(h, block, t, true);
  const out = new Uint8Array(32);
  for (let k = 0; k < 4; k++) {
    let w = h[k];
    for (let j = 0; j < 8; j++) { out[k * 8 + j] = Number(w & 0xffn); w >>= 8n; }
  }
  return out;
}

// ── sha256 — sync pure JS, needed only for BIP340's tagged hashes ───────────
// (sync avoids crypto.subtle's async/secure-context constraints)
const SHA_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 sha256(data) {
  const bitLen = data.length * 8;
  const padded = new Uint8Array((((data.length + 8) >> 6) << 6) + 64);
  padded.set(data);
  padded[data.length] = 0x80;
  const dv = new DataView(padded.buffer);
  dv.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
  dv.setUint32(padded.length - 4, bitLen >>> 0);
  const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
  const w = new Uint32Array(64);
  const rotr = (x, n) => (x >>> n) | (x << (32 - n));
  for (let i = 0; i < padded.length; i += 64) {
    for (let j = 0; j < 16; j++) w[j] = dv.getUint32(i + j * 4);
    for (let j = 16; j < 64; j++) {
      const s0 = rotr(w[j - 15], 7) ^ rotr(w[j - 15], 18) ^ (w[j - 15] >>> 3);
      const s1 = rotr(w[j - 2], 17) ^ rotr(w[j - 2], 19) ^ (w[j - 2] >>> 10);
      w[j] = (w[j - 16] + s0 + w[j - 7] + s1) >>> 0;
    }
    let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
    for (let j = 0; j < 64; j++) {
      const t1 = (h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + SHA_K[j] + w[j]) >>> 0;
      const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
      h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
    }
    H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
    H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
  }
  const out = new Uint8Array(32);
  const ov = new DataView(out.buffer);
  for (let j = 0; j < 8; j++) ov.setUint32(j * 4, H[j]);
  return out;
}
function taggedHash(tag, ...chunks) {
  const t = sha256(asciiBytes(tag));
  return sha256(concatBytes(t, t, ...chunks));
}

// ── BIP340 Schnorr verification over secp256k1 — BigInt, verify-only ────────
const SECP_P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
const SECP_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const SECP_G = [
  0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
  0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
];
function fmod(a) { const r = a % SECP_P; return r < 0n ? r + SECP_P : r; }
function modInv(a, m) {
  let r0 = ((a % m) + m) % m, r1 = m, s0 = 1n, s1 = 0n;
  while (r1 !== 0n) {
    const q = r0 / r1;
    [r0, r1] = [r1, r0 - q * r1];
    [s0, s1] = [s1, s0 - q * s1];
  }
  return ((s0 % m) + m) % m;
}
function modPow(base, exp, m) {
  let b = ((base % m) + m) % m, e = exp, r = 1n;
  while (e > 0n) {
    if (e & 1n) r = (r * b) % m;
    b = (b * b) % m;
    e >>= 1n;
  }
  return r;
}
// affine points as [x, y]; null = point at infinity
function pointAdd(p, q) {
  if (p === null) return q;
  if (q === null) return p;
  const [x1, y1] = p, [x2, y2] = q;
  let lam;
  if (x1 === x2) {
    if (fmod(y1 + y2) === 0n) return null; // P + (−P)
    lam = fmod(3n * x1 * x1 * modInv(2n * y1, SECP_P)); // double
  } else {
    lam = fmod((y2 - y1) * modInv(x2 - x1, SECP_P));
  }
  const x3 = fmod(lam * lam - x1 - x2);
  return [x3, fmod(lam * (x1 - x3) - y1)];
}
// double-and-add scalar multiplication (verification of public data — no
// side-channel concern)
function pointMul(p, k) {
  let r = null, a = p, e = k;
  while (e > 0n) {
    if (e & 1n) r = pointAdd(r, a);
    a = pointAdd(a, a);
    e >>= 1n;
  }
  return r;
}
// BIP340 lift_x: the curve point with this x and EVEN y, or null
function liftX(x) {
  if (x >= SECP_P) return null;
  const c = fmod(x * x * x + 7n);
  const y = modPow(c, (SECP_P + 1n) / 4n, SECP_P);
  if ((y * y) % SECP_P !== c) return null;
  return [x, (y & 1n) === 0n ? y : SECP_P - y];
}
/* Standard BIP340 verification: pubkey32 = x-only key, msg32 = 32-byte message
   (for kaspulse: the blake2b-256 digest — NOT hashed again outside BIP340's own
   tagged hashing), sig64 = r||s. Returns bool, never throws. */
function bip340Verify(pubkey32, msg32, sig64) {
  if (!(pubkey32 instanceof Uint8Array) || pubkey32.length !== 32) return false;
  if (!(msg32 instanceof Uint8Array) || msg32.length !== 32) return false;
  if (!(sig64 instanceof Uint8Array) || sig64.length !== 64) return false;
  const P = liftX(bytesToBig(pubkey32));
  if (P === null) return false;
  const r = bytesToBig(sig64.subarray(0, 32));
  const s = bytesToBig(sig64.subarray(32));
  if (r >= SECP_P || s >= SECP_N) return false;
  const e = bytesToBig(taggedHash('BIP0340/challenge', sig64.subarray(0, 32), pubkey32, msg32)) % SECP_N;
  const R = pointAdd(pointMul(SECP_G, s), pointMul(P, SECP_N - e)); // s·G − e·P
  if (R === null || (R[1] & 1n) !== 0n || R[0] !== r) return false;
  return true;
}

// ── the kaspulse/v2 signed message and the feed verdict ─────────────────────
/* message = "kaspulse/v2|PAIR|mant|expo|ts|round" (ASCII, decimal integers,
   expo may be negative). Returns the five fields AS STRINGS (no float
   round-trips) or null if the shape is wrong. */
function parseSignedMessage(message) {
  if (typeof message !== 'string') return null;
  const parts = message.split('|');
  if (parts.length !== 6 || parts[0] !== 'kaspulse/v2') return null;
  const [, pair, mant, expo, ts, round] = parts;
  if (!pair || !/^\d+$/.test(mant) || !/^-?\d+$/.test(expo) || !/^\d+$/.test(ts) || !/^\d+$/.test(round)) return null;
  return { pair, mant, expo, ts, round };
}
/* The FIVE-field binding, factored out so the self-test can drive it directly.
   `round` is on the list because it used to be parsed and DROPPED here: a lying
   or compromised API could serve any signed_round it liked and this verifier
   still returned bound:true, while the server-side verifier rejected the identical feed.
   The covenant rebuild does not cover that gap for a feed publishing no
   covenant — which is every archived feed. A verifier that silently stops
   comparing a field is the drift `runCoreSelfTest` now catches. */
function messageBinds(parsed, feed) {
  // string-compare the integers — no float round-trips
  return parsed !== null
    && parsed.pair === feed.pair
    && parsed.mant === String(feed.mant)
    && parsed.expo === String(feed.expo)
    && parsed.ts === String(feed.signed_ts)
    && parsed.round === String(feed.signed_round);
}

/* The MESSAGE half of the verdict, on its own. Pure sync, no network.
   VALID := (count of BIP340-verifying signatures ≥ threshold) AND the signed
   message's PAIR/mant/expo/ts/round equal the JSON's
   pair/mant/expo/signed_ts/signed_round.
   `verifyFeedCore` below wraps this and also folds in the covenant domain — use
   THAT unless you specifically want the message half alone. */
function verifyMessageCore(feed) {
  if (!CORE_SELFTEST.ok) throw new Error('verifier self-test failed');
  if (!feed || typeof feed !== 'object' || typeof feed.message !== 'string') {
    return { ok: false, valid: 0, threshold: 0, bound: false, parsed: null, results: [], error: 'not a feed object (need message/signers/signatures)' };
  }
  const signers = Array.isArray(feed.signers) ? feed.signers : [];
  const sigs = Array.isArray(feed.signatures) ? feed.signatures : [];
  const threshold = Number.isInteger(feed.threshold) && feed.threshold > 0 ? feed.threshold : 0;
  const parsed = parseSignedMessage(feed.message);
  // field binding: what was SIGNED must equal what the JSON claims
  // (string-compare the integers — no float round-trips)
  const bound = messageBinds(parsed, feed);
  const digest = blake2b256(asciiBytes(feed.message));
  const results = [];
  const valid = countDistinctValid(signers, sigs, digest, results);
  const ok = bound && threshold > 0 && valid >= threshold;
  const out = { ok, valid, threshold, bound, parsed, results };
  if (parsed === null) out.error = 'unparsable signed message (want kaspulse/v2|PAIR|mant|expo|ts|round)';
  else if (!bound) out.error = 'signed message fields do not match the JSON fields (pair/mant/expo/signed_ts/signed_round)';
  else if (threshold === 0) out.error = 'missing threshold';
  else if (valid < threshold) out.error = `only ${valid} of ${threshold} required signatures verify`;
  return out;
}

/* Count the signatures that verify over `digest`, crediting each DISTINCT
   signer AT MOST ONCE, and push one {signer, ok, duplicate} row per list entry.

   The dedupe is the point. Counting list ENTRIES let one genuine key, listed
   three times with its one genuine signature, satisfy 3-of-5 — so a single
   compromised committee key plus control of the JSON (the API host, a CDN, an
   untrusted mirror) forged any price and every kaspulse verifier called it
   threshold-signed. The on-chain m-of-n gate was never affected (the redeem
   bakes distinct keys into fixed slots); this was an off-chain-only threshold
   collapse, which is exactly where "3-of-5, verifiable by anyone" lives.
   Deduped on the DECODED 32 bytes, so a re-cased hex string is the same key. */
function countDistinctValid(signers, sigs, digest, results) {
  const seen = new Set();
  let valid = 0;
  for (let i = 0; i < signers.length; i++) {
    const pk = hexToBytes(String(signers[i]));
    const sig = hexToBytes(String(sigs[i] ?? ''));
    const verifies = pk !== null && sig !== null && pk.length === 32 && sig.length === 64 && bip340Verify(pk, digest, sig);
    const key = pk === null ? null : bytesToHex(pk);
    const duplicate = key !== null && seen.has(key);
    const ok = verifies && !duplicate;
    if (key !== null) seen.add(key);
    results.push({ signer: String(signers[i]), ok, duplicate });
    if (ok) valid++;
  }
  return valid;
}

// ── the BOUND covenant domain: kaspulse/cov/v2 ──────────────────────────────
/* WHY THIS EXISTS. Until 2026-07-27 the oracle also signed a COVENANT preimage:
   blake2b(price_bytes(price_e8)) — a bare integer with no pair, no exponent, no
   round and no timestamp. Two live consequences, both real:

     * BTC/USD's five committee signatures unlocked ANY on-chain price gate with
       a lower strike, on ANY pair. The signatures were genuine; they just did
       not say WHAT they were about.
     * three feeds quantized to price_e8 = 0 and published real signatures over
       blake2b(empty), which satisfies every "at or below" gate forever.

   The field was withdrawn and is back only under `kaspulse/cov/v2`: ONE blob,

     "kaspulse/cov/v2"        15 bytes ASCII      0..15  ┐
     blake2b256(PAIR)[0..8]    8 bytes           15..23  ├ bound prefix, baked
     expo                      1 byte  (i8)      23..24  ┘ into the P2SH script
     round                     8 bytes (u64 BE)  24..32
     ts                        8 bytes (u64 BE)  32..40
     mant                      minimal-LE script number, variable, 40..end

   signed as schnorr(blake2b256(blob)).

   THE CHECK BELOW IS THE ONE THAT WOULD HAVE CAUGHT THE ORIGINAL HOLE. It never
   parses the oracle's `covenant.preimage` and trusts what it finds — it REBUILDS
   the blob from the feed's own pair/mant/expo/signed_round/signed_ts and demands
   the published bytes back EXACTLY. A cross-pair blob, a shifted exponent, a
   replayed round, a truncated tail: all of them are a different byte string, and
   a different byte string is a hard FAIL before a single signature is checked.
   Only then are the signatures verified — against the digest of OUR blob. */
const COV_V2_TAG = 'kaspulse/cov/v2';   // exactly 15 ASCII bytes
const COV_V2_PREFIX_LEN = 24;           // tag ‖ pair-hash ‖ expo
const COV_V2_MANT_OFF = 40;             // ‖ round_be ‖ ts_be, then the mantissa

/* blake2b256(PAIR)[0..8] — the pair binding. The raw pair string is hashed
   ("KAS/USD"), not a domain-prefixed one: the 15-byte tag in front already
   separates this domain from the bond record's. */
function covV2PairId(pair) { return blake2b256(asciiBytes(pair)).subarray(0, 8); }
/* Minimal little-endian script-number encoding of a POSITIVE mantissa — the
   variable-length tail, and the operand the on-chain script compares to the
   strike. A trailing byte with the top bit set would read as NEGATIVE on the
   VM, so it gets a zero sign byte. */
function covV2MantBytes(mant) {
  const out = [];
  let v = mant;
  while (v > 0n) { out.push(Number(v & 0xffn)); v >>= 8n; }
  if (out.length && (out[out.length - 1] & 0x80)) out.push(0);
  return new Uint8Array(out);
}
function covV2Be8(v) {
  const out = new Uint8Array(8);
  let x = v;
  for (let i = 7; i >= 0; i--) { out[i] = Number(x & 0xffn); x >>= 8n; }
  return out;
}
/* Strict decimal → BigInt. String-parsed on purpose: a JSON number that arrived
   as 1.7976931348623157e308 or 2**53+1 must not silently become an integer. */
function covV2Uint(v) { const s = String(v); return /^\d+$/.test(s) ? BigInt(s) : null; }
function covV2Int(v) { const s = String(v); return /^-?\d+$/.test(s) ? BigInt(s) : null; }

/* Build the bound preimage from raw fields. Returns Uint8Array, or null when
   the fields cannot form a LEGAL cov/v2 blob — which is itself a verdict:

     * expo must fit one signed byte (live feeds sit in -12..=3);
     * mant must be NON-ZERO (a zero mantissa is an empty tail, which the VM
       reads as numeric zero — the blake2b(empty) hole, one layer down);
     * mant must fit a signed 8-byte script number, or the on-chain compare
       would abort;
     * round and ts must fit u64. */
function covV2Preimage(pair, mant, expo, round, ts) {
  if (typeof pair !== 'string' || pair.length === 0) return null;
  const m = covV2Uint(mant), e = covV2Int(expo), r = covV2Uint(round), t = covV2Uint(ts);
  if (m === null || e === null || r === null || t === null) return null;
  if (e < -128n || e > 127n) return null;
  if (m === 0n || m > 0x7fffffffffffffffn) return null;
  const u64max = 0xffffffffffffffffn;
  if (r > u64max || t > u64max) return null;
  const prefix = new Uint8Array(COV_V2_PREFIX_LEN);
  prefix.set(asciiBytes(COV_V2_TAG), 0);
  prefix.set(covV2PairId(pair), 15);
  prefix[COV_V2_PREFIX_LEN - 1] = Number(e & 0xffn); // i8 two's complement
  return concatBytes(prefix, covV2Be8(r), covV2Be8(t), covV2MantBytes(m));
}

/* Verify a feed's cov/v2 covenant attestation. Pure sync, no network.

   Returns {present, ok, valid, threshold, matches, expected, published,
            results:[{signer, ok}]} (+ error).

   `present:false` means the feed simply carries no covenant attestation — that
   is NOT a failure, and a UI should say "no covenant" rather than ✗. Every
   other false is a real refusal. `matches` is the byte-for-byte verdict on the
   preimage and is reported separately from the signature count on purpose: a
   feed can carry five perfectly valid signatures over the WRONG blob, and that
   is exactly the shape the withdrawn domain had. */
function verifyCovenantCore(feed) {
  if (!CORE_SELFTEST.ok) throw new Error('verifier self-test failed');
  const out = { present: false, ok: false, valid: 0, threshold: 0, matches: false,
                expected: null, published: null, results: [] };
  if (!feed || typeof feed !== 'object') { out.error = 'not a feed object'; return out; }
  const cov = feed.covenant && typeof feed.covenant === 'object' ? feed.covenant : null;
  const published = cov && typeof cov.preimage === 'string' ? cov.preimage.toLowerCase() : '';
  const sigs = cov && Array.isArray(cov.signatures) ? cov.signatures : [];
  if (!cov || (published === '' && sigs.length === 0)) {
    out.error = 'feed carries no kaspulse/cov/v2 covenant attestation';
    return out;
  }
  out.present = true;
  out.published = published || null;
  // (a) the v2 MESSAGE must verify first. A covenant ✓ on a feed whose own
  //     signatures do not verify would be a green tick over nothing.
  const base = verifyMessageCore(feed);
  if (!base.ok) { out.error = 'feed signatures/binding failed first: ' + (base.error || 'invalid'); return out; }
  // (b) signatures with no preimage = a pre-withdrawal artifact from the
  //     unbound blake2b(price_bytes) domain. Never verify those.
  if (published === '') {
    out.error = 'covenant.signatures with NO covenant.preimage — pre-withdrawal artifact of the unbound blake2b(price_bytes) domain; refusing';
    return out;
  }
  // (c) REBUILD, then compare bytes. Nothing here reads the published blob's
  //     own fields — that would be asking the document to grade itself.
  const pre = covV2Preimage(feed.pair, feed.mant, feed.expo, feed.signed_round, feed.signed_ts);
  if (pre === null) {
    out.error = "this feed's own fields cannot form a legal cov/v2 preimage (expo outside i8, zero or oversized mant, or non-integer round/ts)";
    return out;
  }
  out.expected = bytesToHex(pre);
  out.matches = out.expected === published;
  if (!out.matches) {
    // the two blobs are in .expected / .published — the caller renders them; an
    // error string carrying 88 hex chars twice is unreadable in every UI there is
    out.error = 'COVENANT PREIMAGE MISMATCH — the published blob is not this feed\'s '
      + '(cross-pair, wrong-expo or replayed-round signature reuse). Do NOT fund a covenant against it.';
    return out;
  }
  // (d) threshold of committee signatures over blake2b256(OUR blob).
  const threshold = Number.isInteger(feed.threshold) && feed.threshold > 0 ? feed.threshold : 0;
  out.threshold = threshold;
  const digest = blake2b256(pre);
  const signers = Array.isArray(feed.signers) ? feed.signers : [];
  out.valid = countDistinctValid(signers, sigs, digest, out.results);
  const valid = out.valid;
  if (threshold === 0) { out.error = 'missing threshold'; return out; }
  if (valid < threshold) { out.error = `covenant: only ${valid} of ${threshold} required signatures verify over blake2b256(preimage)`; return out; }
  out.ok = true;
  return out;
}

/* THE VERDICT the CLI, the client and the browser button all read.

   ok := the v2 message verifies AND field-binds, AND the feed's own safety
   flags are clear (`halted`, `peg_ok`), AND (the feed carries no covenant
   attestation OR that attestation rebuilds byte-for-byte and carries a
   threshold of signatures).

   THE SAFETY FLAGS ARE PART OF THE VERDICT. They were not until 2026-08-25, and
   the consequence was that a HALTED feed — the circuit breaker doing its job,
   holding a stale price — came back ok:true with a green ✓ and a price in this
   client, in the Python client and on the web verify button, while the
   server-side verifiers refused it. Identical bytes produced inconsistent
   verdicts; all public verification surfaces now enforce the safety flags.

   `halted` is a REFUSAL, not an accusation: the signatures are fine and the
   committee is honest, the price is simply not safe to consume. So `halted` and
   `depegged` are set on the result as their own flags, and every renderer must
   show that third state separately from an invalid signature.

   The covenant clause is strictly ADDITIVE — a feed with no covenant is
   unaffected, which is every feed published between the 2026-07-27 withdrawal
   and the cov/v2 ship. It can only turn a ✓ into a ✗, never the reverse. That
   direction is deliberate: a feed that publishes a covenant preimage which is
   not its own is forged or compromised, and NOTHING about it should show a
   green tick. The message-only verdict is still available as `.covenant`'s
   sibling fields and via verifyCovenant(). */
function verifyFeedCore(feed) {
  const r = verifyMessageCore(feed);
  const c = verifyCovenantCore(feed);
  r.covenant = c;
  if (c.present && !c.ok) {
    r.ok = false;
    r.error = 'covenant domain (kaspulse/cov/v2) FAILED: ' + (c.error || 'invalid');
  }
  // the safety flags, last, so their message is the one a caller sees: the
  // signatures may be perfect and the price still not safe to consume.
  r.halted = feed && feed.halted === true;
  r.depegged = feed && feed.peg_ok === false;
  if (r.halted || r.depegged) {
    r.ok = false;
    r.error = r.halted ? 'feed halted (circuit breaker)' : 'chain depegged (peg_ok=false)';
  }
  return r;
}

// ── mandatory self-test: refuse to verify anything with a broken core ───────
function runCoreSelfTest() {
  try {
    const abc = bytesToHex(blake2b256(asciiBytes('abc')));
    if (abc !== 'bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319') {
      return { ok: false, detail: 'blake2b-256("abc") known-answer mismatch' };
    }
    // BIP340 official test vector 0
    const pk = hexToBytes('f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9');
    const msg = new Uint8Array(32);
    const sig = hexToBytes('e907831f80848d1069a5371b402410364bdf1c5f8307b0084c55f1ce2dca821525f66a4a85ea8b71e482a74f382d2ce5ebeee8fdb2172f477df4900d310536c0');
    if (!bip340Verify(pk, msg, sig)) return { ok: false, detail: 'BIP340 test vector 0 did not verify' };
    const bad = sig.slice();
    bad[63] ^= 0x01;
    if (bip340Verify(pk, msg, bad)) return { ok: false, detail: 'corrupted BIP340 signature verified (core is broken)' };
    // cov/v2 known-answer: KAS/USD, mant 290000000, expo -10, round 4242,
    // ts 1756000000. Pinned hex — this is the byte layout the on-chain script
    // slices with OpSubstr, so a builder that drifts by one byte must not be
    // allowed to print a ✓ anywhere.
    const covV = bytesToHex(covV2Preimage('KAS/USD', 290000000, -10, 4242, 1756000000) || new Uint8Array());
    if (covV !== '6b617370756c73652f636f762f7632b84ad8389aa2ebb0f600000000000010920000000068aa6f00800c4911') {
      return { ok: false, detail: 'cov/v2 preimage known-answer mismatch (got ' + covV + ')' };
    }
    // the field binding must cover ALL FIVE signed fields. Each candidate below
    // differs from the reference in exactly one of them and must fail to bind.
    const bref = { pair: 'KAS/USD', mant: 290000000, expo: -10, signed_ts: 1756000000, signed_round: 4242 };
    const bmsg = parseSignedMessage('kaspulse/v2|KAS/USD|290000000|-10|1756000000|4242');
    if (!messageBinds(bmsg, bref)) return { ok: false, detail: 'field binding rejected a matching feed' };
    for (const k of ['pair', 'mant', 'expo', 'signed_ts', 'signed_round']) {
      const bad = Object.assign({}, bref);
      bad[k] = k === 'pair' ? 'BTC/USD' : bref[k] + 1;
      if (messageBinds(bmsg, bad)) return { ok: false, detail: 'field binding ignores ' + k };
    }
    // the two swaps that broke the OLD domain must change the BYTES, or the
    // byte-for-byte check below is decoration
    const covPair = bytesToHex(covV2Preimage('BTC/USD', 290000000, -10, 4242, 1756000000) || new Uint8Array());
    const covExpo = bytesToHex(covV2Preimage('KAS/USD', 290000000, -9, 4242, 1756000000) || new Uint8Array());
    if (covPair === covV || covExpo === covV) return { ok: false, detail: 'cov/v2 preimage does not bind pair and/or expo' };
    // and the shapes that must refuse to build at all
    if (covV2Preimage('KAS/USD', 0, -10, 4242, 1756000000) !== null) return { ok: false, detail: 'cov/v2 built a ZERO mantissa (the blake2b(empty) hole)' };
    if (covV2Preimage('KAS/USD', 290000000, 200, 4242, 1756000000) !== null) return { ok: false, detail: 'cov/v2 built an expo that does not fit one byte' };
    // ONE key, listed THREE times with its one genuine signature, must never
    // count as three. This is the off-chain threshold collapse: a single
    // compromised committee key plus control of the JSON forges any price. The
    // dedupe is one Set in countDistinctValid and is exactly the kind of line
    // that gets refactored away, so it is pinned here at load time.
    const dupSigners = [bytesToHex(pk), bytesToHex(pk), bytesToHex(pk)];
    const dupSigs = [bytesToHex(sig), bytesToHex(sig), bytesToHex(sig)];
    const dupN = countDistinctValid(dupSigners, dupSigs, msg, []);
    if (dupN !== 1) return { ok: false, detail: 'one signer listed 3x counted as ' + dupN + ' (threshold collapse)' };
    // and hex case must not mint a second signer
    const casedN = countDistinctValid([bytesToHex(pk), bytesToHex(pk).toUpperCase()], [bytesToHex(sig), bytesToHex(sig)], msg, []);
    if (casedN !== 1) return { ok: false, detail: 'a re-cased pubkey counted as a second signer' };
    return { ok: true, detail: 'BIP340 vector 0 ✓, corrupted-copy rejected ✓, blake2b-256("abc") ✓, cov/v2 preimage vector ✓, duplicate-signer dedupe ✓' };
  } catch (e) {
    return { ok: false, detail: 'self-test threw: ' + ((e && e.message) || e) };
  }
}
const CORE_SELFTEST = runCoreSelfTest();
function selfTest() { return { ok: CORE_SELFTEST.ok, detail: CORE_SELFTEST.detail }; }
// CRYPTO-CORE-END

// KCC20-CORE-BEGIN
// ── L1 KCC20 re-derivation (Kaspa L1 AMM pools, read from kascov) ───────────
// This block, like the crypto core above, lives byte-for-byte in
// clients/js/kaspulse.mjs (canonical) and web/vendor/verify.js (vendored copy);
// scripts/check-vendored.sh diffs it. Same rules: no dependencies, no exports,
// column 0. `fetch` is the only ambient it uses — present in Node 18+ and every
// browser — and kascov serves `access-control-allow-origin: *`, so the browser
// copy can do the same re-derivation the CLI does.
//
// WHAT AN L1 PRICE IS. A `kcc20-pool` feed carries an EXACT INTEGER RATIONAL:
// `price_num_sompi / price_den`, in sompi per token BASE UNIT. There is no
// verified `decimals` for a KCC20 token — only unverified `claimed_decimals` —
// so nothing here rescales anything, and neither should you.
//
// WHICH TIER PRODUCED IT (the `basis` field), and why it matters:
//   'spot'                — `market.spot_num_sompi / market.spot_den` from
//                           /data/{net}/pools: kascov's gated marginal.
//   'last_verified_state' — kascov WITHHELD spot, because the covenant's live
//                           token balance does not equal the newest verified
//                           trade's after-balance (an anti-donation gate). It
//                           omits both keys with NO error field, so a consumer
//                           that assumes spot is always there silently drops
//                           feeds, and one that substitutes
//                           `reserve / held_by_covenant` publishes a number
//                           measured 22.6% wrong on one live pool. The honest
//                           fallback is the newest NON-carried 1h pool candle
//                           close, which is a verified pool state with a
//                           timestamp.
//
// WHAT reDeriveKcc20 CHECKS: it re-fetches kascov, decides the tier ITSELF
// (never from the feed's own `basis` claim, which would be circular), and
// demands the two integers back EXACTLY — not within a band.
// WHAT IT DOES NOT CHECK: the admission gates (`bracket_holds` /
// `invariant_holds`) are not re-run here. The oracle performs those checks;
// this public client only cross-checks the published rational against kascov.
// Nothing here is independent of kascov: a bug inside kascov reproduces here
// identically. Both limits are printed by the CLI.

const KCC20_NET = 'mainnet';
/* The kascov origin a re-derivation runs against — PINNED HERE, never taken
   from the feed. `price_source_url` is published BY the thing being audited: if
   it also chose the server that answers, a forged price served beside a forged
   kascov reproduces perfectly and this function returns ok:true. So the origin
   is a constant, overridable only by the CALLER (`opts.kascovBase`), and the
   feed's own URL is CHECKED against it below rather than obeyed. The public
   Python client applies the same origin pin. */
const KCC20_PINNED_ORIGIN = 'https://kascov.io';

/* A JSON integer that may arrive as a number or a decimal string -> BigInt|null.
   Values today are far below 2^53; if kascov ever exceeds it, BOTH sides of the
   comparison go through the same JSON.parse and round identically, so the
   verdict stays sound even though the printed digits would not be the wire
   digits. */
function kcc20Int(v) {
  if (typeof v === 'bigint') return v;
  if (typeof v === 'number') return Number.isInteger(v) ? BigInt(v) : null;
  if (typeof v === 'string' && /^-?\d+$/.test(v)) return BigInt(v);
  return null;
}
/* The scheme+host of a URL, without pulling in URL parsing differences. */
function kcc20Origin(url) {
  if (typeof url !== 'string') return null;
  const i = url.indexOf('://');
  if (i < 0) return null;
  const j = url.indexOf('/', i + 3);
  return j < 0 ? url : url.slice(0, j);
}
/* Every L1 provenance field on a FeedObj, in one flat object. Returns null for
   any feed that is not `kind: "kcc20-pool"`. `claimed*` are DEPLOYER-SUPPLIED
   AND UNVERIFIED and must be labelled as such wherever they are shown. */
function kcc20Info(feed) {
  if (!feed || typeof feed !== 'object' || feed.kind !== 'kcc20-pool') return null;
  return {
    pair: feed.pair, leg: feed.leg || null,
    tokenCovenantId: feed.token_covenant_id || null,
    marketCovenantId: feed.market_covenant_id || null,
    displayName: feed.display_name || null,
    listedName: feed.listed_name ?? null,
    listedTicker: feed.listed_ticker ?? null,
    listedDecimals: feed.listed_decimals ?? null,
    listedKnown: feed.listed_known === true,
    listedChecksPassed: feed.listed_checks_passed === true,
    listedListName: feed.listed_list_name ?? null,
    listedArtUrl: feed.listed_art_url ?? null,
    claimedTicker: feed.claimed_ticker ?? null,
    claimedName: feed.claimed_name ?? null,
    claimedDecimals: feed.claimed_decimals ?? null,
    unverifiedNote: 'claimed_* are deployer-supplied and unverified; a listed_* row is BOUND to its covenant (creator key, genesis tx and curve all match the chain) but its name and ticker are the registry\'s labels, not on-chain facts; every signed L1 price remains per token BASE UNIT',
    basis: feed.basis || null,
    basisNote: feed.basis_note || null,
    numSompi: feed.price_num_sompi ?? null,
    den: feed.price_den ?? null,
    verifiedAsOfMs: feed.verified_as_of_ms ?? null,
    lastTradeAgeS: feed.last_trade_age_s ?? null,
    anchorTxid: feed.anchor_txid ?? null,
    anchorDaa: feed.anchor_daa ?? null,
    admitted: feed.admitted ?? null,
    rejectedBracket: feed.rejected_bracket ?? null,
    gateWindow: feed.gate_window ?? null,
    gateNote: feed.gate_note ?? null,
    skeleton: feed.skeleton ?? null,
    invariantOk: feed.invariant_ok ?? null,
    reserveKas: feed.reserve_kas ?? null,
    takerFeeBps: feed.taker_fee_bps ?? null,
    thin: feed.thin ?? null,
    move10pctUsd: feed.move_10pct_usd ?? null,
    depth2pctUsd: feed.depth_2pct_usd ?? null,
    restingAsks: feed.resting_asks ?? null,
    // non-null means the ORACLE ITSELF could not provenance this price — a
    // price you must not consume, not a cosmetic field
    unpricedReason: feed.unpriced_reason ?? null,
    priceSourceUrl: feed.price_source_url || null,
  };
}
/* Which tier applies, decided from kascov's /pools document alone. Pure. */
function kcc20TierOf(poolsDoc, marketCovenantId) {
  const rows = poolsDoc && Array.isArray(poolsDoc.pools) ? poolsDoc.pools : [];
  const row = rows.find((p) => p && p.market_id === marketCovenantId) || null;
  if (!row) return { found: false, basis: null, num: null, den: null, row: null };
  const m = row.market || {};
  const n = kcc20Int(m.spot_num_sompi);
  const d = kcc20Int(m.spot_den);
  if (n !== null && d !== null && n > 0n && d > 0n) return { found: true, basis: 'spot', num: n, den: d, row };
  // spot withheld — silently, by design. Tier 2 needs the candles document.
  return { found: true, basis: 'last_verified_state', num: null, den: null, row };
}
/* Newest NON-carried candle close as {num, den, t}. `carried` rows are gap-fill:
   they repeat the last close forward at zero volume and would make a dead market
   look freshly verified. Pure. */
function kcc20CandleClose(candlesDoc) {
  const cs = candlesDoc && Array.isArray(candlesDoc.candles) ? candlesDoc.candles : [];
  for (let i = cs.length - 1; i >= 0; i--) {
    if (cs[i] && cs[i].carried) continue;
    const close = (cs[i] && cs[i].close) || {};
    const num = kcc20Int(close.quote_sompi);
    const den = kcc20Int(close.base_amount);
    if (num !== null && den !== null && num > 0n && den > 0n) return { num, den, t: cs[i].t ?? null };
    return null; // the newest non-carried candle is unusable — do not walk past it
  }
  return null;
}
async function kcc20Get(url) {
  const res = await fetch(url, { headers: { accept: 'application/json' } });
  if (!res.ok) throw new Error(`kascov: ${url} -> HTTP ${res.status}`);
  return res.json();
}
/* Re-derive one L1 KCC20 feed's price from kascov itself. NETWORK CALL.

     const r = await reDeriveKcc20(feed);
     r.ok        // true only if the tier AND both integers reproduce exactly
     r.checks    // [{name, ok, detail}] — what was compared, and to what
     r.urls      // the exact URLs fetched, so a human can repeat it with curl

   `opts.kascovBase` overrides the origin. The DEFAULT is KCC20_PINNED_ORIGIN,
   not the feed's own `price_source_url` — the audited party does not get to
   choose where it is audited. The feed's URL is checked against the pin.

   `opts.kasUsd` — pass YOUR OWN KAS/USD (from `kaspulse.feed('KAS/USD')`, from
   an exchange, from anywhere you trust) to also check the `/USD` leg. Without
   it only the TOKEN/KAS rational is reproduced, `out.usdChecked` is false, and
   the caller must not report the dollar price as re-derived. */
async function reDeriveKcc20(feed, opts) {
  const info = kcc20Info(feed);
  const out = { ok: false, pair: feed && feed.pair, basis: null, expectedBasis: null, exact: false,
                published: null, rederived: null, price: null, publishedPrice: null, driftPct: null,
                usdChecked: false, checks: [], urls: [], error: null };
  if (!info) { out.error = 'not a kcc20-pool feed'; return out; }
  // same rule as the crypto core: a broken verifier must refuse, never ✓
  if (!KCC20_SELFTEST.ok) {
    out.error = 'kcc20 self-test failed: ' + KCC20_SELFTEST.detail;
    out.checks.push({ name: 'self-test', ok: false, detail: out.error });
    return out;
  }
  out.basis = info.basis;
  if (info.unpricedReason) {
    out.error = `the oracle published this price with no provenance: ${info.unpricedReason}`;
    out.checks.push({ name: 'provenance', ok: false, detail: out.error });
    return out;
  }
  const origin = (opts && opts.kascovBase ? String(opts.kascovBase).replace(/\/+$/, '') : null)
    || KCC20_PINNED_ORIGIN;

  const poolsUrl = `${origin}/data/${KCC20_NET}/pools`;
  out.urls.push(poolsUrl);
  let tier;
  try {
    tier = kcc20TierOf(await kcc20Get(poolsUrl), info.marketCovenantId);
  } catch (e) { out.error = String((e && e.message) || e); return out; }
  if (!tier.found) {
    out.error = `kascov serves no pool with market_id ${info.marketCovenantId}`;
    out.checks.push({ name: 'market', ok: false, detail: out.error });
    return out;
  }
  out.expectedBasis = tier.basis;

  let num = tier.num, den = tier.den;
  if (tier.basis === 'last_verified_state') {
    const cUrl = `${origin}/data/${KCC20_NET}/token/${info.tokenCovenantId}/candles?bucket=1h&phase=pool`;
    out.urls.push(cUrl);
    try {
      const close = kcc20CandleClose(await kcc20Get(cUrl));
      if (close) { num = close.num; den = close.den; }
    } catch (e) { out.error = String((e && e.message) || e); return out; }
  }

  // the source URL must sit on the PINNED origin, must be the one that actually
  // carries this price for this tier, and must name THIS token. The origin half
  // is the load-bearing one: without it the feed picks the server that confirms
  // it. A URL pointing at another token's candles looks plausible and re-derives
  // to a different number, which is the other half.
  const wantsCandles = tier.basis === 'last_verified_state';
  const srcOrigin = kcc20Origin(info.priceSourceUrl);
  const originOk = srcOrigin === origin;
  const urlOk = originOk && typeof info.priceSourceUrl === 'string'
    && (wantsCandles
      ? info.priceSourceUrl.includes('candles') && info.priceSourceUrl.includes(String(info.tokenCovenantId))
      : info.priceSourceUrl.includes('/pools'));
  out.checks.push({ name: 'source url', ok: urlOk,
    detail: String(info.priceSourceUrl) + (originOk ? '' : ` — NOT the pinned origin ${origin}; the feed does not choose where it is checked`) });

  const basisOk = info.basis === tier.basis;
  out.checks.push({ name: 'basis', ok: basisOk,
    detail: `kascov ${tier.basis === 'spot' ? 'publishes' : 'withholds'} spot -> tier is "${tier.basis}"; feed says "${info.basis}"` });

  const fNum = kcc20Int(info.numSompi), fDen = kcc20Int(info.den);
  out.published = fNum === null || fDen === null ? null : { num: String(fNum), den: String(fDen) };
  out.rederived = num === null || den === null ? null : { num: String(num), den: String(den) };
  const exact = num !== null && den !== null && fNum !== null && fDen !== null && num === fNum && den === fDen;
  // a different reduction of the same rational is honest, but it is not the
  // same two integers and must not read as "identical"
  const sameValue = !exact && num !== null && den !== null && fNum !== null && fDen !== null
    && den > 0n && fDen > 0n && num * fDen === fNum * den;
  out.exact = exact;
  out.checks.push({ name: 'exact rational', ok: exact || sameValue,
    detail: `mine ${out.rederived ? out.rederived.num + '/' + out.rederived.den : '(none)'}`
      + `  feed ${out.published ? out.published.num + '/' + out.published.den : '(none)'}`
      + `  -> ${exact ? 'IDENTICAL' : sameValue ? 'equal value, different reduction' : 'MISMATCH'}` });

  // the signed float against the rational it claims. On the /KAS leg this is a
  // 12-sample TWAP median, so it is allowed to lag an instantaneous rational —
  // reported as a measurement inside a stated band, never as equality.
  if (num !== null && den !== null && den > 0n) {
    out.price = Number(num) / Number(den) / 1e8;
    const px = typeof feed.median === 'number' ? feed.median : feed.price;
    if (typeof px === 'number' && info.leg === 'KAS' && out.price > 0) {
      out.publishedPrice = px;
      out.driftPct = ((px - out.price) / out.price) * 100;
      out.checks.push({ name: 'signed price', ok: Math.abs(out.driftPct) <= 10,
        detail: `feed ${px} vs rational ${out.price} -> ${out.driftPct.toFixed(3)}% (TWAP window)` });
    }
    // THE /USD LEG. Everything above this line is a property of the TOKEN/KAS
    // rational; nothing in it touches the dollar price, which is that rational
    // x the oracle's KAS/USD. Left unchecked, a wrong KAS/USD multiplier makes
    // every USD leg wrong by that factor and this function still returns ok.
    // So: check it against a KAS/USD the CALLER supplies (never one taken from
    // the same envelope — that would be the feed grading itself), and when the
    // caller supplies none, say plainly that the dollar half was not checked.
    if (typeof px === 'number' && info.leg === 'USD' && out.price > 0) {
      out.publishedPrice = px;
      const kasUsd = opts && Number(opts.kasUsd) > 0 ? Number(opts.kasUsd) : null;
      if (kasUsd) {
        const implied = px / out.price;              // dollars per KAS the feed used
        const d = ((implied - kasUsd) / kasUsd) * 100;
        out.usdChecked = true;
        out.checks.push({ name: 'USD leg', ok: Math.abs(d) <= 1,
          detail: `feed's KAS/USD works out to $${implied.toFixed(6)} · yours $${kasUsd.toFixed(6)} -> ${d.toFixed(3)}%` });
      } else {
        out.checks.push({ name: 'USD leg', ok: true,
          detail: 'NOT CHECKED — only the TOKEN/KAS rational was reproduced. Pass opts.kasUsd (your own KAS/USD) to check the dollar multiplier too' });
      }
    }
  }

  out.ok = out.checks.every((c) => c.ok);
  if (!out.ok) out.error = 'the published price did not re-derive from kascov';
  return out;
}

/* Pure self-test of the tier logic — no network. The two fixtures are trimmed
   from live mainnet documents: a pool that carries spot, and one where kascov
   withholds it. A reader who breaks the tier decision breaks the most dangerous
   thing on the wire, so it is checked at load like the crypto core is. */
function runKcc20SelfTest() {
  try {
    const withSpot = { pools: [{ market_id: 'f10e', market: { spot_num_sompi: 3029513000000, spot_den: 73048363 } }] };
    const t1 = kcc20TierOf(withSpot, 'f10e');
    if (t1.basis !== 'spot' || t1.num !== 3029513000000n || t1.den !== 73048363n) return { ok: false, detail: 'spot tier misread' };
    const withheld = { pools: [{ market_id: '10f2', market: { reserve_sompi: 45220170000000 } }] };
    const t2 = kcc20TierOf(withheld, '10f2');
    if (t2.basis !== 'last_verified_state' || t2.num !== null) return { ok: false, detail: 'a withheld spot must fall through to tier 2, never to a reserve ratio' };
    if (kcc20TierOf(withheld, 'nope').found) return { ok: false, detail: 'an unknown market_id must not match a row' };
    const cd = { candles: [
      { t: 1, carried: false, close: { quote_sompi: '1', base_amount: '2' } },
      { t: 2, carried: false, close: { quote_sompi: '12995162000000', base_amount: '353889693' } },
      { t: 3, carried: true, close: { quote_sompi: '12995162000000', base_amount: '353889693' } },
    ] };
    const cl = kcc20CandleClose(cd);
    if (!cl || cl.t !== 2 || cl.num !== 12995162000000n) return { ok: false, detail: 'carried candles must be skipped' };
    // the origin pin: a price_source_url on any other host must not resolve to
    // the audit origin, because that is how a forged feed picks its own auditor
    if (kcc20Origin('http://127.0.0.1:8200/data/mainnet/pools') === KCC20_PINNED_ORIGIN) {
      return { ok: false, detail: 'origin comparison is broken' };
    }
    if (KCC20_PINNED_ORIGIN !== 'https://kascov.io') return { ok: false, detail: 'the kascov origin pin was changed' };
    return { ok: true, detail: 'tier decision ✓, withheld-spot fallback ✓, carried-candle skip ✓, origin pin ✓' };
  } catch (e) {
    return { ok: false, detail: 'kcc20 self-test threw: ' + ((e && e.message) || e) };
  }
}
const KCC20_SELFTEST = runKcc20SelfTest();
// KCC20-CORE-END

// ── independently pinned committee verification ────────────────────────────
function decodePinnedCommittee(committee) {
  if (!committee || typeof committee !== 'object') return { error: 'committee pin: missing committee' };
  const need = committee.threshold;
  const numNodes = committee.num_nodes;
  const signers = committee.signers;
  if (!Number.isSafeInteger(need) || need <= 0 || !Array.isArray(signers) || signers.length === 0) {
    return { error: 'committee pin: empty or zero-threshold committee' };
  }
  if (!Number.isSafeInteger(numNodes) || numNodes !== signers.length) {
    return { error: 'committee pin: num_nodes does not match signer count' };
  }
  const pinned = new Set();
  for (const encoded of signers) {
    const pk = hexToBytes(encoded);
    if (pk === null || pk.length !== 32 || liftX(bytesToBig(pk)) === null) {
      return { error: 'committee pin: malformed signer key' };
    }
    const key = bytesToHex(pk);
    if (pinned.has(key)) return { error: 'committee pin: duplicate signer key' };
    pinned.add(key);
  }
  if (need > pinned.size) return { error: 'committee pin: threshold exceeds distinct signer count' };
  return { need, pinned };
}

function countPinnedValid(results, pinned) {
  const valid = new Set();
  for (const row of Array.isArray(results) ? results : []) {
    if (!row || row.ok !== true) continue;
    const pk = hexToBytes(row.signer);
    if (pk === null || pk.length !== 32) continue;
    const key = bytesToHex(pk);
    if (pinned.has(key)) valid.add(key);
  }
  return valid.size;
}

function verifyWithCommitteeCore(feed, committee) {
  const r = verifyFeedCore(feed);
  const decoded = decodePinnedCommittee(committee);
  if (decoded.error) return { ...r, ok: false, error: decoded.error };
  if (!r.ok) return r;

  const messageValid = countPinnedValid(r.results, decoded.pinned);
  if (messageValid < decoded.need) {
    return { ...r, ok: false, pinnedValid: messageValid,
      error: 'committee pin: fewer than committee-threshold valid message signatures from pinned keys' };
  }
  const cov = r.covenant || { present: false };
  let covenantValid = null;
  if (cov.present) {
    covenantValid = countPinnedValid(cov.results, decoded.pinned);
    if (covenantValid < decoded.need) {
      return { ...r, ok: false, pinnedValid: messageValid, pinnedCovenantValid: covenantValid,
        error: 'committee pin: fewer than committee-threshold valid covenant signatures from pinned keys' };
    }
  }
  return { ...r, pinnedValid: messageValid, pinnedCovenantValid: covenantValid };
}

// Regression for the exact substitution this pin exists to stop: a valid
// attacker signature plus an invalid pinned-key label. The old label-overlap
// check counted one pinned signer and passed a 1-of-1 pin.
function runCommitteePinSelfTest() {
  const attacker = 'f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9';
  const pinnedKey = '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
  const decoded = decodePinnedCommittee({ threshold: 1, num_nodes: 1, signers: [pinnedKey] });
  if (decoded.error) return { ok: false, detail: decoded.error };
  const rows = [{ signer: attacker, ok: true }, { signer: pinnedKey, ok: false }];
  if (countPinnedValid(rows, decoded.pinned) !== 0) {
    return { ok: false, detail: 'invalid pinned signer label was credited as a valid committee signature' };
  }
  for (const bad of [
    { threshold: 0, num_nodes: 1, signers: [pinnedKey] },
    { threshold: 2, num_nodes: 1, signers: [pinnedKey] },
    { threshold: 1, num_nodes: 2, signers: [pinnedKey] },
    { threshold: 1, num_nodes: 1, signers: ['00'] },
    { threshold: 1, num_nodes: 2, signers: [pinnedKey, pinnedKey.toUpperCase()] },
  ]) {
    if (!decodePinnedCommittee(bad).error) return { ok: false, detail: 'malformed committee pin was accepted' };
  }
  return { ok: true, detail: 'committee pin valid-signature membership ✓' };
}
const COMMITTEE_PIN_SELFTEST = runCommitteePinSelfTest();
if (!COMMITTEE_PIN_SELFTEST.ok) {
  throw new Error(`kaspulse committee-pin self-test failed: ${COMMITTEE_PIN_SELFTEST.detail}`);
}

// A small positive tolerance prevents harmless clock skew from looking like a
// forged timestamp. Anything farther ahead is not "fresh": its age would stay
// at zero/negative until the local clock caught up.
const FUTURE_TOLERANCE_MS = 30000;
function maxAgeMsError(maxAgeMs) {
  return typeof maxAgeMs !== 'number' || !Number.isFinite(maxAgeMs) || maxAgeMs < 0
    ? 'maxAgeMs must be a finite, non-negative number'
    : null;
}
function freshnessError(signedTs, maxAgeMs, nowMs = Date.now()) {
  const maxError = maxAgeMsError(maxAgeMs);
  if (maxError) return maxError;
  const signedMs = Number(signedTs) * 1000;
  if (!Number.isFinite(signedMs)) return 'signed_ts must be a finite timestamp';
  const ageMs = nowMs - signedMs;
  if (ageMs < -FUTURE_TOLERANCE_MS) {
    return `signature timestamp is ${Math.round(-ageMs)} ms in the future (max ${FUTURE_TOLERANCE_MS} ms)`;
  }
  if (ageMs > maxAgeMs) {
    return `signature is ${Math.round(ageMs)} ms old (max ${maxAgeMs} ms)`;
  }
  return null;
}
function runFreshnessSelfTest() {
  const nowMs = 1800000000000;
  if (freshnessError(nowMs / 1000, 0, nowMs) !== null) {
    return { ok: false, detail: 'current signed timestamp failed a zero-age check' };
  }
  if (!freshnessError(nowMs / 1000 + 31, 60000, nowMs)?.includes('future')) {
    return { ok: false, detail: 'timestamp more than 30 seconds in the future was accepted' };
  }
  if (!freshnessError(nowMs / 1000 - 31, 30000, nowMs)?.includes('old')) {
    return { ok: false, detail: 'timestamp older than maxAgeMs was accepted' };
  }
  for (const bad of [-1, NaN, Infinity, -Infinity, '30000', null]) {
    if (!maxAgeMsError(bad)) return { ok: false, detail: `invalid maxAgeMs was accepted: ${String(bad)}` };
  }
  return { ok: true, detail: 'freshness bounds and maxAgeMs validation ✓' };
}
const FRESHNESS_SELFTEST = runFreshnessSelfTest();
if (!FRESHNESS_SELFTEST.ok) {
  throw new Error(`kaspulse freshness self-test failed: ${FRESHNESS_SELFTEST.detail}`);
}

// ── errors + HTTP client ────────────────────────────────────────────────────
export class NoSuchFeedError extends Error {
  constructor(pair) {
    super(`kaspulse: no such feed: ${pair}`);
    this.name = 'NoSuchFeedError';
    this.pair = pair;
  }
}

// Public hosted oracle. Applications may explicitly configure another base URL.
const DEFAULT_BASE = 'https://pulse.kascov.io';

export class Kaspulse {
  constructor(base = DEFAULT_BASE) { this.base = String(base).replace(/\/+$/, ''); }

  async #get(path) {
    const res = await fetch(`${this.base}${path}`, { headers: { accept: 'application/json' } });
    if (!res.ok) throw new Error(`kaspulse: ${path} → HTTP ${res.status}`);
    return res.json();
  }

  /** Light catalog: {round, timestamp, count, feeds:[{pair, price, halted, thin, ...}]}.
      This is the endpoint dashboards should poll. */
  feeds() { return this.#get('/v1/feeds'); }

  /** Candidate committee artifact from the feed origin. Compare or persist its
      keys and threshold through an independent channel before treating it as a
      pin and passing it to verifyWithCommittee. */
  committee() { return this.#get('/v1/committee'); }

  /** One full FeedObj (price, sources, signatures, history). pair like
      'KAS/USD' or 'KAS-USD', case-insensitive. Unknown pair → NoSuchFeedError. */
  async feed(pair) {
    const path = `/v1/feed/${String(pair).replace(/\//g, '-')}`;
    const res = await fetch(`${this.base}${path}`, { headers: { accept: 'application/json' } });
    if (res.status === 404) throw new NoSuchFeedError(pair);
    if (!res.ok) throw new Error(`kaspulse: ${path} → HTTP ${res.status}`);
    return res.json();
  }

  /** Verify a FeedObj locally — threshold of BIP340 signatures over
      blake2b-256(message) AND signed-field/JSON-field binding. Pure sync.
      Returns {ok, valid, threshold, bound, parsed, results:[{signer, ok}]}. */
  verifyFeed(feed) { return verifyFeedCore(feed); }

  /** Verify the feed's BOUND `kaspulse/cov/v2` covenant attestation — the
      preimage an on-chain price gate consumes. REBUILDS the blob from the
      feed's own pair/mant/expo/signed_round/signed_ts and demands
      `covenant.preimage` back byte-for-byte before it looks at a signature.
      Pure sync. Returns {present, ok, valid, threshold, matches, expected,
      published, results} (+error). `present:false` = this feed carries no
      covenant, which is not a failure. */
  verifyCovenant(feed) { return verifyCovenantCore(feed); }

  // ── L1 KCC20 (Kaspa L1 AMM pools, priced through kascov) ────────────────

  /** The L1 rows of the light catalog: `kind: "kcc20-pool"`, each carrying
      `basis` and `last_trade_age_s` so a dashboard never has to open the full
      envelope to know which tier priced a feed and how old the fill is. */
  async kcc20Feeds() {
    const c = await this.feeds();
    return (c.feeds || []).filter((f) => f && f.kind === 'kcc20-pool');
  }

  /** The L1 census block from /v1/feed: how many markets kascov indexes, how
      many kaspulse prices, and EVERY market it refused with the reason. The
      refusals are the point — a market is never dropped silently. */
  async kcc20Census() {
    const root = await this.#get('/v1/feed');
    return root.kcc20 || null;
  }

  /** Every L1 provenance field on a full FeedObj, flattened — `basis`,
      `last_trade_age_s`, `anchor_txid`, `unpriced_reason` and the rest. Pure
      sync; null for any feed that is not `kind: "kcc20-pool"`. */
  kcc20Info(feed) { return kcc20Info(feed); }

  /** Re-derive an L1 KCC20 price from kascov itself and check it against what
      the oracle published. NETWORK CALL — it fetches a PINNED kascov origin,
      not the one the feed names (`opts.kascovBase` overrides it; the feed's
      `price_source_url` is checked against it, never followed). Pass
      `opts.kasUsd` to also check the `/USD` leg's dollar multiplier. See
      reDeriveKcc20 above for exactly what this does and does not prove. */
  reDeriveKcc20(feed, opts) { return reDeriveKcc20(feed, opts); }

  /** Like verifyFeed, but requires the committee threshold of DISTINCT, VALID
      signatures from decoded pinned keys in both message and cov/v2 domains. */
  verifyWithCommittee(feed, committee) { return verifyWithCommitteeCore(feed, committee); }

  /** The verified price (mant × 10^expo) — or throw with the reason it is
      unsafe: failed verification, halted, depegged, older than maxAgeMs, or
      signed more than 30 seconds in the future. */
  checkedValue(feed, options = {}) {
    if (options === null || typeof options !== 'object' || Array.isArray(options)) {
      throw new TypeError('kaspulse: checkedValue options must be an object');
    }
    const { maxAgeMs = 30000 } = options;
    const maxError = maxAgeMsError(maxAgeMs);
    if (maxError) throw new TypeError(`kaspulse: ${maxError}`);
    const r = verifyFeedCore(feed);
    if (!r.ok) throw new Error(`kaspulse: refusing value: ${r.error || 'verification failed'}`);
    if (feed.halted) throw new Error('kaspulse: refusing value: feed halted (circuit breaker)');
    if (feed.peg_ok === false) throw new Error('kaspulse: refusing value: chain depegged (peg_ok=false)');
    const freshness = freshnessError(feed.signed_ts, maxAgeMs);
    if (freshness) throw new Error(`kaspulse: refusing value: ${freshness}`);
    // Parse "<mant>e<expo>" as ONE decimal literal: that rounds once, to the
    // nearest double (0.0740927429). mant * 10 ** expo rounds twice and can
    // print as 0.07409274290000001. For exact arithmetic keep mant and expo.
    return Number(`${BigInt(feed.mant)}e${Number(feed.expo)}`);
  }
}

// standalone forms of the core, for the curious and the paranoid
export function verifyFeed(feed) { return verifyFeedCore(feed); }
export function verifyCovenant(feed) { return verifyCovenantCore(feed); }
export function verifyWithCommittee(feed, committee) { return verifyWithCommitteeCore(feed, committee); }
export { blake2b256, bip340Verify, selfTest };
export { covV2Preimage, COV_V2_TAG };
export { kcc20Info, kcc20TierOf, kcc20CandleClose, reDeriveKcc20 };
export function kcc20SelfTest() { return { ok: KCC20_SELFTEST.ok, detail: KCC20_SELFTEST.detail }; }
export default Kaspulse;

// ── CLI: node kaspulse.mjs verify KAS/USD [base] ────────────────────────────
if (typeof process !== 'undefined' && !!process.versions?.node && process.argv[1]
    && import.meta.url === (await import('node:url')).pathToFileURL(process.argv[1]).href) {
  const [cmd, pairArg, baseArg] = process.argv.slice(2);
  const st = selfTest();
  if (!st.ok) {
    console.error(`✗ verifier self-test failed: ${st.detail}`);
    process.exit(1);
  }

  // node kaspulse.mjs kcc20 [base] — signature-verify AND re-derive every L1
  // KCC20 price straight from kascov. Same two checks the Rust `verify` binary
  // runs, minus the gate replay (see the KCC20 core block for why).
  if (cmd === 'kcc20') {
    const base = pairArg || DEFAULT_BASE;
    const k2 = new Kaspulse(base);
    const kst = kcc20SelfTest();
    if (!kst.ok) { console.error(`✗ kcc20 self-test failed: ${kst.detail}`); process.exit(1); }
    console.log(`kaspulse kcc20 — re-deriving every L1 price from kascov  (${base})`);
    console.log(`  self-test: ${st.detail} · ${kst.detail}`);
    let census = null;
    try { census = await k2.kcc20Census(); } catch (e) { console.error(`✗ ${(e && e.message) || e}`); process.exit(1); }
    if (census) {
      console.log(`  kascov ${census.source} · ${census.pools_total} graduated pools of ${census.markets_total} markets`
        + ` · priced ${census.priced} · refused ${census.skipped_count} (each with a published reason)`);
    }
    const rows = await k2.kcc20Feeds();
    if (!rows.length) { console.log('  no kcc20-pool feeds in this envelope'); process.exit(0); }
    // An INDEPENDENT KAS/USD for the /USD legs. Everything reDeriveKcc20 checks
    // is a property of the TOKEN/KAS rational, so without this a wrong KAS/USD
    // multiplier leaves all seven dollar prices wrong and every leg still ✓.
    // Kraken direct, over the same zero-dependency `fetch` — no new dependency,
    // and deliberately NOT the oracle's own KAS/USD feed, which would be the
    // thing under audit grading itself. Unreachable is reported, never assumed.
    let kasUsd = null;
    try {
      const kj = await (await fetch('https://api.kraken.com/0/public/Ticker?pair=KASUSD')).json();
      const v = Number(Object.values(kj.result || {})[0]?.c?.[0]);
      if (Number.isFinite(v) && v > 0) kasUsd = v;
    } catch { /* reported below as "not checked", never silently assumed */ }
    console.log(kasUsd
      ? `  my own KAS/USD for the /USD legs: $${kasUsd} (Kraken, fetched by this process — not the oracle's)`
      : `  ${'/USD legs will NOT be dollar-checked — could not reach Kraken for an independent KAS/USD'}`);
    let bad = 0;
    for (const row of rows) {
      // only the /KAS leg carries the TWAP drift check; both legs share the
      // rational, so both are re-derived and both must reproduce
      const feed = await k2.feed(row.pair);
      const sig = k2.verifyFeed(feed);
      const r = await k2.reDeriveKcc20(feed, { kasUsd });
      const info = k2.kcc20Info(feed) || {};
      const ok = r.ok && (sig.ok || feed.halted);
      if (!ok) bad++;
      console.log(`\n  ${ok ? '✓' : '✗'} ${feed.pair}  ${info.displayName || ''}`);
      console.log(`      signatures  ${sig.valid}/${(sig.results || []).length} verify (threshold ${sig.threshold})`
        + `${feed.halted ? ' · feed HALTED by its own circuit breaker' : ''}`
        + `${sig.ok || sig.halted || sig.depegged ? '' : ' · ' + (sig.error || '')}`);
      console.log(`      basis       ${info.basis} · last trade ${info.lastTradeAgeS}s ago · anchor ${String(info.anchorTxid).slice(0, 16)}`
        + ` · gates ${info.admitted}/${info.gateWindow} admitted, ${info.rejectedBracket} bracket-rejected`);
      for (const c of r.checks) console.log(`      ${c.ok ? '✓' : '✗'} ${c.name.padEnd(15)} ${c.detail}`);
      // a re-derivation that never got far enough to produce a check (kascov
      // unreachable, CORS refused) must still say WHY — an empty ✗ is useless
      if (!r.ok && r.error) console.log(`      ✗ ${'error'.padEnd(15)} ${r.error}`);
      for (const u of r.urls) console.log(`      curl ${u}`);
    }
    console.log(bad === 0
      ? `\n✓ ALL ${rows.length} L1 KCC20 LEGS RE-DERIVED — every exact integer rational reproduced from kascov`
        + `${kasUsd ? ', and every /USD leg checked against this process\'s own Kraken KAS/USD' : '; the /USD legs\' dollar multiplier was NOT checked (no independent KAS/USD)'}.`
        + `\n  Not independence: this re-fetches kascov and compares. A bug inside kascov reproduces here too.`
      : `\n✗ ${bad} of ${rows.length} L1 KCC20 legs DID NOT re-derive. Do not consume this feed.`);
    process.exit(bad === 0 ? 0 : 1);
  }

  if (cmd !== 'verify' || !pairArg) {
    console.error('usage: node kaspulse.mjs verify KAS/USD [base]');
    console.error('       node kaspulse.mjs kcc20 [base]      # re-derive every L1 KCC20 price from kascov');
    process.exit(2);
  }
  const base = baseArg || DEFAULT_BASE;
  const pair = pairArg.replace(/-/g, '/');
  try {
    const k = new Kaspulse(base);
    const feed = await k.feed(pair);
    const r = k.verifyFeed(feed);
    console.log(`${feed.pair}  ${base}  signed_round ${feed.signed_round ?? '?'}`);
    r.results.forEach((n, i) => console.log(`  node ${i}  ${n.ok ? '✓' : '✗'}  ${n.signer}`));
    console.log(`  bound=${r.bound} (signed message fields == JSON fields)`);
    // ── the covenant domain. A feed with no covenant is fine; a feed whose
    //    covenant does NOT rebuild is the withdrawn hole, and must never come
    //    out of here as a green ✓.
    const c = r.covenant;   // verifyFeed already folded this into r.ok
    if (!c.present) {
      console.log(`  covenant  — none published on this feed (kaspulse/cov/v2)`);
    } else {
      console.log(`  covenant  kaspulse/cov/v2  preimage rebuilt from THIS feed's fields:`);
      if (c.expected && c.published) {
        console.log(`            expected  ${c.expected}`);
        console.log(`            published ${c.published}`);
        console.log(`            byte-for-byte match: ${c.matches ? 'YES' : 'NO'}`
          + (c.matches ? `  ·  ${c.valid}/${c.results.length} signatures over blake2b256(preimage) (threshold ${c.threshold})` : ''));
      }
      if (!c.ok) console.log(`  ✗ COVENANT ${c.matches ? 'SIGNATURES' : 'DOMAIN'} FAILED — ${c.error}`);
    }
    const covFatal = c.present && !c.ok;
    const sigsOk = r.bound && r.threshold > 0 && r.valid >= r.threshold;
    // A HALTED feed is the breaker WORKING, not a forgery: its signatures are
    // valid and the committee is honest, and the price it carries is the held
    // one the breaker is suppressing. Printing a bare "✓ VALID … price = X" for
    // it — which this path did until 2026-08-25 — hands a consumer exactly the
    // number they must not use. It is its own third verdict, never a green ✓.
    if (sigsOk && !covFatal && (r.halted || r.depegged)) {
      console.log(`⚠ NOT SAFE TO CONSUME — ${r.error}`);
      console.log(`  the ${r.valid}/${r.results.length} committee signatures DO verify (threshold ${r.threshold})`
        + `${c.present ? `, and the covenant is BOUND to ${feed.pair} @ expo ${feed.expo} round ${feed.signed_round}` : ''}.`);
      console.log(`  the price this feed carries is the HELD one its own circuit breaker is suppressing. Do not consume it.`);
      process.exit(1);
    }
    if (r.ok && !covFatal) {
      const px = Number(feed.mant) * Math.pow(10, Number(feed.expo));
      console.log(`✓ VALID — ${r.valid}/${r.results.length} signatures verify (threshold ${r.threshold}), price = ${px}`
        + (c.present ? `, covenant BOUND to ${feed.pair} @ expo ${feed.expo} round ${feed.signed_round}` : ''));
      process.exit(0);
    }
    console.log(`✗ INVALID — ${covFatal ? c.error : (r.error || 'verification failed')}`);
    process.exit(1);
  } catch (e) {
    console.error(`✗ ${(e && e.message) || e}`);
    process.exit(1);
  }
}
