Teach papi to read a Quantus block header, and to hash one #4

Open
opened 2026-09-16 06:15:24 +00:00 by grenade · 0 comments
Owner

Blocks everything else here. Until papi can read a header, runtime$ never emits, Subscribe never renders, and the console shows a splash that eventually gives up. Evidence and the full diagnosis are on #1; this issue is the decision and the work.

The two problems

1. The header layout. qp_header::Header is not sp_runtime::generic::Header:

parent_hash      H256    32
number           u32      4   ← plain u32; Substrate has #[codec(compact)]
state_root       H256    32
extrinsics_root  H256    32
zk_tree_root     H256    32   ← Quantus only
digest           Vec<DigestItem>

Confirmed byte-exact against mainnet block 53845. papi's blockHeader in @polkadot-api/substrate-bindings reads number as a Compact, which consumes 2 bytes where 4 were written; everything after shifts and the digest vector dies on an unknown variant index.

2. The block hash. getHasherFromHeader finds the chain's hasher by searching for one where h(rawHeader) === blockHash. On Quantus none exists — the block hash is Poseidon over a felt-aligned preimage the header constructs itself, not a byte-hash of the SCALE encoding. qp-poseidon-core has the implementation; @quantus/crypto already ships it compiled to WASM.

The design question

blockHeader is a module-level codec in substrate-bindings, shared by every chain in the console, and the console is deliberately multi-chain — the Substrate networks are kept precisely so a signature working on both proves the signer patch widened a check rather than broke one. So neither problem can be fixed by redefining a global.

Three options, roughly in increasing order of cost and honesty:

  1. Round-trip disambiguation in a patched blockHeader. Decode with the Substrate layout, re-encode, and accept only if the bytes come back identical; otherwise try the Quantus layout under the same rule. This is the rule @quantus/codec already applies to caller-supplied extension bytes, and a full-consumption check is a strong discriminator rather than a guess. Cheapest. Does nothing for the hasher.
  2. Chain-aware injection. Thread the selected chain down to the codec and the hasher list. Correct, and a much larger change to code we do not own.
  3. A Quantus-aware fork of papi's chainHead bootstrap. Most control, most maintenance, and the thing patches/ exists to avoid.

Option 1 plus a Poseidon entry in the hasher list may be enough: the hasher list is already a search, so adding a Quantus hasher that the search can find is in the grain of the existing design rather than against it. Worth trying first, precisely because it does not require the console to know which chain it is on.

Worth checking before starting

  • Whether the felt-aligned preimage can be reproduced from the SCALE header alone. If it needs anything the header does not carry, getHasherFromHeader's signature (header, blockHash) is sufficient, but a hasher that must be told the layout is not a drop-in for that search.
  • Whether @quantus/crypto's Poseidon surface covers this. It exposes ext_poseidon_hash for account derivation; the header preimage is a different construction over the same primitive, and the crate may need a second export.
  • What else in papi assumes a Substrate header. archive.js decodes headers too (map(blockHeader[1])).

Acceptance

The console connects to Quantus mainnet, lists blocks in the explorer, and the block hashes it computes match the ones the node reports. Then quantus/extension#7 tier 3's two remaining boxes — listing extension accounts, and signing a transfer that lands in a block — become reachable.

Blocks everything else here. Until papi can read a header, `runtime$` never emits, `Subscribe` never renders, and the console shows a splash that eventually gives up. Evidence and the full diagnosis are on #1; this issue is the decision and the work. ## The two problems **1. The header layout.** `qp_header::Header` is not `sp_runtime::generic::Header`: ``` parent_hash H256 32 number u32 4 ← plain u32; Substrate has #[codec(compact)] state_root H256 32 extrinsics_root H256 32 zk_tree_root H256 32 ← Quantus only digest Vec<DigestItem> ``` Confirmed byte-exact against mainnet block 53845. papi's `blockHeader` in `@polkadot-api/substrate-bindings` reads `number` as a Compact, which consumes 2 bytes where 4 were written; everything after shifts and the digest vector dies on an unknown variant index. **2. The block hash.** `getHasherFromHeader` finds the chain's hasher by searching for one where `h(rawHeader) === blockHash`. On Quantus none exists — the block hash is **Poseidon over a felt-aligned preimage** the header constructs itself, not a byte-hash of the SCALE encoding. `qp-poseidon-core` has the implementation; `@quantus/crypto` already ships it compiled to WASM. ## The design question `blockHeader` is a module-level codec in `substrate-bindings`, shared by every chain in the console, and the console is deliberately multi-chain — the Substrate networks are kept precisely so a signature working on both proves the signer patch widened a check rather than broke one. So neither problem can be fixed by redefining a global. Three options, roughly in increasing order of cost and honesty: 1. **Round-trip disambiguation in a patched `blockHeader`.** Decode with the Substrate layout, re-encode, and accept only if the bytes come back identical; otherwise try the Quantus layout under the same rule. This is the rule `@quantus/codec` already applies to caller-supplied extension bytes, and a full-consumption check is a strong discriminator rather than a guess. Cheapest. Does nothing for the hasher. 2. **Chain-aware injection.** Thread the selected chain down to the codec and the hasher list. Correct, and a much larger change to code we do not own. 3. **A Quantus-aware fork of papi's chainHead bootstrap.** Most control, most maintenance, and the thing `patches/` exists to avoid. Option 1 plus a Poseidon entry in the hasher list may be enough: the hasher list is already a *search*, so adding a Quantus hasher that the search can find is in the grain of the existing design rather than against it. Worth trying first, precisely because it does not require the console to know which chain it is on. ## Worth checking before starting - Whether the felt-aligned preimage can be reproduced from the SCALE header alone. If it needs anything the header does not carry, `getHasherFromHeader`'s signature (`header, blockHash`) is sufficient, but a hasher that must be *told* the layout is not a drop-in for that search. - Whether `@quantus/crypto`'s Poseidon surface covers this. It exposes `ext_poseidon_hash` for account derivation; the header preimage is a different construction over the same primitive, and the crate may need a second export. - What else in papi assumes a Substrate header. `archive.js` decodes headers too (`map(blockHeader[1])`). ## Acceptance The console connects to Quantus mainnet, lists blocks in the explorer, and the block hashes it computes match the ones the node reports. Then quantus/extension#7 tier 3's two remaining boxes — listing extension accounts, and signing a transfer that lands in a block — become reachable.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: blackbeard/qapi#4