Skip to content

Running a verifier node

A verifier is a read-only node that replays every block from the producer against its own copy of the state. If the state root or block hash doesn’t match, it refuses the block and stops serving. Users and wallets connect to the verifier’s RPC, never to the producer directly.

This article covers the practical side: configuration, startup, what gets verified, and what happens when something fails. For the cryptographic details of state roots and block hashes, see State roots.

A verifier needs two config changes from a default (producer) node:

config/runtime.exs
config :chain,
mode: :verifier,
upstream_node: :"producer@10.0.0.1"

mode: :verifier changes what starts on boot:

ComponentProducerVerifier
Genesis initializationYesNo (fetched from upstream)
Block producerYesNo
Transaction poolYesNo
Verifier syncerNoYes
RPC (eth_, /wallet/)Internal onlyPublic-facing
BlockFeed (serves history)YesYes
BlockStage (live broadcast)YesYes

The verifier needs its own database. It never shares storage with the producer.

On first boot with an empty database:

  1. The syncer connects to the upstream node via Erlang distribution.
  2. It subscribes to the live block feed, then starts catching up from block 0.
  3. For genesis (block 0), the verifier independently creates the same initial accounts and computes the expected state root. It compares against the genesis block received from upstream. If they match, genesis is committed locally.
  4. For every subsequent block, the verifier replays all transactions, recomputes the state root and block hash, and commits only if both match.
  5. Once caught up, the verifier processes live blocks as they arrive. Blocks already committed during catch-up are skipped by number.

If the upstream node is unreachable at boot, the syncer retries every 5 seconds.

For every block (including genesis), the verifier independently verifies:

CheckWhat it catches
state_rootProducer wrote state that doesn’t follow from the transactions. Covers balances, HTLC swaps, precompile registrations.
transactions_rootProducer substituted, added, or removed transactions from the block.
block_hashAny field in the block header was tampered with after construction.
parent_hashBlock doesn’t chain correctly from the previous one. Fork detection.
block_numberGaps in the sequence (skipped blocks).
chain_idCross-chain replay (transaction signed for a different network).
sender recoveryFor Ethereum transactions, the sender is re-derived from the signature. For Tron transactions, the signature is re-verified against the claimed sender.
genesis invariantsGenesis timestamp and transactions root match the canonical constants. Prevents adversarial genesis forgery.
bridge cross-chain checkEvery bridge_mints row is independently verified against finalized Ethereum state via JSON-RPC. For bridge_lock rows, the receiverOn2D from the Ethereum Locked event is compared to the HTLC receiver on 2D. See Bridge for the full verification table.

A mismatch on state_root, transactions_root, or block_hash is a consensus violation. The verifier halts and refuses to serve. Operational errors (upstream temporarily down, gap in block sequence) trigger a catch-up retry.

A verifier rejects state-mutating RPC calls:

  • eth_sendRawTransaction returns error code -32601
  • /wallet/broadcasttransaction returns Tron error OTHER_ERROR (code 20)

All read-only methods work normally: eth_getBalance, eth_getTransactionReceipt, eth_getBlockByNumber, /wallet/getaccount, etc. Wallets and explorers can point at a verifier without changes.

Every verifier re-broadcasts verified blocks on its own block feed. A second verifier can subscribe to it instead of the producer:

# Verifier B chains off Verifier A, not the producer
config :chain,
mode: :verifier,
upstream_node: :"verifier_a@10.0.0.2"

Each verifier independently replays every block regardless of where it received it from. The security model is the same: verify, then serve.

Producer ──▶ Verifier A ──▶ Verifier C
▶ Verifier B ──▶ Verifier D

The producer and verifier connect via Erlang distribution. Two ports, both firewalled:

  • EPMD port (4369 by default): the Erlang Port Mapper Daemon.
  • Distribution port (configured in vm.args or RELEASE_DISTRIBUTION): the actual data channel, TLS encrypted.

The producer exposes no public HTTP ports. All user traffic goes through the verifier.

Users ──▶ Verifier (port 4000, public) ──▶ Producer (Erlang dist only, firewalled)
ScenarioVerifier behavior
Upstream unreachable at bootRetry catch-up every 5 seconds until connected
Upstream goes down mid-syncLive events stop arriving; reconnect and catch-up on recovery
Block gap (missed blocks)Automatic catch-up from upstream BlockFeed
state_root mismatchHalt. Log critical alert. Stop serving RPC.
block_hash mismatchHalt. Log critical alert. Stop serving RPC.
Nil raw transaction dataBlock recorded as failed (status 0), no crash

A halted verifier requires manual investigation. A mismatch means either the producer is compromised or there is a determinism bug in the executor. Both warrant human review.