igneum/docs/plans/explorer.md
2026-10-06 16:16:11 +00:00

26 KiB

Explorer: plan and recommendation

5 October 2026, after the project lead read WhatToMine's listing requirements ("build it, and do we build our own explorer? who built etherscan?"). Branch explorer. What exists tonight: the public stats API (docs/api/public-stats.md) and the first DAG explorer pages on the site, fed by the observer. What is recommended: Blockscout for the EVM side, our own DAG and mining pages, one shared search box.

1. Who built Etherscan, and the open-source routes

Explorer What it is Licence and cost Fit for Igneum
Etherscan Built and launched in 2015 by Matthew Tan (CEO and founder); the office since January 2017 (etherscan.io/aboutus, read 5 October 2026; the page does not name the city, the project lead's brief says Kuala Lumpur). Since 2020 it sells "Explorer as a Service", a white-label instance for other chains, 40 clients by 2025 (same page) Closed source, a private company; a chain pays for an instance. Price not published; not asked Not for us: closed, paid, and it would show nothing of the DAG, the finality or the proving layer
Blockscout Open-source EVM explorer: blocks, transactions, accounts, verified contracts, token pages, an API in Etherscan's shape. Elixir (Phoenix) backend, PostgreSQL, a separate frontend; "several hundred chains and rollups" use it (README, read 5 October 2026) "Blockscout Software Licence" (the README badge; the licence text was not read line by line, so what it permits for a hosted instance is unverified) The EVM side for free: contracts, transactions, logs, tokens, an API developers already know
Otterscan "open-source, fast, local, laptop-friendly Ethereum block explorer": a React app over an Erigon archive node, using Erigon's custom ots_ JSON-RPC methods (github.com/otterscan/otterscan, read 5 October 2026) MIT (the app); the ots_ API lives inside Erigon under its licence Not for us: it needs Erigon's RPC extensions, which the Igneum node does not have, and it has no contract verification

2. What Blockscout gives us for free, and what it costs to run

Blockscout indexes through standard JSON-RPC. Its documented requirements (docs.blockscout.com, read 5 October 2026):

Item Blockscout's figure Igneum node today (vendor/igneum-node 0.3.6 fork, igneum/exec/src/rpc.rs, 938 lines)
Software Erlang/OTP 26, Elixir 1.15.x, Postgres 14+, Node.js 18.x.x (docs: setup/requirements/requirements) n/a
Hardware, the docs' base line "16 core, 32 thread", "128GB" RAM; the AWS example is one m5a.xlarge (4 vCPU, 16 GB) application server with 8 GB EBS and one db.t3.large RDS Postgres 14+ with 500 GB "depending on chain size" (docs: setup/requirements/resource-requirements) A devnet at 0 EVM transactions per block needs nothing like the base line; the AWS example is the honest size for a small chain
Database Ethereum mainnet 21,000 GiB, Sepolia 5,200 GiB, Ethereum Classic 555 GiB, Gnosis Chiado 470 GiB (docs: setup/requirements/database-storage-requirements, figures dated 23 December 2024) Unmeasured for Igneum. At one chain block per second with empty blocks the row count is 86,400 blocks a day; the byte size per block is the thing to measure in the first week
RPC it needs from every client eth_blockNumber, eth_call, eth_getBalance, eth_getCode, eth_getBlockByHash, eth_getBlockByNumber, eth_getTransactionByHash, eth_getTransactionByBlockHashAndIndex, eth_getTransactionByBlockNumberAndIndex, eth_getTransactionReceipt, eth_getUncleByBlockHashAndIndex, eth_getLogs (docs: setup/requirements/node-tracing-json-rpc-requirements) The fork answers eth_blockNumber, eth_call, eth_getBalance, eth_getCode, eth_getBlockByHash, eth_getBlockByNumber, eth_getTransactionByHash, eth_getTransactionByBlockNumberAndIndex, eth_getTransactionReceipt, eth_getLogs (the match arms of rpc.rs). MISSING: eth_getTransactionByBlockHashAndIndex, eth_getUncleByBlockHashAndIndex (design 8.2 says uncles are "always empty": the method still has to exist). Also absent from the fork but in the design table: eth_getStorageAt is present; eth_getProof, eth_feeHistory present; web3_clientVersion, net_version, net_peerCount, net_listening, eth_syncing, eth_mining, eth_protocolVersion, eth_accounts, eth_getBlockReceipts, eth_getBlockTransactionCountByNumber, eth_maxPriorityFeePerGas, eth_gasPrice, eth_estimateGas, eth_sendRawTransaction present
Pending transactions txpool_content (geth, erigon) or parity_pendingTransactions Neither exists in the fork. Blockscout runs without it (the pending view stays empty)
Internal transactions and block rewards debug_traceBlockByNumber and debug_traceTransaction with callTracer (geth variant), or trace_replayBlockTransactions and trace_block (erigon, nethermind) None of the four exist in the fork. Design 8.2 lists them as "Supported, revm inspectors"; rpc.rs has no debug_ or trace_ method today. Without them Blockscout shows no internal transactions and no block-reward rows, and the indexer's trace fetcher must be switched off (INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER, Blockscout's env; unverified against the current version)

