Indexer architecture
The Quantum explorer indexer is a search-first ingestion pipeline for Quantum chain data. It consumes Quantum-native RPC payloads and exposes a small health and metrics surface for operators.
Runtime shape
The indexer is intentionally single-process and sequential:
- Subscribe to
eth_subscribe("newHeads")over WebSocket for low-latency head notifications. - Fetch canonical blocks and receipts over HTTP JSON-RPC.
- Decode blocks as Quantum-native payloads, not generic Ethereum envelopes.
- Persist immutable chain facts.
- Advance the contiguous committed frontier only after the fact-store write has completed.
The WebSocket path is advisory. HTTP is the deterministic fetch path for initial catch-up, gap fill, and receipt hydration.
Data model
The indexer separates mutable runtime state from immutable chain facts.
Control plane — tracks the committed, finalized, and last-seen-head watermarks; per-block retry and cleanup state; metadata such as ABI records, verified contracts, and address labels. The committed-height value owns the visibility boundary for all explorer-facing reads.
Fact store — holds normalized, immutable chain facts:
blockstxstx_callstx_signaturesreceiptslogstoken_transfers
The explorer's hot reads (block lookup, address activity, log filters) hit the fact store; the committed-height clamp comes from the control plane.
Critical deviations from standard Ethereum
These are the things that will break a generic EVM indexer if you don't handle them.
1. Unknown transaction type 0x7A
Quantum uses a single custom EIP-2718 type 0x7A (QuantumTxEnvelope). There are no Legacy (0x00), EIP-2930 (0x01), EIP-1559 (0x02), EIP-4844 (0x03), or EIP-7702 (0x04) transactions.
Any indexer that whitelists known type IDs will reject every transaction.
2. The tx body is native AA — not a 1559 clone
Fields:
| Field | Type | Notes |
|---|---|---|
chain_id | u64 | |
sender | Address | Explicit — ML-DSA is non-recoverable (no ecrecover) |
nonce_key | U256 | 2D nonce queue ID (0 = legacy sequential) |
nonce | u64 | Sequence within the queue |
key_id | u32 | References a KeyEntry in the on-chain KeyVault |
max_priority_fee_per_gas | u128 | |
max_fee_per_gas | u128 | |
gas_limit | u64 | |
calls | Vec<Call> | Batched calls, atomically executed |
access_list | AccessList | EIP-2930 access list |
fee_payer | Option<Address> | Gas sponsor (None = sender pays) |
fee_payer_key_id | Option<u32> | |
init_primary_pubkey | Option<Bytes> | Bootstrap-only (first-use bootstrapKey()) |
init_cosigner_pubkey | Option<Bytes> | Reserved; must be absent during bootstrap |
Headline differences from EIP-1559:
- Explicit
sender— account identity is decoupled from signer-key derivation. - Batched
callsinstead of singleto/value/input. - 2D nonces for parallel queues.
- Gas sponsorship via optional
fee_payer. - Explicit account bootstrap — first-use registration requires a dedicated
bootstrapKey()call to KeyVault;init_primary_pubkeyonly appears on bootstrap txs. - Key-management lifecycle —
addKey/removeKey/updateKeyAuthare explicit top-level lifecycle operations enforced by transient intent context; nested contract calls cannot trigger them.
3. Multi-scheme signatures, not ECDSA v / r / s
Transactions carry composite signatures — a mandatory primary plus an optional cosigner — instead of v / r / s. Four schemes:
| Scheme | Tag | Pubkey | Signature | Wire total |
|---|---|---|---|---|
| ML-DSA-44 (FIPS 204) | 0x01 | 1,312 B | 2,420 B | 3,733 B |
| P256 (secp256r1) | 0x02 | 64 B | 64 B | 129 B |
| ECDSA (secp256k1) | 0x03 | 33 B | 64 B | 98 B |
| SLH-DSA 128s | 0x04 | 32 B | 7,856 B | 7,889 B |
Implications for an indexer:
- No
v/r/sfields exist. Each signature is ascheme_byte || pubkey || signatureblob. senderis an explicit field — it is the account lane, not derived from the signer's public key.- The fee payer signs over a different domain (
0x78prefix vs0x7A) to prevent signature reuse. - Any code path that calls
ecrecoveror parsesv/r/swill crash or return wrong addresses.
4. Block header has an extra field
The header wraps a standard Ethereum header and prepends one field:
RLP(QuantumHeader) = RLP([timestampMillisPart, ...standard 21 header fields...])
timestamp(seconds) — standard, EVM-compatible.timestampMillisPart(u64, 0–999) — sub-second component for consensus ordering.- RPC responses include
timestampMillis = timestamp * 1000 + timestampMillisPart.
Block hash = keccak256(rlp(QuantumHeader)). Any indexer that recomputes block hashes from the standard header fields alone will get mismatches.
5. No blob / EIP-7702 / beacon chain
| Feature | Status |
|---|---|
Blob transactions (0x03) | Not supported |
| EIP-7702 authorization lists | Not supported |
| Beacon chain / slots / epochs | None — uses Simplex consensus |
parent_beacon_block_root | None or dummy |
| Uncles / ommers | None |
Committed visibility boundary
The indexer never serves data that has not been fully committed. Concretely:
- A new head from
eth_subscribe("newHeads")triggers a fetch of the block and its receipts. - The block is decoded as a Quantum-native payload.
- Fact rows are written to the fact store.
- Only after the fact-store write completes does
committed_heightadvance. - Read paths on the explorer clamp to
committed_height.
This avoids race conditions where the explorer would show partial or torn data after a crash.
Related
- Native account abstraction — the transaction envelope from the protocol side
- Transaction and block lifecycle — where blocks get their shape
- JSON-RPC and dev mode — the RPC surface the indexer consumes