Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Reliability & Consistency18 min read

Ethereum Beacon Light Client Finality and Optimistic Updates over the Beacon API

A practical guide to consuming Ethereum consensus-layer light-client updates over the Beacon REST API, distinguishing optimistic head updates from finality checkpoints.

TL;DR

Ethereum's post-Merge architecture splits execution (EL) and consensus (CL) layers. Light clients verify CL headers via sync-committee signatures and then trust the EL block hash they commit to. The Beacon REST API exposes light-client endpoints: bootstrap seeds the sync committee, optimistic_update advances the head with a signature but is not final, finality_update carries a justification/finalization pair, and finalized_root allows re-bootstrapping. This article explains each endpoint, how to verify updates against the fork digest, and provides runnable Node.js examples. It also covers persistence, re-bootstrapping across sync-committee period rotation, a results table for verifying updates on your own endpoint, and honest limitations of light-client trust.

The Two-Layer Split After the Merge

Since the Merge, Ethereum operates as two distinct layers: an execution layer (EL) that processes transactions and maintains state, and a consensus layer (CL) that orders blocks and provides finality. The EL exposes JSON-RPC methods under the eth_ namespace, while the CL exposes a REST API standardized by the Ethereum Beacon API specification. A light client must verify CL headers and then trust the EL block hash they commit to.

The mechanism that links the two layers is the execution payload header embedded inside each beacon block. When a CL header is verified, the client reads the body.execution_payload_header.block_hash field, which is the commitment the consensus layer makes to a specific execution block. Because that hash is covered by the sync-committee signature over the beacon block root, tampering with it would invalidate the signature, so the EL hash inherits the CL header's trust-minimized guarantee.

This split means that a light client cannot directly verify execution-layer state without additional proofs. Instead, it verifies the CL header, extracts the EL block hash, and then uses that hash to query an EL node for block data or state proofs. For a deeper look at execution-layer proofs, see EVM eth_getProof state and storage proofs.

  • EL: JSON-RPC eth_ namespace, handles transactions, state, and receipts.
  • CL: Beacon REST API, handles consensus, sync committees, and finality.
  • Light client: verifies CL headers via sync-committee signatures, then trusts the EL block hash.

Beacon API Light-Client Endpoints and Their Roles

The Beacon API defines four light-client endpoints: bootstrap, optimistic_update, finality_update, and finalized_root. Each serves a specific purpose in the light-client sync protocol. The bootstrap endpoint (GET /eth/v1/beacon/light_client/bootstrap/{block_root}) seeds the sync committee and current header from a known finalized root. The optimistic_update endpoint (GET /eth/v1/beacon/light_client/optimistic_update) advances the head using a sync-committee signature, but the head it names is not final and can be reorged.

Each response carries a data object with a header (or attested_header/finalized_header) plus a sync_aggregate containing the aggregated BLS signature and the participation bitfield. The bootstrap response additionally includes the current_sync_committee and current_sync_committee_branch, which is a Merkle proof tying the committee to the state root of the finalized header. The client stores that branch so it can later prove the committee membership without re-fetching the full state.

The finality_update endpoint (GET /eth/v1/beacon/light_client/finality_update) carries a justification/finalization pair signed by a sync committee, raising the finalized checkpoint. The finalized_root endpoint (GET /eth/v1/beacon/light_client/finalized_root) provides a finalized root that can be used to re-bootstrap. These endpoints are documented in the Ethereum Beacon API specification.

  • Bootstrap: seeds sync committee and current header from a finalized root.
  • Optimistic update: advances head with sync-committee signature, not final.
  • Finality update: carries justification/finalization pair, raises finalized checkpoint.
  • Finalized root: provides a finalized root for re-bootstrapping.

Why Optimistic Updates Are Trust-Minimized but Not Final

An optimistic update is trust-minimized against the sync committee: it includes a signature from a supermajority of the committee, which is randomly selected and rotates periodically. However, the head it names is not final and can be reorged. Applications that key off the head immediately may be vulnerable to short reorgs. For reorg detection strategies, see Ethereum block reorg detection and RPC depth.

The reason the head is not final is that finality in Ethereum requires two consecutive justified epochs, which takes at least two epochs (roughly 12.8 minutes under normal conditions) to complete. An optimistic update only proves that a supermajority of the current sync committee attested to a header; it does not prove that the corresponding epoch was justified or finalized. A reorg deeper than the optimistic head can therefore still occur before finality catches up.

In contrast, a finality update raises the finalized checkpoint, which is durable and extremely unlikely to be reverted. This is the anchor an application should use for irreversible actions. The distinction is critical: optimistic updates provide low-latency head information, while finality updates provide security guarantees.

  • Optimistic update: signed by sync committee, but head can be reorged.
  • Finality update: raises finalized checkpoint, durable anchor.
  • Use optimistic updates for low-latency reads; use finality updates for irreversible actions.

