Skip to content

Latest commit

 

History

History
955 lines (822 loc) · 63 KB

rpc.md

File metadata and controls

955 lines (822 loc) · 63 KB

HTTP API

ethlambda exposes HTTP over two independent Axum servers on separate ports, so the API and the metrics/debug surface can have different network policies:

  • API server — consensus data (blocks, states, checkpoints, fork choice) and admin controls.
  • Metrics & debug server — Prometheus metrics and heap-profiling endpoints. No store access.

Which API server a node serves follows from its store's chain tag, not from the sub-command: ethlambda node serves the lean surface under /lean/v0, ethlambda beacon serves the Beacon API under /eth/v1 and /eth/v2. The two are alternatives, never merged, because the lean handlers read state variants and metadata keys a beacon directory does not carry. Roots are serialized as 0x-prefixed hex strings on both.

Servers & Ports

Flag Default Description
--http-address 127.0.0.1 Bind address shared by both servers
--api-port 5052 API server port
--metrics-port 5054 Metrics & debug server port

If --api-port and --metrics-port are equal, all routers are merged onto a single port.

API Server (:5052)

Method Path Response Description
GET /lean/v0/health JSON Liveness check
GET /lean/v0/config/spec JSON Protocol constants the node runs with
GET /lean/v0/genesis JSON Genesis time and validator count
GET /lean/v0/states/finalized SSZ Latest finalized State
GET /lean/v0/blocks/finalized SSZ, or JSON on request Latest finalized SignedBlock
GET /lean/v0/checkpoints/justified JSON Latest justified Checkpoint
GET /lean/v0/events SSE Live stream of chain events
GET /lean/v0/blocks/{block_id} JSON Block by root or slot
GET /lean/v0/blocks/{block_id}/header JSON Block header by root or slot
GET /lean/v0/fork_choice JSON Fork-choice tree with per-block weights
GET /lean/v0/fork_choice/ui HTML Interactive D3.js visualization
GET /lean/v0/node/identity JSON Client version and libp2p peer ID
GET /lean/v0/node/syncing JSON Sync status relative to the wall clock
GET /lean/v0/admin/aggregator JSON Current aggregator role
POST /lean/v0/admin/aggregator JSON Toggle aggregator role at runtime

GET /lean/v0/health

The handler emits a fixed, compact body (no whitespace):

{"status":"healthy","service":"lean-rpc-api"}

GET /lean/v0/config/spec

Protocol parameters the node is running on. Keys mirror the leanSpec constant names. MILLISECONDS_PER_SLOT and MILLISECONDS_PER_INTERVAL reflect the network's config file rather than a compile-time constant, so a node on an 8-second network reports 8000 and 1600 here:

{
  "MILLISECONDS_PER_SLOT": 4000,
  "INTERVALS_PER_SLOT": 5,
  "MILLISECONDS_PER_INTERVAL": 800,
  "HISTORICAL_ROOTS_LIMIT": 262144,
  "FORK_DIGEST": "12345678"
}

FORK_DIGEST is the 4-byte hex string (no 0x prefix) embedded in gossipsub topic names.

GET /lean/v0/genesis

{ "genesis_time": 1770407233, "validator_count": 16 }

validator_count is read from the head state's validator registry. Lean validators are fixed at genesis (no churn), so it always equals the size of the genesis registry.

GET /lean/v0/states/finalized

SSZ-encoded State at the latest finalized checkpoint (Content-Type: application/octet-stream). The served state has its latest_block_header.state_root zeroed to match the canonical post-state representation the state transition produces, so checkpoint-sync peers reconstruct an identical state root. See Checkpoint Sync.

GET /lean/v0/blocks/finalized

SSZ-encoded SignedBlock at the latest finalized checkpoint. The genesis/anchor block has no stored signature, so a placeholder blank proof is synthesized and the endpoint still returns 200. Returns 404 only in the rare case where a non-genesis finalized block's signature has been pruned below the finalized boundary and can no longer be served.

GET /lean/v0/checkpoints/justified

{ "slot": 128, "root": "0x1a2b…" }

GET /lean/v0/events

Server-Sent Events stream (Content-Type: text/event-stream) of live chain events published by the blockchain actor. Eight event types:

Payload fields mirror the Ethereum beacon-API eventstream where an analog exists: block is the block root, state the state root, and slot stands in for the beacon epoch. justified_checkpoint and aggregate are ethlambda extensions with no beacon topic.

