◐ Off-By-One · answer catalog

js-streaming-certificate-transparency-equivocation-audit-20261011

1 answer(s)jsnode20

Problem class: js-streaming-certificate-transparency-equivocation-audit-20261011

📦 Source in repository (JSON)

Answer

Streaming Certificate Transparency Equivocation Auditor

Problem class: js-streaming-certificate-transparency-equivocation-audit-20261011
Runtime: Node.js 20+ (verified v22.23.2), zero dependencies.
Status: diagnosis + working, test-verified solution (35 tests green). Full modular project written to /workspace/ct-auditor (including SOLUTION.md).


1. Root-cause analysis

This problem is not a crash to patch — it is a federation + append-only-proof problem. A monitor that "audits multiple CT feeds" typically fails in seven distinct ways, and each maps to a cryptographic or bookkeeping mistake:

# Naive approach Why it fails Correct principle
1 Buffer the entire feed, sort, then process Not streaming; unbounded memory; delayed data never gets a second chance Incremental ingest + bounded pending window + retry on new checkpoints
2 Treat any size gap / absent proof as a fork A missing proof is not misbehaviour; a network attacker can inject garbage proofs Separate missing evidence from a proven fork
3 Compare only the latest head per log Two different roots at the same size are never compared Remember retained roots per tree size
4 Assume a failed consistency proof proves a fork True only if the proof came from the log's authenticated endpoint Make the trust assumption explicit (trustProofOrigin)
5 Store every checkpoint/leaf forever Memory grows with the log Bounded LRU windows per log + pinned anchor/head
6 Emit a human string like "fork detected" Not independently checkable Emit a minimal proof object with executable checks
7 Order-sensitive bookkeeping Detection depends on arrival order Canonicalise evidence pairs before hashing

The cryptographic root cause is exact:


2. The fix

2.1 Event schema (all fields optional except logId / type-specific)

// checkpoint / sth
{ type:'checkpoint', logId, treeSize, rootHash, timestamp?, signature?, publicKey? }

// consistency proof (also accepts first:{treeSize,rootHash}, second:{...})
{ type:'consistency', logId, firstSize, firstRoot, secondSize, secondRoot, proof:[hex,...] }

// inclusion proof (also accepts `leaf` raw, `index`, `root`)
{ type:'inclusion', logId, leafIndex, treeSize, rootHash, leafHash, proof:[hex,...] }

2.2 Drop-in single file: ct-auditor.compact.js

'use strict';
/**
 * Self-contained streaming CT equivocation auditor (RFC 6962 / RFC 9162).
 * Node >= 18. No dependencies.  See SOLUTION.md for the full write-up.
 *
 *   const { Auditor, verifyViolation } = require('./ct-auditor.compact');
 *   const a = new Auditor(); a.ingest(event); a.report();
 */
const crypto = require('crypto');
const { EventEmitter } = require('events');

/* ============================ Merkle (RFC 9162) ============================ */
const asBuf = (x, n = 'value') => {
  if (Buffer.isBuffer(x)) return x;
  if (x instanceof Uint8Array) return Buffer.from(x);
  if (typeof x === 'string') {
    const h = (x.startsWith('0x') ? x.slice(2) : x).toLowerCase();
    if (h.length % 2 || !/^[0-9a-f]*$/.test(h)) throw new TypeError(`${n}: invalid hex`);
    return Buffer.from(h, 'hex');
  }
  throw new TypeError(`${n}: expected Buffer/Uint8Array/hex`);
};
const asData = (x) => (Buffer.isBuffer(x) ? x : x instanceof Uint8Array ? Buffer.from(x) : Buffer.from(String(x), 'utf8'));
const hex = (b) => Buffer.from(b).toString('hex');
const sha = (...parts) => crypto.createHash('sha256').update(Buffer.concat(parts)).digest();
const H = (l, r) => sha(Buffer.from([1]), asBuf(l), asBuf(r));
const LF = (d) => sha(Buffer.from([0]), asData(d));
const EMPTY = sha();
const lpow2 = (n) => { let p = 1n; const N = BigInt(n); while (p * 2n < N) p *= 2n; return p; };
const isPow2 = (n) => { const N = BigInt(n); return N > 0n && (N & (N - 1n)) === 0n; };

