Skip to content

fix: payload status on fork chocie init - #8987

Closed
ensi321 wants to merge 2 commits into
unstablefrom
nc/anchor-state-fix
Closed

fix: payload status on fork chocie init#8987
ensi321 wants to merge 2 commits into
unstablefrom
nc/anchor-state-fix

Conversation

@ensi321

@ensi321 ensi321 commented Mar 5, 2026

Copy link
Copy Markdown
Member

No description provided.

@ensi321
ensi321 requested a review from a team as a code owner March 5, 2026 00:41
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refines the initialization process for fork choice by introducing a standardized method for calculating the payloadStatus. By replacing a direct conditional assignment with a call to getCheckpointPayloadStatus, the change ensures accurate and consistent payload status determination for both finalized and unfinalized states, improving the reliability of the beacon node's fork choice mechanism.

Highlights

  • Payload Status Calculation: Updated the logic for determining the payloadStatus during fork choice initialization to use a dedicated getCheckpointPayloadStatus function instead of a conditional check.
  • Code Refinement: Addressed a TODO comment regarding the uncertainty of payloadStatus for post-Gloas checkpoints by implementing a more robust calculation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/beacon-node/src/chain/forkChoice/index.ts
    • Updated payloadStatus assignment in initializeForkChoiceFromFinalizedState to use getCheckpointPayloadStatus.
    • Updated payloadStatus assignment in initializeForkChoiceFromUnfinalizedState to use getCheckpointPayloadStatus.
Activity
  • No activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request aims to fix the payload status during fork choice initialization. However, it introduces a critical logic flaw for Gloas (ePBS) forks. The ProtoArray only creates PENDING and EMPTY variants for Gloas blocks during initialization, but the new logic can set the ForkChoiceStore status to FULL. This mismatch causes core fork choice functions to throw errors, leading to a denial of service for block processing and API requests. It is recommended to revert to using PayloadStatus.PENDING for Gloas blocks during initialization to maintain consistency with the ProtoArray state. Additionally, the modification for initializing from an unfinalized state introduces an issue by incorrectly using a function designed for checkpoint slots on a block that may not be at a checkpoint, potentially leading to an incorrect fork choice state.

dataAvailabilityStatus: DataAvailabilityStatus.PreData,
payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY?
payloadStatus: getCheckpointPayloadStatus(unfinalizedState, computeEpochAtSlot(blockHeader.slot)),
builderIndex: isForkPostGloas

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.

security-critical critical

Initializing payloadStatus with the result of getCheckpointPayloadStatus for an unfinalized state can lead to a MISSING_PROTO_ARRAY_BLOCK error if the status is FULL. For Gloas blocks, the ProtoArray is initialized with PENDING and EMPTY variants only. Using FULL here will cause a mismatch that prevents the fork choice from correctly identifying the head or walking the block tree. Additionally, the function getCheckpointPayloadStatus is designed for checkpoint slots but is being used here for the headBlock, which is not necessarily at a checkpoint slot. This leads to incorrectly using the payload status of the epoch's checkpoint block for the headBlock. To fix this, you should determine the payload status for blockHeader.slot directly, by checking unfinalizedState.executionPayloadAvailability for the blockHeader.slot.

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.

Agreed on this risk. I reviewed it and confirmed this is actionable: using getCheckpointPayloadStatus(...) for the anchor/head init path can produce a FULL variant mismatch and is semantically wrong for non-checkpoint head slots.\n\nI left the same guidance to the author here: https://github.com/ChainSafe/lodestar/pull/8987#issuecomment-4003698056\n\nRecommended fix remains to keep anchor/head init at PENDING for post-Gloas and limit getCheckpointPayloadStatus usage to checkpoint-context status computation only.

dataAvailabilityStatus: DataAvailabilityStatus.PreData,
payloadStatus: isForkPostGloas ? PayloadStatus.PENDING : PayloadStatus.FULL, // TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY?
payloadStatus: getCheckpointPayloadStatus(state, checkpoint.epoch),
builderIndex: isForkPostGloas ? (state as CachedBeaconStateGloas).latestExecutionPayloadBid.builderIndex : null,

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.

security-critical critical

The pull request modifies the initialization of payloadStatus in initializeForkChoiceFromFinalizedState and initializeForkChoiceFromUnfinalizedState to use getCheckpointPayloadStatus(state, checkpoint.epoch). For Gloas (ePBS) forks, getCheckpointPayloadStatus can return PayloadStatus.FULL if the execution payload is marked as available in the state.

However, ProtoArray.onBlock, which is called during fork choice initialization (lines 134 and 305-308), only creates PENDING and EMPTY variants for Gloas blocks. It does not create the FULL variant. The FULL variant is only created later via onExecutionPayload when the payload actually arrives.