Event Payload Emitted when
head { "slot": 128, "block": "0x…", "state": "0x…" } Fork choice selects a new head within HEAD_EVENT_RECENCY_SLOTS (32 slots) of the wall clock; no head events fire during catch-up
block { "slot": 128, "block": "0x…" } A block is imported into the store
justified_checkpoint { "slot": 120, "block": "0x…", "state": "0x…" } The justified checkpoint advances
finalized_checkpoint { "slot": 96, "block": "0x…", "state": "0x…" } The finalized checkpoint advances
block_gossip { "slot": 128, "block": "0x…" } A block is seen on the network, before import
attestation { "validator_id": 4, "data": { "slot": 128, "head": {…}, "target": {…}, "source": {…} } } A single validator vote passes gossip validation (signature omitted)
aggregate { "participants": [0, 3, 4], "data": { "slot": 128, "head": {…}, "target": {…}, "source": {…} } } A committee-signature aggregate is produced locally or accepted from gossip (proof omitted)
chain_reorg { "slot": 130, "depth": 1, "old_head_block": "0x…", "new_head_block": "0x…", "old_head_state": "0x…", "new_head_state": "0x…" } The new head does not descend from the previous one; sent ahead of that head, and never gated on recency. slot is the new head's; depth is how many slots the old head sat above the two heads' common ancestor (the Beacon API's definition, in slots; the lean_fork_choice_reorg_depth metric counts blocks walked instead)

The topic name travels only on the SSE event: line; the data: line carries the flat JSON payload. Example frame:

event: head
data: {"slot":128,"block":"0x1a2b…","state":"0x3c4d…"}

Filtering with ?topics=

A required comma-separated list of event names selects which events to stream:

curl -N 'http://127.0.0.1:5052/lean/v0/events?topics=head,finalized_checkpoint'

Valid values are exactly the event names above: head, block, justified_checkpoint, finalized_checkpoint, block_gossip, attestation, aggregate, chain_reorg. A Beacon API topic this surface does not serve (data_column_sidecar, say) is refused as unknown. As in the Beacon API eventstream endpoint, topics is mandatory: there is no "subscribe to everything" default; list the topics you want.

Status Condition
200 Stream opened for the listed topics
400 topics is missing or empty, or any listed name is not a known topic (body names the offending value)

Events are fanned out over a single bounded broadcast channel shared by all topics. A client that reads too slowly skips past the events it missed: they are dropped for that subscriber rather than back-pressured onto the actor, so treat the stream as best-effort and re-sync via the blocks endpoints after a gap. A client that falls behind receives an SSE comment line : error - dropped N messages marking the gap (wire-compatible with Lighthouse) before the stream continues; re-sync via the blocks endpoints rather than trusting the skipped range. Keep-alive comments are sent periodically to hold idle connections open.

Because the ring buffer is shared, the high-rate attestation events (roughly one per validator per slot) dominate its occupancy: a subscriber's tolerable stall is capacity / total_event_rate, not per-topic, so filtering with ?topics= narrows what you receive but does not widen the lag window against an attestation flood. Subscribers that only need low-rate topics (head, finalized_checkpoint, …) are still evicted at the aggregate rate. If real usage shows this biting, the fix is a per-topic channel split behind the event bus (the subscribe(TopicSet) API is unaffected).

GET /lean/v0/blocks/{block_id} and /header

block_id is either:

  • a 0x-prefixed 32-byte hex root, or
  • a decimal slot.

Slot lookups resolve through the head state's historical_block_hashes, so only canonical blocks are reachable by slot; blocks on side forks must be addressed by their root. The /header variant returns just the BlockHeader.

Status Condition
200 Block (or header) found, returned as JSON
400 block_id is neither a valid 0x root nor a decimal slot
404 No block at that root, or the slot is empty / out of range

Error bodies are JSON: { "error": "invalid block_id" } / { "error": "block not found" }.

GET /lean/v0/fork_choice

The fork-choice tree from the finalized root, with LMD-GHOST weights computed over the live chain and currently known attestations.

{
  "nodes": [
    { "root": "0x…", "slot": 128, "parent_root": "0x…", "proposer_index": 3, "weight": 12 }
  ],
  "head": "0x…",
  "justified": { "slot": 128, "root": "0x…" },
  "finalized": { "slot": 96,  "root": "0x…" },
  "safe_target": "0x…",
  "validator_count": 16
}

/lean/v0/fork_choice/ui serves an interactive D3.js page rendering this data. See Fork Choice Visualization.

GET /lean/v0/node/identity

{
  "version": "ethlambda/v0.1.0-main-892ad575/x86_64-unknown-linux-gnu/rustc-v1.97.1",
  "peer_id": "16Uiu2HAm7v1x…"
}

version is the full client version string, identical to what ethlambda --version prints: crate semver, git branch and short SHA, target triple, and rustc version. Baked in at compile time from CARGO_PKG_VERSION plus the vergen-git2 build metadata.

peer_id is the node's libp2p peer ID (base58), derived from the node key and fixed for the lifetime of the process; it matches the identity the node presents to peers on the wire.

GET /lean/v0/node/syncing

{ "is_syncing": false, "head_slot": 1024, "sync_distance": 1, "finalized_slot": 986 }

is_syncing is the node's own stateful sync decision: head-vs-wall-clock lag with hysteresis and a network-stall override, updated each tick. It is the same signal that gates validator duties and drives the lean_node_sync_status metric, so the endpoint, the gate, and the metric always agree.

sync_distance is the raw number of slots between the node's current head and the current wall-clock slot, computed per request. Because is_syncing carries hysteresis and stall handling and is not recomputed from sync_distance, the two can point different ways near the threshold or during a network-wide stall.

GET / POST /lean/v0/admin/aggregator

Toggle the aggregator role at runtime without restarting the node (hot-standby model, ported from leanSpec PR #636).

# Read current role
curl http://127.0.0.1:5052/lean/v0/admin/aggregator
# → {"is_aggregator": true}

# Toggle role; body must be a JSON boolean
curl -X POST http://127.0.0.1:5052/lean/v0/admin/aggregator \
  -H 'content-type: application/json' -d '{"enabled": false}'
# → {"is_aggregator": false, "previous": true}
Status Condition
200 Role read / set
400 Missing/malformed body, missing enabled, or enabled not a JSON boolean (integers 0/1 and strings are rejected)
503 Aggregator controller not wired (does not occur in normal main.rs boot)

Note: Runtime toggles do not resubscribe gossip subnets, which are frozen at startup. A standby aggregator should boot with --is-aggregator=true (so subscriptions are in place), then use this endpoint to rotate duties. See the CLAUDE.md "Runtime Aggregator Toggle" notes for the operational model.

Beacon API Server (:5052, on ethlambda beacon)

The subset of the Ethereum Beacon API that this node can answer from its own store. It replaces the /lean/v0 surface rather than sitting beside it; a /lean/v0 path on a beacon node is a 404.

Method Path Response Description
GET /eth/v2/beacon/blocks/{block_id} JSON or SSZ SignedBeaconBlock at block_id
GET /eth/v1/beacon/blocks/{block_id}/root JSON That block's root
GET /eth/v1/beacon/execution_payload_envelopes/{block_id} JSON or SSZ Gloas SignedExecutionPayloadEnvelope of that block (see below)
GET /eth/v1/beacon/headers/{block_id} JSON SignedBeaconBlockHeader, plus canonical
GET /eth/v2/debug/beacon/states/{state_id} JSON or SSZ BeaconState at state_id
GET /eth/v1/beacon/states/{state_id}/finality_checkpoints JSON That state's three checkpoints
GET /eth/v1/beacon/states/{state_id}/fork JSON That state's Fork: previous and current version, and the epoch it changed
GET /eth/v1/beacon/genesis JSON Genesis time, validators root, fork version
GET /eth/v1/config/spec JSON The store's Config, plus PRESET_BASE, CONFIG_NAME, the preset and the constants (see below)
GET /eth/v1/config/deposit_contract JSON The Config's deposit chain id and contract address
GET /eth/v1/config/fork_schedule JSON Every scheduled fork as {previous_version, current_version, epoch}
GET /eth/v1/node/syncing JSON Head slot, sync distance, whether the head is optimistic
GET /eth/v1/node/health (status only) 200 caught up, 206 syncing or on an optimistic head
GET /eth/v1/node/version JSON Client version string
GET /eth/v1/node/identity JSON Peer ID, ENR and listen multiaddrs (see below)
GET, POST /eth/v1/beacon/states/{state_id}/validators JSON Registry entries by index or pubkey, with status
GET /eth/v1/beacon/states/{state_id}/validators/{validator_id} JSON One registry entry, by index or pubkey; 404 if there is none
GET /eth/v1/validator/duties/proposer/{epoch} JSON Proposers for any epoch from the head's up to one past the wall clock's
GET /eth/v2/validator/duties/proposer/{epoch} JSON The same, with v2's dependent_root (what Lighthouse asks for)
POST /eth/v1/validator/duties/attester/{epoch} JSON Committee assignments for the given indices
POST /eth/v1/validator/duties/sync/{epoch} JSON Sync committee seats for the given indices, up to the wall clock's current period plus one
POST /eth/v1/validator/liveness/{epoch} JSON Whether this node saw each validator act in the epoch (doppelganger protection)
POST /eth/v1/validator/duties/ptc/{epoch} JSON Payload timeliness committee seats for the given indices (gloas)
GET /eth/v1/validator/attestation_data JSON What to attest to at slot
GET /eth/v1/validator/payload_attestation_data JSON or SSZ What a committee member signs for slot (gloas)
POST /eth/v2/beacon/pool/attestations (status only) Validate and gossip SingleAttestations (JSON or SSZ body)
GET, POST /eth/v1/beacon/pool/proposer_slashings JSON The operation pool's ProposerSlashings; validate, pool and gossip one
GET, POST /eth/v2/beacon/pool/attester_slashings JSON The pool's AttesterSlashings (GET carries Eth-Consensus-Version, the fork of the wall clock's current epoch); validate, pool and gossip one
GET, POST /eth/v1/beacon/pool/voluntary_exits JSON The pool's SignedVoluntaryExits; validate, pool and gossip one
GET, POST /eth/v1/beacon/pool/bls_to_execution_changes JSON The pool's SignedBLSToExecutionChanges; POST takes an array
POST /eth/v1/beacon/pool/payload_attestations (status only) Validate, pool and gossip PayloadAttestationMessages (gloas; JSON or SSZ body)
GET /eth/v1/beacon/pool/payload_attestations JSON The pool's votes as aggregated PayloadAttestations (gloas)
POST /eth/v1/validator/beacon_committee_subscriptions (status only) Aggregators' entries join their committee's subnet
GET /eth/v2/validator/aggregate_attestation JSON The pooled votes for a data root and committee, aggregated
POST /eth/v2/validator/aggregate_and_proofs (status only) Validate and gossip SignedAggregateAndProofs (JSON or SSZ body)
POST /eth/v1/beacon/pool/sync_committees (status only) Validate, pool and gossip SyncCommitteeMessages
GET /eth/v1/validator/sync_committee_contribution JSON The pooled messages for (slot, subcommittee_index, beacon_block_root), aggregated
POST /eth/v1/validator/contribution_and_proofs (status only) Validate, pool and gossip SignedContributionAndProofs
POST /eth/v1/validator/sync_committee_subscriptions (status only) Join sync committee subnets until an epoch
GET /eth/v3/validator/blocks/{slot} SSZ or JSON An unsigned fulu block built on the head (produceBlockV3)
POST /eth/v4/validator/blocks/{slot} SSZ or JSON An unsigned gloas block: self-built, with its envelope and blobs when asked, or on a pooled builder bid (produceBlockV4)
GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root} SSZ or JSON The unsigned envelope produceBlockV4 built (gloas)
POST /eth/v2/beacon/blocks (status only) Gossip and import a signed fulu or gloas block (publishBlockV2; JSON or SSZ body)
POST /eth/v1/beacon/execution_payload_envelopes (status only) Gossip a signed envelope and its data columns (gloas; JSON or SSZ body)
POST /eth/v1/beacon/execution_payload_bids (status only) Validate, pool and gossip a builder's SignedExecutionPayloadBid (gloas; JSON or SSZ body)
POST /eth/v1/validator/proposer_preferences (status only) Validate, cache and gossip SignedProposerPreferences (gloas; JSON or SSZ body)
POST /eth/v1/beacon/states/{state_id}/builders JSON The gloas builder registry, filtered by id and status
POST /eth/v1/validator/prepare_beacon_proposer (status only) Record each validator's fee recipient for block production (see below)
GET /eth/v1/events SSE Live stream of chain events (see below)

