No commissioned professional audit has happened. Most findings below were surfaced by our own adversarial passes over the guest/host code; two rounds (10 and 11) were AI-assisted full-source reviews by people outside the project, which is a real distinction but not the same thing as a paid audit. We fix what is found, re-run the regression to identical results, and record it here in the open. Independent review is explicitly invited — the open items at the bottom are the starting bounty list. If you find a way to make an invalid input prove valid, that is the finding that matters most.
⚠️ Two numbering schemes run through this repo, and they do not line up. The rounds below (1–9 self, 10–11 external) are the review passes recorded in this file. The internal audits (#1–#5, counted indocs/EXTERNAL_REVIEW.md) are a separate series, and they are whatreproduce/METHOD_ID,docs/ROADMAP.mdanddocs/PROVING.mdmean by "audit #3" and "audit #5". Audit #3 is not round 3: it is the 2026-08-03 pass that found the BIP30 F-1 canonical-chain break, and audit #5 is the 2026-08-04 pass that pinned the current guest.
The property that makes this worth reviewing: the prover runs real Bitcoin Core v28 consensus code
(unmodified interpreter.cpp, SignatureHash, libsecp256k1) inside a RISC0 zkVM, plus a Utreexo
accumulator. There is no consensus reimplementation to diverge from Core — see docs/SOUNDNESS.md
for the full trust base and the two portability shims.
Each release carries a PGP-signed SHA256SUMS.txt (signed in CI by the release-signing workflow). The
maintainer key is:
defenwycke <defenwycke@icloud.com>
777FE81F 8CC077FD 3D08055E 852C2B31 90F5B928
From a release's assets, download the binary keeping its asset filename (hazync-host-x86_64-linux-gnu) plus SHA256SUMS.txt and SHA256SUMS.txt.asc, then:
curl -LO https://github.com/bitcoin-ghost/hazync/releases/latest/download/hazync-host-x86_64-linux-gnu
curl -LO https://github.com/bitcoin-ghost/hazync/releases/latest/download/SHA256SUMS.txt
curl -LO https://github.com/bitcoin-ghost/hazync/releases/latest/download/SHA256SUMS.txt.asc
curl -s https://github.com/defenwycke.gpg | gpg --import # the key, from the account that publishes
# the releases (see "second source" below)
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt # must report a GOOD signature from the key above
sha256sum -c --ignore-missing SHA256SUMS.txt # → hazync-host-x86_64-linux-gnu: OKThe second source is https://github.com/defenwycke.gpg. A fingerprint published in the same
repository you are auditing is not independent evidence — anyone who could alter the releases could
alter this file. GitHub serves the maintainer's key from the account the releases come from, which is
a different system with a different compromise path, so agreement between the two is worth something.
Fetching it is also the whole import step above; gpg --recv-keys 852C2B3190F5B928 from a keyserver
works too and is a third, independent source.
Whichever you use, the fingerprint must be:
777FE81F 8CC077FD 3D08055E 852C2B31 90F5B928
The key expires 2028-04-25. After that, signatures on releases made before then still verify but
gpg reports the key as expired — that is the key's lifetime ending, not evidence of tampering.
gpg will also warn that the key "is not certified with a trusted signature". That is expected and
is not a problem with the signature: it means you have not personally signed this key, which nobody
has on a fresh keyring. What matters is the Good signature line and that the fingerprint matches the
one above. If you want the warning to go away, sign the key locally (gpg --lsign-key 852C2B3190F5B928) — but check the fingerprint against a second source first, because that is the step
the warning is asking you to think about.
--ignore-missing matters: the manifest covers both published binaries (CPU and CUDA), so a plain
sha256sum -c reports FAILED open or read and exits non-zero for the one you didn't download. That is
the command complaining about a missing file, not about a bad checksum — but it looks alarming, so use
the flag and check only what you fetched.
(If you saved the binary as host per the quick-start, sha256sum -c won't find it by name — either keep the asset filename as above, or run sha256sum host and compare the digest to the matching line in SHA256SUMS.txt by eye.)
A good signature plus a matching checksum means the binary is the maintainer's published build. Note
that the stronger guarantee is reproducibility, not the signature: anyone can rebuild the guest and
confirm its METHOD_ID matches the canonical id in reproduce/METHOD_ID, and any proof verifies against
that id regardless of who built the host. The signature adds provenance on top of that. (The canonical id
is re-pinned whenever the guest changes — most recently to 3f52baff (v0.10.0: libsecp's
ECMULT_WINDOW_SIZE 15→19, a compile-time speed trade with no consensus change), after the witness
wire-format change (68819a54, v0.9.0), the round-9 post-audit hardening (cb114426), the real-Core
pow.cpp retarget carve (ffdc6095), and chainparams-sourced consensus constants (7a8b29e0); see
reproduce/METHOD_ID for the authoritative lineage. The reproducible-build mechanism itself — pinned toolchain, Core v28.0, secp256k1 v0.5.1, Cargo.lock,
reproduce/Dockerfile — is unchanged, so the id stays reproducible, it is just a new value.)
| ID | Area | Severity | Status |
|---|---|---|---|
| SEC-1 | Witness-commitment bypass (has_witness host-controlled) |
med-high | fixed (6c63565) |
| SEC-2 | Accumulator delete trusted an unverified position |
high-crit location | fixed (6c63565) |
| SEC-3 | Prevouts vector length unchecked (OOB read) | low | fixed (6c63565) |
| S1 | Recursion self_id self-reference argument |
soundness | fixed (committed + verifier-asserted; adversarial wrong-id chain rejected) |
| S2 | Coinbase maturity / BIP68-height fed placeholder metadata | soundness | fixed (real coin metadata; validated on 741000) |
| S4a/b | BIP34 (height in coinbase), BIP30 (duplicate txid) | completeness | fixed (validated on 741000) |
| C1 | No automated regression harness | quality | fixed (check-full / regress execute-mode) |
| SEC-neg | Negative regression tests (reject-path coverage) | quality | done + continuously CI-enforced (prover/ci_negative_tests.sh): SEC-1 witness (witness_ok), SEC-2 position (all_ok/root_matches), COV-1 time-too-old, COV-2 merkle mutation, and the retarget / block-weight / sigop-cost reject-paths — each asserts the malicious input REJECTS and an honest baseline ACCEPTS, so a future guest change can't silently regress one |
| DIFF-retarget | The retarget carve (real pow.cpp) matches Core on real data |
quality | done + CI-enforced (prover/retarget_diff_test.sh): the guest's calc_next_bits — now Core's compiled CalculateNextWorkRequired — reproduces the actual on-chain nBits at all 476 mainnet retargets, agrees with an independent transcription over the vectors + a span sweep, and satisfies the clamp/powLimit/monotonicity invariants |
| DIFF-flags | Script-flag schedule is a sound superset of Core | quality | done + CI-enforced (host script-flags-test, sharing the guest's script_flags module): proves guest flags ⊇ Core's GetBlockScriptFlags at every activation boundary (retroactive base flags can only reject more — soundness-preserving), the buried soft-forks (DERSIG/CLTV/CSV/NULLDUMMY) flip at Core's exact heights, and the two script_flag_exception blocks behave (BIP16 → no flags, taproot → TAPROOT cleared) |
| S3 | Standalone block proofs don't bind to the real UTXO set | inherent | by design — real binding comes from the chain recursion; closed operationally by the archive-node bridge |
| BIP68-time | Block-proving path now commits real MTP(coinHeight−1) |
soundness | fixed + now CI-enforced (prover/test_bip68_locks.sh + test_bip68_real.sh: height- and time-based locks on real mainnet MTP, a real CSV-locked mainnet tx, and a pre-CSV control asserting we do not enforce where Core doesn't. Both harnesses previously only printed results and were not run — test_bip68_real.sh was in fact announcing "expect REJECT" while getting VALID, because it left the heights below the CSV activation gate so the branch never ran. Now they assert and run on every push.) |
| COV-1 | time-too-old: block timestamp must exceed MTP(prev 11) — was unchecked |
soundness | fixed + negative-tested (asserted in chain_step/aggregate/prove_range) |
| COV-2 | Merkle CVE-2012-2459 mutation flag was discarded (nullptr) |
soundness | fixed + negative-tested (capture Core's mutated and reject) |
| H1 | Block height host-controlled (flag/subsidy downgrade) | critical | fixed + negative-tested (w.height == prev.height+1; host adversarial #1) |
| H2 | Segmented chunks bound neither flags nor spending witness | critical | fixed + negative-tested (per-input binding digest; HAZYNC_H2_BADHEIGHT prove-seg) |
| H3 | In-block coin double-spend / ordering (inflation) | high | fixed + negative-tested (ordered multiplicity guard; host adversarial #3) |
| H4 | Coinbase never run through CheckTransaction |
med | fixed + negative-tested (host adversarial #4) |
| H5 | Multi-input tx: non-input_idx fee-prevouts unbound to the accumulator (inflation/theft) |
critical | fixed + negative-tested (per-tx input-list pre-pass; host adversarial #5) |
| H6 | Range verifier under-pinned the genesis in-boundary (in_epoch_start/in_roots/in_recent) → forgeable first retarget / phantom UTXO seed |
high | fixed (assert_genesis_in_boundary in verify-range; verify-any applies it when the range claims genesis) |
| H7 | Coordinator chained ranges by tip-hash only — no cross-range difficulty/MTP continuity | medium | fixed (verify-any now pins the genesis in-boundary + exposes nbits/epoch; coordinator _frontier_chain requires out_nbits/out_epoch(k) == in_nbits/in_epoch(k+1) across every seam) |
| H8 | Cross-mode journal laundering: block_proof (mode 1) commits a self_id-free journal that never aborts |
speculative | fixed (domain tag KIND_* is the first committed field of every recursion-consumed journal — ChainState/RangeState/ChunkOut — and asserted on every decode) |
| A1 | Bare ChainState (mode-2/5) receipt did not commit its anchor → a fabricated-anchor receipt is journal-indistinguishable from a genesis-anchored one |
verifier-hole | fixed (S5: anchor_id = dsha256(base anchor) committed + carried; new verify-chain pins it to genesis. Not exploitable via shipped verifiers before — they take only KIND_RANGE) |
| G3 | BIP141 witness-commitment check ran at all heights (Core gates on segwit ≥481824) → reject-valid stall in 433k–481823 | med (reject-valid) | fixed (gate at 481824; below, witness_ok=!has_witness = Core unexpected-witness) |
| G2 | unexpected-witness excluded the coinbase from has_witness |
low | fixed (coinbase now counted) |
| G5 | Block-level MoneyRange(nFees) implicit; subsidy+fee unguarded i64 add |
low | fixed (explicit MoneyRange + i128-safe bound, folded into subsidy_ok) |
| G1 | General BIP30 HaveCoin replaced by a structural argument (utreexo has no non-membership proof) |
low (bounded) | CLOSED (#54) — replaced by a coinbase-only sparse Merkle tree that proves non-membership directly; root journalled + seam-enforced, pinned empty at the genesis anchor. The ~1,983,702 ceiling no longer applies |
| G4 | 2-hour future-time limit | (not a bug) | intentionally not enforced (verifier-local wall-clock, unprovable in a zkVM) |
| N1 | Dead BlockInput.flags wire field (flags are guest-derived) |
hygiene | removed (guest+host+all sites, incl. host Spend.flags) |
| N2 | scriptPubKey not length-prefixed in the UTXO leaf |
fragility | fixed (length-prefixed in all 3 byte-identical leaf sites — changes every leaf hash/root) |
| N3 | tx_full_sigops returned legacy-only cost on a short prevouts blob |
fragility | fixed (fails closed: poison cost → sigops_ok=false) |
| #4 | P2SH sigop over-count: tx_full_sigops counted redeemScript sigops for every input (reject-valid near the sigop cap) |
med (reject-valid) | fixed (round 9: guard the redeemScript count with IsPayToScriptHash(), matching Core's GetP2SHSigOpCount; guest change → id re-baselined) |
| #6 | BIP30 grandfathered duplicate coinbases (91842 / 91880) require Core's overwrite | completeness | fixed (round 9: guest mandates the overwrite witness at exactly those two block hashes; the bridge emits it; 91842 proved end-to-end, RANGE-OK) |
| #8 | In-block-spend detection keyed on txid, not the coin leaf (pre-BIP34 coinbase-txid collision) | liveness (host-side) | fixed (round 9: both host witness-builders detect in-block spends by created.contains(&coin_leaf) — the guest's exact rule; guest unchanged, v0.7.2. A valid block was made unprovable — no invalid block was ever accepted) |
| R-1 | Coinbase vin[0] empty-guard |
robustness | fixed (soundness audit: empty-guard hardening against UB — a robustness fix, not a soundness hole) |
| — | External audit | — | open / wanted |
All three were validated by rebuilding the guest and re-running the full regression (block 170, block
741000, check-ibd genesis→550) to byte-identical tip hashes — i.e. the fixes change nothing on
valid data, they only reject the malicious cases they close.
The guest derived has_witness (and the wtxids) from host-supplied input. A malicious prover could
claim "no witness" for a segwit block with a missing or invalid witness commitment and have it prove
valid, even though Core rejects it — and it opened a witness-malleability divergence.
Fix: recompute has_witness and every wtxid in-guest from the raw transaction bytes, using
Core's own HasWitness() / GetWitnessHash(). The host can no longer influence the witness-commitment
decision. Block 741000 (segwit+taproot) still proves valid with an identical tip hash and 394 UTXO
leaves.
Leaf-count note (2026-07-27). The
394recorded here and below was measured against the then-currentblock_741000.json, which predated thecoin_mtp/coin_heightfields. Withoutcoin_heighta fixture cannot express an in-block spend (a coin created and spent inside the same block), so that coin never netted out and left one stray leaf. Regenerating the fixtures from an archive node (prover/fetch_block_rpc.py) makes it 393, and the same −1 appears on 130000 (127→126) and 140000 (330→329) — each of those blocks contains exactly one in-block spend. Tip hashes andcum_workare unchanged throughout, so this corrects the fixtures, not consensus. The historical figures are left as recorded.
delete(i, proof_i, proof_last) verified membership of the proven leaves but never checked that the
global index i actually matched them, nor that proof_last was the current rightmost coin. Fed
inconsistent values, a prover could corrupt the accumulator — the worst case being a spent coin
surviving (a double-spend). No working exploit was built, but the assumption was untested.
Fix: pin i to the proven leaf — its tree height must equal the proof's, and its local offset
(i − tree_offset) must equal proof_i.position (the local in-tree index) — and likewise pin
proof_last to last. (Subtlety: Proof.position is the local index, not the global one; a first
attempt that compared against the global i broke honest deletes at block 170 and was corrected.)
verify_input / check_tx / tx_full_sigops indexed spent[...] without asserting
spent.size() == tx.vin.size(); a short blob is an out-of-bounds read (the zkVM has no memory
protection). Failed closed in practice.
Fix: explicit length asserts on the prevouts vector in all three entry points.
A second adversarial review looked specifically for ways a mining-capable prover could make an invalid
block prove valid, and found four under-constraints between the (real, correct) Core code and the block
being proven. All four are fixed; each has a negative test that must reject, alongside an honest
baseline that must still be accepted. The execute-mode cases run self-contained via host adversarial
(and in CI); the segmented one runs on a GPU box.
chain_step/aggregate committed height = prev.height + 1 but validated the block using the
host-supplied w.height with no equality check. Height selects the script flags and the coinbase
subsidy, so w.height = 1 turned every soft-fork flag off (segwit/taproot outputs become
anyone-can-spend) and set the subsidy to 50 BTC, while the journal still committed the true height.
Fix: assert w.height == prev.height + 1. Test: host adversarial (#1). The range-fold path
was already bound by its genesis pin + adjacency check.
A chunk proof committed only the coin leaf, so the aggregation could accept a different valid spend of
the same coin, or the spend validated under attacker-chosen weaker flags. Fix: each chunk commits a
binding digest over (raw_tx, input_idx, prevouts, coin metadata, flags); the aggregation recomputes it
under the block's real flags and requires equality. Test: HAZYNC_H2_BADHEIGHT=1 host prove-seg
(aggregate rejects).
A coin created earlier in the same block bypasses the accumulator; the set that tracked it did not count
multiplicity or check ordering, so it could be spent twice (minting its value) or before it was created.
Fix: enforce creation by a strictly earlier tx and spend-at-most-once. Tests: host adversarial
(#3 double-spend, #3 spend-before-create).
The coinbase reached only the subsidy/BIP34/witness-commitment checks, never CheckTransaction, so a
malformed coinbase (bad-cb-length, out-of-range or overflowing output sum) could pass. Fix: run the
coinbase through real Core CheckTransaction plus an IsCoinBase assertion. Test: host adversarial (#4).
A second adversarial pass (three reviewers) attacked the H1–H4 fixes and swept the rest. H2/H3/H4 held up; H1 held on the folded path. It found one more critical and a cluster of range/coordinator anchoring gaps.
Each BlockInput carries its own prevouts blob, but only the entry at its input_idx is authenticated
(folded into the leaf + stump.delete). check_tx runs once per tx on the first input's blob and sums
every entry into the fee. So for a ≥2-input tx a prover puts a phantom high-value coin at another
index of the first input's blob — never authenticated — inflating the fee to ~21M BTC and minting it via
the coinbase (a sibling variant omits a BlockInput to skip a script check + a deletion → theft /
double-spend). Fix: a pre-pass ties the flat input list to each tx's real vin — exactly
tx_vin_count consecutive BlockInputs, sequential input_idx, one shared raw_tx + prevouts blob —
so every entry check_tx/sigops read is an authenticated coin. Test: host adversarial (#5), with an
honest 2-input baseline that must still pass.
verify-range pinned in_tip/in_leaves/in_nbits but not in_epoch_start (feeds the block-2016
retarget, propagates across fold seams → forgeable difficulty), in_roots (in_leaves==0 alone permits
phantom roots), or in_recent/in_time. Fix: assert_genesis_in_boundary pins the full genesis
boundary; verify-any applies it whenever a range claims to connect to genesis.
server.py chained verified ranges by tip-hash only, so in_nbits/in_epoch_start of range k+1 weren't
checked against range k's out_* (a range could claim an easier in_nbits and be mined cheaper).
Fix: verify-any pins the genesis in-boundary and now prints in_nbits/out_nbits/in_epoch/out_epoch;
the coordinator's _frontier_chain walks ranges from genesis requiring out_nbits/out_epoch(k) == in_nbits/in_epoch(k+1) across every seam (deploy: vranges gains those columns + a redeploy).
block_proof (mode 1) commits a self_id-free BlockOutput and never aborts; in principle a mode-1
receipt could be laundered as a fake prev if its bytes decoded as a ChainState with trailing
self_id == METHOD_ID. No exploit was constructed (the type-mismatched decode makes it very hard).
Fix: every recursion-consumed journal (ChainState, RangeState, ChunkOut) now commits a distinct
domain tag (KIND_CHAIN/KIND_RANGE/KIND_CHUNK) as its first field, and every consumer asserts it —
so a journal of the wrong type can never be laundered across modes.
A third pass (three reviewers: bypass H5–H8, consensus-flag surface, trust boundary). H5/H6/H8 and the genesis-catch were attacked and held. New findings, all fixed:
Script-flag / activation layer (guest — these were mostly reject-valid, i.e. the from-genesis prover would STALL on canonical blocks)
- H-S1 (high):
block_script_flagsignored Core'sscript_flag_exceptions. Core forces P2SH|WITNESS|TAPROOT on for all blocks except two historical violating blocks (BIP16 → no flags; Taproot ~709632 → no TAPROOT). The guest enforced TAPROOT everywhere and would permanently stall on the Taproot-exception block. Fix: rewroteblock_script_flagsto match Core'sGetBlockScriptFlagsexactly — always-on base + a block-hash exception table (hash passed tochunk_provetoo, bound via the H2 digest) + buried deployments. - H-S2 (high): BIP34 enforced from 227836; Core's
BIP34Heightis 227931 → guest rejected valid blocks in that 95-block window. Fix: 227931. - H-S3 (medium): BIP68 relative-locktime enforced with no CSV gate; Core only enforces it from 419328.
Fix: gate the relative-lock branch on
spend_height >= 419328. - H-S4 (low, accept-invalid): the old height-gated P2SH/WITNESS/TAPROOT were more lenient than Core below the gates (a proof there didn't imply Core-validity). Closed by the same always-on rewrite (H-S1).
- S1 / F1 (high): the coordinator chained ranges on a WEAKER seam check than the guest fold — it matched
tip-hash (and, after H7, nbits/epoch) but not the UTXO accumulator roots,
in_time, or the MTP window. A mid-chain range could fabricate its in-boundary UTXO set (spend non-existent coins / double-spend) or itsin_time(forge a 4× easier retarget) with a valid STARK, and be spliced into the frontier. Fix:verify-anycomputes a full boundary digest (boundary_digest: tip + normalized UTXO roots + leaves + nBits + time + epoch + MTP window) — exactly whatfold_rangebinds — and the coordinator's_frontier_chainrequiresout_bhash(k) == in_bhash(k+1)across every seam. Not live-exploitable before (frontier below the first retarget, single prover), now closed. - F2 (low):
_frontier_chainused an unordered SELECT with first-wins; addedORDER BY lo, tsand the full-boundary digest makes a preempting range have to match the real boundary anyway. - F3 (low): rows with no boundary digest (pre-migration NULL) are no longer chainable.
- S2 (medium foot-gun):
VERIFY_MODEdefaulted tomock(accept-everything) whenHAZYNC_HOSTwas unset. Fix: mock now fails closed unlessCOORD_ALLOW_MOCK=1. - S3 (low):
verify-anyoutput was scraped from stdout+stderr; now only the singleRANGE-OKstdout line. - Signature fail-closed:
verify_sigaccepted everything when the ed25519 lib was missing; now fails closed unlessCOORD_ALLOW_UNSIGNED=1.
Verified sound (attacked, no action): genesis constants (GENESIS_WORK etc. checked against real block 0),
retarget/MTP/PoW math, weight/sigop formulas, taproot/annex path, test-only env hooks (guest reads no env),
METHOD_ID handling. regress + full adversarial suite + honest segmented composition pass on the round-3 guest.
A fourth pass (three reviewers: C++/Core integration + patches, Utreexo accumulator + primitive math,
whole-chain no-inflation + UTXO carry) found no new soundness hole. Confirmed sound: the VerifyScript
invocation (correct precomputed data / amount / sigversion for legacy/segwit/taproot), ECDSA is real
libsecp256k1 (the k256 acceleration experiment has since been removed — the guest is pure Core), the SHA-256 accelerator is byte-identical, the
serialize shim is consensus-neutral, static-ctor tagged-hash init covers all paths, the accumulator
delete/proof handling and num_leaves/root recomputation, all primitive math (check_pow/SetCompact,
add_work, calc_next_bits clamping, subsidy halving, merkle root), global no-inflation, and UTXO carry
(no resurrection; w.new_outputs/w.wtxids/inp.flags confirmed dead/unused). Hardening applied:
- Bench backdoor fenced.
verify_inputshort-circuited to a fixed test-vector ECDSA result for two magic flag values (0xB0/0xB1) — a "return valid" path, unreachable in consensus (block_script_flagsnever yields those) but now compiled out unlessHAZYNC_ECDSA_BENCHis defined. MiniReaderfails closed.read/ignorenow trap on any read past the buffer end (was an uncheckedmemcpy— OOB-read only, never accept-invalid, but now a clean rejection).- Reference-spec doc corrected.
accumulator/src/lib.rsnow states the guestutreexo.rsadds the SEC-2 pinning the reference oracle lacks (the proven guest is the authority).
Update 2026-07-30 — the domain-tag item below is now FIXED, and "non-exploitable" was wrong. Leaf
and interior hashes are domain-separated as of canonical id 85dc0b56… (since superseded by
be5e0528… for RUSTSEC-2026-0220, by 71790584… in v0.14.0, by dfc9eeda… for the #54 BIP30
SMT plus audit #3, by b161735a… in v0.16.0 when #88 removed the repo checkout path from the id, and
by 4722cec8… for audit #5's guest guards, by b62d2a60… for the #135 chunk-payload
re-baseline (#136 read_slice + #137 per-transaction grouping), and by 1d6c3792… for parallel block
validation — see reproduce/METHOD_ID for the full chain). A leaf preimage is
57 + |scriptPubKey| bytes, so a 7-byte scriptPubKey gives a 64-byte preimage — the width an interior
node hashes — and the stated barrier (leaf preimages open with an uncontrollable txid) is a grinding
cost rather than a separation, since a txid is the hash of a transaction an attacker composes. See
docs/SPEC.md §3. The other two items here remain deferred.
Documented, not changed (non-exploitable, deliberately deferred to avoid churn/re-prove): a trailing-byte
r.p==r.e parity assert (trailing bytes are inert — txid is PoW-bound); explicit leaf/internal-node hash
domain tags (implicit separation already holds because every leaf preimage begins with an uncontrollable
txid); and removing the dead new_outputs/wtxids/inp.flags witness fields.
Fifth pass (three reviewers: a regression-hunt inside the fixes, a fresh full-surface sweep, and a docs/web-page currency check). The regression hunt traced every H1–H8 + flag/coordinator fix against Core v28 and found no regression — the fixes are correct as written. Net: one trivial fix, one documented gap, one false alarm, plus a large docs-currency pass.
- F2 (low, accept-invalid) — FIXED. Block weight omitted Core's
4*(80 + CompactSize(ntx))header + tx-count term, so a block could sit up to ~324 WU overMAX_BLOCK_WEIGHT. No inflation; now matches Core'sGetBlockWeight. - F1 (flag always-on "diverges from Core") — NOT A BUG. Two independent reviewers and Core's own
GetBlockScriptFlags(read on the box) confirm the base P2SH|WITNESS|TAPROOT is always-on with exactly the two exception blocks — the exact code a from-genesis IBD runs (Core itself would stall otherwise). Height-gating would be the accept-invalid behaviour already flagged as H-S4. No change. - F3 (low, pre-BIP34 BIP30 duplicate-coinbase overwrite) — FIXED + tested. The two historical
duplicate-coinbase blocks (91842 / 91880, below the BIP34 height) have distinct accumulator leaves
(the leaf commits height, so no collision and the "collision-free" claim holds), but pre-enforcement Core
overwrites the old outpoint whereas the guest kept both, leaving one extra leaf Core discards — which
a from-genesis prove crossing height ~91842 could later spend. Fix: at exactly those two block hashes
the guest now deletes the superseded coinbase leaf, recomputed from this block's coinbase at the
host-supplied old height/mtp (the duplicate coinbase is byte-identical), so the delete can only remove a
genuine earlier duplicate of this coinbase's outpoint, and it is mandatory at those hashes (a prover
cannot skip it). BIP34 (enforced from 227931) makes coinbases unique thereafter, so no later duplicate can
occur. Test:
host check-bip30on real block 91842 — honest overwrite accepted with a matching root, skipping it rejected, wrong old-height rejected. In CI. - Docs currency: rewrote the stale
PROVING.md(it described recursion as unimplemented and handed out a pre-hardeningchain_step), corrected the READMEACCELERATIONrepo-map line + status/audit language,HAZYNC_ARCHITECTURE.md's BIP34 height (227836→227931), annotated the stale 741000 evidence log (402→394), added a working-notes banner toHAZYNC_ARCHITECTURE.md, and updated the live page's self-audit copy to the four-round history.
Sixth pass (three reviewers: guest consensus, host witness-binding, coordinator seam). The soundness core held (H1–H8 / SEC / F1–F3 all survived concrete attack), but the host reviewer found one genuine MAJOR hole in the coordinator seam.
- H9 (MAJOR, over-issuance / weak-flags splice) — FIXED. The coordinator chains independently-verified
ranges on tip-hash + full
boundary_digestcontinuity, but the digest bound the UTXO set / difficulty / MTP window and not the block height, andprove_rangenever tiedw.heightto the in-boundary's chain position. So a block mined onto the real tip (real prev-hash, real UTXO root, real current difficulty ⇒ real PoW) but labelled a false low height would haveblock_subsidy(low)= up to 50 BTC (over-issuance) andblock_script_flags(low)omitting DERSIG/CLTV/CSV/NULLDUMMY (a script invalid at the true height validates) — a self-inconsistent(height, boundary)pair the guest never rejects standalone. The guestfold_rangerejects it (hi+1==loadjacency); the coordinator's non-folding chain did not, so it could splice into the genesis-anchored frontier composite. Fix (defence-in-depth, two independent layers): (1) coordinator_frontier_chainenforceslo==1at genesis andlo==prev_hi+1at every seam; (2) the hostboundary_digestnow binds height (out-boundary =hi, in-boundary =lo-1) from the in-circuit-committedRangeState.lo/hi, soout_bhash(k)==in_bhash(k+1)chaining structurally requires adjacency. Either layer alone closes it. Verified: a mislabeled-height range no longer advances the frontier while honest contiguous + out-of-order gap-fill still do. - Coordinator hardening (web/liveness). Stored XSS via the contributor
handlerendered into the public dashboard throughinnerHTML— fixed at both layers (clean_handlestrips< > & " '; the dashboard escapes every render sink). Liveness DoS — the up-to-120 sverify-anyran inside the global write lock, stalling every claim/heartbeat/submit — moved out of the lock (re-check status on commit; unique temp paths). Stat double-count —provensummed overlapping ranges — replaced with an interval-merge. - Completeness / robustness (guest + host). Added the nVersion soft-fork rejection (Core
ContextualCheckBlockHeader: rejectv<2 @227931,v<3 @363725,v<4 @388381) closing an accept-invalid gap; assertedheader.len()==80; bounds-guardedcheck_input_locks'input_idx(fail-closed-43).
Validated in execute mode with no regression: regress (block 170 byte-exact), adversarial (all
holes reject), check-full 741000 (byte-exact tip, UTXO 394 — 393 with the regenerated fixtures, see the leaf-count note above), check-bip30, and an nVersion negative that
rejects. METHOD_ID changed (guest changed) ⇒ prior proofs invalid, re-proven from genesis.
Seventh pass (three reviewers over commit a094ae5: coordinator changes, guest/host changes, holistic
H9-closure sweep). All three clean on soundness. The sweep confirmed H9 is closed on all five proof
paths (chain_step, aggregate, chunk_prove, prove_range, fold_range) with the two fix layers each
independently sufficient. One genuine new-in-round-6 minor was fixed: the lock-free submit() had no cap
on concurrent verify-any subprocesses (fan-out DoS on the small box) — bounded with a Semaphore(cpu_count).
Two other observations (the 0-999 seed range is unprovable by design so blocks 1–999 are proven as
single-block ranges; by_in first-wins can understate the frontier) are pre-existing and non-soundness.
Eighth pass (five reviewers, one dimension each, each told to break it: consensus completeness,
enforcement gating, accumulator soundness, the FFI/VerifyScript boundary, and the end-to-end trust
scope). Four dimensions came back sound — the accumulator (no inclusion-forgery / skipped-delete /
double-spend), the FFI boundary (script runs against exactly the leaf-committed coin with the full
prevout set force-computed, MissingDataBehavior::FAIL), enforcement gating (every proving-mode check on
an asserted panic path; mode 1 confirmed debug-only + tag-un-launderable), and consensus completeness (all
Core block-connection rules bar the deviations below, comments verified against the code). One
verifier-hole and five completeness deviations were found; none allowed minting, theft, or double-spend on
the real chain today. All fixed except G4 (unprovable — left documented). See the status table above and
the finding-by-finding detail + verification in docs/AUDIT_2026-07.md.
- A1 (verifier-hole) — FIXED. A bare
ChainStatereceipt (mode 2chain_step/ mode 5aggregate) committed no record of the anchor it bottomed out at, so anis_base=1receipt built on a fabricated anchor (arbitrary height/UTXO/work/easy nBits) was journal-indistinguishable from a genesis-anchored one. Not reachable through any shipped verifier —verify-range/verify-anydecode onlyRangeStateand pin the full genesis in-boundary — but a standing foot-gun for anyone handed a raw chain receipt. Fix (S5): the guest commitsanchor_id = dsha256(base-anchor journal)in the base step and carries it forward unchanged; new host commandverify-chainverifies the STARK, assertsself_id==METHOD_ID+ theKIND_CHAINtag, and pinsanchor_id == dsha256(genesis_anchor)— the chain-track analogue ofverify-range's genesis pin. A checkpoint-anchored proof correctly fails it. - G3 (medium, reject-valid/liveness) — FIXED. The BIP141 witness-commitment check ran unconditionally;
Core enforces it only from segwit activation (481824). A canonical pre-activation block carrying an
early commitment output but a witness-free coinbase would be wrongly rejected — a from-genesis stall in
433k–481823. Fix: gate the commitment at 481824; below activation
witness_ok = !has_witness(Core'sunexpected-witness, which now also covers the coinbase — G2). - G5 (low) — FIXED. Added the explicit block-level
MoneyRange(total_fee)+ an i128-safesubsidy + fees ≤ MAX_MONEYbound (Core'sbad-txns-accumulated-fee-outofrange), folded into the already-gatedsubsidy_ok, rather than relying on anchor-integrity induction. - G1 (low, bounded) — CLOSED (#54), was a gated invariant. A utreexo
Stumpcannot prove non-membership, so Core's general BIP30HaveCoinlookup used to be replaced by a structural argument: BIP34 (asserted, ≥227931) forces coinbase-txid uniqueness, the two pre-BIP34 duplicates are handled by F3, and a non-coinbase duplicate is a double-spend the accumulator rejects. Sound, and it EXPIRED — at ~1,983,702 (≈2046) a BIP34 height-push can reproduce a pre-BIP34 coinbase scriptSig and the reasoning stops holding. A second accumulator now proves the property instead: a coinbase-only sparse Merkle tree (txid -> unspent output count) whose root is journalled beside the utreexo roots and enforced at the fold seam. Absence and a zero count are the same state, so "prove it is absent" is exactly "prove BIP30 is satisfied" — and a fully-spent duplicate stays legal, which it must. The ceiling is gone because the check no longer depends on any property of scriptSig encoding. The guest DERIVES the coinbase txid, its spendable-output count and the spent-coinbase list itself and takes only the proofs from the witness;is_genesis_anchoredpins the tree to empty at the anchor. - N1/N2/N3 (hardening). Removed the dead
BlockInput.flags(guest+host; flags are guest-derived — a refactor trap); length-prefixedscriptPubKeyin all three byte-identical UTXO-leaf sites (future-proofs the preimage — changes every leaf hash/root); andtx_full_sigopsnow fails closed on a short prevouts blob instead of under-counting.
Validated in execute mode with no regression (real Core code in the zkVM): regress (block 170
byte-exact tip), check-full 130000 (pre-segwit G3 branch, VALID) and 741000 (active G3 branch + taproot +
~670 inputs, VALID, tip byte-exact to the pre-change value — proving N2 is consensus-neutral),
741000_badwit correctly REJECTED (witness_ok=false only), and check-bip30 PASS. METHOD_ID changed
(guest + leaf format changed) ⇒ all prior proofs invalid, re-proven from genesis. cargo fmt clean; the
prove-based adversarial suite + clippy are re-run on the GPU box as part of the re-prove.
Three fixes after round 8, then an empirical pass over the chain's era transitions on a real archive node.
tx_full_sigops counted redeemScript sigops for every input, not just P2SH ones — over-counting legacy
inputs versus Core's GetP2SHSigOpCount, so a from-genesis prover could reject a Core-valid block near
the sigop limit. Fix: guard the redeemScript count with IsPayToScriptHash(). Guest change →
METHOD_ID re-baselined 601d7ca2… → 36a0415d…. (#4)
The two pre-BIP30 blocks whose coinbase duplicates an earlier still-unspent coinbase require Core's
overwrite. The guest now mandates the overwrite witness at exactly those two block hashes and the bridge
emits it; validated end-to-end (91842 proved, RANGE-OK). (#6)
Both host witness builders (build_full for check-full, and build_block_carried, the production
bridge) cancelled in-block-created coins (H1) by txid membership. The guest cancels by leaf
membership — and the leaf carries the coin's creation height. They diverge on a pre-BIP34 coinbase-txid
collision: a block spending an older coin whose funding tx shares a txid with a tx in the spending block.
The guest keeps that coin external (its leaf has the older height); the txid heuristic falsely cancelled
it. Effect: check-full false-failed on busy blocks with in-block spends, and — the real bug — the bridge
would emit a witness the guest rejects, stalling a genesis→tip proof at the first colliding-txid block.
Fix: both builders now detect in-block spends by created.contains(&coin_leaf) — the guest's exact
rule. Host-only; guest unchanged → METHOD_ID still 36a0415d. Note this is a case where the guest was
already correct and the host builder was wrong: it is a liveness bug (a valid block made unprovable), not
a soundness one (no invalid block could be accepted). (#8, v0.7.2)
On a real archive node, every representative era block validates at 36a0415d with all consensus flags
true: segwit (500000), taproot (750000), big-block (741000, ~6.4k inputs), and the pre-BIP34
collision case (130000), plus the COV/SEC reject-path suite. The in-block-leaf bug above is the only
defect the pass surfaced.
Outcome: re-baselined to
71790584…(v0.14.0). The two guest-side findings below —cshims.candmulti_check— could not ship without a newMETHOD_ID, so they were batched into one deliberate re-baseline rather than triggering two. It discarded 46,177 proven blocks: the attribution ledger was archived (coordinator.db.be5e0528, 52 MB — who proved what is worth keeping), but the 12 GB of receipts were deleted, since nothing can verify them under the new id and re-proving is required regardless. Taken at 4.8% of the chain precisely because that cost only grows — the previous re-baseline was taken when the board held zero blocks, so this is the first with real work on it. Everything else from both reviews shipped earlier in v0.13.5 underbe5e0528….
The first reviews by people outside the project. Two independent passes, both AI-assisted full-source reviews rather than a commissioned professional audit — that distinction matters and is kept throughout. Neither found a soundness break in the guest, the verifier or the coordinator frontier.
Both landed on the same two places as the highest residual risk: the C++ bridge compiled into the guest, and the accumulator. That agreement is itself a finding — it is where outside eyes go first.
No defect found in the Rust. Its largest stated unknown was the two files it could not read,
verify_input.cpp and cshims.c, observing that a missing domain tag in the C++ leaf builder "would
undermine the entire security claim". Both were then read directly.
verify_input.cpp — not a defect. The C++ leaf builders do apply TAG_LEAF, and
scripts/check-utreexo.sh has been asserting agreement across host Rust, guest Rust and guest C++ in
CI since the domain-separation re-baseline. The stronger argument is structural: the host builds
witnesses with Rust leaves and commits root_next, the guest rebuilds them in C++ and asserts
root_matches — so a tag mismatch would change every leaf hash and no proof would verify at all.
Every proof on the board is an execution of that check.
cshims.c — two real defects, both latent. _sbrk checked only its upper bound while
_malloc_trim_r calls it with negative increments (confirmed in the linked guest ELF), and strtoul
ignored its base argument entirely. Neither can produce a false proof: all four strtoul call sites
are libstdc++/newlib scaffolding with none on the consensus path, and heap exhaustion is fail-closed
(1 MiB heap, -fexceptions, no catch anywhere in the guest validator, so bad_alloc reaches
std::terminate). FIXED here (#56), as part of the re-baseline this change forced. cshims.c compiles into the
guest, so the fix could not land without invalidating every proof on the board; it waited until a
re-baseline was being done deliberately rather than triggering one on its own.
Also filed from this round: the BIP30 structural argument's hard ceiling at height ~1,983,702 (#54),
and std::random_device being linked into the guest though never called (#55).
Verdict: consensus-critical core sound. One High, one Medium, three Low.
| ID | Finding | Status |
|---|---|---|
| H-1 | Script injection in release-sign.yml — the release tag was interpolated into three run: blocks in the job holding GPG_PRIVATE_KEY and contents: write |
FIXED (#57) |
| M-1 | hazync-bridge.service ran as root with no hardening |
FIXED, stage 1 (#61); path migration → #58 |
| L-1 | API did not distinguish genesis-anchored from mid-chain | FIXED (#62) |
| L-2 | Accumulator panic paths reachable from untrusted proof data | FIXED (#63) |
| L-3 | multi_check hardcodes coin metadata |
FIXED (#56) — held back until a re-baseline was happening anyway, because a guest comment moves the id; landed, and in main |
H-1 was the serious one and is worth stating plainly. GitHub substitutes a ${{ }} expression
into the script text before bash parses it, so a tag containing shell metacharacters executed as
code — and workflow_dispatch made that input directly supplyable by any account with write access.
The payoff was the release-signing private key and, with it, forged SHA256SUMS.txt.asc for every
published binary: the artefacts this document tells people to verify. The tag now arrives through
env: and is shape-checked, and scripts/check-workflow-injection.sh bans ${{ }} in any run:
block repo-wide, with a positive control.
Note the limit of reproducibility as a defence here: it protects anyone who rebuilds the guest and
checks METHOD_ID, and does nothing for someone who does exactly what this document says and verifies
the signature.
L-2 was larger than reported. Beyond the three panic paths, delete could mutate before
rejecting — the disjoint-tree branch set a root and only then discovered a malformed rightmost proof,
leaving a corrupted Stump behind a false return. Worse than the panic, because the caller believes
nothing happened. Every rejection path now returns before touching state.
The report also asked to "confirm the fork hazync-utreexo stays in sync". There is no fork —
hazync-utreexo is accumulator/, consumed by path from prover/host and audit-fuzz. The guest
does not use it at all; it has its own prover/methods/guest/src/utreexo.rs.
hazync-coordinator.servicesetsProtectHome=truewhile reading/root/bridge_bundles— whichProtectHomemakes inaccessible.can_serve_witnessprobes withos.path.exists, so it fails silently to the legacy witness window: no exception, no log (#60).verify-anyhad no CI coverage at all — the command the coordinator runs on every submission (#65). The standalone verifier was well covered, but it is a different path with a different genesis condition.- A test that passed its own positive control. The first draft of the L-2 tests passed against the
unfixed code, because fully random proofs never get past
verify()and so never reached the paths. They now build genuine proofs and perturb one field. build-release.shbind-mounts the live working tree, so switching branches mid-build silently produces a binary that is a mixture of them, withMETHOD_IDand the smoke tests all reporting fine.- Any guest-source edit that moves line numbers changes
METHOD_ID— including comments. Adding fifteen lines of pure comment toprover/methods/guest/src/main.rsmoved the id frombe5e0528…to2a334ebe…; the mechanism is Rust embedding file/line into panic metadata. This was measured only because the opposite was assumed and CI was asked to prove it. The consequence is easy to get wrong: a "harmless" comment in the guest is a re-baseline that invalidates every proof on the board, so guest documentation must be batched into a change that is already re-baselining. Recorded inreproduce/METHOD_ID. It is also why L-3 above waited for a re-baseline instead of landing on its own — it has since landed, and this line said "fixed but unmerged" for long after that was true. Verified 2026-08-11: the doc comment is inmain's guest. A status line that goes stale in the pessimistic direction is the cheaper failure, but it still cost a branch audit to disprove.
- S1 — recursion
self_idis host-supplied. The chain/aggregation guests callenv::verify(self_id, prev_journal)with a host-controlledself_id; the concern is the IVC self-reference trap (a nestedself_id ≠ METHOD_IDsmuggling a malicious guest's receipt). Fixed:self_idis committed and the verifier asserts== METHOD_IDat every level; the positive chain verifies and an adversarial wrong-id chain is rejected. Argument written up inSOUNDNESS.md §3. Single-block proofs were always unconditional here; this hardened the recursive case. - S2 — maturity / BIP68-height fed placeholder metadata. The harness set every spent coin's
coin_height/coin_is_coinbase/coin_mtpbenign, so maturity + BIP68 never fired on real blocks. Fixed: the fetcher/bridge sources each spent coin's real height + coinbase flag and threads them into the witness; both checks fire on real blocks (validated on 741000). While closing this we also found and fixed a latent header bug — the header builder hardcoded version 1 (masked by the version-1-era test vectors), so PoW was wrong on modern blocks; it now uses the real versionbits. - S3 — standalone block proofs don't bind to the real UTXO set.
prove_fullfabricatesroot_prevfrom the block's own prevouts + filler, so a standalone proof attests internal validity + accumulator consistency, not "these coins were in mainnet's UTXO set at height N". Not a bug — inherent to standalone testing. Real-UTXO binding comes from the chain recursion carryingroot_next(N−1) == root_prev(N)from a trusted anchor; operationally the archive-node bridge drives the accumulator from the real coin set. Stated plainly so results aren't over-read. - S4 — BIP34 / BIP30. Both added and validated on 741000 (
bip34_ok/bip30_ok). BIP68-time is fixed too: the block-proving path commits the coin's real creation median-time-past (coin_mtp, leaf-committed and unforgeable) and the relative-lock branch is gated onspend_height >= 419328+version >= 2with the disable bit clear — matching Core's CSV activation (check_input_locksinverify_input.cpp). The earliercoin_mtp = 0placeholder only ever existed in the standalonebuild_fulldiagnostic harness (fabricated anchor), never the real IBD/chain proving path that produces published proofs. Consistent with the S2/BIP68 rows in the table above. - C1 — regression harness. Added:
check-full/regressrun known blocks + the adversarial inflation case in execute mode (seconds, no proving) and assert the flags; standard pre-flight before any GPU prove. - C2 — duplication between
build_block/build_fullandchain_step/aggregate(the witness_ok conjunction drifted once). Low priority; a shared helper would prevent re-drift. Open, cosmetic. - C3 — fabricated anchor timestamps/nbits in standalone runs — resolved by real recursion (tied to S2/S3). Open only for near-retarget standalone runs.
- H1 — a committed
.pyc; H2 — resolved: all threeCargo.lockfiles (accumulator/,prover/,prover/methods/guest/) are now committed for reproducible builds; H3 — README refresh. Housekeeping.
A pass over Bitcoin Core's block-validation surface for rules we might not enforce found two — both real consensus rules Core checks, both now fixed (see the COV rows above):
- COV-1
time-too-old: a block whose timestamp is ≤ the median-time-past of the previous 11 blocks is invalid;chain_step/aggregate/prove_rangenow assertblock_time > prev_mtp. - COV-2 merkle mutation (CVE-2012-2459): the
ComputeMerkleRootcall discarded Core'smutatedflag (duplicate-txid tree malleability); it is now captured and rejected.
Deliberately not enforced (documented trust boundaries, not gaps): the 2-hour future-time limit
(node-local wall-clock, unprovable); standardness/policy (not consensus). "Only the coinbase is a
coinbase" is covered by Core's CheckTransaction (null-prevout rejection for non-coinbase inputs).
The COV fixes were validated for no-regression (check-ibd 550 + 741000 + demo all still VALID) and by
adversarial negative tests (prover/ci_negative_tests.sh, evidence prover/evidence/cov_negatives.txt):
- COV-2: an honest
[A,B,C]tx list and a malleated[A,B,C,C](last tx duplicated) produce the identical merkle root — the CVE-2012-2459 collision — but the malleated one is flaggedmutated=1and rejected onmerkle_ok. - COV-1: with the previous-11 median-time-past forced to equal the block's own timestamp,
check-fullrejects withtime_ok=falseand every other flag true (isolated to the timestamp check); the same block without the knob is VALID.
- SEC-neg — DONE and continuously CI-enforced. Negative regression tests proving the fixes
reject the malicious cases (not just that valid blocks still pass). Both halves below demonstrate
rejection, and — beyond SEC-1/SEC-2 — the retarget, block-weight, and sigop-cost reject-paths are
now covered too:
prover/ci_negative_tests.shdrives each ofretarget_ok/weight_ok/sigops_okfalse on a crafted malicious block (with an honest baseline that must still pass) and runs in CI, so a future guest change cannot silently regress one. Anything earlier that framed those three reject-paths as untested or a follow-up is stale.- SEC-1 (witness) — done.
prover/make_negative_tests.pyproducesblock_741000_badwit.json(one byte flipped inside a transaction's witness → wtxid changes, txid does not).check-fullreportsmerkle_ok=true, witness_ok=false, all_ok=false— the block is rejected specifically on the BIP141 witness commitment, confirming the check is enforced and unskippable. - SEC-2 (position) — done. A test-only host knob (
HAZYNC_SEC2_BADPOS=1) corrupts the first spend'sglobal_posto a different in-range index while leaving its inclusion proof honest — the exact inconsistency the normal path can't express (both fields derive from the same accumulator lookup).check-fullon block 170 then reportsall_ok=false, root_matches=falsewith every other flag true, isolating the rejection to the hardeneddelete's position check. Without the knob the same block is VALID. The knob is inert unless the env var is set. Seeprover/make_negative_tests.py.
- SEC-1 (witness) — done.
- BIP68 time-based (soundness) — FIXED. The correct value is Core's
GetMedianTimePast(coinHeight−1). The block-proving path previously committed the creating block's raw timestamp (or0), skewing the required-elapsed test so a premature time-based relative-locked spend could prove valid.- The check is proven correct on REAL mainnet data.
prover/test_bip68_real.sh(evidenceprover/evidence/bip68_real_mainnet.txt) runs the realcheck_input_lockson a real mainnet tx —3fa669af…in block 958250, a 90-day Taproot CSV lock — with the realcoin_mtp = MTP(945408)andspend_mtp = MTP(958250). The coin is 90.2 days old, mainnet accepted it, and the check returns VALID; a coin ~0.3 days younger is REJECTED (-42). - The proving path now commits the real value. A coordinated host+guest change: the guest's
validate_blockcommitsmtp(=median(prev.recent_times)=MTP(h−1)) on created-output leaves, and every host builder derives the same value from the chain it has processed (theblock_mtpwindow in the IBD path, mirroring what an archive node holds for free) — so the creation-side and spend-side leaves match. Validated:check-ibdgenesis→550,check-full741000, and the 170→172 chain demo all remain VALID with identical tip hashes (those are header-derived; only the internal leaf MTP is now correct). No fetcher dependency for the IBD path — the host derives the MTP itself.
- The check is proven correct on REAL mainnet data.
- External audit — especially of the accumulator (the one non-Core component) and the recursion binding. Wanted.
The hard part — proving the real Core consensus code, not a reimplementation — is done and is the thing that removes the soundness gap every prior effort carried. The findings so far are around the edges (a host-controllable witness flag, an under-constrained accumulator index, placeholder metadata, a couple of missing rules) and are all fixed and regression-checked. What remains is one time-lock input from the bridge and — the real ask — independent adversarial review. Plainly: the Utreexo accumulator is the single most likely location of any remaining soundness bug — it is the one non-Core component, differentially fuzzed but not externally audited, and it is where we most want outside eyes.