Skip to content

Add head_v2 event and deprecate head event#590

Merged
nflaig merged 24 commits into
ethereum:masterfrom
chong-he:event-head
May 28, 2026
Merged

Add head_v2 event and deprecate head event#590
nflaig merged 24 commits into
ethereum:masterfrom
chong-he:event-head

Conversation

@chong-he

@chong-he chong-he commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

This PR adds a head_v2 event to the event stream as per discussion in #589

Comment thread apis/eventstream/index.yaml Outdated
@rolfyone rolfyone added the Gloas api's needed in Gloas fork. label Mar 19, 2026
Comment thread apis/eventstream/index.yaml Outdated
@chong-he chong-he changed the title Add new_head event Add head_v2 event and deprecate head event Mar 20, 2026

@chong-he chong-he left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some questions that I have

Comment thread apis/validator/duties/attester.yaml Outdated

After Fulu:

- event.previous_epoch_dependent_root when `compute_epoch_at_slot(event.slot) == epoch`

@chong-he chong-he Mar 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use the term previous_epoch_dependent_root here (instead of previous_duty_dependent_root) as I thought after Fulu it is "epoch", not "duty". Is this right? If so, maybe I can also update this part in proposer.v2.yaml:

- event.previous_duty_dependent_root when `compute_epoch_at_slot(event.slot) == epoch`

Edit: Should it actually be current_epoch_dependent_root instead of previous_epoch_dependent_root for post-Fulu case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussed with @michaelsproul , we revise this part to make it clearer:

  • Remove before Fulu / after Fulu as now the chain is at Fulu
  • rename to use current_epoch... instead of current_duty...
  • remove event.block otherwise bullet point as it serves no purpose (and could be confusing)
  • changes made to: attester.yaml, proposer.v2.yaml and ptc.yaml