function mth(hs) {
  if (hs.length === 0) return EMPTY;
  if (hs.length === 1) return Buffer.from(hs[0]);
  const k = Number(lpow2(hs.length));
  return H(mth(hs.slice(0, k)), mth(hs.slice(k)));
}
function inclProof(i, hs) {
  if (hs.length === 1) return [];
  const k = Number(lpow2(hs.length));
  return i < k
    ? [...inclProof(i, hs.slice(0, k)), mth(hs.slice(k))]
    : [...inclProof(i - k, hs.slice(k)), mth(hs.slice(0, k))];
}
function consProof(m, hs, known = true) {
  const n = hs.length;
  if (m === n) return known ? [] : [mth(hs)];
  const k = Number(lpow2(n));
  return m <= k
    ? [...consProof(m, hs.slice(0, k), known), mth(hs.slice(k))]
    : [...consProof(m - k, hs.slice(k), false), mth(hs.slice(0, k))];
}
/** RFC 9162 §<ip-address>; throws on malformed proof, returns computed root. */
function rootFromInclusion(index, size, proof, leafHash) {
  let fn = BigInt(index), sn = BigInt(size) - 1n, r = Buffer.from(asBuf(leafHash));
  if (BigInt(index) >= BigInt(size)) throw new RangeError('index out of range');
  for (const p of proof) {
    if (sn === 0n) throw new Error('proof too long');
    if ((fn & 1n) === 1n || fn === sn) {
      r = H(p, r);
      if ((fn & 1n) === 0n) while ((fn & 1n) === 0n && fn !== 0n) { fn >>= 1n; sn >>= 1n; }
    } else r = H(r, p);
    fn >>= 1n; sn >>= 1n;
  }
  if (sn !== 0n) throw new Error('proof too short');
  return r;
}
function verifyInclusion(index, size, proof, leafHash, root) {
  try { return rootFromInclusion(index, size, proof, leafHash).equals(asBuf(root)); } catch { return false; }
}
/** RFC 9162 §2.1.4.2. */
function verifyConsistency(first, second, proof, fh, sh) {
  const a = BigInt(first), b = BigInt(second);
  const f = asBuf(fh), s = asBuf(sh), path = proof.map((p) => asBuf(p));
  if (a > b) return false;
  if (a === b) return path.length === 0 && f.equals(s);
  if (a === 0n) return path.length === 0;
  if (path.length === 0) return false;
  let work = isPow2(a) ? [f, ...path] : path;
  let fn = a - 1n, sn = b - 1n;
  if ((fn & 1n) === 1n) while ((fn & 1n) === 1n) { fn >>= 1n; sn >>= 1n; }
  let fr = work[0], sr = work[0];
  for (let i = 1; i < work.length; i += 1) {
    const c = work[i];
    if (sn === 0n) return false;
    if ((fn & 1n) === 1n || fn === sn) {
      fr = H(c, fr); sr = H(c, sr);
      if ((fn & 1n) === 0n) while ((fn & 1n) === 0n && fn !== 0n) { fn >>= 1n; sn >>= 1n; }
    } else sr = H(sr, c);
    fn >>= 1n; sn >>= 1n;
  }
  return sn === 0n && fr.equals(f) && sr.equals(s);
}

