---
eip: 8379
title: Top-up Sync
description: CL-driven EL synchronization by exposing the execution client head and distinguishing missing state from missing history in the engine API
author: Jacek Sieka (@arnetheduck), Dustin (@tersec), Tamaghna Choudhuri (@RazorClient)
discussions-to: https://ethereum-magicians.org/t/eip-8379-top-up-sync/29405
status: Draft
type: Standards Track
category: Core
created: 2026-08-06
---

## Abstract

This EIP introduces an engine API method for querying the execution client head, distinguishing the head of the block chain from the head for which state is held, and refines the payload status semantics so that a missing pre-state is distinguishable from missing block history. Together, these changes allow the consensus client to drive execution block sync deterministically ("top-up sync"), using `engine_newPayload` and `engine_forkchoiceUpdated` as the sole block delivery mechanism.

## Motivation

Today, execution and consensus clients each obtain blocks independently: consensus clients via gossip and req/resp, execution clients via devp2p. This duplicates network functionality across the two layers. With top-up sync, the consensus client syncs as usual and, in parallel, feeds the execution client each block that immediately follows the execution client head, until the execution client reaches the consensus client head. Enabling this through the engine API provides:

1. **Consensus-driven sync:** The consensus client can provide a canonical block source for the execution client, opening a path towards deprecating the eth protocol block-distribution messages (`GetBlockHeaders`/`BlockHeaders`, `GetBlockBodies`/`BlockBodies`), with snap sync and the mempool remaining on devp2p.

2. **A head query:** The top-up loop requires the consensus client to learn the head of the execution client's block chain, so it knows which payload to deliver next, and separately the head of the execution client's executed chain — the latest block for which the execution client holds post-state — so it knows how far the execution client has validated. The two differ whenever the execution client holds blocks it has not executed, which is what enables efficient syncing: the execution client can accept payloads without executing them and fast-forward its state by other means. Neither head is currently queryable through the engine API; both queries therefore need to be defined within it.

3. **Deterministic choreography:** When `engine_newPayload` returns `SYNCING`, the consensus client cannot tell whether the execution client is missing the payload's ancestors — in which case the consensus client should supply them — or missing the pre-state while snap-syncing — in which case supplying ancestors is useless and the consensus client should wait. Disambiguating the two makes the `engine_forkchoiceUpdated`/`engine_newPayload` choreography deterministic.


## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).


### `engine_getSyncStatus`

This method is added to the engine API. Client software MUST support it from the activation of the fork in which this EIP is included onwards, and MUST advertise it via `engine_exchangeCapabilities`.

#### Request

* method: `engine_getSyncStatus`
* params: none
* timeout: 500ms

#### Response

* result: `SyncStatus`
* error: code and message set in case of an exception during processing

`SyncStatus` is an object with the following fields:

| Name | Type | Description |
| - | - | - |
| `headBlockHash` | `DATA`, 32 Bytes | Hash of the latest block in the client's canonical chain |
| `headBlockNumber` | `QUANTITY`, 64 Bits | Number of the latest block in the client's canonical chain |
| `executedBlockHash` | `DATA`, 32 Bytes or `null` | Hash of the latest canonical block whose post-state the client holds |
| `executedBlockNumber` | `QUANTITY`, 64 Bits or `null` | Number of the block referenced by `executedBlockHash` |

The pair `(executedBlockHash, executedBlockNumber)` describes the head of the client's executed chain: the latest canonical block that the client has validated and holds the post-state of, i.e. the block whose child the client is able to execute. In a `VALID` `PayloadStatus` response, `latestValidHash` references this same block; see the Rationale for why a distinct field name is used. The pair `(headBlockHash, headBlockNumber)` describes the head of the client's block chain: the latest block in the contiguous chain of held canonical blocks that is rooted in the executed head — or in the genesis block, if there is no executed head.

Client software:

* MUST report as block head the latest block of the contiguous chain of held blocks that starts at the executed head — or at the genesis block, if there is no executed head — and follows the canonical chain as selected by the most recent `engine_forkchoiceUpdated` call; a freshly initialized client thus reports its genesis block until an initial state sync completes
* MUST answer an `engine_newPayload` call for a payload whose parent is the reported block head with one of `VALID`, `INVALID` or `MISSING_STATE` (see [Payload status disambiguation](#payload-status-disambiguation)), and MUST NOT answer it with `SYNCING`
* MUST return the same `SyncStatus` in two consecutive calls unless an `engine_newPayload` or `engine_forkchoiceUpdated` call has been processed between them
* MUST report as executed head the latest canonical ancestor-or-self of the block head that has been fully validated and whose post-state is available
* MUST set `executedBlockHash` and `executedBlockNumber` to `null` if no canonical block satisfies the above, e.g., before an initial state sync has completed
* MUST NOT report blocks that have been received via `engine_newPayload` but not yet appended to the canonical chain

### Payload status disambiguation

The payload status enum returned by the versions of `engine_newPayload` and `engine_forkchoiceUpdated` introduced alongside this EIP is extended with the value `MISSING_STATE`.

Client software:

* MUST return `{status: MISSING_STATE, latestValidHash: null, validationError: null}` if all ancestor blocks of the payload are known but the payload was not executed because the required pre-state is unavailable
* MUST persist a payload answered with `MISSING_STATE` for later processing, extending its chain of known blocks as if the payload had been executed — once the payload is made canonical by a subsequent `engine_forkchoiceUpdated` call, the block head advances while the executed head does not
* MUST return `{status: MISSING_STATE, latestValidHash: null, validationError: null}` from `engine_forkchoiceUpdated` if the block referenced by `headBlockHash` and all its ancestors are known but the block has not been executed because the required pre-state is unavailable
* MUST return `SYNCING` only if ancestor block data required for processing is missing
* MAY close the gap between the executed head and the block head by any means: executing stored payloads in order, or acquiring state directly (e.g., via snap sync or [EIP-7928](./eip-7928.md) block access list fast-forward) and skipping execution of the blocks below it

Consensus layer client software:

* SHOULD respond to `SYNCING` by supplying the missing ancestor payloads via `engine_newPayload` in ascending order, as retrieved from its own block store or via its req/resp protocol
* SHOULD respond to `MISSING_STATE` by continuing to deliver subsequent payloads and `engine_forkchoiceUpdated` calls as its head advances, and MUST NOT interpret `MISSING_STATE` as a request for ancestor payloads
* MUST NOT treat a payload answered with `MISSING_STATE` as validated — the payload remains unvalidated until the execution client's executed head advances past it

### Top-up sync procedure

This section describes how a consensus client uses the above primitives to drive execution block sync. The forkchoice state communicated via `engine_forkchoiceUpdated` is unchanged from the existing engine API specification.

While its own (optimistic) sync proceeds, the consensus client runs a top-up loop against the execution client:

1. Query `engine_getSyncStatus`.
2. If the block head matches the consensus client's view of the canonical execution head, delivery is complete — the chain is fully validated once the executed head catches up to the block head.
3. If the consensus client's canonical chain contains a block at height `headBlockNumber + 1` with parent hash `headBlockHash`, deliver it via `engine_newPayload` and repeat. By the guarantees above, the response is `VALID`, `INVALID` or `MISSING_STATE`, so the loop always makes progress.
4. If the block at height `headBlockNumber + 1` on the consensus client's canonical chain has a different parent hash, the execution client's head is on a fork the consensus client has abandoned; the consensus client locates the fork point against its canonical chain and re-delivers from there.
5. If the block at height `headBlockNumber + 1` is older than the history the consensus client retains (at most the weak subjectivity period), top-up is not possible and the execution client has to acquire recent state by other means, e.g., snap sync. The consensus client continues issuing `engine_forkchoiceUpdated` for its head — from which the execution client selects its state sync target — and resumes the loop once the executed head, and with it the block head, has moved within range.

Since a single top-up may span fork boundaries, execution client software MUST accept payloads belonging to earlier forks via the `engine_newPayload` version introduced alongside this EIP, applying the validation rules of the payload's fork. Consensus client software SHOULD deliver historical payloads through this version rather than the version matching the payload's fork, making `MISSING_STATE` available across fork boundaries, and MUST supply the associated data required by the payload's fork (e.g., blob versioned hashes and execution requests).

### Interface migration

This EIP is specified against the JSON-RPC transport of the engine API. This section is non-normative and records the intended mapping onto the REST/Simple Serialize (SSZ) engine API transport in the REST/Simple Serialize (SSZ) engine API.
| Method | Endpoint |
| - | - |
| `engine_getSyncStatus` | `GET /syncstatus`, unscoped, analogous to `GET /identity` |
| Payload status enum | `MISSING_STATE` added to the payload status SSZ enum for endpoints scoped to the fork in which this EIP is included |


## Rationale

### Why a new method?

Top-up sync inverts the direction of control: the execution client no longer syncs blocks by itself, so the consensus client needs a query meaningful when the execution client is passive. No existing engine API method reports either head on demand, and queries outside the engine API are not consistently served on the authenticated port nor specified precisely enough for a deterministic sync loop to be built on them.

### A single head rather than a set of heads

An alternative design returns the set of chain tips the client is able to extend — every head of the block subtree rooted in the finalized block — giving the consensus client a definitive delivery point for any fork it may switch to. This obliges the execution client to track all such tips and was set aside in favor of a single head carrying the same per-head guarantees (a child of the reported head is never answered with `SYNCING`, and the report only changes in response to `engine_newPayload`/`engine_forkchoiceUpdated`): when the reported head is not on the consensus client's canonical chain, the consensus client can locate the fork point against its own chain and re-deliver from there, exactly as when responding to `SYNCING`. A status query for an arbitrary block hash, allowing a common ancestor to be found in lock step, can later be added as an extension without changing this method's semantics.

### New status value rather than a new field

`MISSING_STATE` follows the precedent of `ACCEPTED`, which was added to the payload status enum without altering the response structure. A separate `syncingReason` field on the `SYNCING` status was considered, but a distinct status value keeps dispatch on the consensus client side a simple switch over the enum and cannot be silently ignored by consensus client implementations.

### Two heads

A client has two distinct notions of "head": the head of its block chain and the head of its executed chain, and syncing benefits from decoupling them. The block head drives payload delivery, so the client does not have to execute every block to accept it — it can defer execution and instead fast-forward its state, e.g., via snap sync or [EIP-7928](./eip-7928.md) block access lists, executing only near the network head. The executed head drives validation status: it is the block that `latestValidHash` references in a `VALID` `PayloadStatus` response, and it determines how far the consensus client may consider the chain validated. A distinct field name — `executedBlockHash` — is used regardless, because `latestValidHash` in an `INVALID` response is branch-relative (the last valid ancestor of the invalid branch), while the executed head is a property of the canonical chain and may even move backwards between calls when the chain reorgs below it. A richer sync-progress report (e.g., pivot block, downloaded trie ranges) is left to future extensions of this method: the two heads are sufficient for driving sync.

### Batched payload delivery

Delivering historical payloads in batches (e.g., 1024 payloads per call) and reduced verification for finalized blocks would further improve top-up throughput. Both are deliberately left to future EIPs; this EIP is the minimal change that makes the choreography correct.

## Backwards Compatibility

`engine_getSyncStatus` is a new method and does not affect existing consumers. `MISSING_STATE` is only returned by the method versions of `engine_newPayload` and `engine_forkchoiceUpdated` introduced alongside this EIP; prior method versions retain their existing semantics, where `SYNCING` covers both missing-data cases. Consensus clients that do not implement top-up sync can treat `MISSING_STATE` exactly as they treat `SYNCING` today without loss of correctness.

Unlike prior method versions, which each accept payloads of exactly one fork, the `engine_newPayload` version introduced alongside this EIP accepts payloads of all earlier forks — a deliberate departure from established engine API versioning, required for top-up across fork boundaries.

## Security Considerations

Blocks delivered via `engine_newPayload` during top-up are validated by the same rules as blocks received during regular operation. Since the consensus client only feeds payloads on its canonical chain, the execution client's exposure to invalid or adversarial block data is reduced relative to devp2p block download.

The requirement to persist payloads answered with `MISSING_STATE` implies unbounded storage growth if the executed head never catches up. Whether execution clients may prune such stored payloads (relying on the consensus client to re-deliver them) and what bounds apply to this buffering need discussion.

The engine API is served on an authenticated port, so both `engine_getSyncStatus` and the `MISSING_STATE` status are only exposed to the trusted consensus client and do not leak sync state to third parties.

The choreography assumes a single consensus client driving the engine API. Multiple consensus clients attached to the same execution client interleave `engine_newPayload` and `engine_forkchoiceUpdated` calls, voiding the stability guarantee of `engine_getSyncStatus` from each individual client's perspective; this matches the existing engine API, where a multiplexed execution client follows the most recent forkchoice update, and multiplexer software is expected to serialize top-up traffic.

TODO: whether this EIP should place normative requirements on multiplexer software (e.g., serializing top-up traffic per consensus client, or presenting each consensus client with a consistent `engine_getSyncStatus` view) remains to be decided.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