Verifying Sync-Committee Signatures and Fork Digest

Sync-committee signatures are verified against the aggregate pubkey and the fork digest. The fork digest is derived from the fork version and genesis validators root, ensuring that updates from the wrong fork are rejected. The Ethereum consensus specs define the exact verification procedure. A light client must compute the signing root, aggregate the public keys, and verify the BLS signature.

Concretely, the client reconstructs the SyncAggregate by taking the sync_committee_bits bitfield and selecting the pubkeys of the committee members whose bits are set, then aggregating them. It computes the signing root as the hash tree root of a SigningData container holding the object root and the domain, where the domain is built from the fork digest and the DOMAIN_SYNC_COMMITTEE type. The BLS FastAggregateVerify is then run over the signing root, the aggregated pubkey, and the sync_committee_signature.

The fork version is included in the header and must match the client's expected fork version. If a client receives an update with a mismatched fork version, it should reject it. This prevents cross-fork replay attacks and ensures the client follows the correct chain.

  • Verify BLS signature against aggregate pubkey.
  • Check fork digest matches expected fork version.
  • Reject updates with mismatched fork version.

Handoff to Execution Layer: EL Block Hash and eth_getBlockByHash

Once a CL header is verified, the light client extracts the EL block hash from the header. This hash is the handoff point to execution-layer reads. The client can then call eth_getBlockByHash on an EL node to retrieve the full block, or eth_getProof to verify specific state. For more on EL queries, see the Ethereum RPC node guide (RPC Assistant).

The handoff is only as strong as the EL node you query. If the EL node is honest, it returns the block whose hash matches the verified commitment; if it is not, it can return a different block or stale state, and the light client has no way to detect that from the CL header alone. This is why the EL hash is a commitment, not a proof: it binds the CL header to an EL block, but it does not prove anything about the contents of that block.

It is important to note that the light client does not verify the execution-layer state itself; it trusts the EL node to return correct data for the given block hash. For trustless state proofs, additional mechanisms like eth_getProof are needed, which are covered in EVM eth_getProof state and storage proofs.

  • Extract EL block hash from verified CL header.
  • Use eth_getBlockByHash to fetch full block from EL node.
  • Light client trusts EL node for state; use eth_getProof for trustless proofs.

Persistence and Re-Bootstrapping Across Sync-Committee Periods

Sync committees rotate roughly every 256 epochs (approximately 27 hours), a period documented in the consensus specs and subject to change with network upgrades. A light client must persist the bootstrap data and the current period. When the period changes, the client must re-bootstrap using a finalized root from the new period. The finalized_root endpoint provides such a root.

The rotation is not a hard cutover at a single slot; the committee for period N is valid for a range of slots, and the client should track the period field returned alongside the bootstrap and update responses. When the current slot crosses into a new period, the old committee's signatures no longer verify against the new committee's aggregate pubkey, so the client must fetch a fresh bootstrap whose current_sync_committee belongs to the new period. Persisting the bootstrap means storing the header, the committee, the committee branch, and the period index so the client can resume without re-fetching from scratch after a restart.

Failing to re-bootstrap will result in invalid signatures, as the sync committee changes. Clients should monitor the period and trigger re-bootstrapping when necessary. This is a critical operational detail for long-running light clients.

  • Sync committees rotate every ~256 epochs (~27 hours).
  • Persist bootstrap data and current period.
  • Re-bootstrap when period changes using finalized_root.

Runnable Node.js Example: Bootstrap, Optimistic Update, Finality Update

The following Node.js script uses fetch to interact with a Beacon API endpoint. It bootstraps from a known finalized root, fetches the optimistic update, fetches the finality update, and prints the fork version. Replace BEACON_API_URL with your provider's endpoint. Note that the light-client server flag may be disabled on some consensus nodes, so the endpoints can return 404.

This example assumes you have a trusted finalized root. In practice, you would obtain this from a trusted source or from a previous finality update. The script prints the slot and head root from the optimistic update, the finalized slot and root from the finality update, and the fork version from the header.

const BEACON_API_URL = 'https://your-beacon-node.example.com';
const FINALIZED_ROOT = '0x...'; // Replace with a known finalized root

