Diagnosed. It is not the Quantus header layout and it is not the header patch —
it is a missing null check in papi, surfacing through three layers of minified
code.
`archive.header$` in @polkadot-api/observable-client is
const header$ = (blockHash) => rawHeader$(blockHash).pipe(map(blockHeader[1]))
with no guard, and `archive_v1_header` answers `{"result": null}` for any hash
the node does not know — a normal answer, not a fault. scale-ts then reaches
`new DataView(null)`. V8 words that "First argument to DataView constructor must
be an ArrayBuffer"; Firefox words the identical error "e is not an object",
which mentions neither blocks nor headers nor the node.
The trigger is a chain switch. The block list still holds the previous chain's
hashes, every one is fetched from the new client, chainHead reports them
unpinned, and the archive returns null for each — hence a burst of identical
errors, one per listed block, milliseconds apart, then silence. Harmless: the
list refreshes a moment later. It just said nothing a reader could act on.
Reproduced deliberately (switch Quantus -> Heisenberg -> Quantus) rather than
waiting for it, and confirmed in the browser before and after:
before fetch header failed TypeError: e is not an object
after fetch header failed Error: no block header: the node does not know this block
The null check lives in the patched decoder because that is the narrowest place
we control that sees the value. The real fix belongs upstream in
observable-client, where `header$` should not map a null result through a codec
at all.
Refs #4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
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