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.
| 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.
| 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 |
The handler emits a fixed, compact body (no whitespace):
{"status":"healthy","service":"lean-rpc-api"}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.
{ "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.
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.
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.
{ "slot": 128, "root": "0x1a2b…" }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…"}
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).
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" }.
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.
{
"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.
{ "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.
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.
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) |
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 withprocess_slotsto the first epoch that can derive it, taken from fork choice'scheckpoint_statecache and run on a blocking thread. An epoch before the head's (attester: more than one before) or past the bound is a400.dependent_rootfollows 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 a503while 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'sproposer_lookaheadandupgrade_to_gloascarries 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_committeefor an epoch in the head's own sync committee period andnext_sync_committeefor 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 a400(an earlier one would need a historical state; seedocs/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 a400, and the endpoint is a503while 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/attestationsoraggregate_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 alwaysfalse); anything else, and an unknown index, is a400, and the endpoint is a503while the node is syncing. attestation_datafollows phase0'svalidator.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 cachedcheckpoint_state, and only when the head is in an earlier epoch). A slot before the head, or past the wall clock, is a400. An optimistic head is a503: the Beacon API requires one whenever the answer'sbeacon_block_roothas 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_indexis optional and ignored: from electra on every committee of a slot attests to the same data.data.indexis0before gloas. At a gloas slot it is the payload-present flag:0when the head block is from the requested slot itself (its payload cannot be revealed yet), otherwise1exactly when fork choice holds the head block's payload as FULL (Store::head_payload_status, or one freshget_head_nodewalk 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 samestate_idhandling,execution_optimisticandfinalized. The id is an index or a0xpubkey; one naming no validator is a404(the list form omits it), a malformed one a400. Lighthouse's validator client resolves each key to its index this way, and stays inactive without it.pool/attestationschecks each attestation against the electrabeacon_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 anIndexedErrorMessagewith 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-Versionmust name electra, fulu or gloas here and onaggregate_and_proofs, and must match the fork of each item's slot: agloasheader on a pre-gloas slot, or an older one on a gloas slot, fails that item (not the batch) withEth-Consensus-Version does not match the attestation slot's fork. At a gloas slotdata.indexmay be0or1, checked with gloas's payload-status gossip rule (a vote in the voted block's own slot must be0;1needs the node to have verified the payload, otherwise the item fails). A gloasSingleAttestationis 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 thebeacon_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 inattnets.aggregate_attestationanswers from the pool: every vote held for the data root and committee, as electra'sAttestationwith the BLS aggregate of their signatures; at a gloas slot the answer is{version: "gloas", data}with gloas'sAttestation(the same JSON shape). JSON only.404when nothing is held, and503when the block the votes name is optimistic, whether or not it is the head, or when no execution client is configured.aggregate_and_proofschecks each aggregate with the samebeacon_aggregate_and_proofgossip 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'sattestation. TheEth-Consensus-Versionheader picks the decoder:gloastakes gloas'sSignedAggregateAndProof, the others electra's.- Request body encodings.
pool/attestations,pool/payload_attestationsandaggregate_and_proofstake either JSON (application/json, also the reading of a request with noContent-Type) or the SSZList[...]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 a415, and what nimbus can send for attestations. Any other content type is a415, and a body that does not decode in the type's encoding a400.POST beacon/blocksandPOST beacon/execution_payload_envelopeschoose 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/attestationsand the aggregator subnets, aggregates fromaggregate_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_proposerrecords 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 (forkchoiceUpdatedV3with payload attributes, thengetPayloadV5), with the proposer's fee recipient. The body packs the pool's best aggregates (committees voting alike merged into one EIP-7549 attestation, up toMAX_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 owneth1_data, and carries the operation pool's slashings, exits and BLS changes, packed bypack_operations(each kept only if itsprocess_*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 throughprocess_block. The answer is fuluBlockContents: the block, the bundle's cell proofs and its blobs, withEth-Execution-Payload-Blinded: false; there is no builder flow. It is a503without 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 a400, checked before the execution client;produceBlockV3is the fulu endpoint, and a gloas slot goes toproduceBlockV4.- Graffiti is the validator client's, with both clients' codes and
commits appended as far as they fit:
hello RH1a2bLA3c4dfor reth at1a2b…under ethlambda at3c4d…. The execution client's half comes fromengine_getClientVersionV1, asked alongside the payload build and given up on afterEL_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; thegraffiti_policyparameter Lighthouse's validator client sends is not read.LAis ethlambda's own code:identification.mdreserves none for it and lets an unlisted client pick any two letters no listed client uses. POST beacon/blockstakes JSON or SSZ, byContent-Typeas above, and theEth-Consensus-Versionheader (fuluorgloas, else400; any other content type is a415). 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: aSignedBlockContents. A slot the schedule does not place at fulu is a400, 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 a400. Then every blob's cells are computed and all cell proofs batch-verified on a blocking thread (400on any failure, nothing gossiped); the time that takes islean_beacon_publish_data_columns_seconds. Then the block is gossiped onbeacon_block, then, if it carries blobs, all 128 data column sidecars, each ondata_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 baregloas::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 (all400otherwise), 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 perhash_tree_root), voluntary exits and BLS changes (one per validator), first one wins. Entries come from the fourpool/*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_exitsandpool/bls_to_execution_changes:GETlists the pool.POSTruns the specification'svalidate_*_gossiprule for the topic against the head's post-state as it is (consensus-specs v1.7.0-beta.1; noprocess_*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 a400.bls_to_execution_changestakes an array: on partial failure the response is a400listing the failures by index, and the valid entries are still pooled and published.
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 isSignedExecutionPayloadEnvelopeContents(the signed envelope,kzg_proofs,blobs).false: the body is the bare signed envelope, and the blobs and proofs come from the production cache.400when 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.
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_bidstakes oneSignedExecutionPayloadBid, as JSON (or noContent-Type) or SSZ (application/octet-stream, at most 196,932 bytes; any other type is415).Eth-Consensus-Versionis optional and must begloaswhen present. An identical bid already pooled is200and 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 a400whose message is"{outcome}: {reason}"(for instancereject: bad_signatureorignore: 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.500when the network actor is down.POST /eth/v1/validator/proposer_preferencestakes a list ofSignedProposerPreferences(at most(MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH, 64 on mainnet), as a JSON array or an SSZ list.Eth-Consensus-Versionis optional and may befuluorgloas, 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 is400{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 answersignore: unknown_blockfor that entry.POST /eth/v1/beacon/states/{state_id}/builderstakes 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.exitedis a setwithdrawable_epoch, elseactiveperis_active_builder, elsepending.400for a malformed body, id or status, or a pre-gloas state;404for an unknown state. The Beacon API defines noGETform.
POST /eth/v1/validator/builder_preferences is not served (404).
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_committeestakes a JSON array ofSyncCommitteeMessages and needs noEth-Consensus-Version. Each is checked against the head state with the rulessync_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, likepool/attestations. A refused message comes back in anIndexedErrorMessage(400) as{outcome}: {reason}with its position, and the rest still go out. A body that is not a list of messages is a400with nofailures; 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}(noversion): the best pooled contribution for the key, extended by every pooled direct message at a position it does not cover.404when the pool holds nothing for it,400whensubcommittee_indexis not belowSYNC_COMMITTEE_SUBNET_COUNT, and503while 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_proofstakes a JSON array ofSignedContributionAndProofs. Each passes the checkssync_committee_contribution_and_proofgossip 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_subscriptionstakes[{validator_index, sync_committee_indices, until_epoch}]. The subnet of a position isindex / SYNC_SUBCOMMITTEE_SIZE; entries are grouped by subnet keeping the latestuntil_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 aboveSYNC_COMMITTEE_SIZEis a400and 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.
POST /eth/v1/validator/duties/ptc/{epoch}takes a JSON array of quoted validator indices (an empty array or a bad index is a400) 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 (else400). An epoch before the gloas fork answers200with an emptydata, so a client can ask every epoch. A gloas head state answers from itsptc_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 a400.dependent_rootis defined as for attester duties. There is no503.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 SSZPayloadAttestationDataonAccept: application/octet-stream, withEth-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) answers204with no body, which a client reads as "cast no vote". A slot before the gloas fork is a400, and a node that is syncing answers503, 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_presentis whether the block's envelope reached this node before the slot's start plusPAYLOAD_DUE_BPSof it, judged by arrival time (see Spec Deviations).blob_data_availableis 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_attestationstakes a JSON array ofPayloadAttestationMessages;Eth-Consensus-Versionis optional and must begloaswhen 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 anIndexedErrorMessage(400) as{outcome}: {reason}with its position, and the rest still go out; a head-state cache miss is a per-itemignore: state_unavailable.GET /eth/v1/beacon/pool/payload_attestations?slot=answers{version: "gloas", data}with the pool's votes as the specification'sPayloadAttestations, those ofslotonly when given: one aggregate per slot andPayloadAttestationData, a bit per seat of that slot's committee (get_ptcon 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.
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.
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".
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 asNOT_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.
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).
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.
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:
genesisis a404on either id.Table::BlockRootsindexes 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_idgiven as a0x…root is a404: 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.
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
headper 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 eachblock, then a singleheadfor 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
INVALIDverdict moving it back to an ancestor). Lighthouse reports only thechain_reorgfor 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;blockevents still show progress.chain_reorgandfinalized_checkpointare not gated. - Order: each
blockas it imports, thenchain_reorg,head,finalized_checkpoint. epoch_transitionis 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.depthis how many slots the previous head sat above the two heads' common ancestor (lighthouse's definition).execution_optimisticis reported as it is, optimistic heads included (lighthouse suppresses thoseheadevents; 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.
| 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.
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 |
| 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 |