Skip to content

feat: implement should_apply_proposer_boost for gloas - #9233

Open
ensi321 wants to merge 12 commits into
unstablefrom
nc/should-apply-proposer-boost
Open

feat: implement should_apply_proposer_boost for gloas#9233
ensi321 wants to merge 12 commits into
unstablefrom
nc/should-apply-proposer-boost

Conversation

@ensi321

@ensi321 ensi321 commented Apr 18, 2026

Copy link
Copy Markdown
Member

Summary

Implements the should_apply_proposer_boost logic ethereum/consensus-specs/pull/4807 (gloas/fork-choice.md#new-should_apply_proposer_boost).

Was blocked on is_head_weak(); now rebased on top of #9654 (merged) and reuses its isHeadWeak() — boost-excluded attestation score + equivocator balance add-back — for the weak-parent check, exactly matching the spec's is_head_weak(store, parent_root) call.

Changes

  • Add ptcTimeliness and proposerIndex fields to ProtoBlock
  • Add isBlockPtcTimely to track PTC deadline timeliness
  • Add shouldApplyProposerBoost which withholds boost when the parent is a weak, equivocating block from the previous slot
  • Reuse isHeadWeak() from feat: implement forkchoice is_head_weak() and is_parent_strong() #9654 for the weak-parent check
  • Add findEquivocatingBlocks in ProtoArray to detect proposer equivocations
  • Gate proposer boost in getWeight on shouldApplyProposerBoost()
  • Pre-gloas blocks retain unconditional boost (backward compatible)

Note on test coverage

The existing gloas fork_choice vectors pass with or without this gate (the withhold branch executes but never decides the asserted head), so CI alone doesn't discriminate it. Discriminating vectors are proposed upstream in consensus-specs (test_should_apply_proposer_boost.py, MC/DC over the three apply conditions); the withheld case fails on unstable and passes on this branch.

AI Assistance Disclosure

Created with the help of Claude.

🤖 Generated with Claude Code

Implement the `should_apply_proposer_boost` logic from consensus-specs
commit 71d1151 (PR #4807). This addresses the builder reveal safety
concern where a colluding next-slot proposer could use proposer boost
to override a legitimately revealed block.

Changes:
- Add `ptcTimeliness` and `proposerIndex` fields to ProtoBlock
- Add `isBlockPtcTimely` to track PTC deadline timeliness
- Add `shouldApplyProposerBoost` which withholds boost when the parent
  is a weak, equivocating block from the previous slot
- Add `findEquivocatingBlocks` in ProtoArray to detect proposer
  equivocations by scanning for PTC-timely blocks at the same slot
  from the same proposer
- Gate proposer boost in `getWeight` on `shouldApplyProposerBoost()`
- Pre-gloas blocks retain unconditional boost (backward compatible)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ensi321
ensi321 requested a review from a team as a code owner April 18, 2026 02:23

@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 implements the Gloas fork-choice logic for proposer boost and PTC (Payload Timeliness Committee) timeliness. It adds ptcTimeliness and proposerIndex to block metadata and introduces the shouldApplyProposerBoost logic, which considers parent block weight and proposer equivocations. Feedback focuses on preventing a crash during the fork transition by using dynamic payload status for parent nodes and optimizing the performance of the equivocation check to avoid O(N) map iterations during head updates.

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated
Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
@ensi321
ensi321 marked this pull request as draft April 18, 2026 02:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bc6eb6ce5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated
@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Performance Report

🚀🚀 Significant benchmark improvement detected

Benchmark suite Current: 2a2a91e Previous: f6a0d68 Ratio
Full columns - reconstruct all 6 blobs 121.02 us/op 495.87 us/op 0.24
Array.fill - length 1000000 1.8499 ms/op 6.2398 ms/op 0.30
Full benchmark results
Benchmark suite Current: 2a2a91e Previous: f6a0d68 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 1.0904 ms/op 920.69 us/op 1.18
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 39.860 us/op 40.070 us/op 0.99
BLS verify - blst 719.80 us/op 729.53 us/op 0.99
BLS verifyMultipleSignatures 3 - blst 1.3758 ms/op 1.2976 ms/op 1.06
BLS verifyMultipleSignatures 8 - blst 2.1652 ms/op 2.0747 ms/op 1.04
BLS verifyMultipleSignatures 32 - blst 6.8600 ms/op 6.3641 ms/op 1.08
BLS verifyMultipleSignatures 64 - blst 13.731 ms/op 12.378 ms/op 1.11
BLS verifyMultipleSignatures 128 - blst 25.967 ms/op 24.481 ms/op 1.06
BLS deserializing 10000 signatures 624.90 ms/op 627.72 ms/op 1.00
BLS deserializing 100000 signatures 6.4190 s/op 6.2758 s/op 1.02
BLS verifyMultipleSignatures - same message - 3 - blst 795.54 us/op 757.78 us/op 1.05
BLS verifyMultipleSignatures - same message - 8 - blst 901.91 us/op 884.07 us/op 1.02
BLS verifyMultipleSignatures - same message - 32 - blst 1.5698 ms/op 1.4804 ms/op 1.06
BLS verifyMultipleSignatures - same message - 64 - blst 2.3880 ms/op 2.3797 ms/op 1.00
BLS verifyMultipleSignatures - same message - 128 - blst 3.9979 ms/op 4.0379 ms/op 0.99
BLS aggregatePubkeys 32 - blst 17.644 us/op 17.551 us/op 1.01
BLS aggregatePubkeys 128 - blst 63.032 us/op 62.517 us/op 1.01
getSlashingsAndExits - default max 46.620 us/op 54.511 us/op 0.86
getSlashingsAndExits - 2k 350.94 us/op 425.90 us/op 0.82
proposeBlockBody type=full, size=empty 1.3637 ms/op 555.88 us/op 2.45
isKnown best case - 1 super set check 162.00 ns/op 187.00 ns/op 0.87
isKnown normal case - 2 super set checks 163.00 ns/op 156.00 ns/op 1.04
isKnown worse case - 16 super set checks 164.00 ns/op 155.00 ns/op 1.06
validate api signedAggregateAndProof - struct 1.5272 ms/op 1.4649 ms/op 1.04
validate gossip signedAggregateAndProof - struct 1.5282 ms/op 1.4594 ms/op 1.05
batch validate gossip attestation - vc 640000 - chunk 32 106.39 us/op 108.83 us/op 0.98
batch validate gossip attestation - vc 640000 - chunk 64 93.652 us/op 96.028 us/op 0.98
batch validate gossip attestation - vc 640000 - chunk 128 86.471 us/op 91.398 us/op 0.95
batch validate gossip attestation - vc 640000 - chunk 256 83.367 us/op 89.707 us/op 0.93
bytes32 toHexString 284.00 ns/op 289.00 ns/op 0.98
bytes32 Buffer.toString(hex) 163.00 ns/op 176.00 ns/op 0.93
bytes32 Buffer.toString(hex) from Uint8Array 236.00 ns/op 241.00 ns/op 0.98
bytes32 Buffer.toString(hex) + 0x 164.00 ns/op 172.00 ns/op 0.95
Return object 10000 times 0.20890 ns/op 0.20940 ns/op 1.00
Throw Error 10000 times 3.2058 us/op 3.2305 us/op 0.99
toHex 89.490 ns/op 94.719 ns/op 0.94
Buffer.from 81.631 ns/op 83.892 ns/op 0.97
shared Buffer 56.276 ns/op 55.940 ns/op 1.01
fastMsgIdFn sha256 / 200 bytes 1.4330 us/op 1.4510 us/op 0.99
fastMsgIdFn h32 xxhash / 200 bytes 154.00 ns/op 158.00 ns/op 0.97
fastMsgIdFn h64 xxhash / 200 bytes 199.00 ns/op 206.00 ns/op 0.97
fastMsgIdFn sha256 / 1000 bytes 4.5700 us/op 4.6690 us/op 0.98
fastMsgIdFn h32 xxhash / 1000 bytes 237.00 ns/op 253.00 ns/op 0.94
fastMsgIdFn h64 xxhash / 1000 bytes 245.00 ns/op 257.00 ns/op 0.95
fastMsgIdFn sha256 / 10000 bytes 40.383 us/op 40.943 us/op 0.99
fastMsgIdFn h32 xxhash / 10000 bytes 1.2580 us/op 1.2610 us/op 1.00
fastMsgIdFn h64 xxhash / 10000 bytes 818.00 ns/op 830.00 ns/op 0.99
send data - 1000 256B messages 4.2365 ms/op 4.6803 ms/op 0.91
send data - 1000 512B messages 5.7253 ms/op 5.8100 ms/op 0.99
send data - 1000 1024B messages 5.7873 ms/op 6.1642 ms/op 0.94
send data - 1000 1200B messages 6.7698 ms/op 6.9578 ms/op 0.97
send data - 1000 2048B messages 18.724 ms/op 22.039 ms/op 0.85
send data - 1000 4096B messages 43.213 ms/op 63.320 ms/op 0.68
send data - 1000 16384B messages 322.91 ms/op 371.30 ms/op 0.87
send data - 1000 65536B messages 1.6076 s/op 1.1464 s/op 1.40
enrSubnets - fastDeserialize 64 bits 730.00 ns/op 713.00 ns/op 1.02
enrSubnets - ssz BitVector 64 bits 260.00 ns/op 255.00 ns/op 1.02
enrSubnets - fastDeserialize 4 bits 102.00 ns/op 98.000 ns/op 1.04
enrSubnets - ssz BitVector 4 bits 262.00 ns/op 255.00 ns/op 1.03
prioritizePeers score -10:0 att 32-0.1 sync 2-0 198.21 us/op 191.77 us/op 1.03
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 224.13 us/op 220.15 us/op 1.02
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 337.73 us/op 320.81 us/op 1.05
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 595.35 us/op 573.55 us/op 1.04
prioritizePeers score 0:0 att 64-1 sync 4-1 744.51 us/op 667.83 us/op 1.11
array of 16000 items push then shift 1.2026 us/op 1.1740 us/op 1.02
LinkedList of 16000 items push then shift 7.3130 ns/op 7.1880 ns/op 1.02
array of 16000 items push then pop 61.425 ns/op 63.254 ns/op 0.97
LinkedList of 16000 items push then pop 5.6990 ns/op 5.6830 ns/op 1.00
array of 24000 items push then shift 1.7599 us/op 1.7746 us/op 0.99
LinkedList of 24000 items push then shift 6.6630 ns/op 7.0050 ns/op 0.95
array of 24000 items push then pop 88.503 ns/op 88.352 ns/op 1.00
LinkedList of 24000 items push then pop 5.7300 ns/op 5.8470 ns/op 0.98
intersect bitArray bitLen 8 3.6950 ns/op 3.7540 ns/op 0.98
intersect array and set length 8 27.769 ns/op 28.254 ns/op 0.98
intersect bitArray bitLen 128 22.242 ns/op 22.156 ns/op 1.00
intersect array and set length 128 473.51 ns/op 482.36 ns/op 0.98
bitArray.getTrueBitIndexes() bitLen 128 882.00 ns/op 926.00 ns/op 0.95
bitArray.getTrueBitIndexes() bitLen 248 1.6080 us/op 1.6660 us/op 0.97
bitArray.getTrueBitIndexes() bitLen 512 3.3920 us/op 3.4230 us/op 0.99
Full columns - reconstruct all 6 blobs 121.02 us/op 495.87 us/op 0.24
Full columns - reconstruct half of the blobs out of 6 66.744 us/op 72.537 us/op 0.92
Full columns - reconstruct single blob out of 6 32.667 us/op 34.381 us/op 0.95
Half columns - reconstruct all 6 blobs 384.26 ms/op 377.09 ms/op 1.02
Half columns - reconstruct half of the blobs out of 6 191.79 ms/op 187.39 ms/op 1.02
Half columns - reconstruct single blob out of 6 68.189 ms/op 66.807 ms/op 1.02
Set add up to 64 items then delete first 1.6538 us/op 1.5583 us/op 1.06
OrderedSet add up to 64 items then delete first 2.5436 us/op 2.3925 us/op 1.06
Set add up to 64 items then delete last 1.8972 us/op 1.7631 us/op 1.08
OrderedSet add up to 64 items then delete last 2.8950 us/op 2.6643 us/op 1.09
Set add up to 64 items then delete middle 1.9041 us/op 1.7441 us/op 1.09
OrderedSet add up to 64 items then delete middle 4.4395 us/op 4.0222 us/op 1.10
Set add up to 128 items then delete first 3.6915 us/op 3.5831 us/op 1.03
OrderedSet add up to 128 items then delete first 5.4802 us/op 5.3221 us/op 1.03
Set add up to 128 items then delete last 3.6602 us/op 3.3452 us/op 1.09
OrderedSet add up to 128 items then delete last 5.6453 us/op 5.1139 us/op 1.10
Set add up to 128 items then delete middle 3.6524 us/op 3.3753 us/op 1.08
OrderedSet add up to 128 items then delete middle 11.698 us/op 10.510 us/op 1.11
Set add up to 256 items then delete first 7.2663 us/op 7.5167 us/op 0.97
OrderedSet add up to 256 items then delete first 11.090 us/op 11.787 us/op 0.94
Set add up to 256 items then delete last 6.6871 us/op 7.0978 us/op 0.94
OrderedSet add up to 256 items then delete last 11.467 us/op 10.997 us/op 1.04
Set add up to 256 items then delete middle 7.2122 us/op 7.0121 us/op 1.03
OrderedSet add up to 256 items then delete middle 35.437 us/op 33.203 us/op 1.07
runFastConfirmationRules vc:100000 bc:96 eq:0 4.6653 ms/op 4.3412 ms/op 1.07
runFastConfirmationRules vc:600000 bc:96 eq:0 36.228 ms/op 35.032 ms/op 1.03
runFastConfirmationRules vc:1000000 bc:96 eq:0 57.364 ms/op 60.093 ms/op 0.95
runFastConfirmationRules vc:600000 bc:320 eq:0 34.183 ms/op 35.536 ms/op 0.96
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.0964 s/op 1.0579 s/op 1.04
pass gossip attestations to forkchoice per slot 2.5952 ms/op 2.5462 ms/op 1.02
forkChoice updateHead vc 100000 bc 64 eq 0 427.28 us/op 399.07 us/op 1.07
forkChoice updateHead vc 600000 bc 64 eq 0 2.5020 ms/op 2.4134 ms/op 1.04
forkChoice updateHead vc 1000000 bc 64 eq 0 4.1722 ms/op 3.9421 ms/op 1.06
forkChoice updateHead vc 600000 bc 320 eq 0 2.4985 ms/op 2.3940 ms/op 1.04
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5303 ms/op 2.3881 ms/op 1.06
forkChoice updateHead vc 600000 bc 7200 eq 0 2.8400 ms/op 3.3436 ms/op 0.85
forkChoice updateHead vc 600000 bc 64 eq 1000 2.4834 ms/op 2.4117 ms/op 1.03
forkChoice updateHead vc 600000 bc 64 eq 10000 2.5956 ms/op 2.4559 ms/op 1.06
forkChoice updateHead vc 600000 bc 64 eq 300000 6.7111 ms/op 6.3187 ms/op 1.06
forkChoice updateHead vc 600000 bc 64 eq 0 gloas boosted 2.4649 ms/op
computeDeltas 1400000 validators 0% inactive 12.289 ms/op 12.138 ms/op 1.01
computeDeltas 1400000 validators 10% inactive 11.673 ms/op 11.251 ms/op 1.04
computeDeltas 1400000 validators 20% inactive 11.039 ms/op 10.840 ms/op 1.02
computeDeltas 1400000 validators 50% inactive 8.9317 ms/op 8.5754 ms/op 1.04
computeDeltas 2100000 validators 0% inactive 18.647 ms/op 18.161 ms/op 1.03
computeDeltas 2100000 validators 10% inactive 17.662 ms/op 17.466 ms/op 1.01
computeDeltas 2100000 validators 20% inactive 16.569 ms/op 15.979 ms/op 1.04
computeDeltas 2100000 validators 50% inactive 11.093 ms/op 10.706 ms/op 1.04
altair processAttestation - 250000 vs - 7PWei normalcase 1.6733 ms/op 2.7704 ms/op 0.60
altair processAttestation - 250000 vs - 7PWei worstcase 2.3751 ms/op 3.4683 ms/op 0.68
altair processAttestation - setStatus - 1/6 committees join 102.71 us/op 97.085 us/op 1.06
altair processAttestation - setStatus - 1/3 committees join 205.02 us/op 204.76 us/op 1.00
altair processAttestation - setStatus - 1/2 committees join 288.78 us/op 275.75 us/op 1.05
altair processAttestation - setStatus - 2/3 committees join 374.58 us/op 356.27 us/op 1.05
altair processAttestation - setStatus - 4/5 committees join 506.44 us/op 490.17 us/op 1.03
altair processAttestation - setStatus - 100% committees join 598.17 us/op 587.79 us/op 1.02
altair processBlock - 250000 vs - 7PWei normalcase 3.9117 ms/op 4.1240 ms/op 0.95
altair processBlock - 250000 vs - 7PWei normalcase hashState 13.251 ms/op 16.465 ms/op 0.80
altair processBlock - 250000 vs - 7PWei worstcase 21.541 ms/op 21.657 ms/op 0.99
altair processBlock - 250000 vs - 7PWei worstcase hashState 42.994 ms/op 44.641 ms/op 0.96
phase0 processBlock - 250000 vs - 7PWei normalcase 1.4984 ms/op 1.4159 ms/op 1.06
phase0 processBlock - 250000 vs - 7PWei worstcase 17.373 ms/op 16.561 ms/op 1.05
altair processEth1Data - 250000 vs - 7PWei normalcase 285.30 us/op 286.15 us/op 1.00
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 4.2160 us/op 4.8750 us/op 0.86
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 19.916 us/op 20.182 us/op 0.99
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 5.8400 us/op 6.2120 us/op 0.94
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.8340 us/op 4.6750 us/op 0.82
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 93.905 us/op 93.393 us/op 1.01
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.2726 ms/op 1.3491 ms/op 0.94
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.7587 ms/op 1.7943 ms/op 0.98
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.6458 ms/op 1.8052 ms/op 0.91
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.3391 ms/op 4.3727 ms/op 0.76
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 1.9304 ms/op 2.0146 ms/op 0.96
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 3.7382 ms/op 4.8464 ms/op 0.77
Tree 40 250000 create 347.69 ms/op 330.84 ms/op 1.05
Tree 40 250000 get(125000) 88.415 ns/op 87.851 ns/op 1.01
Tree 40 250000 set(125000) 949.57 ns/op 965.73 ns/op 0.98
Tree 40 250000 toArray() 10.639 ms/op 16.466 ms/op 0.65
Tree 40 250000 iterate all - toArray() + loop 11.207 ms/op 17.350 ms/op 0.65
Tree 40 250000 iterate all - get(i) 32.850 ms/op 38.866 ms/op 0.85
Array 250000 create 2.0101 ms/op 2.0956 ms/op 0.96
Array 250000 clone - spread 617.60 us/op 648.81 us/op 0.95
Array 250000 get(125000) 0.27800 ns/op 0.28600 ns/op 0.97
Array 250000 set(125000) 0.28100 ns/op 0.28700 ns/op 0.98
Array 250000 iterate all - loop 54.468 us/op 55.620 us/op 0.98
phase0 afterProcessEpoch - 250000 vs - 7PWei 37.368 ms/op 38.666 ms/op 0.97
Array.fill - length 1000000 1.8499 ms/op 6.2398 ms/op 0.30
Array push - length 1000000 6.8523 ms/op 7.8085 ms/op 0.88
Array.get 0.19885 ns/op 0.20622 ns/op 0.96
Uint8Array.get 0.24144 ns/op 0.22754 ns/op 1.06
phase0 beforeProcessEpoch - 250000 vs - 7PWei 15.358 ms/op 15.558 ms/op 0.99
altair processEpoch - mainnet_e81889 278.01 ms/op 263.82 ms/op 1.05
mainnet_e81889 - altair beforeProcessEpoch 18.542 ms/op 21.698 ms/op 0.85
mainnet_e81889 - altair processJustificationAndFinalization 7.3670 us/op 6.4360 us/op 1.14
mainnet_e81889 - altair processInactivityUpdates 3.3136 ms/op 5.7486 ms/op 0.58
mainnet_e81889 - altair processRewardsAndPenalties 18.209 ms/op 20.073 ms/op 0.91
mainnet_e81889 - altair processRegistryUpdates 552.00 ns/op 578.00 ns/op 0.96
mainnet_e81889 - altair processSlashings 141.00 ns/op 137.00 ns/op 1.03
mainnet_e81889 - altair processEth1DataReset 134.00 ns/op 134.00 ns/op 1.00
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.4092 ms/op 2.8210 ms/op 0.50
mainnet_e81889 - altair processSlashingsReset 683.00 ns/op 725.00 ns/op 0.94
mainnet_e81889 - altair processRandaoMixesReset 2.8950 us/op 1.3920 us/op 2.08
mainnet_e81889 - altair processHistoricalRootsUpdate 146.00 ns/op 133.00 ns/op 1.10
mainnet_e81889 - altair processParticipationFlagUpdates 452.00 ns/op 456.00 ns/op 0.99
mainnet_e81889 - altair processSyncCommitteeUpdates 116.00 ns/op 109.00 ns/op 1.06
mainnet_e81889 - altair afterProcessEpoch 40.266 ms/op 41.859 ms/op 0.96
capella processEpoch - mainnet_e217614 989.34 ms/op 856.94 ms/op 1.15
mainnet_e217614 - capella beforeProcessEpoch 52.281 ms/op 61.958 ms/op 0.84
mainnet_e217614 - capella processJustificationAndFinalization 6.3680 us/op 6.7570 us/op 0.94
mainnet_e217614 - capella processInactivityUpdates 11.133 ms/op 15.766 ms/op 0.71
mainnet_e217614 - capella processRewardsAndPenalties 94.789 ms/op 96.668 ms/op 0.98
mainnet_e217614 - capella processRegistryUpdates 4.6000 us/op 4.6530 us/op 0.99
mainnet_e217614 - capella processSlashings 144.00 ns/op 145.00 ns/op 0.99
mainnet_e217614 - capella processEth1DataReset 140.00 ns/op 144.00 ns/op 0.97
mainnet_e217614 - capella processEffectiveBalanceUpdates 5.2214 ms/op 20.880 ms/op 0.25
mainnet_e217614 - capella processSlashingsReset 689.00 ns/op 716.00 ns/op 0.96
mainnet_e217614 - capella processRandaoMixesReset 1.3430 us/op 1.3800 us/op 0.97
mainnet_e217614 - capella processHistoricalRootsUpdate 138.00 ns/op 144.00 ns/op 0.96
mainnet_e217614 - capella processParticipationFlagUpdates 443.00 ns/op 462.00 ns/op 0.96
mainnet_e217614 - capella afterProcessEpoch 108.38 ms/op 107.58 ms/op 1.01
phase0 processEpoch - mainnet_e58758 347.22 ms/op 326.17 ms/op 1.06
mainnet_e58758 - phase0 beforeProcessEpoch 77.704 ms/op 70.694 ms/op 1.10
mainnet_e58758 - phase0 processJustificationAndFinalization 7.0720 us/op 6.4750 us/op 1.09
mainnet_e58758 - phase0 processRewardsAndPenalties 16.443 ms/op 16.198 ms/op 1.02
mainnet_e58758 - phase0 processRegistryUpdates 2.1980 us/op 3.3540 us/op 0.66
mainnet_e58758 - phase0 processSlashings 139.00 ns/op 133.00 ns/op 1.05
mainnet_e58758 - phase0 processEth1DataReset 137.00 ns/op 133.00 ns/op 1.03
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 788.68 us/op 1.7802 ms/op 0.44
mainnet_e58758 - phase0 processSlashingsReset 851.00 ns/op 904.00 ns/op 0.94
mainnet_e58758 - phase0 processRandaoMixesReset 1.4170 us/op 1.3680 us/op 1.04
mainnet_e58758 - phase0 processHistoricalRootsUpdate 144.00 ns/op 136.00 ns/op 1.06
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.2400 us/op 1.2900 us/op 0.96
mainnet_e58758 - phase0 afterProcessEpoch 34.816 ms/op 33.138 ms/op 1.05
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.2420 ms/op 976.74 us/op 1.27
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.6571 ms/op 1.7790 ms/op 0.93
altair processInactivityUpdates - 250000 normalcase 10.513 ms/op 11.505 ms/op 0.91
altair processInactivityUpdates - 250000 worstcase 10.496 ms/op 11.512 ms/op 0.91
phase0 processRegistryUpdates - 250000 normalcase 2.3240 us/op 2.2100 us/op 1.05
phase0 processRegistryUpdates - 250000 badcase_full_deposits 143.87 us/op 134.84 us/op 1.07
phase0 processRegistryUpdates - 250000 worstcase 0.5 63.493 ms/op 76.471 ms/op 0.83
altair processRewardsAndPenalties - 250000 normalcase 16.117 ms/op 16.078 ms/op 1.00
altair processRewardsAndPenalties - 250000 worstcase 16.011 ms/op 15.185 ms/op 1.05
phase0 getAttestationDeltas - 250000 normalcase 5.2561 ms/op 5.2009 ms/op 1.01
phase0 getAttestationDeltas - 250000 worstcase 5.5270 ms/op 7.3142 ms/op 0.76
phase0 processSlashings - 250000 worstcase 59.053 us/op 60.366 us/op 0.98
altair processSyncCommitteeUpdates - 250000 9.9896 ms/op 10.185 ms/op 0.98
BeaconState.hashTreeRoot - No change 176.00 ns/op 168.00 ns/op 1.05
BeaconState.hashTreeRoot - 1 full validator 74.840 us/op 86.462 us/op 0.87
BeaconState.hashTreeRoot - 32 full validator 742.17 us/op 902.42 us/op 0.82
BeaconState.hashTreeRoot - 512 full validator 7.5866 ms/op 8.5496 ms/op 0.89
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 87.357 us/op 107.49 us/op 0.81
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.5835 ms/op 1.4907 ms/op 1.06
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 13.333 ms/op 20.492 ms/op 0.65
BeaconState.hashTreeRoot - 1 balances 86.027 us/op 77.660 us/op 1.11
BeaconState.hashTreeRoot - 32 balances 553.81 us/op 713.91 us/op 0.78
BeaconState.hashTreeRoot - 512 balances 4.9295 ms/op 6.4851 ms/op 0.76
BeaconState.hashTreeRoot - 250000 balances 147.72 ms/op 144.56 ms/op 1.02
aggregationBits - 2048 els - zipIndexesInBitList 19.074 us/op 19.773 us/op 0.96
regular array get 100000 times 22.320 us/op 22.581 us/op 0.99
wrappedArray get 100000 times 22.231 us/op 22.720 us/op 0.98
arrayWithProxy get 100000 times 9.3664 ms/op 13.073 ms/op 0.72
ssz.Root.equals 20.765 ns/op 21.024 ns/op 0.99
byteArrayEquals 20.528 ns/op 20.883 ns/op 0.98
Buffer.compare 8.5250 ns/op 8.6400 ns/op 0.99
processSlot - 1 slots 9.6470 us/op 10.671 us/op 0.90
processSlot - 32 slots 2.3366 ms/op 2.2372 ms/op 1.04
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.9311 ms/op 4.9355 ms/op 1.00
getCommitteeAssignments - req 1 vs - 250000 vc 1.6454 ms/op 1.6198 ms/op 1.02
getCommitteeAssignments - req 100 vs - 250000 vc 3.4058 ms/op 3.3328 ms/op 1.02
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6568 ms/op 3.6009 ms/op 1.02
findModifiedValidators - 10000 modified validators 672.15 ms/op 789.12 ms/op 0.85
findModifiedValidators - 1000 modified validators 482.47 ms/op 519.32 ms/op 0.93
findModifiedValidators - 100 modified validators 286.57 ms/op 345.91 ms/op 0.83
findModifiedValidators - 10 modified validators 144.47 ms/op 170.25 ms/op 0.85
findModifiedValidators - 1 modified validators 169.82 ms/op 192.00 ms/op 0.88
findModifiedValidators - no difference 176.20 ms/op 193.35 ms/op 0.91
migrate state 1500000 validators, 3400 modified, 2000 new 3.8236 s/op 3.3173 s/op 1.15
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7500 ns/op 3.7000 ns/op 1.01
state getBlockRootAtSlot - 250000 vs - 7PWei 534.36 ns/op 420.93 ns/op 1.27
computeProposerIndex 100000 validators 1.3624 ms/op 1.3373 ms/op 1.02
getNextSyncCommitteeIndices 1000 validators 2.9051 ms/op 2.8621 ms/op 1.02
getNextSyncCommitteeIndices 10000 validators 24.668 ms/op 25.690 ms/op 0.96
getNextSyncCommitteeIndices 100000 validators 88.783 ms/op 87.904 ms/op 1.01
computeProposers - vc 250000 556.07 us/op 575.42 us/op 0.97
computeEpochShuffling - vc 250000 40.587 ms/op 40.149 ms/op 1.01
getNextSyncCommittee - vc 250000 11.247 ms/op 9.4953 ms/op 1.18
nodejs block root to RootHex using toHex 98.599 ns/op 97.220 ns/op 1.01
nodejs block root to RootHex using toRootHex 66.310 ns/op 59.848 ns/op 1.11
nodejs fromHex(blob) 928.79 us/op 877.90 us/op 1.06
nodejs fromHexInto(blob) 647.95 us/op 622.41 us/op 1.04
nodejs block root to RootHex using the deprecated toHexString 479.06 ns/op 471.19 ns/op 1.02
nodejs byteArrayEquals 32 bytes (block root) 26.792 ns/op 26.045 ns/op 1.03
nodejs byteArrayEquals 48 bytes (pubkey) 38.703 ns/op 36.971 ns/op 1.05
nodejs byteArrayEquals 96 bytes (signature) 36.379 ns/op 33.821 ns/op 1.08
nodejs byteArrayEquals 1024 bytes 42.500 ns/op 41.186 ns/op 1.03
nodejs byteArrayEquals 131072 bytes (blob) 1.7794 us/op 1.7331 us/op 1.03
browser block root to RootHex using toHex 147.21 ns/op 140.96 ns/op 1.04
browser block root to RootHex using toRootHex 131.94 ns/op 128.42 ns/op 1.03
browser fromHex(blob) 1.7056 ms/op 1.6743 ms/op 1.02
browser fromHexInto(blob) 627.86 us/op 624.45 us/op 1.01
browser block root to RootHex using the deprecated toHexString 320.33 ns/op 327.44 ns/op 0.98
browser byteArrayEquals 32 bytes (block root) 27.906 ns/op 27.419 ns/op 1.02
browser byteArrayEquals 48 bytes (pubkey) 39.416 ns/op 39.083 ns/op 1.01
browser byteArrayEquals 96 bytes (signature) 73.916 ns/op 72.420 ns/op 1.02
browser byteArrayEquals 1024 bytes 753.85 ns/op 752.21 ns/op 1.00
browser byteArrayEquals 131072 bytes (blob) 95.666 us/op 95.093 us/op 1.01

by benchmarkbot/action

GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request May 1, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request May 1, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request May 1, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
GrapeBaBa added a commit to GrapeBaBa/lodestar that referenced this pull request Jul 7, 2026
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into
the existing `forkChoiceTest` runner. The on-disk layout matches the
standard spec-test layout
(`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`),
so it slots in alongside `fork_choice` and `sync` runners.

Three test-only accommodations the compliance fixtures require:

1. `bls_setting: 2` — every compliance fixture uses placeholder
   signatures. Pass `validSignatures: testcase.meta?.bls_setting !==
   BigInt(1)` to `chain.processBlock` so verification short-circuits.
   Standard `fork_choice` fixtures use `bls_setting: 1` so behavior
   there is unchanged.

2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally
   re-import the same block (`dup_shift` mutations in their
   `meta.yaml`). Spec semantics for `on_block(store, known_block)` is
   a no-op success. Production block import correctly rejects with
   ALREADY_KNOWN; this runner treats that case as success only when
   the step is `valid: true`.

3. Cross-epoch attestation shuffling — `on_attestation` decodes
   aggregation_bits using the state at the attestation's target
   checkpoint, not the head state. The runner now resolves the right
   shuffling via ShufflingCache + regen (mirroring the production
   validation path) instead of `headState.epochCtx.getIndexedAttestation`,
   which only worked when the attestation's epoch happened to be in
   the head's epoch cache (±1 epoch) and broke on cross-epoch fork
   attestations surfaced by the compliance suite.

Adds support for two compliance-only check fields:

- `viable_for_head_roots_and_weights` (consensus-specs#3831): compared
  via `getViableHeads()`. Both sides are sorted by root before
  comparison since the spec doesn't fix order.
- `head_payload_status` (gloas): mapped between our internal enum
  ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0,
  FULL=1, PENDING=2).

Pass rate against the latest comptests workflow `small.tar.gz` artifact:

  fulu/fork_choice_compliance: 253/1472 cases pass (17.2%)

Top remaining failures:
- ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced
  a `block.proposer_index == get_beacon_proposer_index(head_state)`
  guard in `update_proposer_boost_root` that we do not yet implement;
  affects all forks (not just gloas equivocation handling). Tracked
  for follow-up alongside ChainSafe#9233.
- ~1% `Invalid viable heads` — proposer-boost rounding on minimal
  preset (see `getViableHeads()` weight note).
@nflaig

nflaig commented Jul 8, 2026

Copy link
Copy Markdown
Member

@ensi321 is this still relevant?

ensi321 and others added 4 commits July 8, 2026 20:03
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardcoding PayloadStatus.PENDING in the weak-parent lookup crashes at
the gloas fork transition: the first gloas block's parent is pre-gloas
(FULL-only), and getNodeIndexByRootAndStatus throws INVALID_NODE_INDEX
when PENDING is requested for a pre-gloas root. The adjacent-parent
guard does not short-circuit this path at the boundary. Use
parentBlock.payloadStatus instead, matching the pattern already used
for the strong-variant lookup elsewhere in this file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ensi321

ensi321 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@ensi321 is this still relevant?

For some reason, our unstable is now passing these tests even without this PR and we failed them previously

(name.includes("gloas") &&
(name.includes("simple_attempted_reorg_without_enough_ffg_votes") ||
name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_current_epoch") ||
name.includes("include_votes_another_empty_chain_with_enough_ffg_votes_previous_epoch") ||
name.includes("include_votes_another_empty_chain_without_enough_ffg_votes_current_epoch"))),

But we still need to implement shouldApplyProposerBoost to enforce the correct behaviour.

Added some new tests ethereum/consensus-specs#5441 to make sure this is covered.

@ensi321
ensi321 marked this pull request as ready for review July 9, 2026 14:36
@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.49%. Comparing base (e6d38ec) to head (d2bb968).
⚠️ Report is 3 commits behind head on unstable.

Additional details and impacted files
@@            Coverage Diff            @@
##           unstable    #9233   +/-   ##
=========================================
  Coverage     52.49%   52.49%           
=========================================
  Files           848      848           
  Lines         60487    60486    -1     
  Branches       4466     4465    -1     
=========================================
  Hits          31755    31755           
  Misses        28670    28670           
+ Partials         62       61    -1     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2bb9684f2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated

// Indicate whether block arrives in a timely manner ie. before the 4 second mark
// Spec: Store.block_timeliness[ATTESTATION_TIMELINESS_INDEX]
timeliness: boolean;

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.

we may want to use a single block_timeliness: number to represent these 2 flags, cc @wemeetagain to confirm

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Is there a real benefit to this? Using bits impact readability, and we are only saving one boolean field per ProtoNode

@twoeths twoeths 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.

implemented is_head_weak() in #9654
need to have it landed before this PR

wemeetagain pushed a commit that referenced this pull request Jul 24, 2026
**Motivation**

- is_head_weak() is needed for should_apply_proposer_boost() #9233
- we also need it for `getProposerHead()`

**Description**
- implement cross-fork
[is_head_weak()](https://github.com/ethereum/consensus-specs/blob/6408b990c0b4e58d4f50ae49b59384e82e5e06d8/specs/gloas/fork-choice.md?plain=1#L739)
spec api and use it in `getProposerHead()`
- track attestation weight in ProtoNode separately, weight still means
total weight
- back propagate boost deltas and attestation deltas separately
- also implement is_parent_strong()


**AI Assistance Disclosure**

- created with the help of Claude

---------

Co-authored-by: Tuyen Nguyen <twoeths@users.noreply.github.com>
Co-authored-by: bing <spiralladder@fastmail.com>
ensi321 and others added 2 commits July 24, 2026 18:08
…roposer-boost

# Conflicts:
#	packages/beacon-node/test/spec/presets/fork_choice.test.ts
…ProposerBoost

is_head_weak() landed via #9654 with the exact gloas semantics (boost-excluded
attestation score + committee-scoped equivocator balance add-back), so the
weak-parent check can call it directly instead of approximating with raw
node.weight against the reorg threshold.

Also adapt findEquivocatingBlocks to the current VariantIndices layout via
getDefaultNodeIndex, and add the new ProtoBlock fields to test fixtures that
were added on unstable since the last merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ensi321 and others added 2 commits July 24, 2026 21:04
should_apply_proposer_boost judges the parent via is_head_weak against the
attestations known to the store, but the decision ran before applyScoreChanges()
landed this round's deltas, so it read the previous round's attestation scores.
A parent crossing the weak threshold on the pending batch kept the boost
withheld, and a newly detected equivocator kept its discounted vote counted.

For a gloas boosted block, split the score update in two passes: attestation
deltas first, then the boost decision, then the boost deltas. Pre-gloas keeps
the single pass since the boost is unconditional there.

Also replace findEquivocatingBlocks() with hasEquivocatingBlock(): the only
caller tests emptiness, so return on first match instead of allocating a list.

The two ordering tests fail on the previous commit and pass on this one; the
spec vectors cannot catch this because the check step recomputes the head,
self-healing the transient decision before assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measures the two-pass applyScoreChanges overhead against the single-pass
pre-gloas row: 2.96 ms/op vs 2.72 ms/op at vc 600k bc 64 (~9%), paid only
while a gloas block holds proposer boost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
*
* Child class can overwrite this for testing purpose.
*/
protected isBlockPtcTimely(block: BeaconBlock, blockDelaySec: number): boolean {

@nflaig nflaig Aug 4, 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.

so this should only be relevant in case of proposer equivocation to make sure the next proposer can see it and can be aware of the equivocation in time?

so using the ptc timing seems pretty arbitrary to me but the purpose if I understand this correctly is to figure out if the it was an "early equivocation"

should this be renamed?

Suggested change
protected isBlockPtcTimely(block: BeaconBlock, blockDelaySec: number): boolean {
protected isBlockEquivocationTimely(block: BeaconBlock, blockDelaySec: number): boolean {

Edit: after thinking more about it, not sure the rename is better because it could suggest that the block is an equivocation

continue;
}
const node = this.nodes[nodeIndex];
if (node !== undefined && node.slot === slot && node.proposerIndex === proposerIndex && node.ptcTimeliness) {

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.

I am not sure this can happen right now, if we receive a equivocating block over gossip, we would just ignore it

// [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot.
const proposerIndex = block.proposerIndex;
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});
}

this is kinda related to #9757, need to see if we wanna use seenBlockProposers or fork choice for this, but generally, the equivocation check on the api shouldn't care about timings, if there was an equivocation when submitting the payload, then it's safe to withhold as you don't need to pay the bid.value to the proposer

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.

the IGNORE is more about the gossipsub spec, we should still process it and add to forkchoice #9805
some peers may still vote for the 2nd block, we'll use UnknownBlockInput sync to download, process and add to forkchoice soon anyway

Comment on lines -748 to +758
// TODO GLOAS: boost attribution across payload-status variants of the boosted
// root should be handled when we set up gloas compliance test.
// Pre-gloas each root has exactly one entry.
const isBoosted = exp.root === expectedBoostRoot;
// Pre-gloas each root has exactly one entry. Gloas applies the boost to the
// PENDING variant only, keeping it neutral between EMPTY and FULL.
const isBoosted =
exp.root === expectedBoostRoot &&
(!isGloas || exp.payloadStatus === payloadStatusToSpec[PayloadStatus.PENDING]);

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.

@ensi321 this was required to pass your spec tests

Comment on lines +593 to +595
this.protoArray.applyScoreChanges({attestationDeltas, proposerBoost: null, ...checkpoints});
const proposerBoost = this.shouldApplyProposerBoost() ? this.getProposerBoost() : null;
this.protoArray.applyScoreChanges({

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.

is calling applyScoreChanges twice fine? cc @twoeths

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.

right now applyScoreChanges() does 2 things:

  • (1) update node's weights
  • (2) update best child/descendants

we can improve by calling 1) twice and call 2) in the end
it's more like a refactor and does not affect fulu so we can just leave a TODO and address in another PR I guess

// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/gloas/fork-choice.md#new-should_apply_proposer_boost
this.protoArray.applyScoreChanges({attestationDeltas, proposerBoost: null, ...checkpoints});
const proposerBoost = this.shouldApplyProposerBoost() ? this.getProposerBoost() : null;
this.protoArray.applyScoreChanges({

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.

no need to call applyScoreChanges() again if proposerBoost is null?

@ensi321

ensi321 commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Going to reflect the changes from ethereum/consensus-specs#5515 also

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

6 participants