By initializing the ForkChoiceStore with payloadStatus = FULL for the finalized or justified checkpoints while ProtoArray lacks the corresponding FULL variant, several critical operations will fail:

  • getJustifiedBlock() and getFinalizedBlock() will throw a MISSING_PROTO_ARRAY_BLOCK error.
  • isFinalizedRootOrDescendant() will fail to walk back to the finalized block if it attempts to match the FULL variant (e.g., when a child block's parentBlockHash refers to the execution payload hash).
  • findHead() will throw INVALID_BEST_NODE if it cannot verify the finalized root relationship.

This mismatch results in a complete denial of service for the node's fork choice processing and breaks related API endpoints.

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.

+1 from my side as reviewer. This should not initialize anchor/head payload status from checkpoint-epoch availability for the reasons above (can incorrectly choose FULL and mismatch proto-array variants).\n\nAuthor guidance is captured here: https://github.com/ChainSafe/lodestar/pull/8987#issuecomment-4003698056\n\nOnce this is wired to the correct head-slot semantics, the concern should be resolved.

@github-actions

github-actions Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: 48692d7 Previous: 477bc36 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 1.1157 ms/op 1.1031 ms/op 1.01
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 38.225 us/op 37.711 us/op 1.01
BLS verify - blst 1.2495 ms/op 755.06 us/op 1.65
BLS verifyMultipleSignatures 3 - blst 2.2315 ms/op 1.1720 ms/op 1.90
BLS verifyMultipleSignatures 8 - blst 1.8872 ms/op 1.6255 ms/op 1.16
BLS verifyMultipleSignatures 32 - blst 6.2514 ms/op 4.8001 ms/op 1.30
BLS verifyMultipleSignatures 64 - blst 11.087 ms/op 8.8855 ms/op 1.25
BLS verifyMultipleSignatures 128 - blst 19.040 ms/op 17.016 ms/op 1.12
BLS deserializing 10000 signatures 689.86 ms/op 677.32 ms/op 1.02
BLS deserializing 100000 signatures 6.9325 s/op 6.7961 s/op 1.02
BLS verifyMultipleSignatures - same message - 3 - blst 1.4125 ms/op 899.02 us/op 1.57
BLS verifyMultipleSignatures - same message - 8 - blst 1.6404 ms/op 994.29 us/op 1.65
BLS verifyMultipleSignatures - same message - 32 - blst 1.6974 ms/op 1.6605 ms/op 1.02
BLS verifyMultipleSignatures - same message - 64 - blst 2.6113 ms/op 2.5600 ms/op 1.02
BLS verifyMultipleSignatures - same message - 128 - blst 4.3753 ms/op 4.3014 ms/op 1.02
BLS aggregatePubkeys 32 - blst 19.402 us/op 19.113 us/op 1.02
BLS aggregatePubkeys 128 - blst 69.632 us/op 68.861 us/op 1.01
getSlashingsAndExits - default max 66.467 us/op 65.305 us/op 1.02
getSlashingsAndExits - 2k 347.99 us/op 312.07 us/op 1.12
isKnown best case - 1 super set check 217.00 ns/op 192.00 ns/op 1.13
isKnown normal case - 2 super set checks 206.00 ns/op 187.00 ns/op 1.10
isKnown worse case - 16 super set checks 208.00 ns/op 188.00 ns/op 1.11
validate api signedAggregateAndProof - struct 1.5373 ms/op 1.3261 ms/op 1.16
validate gossip signedAggregateAndProof - struct 1.4617 ms/op 1.3408 ms/op 1.09
batch validate gossip attestation - vc 640000 - chunk 32 123.40 us/op 113.10 us/op 1.09
batch validate gossip attestation - vc 640000 - chunk 64 112.70 us/op 105.33 us/op 1.07
batch validate gossip attestation - vc 640000 - chunk 128 106.02 us/op 94.713 us/op 1.12
batch validate gossip attestation - vc 640000 - chunk 256 98.160 us/op 90.897 us/op 1.08
bytes32 toHexString 381.00 ns/op 351.00 ns/op 1.09
bytes32 Buffer.toString(hex) 265.00 ns/op 239.00 ns/op 1.11
bytes32 Buffer.toString(hex) from Uint8Array 360.00 ns/op 321.00 ns/op 1.12
bytes32 Buffer.toString(hex) + 0x 263.00 ns/op 233.00 ns/op 1.13
Return object 10000 times 0.26160 ns/op 0.21960 ns/op 1.19
Throw Error 10000 times 4.3155 us/op 4.1714 us/op 1.03
toHex 132.88 ns/op 128.38 ns/op 1.04
Buffer.from 139.66 ns/op 129.95 ns/op 1.07
shared Buffer 76.142 ns/op 77.687 ns/op 0.98
fastMsgIdFn sha256 / 200 bytes 1.9890 us/op 1.8290 us/op 1.09
fastMsgIdFn h32 xxhash / 200 bytes 226.00 ns/op 189.00 ns/op 1.20
fastMsgIdFn h64 xxhash / 200 bytes 299.00 ns/op 265.00 ns/op 1.13
fastMsgIdFn sha256 / 1000 bytes 6.0820 us/op 5.8000 us/op 1.05
fastMsgIdFn h32 xxhash / 1000 bytes 321.00 ns/op 279.00 ns/op 1.15
fastMsgIdFn h64 xxhash / 1000 bytes 312.00 ns/op 298.00 ns/op 1.05
fastMsgIdFn sha256 / 10000 bytes 52.221 us/op 52.195 us/op 1.00
fastMsgIdFn h32 xxhash / 10000 bytes 1.4930 us/op 1.3800 us/op 1.08
fastMsgIdFn h64 xxhash / 10000 bytes 913.00 ns/op 916.00 ns/op 1.00
send data - 1000 256B messages 4.9765 ms/op 4.4534 ms/op 1.12
send data - 1000 512B messages 4.6370 ms/op 4.5455 ms/op 1.02
send data - 1000 1024B messages 5.1185 ms/op 4.9705 ms/op 1.03
send data - 1000 1200B messages 5.7935 ms/op 4.6508 ms/op 1.25
send data - 1000 2048B messages 6.5610 ms/op 5.3110 ms/op 1.24
send data - 1000 4096B messages 7.2287 ms/op 6.5081 ms/op 1.11
send data - 1000 16384B messages 29.953 ms/op 50.739 ms/op 0.59
send data - 1000 65536B messages 196.47 ms/op 98.954 ms/op 1.99
enrSubnets - fastDeserialize 64 bits 939.00 ns/op 871.00 ns/op 1.08
enrSubnets - ssz BitVector 64 bits 372.00 ns/op 327.00 ns/op 1.14
enrSubnets - fastDeserialize 4 bits 131.00 ns/op 124.00 ns/op 1.06
enrSubnets - ssz BitVector 4 bits 362.00 ns/op 326.00 ns/op 1.11
prioritizePeers score -10:0 att 32-0.1 sync 2-0 256.48 us/op 231.25 us/op 1.11
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 258.33 us/op 252.51 us/op 1.02
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 377.52 us/op 357.79 us/op 1.06
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 704.72 us/op 691.50 us/op 1.02
prioritizePeers score 0:0 att 64-1 sync 4-1 930.47 us/op 854.93 us/op 1.09
array of 16000 items push then shift 1.6383 us/op 1.5783 us/op 1.04
LinkedList of 16000 items push then shift 7.5740 ns/op 7.3010 ns/op 1.04
array of 16000 items push then pop 78.165 ns/op 75.224 ns/op 1.04
LinkedList of 16000 items push then pop 7.3290 ns/op 7.0070 ns/op 1.05
array of 24000 items push then shift 2.4699 us/op 2.3430 us/op 1.05
LinkedList of 24000 items push then shift 7.6020 ns/op 7.4020 ns/op 1.03
array of 24000 items push then pop 108.95 ns/op 104.61 ns/op 1.04
LinkedList of 24000 items push then pop 7.4720 ns/op 7.0750 ns/op 1.06
intersect bitArray bitLen 8 5.7090 ns/op 5.7460 ns/op 0.99
intersect array and set length 8 34.310 ns/op 32.759 ns/op 1.05
intersect bitArray bitLen 128 28.703 ns/op 28.083 ns/op 1.02
intersect array and set length 128 553.20 ns/op 539.36 ns/op 1.03
bitArray.getTrueBitIndexes() bitLen 128 1.2140 us/op 1.0240 us/op 1.19
bitArray.getTrueBitIndexes() bitLen 248 1.9410 us/op 1.7950 us/op 1.08
bitArray.getTrueBitIndexes() bitLen 512 3.8650 us/op 3.6850 us/op 1.05
Full columns - reconstruct all 6 blobs 203.07 us/op 226.00 us/op 0.90
Full columns - reconstruct half of the blobs out of 6 124.91 us/op 121.61 us/op 1.03
Full columns - reconstruct single blob out of 6 32.526 us/op 46.604 us/op 0.70
Half columns - reconstruct all 6 blobs 275.90 ms/op 266.56 ms/op 1.04
Half columns - reconstruct half of the blobs out of 6 135.66 ms/op 129.70 ms/op 1.05
Half columns - reconstruct single blob out of 6 50.954 ms/op 50.301 ms/op 1.01
Full columns - reconstruct all 10 blobs 334.89 us/op 300.94 us/op 1.11
Full columns - reconstruct half of the blobs out of 10 173.97 us/op 153.69 us/op 1.13
Full columns - reconstruct single blob out of 10 33.654 us/op 33.941 us/op 0.99
Half columns - reconstruct all 10 blobs 464.82 ms/op 445.83 ms/op 1.04
Half columns - reconstruct half of the blobs out of 10 238.52 ms/op 219.87 ms/op 1.08
Half columns - reconstruct single blob out of 10 52.590 ms/op 47.417 ms/op 1.11
Full columns - reconstruct all 20 blobs 670.28 us/op 694.13 us/op 0.97
Full columns - reconstruct half of the blobs out of 20 334.79 us/op 309.85 us/op 1.08
Full columns - reconstruct single blob out of 20 33.150 us/op 30.946 us/op 1.07
Half columns - reconstruct all 20 blobs 943.74 ms/op 862.00 ms/op 1.09
Half columns - reconstruct half of the blobs out of 20 474.29 ms/op 460.63 ms/op 1.03
Half columns - reconstruct single blob out of 20 53.538 ms/op 50.033 ms/op 1.07
Set add up to 64 items then delete first 2.1366 us/op 2.0339 us/op 1.05
OrderedSet add up to 64 items then delete first 3.1134 us/op 3.0505 us/op 1.02
Set add up to 64 items then delete last 2.3714 us/op 2.2226 us/op 1.07
OrderedSet add up to 64 items then delete last 3.4359 us/op 3.2875 us/op 1.05
Set add up to 64 items then delete middle 2.3886 us/op 2.2571 us/op 1.06
OrderedSet add up to 64 items then delete middle 5.0447 us/op 4.9963 us/op 1.01
Set add up to 128 items then delete first 4.8771 us/op 4.8476 us/op 1.01
OrderedSet add up to 128 items then delete first 7.4545 us/op 7.3804 us/op 1.01
Set add up to 128 items then delete last 4.8007 us/op 4.5824 us/op 1.05
OrderedSet add up to 128 items then delete last 6.8909 us/op 6.7208 us/op 1.03
Set add up to 128 items then delete middle 4.6491 us/op 4.4768 us/op 1.04
OrderedSet add up to 128 items then delete middle 13.619 us/op 13.357 us/op 1.02
Set add up to 256 items then delete first 10.636 us/op 10.277 us/op 1.03
OrderedSet add up to 256 items then delete first 16.063 us/op 15.789 us/op 1.02
Set add up to 256 items then delete last 9.4764 us/op 9.4404 us/op 1.00
OrderedSet add up to 256 items then delete last 14.688 us/op 14.573 us/op 1.01
Set add up to 256 items then delete middle 9.7290 us/op 9.2752 us/op 1.05
OrderedSet add up to 256 items then delete middle 43.014 us/op 41.717 us/op 1.03
pass gossip attestations to forkchoice per slot 567.48 us/op 496.29 us/op 1.14
computeDeltas 1400000 validators 0% inactive 14.772 ms/op 14.388 ms/op 1.03
computeDeltas 1400000 validators 10% inactive 14.708 ms/op 13.384 ms/op 1.10
computeDeltas 1400000 validators 20% inactive 12.727 ms/op 12.485 ms/op 1.02
computeDeltas 1400000 validators 50% inactive 9.9156 ms/op 9.7281 ms/op 1.02
computeDeltas 2100000 validators 0% inactive 22.265 ms/op 21.727 ms/op 1.02
computeDeltas 2100000 validators 10% inactive 20.800 ms/op 19.919 ms/op 1.04
computeDeltas 2100000 validators 20% inactive 19.335 ms/op 18.546 ms/op 1.04
computeDeltas 2100000 validators 50% inactive 14.899 ms/op 14.445 ms/op 1.03
altair processAttestation - 250000 vs - 7PWei normalcase 2.0689 ms/op 1.9744 ms/op 1.05
altair processAttestation - 250000 vs - 7PWei worstcase 3.5499 ms/op 2.7995 ms/op 1.27
altair processAttestation - setStatus - 1/6 committees join 130.53 us/op 120.18 us/op 1.09
altair processAttestation - setStatus - 1/3 committees join 243.74 us/op 236.11 us/op 1.03
altair processAttestation - setStatus - 1/2 committees join 346.00 us/op 332.49 us/op 1.04
altair processAttestation - setStatus - 2/3 committees join 447.89 us/op 418.80 us/op 1.07
altair processAttestation - setStatus - 4/5 committees join 631.29 us/op 588.53 us/op 1.07
altair processAttestation - setStatus - 100% committees join 725.29 us/op 686.23 us/op 1.06
altair processBlock - 250000 vs - 7PWei normalcase 5.0396 ms/op 3.6433 ms/op 1.38
altair processBlock - 250000 vs - 7PWei normalcase hashState 22.612 ms/op 17.201 ms/op 1.31
altair processBlock - 250000 vs - 7PWei worstcase 25.492 ms/op 23.963 ms/op 1.06
altair processBlock - 250000 vs - 7PWei worstcase hashState 62.647 ms/op 52.188 ms/op 1.20
phase0 processBlock - 250000 vs - 7PWei normalcase 1.7487 ms/op 1.4775 ms/op 1.18
phase0 processBlock - 250000 vs - 7PWei worstcase 21.078 ms/op 18.635 ms/op 1.13
altair processEth1Data - 250000 vs - 7PWei normalcase 440.40 us/op 384.92 us/op 1.14
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 10.188 us/op 9.1500 us/op 1.11
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 47.301 us/op 35.387 us/op 1.34
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 18.781 us/op 10.944 us/op 1.72
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 11.534 us/op 9.6730 us/op 1.19
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 227.75 us/op 170.49 us/op 1.34
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 2.2040 ms/op 1.8069 ms/op 1.22
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 2.9287 ms/op 2.3409 ms/op 1.25
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 2.9028 ms/op 2.3016 ms/op 1.26
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 5.3571 ms/op 4.4147 ms/op 1.21
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 3.2779 ms/op 2.7398 ms/op 1.20
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 5.8521 ms/op 4.8218 ms/op 1.21
Tree 40 250000 create 431.83 ms/op 367.84 ms/op 1.17
Tree 40 250000 get(125000) 137.87 ns/op 134.66 ns/op 1.02
Tree 40 250000 set(125000) 1.3165 us/op 1.2894 us/op 1.02
Tree 40 250000 toArray() 20.878 ms/op 14.657 ms/op 1.42
Tree 40 250000 iterate all - toArray() + loop 22.605 ms/op 12.758 ms/op 1.77
Tree 40 250000 iterate all - get(i) 50.915 ms/op 42.779 ms/op 1.19
Array 250000 create 2.8576 ms/op 2.4302 ms/op 1.18
Array 250000 clone - spread 925.03 us/op 800.28 us/op 1.16
Array 250000 get(125000) 0.39900 ns/op 0.35400 ns/op 1.13
Array 250000 set(125000) 0.41400 ns/op 0.35100 ns/op 1.18
Array 250000 iterate all - loop 65.126 us/op 61.363 us/op 1.06
phase0 afterProcessEpoch - 250000 vs - 7PWei 46.198 ms/op 41.243 ms/op 1.12
Array.fill - length 1000000 4.1487 ms/op 2.9269 ms/op 1.42
Array push - length 1000000 11.977 ms/op 9.6946 ms/op 1.24
Array.get 0.22871 ns/op 0.21770 ns/op 1.05
Uint8Array.get 0.22903 ns/op 0.22230 ns/op 1.03
phase0 beforeProcessEpoch - 250000 vs - 7PWei 14.870 ms/op 18.129 ms/op 0.82
altair processEpoch - mainnet_e81889 264.84 ms/op 250.00 ms/op 1.06
mainnet_e81889 - altair beforeProcessEpoch 20.192 ms/op 19.477 ms/op 1.04
mainnet_e81889 - altair processJustificationAndFinalization 6.7250 us/op 6.0660 us/op 1.11
mainnet_e81889 - altair processInactivityUpdates 3.8779 ms/op 3.8948 ms/op 1.00
mainnet_e81889 - altair processRewardsAndPenalties 20.523 ms/op 18.673 ms/op 1.10
mainnet_e81889 - altair processRegistryUpdates 647.00 ns/op 655.00 ns/op 0.99
mainnet_e81889 - altair processSlashings 174.00 ns/op 172.00 ns/op 1.01
mainnet_e81889 - altair processEth1DataReset 164.00 ns/op 169.00 ns/op 0.97
mainnet_e81889 - altair processEffectiveBalanceUpdates 6.8963 ms/op 5.5222 ms/op 1.25
mainnet_e81889 - altair processSlashingsReset 953.00 ns/op 841.00 ns/op 1.13
mainnet_e81889 - altair processRandaoMixesReset 1.4860 us/op 1.1960 us/op 1.24
mainnet_e81889 - altair processHistoricalRootsUpdate 173.00 ns/op 167.00 ns/op 1.04
mainnet_e81889 - altair processParticipationFlagUpdates 511.00 ns/op 516.00 ns/op 0.99
mainnet_e81889 - altair processSyncCommitteeUpdates 133.00 ns/op 138.00 ns/op 0.96
mainnet_e81889 - altair afterProcessEpoch 46.024 ms/op 45.360 ms/op 1.01
capella processEpoch - mainnet_e217614 853.51 ms/op 818.53 ms/op 1.04
mainnet_e217614 - capella beforeProcessEpoch 71.413 ms/op 63.983 ms/op 1.12
mainnet_e217614 - capella processJustificationAndFinalization 6.3920 us/op 5.8710 us/op 1.09
mainnet_e217614 - capella processInactivityUpdates 17.335 ms/op 16.564 ms/op 1.05
mainnet_e217614 - capella processRewardsAndPenalties 108.17 ms/op 105.67 ms/op 1.02
mainnet_e217614 - capella processRegistryUpdates 6.0490 us/op 6.2260 us/op 0.97
mainnet_e217614 - capella processSlashings 175.00 ns/op 172.00 ns/op 1.02
mainnet_e217614 - capella processEth1DataReset 190.00 ns/op 176.00 ns/op 1.08
mainnet_e217614 - capella processEffectiveBalanceUpdates 15.160 ms/op 15.644 ms/op 0.97
mainnet_e217614 - capella processSlashingsReset 964.00 ns/op 830.00 ns/op 1.16
mainnet_e217614 - capella processRandaoMixesReset 1.2200 us/op 1.1620 us/op 1.05
mainnet_e217614 - capella processHistoricalRootsUpdate 191.00 ns/op 174.00 ns/op 1.10
mainnet_e217614 - capella processParticipationFlagUpdates 518.00 ns/op 536.00 ns/op 0.97
mainnet_e217614 - capella afterProcessEpoch 121.74 ms/op 119.41 ms/op 1.02
phase0 processEpoch - mainnet_e58758 263.61 ms/op 246.19 ms/op 1.07
mainnet_e58758 - phase0 beforeProcessEpoch 53.698 ms/op 54.304 ms/op 0.99
mainnet_e58758 - phase0 processJustificationAndFinalization 6.1800 us/op 5.6350 us/op 1.10
mainnet_e58758 - phase0 processRewardsAndPenalties 19.610 ms/op 19.085 ms/op 1.03
mainnet_e58758 - phase0 processRegistryUpdates 2.8840 us/op 2.9150 us/op 0.99
mainnet_e58758 - phase0 processSlashings 234.00 ns/op 174.00 ns/op 1.34
mainnet_e58758 - phase0 processEth1DataReset 175.00 ns/op 172.00 ns/op 1.02
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 948.25 us/op 1.7253 ms/op 0.55
mainnet_e58758 - phase0 processSlashingsReset 947.00 ns/op 945.00 ns/op 1.00
mainnet_e58758 - phase0 processRandaoMixesReset 1.0870 us/op 1.1310 us/op 0.96
mainnet_e58758 - phase0 processHistoricalRootsUpdate 184.00 ns/op 198.00 ns/op 0.93
mainnet_e58758 - phase0 processParticipationRecordUpdates 925.00 ns/op 907.00 ns/op 1.02
mainnet_e58758 - phase0 afterProcessEpoch 36.657 ms/op 36.566 ms/op 1.00
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.3387 ms/op 1.3287 ms/op 1.01
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 2.1787 ms/op 2.1506 ms/op 1.01
altair processInactivityUpdates - 250000 normalcase 13.662 ms/op 14.254 ms/op 0.96
altair processInactivityUpdates - 250000 worstcase 14.682 ms/op 16.642 ms/op 0.88
phase0 processRegistryUpdates - 250000 normalcase 4.9440 us/op 4.9450 us/op 1.00
phase0 processRegistryUpdates - 250000 badcase_full_deposits 286.18 us/op 242.96 us/op 1.18
phase0 processRegistryUpdates - 250000 worstcase 0.5 79.596 ms/op 69.964 ms/op 1.14
altair processRewardsAndPenalties - 250000 normalcase 22.051 ms/op 17.964 ms/op 1.23
altair processRewardsAndPenalties - 250000 worstcase 20.067 ms/op 18.200 ms/op 1.10
phase0 getAttestationDeltas - 250000 normalcase 6.2130 ms/op 6.7863 ms/op 0.92
phase0 getAttestationDeltas - 250000 worstcase 6.2552 ms/op 6.6329 ms/op 0.94
phase0 processSlashings - 250000 worstcase 117.01 us/op 87.515 us/op 1.34
altair processSyncCommitteeUpdates - 250000 11.451 ms/op 11.046 ms/op 1.04
BeaconState.hashTreeRoot - No change 197.00 ns/op 198.00 ns/op 0.99
BeaconState.hashTreeRoot - 1 full validator 89.089 us/op 67.771 us/op 1.31
BeaconState.hashTreeRoot - 32 full validator 953.46 us/op 885.53 us/op 1.08
BeaconState.hashTreeRoot - 512 full validator 8.0957 ms/op 7.9080 ms/op 1.02
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 107.24 us/op 113.22 us/op 0.95
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 2.4828 ms/op 1.8593 ms/op 1.34
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 24.624 ms/op 15.550 ms/op 1.58
BeaconState.hashTreeRoot - 1 balances 89.941 us/op 79.734 us/op 1.13
BeaconState.hashTreeRoot - 32 balances 1.0206 ms/op 630.08 us/op 1.62
BeaconState.hashTreeRoot - 512 balances 6.6794 ms/op 5.8228 ms/op 1.15
BeaconState.hashTreeRoot - 250000 balances 138.83 ms/op 147.74 ms/op 0.94
aggregationBits - 2048 els - zipIndexesInBitList 21.132 us/op 21.084 us/op 1.00
regular array get 100000 times 25.121 us/op 25.290 us/op 0.99
wrappedArray get 100000 times 25.093 us/op 25.250 us/op 0.99
arrayWithProxy get 100000 times 14.916 ms/op 14.078 ms/op 1.06
ssz.Root.equals 52.947 ns/op 24.043 ns/op 2.20
byteArrayEquals 23.484 ns/op 23.624 ns/op 0.99
Buffer.compare 10.106 ns/op 9.9620 ns/op 1.01
processSlot - 1 slots 11.977 us/op 9.7310 us/op 1.23
processSlot - 32 slots 2.3389 ms/op 2.6217 ms/op 0.89
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 5.2976 ms/op 5.3738 ms/op 0.99
getCommitteeAssignments - req 1 vs - 250000 vc 1.8869 ms/op 1.9977 ms/op 0.94
getCommitteeAssignments - req 100 vs - 250000 vc 3.7182 ms/op 3.6648 ms/op 1.01
getCommitteeAssignments - req 1000 vs - 250000 vc 3.9971 ms/op 3.9505 ms/op 1.01
findModifiedValidators - 10000 modified validators 577.94 ms/op 683.97 ms/op 0.84
findModifiedValidators - 1000 modified validators 449.94 ms/op 423.11 ms/op 1.06
findModifiedValidators - 100 modified validators 286.12 ms/op 331.51 ms/op 0.86
findModifiedValidators - 10 modified validators 132.98 ms/op 145.32 ms/op 0.92
findModifiedValidators - 1 modified validators 164.07 ms/op 160.43 ms/op 1.02
findModifiedValidators - no difference 163.70 ms/op 170.46 ms/op 0.96
migrate state 1500000 validators, 3400 modified, 2000 new 994.03 ms/op 983.93 ms/op 1.01
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 4.2900 ns/op 4.2800 ns/op 1.00
state getBlockRootAtSlot - 250000 vs - 7PWei 553.73 ns/op 366.73 ns/op 1.51
computeProposerIndex 100000 validators 1.5098 ms/op 1.5321 ms/op 0.99
getNextSyncCommitteeIndices 1000 validators 112.61 ms/op 119.79 ms/op 0.94
getNextSyncCommitteeIndices 10000 validators 112.26 ms/op 119.90 ms/op 0.94
getNextSyncCommitteeIndices 100000 validators 114.30 ms/op 120.07 ms/op 0.95
computeProposers - vc 250000 626.18 us/op 614.93 us/op 1.02
computeEpochShuffling - vc 250000 42.223 ms/op 41.053 ms/op 1.03
getNextSyncCommittee - vc 250000 10.797 ms/op 10.347 ms/op 1.04
nodejs block root to RootHex using toHex 141.91 ns/op 133.02 ns/op 1.07
nodejs block root to RootHex using toRootHex 85.134 ns/op 84.593 ns/op 1.01
nodejs fromHex(blob) 252.65 us/op 437.16 us/op 0.58
nodejs fromHexInto(blob) 713.75 us/op 703.11 us/op 1.02
nodejs block root to RootHex using the deprecated toHexString 217.90 ns/op 550.16 ns/op 0.40
nodejs byteArrayEquals 32 bytes (block root) 29.007 ns/op 28.408 ns/op 1.02
nodejs byteArrayEquals 48 bytes (pubkey) 41.393 ns/op 40.680 ns/op 1.02
nodejs byteArrayEquals 96 bytes (signature) 46.785 ns/op 40.341 ns/op 1.16
nodejs byteArrayEquals 1024 bytes 50.343 ns/op 45.952 ns/op 1.10
nodejs byteArrayEquals 131072 bytes (blob) 1.9113 us/op 1.8767 us/op 1.02
browser block root to RootHex using toHex 167.87 ns/op 164.49 ns/op 1.02
browser block root to RootHex using toRootHex 157.27 ns/op 154.84 ns/op 1.02
browser fromHex(blob) 1.2226 ms/op 1.1741 ms/op 1.04
browser fromHexInto(blob) 746.31 us/op 696.78 us/op 1.07
browser block root to RootHex using the deprecated toHexString 577.36 ns/op 400.52 ns/op 1.44
browser byteArrayEquals 32 bytes (block root) 32.065 ns/op 30.727 ns/op 1.04
browser byteArrayEquals 48 bytes (pubkey) 47.490 ns/op 42.871 ns/op 1.11
browser byteArrayEquals 96 bytes (signature) 87.591 ns/op 84.221 ns/op 1.04
browser byteArrayEquals 1024 bytes 820.45 ns/op 799.95 ns/op 1.03
browser byteArrayEquals 131072 bytes (blob) 102.64 us/op 100.96 us/op 1.02

by benchmarkbot/action

@ensi321

ensi321 commented Mar 5, 2026

Copy link
Copy Markdown
Member Author

@lodekeeper can you review this please? Also see if comments left by gemini-code-assist make sense

@ensi321 ensi321 changed the title fix payload status on fork chocie init fix: payload status on fork chocie init Mar 5, 2026
@lodekeeper

Copy link
Copy Markdown
Contributor

@ensi321 I reviewed the new Gemini comments — they are actionable.\n\nThe key issue is that getCheckpointPayloadStatus(...) is checkpoint-epoch based and can return FULL for Gloas, but this payloadStatus assignment is for the anchor/head proto-block during forkchoice init. In that path, proto-array initialization currently expects the Gloas block variant lifecycle to start from pending/empty, with FULL reached via payload-processing path later.\n\nSo using getCheckpointPayloadStatus for the anchor block here is not safe as-is (and is also semantically mismatched for non-checkpoint head slots).\n\nSuggested fix for this PR: keep the current anchor/head initialization behavior (PENDING for post-Gloas) and only use getCheckpointPayloadStatus where we are actually computing checkpoint statuses (justified/finalized store state).

@ensi321
ensi321 marked this pull request as draft March 5, 2026 19:23
@nflaig

nflaig commented May 30, 2026

Copy link
Copy Markdown
Member

@ensi321 is this still relevant? I would think not, but please double check

@ensi321

ensi321 commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Issue it aims to solve is still relevant but the PR itself is super stale

@lodekeeper

Copy link
Copy Markdown
Contributor

Agreed — and the PR can't really be rebased even if we wanted to: getCheckpointPayloadStatus() was deleted wholesale in #9259 along with payloadStatus on Checkpoint, and the TODO GLOAS: Post-gloas how do we know if the checkpoint payload is FULL or EMPTY? that this PR targeted was removed in your own #9257. The API this was built on no longer exists, so keeping it closed is right.

On "the issue it aims to solve is still relevant" — I re-read the init path on unstable (d16fa2b) and I think the residual is narrower but sharper than the original framing:

  • initializeForkChoiceFrom{Finalized,Unfinalized}State seed the anchor with payloadStatus: PENDING and parentBlockHash: toRootHex(state.latestBlockHash) (forkChoice/index.ts:162, :266).
  • ProtoArray.onBlock creates only PENDING + EMPTY for a Gloas block; the FULL variant is created exclusively by onExecutionPayload. For a checkpoint-sync anchor that envelope is long gone, so the anchor never gets a FULL variant.
  • Under deferred payload processing (feat: defer payload processing to next block #9257), state.latestBlockHash = parentBid.blockHash (processParentExecutionPayload.ts:107) — the parent's payload hash, not the anchor's own. So the anchor's EMPTY variant correctly carries the parent hash.

The consequence: if the anchor block's own payload was revealed on chain, the first child extending it carries parentBlockHash = the anchor's bid blockHash. In getNodeIndexByRootAndBlockHash there is no FULL variant to match, and EMPTY holds the parent's hash instead — so getParentPayloadStatus falls through to UNKNOWN_PARENT_BLOCK and the block fails to import.

Note that the state can't trivially fix this itself: executionPayloadAvailability for the anchor's own slot isn't set until the next block is processed, so the anchor state genuinely doesn't know whether its own payload was revealed. Which is arguably why PENDING is the only honest answer at the anchor — and would put the gap on the consumer side instead, i.e. getParentPayloadStatus throwing rather than tolerating a missing FULL variant at the fork-choice root.

Caveat: I traced this by reading, not by reproducing — and neither init function has any test coverage today, so I can't rule out a compensating path in sync. Does that match your read? If so I'm happy to open a focused issue so it doesn't get lost now that this PR is closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants