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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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-infotype registry describing the structure of every call, event and storage item, andstate_getMetadataat a block hash makes the node runMetadata_metadataagainst 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.rsheader is worth reading before touching anything here: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.
qsafe.afwent 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.AccountId32is assumed to be a public key. On Quantus it isPoseidon2(publicKey). Displaying it is fine; treating it as recoverable key material is not.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_versionrather 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 whatblackbeard-datadoes 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.