Audit the rest of the console for hardcoded signature and address assumptions #2

Open
opened 2026-09-10 10:38:10 +00:00 by grenade · 0 comments
Owner

Follow-on from #1. That issue fixes the signing path; this one checks whether the same assumption is hiding anywhere else.

The doctrine

Decode the chain's data using the chain's own description of itself. Metadata v14+ carries a complete scale-info type registry describing the structure of every call, event and storage item, and state_getMetadata at a block hash makes the node run Metadata_metadata against the runtime wasm in that block's state — so the runtime is the oracle and the node executes it for us.

blackbeard.observer arrived at this the hard way, and its blackbeard-core/src/runtime.rs header is worth reading before touching anything here:

An earlier attempt at this problem (qsafe.af) generated pallet and event index tables from the runtime and then hand-wrote a SCALE reader for every event's fields. It worked and it was miserable: every new event was a code change, and Quantus had not settled its encodings, so the hand-written half kept rotting underneath. On the chain we can watch that happen — transactionVersion went 2 → 3 → 6 across four runtime upgrades, and every one of those is an extrinsic-format change that silently breaks a decoder written against the previous one.

Quantus is a moving target. Anything here that knows a type by name rather than by metadata will break at the next upgrade, and will break quietly.

What to look for

papi is metadata-driven by construction, so this is expected to be a short list — but #1 proves the assumption can appear as a guard bolted on top of correct code, which is the pattern to hunt.

  • Block and extrinsic views. Decoding a signed extrinsic from a block means reading past the signature field, which needs its width from metadata rather than a constant. This is where qsafe.af went wrong in the other direction — its hand-rolled parser reads the signature as SCALE bytes with a compact length prefix, where the runtime encodes a fixed array with none. Whichever of those two is used, a wrong choice re-frames every byte after it and yields a plausible, wrong decode.
  • Anywhere AccountId32 is assumed to be a public key. On Quantus it is Poseidon2(publicKey). Displaying it is fine; treating it as recoverable key material is not.
  • SS58 handling at prefix 189.
  • Fee estimation / dry-run, which builds a mock extrinsic. papi mocks a 64-byte signature (SR_MOCK) when no signer is present — a fee estimate against a 64-byte placeholder will understate a 5261-byte Quantus extrinsic, possibly enough to matter.

Cache metadata by spec_version

If the console fetches metadata per block, cache by spec_version rather than by block hash: one fetch per runtime version, ever, and decoding keeps working after the node prunes the state that would have answered for an old block. This is what blackbeard-data does and the reasoning is in the same file.

Outcome

If the audit finds nothing beyond #1, close this saying so. A recorded "we looked, it is clean" is worth having — the next person to hit a decode oddity should not have to redo the search.

Follow-on from #1. That issue fixes the *signing* path; this one checks whether the same assumption is hiding anywhere else. ## The doctrine Decode the chain's data using the chain's own description of itself. Metadata v14+ carries a complete `scale-info` type registry describing the *structure* of every call, event and storage item, and `state_getMetadata` at a block hash makes the node run `Metadata_metadata` against the runtime wasm in that block's state — so the runtime is the oracle and the node executes it for us. blackbeard.observer arrived at this the hard way, and its `blackbeard-core/src/runtime.rs` header is worth reading before touching anything here: > An earlier attempt at this problem (`qsafe.af`) generated pallet and event *index* tables from the runtime and then hand-wrote a SCALE reader for every event's fields. It worked and it was miserable: every new event was a code change, and Quantus had not settled its encodings, so the hand-written half kept rotting underneath. On the chain we can watch that happen — `transactionVersion` went 2 → 3 → 6 across four runtime upgrades, and every one of those is an extrinsic-format change that silently breaks a decoder written against the previous one. Quantus is a moving target. Anything here that knows a type by name rather than by metadata will break at the next upgrade, and will break *quietly*. ## What to look for papi is metadata-driven by construction, so this is expected to be a short list — but #1 proves the assumption can appear as a guard bolted on top of correct code, which is the pattern to hunt. - **Block and extrinsic views.** Decoding a signed extrinsic from a block means reading past the signature field, which needs its width from metadata rather than a constant. This is where `qsafe.af` went wrong in the other direction — its hand-rolled parser reads the signature as SCALE bytes *with* a compact length prefix, where the runtime encodes a fixed array with none. Whichever of those two is used, a wrong choice re-frames every byte after it and yields a plausible, wrong decode. - **Anywhere `AccountId32` is assumed to be a public key.** On Quantus it is `Poseidon2(publicKey)`. Displaying it is fine; treating it as recoverable key material is not. - **SS58 handling** at prefix 189. - **Fee estimation / dry-run**, which builds a mock extrinsic. papi mocks a 64-byte signature (`SR_MOCK`) when no signer is present — a fee estimate against a 64-byte placeholder will understate a 5261-byte Quantus extrinsic, possibly enough to matter. ## Cache metadata by spec_version If the console fetches metadata per block, cache by `spec_version` rather than by block hash: one fetch per runtime version, ever, and decoding keeps working after the node prunes the state that would have answered for an old block. This is what `blackbeard-data` does and the reasoning is in the same file. ## Outcome If the audit finds nothing beyond #1, close this saying so. A recorded "we looked, it is clean" is worth having — the next person to hit a decode oddity should not have to redo the search.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: blackbeard/qapi#2