Validator endpoints

These are what ethlambda validator needs to attest through this node. Every answer is computed from the fork-choice head's post-state, read off the store the chain actor writes, so no request waits on the actor.

  • Duties are bounded by the wall clock, as the Beacon API defines it: any epoch up to one past the current one is served (or one past the head's, if the head is ahead of a lagging clock), so a validator client's next-epoch lookahead at an epoch boundary, while the head is still in the previous epoch, is answered. Proposer duties (v1 and v2) read fulu's proposer_lookahead, which covers the head's epoch and the next; attester duties read the head state as it is for its previous, current and next epoch. A later epoch is computed from a copy of the head advanced with process_slots to the first epoch that can derive it, taken from fork choice's checkpoint_state cache and run on a blocking thread. An epoch before the head's (attester: more than one before) or past the bound is a 400. dependent_root follows each endpoint's own definition: the last block before the epoch for v1 proposer duties, and the last block before the previous epoch for v2 proposer duties and attester duties. v2's is the block fulu's lookahead depends on, so it changes only on reorgs that can change the duties. Proposer duties v2 is a 503 while syncing. Attester duties walk every committee of the epoch, a full shuffle per request on mainnet. Gloas epochs are served like fulu ones: gloas keeps fulu's proposer_lookahead and upgrade_to_gloas carries it over, so a fulu head already answers proposer duties (v1 and v2) for the first gloas epoch, and a gloas head answers from its own lookahead.
  • Sync duties read the head state's current_sync_committee for an epoch in the head's own sync committee period and next_sync_committee for the one after. The upper bound is the wall clock's, as the Beacon API defines it: the period after the clock's current one. When the head lags a period boundary (its block is late or missing), the later period is read from a copy of the head advanced to the first epoch of the period before it, through fork choice's checkpoint-state cache on a blocking thread. A period past that bound or before the head's is a 400 (an earlier one would need a historical state; see docs/spec_deviations.md). A validator is matched by pubkey and gets every seat it holds, since the committee is drawn with replacement; one with no seat is left out. An unknown index is a 400, and the endpoint is a 503 while the node is syncing. What a duty asks for is served by the four sync committee endpoints in Sync committee below.
  • Liveness is this node's own view, which the Beacon API allows. A validator is live in an epoch if the head state credits it for that epoch (a non-zero participation byte, so anything a block already included), or the node observed it act: an accepted gossip aggregate (its aggregator and every attester its signature verified) or subnet attestation, an imported block's proposer, or a submission through pool/attestations or aggregate_and_proofs. The observed half covers what no block has included yet, most of the current epoch; it keeps the newest three epochs recorded, as one bitset each. The window is the store clock's previous, current and next epoch (the next is always false); anything else, and an unknown index, is a 400, and the endpoint is a 503 while the node is syncing.
  • attestation_data follows phase0's validator.md: the head block, the epoch's boundary block as target, and as source the current justified checkpoint of the head state advanced to the slot's epoch (through fork choice's cached checkpoint_state, and only when the head is in an earlier epoch). A slot before the head, or past the wall clock, is a 400. An optimistic head is a 503: the Beacon API requires one whenever the answer's beacon_block_root has not been validated by the execution client, and the head is always that root here. So is every request on a node run without an execution client, whose blocks are never marked optimistic because nothing validates them. committee_index is optional and ignored: from electra on every committee of a slot attests to the same data. data.index is 0 before gloas. At a gloas slot it is the payload-present flag: 0 when the head block is from the requested slot itself (its payload cannot be revealed yet), otherwise 1 exactly when fork choice holds the head block's payload as FULL (Store::head_payload_status, or one fresh get_head_node walk when none is recorded for this head). A pre-gloas head counts as FULL (see Spec Deviations).
  • states/{state_id}/validators/{validator_id} is one entry of the list endpoint, in the same shape and with the same state_id handling, execution_optimistic and finalized. The id is an index or a 0x pubkey; one naming no validator is a 404 (the list form omits it), a malformed one a 400. Lighthouse's validator client resolves each key to its index this way, and stays inactive without it.
  • pool/attestations checks each attestation against the electra beacon_attestation_{subnet_id} gossip conditions it can evaluate (clock window, data.index == 0, target epoch, the voted block known and the target its checkpoint block, committee membership, BLS signature), then gossips it on its subnet through fanout, without subscribing. A rejected attestation comes back in an IndexedErrorMessage with its position; the valid ones in the same batch are still published. There is no seen-attestation cache. Each accepted attestation also goes into the node's attestation pool, since gossip never delivers a node its own messages. Eth-Consensus-Version must name electra, fulu or gloas here and on aggregate_and_proofs, and must match the fork of each item's slot: a gloas header on a pre-gloas slot, or an older one on a gloas slot, fails that item (not the batch) with Eth-Consensus-Version does not match the attestation slot's fork. At a gloas slot data.index may be 0 or 1, checked with gloas's payload-status gossip rule (a vote in the voted block's own slot must be 0; 1 needs the node to have verified the payload, otherwise the item fails). A gloas SingleAttestation is electra's own container.
  • beacon_committee_subscriptions: each aggregator's entry makes the node join its committee's attestation subnet until the end of that slot, so the committee's votes from other validators reach the pool too: every attestation this node relays has passed the beacon_attestation_{subnet_id} checks first, and the ones accepted on a joined subnet are pooled. Joined subnets are left once their slot has passed, and never appear in attnets.
  • aggregate_attestation answers from the pool: every vote held for the data root and committee, as electra's Attestation with the BLS aggregate of their signatures; at a gloas slot the answer is {version: "gloas", data} with gloas's Attestation (the same JSON shape). JSON only. 404 when nothing is held, and 503 when the block the votes name is optimistic, whether or not it is the head, or when no execution client is configured.
  • aggregate_and_proofs checks each aggregate with the same beacon_aggregate_and_proof gossip conditions this node applies to its peers' aggregates (gossip::aggregate), signatures included, against a fresh seen-cache (a node never receives its own messages, so P2P's says nothing about them). What passes is gossiped on the topic, goes into the pool, and is handed to the chain actor, which applies it to fork choice and announces it on /eth/v1/events's attestation. The Eth-Consensus-Version header picks the decoder: gloas takes gloas's SignedAggregateAndProof, the others electra's.
  • Request body encodings. pool/attestations, pool/payload_attestations and aggregate_and_proofs take either JSON (application/json, also the reading of a request with no Content-Type) or the SSZ List[...] of the same items (application/octet-stream): the offset-table encoding every variable-size list uses, which is what prysm sends for aggregates and payload votes, with a JSON retry only on a 415, and what nimbus can send for attestations. Any other content type is a 415, and a body that does not decode in the type's encoding a 400. POST beacon/blocks and POST beacon/execution_payload_envelopes choose the same way (below); the other validator-client submissions are JSON.
  • The attestation pool holds, the best-covered per data root and committee: votes from pool/attestations and the aggregator subnets, aggregates from aggregate_and_proofs, and every electra gossip aggregate P2P accepts (gloas aggregates included, in gloas's container), once all three of its signatures have verified. Gossip aggregates are pooled on arrival, so a slot's aggregates are there when the next slot's block is asked for. Entries more than an epoch old are dropped once a slot.
  • prepare_beacon_proposer records each validator's fee recipient, in memory (a validator client repeats the call every epoch).
  • blocks/{slot} advances the head state to the slot and asks the node's own execution client to build on the head (forkchoiceUpdatedV3 with payload attributes, then getPayloadV5), with the proposer's fee recipient. The body packs the pool's best aggregates (committees voting alike merged into one EIP-7549 attestation, up to MAX_ATTESTATIONS_ELECTRA). A candidate is packed only if its target root is the advanced state's own block root for that epoch and its aggregate signature verifies against that state, so an aggregate made on another branch cannot fail the whole block. The body also votes the state's own eth1_data, and carries the operation pool's slashings, exits and BLS changes, packed by pack_operations (each kept only if its process_* succeeds on a scratch copy of the state with everything packed before it applied, up to the preset's maximums), and the sync aggregate described under Sync committee (empty when nothing verifiable is pooled). The payload bundle's KZG commitments go in the body. The state root comes from running the block through process_block. The answer is fulu BlockContents: the block, the bundle's cell proofs and its blobs, with Eth-Execution-Payload-Blinded: false; there is no builder flow. It is a 503 without a configured execution client, or when the bundle is malformed (its commitments, proofs and blobs disagree in count). A slot the schedule does not place at fulu (gloas included) is a 400, checked before the execution client; produceBlockV3 is the fulu endpoint, and a gloas slot goes to produceBlockV4.
  • Graffiti is the validator client's, with both clients' codes and commits appended as far as they fit: hello RH1a2bLA3c4d for reth at 1a2b… under ethlambda at 3c4d…. The execution client's half comes from engine_getClientVersionV1, asked alongside the payload build and given up on after EL_VERSION_TIMEOUT, and is left out when it cannot be had (hello LA3c4d). The suffix shrinks rather than cutting the validator's text short, in Lighthouse's tiers, and is appended to every block with no opt-out; the graffiti_policy parameter Lighthouse's validator client sends is not read. LA is ethlambda's own code: identification.md reserves none for it and lets an unlisted client pick any two letters no listed client uses.
  • POST beacon/blocks takes JSON or SSZ, by Content-Type as above, and the Eth-Consensus-Version header (fulu or gloas, else 400; any other content type is a 415). The JSON is the specification's: lodestar's and nimbus's validator clients post it, teku's falls back to it, and lighthouse and prysm post SSZ. Both reach the same checks after decoding. Fulu: a SignedBlockContents. A slot the schedule does not place at fulu is a 400, checked before any state is advanced. The block is then checked against its parent's state advanced to its slot: an unknown parent, a slot not after the parent's, the wrong proposer or a bad proposer signature is a 400. Then every blob's cells are computed and all cell proofs batch-verified on a blocking thread (400 on any failure, nothing gossiped); the time that takes is lean_beacon_publish_data_columns_seconds. Then the block is gossiped on beacon_block, then, if it carries blobs, all 128 data column sidecars, each on data_column_sidecar_{compute_subnet_for_data_column_sidecar(index)}, and last the chain actor gets this node's custody columns, then the block, to import. Gloas: a bare gloas::SignedBeaconBlock (a gloas block carries no payload or blobs, so no sidecars go with it; they follow in the envelope). The slot must be scheduled at gloas, the parent must be held, the slot must be after the parent's, and the proposer and signature are checked against the parent's state advanced to the slot (all 400 otherwise), before the block is gossiped and handed to the chain actor.
  • The operation pool holds, beside the attestation pool and in memory in the same Store (Store::operation_pool): proposer slashings (one per proposer), attester slashings (one per hash_tree_root), voluntary exits and BLS changes (one per validator), first one wins. Entries come from the four pool/* POST routes and from gossip P2P accepts, so each has passed the topic's gossip rules; the pool never checks a signature. Entries the head state has made pointless are pruned.
  • pool/proposer_slashings, pool/attester_slashings, pool/voluntary_exits and pool/bls_to_execution_changes: GET lists the pool. POST runs the specification's validate_*_gossip rule for the topic against the head's post-state as it is (consensus-specs v1.7.0-beta.1; no process_* and no state advance, and an attester slashing's range and slashability checks run before its BLS checks), pools the operation and gossips it on the topic. A rejected operation is a 400. bls_to_execution_changes takes an array: on partial failure the response is a 400 listing the failures by index, and the valid entries are still pooled and published.

Gloas block production

A gloas proposer signs two things on their own, the block and the envelope that reveals its payload, and this node serves both halves for a block it builds itself. It may instead build on a builder's bid it holds in the shared builder market (see Builder market); then the builder, not this node, reveals the payload. The builder-API path (a bid requested from a builder's URL through the BuilderConfig's builders entries) is not implemented: those entries are decoded and not consulted.

POST /eth/v4/validator/blocks/{slot} (produceBlockV4):

Query randao_reveal (required), include_payload (required, true or false), graffiti (optional, 32-byte hex), skip_randao_verification (accepted, ignored)
Request headers Eth-Consensus-Version is optional but must be gloas when present. Accept: application/octet-stream for SSZ, JSON otherwise
Body A BuilderConfig, JSON or SSZ with Content-Type: application/octet-stream: min_bid and builder_boost_factor (govern the bids seen over p2p) and builders (up to 64 entries, decoded and ignored). Integers are quoted strings, as everywhere in the Beacon API. A missing or undecodable one is a 400
200 SSZ Self-built with include_payload=true: BlockContents (block, execution_payload_envelope, kzg_proofs, blobs). Otherwise, and for every bid-won block, the bare gloas::BeaconBlock
200 JSON {version: "gloas", consensus_block_value, execution_payload_value, execution_payload_included, data}, where data is the same container as the SSZ body
Response headers Eth-Consensus-Version: gloas, Eth-Execution-Payload-Included (true or false; always false for a bid-won block), Eth-Execution-Payload-Value (wei, decimal; a bid's value times 10^9 when it won), Eth-Consensus-Block-Value (always 0: the comparison is of execution payload values only). No Eth-Builder-Url: that is for builder-API bids
400 A slot the schedule does not place at gloas, a missing or malformed query, a wrong Eth-Consensus-Version, a bad body, a slot not after the head block, or a randao_reveal that does not verify against the slot's proposer. The slot check precedes the execution-client check, so it is a 400 on any node
503 No execution client and no viable pooled bid for the slot, the execution client did not start or return a build (and no bid took its place), the node is building on a FULL parent whose envelope it does not hold, or the winning bid failed to build and there is no local payload to fall back on

The node advances the head state to the slot, and decides which parent payload to build on with should_build_on_full over the payload status fork choice recorded for the head. It then asks its execution client to build (forkchoiceUpdatedV4 with PayloadAttributesV4, then getPayloadV6). The fee recipient and gas target of that build come from the proposer's signed ProposerPreferences for the slot when the market holds them under the slot's dependent root, else from prepare_beacon_proposer (fee recipient, or the zero address with a warning) and the parent bid's gas limit. The body packs the attestation pool's best aggregates and the payload attestation pool's votes for the parent block (an aggregate that does not verify against the advanced state is dropped, since one bad operation fails the block), and the state root comes from running the block through process_block. The bid is a zero-value self-build bid read off the built payload. What was built is cached by (slot, block root), for the current and previous slot only.

Choosing between the local payload and a bid. The bids considered are the market's for (slot, head root, parent payload hash) with this build's prev_randao, packable into a block on the advanced state, and paying the preferences' fee recipient when those are held. The best bid is the highest whose value + execution_payment is at least the config's min_bid. It wins iff builder_boost_factor * bid_gwei > floor(local_value_wei / 10^7) (the specification's weighting of the local value by 100, in integers), and the local payload wins a tie, so a factor of 0 prefers it and 2^64 - 1 prefers the bid. An engine that sets shouldOverrideBuilder keeps the local payload. Without a local payload (no engine, or the build failed) the best bid is taken whatever its weight. A bid-won block caches nothing, so the envelope GET below answers 404 for it, and POST /eth/v1/beacon/execution_payload_envelopes refuses a proposer-signed (self-build) envelope for it because the builder index differs from the bid's. The builder's own envelope goes through that endpoint as for any block.

GET /eth/v1/validator/execution_payload_envelopes/{slot}/{beacon_block_root} serves the cached unsigned envelope, for a client that asked for the block with include_payload=false: {version: "gloas", data} as JSON, or the SSZ ExecutionPayloadEnvelope on Accept: application/octet-stream, with Eth-Consensus-Version: gloas. 404 when nothing is cached for the pair (unknown, or older than the previous slot); 400 for an unparseable slot or root.

POST /eth/v1/beacon/execution_payload_envelopes takes the signed envelope, as JSON or SSZ by Content-Type (any other is a 415; teku's validator client posts JSON, nimbus posts the bare envelope as JSON and the contents as SSZ, lighthouse posts SSZ, prysm posts SSZ and retries as JSON on a 415), with Eth-Consensus-Version: gloas and Eth-Blob-Data-Included (both required, else 400):

  • true: the body is SignedExecutionPayloadEnvelopeContents (the signed envelope, kzg_proofs, blobs).
  • false: the body is the bare signed envelope, and the blobs and proofs come from the production cache. 400 when none is cached and the block's bid commits to blobs.

The envelope's block is waited for, polling every 50 ms for up to 4 seconds, since a validator client publishes the envelope right after the block returns, which is before the chain actor has imported it; a block still unknown after that is a 400 (unknown block). The envelope must fulfill the block's bid (builder index and block hash), the blobs must match the bid's commitments, the signature must verify under DOMAIN_BEACON_BUILDER against the block's post-state, and every data column built from the blobs must verify against the commitments and cell proofs; any failure is a 400. A success is 200 once the envelope and all NUMBER_OF_COLUMNS gloas data column sidecars are handed to P2P, which gossips them together. A node that does not subscribe to a column's subnet publishes through gossipsub fanout.

Builder market

Three endpoints serve the gloas builder market. Bids and preferences go through the same rules as the execution_payload_bid and proposer_preferences gossip topics and are held in one shared builder market that p2p also fills, so a message accepted from either side is seen by both.

  • POST /eth/v1/beacon/execution_payload_bids takes one SignedExecutionPayloadBid, as JSON (or no Content-Type) or SSZ (application/octet-stream, at most 196,932 bytes; any other type is 415). Eth-Consensus-Version is optional and must be gloas when present. An identical bid already pooled is 200 and is not republished. Otherwise the gossip rules run (a known parent, the preferences for the slot and their fee recipient and gas limit, a known parent payload, an active funded builder, the signature), and a bid that is anything but accepted is a 400 whose message is "{outcome}: {reason}" (for instance reject: bad_signature or ignore: preferences_unseen), because there is no status for a valid bid this node would not relay. An accepted bid is pooled for block production and gossiped. 500 when the network actor is down.
  • POST /eth/v1/validator/proposer_preferences takes a list of SignedProposerPreferences (at most (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH, 64 on mainnet), as a JSON array or an SSZ list. Eth-Consensus-Version is optional and may be fulu or gloas, since a validator client submits during the epoch before the fork. Each entry runs the gossip rules in order; an entry already cached, identical, succeeds without a republish. Accepted entries are cached (the first per (proposal slot, dependent root) wins) and gossiped. If any entry fails the answer is 400 {code, message, failures: [{index, message}]} and the others were still published. The endpoint is not gated on the sync status. A node whose dependent block is unknown answers ignore: unknown_block for that entry.
  • POST /eth/v1/beacon/states/{state_id}/builders takes an optional JSON body {ids?: [index | 0x pubkey], statuses?: [pending | active | exited]} (empty or absent selects everything) and answers {execution_optimistic, finalized, data: [{index, status, builder}]} in registry order, integers quoted. An id naming no builder is omitted. exited is a set withdrawable_epoch, else active per is_active_builder, else pending. 400 for a malformed body, id or status, or a pre-gloas state; 404 for an unknown state. The Beacon API defines no GET form.

POST /eth/v1/validator/builder_preferences is not served (404).

Sync committee

All four endpoints are JSON only (beacon-APIs lists no SSZ body for them), and read integers quoted or bare. All share one SharedSyncCommitteePool, which P2P fills from accepted gossip as well. None of it reaches the chain actor: sync committee votes have no fork-choice effect.

  • POST /eth/v1/beacon/pool/sync_committees takes a JSON array of SyncCommitteeMessages and needs no Eth-Consensus-Version. Each is checked against the head state with the rules sync_committee_{subnet_id} gossip applies (the current slot; the validator holds a seat in the committee its slot names, which is the next committee at a period's last slot; the signature), then pooled once per seat and published on every distinct subnet its seats sit on. One BLS verification per message, on a blocking thread. Not gated on syncing, like pool/attestations. A refused message comes back in an IndexedErrorMessage (400) as {outcome}: {reason} with its position, and the rest still go out. A body that is not a list of messages is a 400 with no failures; a dead network actor is a per-item failure.
  • GET /eth/v1/validator/sync_committee_contribution?slot=&subcommittee_index=&beacon_block_root= answers {data: SyncCommitteeContribution} (no version): the best pooled contribution for the key, extended by every pooled direct message at a position it does not cover. 404 when the pool holds nothing for it, 400 when subcommittee_index is not below SYNC_COMMITTEE_SUBNET_COUNT, and 503 while the node is syncing or when the block is optimistic or unknown (an aggregator must not sign over a root this node cannot vouch for).
  • POST /eth/v1/validator/contribution_and_proofs takes a JSON array of SignedContributionAndProofs. Each passes the checks sync_committee_contribution_and_proof gossip applies (current slot, subcommittee in range, a participant, the selection proof selects the aggregator, aggregator is in the subcommittee, and the selection proof, envelope and aggregate signatures) against a fresh seen cache, since P2P's holds what peers sent. A valid one is pooled and gossiped; the failure shape is the same as above.
  • POST /eth/v1/validator/sync_committee_subscriptions takes [{validator_index, sync_committee_indices, until_epoch}]. The subnet of a position is index / SYNC_SUBCOMMITTEE_SIZE; entries are grouped by subnet keeping the latest until_epoch (exclusive), clamped to the end of the next sync committee period, and handed to P2P, which joins the subnets immediately. A position at or above SYNC_COMMITTEE_SIZE is a 400 and nothing is joined. A node forgets these on restart, so a client repeats them every epoch. sync_committee_selections (distributed validators) is not served; beacon-APIs lets a node leave it out.

Block production. The block at slot N packs only what is pooled for (N - 1, parent_root): per subcommittee the best contribution, extended by pooled direct messages at uncovered positions. The result is verified exactly as process_sync_aggregate will check it, against the state the block is built on; if it fails, or nothing is pooled, the block carries the empty aggregate. Fulu (blocks/{slot}) and gloas (produceBlockV4) do the same, and the "retry without operations" fallback of each also drops the sync aggregate.

Payload timeliness committee

  • POST /eth/v1/validator/duties/ptc/{epoch} takes a JSON array of quoted validator indices (an empty array or a bad index is a 400) and answers {dependent_root, execution_optimistic, data: [{pubkey, validator_index, slot}]}: one duty per validator, the first slot of the epoch whose committee seats it. An index with no seat, or unknown, gets none. The epoch may be at most one past the later of the head state's and the wall clock's epoch (else 400). An epoch before the gloas fork answers 200 with an empty data, so a client can ask every epoch. A gloas head state answers from its ptc_window; an epoch the head has not caught up to, or the first gloas epoch under a fulu head, is answered from a copy of the head state advanced to the epoch's first slot, crossing the gloas upgrade where it falls, on a blocking thread. A gloas epoch more than one before the head state's is a 400. dependent_root is defined as for attester duties. There is no 503.
  • GET /eth/v1/validator/payload_attestation_data?slot= answers {version: "gloas", data: {beacon_block_root, slot, payload_present, blob_data_available}} as JSON, or the SSZ PayloadAttestationData on Accept: application/octet-stream, with Eth-Consensus-Version: gloas. The data is for the block of that very slot on the head's chain; a slot with no such block (or whose block is not a gloas block) answers 204 with no body, which a client reads as "cast no vote". A slot before the gloas fork is a 400, and a node that is syncing answers 503, as does one run without an execution client or whose block has an unvalidated execution payload behind it (the same rule as attestation data). payload_present is whether the block's envelope reached this node before the slot's start plus PAYLOAD_DUE_BPS of it, judged by arrival time (see Spec Deviations). blob_data_available is true when the bid commits to no blobs, the payload is verified, or every column this node custodies for the block is stored.
  • POST /eth/v1/beacon/pool/payload_attestations takes a JSON array of PayloadAttestationMessages; Eth-Consensus-Version is optional and must be gloas when present. Each message passes the checks gossip applies to a peer's (the current slot, a known block at the slot, committee membership, signature) against a fresh seen-cache. A valid one is pooled and gossiped; one already pooled is neither pooled nor gossiped again and still counts as accepted. A rejected one comes back in an IndexedErrorMessage (400) as {outcome}: {reason} with its position, and the rest still go out; a head-state cache miss is a per-item ignore: state_unavailable.
  • GET /eth/v1/beacon/pool/payload_attestations?slot= answers {version: "gloas", data} with the pool's votes as the specification's PayloadAttestations, those of slot only when given: one aggregate per slot and PayloadAttestationData, a bit per seat of that slot's committee (get_ptc on the head state) and the seats' signatures aggregated, the way block production packs them. The pool itself keeps the individual messages. JSON only; a vote whose slot falls outside the head state's committee window is left out.

The payload attestation pool is shared with P2P, which fills it from accepted gossip, so a block this node builds carries the votes its peers sent too.

A cl_type: ethlambda participant in our ethereum-package fork (lambdaclass/ethereum-package, branch add-ethlambda) runs an ethlambda validator client that, by default, talks to this node alone, so every block its validators propose is one this node built.

Encoding

JSON is the default; SSZ is served on Accept: application/octet-stream, which is the order lighthouse serves. An Accept listing both is ranked by its q weights. Every response carrying a fork-versioned container also sets Eth-Consensus-Version to the lowercase fork name, in both encodings, since SSZ carries no type tag.

JSON here follows the Beacon API's own encoding, which is not the lean surface's: every integer is a quoted decimal string, byte strings are 0x-prefixed hex, and Uint256 is quoted decimal rather than hex. Errors are {"code": ..., "message": ...}, where the lean surface uses {"error": ...}.

Serving /eth/v2/debug/beacon/states/finalized as SSZ is what makes this client checkpoint-syncable from itself: it is the exact path checkpoint_sync.rs fetches from other clients.

GET /eth/v1/beacon/execution_payload_envelopes/{block_id}

Gloas moves a block's execution payload into a separate signed envelope. This serves the one the node has verified, as {version: "gloas", execution_optimistic, finalized, data} JSON or as SSZ on Accept: application/octet-stream, with Eth-Consensus-Version: gloas either way. It is also what a checkpoint-syncing node asks for the payload of its anchor (see checkpoint_sync.md).

Status When
400 block_id is malformed
404 the block is unknown, or the node holds no envelope for it

The second 404 is deliberately one status for several situations: the store keeps verified envelopes only, so a withheld payload, a payload not yet received and a pre-gloas block (which has no envelope; the specification's error list names no other status for it) are indistinguishable here. A caller must read 404 as "unknown to this node", not as "the payload is empty".

execution_optimistic under gloas

Before gloas a block's payload ran inside the block, so the flag is whether the block is in the store's optimistic set. From gloas on the payload is a separate object that may arrive late or never, and two rules share the flag:

  • an envelope response is optimistic iff that payload's verdict is not VALID (an unrecorded verdict reads as NOT_VALIDATED);
  • a block, header, state or finality-checkpoints response for a gloas block is optimistic iff the latest FULL payload the block builds on is not VALID. The walk starts at the block's parent: if the block's bid names the parent's payload as its parent block hash it reads the parent's verdict, and otherwise it steps back to the parent and repeats. A pre-gloas block on the way reads the optimistic set. The block's own payload does not count, so a withheld payload does not make the chain look unverified.

Pre-gloas responses are unchanged. One helper implements both rules (shared/optimistic.rs) and every response that carries the flag goes through it.

GET /eth/v1/config/fork_schedule

The Fork objects of the Config's schedule, oldest first, as {previous_version, current_version, epoch} with the epoch quoted. Phase0 is first, at epoch 0 and its own predecessor; every later fork whose epoch is not FAR_FUTURE_EPOCH follows, and an unscheduled fork is skipped without breaking the chain, so each entry's previous_version is the one before it in the list. Nimbus's validator client reads this every epoch, requires that linked-list shape, and marks a node it cannot decode as incompatible. Nimbus also compares /eth/v1/config/spec against its own constants before use: the 13 preset values, the 7 domain types, SECONDS_PER_SLOT (or SLOT_DURATION_MS, which this node does not report) and every fork's *_FORK_VERSION and *_FORK_EPOCH, with altair scheduled. All are served (a unit test pins the list).

GET /eth/v1/config/spec

One flat object holding the network's configuration, the compiled preset, and the specification's constants, as the Beacon API asks. Validator clients depend on it: lighthouse's refuses a beacon node whose PRESET_BASE does not match its own, and treats an absent key as a mismatch.

The key set is lighthouse's plus gloas's preset and constant keys, read from beacon::preset and beacon::constants rather than typed here: PTC_SIZE, MAX_PAYLOAD_ATTESTATIONS, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, the four gloas domains (DOMAIN_BEACON_BUILDER, DOMAIN_PTC_ATTESTER, DOMAIN_PROPOSER_PREFERENCES, DOMAIN_BUILDER_DEPOSIT), the builder withdrawal prefix and request types, and the BUILDER_INDEX_*, BUILDER_PAYMENT_THRESHOLD_* and PAYLOAD_BUILDER_VERSION constants. Gloas's schedule, timing and churn keys are Config fields, so they were already reported. Three keys are left out because the specification does not define them (GAS_LIMIT_ADJUSTMENT_FACTOR, RESP_TIMEOUT, TTFB_TIMEOUT), and so are gloas's MAX_*_SIZE preset keys, which bound gossip message sizes the node does not run on. Domain types and withdrawal prefixes are 0x-prefixed hex; VERSIONED_HASH_VERSION_KZG is a decimal, as lighthouse reports it. GENESIS_TIME is absent: /eth/v1/beacon/genesis reports it.

The configuration keys come from the Config the data directory was initialized with. Some of them (the custody and subnet counts, the MAX_REQUEST_* limits, MAX_PAYLOAD_SIZE, the snappy message domains, MAXIMUM_GOSSIP_CLOCK_DISPARITY) the node runs on compile-time constants for, rather than reading them from Config. Startup refuses a network whose config.yaml sets any of them to a different value (see cli.md), so what this endpoint reports is what the node uses. CONFIG_NAME is the stored name: a resume under a renamed config.yaml warns and keeps it.

Accepted ids, and three that are refused

block_id and state_id accept head, finalized, justified, a slot number, and a 0x-prefixed 32-byte root. Two cases are deliberate refusals, both recorded in Spec Deviations:

  • genesis is a 404 on either id. Table::BlockRoots indexes the canonical branch above the store's anchor, and the anchor's own slot is never written to it; a checkpoint-synced directory has no genesis block either way.
  • A state_id given as a 0x… root is a 404: states are keyed by block root here, with no reverse index. The refusal names the ids that do work.

The anchor's own slot does resolve, despite being absent from that index: the lookup falls back to the roots the store can name and accepts one only when the block under it really sits at that slot. This matters because it is the slot a checkpoint-syncing peer asks for right after reading the finalized state. A slot the store holds nothing at is still a 404.

/eth/v1/node/identity reports the peer id, the ENR discv5 publishes, and the node's QUIC, TCP and discv5 multiaddrs. The multiaddrs need --discovery.advertise-ip, since a node bound to 0.0.0.0 does not know the address peers reach it on; without the flag they are empty and the ENR carries no IP. metadata is the MetaData the node answers peers with over req/resp, read at request time, so syncnets and seq_number follow the sync committee subnets a validator client asks for. That seq_number versions MetaData alone: the p2p specification counts it separately from the ENR's seq.

GET /eth/v1/events

The Beacon API eventstream: the same Server-Sent Events stream as /lean/v0/events (topic on the event: line, payload on data:, one shared best-effort ring, : error - dropped N messages on a gap), with the Beacon API's own topics and payloads. The response also sets X-Accel-Buffering: no, so a reverse proxy does not hold events back.

curl -N 'http://127.0.0.1:5052/eth/v1/events?topics=head&topics=block,finalized_checkpoint'

topics is required, and may be repeated (?topics=head&topics=block, the specification's form) or comma-separated (?topics=head,block, which lighthouse also accepts). Duplicates collapse. A missing or empty topics, or a name that is not a Beacon API topic, is a 400 in the Beacon API error shape: {"code":400,"message":"Invalid topic: weather_forecast"}.

Accepted is not emitted. Every topic in the specification is accepted, so a validator client subscribing to several at once is not turned away, but only these are ever sent:

Event Payload Emitted when
head {"slot":"10", "block":"0x…", "state":"0x…", "epoch_transition":false, "previous_duty_dependent_root":"0x…", "current_duty_dependent_root":"0x…", "execution_optimistic":false} The head changed, and is within 32 slots of the wall clock
block {"slot":"10", "block":"0x…", "execution_optimistic":false} A block is imported
block_gossip {"slot":"10", "block":"0x…"} A block passed gossip validation, or was published through POST /eth/v2/beacon/blocks
finalized_checkpoint {"block":"0x…", "state":"0x…", "epoch":"2", "execution_optimistic":false} The finalized checkpoint advanced
chain_reorg {"slot":"200", "depth":"50", "old_head_block":"0x…", "new_head_block":"0x…", "old_head_state":"0x…", "new_head_state":"0x…", "epoch":"2", "execution_optimistic":false} The new head does not descend from the previous one
attestation the aggregate's Attestation An aggregate passed the beacon_aggregate_and_proof rules: accepted from gossip, or submitted through POST /eth/v2/validator/aggregate_and_proofs
data_column_sidecar {"block_root":"0x…", "index":"1", "slot":"1"} A column this node custodies is stored, from any source

Never emitted: single_attestation (subnet votes are verified and relayed but never reach the chain actor), the operation topics (voluntary_exit, proposer_slashing, attester_slashing, bls_to_execution_change, contribution_and_proof: no operation pools), payload_attributes, the light client topics, blob_sidecar, and head_v2 with every other gloas topic (this build stops at fulu).

How head and finality events behave:

  • One head per head recompute. The chain actor recomputes the head once per tick and once per import cascade, and diffs it against the head it last reported. A cascade importing several blocks announces each block, then a single head for where it ended.
  • Any head change counts, including one no block caused (a tick's votes moving the head to a sibling, or an execution client's INVALID verdict moving it back to an ancestor). Lighthouse reports only the chain_reorg for those; Prysm reports both, as this node does.
  • Stale heads are not reported. A head more than 32 slots behind the wall clock (catch-up) sends no head; block events still show progress. chain_reorg and finalized_checkpoint are not gated.
  • Order: each block as it imports, then chain_reorg, head, finalized_checkpoint.
  • epoch_transition is whether the head's epoch is later than the previous head's. The dependent roots follow the specification, with the genesis block root on underflow (epochs 0 and 1).
  • chain_reorg.depth is how many slots the previous head sat above the two heads' common ancestor (lighthouse's definition).
  • execution_optimistic is reported as it is, optimistic heads included (lighthouse suppresses those head events; Prysm sends them flagged).

An aggregate this node's own validator client submits reaches the chain actor too: gossip never delivers a node its own messages, so POST /eth/v2/validator/aggregate_and_proofs hands each accepted one to the actor after gossiping it. That is also what puts this node's own aggregates into its fork choice.

Event payloads are built only while at least one client is connected, whatever topics that client asked for; with none, the stream costs nothing.

Metrics & Debug Server (:5054)

Method Path Response Description
GET /metrics text Prometheus-format metrics
GET /health JSON Liveness check (same payload as the API health endpoint)
GET /debug/pprof/allocs pprof jemalloc heap profile
GET /debug/pprof/allocs/flamegraph SVG jemalloc heap flamegraph

The metrics endpoint reads from the global Prometheus registry and needs no store access. See Metrics for the full list of exposed series.

Heap-profiling endpoints are backed by jemalloc's built-in profiler and are only functional on Linux; other platforms return 501 Not Implemented. On Linux they return 500 if profiling was not enabled at startup.

Test-Driver Endpoints (Hive)

When the binary boots with HIVE_LEAN_TEST_DRIVER=1 (any of 1/true/yes), it runs in test-driver mode instead of the normal API server. The ethereum/hive lean simulator drives these endpoints to replay leanSpec fixtures over HTTP. The driver swaps its in-process Store on every fork_choice/init, so one container can replay many fixtures without restart.

Method Path Response
GET /lean/v0/health JSON liveness (for the hive port check)
POST /lean/v0/test_driver/fork_choice/init 204 / 400
POST /lean/v0/test_driver/fork_choice/step StepResponse
POST /lean/v0/test_driver/state_transition/run StateTransitionResponse
POST /lean/v0/test_driver/verify_signatures/run VerifySignaturesResponse

Content Types

Kind Content-Type
JSON application/json; charset=utf-8
SSE text/event-stream
SSZ application/octet-stream
Prometheus metrics text/plain; version=0.0.4; charset=utf-8
HTML text/html; charset=utf-8