Comment thread apis/validator/duties/attester.yaml Outdated
Comment thread apis/eventstream/index.yaml Outdated
Comment thread apis/eventstream/index.yaml Outdated
chong-he and others added 2 commits March 20, 2026 19:21
Co-authored-by: Nico Flaig <nflaig@protonmail.com>
Comment thread apis/eventstream/index.yaml Outdated
Comment thread apis/eventstream/index.yaml Outdated
description: The node has finished processing, resulting in a new head. previous_epoch_dependent_root is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 2) - 1)`, current_epoch_dependent_root is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)` and next_epoch_dependent_root is `get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1)`. All dependent roots use the genesis block root in the case of underflow.
value: |
event: head_v2
data: {"version": "gloas", "data":{"slot":"10", "block":"0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf", "state":"0x600e852a08c1200654ddf11025f1ceacb3c2e74bdd5c630cde0838b2591b69f9", "epoch_transition":false, "previous_epoch_dependent_root":"0x5e0043f107cb57913498fbf2f99ff55e730bf1e151f02f221e977c91a90a0e91", "current_epoch_dependent_root":"0x5e0043f107cb57913498fbf2f99ff55e730bf1e151f02f221e977c91a90a0e91", "next_epoch_dependent_root":"0x5e0043f107cb57913498fbf2f99ff55e730bf1e151f02f221e977c91a90a0e91", "execution_optimistic": false}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thought of another field we might want to add to this event, possibly only after Gloas: the head's payload status.

With Gloas fork choice it's likely we'll get two head events every slot:

  1. block_root: X, payload_status: pending
  2. block_root: X, payload_status: full

If we do have versioning on head_v2, we could add the payload_status attribute just for Gloas. Or otherwise we could just default them to pending prior to Gloas (although there are arguments for pending, empty or full for pre-Gloas blocks 😅 ).

@nflaig nflaig Apr 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to think more about it, but head seems unrelated to payload, what's the purpose or use case you had in mind?

regarding pending, I don't think we "leak" that anywhere currently outside of fork choice, so not sure that should be used anywhere else

if we would emit the event twice per slot, I would rather just do full / empty (if not received by ptc deadline), (I guess you would need pending for first time the event is emitted)

but maybe it should rather have payload_status as in status of the previous payload, maybe previous_payload_status

(although there are arguments for pending, empty or full for pre-Gloas blocks 😅 ).

I haven't seen a pending or empty block since the merge happened, using anything other than full for pre-gloas seems confusing to me, at least in lodestar we use full but it's a fork choice implementation detail anyways

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to think more about it, but head seems unrelated to payload, what's the purpose or use case you had in mind?

I don't think it's unrelated any more, the output of fork choice get_head now includes a payload status. I can imagine that some EL tools could be interested in knowing when certain payloads become enshrined as head. They can use the new *payload events, but I think they'll also want to know that the payload became canonical (not just that it exists).

I haven't seen a pending or empty block since the merge happened, using anything other than full for pre-gloas seems confusing to me, at least in lodestar we use full but it's a fork choice implementation detail anyways

In Lighthouse we treat pre-Gloas blocks as pending in some places (no payload applied), and empty (no payload applied) 😁 But yeah I agree it's an impl detail, conceptually full (or no status) makes the most sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO pending inevitably leaks into the state-based APIs, which have to now make the differentiation between block-states (pending) and payload-states (full).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this kinda ties into discussion here #572 which doesn't seem fully resolved, I would trust @twoeths opinion on this which from his last comment seems like we wanna be more explicit about payload status after gloas

so we probably wanna align what state_id=head returns from the state-based apis, and how the head event is emitted

in that case it might make sense to emit two head events per slot

@chong-he chong-he Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the description to have the possibility of having two head events per slot after Gloas, given the payload_status could be received after.

From my understanding, it will be something like:

First head emitted event:

data: {"version": "gloas", "data":{"slot":"10", "block":"x", "state":"y", payload_status: empty, "epoch_transition": ...}}

Second head emitted event for the same slot with full payload which changes the state:

data: {"version": "gloas", "data":{"slot":"10", "block":"x", "state":"z", payload_status: full, "epoch_transition": ...}}

If we do have the payload_status in the head_v2 event, then I think we keep the version field.

@nflaig nflaig May 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First head emitted event:

what about payload_status: pending?

Second head emitted event for the same slot with full payload which changes the state:

so we would not emit this if there was no payload for the slot? or would you just emit empty after some time in the slot, ie. if not observed after ptc deadline or maybe just if ptc voted for not available?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I am confused and not sure. Should the first emitted event (after receiving the beacon/consensus part of the block), should the payload_status be pending? This is from here: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md#modified-get_head

And if it starts with pending for the first head event, then we update to either empty or full in the second emission?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now anti-pending, the output of get_head is always empty or full, because pending nodes always have at least one child (their empty node), so can never be returned by get_head.

@rolfyone

Copy link
Copy Markdown
Contributor

happy to run with this if everyone else is now? @nflaig @michaelsproul ?

Comment thread apis/eventstream/index.yaml Outdated
@michaelsproul

Copy link
Copy Markdown
Contributor

Yep. LGTM.

Comment thread apis/eventstream/index.yaml Outdated
Co-authored-by: Michael Sproul <michaelsproul@users.noreply.github.com>
rolfyone
rolfyone previously approved these changes May 26, 2026

@rolfyone rolfyone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread apis/validator/duties/attester.yaml
Comment thread apis/eventstream/index.yaml Outdated
Comment thread apis/eventstream/index.yaml Outdated
Co-authored-by: Nico Flaig <nflaig@protonmail.com>

@nflaig nflaig left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nflaig nflaig merged commit ed6b9ab into ethereum:master May 28, 2026
3 checks passed
pull Bot pushed a commit to All-Blockchains/prysm that referenced this pull request Jun 11, 2026
**What type of PR is this?**

> Feature

**What does this PR do? Why is it needed?**

Implements a new `head_v2` event. Ref:
ethereum/beacon-APIs#590

Note that it doesn't deprecate the legacy `head` event. So it is
**expected** to receive both `head` and `head_v2` events if subscribed
for them.

**Which issues(s) does this PR fix?**

Fixes OffchainLabs#16922 

**Other notes for review**

### Kurtosis config

```yaml
participants:
  - el_type: ethrex
    el_image: ethpandaops/ethrex:glamsterdam-devnet-4
    el_extra_params:
      - --http.api=eth,net,web3,admin
    cl_type: prysm
    cl_image: prysm-bn-custom-image:latest
    vc_image: prysm-vc-custom-image:latest
    supernode: true
    count: 3
    cl_extra_params:
      - --subscribe-all-subnets
      - --verbosity=debug
    vc_extra_params:
      - --verbosity=debug
      - --enable-beacon-rest-api

network_params:
  fulu_fork_epoch: 0
  gloas_fork_epoch: 2
  seconds_per_slot: 6
  genesis_delay: 40

additional_services:
  - dora

global_log_level: debug
```

### Result

Command:

```bash
$ curl -X 'GET' \
  'http://<BN>/eth/v1/events?topics=head&topics=head_v2' \
  -H 'accept: text/event-stream'
```

so that we can subscribe both `head` and `head_v2`.

Outcome:
```
event: head
data: {"slot":"5","block":"0xbe03088c8d737d41de220c6758c0dcb920d3df9befdd7e75c6bc156695118611","state":"0x210c280f57344228104b410d1855a9ef720cdd64e242476ed7a304198cb8b76a","epoch_transition":false,"execution_optimistic":false,"previous_duty_dependent_root":"0x7851e4368487090b5a9170ad357563850c5e86a8162de7372bfa4c46133de197","current_duty_dependent_root":"0x7851e4368487090b5a9170ad357563850c5e86a8162de7372bfa4c46133de197"}

event: head_v2
data: {"version":"fulu","data":{"slot":"5","block":"0xbe03088c8d737d41de220c6758c0dcb920d3df9befdd7e75c6bc156695118611","state":"0x210c280f57344228104b410d1855a9ef720cdd64e242476ed7a304198cb8b76a","payload_status":"full","current_epoch_dependent_root":"0x7851e4368487090b5a9170ad357563850c5e86a8162de7372bfa4c46133de197","next_epoch_dependent_root":"0x7851e4368487090b5a9170ad357563850c5e86a8162de7372bfa4c46133de197","epoch_transition":false,"execution_optimistic":false}}

...


event: head
data: {"slot":"83","block":"0xd7507c7df5a5c28813e3c93bbedd4871b378e4f869571bf190f8675fa6293244","state":"0xa10e78912570d65272b3facb9cc9a0d50e209ba73fa3f8f9d9a8961b669fa431","epoch_transition":false,"execution_optimistic":false,"previous_duty_dependent_root":"0x8ab1f18715a944835e8c5a3b966126b31c3b6d282f237f8d20846f4db4cc4c4d","current_duty_dependent_root":"0xa078bc2d1264d2358f611549b6461983ed5c5d1ba58a6c8a6a5e8f6db35cad3b"}

event: head_v2
data: {"version":"gloas","data":{"slot":"83","block":"0xd7507c7df5a5c28813e3c93bbedd4871b378e4f869571bf190f8675fa6293244","state":"0xa10e78912570d65272b3facb9cc9a0d50e209ba73fa3f8f9d9a8961b669fa431","payload_status":"empty","current_epoch_dependent_root":"0x8ab1f18715a944835e8c5a3b966126b31c3b6d282f237f8d20846f4db4cc4c4d","next_epoch_dependent_root":"0xa078bc2d1264d2358f611549b6461983ed5c5d1ba58a6c8a6a5e8f6db35cad3b","epoch_transition":false,"execution_optimistic":false}}

event: head_v2
data: {"version":"gloas","data":{"slot":"83","block":"0xd7507c7df5a5c28813e3c93bbedd4871b378e4f869571bf190f8675fa6293244","state":"0xa10e78912570d65272b3facb9cc9a0d50e209ba73fa3f8f9d9a8961b669fa431","payload_status":"full","current_epoch_dependent_root":"0x8ab1f18715a944835e8c5a3b966126b31c3b6d282f237f8d20846f4db4cc4c4d","next_epoch_dependent_root":"0xa078bc2d1264d2358f611549b6461983ed5c5d1ba58a6c8a6a5e8f6db35cad3b","epoch_transition":false,"execution_optimistic":false}}
```

Notable points:
- Data of `head_v2` is properly versioned. It will give `"fulu"` if the
current fork is `fulu`.
- `head_v2` is emitted two times for same block, right after the
execution payload is received.
- `current_epoch_dependent_root` == `previous_duty_dependent_root` and
`next_epoch_dependent_root` == `current_duty_dependent_root` as
expected.

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rolfyone added a commit to Consensys/teku that referenced this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gloas api's needed in Gloas fork.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants