A CT log commits to (treesize, roothash) under its key; the append-only promise is that a size-m tree is a prefix of every larger tree. Equivocation = two signed heads that cannot coexist in one history. The failure modes a naive detector hits:
Solution implemented and verified in ~/ctfork (full write-up at ~/ctfork/SOLUTION.md, copied to ~/SOLUTION.md). One command reproduces everything: cd ~/ctfork && ./verify.sh.
A CT log commits to (tree_size, root_hash) under its key; the append-only promise is that a size-m tree is a prefix of every larger tree. Equivocation = two signed heads that cannot coexist in one history. The failure modes a naive detector hits:
union-find over "consistent" edges is unsound: a log can give valid A→B and valid A→C while B and C diverge after A. Heads must be compared pairwise (or via a proof).assert sn == 0 fails for non-power-of-two trees: consistency 5→6 and inclusion 2/3 correctly end at fn = sn = 1. The correct terminal test is reconstructed-root equality, with an in-loop if (sn === 0) return false guard against over-long proofs.Three layers, in src/:
merkle.js — RFC 6962 hashing (0x00‖leaf, 0x01‖l‖r), MerkleTree proof generation, and iterative verifyInclusion / verifyConsistency using the corrected terminal condition:function verifyInclusion(leafIndex, treeSize, leafHashValue, path, root) {
let fn = leafIndex, sn = treeSize - 1, r = toBuffer(leafHashValue);
for (const p of path) {
if (sn === 0) return false; // proof too long
const pb = toBuffer(p);
if ((fn & 1) === 1 || fn === sn) r = nodeHash(pb, r); // left sibling
else r = nodeHash(r, pb); // right sibling
fn >>>= 1; sn >>>= 1;
}
return Buffer.compare(r, toBuffer(root)) === 0; // root equality
}
function verifyConsistency(firstSize, secondSize, firstRoot, secondRoot, path) {
if (firstSize === secondSize) return path.length === 0 && eq(firstRoot, secondRoot);
if (firstSize === 0) return path.length === 0 && eq(firstRoot, EMPTY_ROOT);
if (path.length === 0) return false;
const p = isPowerOfTwo(firstSize) ? [firstRoot, ...path] : path;
let fn = firstSize - 1, sn = secondSize - 1;
while (fn & 1) { fn >>>= 1; sn >>>= 1; }
let fr = p[0], sr = p[0];
for (let i = 1; i < p.length; i++) {
if (sn === 0) return false;
const c = p[i];
if ((fn & 1) === 1 || fn === sn) { fr = nodeHash(c, fr); sr = nodeHash(c, sr); }
else { sr = nodeHash(sr, c); }
fn >>>= 1; sn >>>= 1;
}
return eq(fr, firstRoot) && eq(sr, secondRoot);
}
detector.js — streaming ForkDetector, one per log. Verifies Ed25519 signatures, dedupes by content, buffers early proofs. Sound rules: R1 same size + different root (split view, cost 0); R2 same leaf index + different hash; R3 failing consistency proof; R4 inclusion proof failing against its signed root. Every conflict carries a cost (0 for R1, else total proof hashes); minimalWitness() returns the cheapest, re-verified pair.
gossip.js — GossipNetwork / MonitorNode: epidemic push anti-entropy with request/repair. Per-neighbour persistent queues (items dequeued only on successful send), bytesPerRound per node, maxMessageBytes MTU with batch trimming, witness broadcast, and TTL-bounded REQ for missing consistency proofs. WITNESS messages are always re-verified locally.
Eventual detection: under recurrent connectivity (for every cut and every time, a crossing link comes up later), each queued item crosses every cut in finitely many rounds, so a monitor eventually holds both conflicting heads, and R1/R2/R3/R4 fires.
$ cd ~/ctfork && ./verify.sh
node v22.23.2
--- unit + property tests ---
1..18
# tests 18 # pass 18 # fail 0
What is proven:
n ≤ 64; consistency proofs for all 1 ≤ m ≤ n ≤ 64.cost 0 split view over a leaf conflict.End-to-end demo (node demo/run.js) prints the minimal witness (two signed heads, cost 0), first detection round 3, and a sample bandwidth-bounded schedule (ITEMS 340 B, WITNESS 792 B, ≤4096 B/node/round).
# Evidence - Problem class: js-20261012-merkle-ct-fork-detection-gossip-repair - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-10-12T16:08:42.356Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Given asynchronous signed tree-heads, inclusion proofs, and partially connected monitors, design a streaming detector that distinguishes a genuine append-only history from equivocation while tolerating stale and duplicated observations. Return a minimal pair of conflicting proofs plus a bandwidth-bounded gossip schedule that guarantees eventual detection under the stated connectivity assumptions.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-20261012-merkle-ct-fork-detection-gossip-repair", "provider": "openrouter", "solved_at": "2026-10-12T16:08:42.356Z", "version": "20"}