This EIP adds an Engine API method, engine_proposalLookaheadUpdatedV1, with which a consensus
layer client (CL) tells its execution layer client (EL) the slots, timestamps, fee recipients and
target gas limits of the blocks its validators are scheduled to propose in the current and next
epoch. It is a notice, not a command: it starts no build, names no parent block and changes no
consensus rule. The EL uses it to prepare for those blocks in ways that do not need the parent
block: fetching blob data, warming the state, pre-executing pending transactions, and keeping its
own heavy work and network traffic away from the slot.
In the Engine API up to and including Amsterdam, an EL learns that it has to produce a block when
the CL calls engine_forkchoiceUpdated with payload attributes, which also starts the build. The
call names the head to build on, and the attributes depend on it: the parent beacon block root,
and prevRandao and the withdrawals, computed from the parent's state advanced to the proposal
slot. A CL can send attributes before the parent arrives, but they then start a build on its
current head, which will not be the parent. In practice, the notice starts when the parent is
imported.
When CLs send it varies. Before Gloas, Prysm, Teku and Grandine send it on the parent's import, Lighthouse and Lodestar 4 seconds before the slot, and Nimbus 1.5 seconds before; only Lighthouse's lead can be configured. Even the earliest leaves only what remains after the parent arrives: at least 8 seconds when the parent is on time, by 4 seconds into its slot, and more only when the slot before it was empty.
The CL knows much earlier. Since EIP-7917 the proposer lookahead for the current and next epoch is part of the beacon state, so every proposal is known at least one epoch, 6.4 minutes, before its slot. Of the payload attributes, the timestamp, the fee recipient and the target gas limit are known as early. In the consensus specifications for Gloas, the consensus side of the fork that brings EIP-7732, a CL already lists these slots for its validators and may broadcast its preferences for them, the fee recipient and the target gas limit, to builders. Its own EL is not told.
A large part of what an EL can do to produce a block well does not depend on the parent block.
Only the EL knows how much time it needs for this preparation, so trying to drive it with the
timing of engine_forkchoiceUpdated is not ideal. Some examples, without trying to be exhaustive:
A node that builds every slot, as a professional builder does, can do all of this all the time. A validator proposes rarely. Without notice, whatever it prepares is mostly for blocks it will not build: it can prepare continuously and pay for it, prepare within a small budget and gain little, or not prepare at all.
Several changes shorten the time between knowing the parent and the slot, or add work to it:
Every validator, not only those that build all their blocks themselves. Consensus clients request a local payload alongside an external builder's bid, as a fallback and to compare their values (all six major ones, as of October 2026), so that payload has to be ready at the slot too; under EIP-7732, proposers can still build their own payloads. Keeping local building viable on recommended hardware as limits rise also keeps validators from depending on external builders for every block, which censorship resistance relies on. For a CL, supporting the method costs about one call per epoch, with data it already holds.
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 and RFC 8174.
All fields are required.
slot: QUANTITY, 64 bits: the beacon slot of the proposal.timestamp: QUANTITY, 64 bits: the timestamp of the block, as it will appear in the payload
attributes, computed with the slot duration that applies at slot.suggestedFeeRecipient: DATA, 20 bytes: the fee recipient the CL would put in the payload
attributes if slot were the next slot.targetGasLimit: QUANTITY, 64 bits: the target gas limit the CL would put in the payload
attributes if slot were the next slot.Request:
engine_proposalLookaheadUpdatedV1proposals: Array of ProposalV1, the upcoming proposals of the validators attached to the
CL, in strictly increasing slot order, at most 256 entries.Response:
nullSpecification:
proposals is the complete set of upcoming proposals the CL knows for its validators. The call
replaces the set the EL holds; an empty array clears it. The EL need not keep the set across
restarts.-38004: Too large request if proposals has more than 256 entries;-32602: Invalid params if an entry lacks a field or a field is malformed, or if slot or
timestamp does not strictly increase from one entry to the next.timestamp is not later
than its head's or that it cannot use, but MUST still validate the request as in item 2. It MUST
NOT start a build because of the call: a build starts, as before, with
engine_forkchoiceUpdated and payload attributes, which alone determine the parent and the
attributes of the payload. Where they differ from an announced proposal, the payload
attributes apply.engine_forkchoiceUpdated MAY call this
method: each call replaces the whole set, so calls from several CLs would overwrite each other.engine_exchangeCapabilities
exchange, at least once per epoch, and when its proposals change: when the dependent root of
the current or next epoch changes, when validators are added or removed, and when a fee
recipient or target gas limit changes. It MAY call it at any time.Support for the method is optional on both sides, and not tied to a fork.
engine_exchangeCapabilities. It validates every call (item 2), and uses as many of the
proposals as it chooses, or none (item 3).engine_forkchoiceUpdated and the payload attributes, as before. A failed call
MUST NOT affect the CL's other use of the Engine API.How this differs from engine_preparePayload. The Engine API's first drafts had a method,
engine_preparePayload. It took the parent block's hash with the timestamp, random and fee
recipient, started building a payload on that parent and returned the build's identifier. In 2021
it was folded into engine_forkchoiceUpdated as optional payload attributes, to avoid "potential
synchronicity issues that arise when the methods are decoupled" (ethereum/execution-apis#83): a
build started on one parent by a separate call could disagree with a fork choice update naming
another head. That reasoning holds for a build command bound to a parent, and folding it into
fork choice was the right fix. It does not apply to a notice. The race came from naming a parent
and starting a build, not from telling the EL that a block will be needed, and no Engine API
method has ever told it that before the parent is known. This method is such a notice. It carries
only values that are fixed before the parent exists and it never starts a build: what a block
builds on, and with which attributes, is still decided only by engine_forkchoiceUpdated, so there
is nothing in this call for fork choice to disagree with. An EL can also safely disregard it, and
start building, and preparing to build, on engine_forkchoiceUpdated as it does today.
Why this interface. Preparing a block needs two kinds of knowledge, on the two sides of the Engine API. The CL knows the schedule: which of its validators propose, at which slots, with which fee recipient and target gas limit. The EL knows how to prepare: its state layout and caches, its disk, its mempool and blob pool, its maintenance jobs and its network, and how long each of these takes on its hardware. The method carries what the EL cannot know, the schedule, and nothing about how to use it.
engine_forkchoiceUpdated decides. Keeping the
two in separate methods is what lets the schedule be sent before the parent exists.Why optional. Nothing depends on the method for correctness. Every block is still built from
engine_forkchoiceUpdated and its payload attributes, so clients that do not implement the method
build exactly as today, and clients that do can ship it without waiting for a fork. Whether
preparing pays, and how much, depends on each EL's design and hardware, so each EL decides whether
to use the proposals.
Precedents. The Engine API already passes the EL information from the CL that changes nothing
in consensus, only how the EL runs: since Amsterdam, engine_forkchoiceUpdatedV4 carries the CL's
custody columns (custodyColumns) for the sparse blobpool. Methods that are optional and not tied
to a fork exist too: engine_getBlobsV1 was introduced after Cancun and defined as
backwards-compatible with it.
Why not a longer payload preparation lookahead. A CL can send payload attributes earlier than its default, but they name a head: sent before the parent is known, they start a build on a block that will not be the parent. The preparation in the motivation needs no parent: it should not have to wait for one, nor pay for builds on the wrong one.
Why not let the EL look up the duties. An EL could poll a beacon node for proposer duties, but it would have to know which validators it serves, which only the CL and its validator client know, and follow the duties as they change, when an epoch's dependent root changes and as validators are added or removed, which the CL already does.
A full set per call. Sending the complete set every time, instead of additions and removals, makes the call idempotent: the CL corrects the EL after reorgs, restarts or failed calls by calling again, and an EL that restarted gets the set back at the next call.
What is sent. The timestamp, fee recipient and target gas limit are the payload attributes the
CL knows in advance. Pre-execution needs the timestamp and the gas limit, and the gas limit also
bounds how much is worth preparing. Most transactions do not depend on the fee recipient: an EL can
pre-execute them with a placeholder and move the fee to the real recipient when it reuses the
result, as some execution clients already do for pending transactions. Transactions that read
COINBASE or touch the fee recipient's account need the real one. The values are those the CL would
use if the slot were next; the payload attributes, sent later, are authoritative. The timestamp is
sent rather than derived from the slot because the EL does not know the beacon chain's genesis time
and slot length, and EIP-8198 would make the slot length a setting that can change at a fork. The
parent block, prevRandao and the parent beacon block root are not known in advance; preparation
that depends on them has to wait for the build. The base fee follows from the parent. The slot
identifies the entry for logging and replacement, and matches the slot number of the payload
attributes.
The bound. EIP-7917 fixes the proposer lookahead for the current and next epoch, so a CL knows at most 64 proposals at a time, however many validators it runs. The bound is 256 so that a longer lookahead does not need a new version of the method; it only limits the size of a request.
No response data. The EL has nothing the CL needs back.
The method is new, optional on both sides and not tied to a fork (see Optional support): clients that do not implement it are unaffected. No other method changes.
The test vectors pair each request with the outcome an EL must return for it, whatever calls came before. Only error codes are checked; messages differ between clients.
null): 0, 1 and 256 proposals; entries already in the past; timestamps whose spacing
changes, as across a fork that shortens the slot; the largest 64-bit values; upper-case hex
digits.-38004: 257 proposals.-32602: a missing or null field; a QUANTITY that is a JSON number, lacks the 0x prefix,
is empty, has a leading zero or exceeds 64 bits; a fee recipient that is not 20 bytes of hex; a
slot or timestamp that repeats or decreases; proposals that is null or an object, not
an array; no parameter. The call fails as a whole, also when only one of its entries is
malformed.For example, this call announces two proposals, one in the current epoch and one in the next, by validators with different fee recipients and target gas limits:
It returns {"jsonrpc": "2.0", "id": 1, "result": null}. With the second slot set to the
first's, "0xecd10d", it returns an error with code -32602.
Further cases involve the EL's state or other methods:
engine_forkchoiceUpdated with payload attributes starts a build as usual.Copyright and related rights waived via CC0.