What a stranger sees: paste a chain block hash on /proof, the page downloads the captured proof bytes (1,272,897 bytes), hashes them in the tab against the proof_hash the signed record carries, parses the 328-byte public values out of the SP1 container and checks keccak against the record's statement and the decoded fields against the block (site/lib/proof.mjs, no library). The STARK is verified by this site's node (the observer runs igneum-prove-host --mode verify with the pinned key on each capture: 29 ms verify, 197 ms key setup on the fixture proof); the page says so and labels the in-browser STARK verifier as coming. docs/plans/explorer.md section 8 carries the size and time numbers and the two routes (Groth16 wrap plus sp1-verifier in wasm, or the compressed verifier ported to wasm32). Observer: a sample of pool proofs captured through igneum_getProofBytes while the node holds them (PROOF_CAPTURE_EVERY_MS, PROOF_BYTES_KEEP), checked and verified, written to live_proof_bytes; every live_proofs row carries the record (key_hash, payout, statement, proof_hash); getBlockTemplate.powEpoch read every 10 s into live_state.pow_epoch. RPC load: wrpc 230 to 248 per minute against 222 to 224 before, evm unchanged. P17: the node release 0.3.13 (bb43e9a8) does not carry the state field (it is on ledger-fixes-0311 fbb0082a), so the explorer cuts the one word from the observer's tables by the design 2.4 rule and takes the node's word per transaction when the fork answers one. A block that left the selected chain reads included with a note, never reorged out. C46: /api/stats algorithm reads "class v3 / generator 3 (epoch 55; ...)" from the node's epoch line, v4 when the node reports 4, "unknown" before the observer has read it; new lottery field. Tests: site/lib/proof.test.mjs (the real tail of block 59199's proof reproduces the host's statement), site/api/verify.test.mjs, tools/observer/proof-capture.test.mjs (the native verifier refusing a pre-pin proof), site/api/public-stats.test.mjs. Dry run on the fixture proof of block 56 through the local preview: VERIFIED, 5.8 ms of checks and 139 ms of download in the browser, STARK 29 ms on the node. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| autosync.sh | ||
| observer.mjs | ||
| proof-capture.mjs | ||
| proof-capture.test.mjs | ||
| README.md | ||
| run.sh | ||
Igneum devnet observer
Watches one Igneum node over wRPC JSON and writes what it sees to Neon, so /api/live and /live on the site can show the devnet as it runs. Node 22 or newer, no dependencies.
Run
node tools/observer/observer.mjs
Environment, every value optional:
| Variable | Default | Meaning |
|---|---|---|
IGNEUM_RPC |
ws://127.0.0.1:28610 |
The node's wRPC JSON url. 28610 is the igneum-devnet wRPC JSON port (consensus/core/src/network.rs). Start the node with --rpclisten-json=127.0.0.1:28610 or the port of your choice. |
DATABASE_URL |
read from ~/.config/igneum/env |
Neon connection string. Never commit it. |
LIVE_RETAIN_HOURS |
24 |
Hours of blocks kept in live_blocks. Older rows are deleted once a minute. |
LIVE_TABLE_PREFIX |
empty | Prefix for every table name, so a test observer against a test network can write fintest_live_* without touching the site. |
IGNEUM_PROOF_VERIFIER |
the Mac app bundle's igneum-prove-host when it exists, else none |
Path to igneum-prove-host, the native SP1 verifier run on each captured proof (--mode verify --proof <file> --statement 0x.., the pinned key, 0.2 s of key setup and 0.03 s of verify per proof; docs/bench-log.md). Its pinned ids are logged at start. Empty = captured proofs get the browser checks only. |
PROOF_CAPTURE_EVERY_MS |
300000 |
One pool proof is captured every 5 minutes: fetched with igneum_getProofBytes [number, shard, keyHash] while the node still holds it (600 chain blocks, about 10 minutes after the proven block), checked as a browser would (SHA-256 against the record's proof_hash, keccak of the public values against its statement, the decoded statement against the block), verified natively, written to live_proof_bytes. A compressed shard proof is 1,272,897 bytes, so every proof at one chain block per second would be 110 GB a day; the sample is the size that fits. |
PROOF_BYTES_KEEP |
50 |
How many captures keep their bytes (about 64 MB in Neon). Older rows keep the record, the checks and the verdict. |
IGNEUM_TEMPLATE_ADDRESS |
the newest block's coinbase address | The pay address getBlockTemplate is asked with every 10 s for the epoch line (powEpoch: program class, generator, epoch index), written to live_state.pow_epoch for /api/stats (reviewer C46). |
IGNEUM_EVM_RPC |
http://127.0.0.1:26800 |
The execution layer's JSON-RPC (http) of a node on the proving build, for igneum_getShardPlan, igneum_getProofRecords and igneum_getProvingStatus. On the Mac that port is the Igneum Miner app's node, so an app restart (an OTA at its slot minute, a quit) blips the proving feed with fetch failed lines until it is back; the observer's own node (observer-v4) has no --evm-rpclisten. The default is the Mac app's node; the observer node itself has no EVM listener yet (start it with --evm-rpclisten=127.0.0.1:<port> once it runs the proving build and point this at it). A node without the RPCs (method not found) gives proving = {supported: false}, rechecked every 5 minutes; an unreachable endpoint is retried every 20 s. |
A node started by another tool may listen on gRPC only. Then run your own non-mining peer with a JSON listener, on ports that do not clash with the devnet's (gRPC 26610, P2P 26611):
vendor/igneum-node/target/release/igneumd --devnet --nodnsseed --disable-upnp \
--appdir=/tmp/igneum-obsnode --rpclisten=127.0.0.1:26640 --rpclisten-json=127.0.0.1:28640 \
--listen=127.0.0.1:26641 --connect=127.0.0.1:26611 --nologfiles
IGNEUM_RPC=ws://127.0.0.1:28640 node tools/observer/observer.mjs
What it does
- Subscribes to
blockAddedandvirtualChainChanged; pollsgetBlockDagInfo,getInfo,getConnectedPeerInfo,getSinkBlueScoreandestimateNetworkHashesPerSecondevery 2 s. - Decodes the miner address from the coinbase payload script (same bech32 variant as
crypto/addresses). The fork'svote_key_hashheader field is stored per block; the first 8 hex characters are the miner's short id on the site. engineis the miner's tag in the coinbase extra data after the node's version prefix. The node exposes no engine name over RPC, so this is null on devnet v0.- When the node refuses the hash-rate estimate (it needs a 1,000-block window) the observer reports blue work added per second over the last 10 minutes instead.
- Proving v0 (4 Oct 2026, spec 7.7): every chain block (the
isChainBlockflag of a new block, or avirtualChainChangedaddition) has its shard plan read overIGNEUM_EVM_RPCas{blockHash}(the chain block hash is the same hash on both layers), onelive_proofsrow per shard in stateplanned; a plan the EVM node has not executed yet is retried with a growing delay (up to 30 tries). The proof records of the chain blocks of the last 10 minutes are polled in rotation (80 blocks per 2 s tick while active, 10 before activation, four calls in flight); a shard moves toproving(a record in the node's pool),verified(the SP1 proof verified by the node's verifier, or the record carried by a block and checked by consensus, which is what happens before activation) orpaid(a carrying segment paid it).lag_daais the carrier's DAA score minus the block's;proveris the first 8 hex characters of the record's vote key hash. A block whose every shard is paid, or older than 10 minutes, leaves the rotation. Events:proving(activation reached, first paid shard seen) andprover_seen(one per prover per run). A reorg drops the removed chain blocks from the rotation. - Explorer (5 Oct 2026,
docs/plans/explorer.md): every block row also carries what/explorer,/block/<hash>and/address/<addr>show, all read from theblockAddednotification itself (no extra RPC per block, measured: 282 against 283 wRPC calls per minute, 785 against 776 EVM calls in the same minute, before and after):tx_count(EVM transactions in the block),evm_miner(the coinbase'sIGNApayout address, else the vote key hash's low 20 bytes asconsensus/core/src/evm.rsfalls back to),proof_records(records in theIGNPsection, 274 bytes each),subsidy_sompi(theE(daa)the payload declares),paid_sompi(the coinbase outputs' sum),selected_parent,number(the chain block number, filled in when the shard plan arrives) anddetail(header fields, mergeset, coinbase outputs, EVM transaction hashes, certificate indices).live_state.rpc_loadcounts this process's RPC calls per minute;live_state.supply_checkis the hourly comparison of the newest 500 blocks with the emission rule (site/lib/emission.mjs, spec 2.5): the declared subsidy againstblockSubsidy(daa, 1), and each block's outputs against the declared subsidies of the blocks it merges. The observer now importssite/lib/emission.mjsandsite/lib/eth.mjs(keccak for transaction hashes); autosync restarts only onobserver.mjsandrun.shchanges, so a change to those two libraries needs a restart by hand. - Finality v2 (3 Oct 2026): subscribes to
FinalityLock(the node's lock event) and pollsgetFinalityCheckpointsevery 2 s andgetFinalityWeightsevery 10 s. Every checkpoint the node reports is upserted intolive_checkpoints; a checkpoint turninglockedwrites the eventcheckpoint N locked (xx% of weight, yy% of active, v votes of n voters) at block h. The weights snapshot (total, active, per key) goes intolive_state.finality. A node from before the finality layer answers the RPC with an error; the observer then logs once and skips finality.
Tables
Created on start if missing.
| Table | Rows | Columns |
|---|---|---|
live_blocks |
one per block, kept LIVE_RETAIN_HOURS |
hash, blue_score, daa_score, timestamp_ms, parents (count), parent_hashes, is_chain_block, vote_key_hash, miner_address, engine, received_at, color; explorer: tx_count, evm_miner, proof_records, subsidy_sompi, paid_sompi, selected_parent, number, detail (jsonb). Indexes on received_at, timestamp_ms, (vote_key_hash, received_at), (evm_miner, received_at), (miner_address, received_at), number. |
live_state |
one row, updated every 2 s | block_count, header_count, blue_score, difficulty, hashes_per_second_estimate, peers, mempool, node_version, network, blocks_60s, blocks_per_minute (60 pairs of minute epoch ms and count), observer_started_at, updated_at |
live_events |
one per event, kept 7 days | ts, kind, text. Kinds: observer, miner_seen, miner_quiet, miner_back, peer_joined, peer_left, difficulty (step over 5%), checkpoint_locked. |
live_checkpoints |
one per checkpoint index, kept 7 days | index, hash, blue_score, daa_score, state (proposed, certified, locked), signed_weight, active_weight, total_weight, fraction_active, fraction_total, votes_seen, voters, aggregators (key hashes whose sortition proof made them aggregators), locked_at, first_seen_at, updated_at. |
live_proofs |
one per planned shard of a chain block, kept LIVE_RETAIN_HOURS |
block_hash, shard, shards (in the plan), block_number, block_daa, block_ts, pgas, state (planned, proving, verified, paid), prover (id8), verified, carried_by, carrier_number, carrier_daa, lag_daa, payout_wei, received_at, updated_at. Primary key (block_hash, shard), index on received_at. |
live_proof_bytes |
one per captured shard proof, kept LIVE_RETAIN_HOURS |
block_hash, shard, key_hash (the prover's vote key hash), number, prover (id8), payout, statement, proof_hash, proof (bytea, the newest PROOF_BYTES_KEEP only), proof_bytes, public_values (the 328-byte shard statement parsed from the proof's tail), sp1_version, node_verified, proof_hash_check, statement_check, fields_check, native_verified, native_verify_ms, native_setup_ms, native_total_ms, native_program_id (the id the proof names), native_pinned_id, native_ours, native_note, verifier, received_at. Primary key (block_hash, shard, key_hash). live_proofs also gained key_hash, payout, statement, proof_hash (the record the chain carries per shard). |
live_state.pow_epoch |
jsonb, refreshed every 10 s | epoch_index, epoch_seed, epoch_blocks, boundary_daa, virtual_daa, program_class (2 or 3, later 4: the generator version of the running epoch), next_program_class, class_v3_activation_daa, era_index, era_seed, dataset_log2, day_index, day_ms, read_at, source. |
live_state.proving |
jsonb, updated every 2 s | supported (false with reason when the node has no proving RPCs or the endpoint is unreachable), active, activation_daa, tip_daa, verifier, pool (entries, pending, verified, failed), paid_shards_total, shard_budget_pgas, blocks_10m, blocks_fully_proven_10m, shards_proven_10m, shards_paid_10m, median_proof_lag_s (median lag_daa of the last 10 minutes; the devnet targets one DAA step per second), provers_10m, open_blocks, pending_plans, evm_rpc. |
live_state.finality |
jsonb, updated every 2 s | params, chain_id, next_index, finality_active, latest_locked_index, latest_locked_hash, latest_locked_blue_score, weights (total_weight, active_weight, voters, keys[] with id, blocks, voter, participation, stripped_until_daa, revealed). |
Keeping it current (autosync)
tools/observer/autosync.sh (loop: nohup bash tools/observer/autosync.sh & from the shared checkout; log /tmp/igneum-devnet/autosync.out) fetches origin/master every 5 min and fast-forwards the shared checkout when it is clean under tools/observer and site/api; a failed fast-forward logs git's reason and the dirty files the incoming commits also touch. Whoever moved HEAD (this loop or a pull by hand), the observer is restarted (a kill; run.sh starts it again in 3 s) whenever the checked-out observer.mjs or run.sh differs from what the running one started from: the key is the two blob ids hashed, kept in /tmp/igneum-devnet/observer.tree and rewritten at each restart. tools/observer/autosync.sh check prints the key, the marker and whether a restart is due (exit 3 when it is). A change to autosync.sh itself does not restart the observer, but the running loop keeps its old code: replace it by hand (kill it, start it again) after such a change; write the marker first (git rev-parse HEAD:tools/observer/observer.mjs HEAD:tools/observer/run.sh | tr -d '\n' | shasum | cut -c1-40 > /tmp/igneum-devnet/observer.tree) when the running observer is already current, or the new loop restarts it once for nothing. Each restart logs seeded N checkpoint states and, if a lock is recorded over a seeded state, names that state.
Reading it
site/api/stats.mjs, site/api/supply.mjs, site/api/explorer.mjs and site/api/verify.mjs serve /api/stats, /api/supply, /api/explorer and /api/verify (docs/api/public-stats.md). site/api/live.mjs serves /api/live from these tables in five indexed queries (proving from live_state.proving; every block carries shards: [{i, n, state, prover, lag, payout, pgas}] and proven). LIVE_TABLE_PREFIX on the API reads a test observer's tables. site/live.html polls it every 2 s. The site shows OFFLINE when live_state.updated_at is older than 30 s.