Cost of one instance on Hetzner (the price list the seeds are on, docs/plans/seed-nodes.md: cx23 2 vCPU 4 GB at USD 6.49 net a month; larger types not priced here): Blockscout's own AWS example is 4 vCPU 16 GB plus a 2 vCPU 8 GB database. The matching Hetzner shape is one box in the 8 GB to 16 GB class plus Postgres on the same box for a devnet, a second box for the database when the chain carries real traffic. Price it from the Hetzner API when the box is ordered; the figure here is approximate: USD 15 to 40 a month for the single box, under USD 80 for two. Plus an Igneum node on the same box or next to it (Blockscout wants a local, unlimited RPC; the public rpc.testnet.igneum.network is rate limited to 20 req/s, docs/plans/testnet-go.md).

What it costs in work, in hours not weeks: the two missing eth_ methods (small, same shape as their by-number siblings); a decision on tracing (the debug_ namespace with revm inspectors, design 8.2, is the larger piece and is not needed to run Blockscout without internal transactions); Blockscout's env file and a Docker compose on the box; the chain's entry in its config (chain id 4463 devnet, 4462 testnet, 4461 mainnet, design 8.1); contract verification through Sourcify or Blockscout's own verifier microservice.

3. What Igneum needs that must be ours

Blockscout shows a chain: numbered blocks, one parent, transactions, accounts. Igneum's execution layer is such a chain (RPC "blocks" are chain blocks, design 8.2), so Blockscout is right for it. Everything the consensus layer adds is invisible to it:

