Files
qapi/patches/@polkadot-api__substrate-bindings@0.21.0.patch
rob thijssen 84f60959d9 fix: decode Quantus block headers, which is what was actually broken
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
2026-09-16 09:37:55 +03:00

100 lines
3.6 KiB
Diff

diff --git a/dist/codecs/blockHeader.js b/dist/codecs/blockHeader.js
index 0675e59633a14c34148e90aa8c32bcfbc437330c..691ac4ede3c816bad444a6054860ed0bb3fcd37d 100644
--- a/dist/codecs/blockHeader.js
+++ b/dist/codecs/blockHeader.js
@@ -1,4 +1,4 @@
-import { enhanceCodec, Bytes, _void } from 'scale-ts';
+import { enhanceCodec, Bytes, _void, u32 } from 'scale-ts';
import '../utils/ss58-util.js';
import './scale/Binary.js';
import './scale/bitSequence.js';
@@ -33,7 +33,23 @@ const diggest = Variant(
[0, 4, 5, 6, 8]
);
const hex32 = Hex(32);
-const blockHeader = Struct({
+
+// Quantus patch: this console serves two header layouts.
+//
+// `sp_runtime::generic::Header` encodes `number` with #[codec(compact)] and has
+// no field between `extrinsics_root` and `digest`. Quantus's `qp_header::Header`
+// does neither: `number` is a plain u32, and a `zk_tree_root: H256` sits before
+// the 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. That surfaces three layers away as
+// `TypeError: innerDecoder is not a function`, papi retries the subfollow
+// forever, `runtime$` never emits, and the page renders nothing at all.
+//
+// See quantus/papi-console#4.
+const substrateHeader = Struct({
parentHash: hex32,
number: compactNumber,
stateRoot: hex32,
@@ -41,5 +57,63 @@ const blockHeader = Struct({
digests: Vector(diggest)
});
+const quantusHeader = Struct({
+ parentHash: hex32,
+ number: u32,
+ stateRoot: hex32,
+ extrinsicRoot: hex32,
+ zkTreeRoot: hex32,
+ digests: Vector(diggest)
+});
+
+const asBytes = (input) =>
+ typeof input === "string"
+ ? Uint8Array.from(
+ input
+ .slice(2)
+ .match(/../g)
+ ?.map((b) => parseInt(b, 16)) ?? []
+ )
+ : input;
+
+const sameBytes = (a, b) =>
+ a.length === b.length && a.every((x, i) => x === b[i]);
+
+// Decide by round trip, not by guessing. 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 the layouts in a
+// fixed order and taking the first that survives that check is deterministic;
+// picking one by inspecting a byte would not be.
+const decodeEitherLayout = (input) => {
+ const bytes = asBytes(input);
+ let firstError;
+
+ for (const codec of [substrateHeader, quantusHeader]) {
+ try {
+ const value = codec.dec(bytes);
+
+ if (sameBytes(codec.enc(value), bytes)) return value;
+ } catch (error) {
+ firstError ??= error;
+ }
+ }
+
+ throw firstError ?? new Error("block header matches no known layout");
+};
+
+// Keep every property the original codec carried and override only the decoder.
+// A scale-ts Struct exposes `inner` — its field codecs — and consumers reach for
+// it: this console's own block.state.ts does `blockHeader.inner.digests.inner`.
+// Replacing the codec with a bare [enc, dec] pair drops that, block.state.ts
+// throws while its module is evaluating, `createRoot().render()` never runs, and
+// the page is blank with no error on screen — the same symptom as the bug this
+// patch exists to fix, from the opposite cause.
+const blockHeader = Object.assign([], substrateHeader, {
+ 0: substrateHeader.enc,
+ 1: decodeEitherLayout,
+ enc: substrateHeader.enc,
+ dec: decodeEitherLayout
+});
+
export { blockHeader };
//# sourceMappingURL=blockHeader.js.map