Перейти к содержанию

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)]
  1. 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.
  2. 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).
  3. The fetcher pulls the range and returns fully-assembled, validated blocks.
  4. The worker confirms each block extends what we already have, then writes it and logs an Imported block line 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 detected line with the depth, and refetches and overwrites the chain from there.
  • Reorg deeper than the tail — if no common ancestor is found within --seal-confirmations blocks 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.