Skip to content

Add GET /eth/v2/debug/fork_choice endpoint#615

Open
potuz wants to merge 3 commits into
ethereum:masterfrom
potuz:forkchoice_endpoint
Open

Add GET /eth/v2/debug/fork_choice endpoint#615
potuz wants to merge 3 commits into
ethereum:masterfrom
potuz:forkchoice_endpoint

Conversation

@potuz

@potuz potuz commented Jun 2, 2026

Copy link
Copy Markdown

AI generated to match Prysm's implementation, even this description is fully AI:

Adds a Gloas (EIP-7732) aware fork choice debug endpoint at GET /eth/v2/debug/fork_choice.

Corresponds to Prysm PR OffchainLabs/prysm#16862.

Why a new version

Under EIP-7732 a single block root can have multiple fork-choice nodes depending on its payload status (the payload may be pending, revealed empty, or revealed full), and the node now tracks individual PTC (Payload Timeliness Committee) votes. The v1 response cannot represent this: it has one node per block root and no payload status. The v2 response:

  • emits one node per (block_root, payload_status) tuple,
  • adds a required payload_status field with enum pending / empty / full,
  • exposes PTC vote counts (payload_attester_count, payload_availability_yes_count, payload_data_availability_yes_count) in each node's extra_data.

/eth/v1/debug/fork_choice is left unchanged, as it remains valid for pre-Gloas chains.

Changes

  • New operation apis/debug/fork_choice.v2.yaml.
  • New NodeV2 / NodeV2ExtraData schemas in types/fork_choice.yaml, registered in beacon-node-oapi.yaml.
  • New path /eth/v2/debug/fork_choice registered in beacon-node-oapi.yaml.
  • CHANGES.md row added.

🤖 Generated with Claude Code

Adds a Gloas (EIP-7732) aware fork choice debug endpoint. Unlike v1, it
emits one node per (block_root, payload_status) tuple, adds a
payload_status field (pending/empty/full), and exposes individual PTC
vote counts in each node's extra_data.

v1 is left unchanged as it remains valid for pre-Gloas chains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread types/fork_choice.yaml
NodeV2ExtraData:
type: object
additionalProperties: true
description: 'Optional extra data that clients may provide, which could differ from client to client. The properties documented below are commonly provided; clients may add their own.'

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.

why would we put this into extra_data, this seems to be a free form field which clients can populate as they would like, so defining that in the spec seems the opposite of what we want for it

type: object
description: "Debugging context of fork choice"
required: [justified_checkpoint, finalized_checkpoint, fork_choice_nodes]
properties:

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.

if we got with a v2 endpoint, this should be wrapped into a data container to follow the standard response format

