Files
extension/packages
rob thijssen bfd3aa80ba feat: fetch a chain's metadata from a node, not from the dapp asking for a signature
Substrate chains describe themselves. Almost nothing that talks to them takes
advantage of it: polkadot-js hardcodes `ExtrinsicSignature: 'MultiSignature'` in
a definitions file and caps fixed arrays at 2048 bytes, polkadot-api reads the
signature type out of metadata and then discards the answer for a three-name
whitelist, and its block header is a static struct. Each is a client deciding in
advance what a chain is allowed to be. This extension already reads calls and
signed extensions from the runtime; this is the last piece that did not — where
the runtime's description came from.

It came from the dapp. Two problems, and the second is the serious one.

**It does not always happen.** Not every dapp calls `metadata.provide` — the qapi
console does not, because `@polkadot-api/pjs-signer` narrows the injected
interface to `{ name, getAccounts, subscribe, disconnect }` and never surfaces
that half. Since this extension refuses to sign for a chain it cannot describe,
such a dapp could not get a signature at all.

**And the dapp is not a trustworthy source.** Metadata decides what the approval
screen *says* a call does. A dapp supplying its own controls both the
transaction and its description, so it can render something harmless and have
the user sign a transfer. Asking the chain means the description and the thing
described come from the same place, and it is not the page asking for the
signature.

So `ChainMetadata` fetches it, and `pub(extrinsic.sign)` awaits that before
queuing — before the popup, not after approval, because a user asked to approve
a transaction rendered as raw hex has been given nothing to approve.

Three things that make this safe rather than merely convenient:

  - **Endpoints are believed only if they prove their identity.** A sign request
    names a genesisHash; the endpoint's `chain_getBlockHash(0)` must equal it.
    Without that the table of hosts would itself be the thing taken on trust,
    which is not an improvement over trusting the dapp. Verified by deliberately
    pointing Planck's genesis at Heisenberg's nodes: both were rejected —
    "serves 0xa5aa9e5c…, not 0x4901bf5c…" — and the fetch returned null.
  - **The payload's spec version is the cache key, not mere presence.** A runtime
    upgrade changes which calls and signed extensions exist, and metadata from
    before one decodes this chain's calls into something plausible and wrong.
    It also keeps the common case off the network entirely. Mainnet is on spec
    152 today where the testnets are on 148, so this is not hypothetical.
  - **A chain with no endpoint returns null instantly**, with no connection
    attempted, and the signer's existing refusal explains it.

Verified against the live chains rather than by unit test: Quantus spec 152 QTC,
Heisenberg 148 HEI, Planck 148 PLK, each 101 KB of metadata with ss58 189;
unknown chain null in 0 ms; second call served from cache in 0 ms.

`extrinsicSign` is now async before it queues, so the specs poll for the request
instead of assuming it is already there — it was a race they had been winning by
accident.

92 tests pass, lint clean, builds.

Refs #1, quantus/papi-console#3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 11:05:24 +03:00
..
2026-07-31 12:43:58 -03:00