The console rendered nothing against Quantus and I had reported tier 3 as
working on the strength of a unit-level test. It was not working. This is the
fault.
`qp_header::Header` is not `sp_runtime::generic::Header`. `number` is a plain
u32 where Substrate writes a Compact, and a `zk_tree_root: H256` sits between
`extrinsics_root` and `digest` — deliberately, so the ZK root has a fixed offset
in the header preimage that miners cannot shift by manipulating the digest.
Reading a Quantus header as a Substrate one consumes 2 bytes where 4 were
written. Every field after shifts, and the digest vector eventually reads a byte
that is not a known DigestItem index, which surfaces three layers away as
`TypeError: innerDecoder is not a function`. papi retried the subfollow forever,
`runtime$` never emitted, and the page stayed blank.
The patched codec carries both layouts and picks by **round trip**: a layout is
right only if decoding and re-encoding reproduces the input byte for byte, which
also catches a short read, trailing bytes and a non-canonical compact. Trying
them in a fixed order and taking the first that survives is deterministic;
choosing by inspecting a byte would not be.
Pinned per version rather than by bare package name, unlike the signers-common
patch: four copies of substrate-bindings resolve in this tree and blockHeader.js
is not identical across majors — a bare-name key fails the whole install trying
to patch 0.19.0 and 0.6.0.
Two things this cost, both recorded because they are the kind that recur:
- My first attempt replaced the codec with a bare [enc, dec] pair, which
dropped scale-ts's `inner`. This console's own block.state.ts does
`blockHeader.inner.digests.inner`, so that module threw while evaluating,
`createRoot().render()` never ran, and the page was blank with nothing in
the UI — the same symptom as the bug, from the opposite cause. The patch now
copies the original codec and overrides only the decoder.
- The Poseidon block hash is NOT a second blocker, which I had claimed on #1
and #4 without checking. `getHasherFromHeader` returns a function that
throws rather than throwing, and only the extrinsic Analyzer page consumes
`hasher$`. Everything else works without it.
Verified in a browser this time, not by unit test: against mainnet the console
renders live data — Finalized 53,989, Best 54,089, block time — advancing, and
matching what the node reports. Polkadot still decodes through the same patched
codec, and garbage is refused rather than mis-read.
Residual: four "fetch header failed TypeError: e is not an object" on first load,
from the per-block detail fetch, not recurring since. Not diagnosed.
Refs #1, #4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f