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