async function fetchLightClientData() {
  // 1. Bootstrap
  const bootstrapRes = await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/bootstrap/${FINALIZED_ROOT}`);
  const bootstrap = await bootstrapRes.json();
  console.log('Bootstrap slot:', bootstrap.data.header.beacon.slot);
  console.log('Fork version:', bootstrap.data.header.beacon.fork_version);

  // 2. Optimistic update
  const optimisticRes = await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/optimistic_update`);
  const optimistic = await optimisticRes.json();
  console.log('Optimistic slot:', optimistic.data.attested_header.beacon.slot);
  console.log('Optimistic head root:', optimistic.data.attested_header.beacon.body_root);

  // 3. Finality update
  const finalityRes = await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/finality_update`);
  const finality = await finalityRes.json();
  console.log('Finalized slot:', finality.data.finalized_header.beacon.slot);
  console.log('Finalized root:', finality.data.finalized_header.beacon.body_root);

  // 4. Fork version from header
  console.log('Fork version from finality header:', finality.data.finalized_header.beacon.fork_version);
}

fetchLightClientData().catch(console.error);

Persisting the Bootstrap and Re-Bootstrapping on Period Rotation

A long-running light client cannot hold the bootstrap in memory alone; it must write the bootstrap header, the current sync committee, the committee branch, and the period index to durable storage so that a restart does not force a full re-sync. A practical layout is a small JSON or key-value record keyed by the finalized root used at bootstrap, with the period index stored alongside so the client can compare it against the period implied by the latest update's slot.

The re-bootstrap trigger is a period mismatch: when the slot of an incoming update falls into a period different from the stored one, the client fetches a new bootstrap from the finalized_root endpoint and replaces the stored committee and branch. Because the finalized root is itself a finalized checkpoint, the new bootstrap is anchored to the same security assumption as the original, and the client can discard the old committee once the new one is verified.

Operationally, this means the client should treat the bootstrap as a cache with an explicit invalidation rule rather than as a one-time initialization. Logging the period index and the finalized root at each re-bootstrap makes it possible to audit whether the client stayed on the correct committee across rotations, and it surfaces cases where a provider returned a stale finalized root.

  • Persist header, sync committee, committee branch, and period index.
  • Trigger re-bootstrap when the update's slot crosses into a new period.
  • Fetch the new bootstrap from the finalized_root endpoint and replace the stored committee.
  • Log period index and finalized root at each re-bootstrap for auditing.
const BEACON_API_URL = 'https://your-beacon-node.example.com';

// Minimal in-memory store; swap for a file or database in production.
let store = { period: null, finalizedRoot: null, committee: null };

async function bootstrapFrom(finalizedRoot) {
  const res = await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/bootstrap/${finalizedRoot}`);
  const { data } = await res.json();
  store = {
    period: data.current_sync_committee_branch ? data.header.beacon.slot : null,
    finalizedRoot,
    committee: data.current_sync_committee,
  };
  return data;
}