Need Where it comes from State tonight
The DAG: every block, its parents, blue or red, pending, the selected chain, the mergeset of each chain block the observer's blockAdded feed (live_blocks, detail.mergeset) /explorer table and /block/<hash> built; the live DAG picture stays on /live
Blue score and DAA score per block, the miner's vote key, the engine tag the block header and coinbase built
Miners: payout address (the coinbase's IGNA tag) and the coinbase address, blocks mined, what they earned, balance observer columns evm_miner, miner_address; eth_getBalance through EXPLORER_EVM_RPC /address/<addr> built; balance shows when the deployment has an EVM RPC (none public for the devnet; rpc.testnet.igneum.network for the testnet)
The lottery program per epoch (which generated program is live, the epoch seed, the VDF) the node reports epochSeed per shard plan (igneum_getShardPlan); the program id and the epoch boundary are not in any RPC the observer reads not built; needs an RPC for the current program id and epoch (open)
Finality: checkpoints every 30 blue score, locks at two thirds of all 30-day weight, certificates, voter tables, the in-browser verifier live_checkpoints, live_certificates, /api/checkpoint, site/verify the block page shows a block's checkpoint state and certificate; a checkpoints list page is not built
Proof records and shards: the plan per chain block, who proved what, lag, payout live_proofs, igneum_getProofRecords the block page shows the shards and the records a block carries; a provers page (per prover: shards, lag, income) is not built
Pool payouts a public pool does not exist (section 5) not built
The switches: difficulty_v2_activation_daa, proving_v0_activation_daa, fees_v1_activation_daa, finality_v3_activation_daa (/tmp/igneum-devnet/override-v3.json on the devnet: 33,000, 84,100, 210,000, 135,200) the params file; igneum_getProvingStatus.activationDaa; no RPC lists them all not built; a "network parameters" card on /explorer reading a params RPC is the clean way

4. Recommendation

Confirmed from the code and the RPC surface: Blockscout for contracts, transactions and accounts; our own DAG, mining, finality and proving pages on the site, fed by the observer; one search box that routes a transaction or contract to Blockscout and a block hash, a chain block number or a miner to our pages. Two things qualify it:

  1. Blockscout cannot run against the fork as it is: eth_getTransactionByBlockHashAndIndex and eth_getUncleByBlockHashAndIndex are missing (an hour), and there is no tracing (debug_traceTransaction), so internal transactions stay off until the revm inspectors of design 8.2 exist. The execution engineer owns both.
  2. One box of our own and Postgres on it, next to a node with an unlimited local RPC. Not before the public testnet has transactions worth looking at; the devnet's blocks are empty and our pages already show them.

The search box: site/lib/explorer.mjs classify() already routes 64-hex to a block (the API falls through to a transaction hash in the last 24 hours), 0x40 and bech32 to an address, digits to a chain block number. When Blockscout is up, a 64-hex that is not a DAG block and a 0x40 that is a contract go to it instead of a 404.

5. What was built tonight

Item Where
/api/stats, /api/supply site/api/stats.mjs, site/api/supply.mjs, site/api/_neon.mjs; the emission rule in site/lib/emission.mjs; docs/api/public-stats.md with example responses
/api/explorer site/api/explorer.mjs: latest blocks, one block, height, address, search
Pages site/explorer.html, site/block.html, site/address.html; site/vercel.json rewrites /block/:id and /address/:addr; the footer carries an Explorer link (the nav is unchanged: its eight items are measured to fit at 941 px, a ninth is a layout decision for the site owner)
Observer tools/observer/observer.mjs: per-block explorer columns and detail, number from the shard plan, rpc_load, hourly supply_check
Preview node tools/site-serve.mjs (clean URLs, the rewrites, the API functions in-process; LIVE_TABLE_PREFIX, EXPLORER_EVM_RPC, PORT)
Tests site/lib/explorer.test.mjs (router, formatters), site/lib/emission.test.mjs (the node's own test values from igneum.rs, a devnet coinbase, the floor sum against a loop), site/api/public-stats.test.mjs (every documented field from a fixture); CI runs them, and on master tools/ci/public-api-check.mjs https://igneum.network
Screenshots docs/plans/explorer/explorer.png, block.png, address.png (local preview against a test observer, 5 October 2026)

Observer load, measured

Two copies of the observer ran side by side against the observer node (ws://127.0.0.1:28640, EVM RPC http://127.0.0.1:26790) with test table prefixes, 19:21 to 19:28 UTC on 5 October 2026: master's code with only a call counter added, and this branch. Calls per wall-clock minute, as each process counted them:

Minute (UTC) Before, wRPC After, wRPC Before, EVM After, EVM
19:21 to 19:22 (partial, 50 s) 283 282 776 785
19:22 211 211 2,119 2,145
19:23 252 253 2,487 2,493
19:24 232 232 2,487 2,492
19:25 266 266 2,491 2,497
19:26 210 210 2,485 2,481

Ratio after to before: 1.00 on both. The explorer reads everything from the notification the observer already receives; the only new work is one SQL update per shard-plan batch. The EVM figure is the proving feed's record polling (80 blocks per 2 s tick while proving is active, master behaviour); the observer that is live tonight points at http://127.0.0.1:26800 (the app's node, down), so its EVM load is zero and its proving feed reads "unreachable". The observer node answers the proving RPCs on 26790 (eth_chainId 0x116f, igneum_getProvingStatus active); pointing IGNEUM_EVM_RPC there when the observer is next restarted is a one-line change to the launch environment, not to the code.

Supply check, measured

The first sample after start: "21 blocks, payload subsidy = rule 21/21, outputs = merged subsidies 20/20 (1 without a verdict)". The rule in site/lib/emission.mjs reproduces the node's own test values (igneum.rs: 3,168,808,781 at DAA 2,592,000; 1,584,404,390 at the first halving; 0 at the 32nd) and the devnet coinbase of block 2622db76 (payload subsidy 454,486,399 at DAA 125,064; outputs 363,588,240 + 90,897,059 = 454,485,299 = E(125,063), what its merged parent declared: utxo_validation.rs:176 pays a merged block the subsidy its own payload carries).

6. A public pool with a stats API (separate item, not built)

The app carries a pool mode in the Hive package: the Flight Sheet's Pool URL is grpc://<your node>:26610 for solo mining or local to run the bundled node on the rig (packaging/hive/README.md); "there is no pool". A public pool needs, in order: the pool protocol of docs/spec/09-pool-protocol.md implemented (templates, shares, the member's own vote key and votes relayed, vardiff, the stats message: member hashrate, workers, refusals, shares, pool members, hashrate, blocks_24h, declared_share); a pool server on its own box with a node; a payout scheme (the spec leaves PPLNS windows and PPS fees to the pool); a stats API in the shape WhatToMine and MiningPoolStats read (pool hashrate, miners, workers, blocks found with heights and times, fee, minimum payout, luck); and a page on the site. None of this is in the repo; the stats API of this branch is the network side of what those sites ask for.

7. Unverified

  • Vercel's cleanUrls with a rewrite destination of /block and /address (the clean names of block.html and address.html): checked locally through tools/site-serve.mjs, not on a Vercel preview, because nothing was pushed.
  • /api/stats and /api/supply on the live tables: the live observer has not been restarted on this code, so the live live_blocks has no number, tx_count or detail columns yet; the handlers answer with nulls there until the restart (the schema adds itself on start, ALTER TABLE ... ADD COLUMN IF NOT EXISTS). The examples in docs/api/public-stats.md come from the test observer's tables.
  • Blockscout's licence terms for a hosted instance, and the exact env flag that disables its trace fetcher.
  • The database size per Igneum block in Blockscout, and the Hetzner price of the box: approximate above, measure and price when ordered.
  • EXPLORER_EVM_RPC on Vercel: no public devnet EVM RPC exists, so balances show "no EVM RPC configured" on the devnet deployment; the testnet's https://rpc.testnet.igneum.network is the value for the testnet.

8. Verify a proof (6 October 2026, branch explorer-verify)

Goal: a stranger checks a block's proof without our software. What shipped, what is verified where, and the number that decides the in-browser verifier.

What a stranger sees

/proof/<chain block hash or number> (linked from every proven or finalised block on /explorer, from the shard table on /block/<hash>, and from the "verify this block's proof" line under it). Paste a hash: the page fetches /api/verify?block=<hash>, which returns the block's one state word, every planned shard with the record the chain carries for it (vote key hash, payout, statement, proof hash, the carrying chain block) and, where the observer captured the shard's proof, the bytes url and the verdicts. The browser then downloads the bytes (/api/verify?...&bytes=1, 1,272,897 bytes for a compressed SP1 shard proof) and runs three checks in the tab (site/lib/proof.mjs, no library, no server trust): SHA-256 of the bytes against the record's proof_hash (igneum/exec/src/proving.rs proof_hash), keccak-256 of the public values parsed from the proof's bincode tail against the record's statement, and the decoded 328-byte statement (ShardOutput::to_bytes) against this block's hash, number, shard and payout. The strip shows the verdict (VERIFIED, NOT VERIFIED, binding only, not captured), the time (checks plus download in this browser; the STARK's time on the node), the block's state word and the pinned program id. Three cards say who verified what: in this browser (the binding), by this site's node (the native SP1 light verifier with the pinned key, igneum-prove-host --mode verify, its verify and setup times), by the network (carried and paid; the observer's node runs with its own verifier off, and says so). The page carries the label "the in-browser verifier is coming" on the STARK line, and never says the STARK was verified in the browser.

The data path

Step Where Rate
The pool entry appears in igneum_getProofRecords (proofBytes > 0) observer pollRecords every 2 s, as before
igneum_getProofBytes [number, shard, keyHash] while the node holds it (600 chain blocks, RECORD_WINDOW_CHAIN_BLOCKS) observer captureTick, tools/observer/proof-capture.mjs one proof every PROOF_CAPTURE_EVERY_MS (5 min)
The three browser checks, then the native verifier the observer, igneum-prove-host --mode verify from the Mac app bundle 0.213 s key setup plus the verify per proof (below)
live_proof_bytes row (bytes kept for the newest PROOF_BYTES_KEEP = 50, about 64 MB; record, checks and verdict kept 24 h) Neon prune each minute
/api/verify and /proof/<hash> Vercel function site/api/verify.mjs, site/proof.html cached 5 s; the bytes immutable

Why a sample: 1,272,897 bytes per shard proof at one chain block per second is 110 GB a day. Every proof's RECORD (statement, proof hash, key hash, payout) is now stored per shard in live_proofs, so a shard without captured bytes still shows what the chain carries; any node hands the bytes out again while it holds them.

The in-browser verifier: the size and time problem, with numbers

Item Number Source
Compressed SP1 shard proof 1,272,897 bytes (1,272,769 to 1,272,909 across runs) docs/bench-log.md 5 October, the saved proof of block 59199
Native verify of it, pinned key 0.029 to 0.038 s; key setup 0.213 s with the light verifier (measured 6 October 15:46 UTC under other load, a functional run, not a benchmark) bench-log; this branch's run of --mode verify on block 59199's proof
Browser checks on the same bytes SHA-256 and keccak of 1.27 MB and 328 bytes: a few ms (measured on the page, shown in the strip) site/proof.html
What verifies a COMPRESSED proof sp1-prover verify_compressed: the recursion STARK verifier of sp1-hypercube 6.8.1 (KoalaBear, jagged PCS) plus the recursion verifying key and the allowed-vk Merkle root; std crates with rayon, tokio and the circuit artifacts proving/igneum-prove/host/src/proof_system.rs, ~/.cargo/registry sp1-* 6.8.1
What sp1-verifier (the no_std, wasm-ready crate) verifies Groth16 and Plonk WRAPPED proofs only the crate's README (vendor registry)
The wrap step NOT RUN here: wrap in proof_system.rs bails ("needs SP1's circuit artifacts", ledger P3); the wrapped proof would be about 260 bytes and the wasm verifier a few hundred KB, verify in tens of ms (approximate, from SP1's published figures, unmeasured on this project) host/src/proof_system.rs line 327; ledger P3

Decision for the first cut: ship the server-side verify (the observer's native verifier, labelled on the page) and the browser binding checks. The in-browser STARK verifier is owed on one of two routes, both unmeasured here: (a) wrap each shard proof to Groth16 (the P3 measurement: the circuit artifacts, the minutes per wrap on a consumer GPU, who pays for it) and compile sp1-verifier to wasm; (b) port the compressed-proof verifier to wasm32 (drop rayon, tokio and the file reads from the sp1-hypercube verify path; the recursion vk and allowed-vk root shipped as static bytes, about 1 MB). Route (a) is the one that gives a phone a millisecond verify; route (b) keeps the 1.27 MB download per proof. Hours of work either way, after P3's numbers exist.

The state word (ledger P17) and the fork

The P17 state and failure fields live on the fork ledger-fixes-0311 (fbb0082a, tx_status_json, igneum_getFinalityView). They are NOT in release-0.3.13-node (bb43e9a8 does not contain b1e98b79; igneum_getTransactionStatus there still answers the three flags). So the explorer cuts the word itself from the observer's tables with the same rule (site/lib/proof.mjs blockState, txState; design 2.4): a block is pending (not merged), included (merged, or a chain block the executor has not run), executed (the node planned it), proven (every shard's record carried and paid), finalised (at or below the latest locked checkpoint, finality active) or "finality not active"; the failure "finality paused" (executed, not locked, finality not active). A block that had a number and left the selected chain is NOT "reorged out": it is still held and merged (the tip flips between parallel blocks several times a minute on the devnet); the note says so, and the word stays included. "Reorged out" and "skipped" are transaction failures, read from the node. Per transaction the block page asks igneum_getTransactionStatus when EXPLORER_EVM_RPC is set and takes the node's state when the fork carries it; otherwise the word is cut from the block's. Owed: the fork merge (P17 into the node release), after which the page reads the node's word and the derived one is the fallback.

Reviewer C46

/api/stats algorithm read "generator v2" as fixed text. The observer now asks getBlockTemplate every 10 s and writes powEpoch (program_class 3, next_program_class 3, program_class_v3_activation_daa 154,800, epoch 55 on the devnet at 15:52 UTC) to live_state.pow_epoch; the line reads "class v3 / generator 3 (epoch 55; ...)" and will read v4 when the node reports 4. A new lottery field carries the epoch line. Until the restarted observer has written it, the line says the class is unknown, never v2.

Tests and the dry run

site/lib/proof.test.mjs (the tail parse and the statement decode against the real tail of block 59199's proof, reproducing the statement the host printed; the three checks on genuine and tampered bytes; the state words), site/api/verify.test.mjs (the API from a fixture: block, shards, capture, the bytes route, 404/410/400, the state cut), tools/observer/proof-capture.test.mjs (the host-output parsers against real lines; the capture against a fake node; on a Mac with the app, the native verifier refusing block 59199's pre-pin proof: "IS NOT OURS"), site/api/public-stats.test.mjs (C46). CI runs the first, second and fourth with the explorer tests.

Dry run, 6 October 2026, 16:08 to 16:16 UTC. The devnet's execution was reset for 0.3.13 at about 15:48 UTC and node 1's exec RPC (127.0.0.1:26790) came back at tip 135,933 by 16:12 with an EMPTY pool (the provers were still off), so the live capture could not run; the fixture route was used instead, with the real verifier and the real tables:

Step Result
Pinned host (the Mac app bundle's igneum-prove-host) proves shard 0 of proving/fixtures/block-56-transfers.json on the CPU 81.3 s beside another agent's cargo build (not a number); proof 1,272,897 bytes, statement 0x5a4d59f4..., --mode verify 0.041 s, program id the pinned one
captureProof() on those bytes with a fake node handing them out, the native verifier on SHA-256 ok, statement ok, fields ok; native VERIFIED in 29 ms, key setup 197 ms, process 240 ms; the whole capture 282 ms
A test observer (LIVE_TABLE_PREFIX=vtest_) against the live node for 5 minutes schema created, pow_epoch written (class 3, next 3, epoch 55, activation 154,800), proving feed on 26790; RPC load 230 to 248 wRPC calls a minute against the live observer's 222 to 224, EVM 2,466 against 2,481
/proof/c47bacc8... through tools/site-serve.mjs in the browser pane VERDICT VERIFIED; 5.8 ms of checks and 139 ms of download in the browser, STARK 29 ms on the node; the three checks print got and want; the statement decodes to block 56, 3 transactions, gas 63,000, pgas 600
/block/c47bacc8..., /block/136508, /explorer the state chip and the note; height 136508 first resolved to a block that had left the chain (two blocks shared the number after a tip reorg), fixed: the height lookup prefers the chain block
The known-failed case tools/observer/proof-capture.test.mjs: the pinned verifier refuses block 59199's pre-pin proof ("IS NOT OURS", 0x0559759b...), parsed and stored as native NOT VERIFIED with the note

Owed: the same run against a devnet pool proof once the provers are back (the test observer captures one within PROOF_CAPTURE_EVERY_MS of seeing it; grep "proof captured" in its log), and the live observer's restart on this code (autosync picks it up from master; until then /api/verify answers with empty shards and the stats line says the class is unknown).