Skip to content

feat: builder selection, broadcast validation, stateless flow for gloas block production#9595

Open
nflaig wants to merge 38 commits into
unstablefrom
nflaig/stateless-flow
Open

feat: builder selection, broadcast validation, stateless flow for gloas block production#9595
nflaig wants to merge 38 commits into
unstablefrom
nflaig/stateless-flow

Conversation

@nflaig

@nflaig nflaig commented Jul 5, 2026

Copy link
Copy Markdown
Member
  • per remove BlindedExecutionPayloadEnvelope ethereum/beacon-APIs#624
  • validator client defaults to the stateless flow if multiple beacon nodes are configured
  • add --payloadLocal flag to configure behavior stateless vs. stateful
  • produceBlockV4 now respects builder_selection and builder_boost_factor
  • post-gloas the validator client defaults to the default builder selection strategy (uses in-protocol p2p bids and local block, selecting by value), explicit --builder.selection config is still honored
  • implement broadcast_validation for publishing execution payload envelopes (except equivocation checks)
  • extend oapi spec test framework to support {schema: type: boolean} in query params and request headers
  • remove builderonly builder selection option, it's a safety concern and adds unnecessary complexity

@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 introduces support for both stateless and stateful block production flows in the post-Gloas fork, allowing validators to request full block contents (including the execution payload envelope, KZG proofs, and blobs) when self-building. It updates the produceBlockV4 and publishExecutionPayloadEnvelope endpoints, implements broadcast validation logic, and updates the CLI, types, and tests accordingly. The review feedback highlights two important improvements: validating the submitted blob and KZG proof counts against expected values in the stateless flow to prevent downstream errors, and using optional chaining when accessing blobsBundle properties to avoid potential runtime crashes during block production.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Comment thread packages/beacon-node/src/api/impl/validator/index.ts
Comment thread packages/api/src/beacon/routes/beacon/block.ts Outdated
Comment thread packages/cli/src/cmds/validator/options.ts Outdated
Comment thread packages/validator/src/services/validatorStore.ts Outdated
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: ed3a95a Previous: 27da921 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 964.81 us/op 1.2362 ms/op 0.78
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 39.828 us/op 39.670 us/op 1.00
BLS verify - blst 693.15 us/op 751.55 us/op 0.92
BLS verifyMultipleSignatures 3 - blst 1.3032 ms/op 1.3564 ms/op 0.96
BLS verifyMultipleSignatures 8 - blst 2.0594 ms/op 2.1595 ms/op 0.95
BLS verifyMultipleSignatures 32 - blst 6.4661 ms/op 6.8164 ms/op 0.95
BLS verifyMultipleSignatures 64 - blst 12.615 ms/op 13.555 ms/op 0.93
BLS verifyMultipleSignatures 128 - blst 24.661 ms/op 25.874 ms/op 0.95
BLS deserializing 10000 signatures 623.28 ms/op 640.88 ms/op 0.97
BLS deserializing 100000 signatures 6.2312 s/op 6.3913 s/op 0.97
BLS verifyMultipleSignatures - same message - 3 - blst 759.83 us/op 804.75 us/op 0.94
BLS verifyMultipleSignatures - same message - 8 - blst 862.54 us/op 933.10 us/op 0.92
BLS verifyMultipleSignatures - same message - 32 - blst 1.5075 ms/op 1.5457 ms/op 0.98
BLS verifyMultipleSignatures - same message - 64 - blst 2.3127 ms/op 2.3648 ms/op 0.98
BLS verifyMultipleSignatures - same message - 128 - blst 3.8490 ms/op 4.0503 ms/op 0.95
BLS aggregatePubkeys 32 - blst 16.975 us/op 17.580 us/op 0.97
BLS aggregatePubkeys 128 - blst 60.092 us/op 62.618 us/op 0.96
getSlashingsAndExits - default max 48.466 us/op 54.658 us/op 0.89
getSlashingsAndExits - 2k 373.45 us/op 367.74 us/op 1.02
proposeBlockBody type=full, size=empty 634.54 us/op 638.74 us/op 0.99
isKnown best case - 1 super set check 162.00 ns/op 166.00 ns/op 0.98
isKnown normal case - 2 super set checks 155.00 ns/op 164.00 ns/op 0.95
isKnown worse case - 16 super set checks 156.00 ns/op 166.00 ns/op 0.94
validate api signedAggregateAndProof - struct 1.4460 ms/op 1.5062 ms/op 0.96
validate gossip signedAggregateAndProof - struct 1.4497 ms/op 1.5042 ms/op 0.96
batch validate gossip attestation - vc 640000 - chunk 32 103.70 us/op 106.35 us/op 0.98
batch validate gossip attestation - vc 640000 - chunk 64 92.266 us/op 92.630 us/op 1.00
batch validate gossip attestation - vc 640000 - chunk 128 87.333 us/op 86.025 us/op 1.02
batch validate gossip attestation - vc 640000 - chunk 256 85.244 us/op 82.400 us/op 1.03
bytes32 toHexString 275.00 ns/op 296.00 ns/op 0.93
bytes32 Buffer.toString(hex) 181.00 ns/op 161.00 ns/op 1.12
bytes32 Buffer.toString(hex) from Uint8Array 233.00 ns/op 233.00 ns/op 1.00
bytes32 Buffer.toString(hex) + 0x 164.00 ns/op 164.00 ns/op 1.00
Return object 10000 times 0.20540 ns/op 0.21090 ns/op 0.97
Throw Error 10000 times 3.2468 us/op 3.4511 us/op 0.94
toHex 92.070 ns/op 97.261 ns/op 0.95
Buffer.from 83.047 ns/op 88.599 ns/op 0.94
shared Buffer 56.260 ns/op 60.967 ns/op 0.92
fastMsgIdFn sha256 / 200 bytes 1.4230 us/op 1.4680 us/op 0.97
fastMsgIdFn h32 xxhash / 200 bytes 151.00 ns/op 152.00 ns/op 0.99
fastMsgIdFn h64 xxhash / 200 bytes 193.00 ns/op 202.00 ns/op 0.96
fastMsgIdFn sha256 / 1000 bytes 4.5670 us/op 4.7630 us/op 0.96
fastMsgIdFn h32 xxhash / 1000 bytes 237.00 ns/op 243.00 ns/op 0.98
fastMsgIdFn h64 xxhash / 1000 bytes 236.00 ns/op 250.00 ns/op 0.94
fastMsgIdFn sha256 / 10000 bytes 40.364 us/op 41.697 us/op 0.97
fastMsgIdFn h32 xxhash / 10000 bytes 1.2170 us/op 1.2710 us/op 0.96
fastMsgIdFn h64 xxhash / 10000 bytes 770.00 ns/op 830.00 ns/op 0.93
send data - 1000 256B messages 4.5939 ms/op 4.1716 ms/op 1.10
send data - 1000 512B messages 5.4333 ms/op 5.1064 ms/op 1.06
send data - 1000 1024B messages 5.6801 ms/op 5.5459 ms/op 1.02
send data - 1000 1200B messages 6.6278 ms/op 6.0571 ms/op 1.09
send data - 1000 2048B messages 17.952 ms/op 15.662 ms/op 1.15
send data - 1000 4096B messages 36.096 ms/op 54.685 ms/op 0.66
send data - 1000 16384B messages 196.65 ms/op 262.20 ms/op 0.75
send data - 1000 65536B messages 735.91 ms/op 795.72 ms/op 0.92
enrSubnets - fastDeserialize 64 bits 695.00 ns/op 722.00 ns/op 0.96
enrSubnets - ssz BitVector 64 bits 256.00 ns/op 254.00 ns/op 1.01
enrSubnets - fastDeserialize 4 bits 100.00 ns/op 98.000 ns/op 1.02
enrSubnets - ssz BitVector 4 bits 248.00 ns/op 253.00 ns/op 0.98
prioritizePeers score -10:0 att 32-0.1 sync 2-0 198.85 us/op 200.23 us/op 0.99
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 228.40 us/op 228.56 us/op 1.00
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 330.95 us/op 338.72 us/op 0.98
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 594.80 us/op 593.40 us/op 1.00
prioritizePeers score 0:0 att 64-1 sync 4-1 688.17 us/op 688.85 us/op 1.00
array of 16000 items push then shift 1.2075 us/op 1.2873 us/op 0.94
LinkedList of 16000 items push then shift 7.4430 ns/op 6.8720 ns/op 1.08
array of 16000 items push then pop 69.464 ns/op 65.458 ns/op 1.06
LinkedList of 16000 items push then pop 5.8250 ns/op 5.8900 ns/op 0.99
array of 24000 items push then shift 1.7830 us/op 1.9002 us/op 0.94
LinkedList of 24000 items push then shift 7.2280 ns/op 6.5930 ns/op 1.10
array of 24000 items push then pop 99.399 ns/op 91.480 ns/op 1.09
LinkedList of 24000 items push then pop 5.9360 ns/op 5.8720 ns/op 1.01
intersect bitArray bitLen 8 4.5600 ns/op 4.7410 ns/op 0.96
intersect array and set length 8 28.775 ns/op 29.071 ns/op 0.99
intersect bitArray bitLen 128 22.999 ns/op 23.933 ns/op 0.96
intersect array and set length 128 478.26 ns/op 495.49 ns/op 0.97
bitArray.getTrueBitIndexes() bitLen 128 1.0480 us/op 1.0690 us/op 0.98
bitArray.getTrueBitIndexes() bitLen 248 1.6890 us/op 1.8740 us/op 0.90
bitArray.getTrueBitIndexes() bitLen 512 3.4960 us/op 3.7640 us/op 0.93
Full columns - reconstruct all 6 blobs 207.00 us/op 150.78 us/op 1.37
Full columns - reconstruct half of the blobs out of 6 98.398 us/op 93.882 us/op 1.05
Full columns - reconstruct single blob out of 6 55.569 us/op 51.702 us/op 1.07
Half columns - reconstruct all 6 blobs 387.37 ms/op 380.30 ms/op 1.02
Half columns - reconstruct half of the blobs out of 6 193.18 ms/op 189.97 ms/op 1.02
Half columns - reconstruct single blob out of 6 69.275 ms/op 66.915 ms/op 1.04
Set add up to 64 items then delete first 1.5931 us/op 1.6418 us/op 0.97
OrderedSet add up to 64 items then delete first 2.4720 us/op 2.5336 us/op 0.98
Set add up to 64 items then delete last 2.0737 us/op 1.8865 us/op 1.10
OrderedSet add up to 64 items then delete last 3.4052 us/op 2.7940 us/op 1.22
Set add up to 64 items then delete middle 2.1559 us/op 1.8804 us/op 1.15
OrderedSet add up to 64 items then delete middle 4.7526 us/op 4.3006 us/op 1.11
Set add up to 128 items then delete first 4.0142 us/op 3.7476 us/op 1.07
OrderedSet add up to 128 items then delete first 6.0186 us/op 5.7827 us/op 1.04
Set add up to 128 items then delete last 4.1559 us/op 3.7287 us/op 1.11
OrderedSet add up to 128 items then delete last 6.3937 us/op 5.5197 us/op 1.16
Set add up to 128 items then delete middle 4.1607 us/op 3.5942 us/op 1.16
OrderedSet add up to 128 items then delete middle 12.278 us/op 11.467 us/op 1.07
Set add up to 256 items then delete first 7.9252 us/op 7.6934 us/op 1.03
OrderedSet add up to 256 items then delete first 12.621 us/op 11.795 us/op 1.07
Set add up to 256 items then delete last 8.2580 us/op 7.1251 us/op 1.16
OrderedSet add up to 256 items then delete last 13.228 us/op 10.918 us/op 1.21
Set add up to 256 items then delete middle 8.1539 us/op 7.1051 us/op 1.15
OrderedSet add up to 256 items then delete middle 36.106 us/op 34.079 us/op 1.06
runFastConfirmationRules vc:100000 bc:96 eq:0 5.0231 ms/op 4.4876 ms/op 1.12
runFastConfirmationRules vc:600000 bc:96 eq:0 35.401 ms/op 33.985 ms/op 1.04
runFastConfirmationRules vc:1000000 bc:96 eq:0 59.172 ms/op 57.236 ms/op 1.03
runFastConfirmationRules vc:600000 bc:320 eq:0 35.664 ms/op 34.014 ms/op 1.05
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.3803 s/op 1.1447 s/op 1.21
pass gossip attestations to forkchoice per slot 2.4726 ms/op 2.6024 ms/op 0.95
forkChoice updateHead vc 100000 bc 64 eq 0 385.40 us/op 414.07 us/op 0.93
forkChoice updateHead vc 600000 bc 64 eq 0 2.3313 ms/op 2.4896 ms/op 0.94
forkChoice updateHead vc 1000000 bc 64 eq 0 3.9182 ms/op 4.1295 ms/op 0.95
forkChoice updateHead vc 600000 bc 320 eq 0 2.3727 ms/op 2.4877 ms/op 0.95
forkChoice updateHead vc 600000 bc 1200 eq 0 2.4049 ms/op 2.4913 ms/op 0.97
forkChoice updateHead vc 600000 bc 7200 eq 0 3.2690 ms/op 2.8884 ms/op 1.13
forkChoice updateHead vc 600000 bc 64 eq 1000 2.4538 ms/op 2.4493 ms/op 1.00
forkChoice updateHead vc 600000 bc 64 eq 10000 2.4466 ms/op 2.5434 ms/op 0.96
forkChoice updateHead vc 600000 bc 64 eq 300000 6.5005 ms/op 6.5508 ms/op 0.99
computeDeltas 1400000 validators 0% inactive 11.716 ms/op 12.187 ms/op 0.96
computeDeltas 1400000 validators 10% inactive 11.123 ms/op 11.618 ms/op 0.96
computeDeltas 1400000 validators 20% inactive 10.569 ms/op 10.978 ms/op 0.96
computeDeltas 1400000 validators 50% inactive 8.6219 ms/op 8.8811 ms/op 0.97
computeDeltas 2100000 validators 0% inactive 17.673 ms/op 18.362 ms/op 0.96
computeDeltas 2100000 validators 10% inactive 16.817 ms/op 17.647 ms/op 0.95
computeDeltas 2100000 validators 20% inactive 15.661 ms/op 16.526 ms/op 0.95
computeDeltas 2100000 validators 50% inactive 10.479 ms/op 11.024 ms/op 0.95
altair processAttestation - 250000 vs - 7PWei normalcase 2.4815 ms/op 1.6719 ms/op 1.48
altair processAttestation - 250000 vs - 7PWei worstcase 3.2397 ms/op 2.4184 ms/op 1.34
altair processAttestation - setStatus - 1/6 committees join 94.070 us/op 103.86 us/op 0.91
altair processAttestation - setStatus - 1/3 committees join 191.89 us/op 195.30 us/op 0.98
altair processAttestation - setStatus - 1/2 committees join 274.04 us/op 283.62 us/op 0.97
altair processAttestation - setStatus - 2/3 committees join 346.90 us/op 364.24 us/op 0.95
altair processAttestation - setStatus - 4/5 committees join 491.82 us/op 504.04 us/op 0.98
altair processAttestation - setStatus - 100% committees join 578.66 us/op 603.02 us/op 0.96
altair processBlock - 250000 vs - 7PWei normalcase 4.5160 ms/op 2.9146 ms/op 1.55
altair processBlock - 250000 vs - 7PWei normalcase hashState 16.121 ms/op 11.875 ms/op 1.36
altair processBlock - 250000 vs - 7PWei worstcase 21.657 ms/op 19.820 ms/op 1.09
altair processBlock - 250000 vs - 7PWei worstcase hashState 42.081 ms/op 38.342 ms/op 1.10
phase0 processBlock - 250000 vs - 7PWei normalcase 1.3612 ms/op 1.2888 ms/op 1.06
phase0 processBlock - 250000 vs - 7PWei worstcase 17.171 ms/op 16.086 ms/op 1.07
altair processEth1Data - 250000 vs - 7PWei normalcase 269.51 us/op 297.18 us/op 0.91
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 3.0120 us/op 3.1990 us/op 0.94
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 18.444 us/op 20.926 us/op 0.88
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 6.3560 us/op 6.0180 us/op 1.06
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.3140 us/op 3.7990 us/op 0.87
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 85.608 us/op 93.085 us/op 0.92
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.3116 ms/op 1.4107 ms/op 0.93
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.7190 ms/op 1.8769 ms/op 0.92
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.7148 ms/op 1.8836 ms/op 0.91
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.4612 ms/op 3.7262 ms/op 0.93
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 1.9563 ms/op 2.0850 ms/op 0.94
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.0831 ms/op 4.0405 ms/op 1.01
Tree 40 250000 create 337.51 ms/op 310.29 ms/op 1.09
Tree 40 250000 get(125000) 88.841 ns/op 95.067 ns/op 0.93
Tree 40 250000 set(125000) 969.90 ns/op 1.0246 us/op 0.95
Tree 40 250000 toArray() 16.957 ms/op 9.0260 ms/op 1.88
Tree 40 250000 iterate all - toArray() + loop 16.756 ms/op 9.2267 ms/op 1.82
Tree 40 250000 iterate all - get(i) 38.745 ms/op 34.305 ms/op 1.13
Array 250000 create 2.2807 ms/op 2.1658 ms/op 1.05
Array 250000 clone - spread 697.53 us/op 671.50 us/op 1.04
Array 250000 get(125000) 0.28400 ns/op 0.29500 ns/op 0.96
Array 250000 set(125000) 0.28600 ns/op 0.29600 ns/op 0.97
Array 250000 iterate all - loop 54.378 us/op 57.806 us/op 0.94
phase0 afterProcessEpoch - 250000 vs - 7PWei 46.790 ms/op 61.007 ms/op 0.77
Array.fill - length 1000000 2.2441 ms/op 2.2202 ms/op 1.01
Array push - length 1000000 9.7495 ms/op 8.1796 ms/op 1.19
Array.get 0.19779 ns/op 0.20575 ns/op 0.96
Uint8Array.get 0.22789 ns/op 0.25414 ns/op 0.90
phase0 beforeProcessEpoch - 250000 vs - 7PWei 15.192 ms/op 16.292 ms/op 0.93
altair processEpoch - mainnet_e81889 266.64 ms/op 261.18 ms/op 1.02
mainnet_e81889 - altair beforeProcessEpoch 15.639 ms/op 19.969 ms/op 0.78
mainnet_e81889 - altair processJustificationAndFinalization 6.3340 us/op 5.6180 us/op 1.13
mainnet_e81889 - altair processInactivityUpdates 6.0921 ms/op 3.4682 ms/op 1.76
mainnet_e81889 - altair processRewardsAndPenalties 19.163 ms/op 18.019 ms/op 1.06
mainnet_e81889 - altair processRegistryUpdates 515.00 ns/op 565.00 ns/op 0.91
mainnet_e81889 - altair processSlashings 134.00 ns/op 156.00 ns/op 0.86
mainnet_e81889 - altair processEth1DataReset 128.00 ns/op 146.00 ns/op 0.88
mainnet_e81889 - altair processEffectiveBalanceUpdates 4.8803 ms/op 1.1921 ms/op 4.09
mainnet_e81889 - altair processSlashingsReset 648.00 ns/op 746.00 ns/op 0.87
mainnet_e81889 - altair processRandaoMixesReset 1.2570 us/op 1.0950 us/op 1.15
mainnet_e81889 - altair processHistoricalRootsUpdate 133.00 ns/op 149.00 ns/op 0.89
mainnet_e81889 - altair processParticipationFlagUpdates 489.00 ns/op 432.00 ns/op 1.13
mainnet_e81889 - altair processSyncCommitteeUpdates 104.00 ns/op 104.00 ns/op 1.00
mainnet_e81889 - altair afterProcessEpoch 39.634 ms/op 43.315 ms/op 0.92
capella processEpoch - mainnet_e217614 773.39 ms/op 764.76 ms/op 1.01
mainnet_e217614 - capella beforeProcessEpoch 62.399 ms/op 55.580 ms/op 1.12
mainnet_e217614 - capella processJustificationAndFinalization 6.2050 us/op 5.4760 us/op 1.13
mainnet_e217614 - capella processInactivityUpdates 16.623 ms/op 12.299 ms/op 1.35
mainnet_e217614 - capella processRewardsAndPenalties 91.043 ms/op 91.491 ms/op 1.00
mainnet_e217614 - capella processRegistryUpdates 4.1850 us/op 4.4520 us/op 0.94
mainnet_e217614 - capella processSlashings 143.00 ns/op 147.00 ns/op 0.97
mainnet_e217614 - capella processEth1DataReset 138.00 ns/op 145.00 ns/op 0.95
mainnet_e217614 - capella processEffectiveBalanceUpdates 17.320 ms/op 5.7287 ms/op 3.02
mainnet_e217614 - capella processSlashingsReset 658.00 ns/op 721.00 ns/op 0.91
mainnet_e217614 - capella processRandaoMixesReset 1.2530 us/op 1.2290 us/op 1.02
mainnet_e217614 - capella processHistoricalRootsUpdate 138.00 ns/op 147.00 ns/op 0.94
mainnet_e217614 - capella processParticipationFlagUpdates 430.00 ns/op 434.00 ns/op 0.99
mainnet_e217614 - capella afterProcessEpoch 105.18 ms/op 110.10 ms/op 0.96
phase0 processEpoch - mainnet_e58758 282.75 ms/op 278.06 ms/op 1.02
mainnet_e58758 - phase0 beforeProcessEpoch 61.681 ms/op 54.040 ms/op 1.14
mainnet_e58758 - phase0 processJustificationAndFinalization 5.4480 us/op 5.6140 us/op 0.97
mainnet_e58758 - phase0 processRewardsAndPenalties 16.108 ms/op 15.257 ms/op 1.06
mainnet_e58758 - phase0 processRegistryUpdates 2.1620 us/op 4.4560 us/op 0.49
mainnet_e58758 - phase0 processSlashings 141.00 ns/op 144.00 ns/op 0.98
mainnet_e58758 - phase0 processEth1DataReset 133.00 ns/op 145.00 ns/op 0.92
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 2.0107 ms/op 826.43 us/op 2.43
mainnet_e58758 - phase0 processSlashingsReset 873.00 ns/op 828.00 ns/op 1.05
mainnet_e58758 - phase0 processRandaoMixesReset 1.1770 us/op 1.0910 us/op 1.08
mainnet_e58758 - phase0 processHistoricalRootsUpdate 157.00 ns/op 146.00 ns/op 1.08
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.0340 us/op 1.0110 us/op 1.02
mainnet_e58758 - phase0 afterProcessEpoch 31.919 ms/op 33.596 ms/op 0.95
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.0075 ms/op 1.0119 ms/op 1.00
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.5120 ms/op 1.7887 ms/op 0.85
altair processInactivityUpdates - 250000 normalcase 11.247 ms/op 10.632 ms/op 1.06
altair processInactivityUpdates - 250000 worstcase 11.020 ms/op 11.186 ms/op 0.99
phase0 processRegistryUpdates - 250000 normalcase 2.1730 us/op 2.3270 us/op 0.93
phase0 processRegistryUpdates - 250000 badcase_full_deposits 135.09 us/op 140.89 us/op 0.96
phase0 processRegistryUpdates - 250000 worstcase 0.5 58.864 ms/op 61.334 ms/op 0.96
altair processRewardsAndPenalties - 250000 normalcase 14.953 ms/op 15.776 ms/op 0.95
altair processRewardsAndPenalties - 250000 worstcase 14.746 ms/op 15.059 ms/op 0.98
phase0 getAttestationDeltas - 250000 normalcase 5.0259 ms/op 8.7563 ms/op 0.57
phase0 getAttestationDeltas - 250000 worstcase 5.1435 ms/op 8.6709 ms/op 0.59
phase0 processSlashings - 250000 worstcase 59.268 us/op 63.346 us/op 0.94
altair processSyncCommitteeUpdates - 250000 29.715 ms/op 10.235 ms/op 2.90
BeaconState.hashTreeRoot - No change 162.00 ns/op 174.00 ns/op 0.93
BeaconState.hashTreeRoot - 1 full validator 74.571 us/op 71.694 us/op 1.04
BeaconState.hashTreeRoot - 32 full validator 776.58 us/op 859.62 us/op 0.90
BeaconState.hashTreeRoot - 512 full validator 8.4825 ms/op 6.1558 ms/op 1.38
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 106.74 us/op 83.298 us/op 1.28
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.4578 ms/op 1.2731 ms/op 1.15
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 20.050 ms/op 14.652 ms/op 1.37
BeaconState.hashTreeRoot - 1 balances 73.275 us/op 63.223 us/op 1.16
BeaconState.hashTreeRoot - 32 balances 738.32 us/op 741.93 us/op 1.00
BeaconState.hashTreeRoot - 512 balances 6.1731 ms/op 4.7887 ms/op 1.29
BeaconState.hashTreeRoot - 250000 balances 129.33 ms/op 113.88 ms/op 1.14
aggregationBits - 2048 els - zipIndexesInBitList 18.575 us/op 20.060 us/op 0.93
regular array get 100000 times 21.851 us/op 23.476 us/op 0.93
wrappedArray get 100000 times 21.818 us/op 23.452 us/op 0.93
arrayWithProxy get 100000 times 9.3609 ms/op 10.484 ms/op 0.89
ssz.Root.equals 20.305 ns/op 21.800 ns/op 0.93
byteArrayEquals 20.195 ns/op 21.544 ns/op 0.94
Buffer.compare 8.3720 ns/op 8.9100 ns/op 0.94
processSlot - 1 slots 8.7850 us/op 8.3360 us/op 1.05
processSlot - 32 slots 2.0132 ms/op 1.5354 ms/op 1.31
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.7711 ms/op 2.8076 ms/op 1.70
getCommitteeAssignments - req 1 vs - 250000 vc 1.6074 ms/op 1.6868 ms/op 0.95
getCommitteeAssignments - req 100 vs - 250000 vc 3.2530 ms/op 3.4555 ms/op 0.94
getCommitteeAssignments - req 1000 vs - 250000 vc 3.5158 ms/op 3.7030 ms/op 0.95
findModifiedValidators - 10000 modified validators 718.33 ms/op 757.44 ms/op 0.95
findModifiedValidators - 1000 modified validators 389.48 ms/op 520.40 ms/op 0.75
findModifiedValidators - 100 modified validators 286.49 ms/op 306.47 ms/op 0.93
findModifiedValidators - 10 modified validators 140.82 ms/op 258.46 ms/op 0.54
findModifiedValidators - 1 modified validators 140.33 ms/op 192.58 ms/op 0.73
findModifiedValidators - no difference 138.14 ms/op 157.70 ms/op 0.88
migrate state 1500000 validators, 3400 modified, 2000 new 3.4501 s/op 3.2322 s/op 1.07
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.6500 ns/op 3.7600 ns/op 0.97
state getBlockRootAtSlot - 250000 vs - 7PWei 410.03 ns/op 283.09 ns/op 1.45
computeProposerIndex 100000 validators 1.3368 ms/op 1.4434 ms/op 0.93
getNextSyncCommitteeIndices 1000 validators 2.8592 ms/op 2.9166 ms/op 0.98
getNextSyncCommitteeIndices 10000 validators 24.707 ms/op 27.001 ms/op 0.92
getNextSyncCommitteeIndices 100000 validators 85.995 ms/op 89.673 ms/op 0.96
computeProposers - vc 250000 550.44 us/op 557.58 us/op 0.99
computeEpochShuffling - vc 250000 39.260 ms/op 40.495 ms/op 0.97
getNextSyncCommittee - vc 250000 9.2918 ms/op 9.5625 ms/op 0.97
nodejs block root to RootHex using toHex 94.410 ns/op 96.027 ns/op 0.98
nodejs block root to RootHex using toRootHex 60.299 ns/op 63.244 ns/op 0.95
nodejs fromHex(blob) 917.06 us/op 790.43 us/op 1.16
nodejs fromHexInto(blob) 612.09 us/op 635.07 us/op 0.96
nodejs block root to RootHex using the deprecated toHexString 465.12 ns/op 510.43 ns/op 0.91
nodejs byteArrayEquals 32 bytes (block root) 25.429 ns/op 26.455 ns/op 0.96
nodejs byteArrayEquals 48 bytes (pubkey) 37.302 ns/op 38.224 ns/op 0.98
nodejs byteArrayEquals 96 bytes (signature) 33.276 ns/op 35.431 ns/op 0.94
nodejs byteArrayEquals 1024 bytes 39.557 ns/op 45.413 ns/op 0.87
nodejs byteArrayEquals 131072 bytes (blob) 1.7354 us/op 1.7879 us/op 0.97
browser block root to RootHex using toHex 139.46 ns/op 146.84 ns/op 0.95
browser block root to RootHex using toRootHex 127.21 ns/op 132.88 ns/op 0.96
browser fromHex(blob) 1.6411 ms/op 1.6701 ms/op 0.98
browser fromHexInto(blob) 623.29 us/op 632.02 us/op 0.99
browser block root to RootHex using the deprecated toHexString 470.06 ns/op 350.01 ns/op 1.34
browser byteArrayEquals 32 bytes (block root) 27.687 ns/op 28.442 ns/op 0.97
browser byteArrayEquals 48 bytes (pubkey) 39.046 ns/op 40.033 ns/op 0.98
browser byteArrayEquals 96 bytes (signature) 73.114 ns/op 74.679 ns/op 0.98
browser byteArrayEquals 1024 bytes 746.82 ns/op 767.09 ns/op 0.97
browser byteArrayEquals 131072 bytes (blob) 95.037 us/op 96.522 us/op 0.98

by benchmarkbot/action

if (builderBid === null && builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
throw new ApiError(400, `No builder bid available for slot=${slot} with builderSelection=builderonly`);
}
const buildLocalBlock = builderSelection !== routes.validator.BuilderSelection.BuilderOnly;

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.

I feel like this builderonly option shouldn't exist at all, but removing it would be a breaking change, at the same time, I don't know why anyone would use this, was added by Gajinder, if I remember correctly this was relevant for dvts at some point, but there is no known users that I am aware of now

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.

technically every hard fork is a breaking change. I don't think we have to support this thing forever. Happy to retire it, think about a deprecation strategy.

@nflaig nflaig Jul 17, 2026

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.

technically every hard fork is a breaking change.

not really, I mean yes, users have to upgrade their node but with 0 other changes otherwise, but regarding builder settings, if we go with the proposal from terence/potuz, this needs to be reworked anyways

* TODO GLOAS: re-evaluate cutoff timing
* TODO GLOAS: re-evaluate cutoff timing due to attestation deadline changes in gloas
*/
const BLOCK_PRODUCTION_RACE_CUTOFF_MS = 2_000;

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.

this may be fine even for gloas if the deadline is at 3 seconds since we only need to publish the tiny beacon block, but if the attestation deadline is further reduced, this needs to be revisited for sure

also worth considering using a _BPS value instead of _MS to scale with slot duration

// handle shouldOverrideBuilder separately
if (engine.status === "fulfilled" && engine.value.shouldOverrideBuilder) {
logger.info("Selected engine block: censorship suspected in builder blocks", {
logger.warn("Selected engine block: censorship suspected in builder blocks", {

@nflaig nflaig Jul 14, 2026

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.

unrelated, but I thought we always logged this as a warn so users can notice it more easily in the logs

@nflaig
nflaig marked this pull request as ready for review July 14, 2026 16:51
@nflaig
nflaig requested a review from a team as a code owner July 14, 2026 16:51
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.49%. Comparing base (242f99e) to head (7b04cb9).

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #9595      +/-   ##
============================================
- Coverage     52.49%   52.49%   -0.01%     
============================================
  Files           848      848              
  Lines         60485    60483       -2     
  Branches       4466     4466              
============================================
- Hits          31754    31752       -2     
  Misses        28670    28670              
  Partials         61       61              
🚀 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.

@nflaig
nflaig marked this pull request as draft July 17, 2026 18:00
@nflaig

nflaig commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

need to rework this a bit, after bringing in the circuit breaker changes dealing with builderonly is a headache, I am gonna remove this pre/post gloas, per discussion on discord, nobody should be using this

})
: Promise.reject(new Error("Builder disabled"));

const enginePromise = isEngineEnabled

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.

the diff here is kinda annoying but it's due to getting rid of builderonly behavior, so isEngineEnabled can only be true now, the bool is no longer required

@nflaig
nflaig marked this pull request as ready for review July 17, 2026 21:28
@gemini-code-assist

This comment was marked as off-topic.

@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: b2d14ade48

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Comment thread packages/api/src/beacon/routes/validator.ts
Comment thread packages/api/src/beacon/routes/beacon/block.ts
@nflaig nflaig changed the title feat: stateless flow for local block production post-gloas feat: builder selection and stateless flow for gloas block production Jul 17, 2026
@nflaig nflaig changed the title feat: builder selection and stateless flow for gloas block production feat: builder selection, broadcast validation, stateless flow for gloas block production Jul 17, 2026
@nflaig
nflaig requested a review from wemeetagain July 17, 2026 22:08
publishBlindedBlockV2,

async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) {
async publishExecutionPayloadEnvelope({signedEnvelope, broadcastValidation}) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this signedEnvelope could be SignedExecutionPayloadEnvelopeContents so I'd like to reflect on the name

Suggested change
async publishExecutionPayloadEnvelope({signedEnvelope, broadcastValidation}) {
async publishExecutionPayloadEnvelope({signedEnvelopeOrContents, broadcastValidation}) {

if (cachedGloasResult?.cells) {
cells = cachedGloasResult.cells;
} else {
dataColumnTimer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this dataColumTimer is hard to monitor as is
consider adding source: computed | cache or something like that to differentiate having to compute cells or not
getGloasDataColumnSidecars() takes minority of the total time I guess

!isForkPostGloas(produceResult.fork) ||
produceResult.type !== BlockType.Full
) {
throw Error(`Missing cached block production result for produced block root=${blockRoot}`);

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.

add more information to the error message, at least the slot

@@ -987,9 +1051,41 @@ export function getValidatorApi(
void chain.persistBlock(block, "produced_engine_block");

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.

post-gloas we need to also at least persist payload envelope
(not part of this PR through)

if (blockState === null || !isStatePostGloas(blockState)) {
throw new ApiError(
400,
`Unable to regenerate block state for consensus checks blockRoot=${blockRootHex}`

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.

add block slot to the error message
also this means an issue of beacon-node itself (5xx), not validator client (4xx)

// Publish all data column sidecars
...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
// Import execution payload. Signature already verified above
// Import execution payload. Signature is verified during broadcast validation, an already

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.

signature was not verified if "none" BroadcastValidation

}

const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
if (!payloadInput) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this check should be moved to before line 852
we only need to get payloadInput from seenPayloadEnvelopeInputCache once
then reuse the returned payloadInput found

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.

also include the slot there

builderIndex: envelope.builderIndex,
isSelfBuild,
blobDataIncluded,
broadcastValidation,

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.

would be great to also include number of blobs if we have it
...(submittedContents !== null ? {submittedBlobs: submittedContents.blobs.length} : {}),

broadcastValidation,
})
.catch((e: Error) => {
this.metrics?.blockProposingErrors.inc({error: "publish"});

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.

create (if we don't have any) and use new metrics for payload
also at least include slot/block root to the error messages

).assertOk();
}

this.logger.info("Published block and execution payload envelope", {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

need to include stateful/stateless information by using executionPayloadIncluded variable

})
.catch((e: Error) => {
this.metrics?.blockProposingErrors.inc({error: "publish"});
throw extendError(e, "Failed to publish execution payload envelope");

@twoeths twoeths Jul 20, 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.

same to above, use payload metrics + enhance error message to include slot
also include stateful/stateless information

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

looks good to me, I dropped minor comments

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