Realtime sync¶
The realtime worker keeps the block store current with the chain head: as new blocks appear it downloads each one — header, body and (unless execution derives them) receipts — from peers, validates it, and writes it to the store. It is one of UMNode's two fill fronts; historical backfill fills old blocks downward to genesis. The two run concurrently and meet in the middle.
It runs by default whenever a block store (--block-db) and head polling
(--head-poll-every > 0) are both enabled. Disable it with --realtime=false.
How it works¶
flowchart LR
HP[Head poller<br/>polls one peer] -->|new head| RT[Realtime worker]
RT -->|fetch gap| F[Fetcher]
F -->|headers / bodies / receipts| PS[(Ready peers)]
F -->|validated blocks| RT
RT -->|Put / overwrite tail| ST[(Block store)]
- The head poller tracks the chain head by polling one peer (switching peers when it stops advancing) and emits a new-head event whenever it advances.
- On each event the worker reads the store's own head and asks the fetcher for the gap between them (bounded per event — see Catching up below).
- The fetcher pulls the range and returns fully-assembled, validated blocks.
- The worker confirms each block extends what we already have, then writes it
and logs an
Imported blockline with its number, hash and transaction count.
The fetcher¶
The fetcher is the reusable, load-balanced puller (the backfill worker uses the same component). For a range it:
- requests headers by number, then bodies and receipts by hash;
- spreads those requests across all ready peers and retries a failure on a different peer, so no single peer is a bottleneck or a single point of failure; configured RPC peers — JSON-RPC endpoints of existing nodes — are tried first, and the wire peers take whatever they lack;
- validates every block against its header before returning it — the transactions, uncles and withdrawals roots for the body, and the receipts root for the receipts. A peer that reorders or tampers with data fails validation and the block is refetched elsewhere, so stored data is trustworthy even though peers are not.
Receipts need eth/69+ — when they are fetched at all
Bodies are fetched from any peer, but receipts use the compact eth/69+ wire encoding, so they are only requested from eth/69 and eth/70 peers. eth/68 peers still contribute to head tracking and header/body fetching. On eth/70 a peer may split a large block's receipts across responses; the fetcher resumes and reassembles them transparently.
By default receipts are derived, not fetched (--receipts derive):
blocks are assembled header + body only — from any peer, about a quarter
less download — and the execution subsystem supplies the receipts. Only the
pre-Byzantium range is still fetched. Set --receipts fetch to download
them instead. See Receipts.
Reorgs¶
A reorg replaces recent blocks. Because the most recent --seal-confirmations
blocks are kept overwritable in the tail,
the worker can absorb any reorg within that depth:
flowchart TB
NH[New head's first block] --> CHK{Parent ==<br/>our stored head?}
CHK -->|yes| EXT[Extend: store the new blocks]
CHK -->|no| SEEK[Seek the common ancestor<br/>down through the tail]
SEEK --> FOUND{Found within<br/>the tail?}
FOUND -->|yes| OW[Log the reorg, refetch and<br/>overwrite from the ancestor]
FOUND -->|no| HALT[Halt: reorg deeper than the tail]
- Extend — the common case: the incoming block's parent is our head, so the blocks are simply appended.
- Reorg within the tail — the parent doesn't match, so the worker walks back
through the tail comparing the peers' chain to ours until the common
ancestor, logs a
Reorg detectedline with the depth, and refetches and overwrites the chain from there. - Reorg deeper than the tail — if no common ancestor is found within
--seal-confirmationsblocks of the head, the divergence reaches sealed, already-confirmed history. That is a finality violation that should not happen post-merge; rather than rewrite immutable data on a peer's say-so, the worker logs an error and halts. Sealed data is never touched. Recovery is an operator decision.
Cold start¶
On an empty store there is no parent to validate against, so the worker anchors on the current head: it stores that single block and chains forward from there. It does not fill in earlier history — that is backfill's job. So a fresh node begins storing from the first block it sees after start-up, while backfill fills everything below it.
The same anchoring happens on a non-empty store when a forward backfill is filling from genesis and the head is more than the handoff distance above the store's highest block (see forward fill with realtime): realtime anchors at the head and leaves the gap to the backfill.
Choosing where a fresh store starts: --init-start-block
By default a fresh node starts from the live head. Set --init-start-block N
to initialize an uninitialized store at block N instead — it fills
forward from there, leaving everything below N absent (handy to begin from a
chosen point, or to reproduce a deep catch-up without waiting). It applies
only when the database has no blocks yet: once the store is initialized it is
ignored, and an N above the current head clamps to the head. A mid-segment
N leaves that one segment unsealed until backfill completes it from below, so
choose a multiple of --segment-size if you want it to seal during the run.
Catching up¶
When the store is behind the head — a fresh node, or one restarted after
downtime — the worker fills the gap in chunks of --max-catch-up blocks,
looping over chunks within a single head event until it reaches the head, so
catch-up runs at fetch speed instead of waiting for the next head to arrive.
Each chunk is pulled by the fetcher in parallel
--fetch-batch-sized batches: it runs as many batches at once as there are
eth/69+ peers, up to --fetch-concurrency, and pipelines storage with the
network — each batch is written to the store as soon as it arrives while later
batches are still downloading. Blocks are written in order, so an interruption (a
shutdown, a peer failure) leaves a contiguous prefix and the remainder is
re-fetched on the next attempt.
Bulk filling of history below the anchor is the backfill worker's job, not realtime's.
Shutdown¶
On Ctrl-C (SIGINT) or SIGTERM the node shuts down gracefully: it cancels
the worker context and then waits for the realtime worker — and the other
background workers, including the RPC server's in-flight reads — to return before
it closes the block store. This ordering matters. The worker writes to the store
and the store is closed during teardown; closing it while a block write is still
in flight would pull the store out from under that write and crash the node. By
draining the workers first, the node keeps the store alive until its last user is
gone, so an interrupt during a write exits cleanly instead of panicking.
The worker also checks for cancellation between blocks while importing, so interrupting a large catch-up batch stops after the current block rather than draining the whole batch. Blocks are written in order, so stopping early just leaves the head a little lower; the remainder is re-fetched on the next head event, or after a restart by the next catch-up. Shutdown is therefore prompt even mid-fill.
Configuration¶
| Flag | Default | Meaning |
|---|---|---|
--realtime ⚙ |
true |
Run the realtime worker. Needs --head-poll-every > 0 and --block-db. |
--request-timeout ⚙ |
15s |
Per-peer timeout for each header/body/receipt request. |
--max-catch-up ⚙ |
4096 |
Blocks filled per catch-up chunk; the worker loops over chunks to the head. |
--fetch-batch ⚙ |
128 |
Blocks per header/body/receipt request batch. |
--fetch-concurrency ⚙ |
16 |
Max parallel fetch batches; scales to eth/69+ peer count up to this cap. |
--fetch-retries ⚙ |
3 |
Fetch attempts per batch before giving up. |
--rpc-peers ⚙ |
(none) | JSON-RPC endpoints of existing nodes to fetch from first; see RPC peers. |
All can also be set per-network in the config (Realtime, RequestTimeout,
MaxCatchUp, FetchBatch, FetchConcurrency, FetchRetries, RPCPeers); see
Configuration. If realtime is enabled but head polling is
off, the worker logs a warning and stays idle — it has no source of new heads.
For the storage side of reorgs and the tail, see Block store.