async function maybeRebootstrap(updateSlot, currentPeriod) {
  if (store.period !== currentPeriod) {
    const rootRes = await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/finalized_root`);
    const { data } = await rootRes.json();
    await bootstrapFrom(data.root);
    console.log('Re-bootstrapped for period', currentPeriod, 'at slot', updateSlot);
  }
}

// Example: call maybeRebootstrap(updateSlot, periodFromSlot(updateSlot)) on each update.

Reproducible Results Table for Your Endpoint

To measure your own endpoint's behavior, fill in the following table with values from your runs. This helps compare providers and detect anomalies. Run the script multiple times to observe slot advances and finality lag.

The table should include: bootstrap slot, optimistic slot advance (difference between optimistic and bootstrap slots), finality slot, finalized root, fork version, and sync-committee period. Record these values over time to understand your endpoint's performance.

  • Bootstrap slot: the slot of the header returned by bootstrap.
  • Optimistic slot advance: optimistic slot minus bootstrap slot.
  • Finality slot: the slot of the finalized header.
  • Finalized root: the body root of the finalized header.
  • Fork version: from the header (e.g., 0x03000000).
  • Sync-committee period: current period index.

Results table: verifying light-client updates on your own endpoint

The table below is a template for recording the outcome of each verification step against your own Beacon API endpoint. Because light-client behavior depends on the provider, the network, and the moment you query, the values are expected to vary between runs; the point is to capture them consistently so you can compare endpoints and spot regressions. Fill one row per run and keep the raw JSON responses alongside the row for later inspection.

When you verify an update, the checks that matter are: the BLS signature verifies against the aggregate pubkey derived from the stored committee, the fork digest matches your expected fork version, the finalized root in the finality update matches the root you would use to re-bootstrap, and the period implied by the update's slot matches the stored period. Recording each check as pass or fail, rather than only the final verdict, makes it obvious which step failed when something goes wrong.

Use the table as a living artifact: re-run it after provider changes, after network upgrades that alter the fork version, and after any change to your own persistence logic. Because the sync committee rotates, a table that was accurate for one period may not be accurate for the next, so include the period index in every row.

  • Run: timestamp or run identifier.
  • Bootstrap slot and finalized root used.
  • Optimistic slot and slot advance over bootstrap.
  • Finality slot and finalized root returned.
  • Fork version observed in the header.
  • Sync-committee period index.
  • Signature verification result (pass/fail).
  • Fork digest match result (pass/fail).
// Sketch of a verification harness that prints one table row per run.
async function verifyOnce() {
  const bootstrap = await (await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/bootstrap/${FINALIZED_ROOT}`)).json();
  const optimistic = await (await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/optimistic_update`)).json();
  const finality = await (await fetch(`${BEACON_API_URL}/eth/v1/beacon/light_client/finality_update`)).json();

  const row = {
    bootstrapSlot: bootstrap.data.header.beacon.slot,
    optimisticSlot: optimistic.data.attested_header.beacon.slot,
    finalitySlot: finality.data.finalized_header.beacon.slot,
    finalizedRoot: finality.data.finalized_header.beacon.body_root,
    forkVersion: finality.data.finalized_header.beacon.fork_version,
  };
  console.log(JSON.stringify(row));
}

verifyOnce().catch(console.error);

Troubleshooting Common Beacon API Light-Client Issues

If the bootstrap endpoint returns 404, the light-client server flag may be disabled on your consensus node. Check your provider's documentation. Some providers do not enable light-client endpoints by default. For a list of Ethereum nodes, see Ethereum RPC node guide (RPC Assistant).

A 404 can also mean the finalized root you passed is not known to that node, for example if the node is still syncing or if the root belongs to a different network. Distinguishing the two cases is straightforward: query a known-good endpoint such as the node's version or health route, and confirm the network before assuming the light-client flag is off. If the node is healthy and on the right network but still 404s on bootstrap, the flag is the likely cause.

If signature verification fails, ensure the fork version matches and that you are using the correct aggregate pubkey from the bootstrap. If the period has changed, re-bootstrap. If you encounter rate limits, consider using a dedicated provider like OnFinality's API service or check RPC pricing for higher limits.

  • 404 on bootstrap: light-client server flag may be disabled.
  • Signature verification failure: check fork version and aggregate pubkey.
  • Period change: re-bootstrap with finalized_root.
  • Rate limits: use a provider with higher limits.

Limitations and Tradeoffs of Light-Client Trust

The Beacon API is a CL-client REST interface whose paths are standardized by the consensus specs, but availability and rate limits vary by provider. The light-client server flag may be disabled on some consensus nodes, causing 404s. Sync-committee verification in a browser still requires the aggregate pubkey from the same trusted bootstrap, so the trust model is not fully trustless.

The trust model is best described as a chain of assumptions: you trust the bootstrap source to give you a correct finalized root, you trust the sync committee to be honest and sufficiently decentralized, and you trust the EL node for execution-layer data. Each link is weaker than a full consensus client that validates every block from genesis, but the combination is far cheaper than running a full node and is adequate for many read-oriented applications.

A light client provides trust-minimized finality, not a full trustless state proof of arbitrary execution-layer storage. For trustless state proofs, you need additional mechanisms like eth_getProof. Also, the light client trusts the EL node for execution-layer data, so the overall trust model is a combination of CL verification and EL trust. For more on EL trust, see Ethereum archive node and historical RPC.

  • Beacon API availability and rate limits vary by provider.
  • Light-client server flag may be disabled, causing 404s.
  • Browser verification requires aggregate pubkey from trusted bootstrap.
  • Light client gives trust-minimized finality, not full trustless state proofs.

Next Steps: Integrating Light-Client Updates into Your Application

To integrate light-client updates, start by selecting a provider that supports the Beacon API light-client endpoints. Use the bootstrap endpoint to initialize, then poll optimistic_update for head information and finality_update for finality. Persist the bootstrap and period, and re-bootstrap when the period changes. For production, consider using a reliable provider like OnFinality's API service and review RPC pricing for your needs.

A reasonable integration pattern is to run a background loop that fetches the optimistic update on a short interval for head tracking and the finality update on a longer interval for the durable anchor, writing both into your persistence layer. Gate irreversible actions on the finalized checkpoint only, and treat the optimistic head as advisory. When the period index changes, pause the loop, re-bootstrap, and resume once the new committee verifies.

For further reading, explore the OnFinality Learn hub for more guides on Ethereum reliability and consistency. Also, check the Ethereum network page for network-specific details. As you build, monitor your node's sync status with Ethereum eth_syncing and node sync status to ensure your EL node is healthy.

  • Choose a provider with light-client endpoint support.
  • Bootstrap, then poll optimistic and finality updates.
  • Persist bootstrap and period; re-bootstrap on period change.
  • Monitor EL node sync status for health.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started