Files
qapi/patches/@polkadot-api__substrate-bindings@0.21.0.patch
rob thijssen ad670842b3
All checks were successful
deploy / build (push) Successful in 2m36s
deploy / deploy-web (push) Successful in 7s
fix: say which block is missing instead of "e is not an object"
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
2026-09-16 10:36:58 +03:00

113 lines
4.4 KiB
Diff

diff --git a/dist/codecs/blockHeader.js b/dist/codecs/blockHeader.js
index 0675e59633a14c34148e90aa8c32bcfbc437330c..3cb9f47e889827d3b9d2c8490418176eda2d7590 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,76 @@ 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) => {
+ // `archive.header$` in @polkadot-api/observable-client maps the RPC result
+ // straight through this decoder with no null check, and `archive_v1_header`
+ // answers `null` for any hash the node does not know. scale-ts then reaches
+ // `new DataView(null)`, which V8 reports as "First argument to DataView
+ // constructor must be an ArrayBuffer" and Firefox as "e is not an object" —
+ // neither of which mentions blocks, headers, or the node.
+ //
+ // It happens in bursts on a chain switch: the block list still holds the
+ // previous chain's hashes, and every one of them is unknown to the new node.
+ // Harmless, but it needs to say so.
+ if (input == null)
+ throw new Error("no block header: the node does not know this block");
+
+ 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