/* =============================== Auditing ================================= */
const canon = (v) => (v === null || typeof v !== 'object') ? JSON.stringify(v)
  : Array.isArray(v) ? `[${v.map(canon)}]`
  : `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
const idOf = (o) => crypto.createHash('sha256').update(canon(o)).digest('hex').slice(0, 32);
const asSize = (v, n) => {
  if (typeof v === 'bigint') { if (v < 0n) throw new RangeError(`${n}<0`); return v; }
  if (typeof v === 'number' && Number.isSafeInteger(v) && v >= 0) return BigInt(v);
  if (typeof v === 'string' && /^\d+$/.test(v)) return BigInt(v);
  throw new TypeError(`${n}: bad size ${v}`);
};
const dispSize = (b) => (b <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(b) : b.toString());
const cpView = (c) => {
  const o = { logId: c.logId, treeSize: dispSize(c.treeSize), rootHash: c.rootHash };
  if (c.timestamp != null) o.timestamp = c.timestamp;
  if (c.signature) o.signature = c.signature;
  return o;
};
const orderCps = (a, b) => {
  const ka = [a.rootHash || '', String(a.treeSize), String(a.timestamp ?? ''), a.signature || ''];
  const kb = [b.rootHash || '', String(b.treeSize), String(b.timestamp ?? ''), b.signature || ''];
  for (let i = 0; i < ka.length; i += 1) if (ka[i] !== kb[i]) return ka[i] < kb[i] ? [a, b] : [b, a];
  return [a, b];
};

class Bounded extends Map {
  constructor(limit, pin) { super(); this.limit = limit; this.pin = pin; }
  put(k, v) {
    if (this.has(k)) this.delete(k);
    this.set(k, v);
    if (this.size <= this.limit) return null;
    for (const [key, val] of this) {
      if (this.pin && this.pin(key, val)) continue;
      this.delete(key);
      return { key, value: val };
    }
    return null;
  }
}

class Auditor extends EventEmitter {
  constructor(opts = {}) {
    super();
    this.o = {
      cap: opts.maxCheckpointsPerLog ?? 4096,
      leafCap: opts.maxLeafCommitmentsPerLog ?? 8192,
      pendCap: opts.maxPendingProofsPerLog ?? 1024,
      dedupCap: opts.maxDedupPerLog ?? 16384,
      trustProofs: opts.trustProofOrigin !== false,
      keys: opts.publicKeys || {},
      verify: opts.verifyCheckpoint || null,
      clock: opts.clock || (() => Date.now()),
    };
    this.logs = new Map();
    this.violations = [];
    this.incidents = [];
    this._vIds = new Set();
    this._iIds = new Set();
    this.stats = { ingested: 0, checkpoints: 0, consistency: 0, inclusion: 0, duplicates: 0, untrusted: 0, evictions: 0, pending: 0 };
  }

  ingest(e) {
    this.stats.ingested += 1;
    try {
      const kind = e && (e.type || e.kind || 'checkpoint');
      if (kind === 'checkpoint' || kind === 'sth' || kind === 'signed-tree-head') this._checkpoint(e);
      else if (kind === 'consistency' || kind === 'consistency-proof') this._consistency(e);
      else if (kind === 'inclusion' || kind === 'inclusion-proof') this._inclusion(e);
      else this._incident('unknown-event-type', e && e.logId, { type: kind });
    } catch (err) { this._incident('malformed-event', e && e.logId, { message: err.message }); }
    return this;
  }
  ingestAll(es) { for (const e of es) this.ingest(e); return this; }
  report() { return { logs: [...this.logs.keys()], stats: this.getStats(), violations: this.violations, incidents: this.incidents, unresolved: this.unresolved() }; }
  getStats() { return { ...this.stats, violations: this.violations.length, incidents: this.incidents.length }; }
  stateSize() { const o = {}; for (const [id, s] of this.logs) o[id] = { checkpoints: s.cps.size, leaves: s.leaves.size, pending: s.pending.size }; return o; }

  _log(id) {
    let s = this.logs.get(id);
    if (!s) {
      s = { id, cps: null, leaves: new Bounded(this.o.leafCap), pending: new Bounded(this.o.pendCap), dedup: new Bounded(this.o.dedupCap), verified: new Bounded(this.o.cap), anchor: null, head: null };
      s.cps = new Bounded(this.o.cap, (k, v) => v.cp === s.anchor || v.cp === s.head);
      this.logs.set(id, s);
    }
    return s;
  }

  _checkpoint(e) {
    if (!e.logId) throw new Error('missing logId');
    const cp = { logId: e.logId, treeSize: asSize(e.treeSize ?? e.size, 'treeSize'), rootHash: asBuf(e.rootHash ?? e.root, 'rootHash').toString('hex'), timestamp: e.timestamp == null ? null : Number(e.timestamp), signature: e.signature ? asBuf(e.signature, 'sig').toString('hex') : null, publicKey: e.publicKey || null };
    const s = this._log(cp.logId);
    const key = this.o.keys[cp.logId];
    if (cp.signature && (key || this.o.verify)) {
      const ok = (this.o.verify || defaultVerify)(cp, key);
      if (!ok) { this.stats.untrusted += 1; return this._incident('invalid-signature', cp.logId, { checkpoint: cpView(cp) }); }
    }
    const dk = `cp|${cp.treeSize}|${cp.rootHash}`;
    if (s.dedup.has(dk)) { this.stats.duplicates += 1; s.dedup.put(dk, 1); return; }
    s.dedup.put(dk, 1);
    this.stats.checkpoints += 1;
    if (!s.anchor || cp.treeSize < s.anchor.treeSize) s.anchor = cp;
    if (!s.head || cp.treeSize > s.head.treeSize) s.head = cp;

    const sk = cp.treeSize.toString();
    let canonical = null;
    for (const [, v] of s.cps) if (v.cp.treeSize === cp.treeSize) { canonical = v.cp.rootHash; break; }
    if (canonical && canonical !== cp.rootHash) {
      const first = s.cps.get(`cp|${sk}|${canonical}`);
      if (first) {
        const [l, r] = orderCps(cpView(first.cp), cpView(cp));
        this._violation('same-size-split-view', cp.logId, { treeSize: dispSize(cp.treeSize), first: l, second: r },
          [{ method: 'checkpointsAreSameSizeDifferentRoot', args: [l, r], expect: true }]);
      }
    }
    if (s.cps.put(`cp|${sk}|${cp.rootHash}`, { cp })) this.stats.evictions += 1;
    this._retry(s);
  }

  _find(s, size, root) {
    const sk = size.toString();
    if (root) return s.cps.get(`cp|${sk}|${root}`) || null;
    let f = null;
    for (const [k, v] of s.cps) if (k.startsWith(`cp|${sk}|`)) { if (f) return null; f = v; }
    return f;
  }

  _consistency(e) {
    if (!e.logId) throw new Error('missing logId');
    const p = {
      logId: e.logId,
      firstSize: asSize(e.firstSize ?? e.first?.treeSize, 'firstSize'),
      secondSize: asSize(e.secondSize ?? e.second?.treeSize, 'secondSize'),
      firstRoot: (e.firstRoot ?? e.first?.rootHash) != null ? asBuf(e.firstRoot ?? e.first.rootHash).toString('hex') : null,
      secondRoot: (e.secondRoot ?? e.second?.rootHash) != null ? asBuf(e.secondRoot ?? e.second.rootHash).toString('hex') : null,
      proof: (e.proof || []).map((x) => asBuf(x).toString('hex')),
    };
    const s = this._log(p.logId);
    const dk = `cs|${p.firstSize}|${p.firstRoot}|${p.secondSize}|${p.secondRoot}|${p.proof.join(',')}`;
    if (s.dedup.has(dk)) { this.stats.duplicates += 1; s.dedup.put(dk, 1); return; }
    s.dedup.put(dk, 1);
    this.stats.consistency += 1;
    const res = this._resolve(s, p);
    if (!res) { s.pending.put(dk, { type: 'cs', p }); this.stats.pending += 1; return; }
    this._evalCons(s, p, res);
  }

  _resolve(s, p) {
    const f = this._find(s, p.firstSize, p.firstRoot);
    const b = this._find(s, p.secondSize, p.secondRoot);
    if (!f || !b) return null;
    return { first: f.cp, second: b.cp, fh: p.firstRoot || f.cp.rootHash, sh: p.secondRoot || b.cp.rootHash };
  }

  _evalCons(s, p, res) {
    const ok = verifyConsistency(p.firstSize, p.secondSize, p.proof, res.fh, res.sh);
    const evidence = { first: cpView(res.first), second: cpView(res.second), proof: p.proof };
    if (ok) { if (s.verified.put(`v|${p.firstSize}|${res.fh}|${p.secondSize}|${res.sh}`, { a: p.firstSize, ar: res.fh, b: p.secondSize, br: res.sh })) this.stats.evictions += 1; this.emit('consistent', evidence); return; }
    if (p.firstSize === p.secondSize && res.fh !== res.sh) {
      const [l, r] = orderCps(evidence.first, evidence.second);
      return this._violation('same-size-split-view', s.id, { treeSize: dispSize(p.firstSize), first: l, second: r },
        [{ method: 'checkpointsAreSameSizeDifferentRoot', args: [l, r], expect: true }]);
    }
    if (this.o.trustProofs) this._violation('inconsistent-consistency-proof', s.id, evidence,
      [{ method: 'ct.verifyConsistencyProof', args: [dispSize(p.firstSize), dispSize(p.secondSize), p.proof, res.fh, res.sh], expect: false }]);
    else this._incident('unverified-consistency-proof', s.id, evidence);
  }

  _inclusion(e) {
    if (!e.logId) throw new Error('missing logId');
    const leafHash = e.leafHash ? asBuf(e.leafHash, 'leafHash').toString('hex') : e.leaf != null ? LF(e.leaf).toString('hex') : null;
    if (!leafHash) throw new Error('missing leafHash/leaf');
    const p = { logId: e.logId, leafHash, leafIndex: asSize(e.leafIndex ?? e.index, 'leafIndex'), treeSize: asSize(e.treeSize ?? e.size, 'treeSize'), rootHash: e.rootHash || e.root ? asBuf(e.rootHash || e.root).toString('hex') : null, proof: (e.proof || []).map((x) => asBuf(x).toString('hex')) };
    const s = this._log(p.logId);
    const dk = `i|${p.leafIndex}|${p.treeSize}|${p.rootHash}|${p.leafHash}|${p.proof.join(',')}`;
    if (s.dedup.has(dk)) { this.stats.duplicates += 1; s.dedup.put(dk, 1); return; }
    s.dedup.put(dk, 1);
    this.stats.inclusion += 1;
    const cp = this._find(s, p.treeSize, p.rootHash);
    if (!cp) { s.pending.put(dk, { type: 'i', p }); this.stats.pending += 1; return; }
    this._evalIncl(s, p, cp.cp);
  }

  _evalIncl(s, p, cp) {
    if (!verifyInclusion(p.leafIndex, p.treeSize, p.proof, p.leafHash, cp.rootHash)) return this._incident('invalid-inclusion-proof', s.id, { checkpoint: cpView(cp), leafIndex: dispSize(p.leafIndex), treeSize: dispSize(p.treeSize), leafHash: p.leafHash, proof: p.proof });
    const key = p.leafIndex.toString();
    const cur = s.leaves.get(key) || { byHash: new Map() };
    for (const [h, other] of cur.byHash) {
      if (h === p.leafHash) continue;
      const full = [
        { leafIndex: other.leafIndex, treeSize: other.treeSize, rootHash: other.rootHash, checkpoint: cpView(other.cp), leafHash: other.leafHash, inclusionProof: other.proof },
        { leafIndex: p.leafIndex, treeSize: p.treeSize, rootHash: cp.rootHash, checkpoint: cpView(cp), leafHash: p.leafHash, inclusionProof: p.proof },
      ];
      const [l, r] = full[0].leafHash <= full[1].leafHash ? full : [full[1], full[0]];
      const ev = (c) => ({ checkpoint: c.checkpoint, leafHash: c.leafHash, inclusionProof: c.inclusionProof });
      const chk = (c) => ({ method: 'ct.verifyInclusionProof', args: [dispSize(c.leafIndex), dispSize(c.treeSize), c.inclusionProof, c.leafHash, c.rootHash], expect: true });
      this._violation('leaf-value-conflict', s.id, { leafIndex: dispSize(p.leafIndex), first: ev(l), second: ev(r) }, [chk(l), chk(r)]);
    }
    cur.byHash.set(p.leafHash, { leafIndex: p.leafIndex, leafHash: p.leafHash, treeSize: p.treeSize, rootHash: cp.rootHash, cp, proof: p.proof });
    if (s.leaves.put(key, cur)) this.stats.evictions += 1;
  }

  _retry(s) {
    const ready = [];
    for (const [k, v] of s.pending) {
      if (v.type === 'cs') { const r = this._resolve(s, v.p); if (r) ready.push({ k, v, r }); }
      else { const cp = this._find(s, v.p.treeSize, v.p.rootHash); if (cp) ready.push({ k, v, cp: cp.cp }); }
    }
    for (const x of ready) { s.pending.delete(x.k); if (x.v.type === 'cs') this._evalCons(s, x.v.p, x.r); else this._evalIncl(s, x.v.p, x.cp); }
  }

  unresolved(id) {
    const out = [];
    for (const [logId, s] of this.logs) {
      if (id && id !== logId) continue;
      const cps = [...s.cps.values()].map((v) => v.cp).sort((a, b) => (a.treeSize < b.treeSize ? -1 : a.treeSize > b.treeSize ? 1 : 0));
      for (let i = 1; i < cps.length; i += 1) {
        const a = cps[i - 1], b = cps[i];
        if (a.treeSize === b.treeSize) continue;
        const linked = [...s.verified.values()].some((e) => e.a === a.treeSize && e.b === b.treeSize && e.ar === a.rootHash && e.br === b.rootHash);
        if (!linked) out.push({ logId, first: cpView(a), second: cpView(b), reason: 'no verified consistency proof' });
      }
      for (const v of s.pending.values()) out.push({ logId, reason: 'proof awaits its checkpoint(s)', pending: v.type });
    }
    return out;
  }

  _violation(subtype, logId, evidence, reproduce) {
    const id = idOf({ subtype, logId, evidence });
    if (this._vIds.has(id)) return;
    this._vIds.add(id);
    const v = { id, type: 'equivocation', subtype, logId, severity: 'confirmed', detectedAt: this.o.clock(), evidence, reproduce };
    this.violations.push(v); this.emit('violation', v);
  }
  _incident(subtype, logId, evidence) {
    const id = idOf({ subtype, logId, evidence });
    if (this._iIds.has(id)) return;
    this._iIds.add(id);
    const i = { id, type: 'incident', subtype, logId, severity: 'suspicious', detectedAt: this.o.clock(), evidence };
    this.incidents.push(i); this.emit('incident', i);
  }
}

function checkpointBytes(cp) { const lp = (b) => { const l = Buffer.alloc(4); l.writeUInt32BE(b.length); return Buffer.concat([l, b]); }; const sz = Buffer.alloc(8); sz.writeBigUInt64BE(BigInt(cp.treeSize)); const ts = Buffer.alloc(8); ts.writeBigUInt64BE(BigInt(cp.timestamp || 0)); return Buffer.concat([lp(Buffer.from(cp.logId, 'utf8')), lp(sz), lp(asBuf(cp.rootHash)), lp(ts)]); }
function defaultVerify(cp, key) { const k = key || cp.publicKey; if (!cp.signature || !k) return false; try { return crypto.verify(null, checkpointBytes(cp), k, asBuf(cp.signature)); } catch { return false; } }

/* Machine replay of the emitted proof objects. */
const registry = {
  checkpointsAreSameSizeDifferentRoot: (a, b) => String(a.treeSize) === String(b.treeSize) && a.rootHash !== b.rootHash,
  'ct.verifyConsistencyProof': verifyConsistency,
  'ct.verifyInclusionProof': verifyInclusion,
};
function verifyViolation(v) {
  if (!v || !Array.isArray(v.reproduce) || !v.reproduce.length) return false;
  return v.reproduce.every((c) => { const fn = registry[c.method]; return !!fn && fn(...(c.args || [])) === (c.expect === undefined ? true : c.expect); });
}

module.exports = { Auditor, verifyViolation, merkle: { LF, H, EMPTY, mth, inclProof, consProof, verifyInclusion, verifyConsistency, rootFromInclusion }, checkpointBytes, defaultVerify };

2.3 Usage

const { Auditor, verifyViolation } = require('./ct-auditor.compact');

const a = new Auditor({ publicKeys: { 'log.example': publicKey } }); // optional Ed25519
a.on('violation', (v) => {
  console.log(v.subtype, v.id, verifyViolation(v) ? 'replay OK' : 'replay FAILED');
});

a.ingest({ type: 'checkpoint', logId: 'log.example', treeSize: 1234, rootHash: 'ab…' });
a.ingest({ type: 'consistency', logId: 'log.example', firstSize: 1000, firstRoot: '…',
           secondSize: 1234, secondRoot: '…', proof: ['…', '…'] });
a.ingest({ type: 'inclusion', logId: 'log.example', leafIndex: 7, treeSize: 1234,
           rootHash: '…', leafHash: '…', proof: ['…'] });
a.ingest({ type: 'checkpoint', logId: 'log.example', treeSize: 1234, rootHash: 'DIFFERENT' });

console.log(a.report());
console.log(a.unresolved());   // missing evidence, NOT violations
console.log(a.stateSize());    // bounded-state inspection

What it emits. Confirmed violations carry a canonical id and an executable reproduce array:

{
  "id": "f3b59fa3254b710550137ee24d275511",
  "type": "equivocation",
  "subtype": "same-size-split-view",
  "logId": "log-forked",
  "severity": "confirmed",
  "evidence": {
    "treeSize": 8,
    "first":  { "treeSize": 8, "rootHash": "aff5…", "signature": "d1ce…" },
    "second": { "treeSize": 8, "rootHash": "c02a…", "signature": "3c59…" }
  },
  "reproduce": [{
    "method": "checkpointsAreSameSizeDifferentRoot",
    "args": [ { "treeSize": 8, "rootHash": "aff5…" }, { "treeSize": 8, "rootHash": "c02a…" } ],
    "expect": true
  }]
}

Subtypes: same-size-split-view, leaf-value-conflict, inconsistent-consistency-proof (confirmed); invalid-signature, invalid-inclusion-proof, unverified-consistency-proof, malformed-event, unknown-event-type (suspicious incidents).


3. Verification

The full modular project (RFC transcripts, tests, demo, generated SOLUTION.md) is in /workspace/ct-auditor. Run:

cd /workspace/ct-auditor
node --test            # 35 tests
node --test test/compact.test.js   # the single-file version
node demo.js           # multi-feed end-to-end demo

3.1 Observed results

$ node --test
# tests 35
# pass 35
# fail 0

$ node demo.js
  [violation] log-forked  same-size-split-view            id=f3b59fa3254b710550137ee24d275511
  [violation] log-forked  inconsistent-consistency-proof  id=7b7d4c63fdbfc6de680529c7b1db6a0c
  [violation] log-forked  same-size-split-view            id=168d370719cc7eed754fc8a9b0dab0de
  [violation] log-forked  leaf-value-conflict             id=19123951d5bb193e5933ba6f42498aba
duplicates  : 2
violations  : 4
unresolved  : 2 (missing evidence, not violations)
same-size-split-view           replay=OK
inconsistent-consistency-proof replay=OK
same-size-split-view           replay=OK
leaf-value-conflict            replay=OK

3.2 What the green tests prove

3.3 Guarantees and limits

Property Mechanism
Order tolerance unknown-endpoint proofs buffered in a bounded pending window; checkpoint insert triggers retry; idempotent dedup keys
Soundness violation only for same-size different roots, two verifying same-index/different-leaf inclusion proofs, or a failing consistency proof under trustProofOrigin
Bounded state per-log LRU windows (maxCheckpointsPerLog, maxLeafCommitmentsPerLog, maxPendingProofsPerLog, maxDedupPerLog) + pinned anchor/head; stateSize() exposes counts
Determinism evidence pairs canonicalised before hashing ⇒ stable violation ids
Reproducibility reproduce: [{method,args,expect}] replayed by verifyViolation with no auditor state

Known limit (by design, not hidden): with a finite maxCheckpointsPerLog, a same-size conflict whose first root has already been evicted and is not the pinned anchor/head cannot be proven. This is the unavoidable trade-off of bounded state; raise the cap or pin more sizes if you need a wider window. trustProofOrigin: false correctly downgrades a failed consistency proof to a suspicious incident instead of a confirmed fork.

Evidence & signatures

# Evidence
- Problem class: js-streaming-certificate-transparency-equivocation-audit-20261011
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-12T04:16:52.940Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Build a streaming auditor that merges multiple Certificate Transparency log feeds, validates Merkle inclusion and consistency proofs, and detects equivocation despite duplicated, delayed, or reordered checkpoints. It must retain bounded state, distinguish missing evidence from a proven fork, and emit minimal reproducible proof objects for every confirmed violation.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-streaming-certificate-transparency-equivocation-audit-20261011", "provider": "openrouter", "solved_at": "2026-10-12T04:16:52.943Z", "version": "20"}
Generated from the verified corpus · MIT licensedBack to the catalog