Skip to content

feat: add circuit breaker for gloas block production#9598

Merged
nflaig merged 20 commits into
unstablefrom
nflaig/gloas-circuit-breaker
Jul 17, 2026
Merged

feat: add circuit breaker for gloas block production#9598
nflaig merged 20 commits into
unstablefrom
nflaig/gloas-circuit-breaker

Conversation

@nflaig

@nflaig nflaig commented Jul 5, 2026

Copy link
Copy Markdown
Member

Motivation

The pre-gloas builder circuit breaker (updateBuilderStatus) counts missed slots via fork choice and disables the external builder flow when the chain is unhealthy. This mechanism does not apply to gloas:

  • produceBlockV4 always prefers a builder bid from the bid pool without any chain health check
  • post-gloas the beacon block is produced by the proposer regardless of bid source, so missed blocks are no longer a useful builder health signal. The failure mode to protect against is unrevealed payloads
  • the missed slot counting via getSlotsPresent also breaks post-gloas since each gloas block inserts multiple protoArray nodes (PENDING/EMPTY/FULL variants)

Description

  • add BuilderCircuitBreaker which counts blocks with unrevealed payloads (no FULL variant in fork choice) within the fault inspection window and ignores builder bids in produceBlockV4 while the breaker is active
    • the fault budget is scaled by blocks present in the window so sparse windows still trigger on high non-reveal rates
    • the current slot is excluded from the window as its payload reveal may still be in flight
    • blocks on all branches are counted, this keeps the result independent of which branch is head when the breaker is evaluated and errs toward local building when forks are frequent
    • the local block is always built, meaning fallback to the self-built block requires no extra work
    • the breaker will apply uniformly once bids are also accepted via the builder api (feat: implement gloas builder api #9594), bid value comparison vs. the local payload is handled in feat: builder selection, broadcast validation, stateless flow for gloas block production #9595
  • update breaker status during prepareNextSlot and lazily at block production time, the pre-gloas builder status update is now gated to pre-gloas forks
  • the existing --builder.faultInspectionWindow and --builder.allowedFaults flags configure the breaker on every fork, defaults are randomized per boot as before
  • add metrics
    • beacon_builder_circuit_breaker_active: whether the breaker is currently active (1) causing builder bids to be ignored
    • beacon_builder_circuit_breaker_faults: count of blocks with unrevealed payloads in the fault inspection window
    • beacon_builder_circuit_breaker_blocks_present: count of blocks present in the fault inspection window
    • beacon_builder_circuit_breaker_payloads_revealed: count of blocks with revealed payloads in the fault inspection window

@nflaig
nflaig requested a review from a team as a code owner July 5, 2026 21:50

@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 a post-Gloas bid circuit breaker for builder bids to mitigate issues with unrevealed payloads. It adds the BidCircuitBreaker class, integrates it into the block production and slot preparation workflows, exposes configuration options via the CLI, and implements tracking in the fork choice and proto-array modules. The review feedback correctly identifies a potential issue at genesis where clockSlot - 1 can evaluate to -1, which is semantically incorrect for slot numbers, and provides a clear code suggestion to guard against this case.

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/chain/builderCircuitBreaker.ts

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

ℹ️ 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/fork-choice/src/protoArray/protoArray.ts
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Performance Report

🚀🚀 Significant benchmark improvement detected

Benchmark suite Current: ae37d8c Previous: 21f8c81 Ratio
isKnown normal case - 2 super set checks 166.00 ns/op 512.00 ns/op 0.32
Full benchmark results
Benchmark suite Current: ae37d8c Previous: 21f8c81 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 879.22 us/op 842.00 us/op 1.04
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 38.735 us/op 37.942 us/op 1.02
BLS verify - blst 708.42 us/op 706.41 us/op 1.00
BLS verifyMultipleSignatures 3 - blst 1.3191 ms/op 1.3313 ms/op 0.99
BLS verifyMultipleSignatures 8 - blst 2.0961 ms/op 2.0956 ms/op 1.00
BLS verifyMultipleSignatures 32 - blst 6.5351 ms/op 6.5420 ms/op 1.00
BLS verifyMultipleSignatures 64 - blst 12.968 ms/op 12.830 ms/op 1.01
BLS verifyMultipleSignatures 128 - blst 24.814 ms/op 25.257 ms/op 0.98
BLS deserializing 10000 signatures 628.20 ms/op 633.75 ms/op 0.99
BLS deserializing 100000 signatures 6.2622 s/op 6.4209 s/op 0.98
BLS verifyMultipleSignatures - same message - 3 - blst 739.61 us/op 712.07 us/op 1.04
BLS verifyMultipleSignatures - same message - 8 - blst 868.35 us/op 931.14 us/op 0.93
BLS verifyMultipleSignatures - same message - 32 - blst 1.4166 ms/op 1.5119 ms/op 0.94
BLS verifyMultipleSignatures - same message - 64 - blst 2.2444 ms/op 2.3725 ms/op 0.95
BLS verifyMultipleSignatures - same message - 128 - blst 3.9319 ms/op 4.0647 ms/op 0.97
BLS aggregatePubkeys 32 - blst 17.287 us/op 17.931 us/op 0.96
BLS aggregatePubkeys 128 - blst 61.432 us/op 62.940 us/op 0.98
getSlashingsAndExits - default max 45.445 us/op 58.160 us/op 0.78
getSlashingsAndExits - 2k 333.17 us/op 456.81 us/op 0.73
proposeBlockBody type=full, size=empty 881.23 us/op 662.49 us/op 1.33
isKnown best case - 1 super set check 163.00 ns/op 422.00 ns/op 0.39
isKnown normal case - 2 super set checks 166.00 ns/op 512.00 ns/op 0.32
isKnown worse case - 16 super set checks 167.00 ns/op 405.00 ns/op 0.41
validate api signedAggregateAndProof - struct 1.4354 ms/op 1.5140 ms/op 0.95
validate gossip signedAggregateAndProof - struct 1.4298 ms/op 1.5449 ms/op 0.93
batch validate gossip attestation - vc 640000 - chunk 32 105.82 us/op 115.62 us/op 0.92
batch validate gossip attestation - vc 640000 - chunk 64 91.007 us/op 109.24 us/op 0.83
batch validate gossip attestation - vc 640000 - chunk 128 84.371 us/op 98.313 us/op 0.86
batch validate gossip attestation - vc 640000 - chunk 256 81.443 us/op 97.184 us/op 0.84
bytes32 toHexString 279.00 ns/op 527.00 ns/op 0.53
bytes32 Buffer.toString(hex) 162.00 ns/op 398.00 ns/op 0.41
bytes32 Buffer.toString(hex) from Uint8Array 234.00 ns/op 482.00 ns/op 0.49
bytes32 Buffer.toString(hex) + 0x 163.00 ns/op 399.00 ns/op 0.41
Return object 10000 times 0.20740 ns/op 0.23970 ns/op 0.87
Throw Error 10000 times 3.2279 us/op 3.4878 us/op 0.93
toHex 91.181 ns/op 117.01 ns/op 0.78
Buffer.from 84.330 ns/op 90.730 ns/op 0.93
shared Buffer 56.576 ns/op 58.479 ns/op 0.97
fastMsgIdFn sha256 / 200 bytes 1.4000 us/op 1.7210 us/op 0.81
fastMsgIdFn h32 xxhash / 200 bytes 151.00 ns/op 374.00 ns/op 0.40
fastMsgIdFn h64 xxhash / 200 bytes 202.00 ns/op 427.00 ns/op 0.47
fastMsgIdFn sha256 / 1000 bytes 4.5230 us/op 5.1690 us/op 0.88
fastMsgIdFn h32 xxhash / 1000 bytes 234.00 ns/op 474.00 ns/op 0.49
fastMsgIdFn h64 xxhash / 1000 bytes 242.00 ns/op 488.00 ns/op 0.50
fastMsgIdFn sha256 / 10000 bytes 39.960 us/op 43.608 us/op 0.92
fastMsgIdFn h32 xxhash / 10000 bytes 1.2360 us/op 1.5260 us/op 0.81
fastMsgIdFn h64 xxhash / 10000 bytes 792.00 ns/op 1.0740 us/op 0.74
send data - 1000 256B messages 4.0072 ms/op 5.5999 ms/op 0.72
send data - 1000 512B messages 5.0505 ms/op 6.3115 ms/op 0.80
send data - 1000 1024B messages 5.4830 ms/op 6.2820 ms/op 0.87
send data - 1000 1200B messages 6.1255 ms/op 7.1147 ms/op 0.86
send data - 1000 2048B messages 9.6230 ms/op 11.904 ms/op 0.81
send data - 1000 4096B messages 49.097 ms/op 41.147 ms/op 1.19
send data - 1000 16384B messages 281.94 ms/op 386.71 ms/op 0.73
send data - 1000 65536B messages 1.5052 s/op 1.6105 s/op 0.93
enrSubnets - fastDeserialize 64 bits 710.00 ns/op 1.0140 us/op 0.70
enrSubnets - ssz BitVector 64 bits 267.00 ns/op 493.00 ns/op 0.54
enrSubnets - fastDeserialize 4 bits 106.00 ns/op 300.00 ns/op 0.35
enrSubnets - ssz BitVector 4 bits 281.00 ns/op 488.00 ns/op 0.58
prioritizePeers score -10:0 att 32-0.1 sync 2-0 199.31 us/op 211.70 us/op 0.94
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 225.90 us/op 232.92 us/op 0.97
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 344.45 us/op 336.08 us/op 1.02
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 587.99 us/op 591.42 us/op 0.99
prioritizePeers score 0:0 att 64-1 sync 4-1 678.51 us/op 699.16 us/op 0.97
array of 16000 items push then shift 1.2375 us/op 1.2604 us/op 0.98
LinkedList of 16000 items push then shift 7.3150 ns/op 8.1310 ns/op 0.90
array of 16000 items push then pop 75.112 ns/op 75.794 ns/op 0.99
LinkedList of 16000 items push then pop 5.9130 ns/op 6.4890 ns/op 0.91
array of 24000 items push then shift 1.8545 us/op 1.8741 us/op 0.99
LinkedList of 24000 items push then shift 7.0410 ns/op 8.2790 ns/op 0.85
array of 24000 items push then pop 107.78 ns/op 102.29 ns/op 1.05
LinkedList of 24000 items push then pop 6.0140 ns/op 6.3050 ns/op 0.95
intersect bitArray bitLen 8 4.7720 ns/op 4.7920 ns/op 1.00
intersect array and set length 8 29.111 ns/op 29.066 ns/op 1.00
intersect bitArray bitLen 128 23.947 ns/op 23.744 ns/op 1.01
intersect array and set length 128 491.85 ns/op 485.40 ns/op 1.01
bitArray.getTrueBitIndexes() bitLen 128 1.0940 us/op 1.2210 us/op 0.90
bitArray.getTrueBitIndexes() bitLen 248 1.9620 us/op 2.0250 us/op 0.97
bitArray.getTrueBitIndexes() bitLen 512 3.9890 us/op 3.8920 us/op 1.02
Full columns - reconstruct all 6 blobs 143.89 us/op 202.66 us/op 0.71
Full columns - reconstruct half of the blobs out of 6 102.33 us/op 75.632 us/op 1.35
Full columns - reconstruct single blob out of 6 34.204 us/op 34.287 us/op 1.00
Half columns - reconstruct all 6 blobs 390.26 ms/op 375.71 ms/op 1.04
Half columns - reconstruct half of the blobs out of 6 194.42 ms/op 185.98 ms/op 1.05
Half columns - reconstruct single blob out of 6 69.189 ms/op 67.091 ms/op 1.03
Set add up to 64 items then delete first 2.0597 us/op 1.5510 us/op 1.33
OrderedSet add up to 64 items then delete first 3.2815 us/op 2.3990 us/op 1.37
Set add up to 64 items then delete last 2.1418 us/op 1.7869 us/op 1.20
OrderedSet add up to 64 items then delete last 3.2708 us/op 2.6197 us/op 1.25
Set add up to 64 items then delete middle 2.1068 us/op 1.7826 us/op 1.18
OrderedSet add up to 64 items then delete middle 4.7579 us/op 4.0426 us/op 1.18
Set add up to 128 items then delete first 4.1929 us/op 3.4751 us/op 1.21
OrderedSet add up to 128 items then delete first 6.5286 us/op 5.4160 us/op 1.21
Set add up to 128 items then delete last 3.8766 us/op 3.3701 us/op 1.15
OrderedSet add up to 128 items then delete last 5.8698 us/op 5.0713 us/op 1.16
Set add up to 128 items then delete middle 3.8350 us/op 3.3652 us/op 1.14
OrderedSet add up to 128 items then delete middle 12.106 us/op 10.778 us/op 1.12
Set add up to 256 items then delete first 7.9769 us/op 7.1290 us/op 1.12
OrderedSet add up to 256 items then delete first 12.277 us/op 11.718 us/op 1.05
Set add up to 256 items then delete last 7.6627 us/op 6.8164 us/op 1.12
OrderedSet add up to 256 items then delete last 11.707 us/op 11.206 us/op 1.04
Set add up to 256 items then delete middle 7.5639 us/op 7.2211 us/op 1.05
OrderedSet add up to 256 items then delete middle 35.523 us/op 34.230 us/op 1.04
runFastConfirmationRules vc:100000 bc:96 eq:0 4.7053 ms/op 4.3362 ms/op 1.09
runFastConfirmationRules vc:600000 bc:96 eq:0 34.913 ms/op 35.139 ms/op 0.99
runFastConfirmationRules vc:1000000 bc:96 eq:0 56.178 ms/op 58.135 ms/op 0.97
runFastConfirmationRules vc:600000 bc:320 eq:0 37.798 ms/op 35.629 ms/op 1.06
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.1705 s/op 1.2201 s/op 0.96
pass gossip attestations to forkchoice per slot 2.5029 ms/op 2.4615 ms/op 1.02
forkChoice updateHead vc 100000 bc 64 eq 0 395.15 us/op 388.15 us/op 1.02
forkChoice updateHead vc 600000 bc 64 eq 0 2.3557 ms/op 2.3452 ms/op 1.00
forkChoice updateHead vc 1000000 bc 64 eq 0 3.9616 ms/op 3.8989 ms/op 1.02
forkChoice updateHead vc 600000 bc 320 eq 0 2.3969 ms/op 2.3802 ms/op 1.01
forkChoice updateHead vc 600000 bc 1200 eq 0 2.4163 ms/op 2.3526 ms/op 1.03
forkChoice updateHead vc 600000 bc 7200 eq 0 2.8975 ms/op 2.8951 ms/op 1.00
forkChoice updateHead vc 600000 bc 64 eq 1000 2.3580 ms/op 2.3538 ms/op 1.00
forkChoice updateHead vc 600000 bc 64 eq 10000 2.4742 ms/op 2.4278 ms/op 1.02
forkChoice updateHead vc 600000 bc 64 eq 300000 6.8814 ms/op 6.3562 ms/op 1.08
computeDeltas 1400000 validators 0% inactive 12.012 ms/op 12.263 ms/op 0.98
computeDeltas 1400000 validators 10% inactive 11.607 ms/op 10.978 ms/op 1.06
computeDeltas 1400000 validators 20% inactive 10.756 ms/op 10.392 ms/op 1.04
computeDeltas 1400000 validators 50% inactive 8.6783 ms/op 8.4098 ms/op 1.03
computeDeltas 2100000 validators 0% inactive 18.005 ms/op 17.446 ms/op 1.03
computeDeltas 2100000 validators 10% inactive 16.974 ms/op 16.715 ms/op 1.02
computeDeltas 2100000 validators 20% inactive 16.203 ms/op 15.471 ms/op 1.05
computeDeltas 2100000 validators 50% inactive 10.654 ms/op 10.339 ms/op 1.03
altair processAttestation - 250000 vs - 7PWei normalcase 2.1376 ms/op 2.0885 ms/op 1.02
altair processAttestation - 250000 vs - 7PWei worstcase 2.9269 ms/op 3.2744 ms/op 0.89
altair processAttestation - setStatus - 1/6 committees join 95.679 us/op 103.55 us/op 0.92
altair processAttestation - setStatus - 1/3 committees join 195.82 us/op 203.08 us/op 0.96
altair processAttestation - setStatus - 1/2 committees join 288.23 us/op 315.97 us/op 0.91
altair processAttestation - setStatus - 2/3 committees join 361.34 us/op 362.28 us/op 1.00
altair processAttestation - setStatus - 4/5 committees join 513.45 us/op 560.03 us/op 0.92
altair processAttestation - setStatus - 100% committees join 611.32 us/op 579.08 us/op 1.06
altair processBlock - 250000 vs - 7PWei normalcase 4.4938 ms/op 4.3653 ms/op 1.03
altair processBlock - 250000 vs - 7PWei normalcase hashState 15.581 ms/op 15.700 ms/op 0.99
altair processBlock - 250000 vs - 7PWei worstcase 21.702 ms/op 21.439 ms/op 1.01
altair processBlock - 250000 vs - 7PWei worstcase hashState 44.265 ms/op 45.374 ms/op 0.98
phase0 processBlock - 250000 vs - 7PWei normalcase 1.3964 ms/op 1.6100 ms/op 0.87
phase0 processBlock - 250000 vs - 7PWei worstcase 16.938 ms/op 17.228 ms/op 0.98
altair processEth1Data - 250000 vs - 7PWei normalcase 279.08 us/op 301.51 us/op 0.93
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 3.4180 us/op 4.7770 us/op 0.72
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 21.014 us/op 22.410 us/op 0.94
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 6.1570 us/op 6.5700 us/op 0.94
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.7290 us/op 4.0420 us/op 0.92
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 91.796 us/op 92.687 us/op 0.99
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.3906 ms/op 1.3398 ms/op 1.04
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8292 ms/op 1.7665 ms/op 1.04
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.8250 ms/op 1.7658 ms/op 1.03
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.6781 ms/op 3.2869 ms/op 1.12
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.0974 ms/op 2.0748 ms/op 1.01
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 3.7983 ms/op 3.6741 ms/op 1.03
Tree 40 250000 create 317.24 ms/op 328.90 ms/op 0.96
Tree 40 250000 get(125000) 93.586 ns/op 90.299 ns/op 1.04
Tree 40 250000 set(125000) 1.0027 us/op 969.16 ns/op 1.03
Tree 40 250000 toArray() 14.638 ms/op 13.254 ms/op 1.10
Tree 40 250000 iterate all - toArray() + loop 14.864 ms/op 14.568 ms/op 1.02
Tree 40 250000 iterate all - get(i) 38.375 ms/op 38.281 ms/op 1.00
Array 250000 create 2.4328 ms/op 2.1984 ms/op 1.11
Array 250000 clone - spread 753.73 us/op 678.05 us/op 1.11
Array 250000 get(125000) 0.29700 ns/op 0.48900 ns/op 0.61
Array 250000 set(125000) 0.30200 ns/op 0.49600 ns/op 0.61
Array 250000 iterate all - loop 57.584 us/op 57.496 us/op 1.00
phase0 afterProcessEpoch - 250000 vs - 7PWei 40.257 ms/op 41.252 ms/op 0.98
Array.fill - length 1000000 2.3644 ms/op 2.2278 ms/op 1.06
Array push - length 1000000 9.8343 ms/op 9.4950 ms/op 1.04
Array.get 0.20775 ns/op 0.20291 ns/op 1.02
Uint8Array.get 0.23787 ns/op 0.24852 ns/op 0.96
phase0 beforeProcessEpoch - 250000 vs - 7PWei 14.893 ms/op 15.601 ms/op 0.95
altair processEpoch - mainnet_e81889 266.41 ms/op 285.67 ms/op 0.93
mainnet_e81889 - altair beforeProcessEpoch 20.085 ms/op 16.410 ms/op 1.22
mainnet_e81889 - altair processJustificationAndFinalization 6.0050 us/op 7.1600 us/op 0.84
mainnet_e81889 - altair processInactivityUpdates 3.6664 ms/op 7.0020 ms/op 0.52
mainnet_e81889 - altair processRewardsAndPenalties 19.744 ms/op 20.280 ms/op 0.97
mainnet_e81889 - altair processRegistryUpdates 540.00 ns/op 806.00 ns/op 0.67
mainnet_e81889 - altair processSlashings 131.00 ns/op 375.00 ns/op 0.35
mainnet_e81889 - altair processEth1DataReset 129.00 ns/op 360.00 ns/op 0.36
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.5426 ms/op 1.5008 ms/op 1.03
mainnet_e81889 - altair processSlashingsReset 691.00 ns/op 905.00 ns/op 0.76
mainnet_e81889 - altair processRandaoMixesReset 1.2770 us/op 1.3590 us/op 0.94
mainnet_e81889 - altair processHistoricalRootsUpdate 146.00 ns/op 357.00 ns/op 0.41
mainnet_e81889 - altair processParticipationFlagUpdates 432.00 ns/op 674.00 ns/op 0.64
mainnet_e81889 - altair processSyncCommitteeUpdates 111.00 ns/op 335.00 ns/op 0.33
mainnet_e81889 - altair afterProcessEpoch 41.040 ms/op 39.922 ms/op 1.03
capella processEpoch - mainnet_e217614 791.67 ms/op 810.84 ms/op 0.98
mainnet_e217614 - capella beforeProcessEpoch 57.196 ms/op 66.227 ms/op 0.86
mainnet_e217614 - capella processJustificationAndFinalization 6.6290 us/op 7.0650 us/op 0.94
mainnet_e217614 - capella processInactivityUpdates 17.140 ms/op 16.147 ms/op 1.06
mainnet_e217614 - capella processRewardsAndPenalties 101.49 ms/op 94.986 ms/op 1.07
mainnet_e217614 - capella processRegistryUpdates 4.4600 us/op 4.6110 us/op 0.97
mainnet_e217614 - capella processSlashings 132.00 ns/op 386.00 ns/op 0.34
mainnet_e217614 - capella processEth1DataReset 128.00 ns/op 367.00 ns/op 0.35
mainnet_e217614 - capella processEffectiveBalanceUpdates 17.282 ms/op 15.469 ms/op 1.12
mainnet_e217614 - capella processSlashingsReset 666.00 ns/op 905.00 ns/op 0.74
mainnet_e217614 - capella processRandaoMixesReset 1.3930 us/op 1.5660 us/op 0.89
mainnet_e217614 - capella processHistoricalRootsUpdate 126.00 ns/op 373.00 ns/op 0.34
mainnet_e217614 - capella processParticipationFlagUpdates 430.00 ns/op 682.00 ns/op 0.63
mainnet_e217614 - capella afterProcessEpoch 107.63 ms/op 105.14 ms/op 1.02
phase0 processEpoch - mainnet_e58758 297.10 ms/op 298.85 ms/op 0.99
mainnet_e58758 - phase0 beforeProcessEpoch 81.946 ms/op 67.698 ms/op 1.21
mainnet_e58758 - phase0 processJustificationAndFinalization 6.5700 us/op 6.7310 us/op 0.98
mainnet_e58758 - phase0 processRewardsAndPenalties 16.385 ms/op 14.803 ms/op 1.11
mainnet_e58758 - phase0 processRegistryUpdates 2.2270 us/op 2.4770 us/op 0.90
mainnet_e58758 - phase0 processSlashings 134.00 ns/op 379.00 ns/op 0.35
mainnet_e58758 - phase0 processEth1DataReset 130.00 ns/op 376.00 ns/op 0.35
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 3.2954 ms/op 995.80 us/op 3.31
mainnet_e58758 - phase0 processSlashingsReset 908.00 ns/op 1.1340 us/op 0.80
mainnet_e58758 - phase0 processRandaoMixesReset 1.3950 us/op 1.5620 us/op 0.89
mainnet_e58758 - phase0 processHistoricalRootsUpdate 126.00 ns/op 387.00 ns/op 0.33
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.2960 us/op 1.3760 us/op 0.94
mainnet_e58758 - phase0 afterProcessEpoch 33.129 ms/op 34.381 ms/op 0.96
phase0 processEffectiveBalanceUpdates - 250000 normalcase 989.59 us/op 1.0894 ms/op 0.91
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.5959 ms/op 1.6313 ms/op 0.98
altair processInactivityUpdates - 250000 normalcase 14.351 ms/op 11.654 ms/op 1.23
altair processInactivityUpdates - 250000 worstcase 10.738 ms/op 10.897 ms/op 0.99
phase0 processRegistryUpdates - 250000 normalcase 2.1910 us/op 2.4250 us/op 0.90
phase0 processRegistryUpdates - 250000 badcase_full_deposits 139.49 us/op 138.63 us/op 1.01
phase0 processRegistryUpdates - 250000 worstcase 0.5 59.903 ms/op 71.286 ms/op 0.84
altair processRewardsAndPenalties - 250000 normalcase 15.264 ms/op 15.075 ms/op 1.01
altair processRewardsAndPenalties - 250000 worstcase 15.055 ms/op 14.150 ms/op 1.06
phase0 getAttestationDeltas - 250000 normalcase 5.4142 ms/op 5.4740 ms/op 0.99
phase0 getAttestationDeltas - 250000 worstcase 5.6184 ms/op 5.4841 ms/op 1.02
phase0 processSlashings - 250000 worstcase 61.604 us/op 61.112 us/op 1.01
altair processSyncCommitteeUpdates - 250000 13.420 ms/op 10.244 ms/op 1.31
BeaconState.hashTreeRoot - No change 168.00 ns/op 415.00 ns/op 0.40
BeaconState.hashTreeRoot - 1 full validator 84.534 us/op 70.774 us/op 1.19
BeaconState.hashTreeRoot - 32 full validator 898.05 us/op 856.74 us/op 1.05
BeaconState.hashTreeRoot - 512 full validator 8.2565 ms/op 7.0157 ms/op 1.18
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 94.191 us/op 88.090 us/op 1.07
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.2953 ms/op 1.4279 ms/op 0.91
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 14.937 ms/op 16.577 ms/op 0.90
BeaconState.hashTreeRoot - 1 balances 73.924 us/op 77.106 us/op 0.96
BeaconState.hashTreeRoot - 32 balances 733.92 us/op 725.07 us/op 1.01
BeaconState.hashTreeRoot - 512 balances 5.0325 ms/op 5.8230 ms/op 0.86
BeaconState.hashTreeRoot - 250000 balances 136.36 ms/op 136.15 ms/op 1.00
aggregationBits - 2048 els - zipIndexesInBitList 19.434 us/op 19.846 us/op 0.98
regular array get 100000 times 22.844 us/op 23.258 us/op 0.98
wrappedArray get 100000 times 22.593 us/op 22.958 us/op 0.98
arrayWithProxy get 100000 times 9.8609 ms/op 10.080 ms/op 0.98
ssz.Root.equals 21.282 ns/op 21.601 ns/op 0.99
byteArrayEquals 20.999 ns/op 21.286 ns/op 0.99
Buffer.compare 8.7310 ns/op 8.9630 ns/op 0.97
processSlot - 1 slots 10.579 us/op 10.761 us/op 0.98
processSlot - 32 slots 2.3891 ms/op 2.3107 ms/op 1.03
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 6.4381 ms/op 3.7273 ms/op 1.73
getCommitteeAssignments - req 1 vs - 250000 vc 1.7193 ms/op 1.6893 ms/op 1.02
getCommitteeAssignments - req 100 vs - 250000 vc 3.5147 ms/op 3.4432 ms/op 1.02
getCommitteeAssignments - req 1000 vs - 250000 vc 3.7965 ms/op 3.7382 ms/op 1.02
findModifiedValidators - 10000 modified validators 842.90 ms/op 746.05 ms/op 1.13
findModifiedValidators - 1000 modified validators 624.00 ms/op 470.64 ms/op 1.33
findModifiedValidators - 100 modified validators 382.76 ms/op 332.14 ms/op 1.15
findModifiedValidators - 10 modified validators 306.06 ms/op 159.48 ms/op 1.92
findModifiedValidators - 1 modified validators 196.64 ms/op 157.49 ms/op 1.25
findModifiedValidators - no difference 223.64 ms/op 157.81 ms/op 1.42
migrate state 1500000 validators, 3400 modified, 2000 new 2.9823 s/op 3.7236 s/op 0.80
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7100 ns/op 5.9800 ns/op 0.62
state getBlockRootAtSlot - 250000 vs - 7PWei 390.44 ns/op 402.53 ns/op 0.97
computeProposerIndex 100000 validators 1.3464 ms/op 1.4548 ms/op 0.93
getNextSyncCommitteeIndices 1000 validators 2.9247 ms/op 3.0023 ms/op 0.97
getNextSyncCommitteeIndices 10000 validators 26.372 ms/op 26.318 ms/op 1.00
getNextSyncCommitteeIndices 100000 validators 90.420 ms/op 90.358 ms/op 1.00
computeProposers - vc 250000 560.87 us/op 596.60 us/op 0.94
computeEpochShuffling - vc 250000 41.626 ms/op 40.708 ms/op 1.02
getNextSyncCommittee - vc 250000 9.6053 ms/op 11.640 ms/op 0.83
nodejs block root to RootHex using toHex 100.35 ns/op 104.53 ns/op 0.96
nodejs block root to RootHex using toRootHex 63.274 ns/op 62.328 ns/op 1.02
nodejs fromHex(blob) 807.19 us/op 1.0857 ms/op 0.74
nodejs fromHexInto(blob) 645.50 us/op 648.12 us/op 1.00
nodejs block root to RootHex using the deprecated toHexString 484.95 ns/op 507.74 ns/op 0.96
nodejs byteArrayEquals 32 bytes (block root) 26.562 ns/op 26.909 ns/op 0.99
nodejs byteArrayEquals 48 bytes (pubkey) 38.426 ns/op 39.044 ns/op 0.98
nodejs byteArrayEquals 96 bytes (signature) 34.290 ns/op 35.036 ns/op 0.98
nodejs byteArrayEquals 1024 bytes 40.849 ns/op 42.313 ns/op 0.97
nodejs byteArrayEquals 131072 bytes (blob) 1.8039 us/op 1.8314 us/op 0.98
browser block root to RootHex using toHex 150.06 ns/op 150.99 ns/op 0.99
browser block root to RootHex using toRootHex 132.83 ns/op 136.53 ns/op 0.97
browser fromHex(blob) 1.7319 ms/op 1.8576 ms/op 0.93
browser fromHexInto(blob) 624.46 us/op 658.20 us/op 0.95
browser block root to RootHex using the deprecated toHexString 342.49 ns/op 351.23 ns/op 0.98
browser byteArrayEquals 32 bytes (block root) 28.260 ns/op 28.902 ns/op 0.98
browser byteArrayEquals 48 bytes (pubkey) 39.890 ns/op 40.686 ns/op 0.98
browser byteArrayEquals 96 bytes (signature) 74.320 ns/op 76.161 ns/op 0.98
browser byteArrayEquals 1024 bytes 753.22 ns/op 781.32 ns/op 0.96
browser byteArrayEquals 131072 bytes (blob) 95.036 us/op 97.959 us/op 0.97

by benchmarkbot/action

@nflaig nflaig changed the title feat: add bid circuit breaker for gloas block production feat: add circuit breaker for gloas block production Jul 6, 2026
@nflaig

nflaig commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

merged into glamsterdam-devnet-7 for now

Comment on lines +74 to +75
* Beacon clients select randomized values from the following ranges when initializing
* the circuit breaker (so at boot time and once for each unique boot).

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 know this is just moved (existing code). But why are we randomizing these values?

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.

so that you can't game the circuit breaker, if you know the exact algorithm the builders could abuse that. this isn't that relevant since each client has it's own implementation but assuming we had 100% network share this would be needed so each proposer behaves slightly different and builders are not able to predict when local blocks will be forced by the circuit breaker

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.

added a comment here 6004e8e

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

dropped some minor comments

Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} {
let blocksPresent = 0;
let payloadsRevealed = 0;
for (const node of this.nodes) {

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.

per slot, there could be different Blocks on different branches (this happened on devnets)
so we should count on the head branch to make it more precise
we can also iterate backward from head and break when slot is out of range to save some iterations

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.

reviewed again, we should not only count on a specific branch
but looping backward is more performant, especially in non-finality time

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.

need to think about this more, it's not clear we should count unrevealed on multiple branches, I guess you could have a circuit breaker per branch

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 think counting as we currently do is most robust, added a test with a comment here

// Counting all branches is intentional, it keeps the count independent of which branch is
// head at evaluation time and errs toward local building when forks are frequent
it("counts competing blocks at the same slot on different branches", () => {

Comment thread packages/beacon-node/src/metrics/metrics/beacon.ts
vi.clearAllMocks();
});

it("builds with the builder bid when a bid is available", async () => {

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 is unrelated but obviously a bug, we should not pick builder bid always, if local payload has higher value we should pick that one instead, will be addressed by #9595

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

Comment thread packages/fork-choice/test/unit/protoArray/gloas.test.ts Outdated
@nflaig
nflaig requested review from twoeths and wemeetagain July 17, 2026 15:38
@nflaig
nflaig merged commit 9d0edba into unstable Jul 17, 2026
22 of 23 checks passed
@nflaig
nflaig deleted the nflaig/gloas-circuit-breaker branch July 17, 2026 16:29
@github-project-automation github-project-automation Bot moved this from Todo to Done in Lodestar PM Jul 17, 2026
continue;
}
blocksPresent++;
if (this.hasPayload(node.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.

What do you think calling isPayloadTimely here? It would make the circuit breaker more sensitive due to late payloads

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.

it's a consideration, need to think more about it, generally, a late payload can still become canonical if attesters vote for it and next proposer doesn't reorg, so in that case I think it would be a mistake to count these as well. the ptc becomes less reliable in bad network conditions as well since you can't reach the >50% threshold easily. that would be covered by is_payload_timely so maybe there is no harm is using that instead

let payloadsRevealed = 0;
// Iterate backward as recent blocks are at the end of the nodes array. Only break on block
// nodes below the window since FULL variants are appended late when the envelope is imported
for (let i = this.nodes.length - 1; i >= 0; i--) {

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 would also counts blocks from the side chains. Then the rboth blocksPresent and payloadsRevealted will be inflated. If there are multiple side chains.

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 think a safer heuristic is to start from head block, and traverse through the ancestors to get the reveal count

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.

counting from side chains is intended as discussed with @twoeths above, see #9598 (comment). I do think the safer heuristic is to actually consider side chains but we can discuss this more

let payloadsRevealed = 0;
// Iterate backward as recent blocks are at the end of the nodes array. Only break on block
// nodes below the window since FULL variants are appended late when the envelope is imported
for (let i = this.nodes.length - 1; i >= 0; i--) {

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.

Another thing is we should also filter out self-built blocks here. Since we are measuring the external builder reliability.
Counting self-built blocks will lead to it contribute favourably towards builder.

@nflaig nflaig Jul 18, 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.

Another thing is we should also filter out self-built blocks here

no I don't think I would support this, it's intended to also consider self-builds, in fact, any safety mechanism should consider them equal, the problem is there is nothing stopping out of protocol mechanism to allow builders to work together with proposers to get their payloads onchain via self-builds


const wasActive = this.active;
// Scale the fault budget by blocks present so sparse windows still trigger on high non-reveal rates
this.active = faults * this.faultInspectionWindow > this.allowedFaults * blocksPresent;

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 think it would be great to also have minObservedBlocks or minObservedBlocksPercentage.

If we have only observed say 4 blocks in the past 32 slots, we would probably want to activate this because chain is unhealthy.
Only if it's >= 4 blocks, that we start the calculation of blocksPresent and payloadRevealed.

Numbers here are just arbitrary, just to illustrate an example

@nflaig nflaig Jul 18, 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.

If we have only observed say 4 blocks in the past 32 slots, we would probably want to activate this because chain is unhealthy.

to be clear, the circuit breaker has nothing to do with blocks at all in gloas

nativeStateView?: boolean;
/** Builder circuit breaker fault inspection window in slots */
faultInspectionWindow?: number;
/** Allowed unrevealed payloads within the fault inspection window */

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 description is wrong though. I originally thought this means the absolute count of unrevealed payloads that triggers the circuit breaker but apparently not.

What it means is if fault rate is greater than allowedFaults / window, circuit breaker will trigger.

If window = 32, allowedFaults = 8, observedBlocks = 16, then circuit breaker activates at 5 unrevealed payloads

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 think it's better to define allowedFaultsPercentage = 0.25 to allow up to 25% fault

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.

yeah it's not 100% accurate, I also think for gloas we can even re-design the inputs a bit, I don't really like faultInspectionWindow and allowedFaults as input parameters, just kept to not overcomplicate this PR, I think we can do better here if we have more time to think how we wanna design this. Especially now that we have to consider blocks as well, it makes sense to use different semantics

}

const testCases: [string, {blocksPresent: number; payloadsRevealed: number}, boolean][] = [
["empty window", {blocksPresent: 0, payloadsRevealed: 0}, false],

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 is an interesting interaction. If we observe no block within the window, we deactivate the circuit breaker. Shouldn't we fall back to local build if this is the case?

@nflaig nflaig Jul 18, 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.

as mentioned above, blocks are irrelevant for the circuit breaker activation/deactivation, we just need to consider them to know how many payloads could have been revealed.

but to this specific case, I think if there is no data observed in the window it might be better to just keep the previous circuit breaker state, ie. if we it was active before, keep it active, and similar, if it's was deactivated, keep it deactivated

);
// allowedFaults should be < faultInspectionWindow, limiting them to faultInspectionWindow/4
const allowedFaults = Math.min(
opts.allowedFaults ?? Math.floor(faultInspectionWindow / 4),

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 would prefer ?? infinity.

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 don't get what you mean here so you are saying if opts.allowedFaults is not configured the is should be infinity?

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.

ah I think I got what you mean, sure can do that

* Beacon clients select randomized values from the following ranges when initializing
* the circuit breaker (so at boot time and once for each unique boot).
*
* ALLOWED_FAULTS: between 1 and SLOTS_PER_EPOCH // 4

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.

Allowed faults is not between 1 and 8 (mainnet), but L93 calculation says between 8 and 15

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.

ah good catch, this comment is wrong, should have read more carefully, would be concerning if allowed faults could be 1, that would be too aggressive behavior

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.

6 participants