The Quantus runtime accepts only ML-DSA signatures. qapi offered every signer
polkahub knows, and none of those could sign here:
- Polkadot Vault, Ledger and WalletConnect produce sr25519, ed25519 or ecdsa
signatures (and WalletConnect was configured only for Polkadot, Kusama and
Paseo);
- the external multisig signer hands signing to multisig.usepapi.app;
- the browser-extension list offered polkadot{.js}, Nova, Talisman,
SubWallet, PolkaGate and Fearless.
These providers, their management buttons, and the Vault and multisig modals
are removed. What remains: browser extensions, read-only (watch) addresses,
and proxies, which sign through whichever signer they wrap.
@polkahub/pjs-wallet is patched (pinned to 0.9.1, like the substrate-bindings
patches) so the extension list is blackbeard alone, with its sigil as the
logo. Its provider only ever sees, offers or connects blackbeard: anything
else injected into window.injectedWeb3 is ignored, not merely unlabelled.
Verified against a local build in Firefox with blackbeard installed. The
Account Providers panel shows blackbeard (with the sigil), Address and Proxy,
and nothing else, including after fake polkadot-js and talisman entries were
injected into the page and the provider's 2 s re-scan had run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
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
## The check that fired before code that would have worked
`getSignerType` pulls the extrinsic's `Address` and `Signature` types **out of
the metadata** — the runtime-as-oracle approach, and better than polkadot-js,
which hardcodes `ExtrinsicSignature: 'MultiSignature'` in a type definition file.
Then it threw the answer away:
if (signature.type !== "enum" || ["Ecdsa", "Ed25519", "Sr25519"].some(...))
throw unkownSignerType();
Quantus's `DilithiumSignatureScheme` carries `Dilithium87` and `Dilithium65`, so
this raised `Unkown signer` and nothing downstream ever ran. Asking the runtime
and then not believing it is the trap this whole port exists to avoid.
Everything past that point was already correct: `createV4Tx` concatenates and
compact-prefixes and never asserts a signature size, and the address prefix is
the `MultiAddress::Id` variant index read from metadata. The name whitelist only
separated the Substrate shape from the Ethereum one, and the Ethereum arm one
line above already decides that structurally. So the patch drops the names and
keeps `signature.type !== "enum"`.
pnpm `patchedDependencies`, keyed on the package name rather than a version, so
it reaches all fifteen resolved copies in the tree — the nested ones under
pjs-signer, ledger-signer, raw-tx-creator and polkadot-api itself are what
actually run.
## Evidence
Byte-for-byte against the quantus/extension#7 tier-1 harness, same call, same
nonce, same signature:
papi assembled 7300 bytes
IDENTICAL to the tier-1 harness
That is the corroboration this repo exists for. polkadot-api shares no code with
the five polkadot-js forks, so until now our stack had only agreed with itself.
Also verified the patch is load-bearing and narrow:
- the unpatched copy still in the store raises `Unkown signer` on the same
inputs, so the check really was the blocker
- Polkadot metadata still takes the Substrate path, and still gets its `01`
variant byte prepended when a caller names a signing type
## Networks
Quantus, Heisenberg and Planck, with the endpoints verified reachable on
2026-09-15, and Quantus as the default. The Substrate networks stay: a signature
that works on both is the only evidence that the patch widened a check rather
than broke one.
Refs #1, #3. Closes quantus/extension#7's cross-tier comparison.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f