Comment thread CHANGES.md
| [#590](https://github.com/ethereum/beacon-APIs/pull/590) `head_v2 EVENT` added | | | | | |
| [#590](https://github.com/ethereum/beacon-APIs/pull/590) `head EVENT` deprecated | | | | | |
| [#598](https://github.com/ethereum/beacon-APIs/pull/598) `fast_confirmation EVENT` added | | | | | |
| [#615](https://github.com/ethereum/beacon-APIs/pull/615) `GET /eth/v2/debug/fork_choice` added | | | | | |

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.

in case we go with v2, should also deprecate the previous endpoint so we can eventually remove it

@nflaig nflaig added the Gloas api's needed in Gloas fork. label Jun 2, 2026
@rolfyone rolfyone mentioned this pull request Jun 8, 2026
2 tasks
@rolfyone

rolfyone commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

It sounds like all the extra data fields are pretty much included by all clients:
state_root, justified_root, unrealised_justified_epoch, unrealized_justified_root, unrealised_finalized_epoch, unrealized_finalized_root
we should probably take these fields and put them into the main object. The extra_data object is still valuable, but this could then start empty in case we need it later, and be optional maybe...

@Savid

Savid commented Jun 15, 2026

Copy link
Copy Markdown
Member

does it make sense to still keep one forkchoice node per slot. something like this;

pre Gloas

similar to v1 forkchoice nodes

{
  "slot": "6",
  "block_root": "0xc44ec78e1f44a06cbd383b7e89f7ada54cccb5420d9ef6eb95beb46bae7d4c5e",
  "parent_root": "0x59c7d6e88e0cdedd2dec59b635732b6b1eb378f684bb6d0b928035da9ff3b97c",
  "weight": "307200000000",
  "validity": "valid",
  "execution_block_hash": "0xf73a47c1f47ae3445e7b5d2b1605f619a106699df639f89f90327bce0bea6ec5",
  "gas_limit": "150000000",
  "justified_epoch": "0",
  "finalized_epoch": "0",
  "extra_data": {
    "balance": "307200000000",
    "target_root": "0x63d3084d2a0ee8c05c2b2bdd13fc0ffd580bef7838800dcf8179c3ad187f2832",
    "unrealized_justified_epoch": "0",
    "unrealized_finalized_epoch": "0",
    "block_seen_ms": "1781071852070"
  }
}

post Gloas

{
  "slot": "257",
  "block_root": "0x9c63a225c893e7cc7d7b71ee1c4211d9f16515e13d75ae460f68aca64fe6771a",
  "parent_root": "0x5abdb01eee2237f5bd3718074f3a34212c12036450f589049e34ef738ad5f478",
  "parent_payload_status": "full",
  "payload_status": "full",
  "weight": "24883200000000",
  "validity": "valid",
  "execution_block_hash": "0xfee367b74767f5fa36598624f458aa3b246502405bcb16213c5072816e7575db",
  "gas_limit": "150000000",
  "justified_epoch": "7",
  "finalized_epoch": "6",
  "payload_attester_count": "24",
  "payload_availability_yes_count": "24",
  "payload_data_availability_yes_count": "24",
  "empty_weight": "0",
  "full_weight": "24883200000000",
  "extra_data": {
    "balance": "0",
    "target_root": "0x5abdb01eee2237f5bd3718074f3a34212c12036450f589049e34ef738ad5f478",
    "unrealized_justified_epoch": "7",
    "unrealized_finalized_epoch": "6",
    "block_seen_ms": "1781137466056",
    "payload_seen_ms": "1781137466255"
  }
}

I had some initial thoughts dumped here

@rolfyone

Copy link
Copy Markdown
Contributor

i think im not set on the v1 output, most of the extra data will be needed for all clients, so having it at top level to me makes sense.

@rolfyone

Copy link
Copy Markdown
Contributor
    {
        "payload_status": "pending",
        "slot": "29",
        "block_root": "0xac33f98a42b42c34c467237d450605ff5f1f6741d282cac079012983ae98b2c1",
        "parent_root": "0x853f3451dd50f2061f017cf07143788685409add3254a07fec1ebf98bd3dd87e",
        "weight": "128000000000",
        "validity": "valid",
        "execution_block_hash": "0xa81415caa4f1635e6fd664b5c26747dd662e77318096435a25341217ef019f1f",
        "payload_attester_count": "16",
        "payload_availability_yes_count": "16",
        "payload_data_availability_yes_count": "16",
        "state_root": "0x36b5cb5cc93603dc3eeaa12548441d212ad960becfcf0061adf588d6686d91e0",
        "justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_justified_epoch": "2",
        "unrealized_justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_finalized_epoch": "0",
        "unrealized_finalized_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "extra_data": {}
      },
      {
        "payload_status": "empty",
        "slot": "29",
        "block_root": "0xac33f98a42b42c34c467237d450605ff5f1f6741d282cac079012983ae98b2c1",
        "parent_root": "0x853f3451dd50f2061f017cf07143788685409add3254a07fec1ebf98bd3dd87e",
        "weight": "0",
        "validity": "valid",
        "execution_block_hash": "0xa81415caa4f1635e6fd664b5c26747dd662e77318096435a25341217ef019f1f",
        "payload_attester_count": "16",
        "payload_availability_yes_count": "16",
        "payload_data_availability_yes_count": "16",
        "state_root": "0x36b5cb5cc93603dc3eeaa12548441d212ad960becfcf0061adf588d6686d91e0",
        "justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_justified_epoch": "2",
        "unrealized_justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_finalized_epoch": "0",
        "unrealized_finalized_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "extra_data": {}
      },
      {
        "payload_status": "full",
        "slot": "29",
        "block_root": "0xac33f98a42b42c34c467237d450605ff5f1f6741d282cac079012983ae98b2c1",
        "parent_root": "0x853f3451dd50f2061f017cf07143788685409add3254a07fec1ebf98bd3dd87e",
        "weight": "0",
        "validity": "valid",
        "execution_block_hash": "0xef47d638b77669968e41b9224eb1d4a52870d93b80de09cfc8cf67f2c81f1282",
        "payload_attester_count": "16",
        "payload_availability_yes_count": "16",
        "payload_data_availability_yes_count": "16",
        "state_root": "0x36b5cb5cc93603dc3eeaa12548441d212ad960becfcf0061adf588d6686d91e0",
        "justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_justified_epoch": "2",
        "unrealized_justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_finalized_epoch": "0",
        "unrealized_finalized_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "extra_data": {}
      },

Sample from my implementation... I grabbed all of a single slot, which has 3 entries at least in our implementation.

@Savid

Savid commented Jun 17, 2026

Copy link
Copy Markdown
Member
    {
        "payload_status": "pending",
        "slot": "29",
        "block_root": "0xac33f98a42b42c34c467237d450605ff5f1f6741d282cac079012983ae98b2c1",
        "parent_root": "0x853f3451dd50f2061f017cf07143788685409add3254a07fec1ebf98bd3dd87e",
        "weight": "128000000000",
        "validity": "valid",
        "execution_block_hash": "0xa81415caa4f1635e6fd664b5c26747dd662e77318096435a25341217ef019f1f",
        "payload_attester_count": "16",
        "payload_availability_yes_count": "16",
        "payload_data_availability_yes_count": "16",
        "state_root": "0x36b5cb5cc93603dc3eeaa12548441d212ad960becfcf0061adf588d6686d91e0",
        "justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_justified_epoch": "2",
        "unrealized_justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_finalized_epoch": "0",
        "unrealized_finalized_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "extra_data": {}
      },
      {
        "payload_status": "empty",
        "slot": "29",
        "block_root": "0xac33f98a42b42c34c467237d450605ff5f1f6741d282cac079012983ae98b2c1",
        "parent_root": "0x853f3451dd50f2061f017cf07143788685409add3254a07fec1ebf98bd3dd87e",
        "weight": "0",
        "validity": "valid",
        "execution_block_hash": "0xa81415caa4f1635e6fd664b5c26747dd662e77318096435a25341217ef019f1f",
        "payload_attester_count": "16",
        "payload_availability_yes_count": "16",
        "payload_data_availability_yes_count": "16",
        "state_root": "0x36b5cb5cc93603dc3eeaa12548441d212ad960becfcf0061adf588d6686d91e0",
        "justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_justified_epoch": "2",
        "unrealized_justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_finalized_epoch": "0",
        "unrealized_finalized_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "extra_data": {}
      },
      {
        "payload_status": "full",
        "slot": "29",
        "block_root": "0xac33f98a42b42c34c467237d450605ff5f1f6741d282cac079012983ae98b2c1",
        "parent_root": "0x853f3451dd50f2061f017cf07143788685409add3254a07fec1ebf98bd3dd87e",
        "weight": "0",
        "validity": "valid",
        "execution_block_hash": "0xef47d638b77669968e41b9224eb1d4a52870d93b80de09cfc8cf67f2c81f1282",
        "payload_attester_count": "16",
        "payload_availability_yes_count": "16",
        "payload_data_availability_yes_count": "16",
        "state_root": "0x36b5cb5cc93603dc3eeaa12548441d212ad960becfcf0061adf588d6686d91e0",
        "justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_justified_epoch": "2",
        "unrealized_justified_root": "0xb2e72b53d1d2ac5ce7645c079c309e04a908e3906b389cfa2f0eb1f77be395d7",
        "unrealised_finalized_epoch": "0",
        "unrealized_finalized_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "extra_data": {}
      },

Sample from my implementation... I grabbed all of a single slot, which has 3 entries at least in our implementation.

i'm all for bringing extra data down a level!

two questions;

  • Are we still for 2-3 row tuple for each slot?
  • I can't tell if a child slot forked from the parents empty or full payload right?

@rolfyone

Copy link
Copy Markdown
Contributor

i'm all for bringing extra data down a level!

two questions;

  • Are we still for 2-3 row tuple for each slot?
  • I can't tell if a child slot forked from the parents empty or full payload right?

because it's a debug endpoint, thats our internal representation... so theres 3 nodes in our memory which is why we're outputting 3...

internally its the weights that dictate direction, o the above example has all the weight in the 'pending' route.

This is why it might be useful to keep simple and just output our data structures though, because that would help us debugging what's going on. The more we abstract the structures away, the more interpretation is needed...

Pre-gloas is simpler because there's not multiple states per node...

@Savid

Savid commented Jun 17, 2026

Copy link
Copy Markdown
Member

makes sense, i think I got confused on the prysm payload as pending tuple looks like it stores the bid.block_hash while it looks like teku's stores the bid.parent_block_hash. This solves solves the other question i had

@tbenr

tbenr commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

the only thing that pre-gloas teku uses payload status to be pending and not full. We can argue both ways but we can runtime patch it (as paul's pr does now) to be full.

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