Skip to content

chore: address builder circuit breaker review follow-ups - #9780

Merged
nflaig merged 19 commits into
unstablefrom
nflaig/circuit-breaker-polish
Aug 14, 2026
Merged

chore: address builder circuit breaker review follow-ups#9780
nflaig merged 19 commits into
unstablefrom
nflaig/circuit-breaker-polish

Conversation

@nflaig

@nflaig nflaig commented Aug 6, 2026

Copy link
Copy Markdown
Member

Motivation

Address the remaining builder circuit breaker review feedback from #9598 that does not require major design changes, these can be discussed separately here.

Description

  • activate immediately when the observed payload non-reveal rate exceeds the fault budget, but deactivate only after at least MIN_BLOCKS_TO_DEACTIVATE = 4 observed blocks are within budget
  • exclude genesis block from payload reveal counts as it's always EMPTY
  • correct documentation for the randomized inspection window and derived ~25% default fault budget, and clarify the fork-specific builder.allowedFaults CLI semantics
  • include clockSlot in circuit breaker logs

Closes #9678

@nflaig nflaig changed the title fix: address builder circuit breaker review follow-ups chore: address builder circuit breaker review follow-ups Aug 6, 2026
@nflaig
nflaig marked this pull request as ready for review August 6, 2026 12:25
@nflaig
nflaig requested a review from a team as a code owner August 6, 2026 12:25
@chatgpt-codex-connector

This comment was marked as resolved.

@nflaig
nflaig requested a review from ensi321 August 6, 2026 12:25
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.60%. Comparing base (742db20) to head (37dfad1).

Additional details and impacted files
@@            Coverage Diff            @@
##           unstable    #9780   +/-   ##
=========================================
  Coverage     52.60%   52.60%           
=========================================
  Files           848      848           
  Lines         59969    59969           
  Branches       4414     4414           
=========================================
  Hits          31545    31545           
  Misses        28365    28365           
  Partials         59       59           
🚀 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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: 630ab91 Previous: 742db20 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 1.0522 ms/op 868.51 us/op 1.21
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 40.537 us/op 39.964 us/op 1.01
BLS verify - blst 772.65 us/op 737.55 us/op 1.05
BLS verifyMultipleSignatures 3 - blst 1.3943 ms/op 1.3784 ms/op 1.01
BLS verifyMultipleSignatures 8 - blst 2.2274 ms/op 2.1870 ms/op 1.02
BLS verifyMultipleSignatures 32 - blst 7.0088 ms/op 6.8353 ms/op 1.03
BLS verifyMultipleSignatures 64 - blst 13.461 ms/op 13.554 ms/op 0.99
BLS verifyMultipleSignatures 128 - blst 26.361 ms/op 25.747 ms/op 1.02
BLS deserializing 10000 signatures 664.24 ms/op 633.09 ms/op 1.05
BLS deserializing 100000 signatures 6.6492 s/op 6.3252 s/op 1.05
BLS verifyMultipleSignatures - same message - 3 - blst 824.22 us/op 826.25 us/op 1.00
BLS verifyMultipleSignatures - same message - 8 - blst 967.81 us/op 894.26 us/op 1.08
BLS verifyMultipleSignatures - same message - 32 - blst 1.6328 ms/op 1.5638 ms/op 1.04
BLS verifyMultipleSignatures - same message - 64 - blst 2.4579 ms/op 2.3877 ms/op 1.03
BLS verifyMultipleSignatures - same message - 128 - blst 4.2286 ms/op 4.0475 ms/op 1.04
BLS aggregatePubkeys 32 - blst 18.199 us/op 17.711 us/op 1.03
BLS aggregatePubkeys 128 - blst 65.001 us/op 63.062 us/op 1.03
getSlashingsAndExits - default max 53.516 us/op 48.294 us/op 1.11
getSlashingsAndExits - 2k 390.08 us/op 338.12 us/op 1.15
proposeBlockBody type=full, size=empty 688.16 us/op 672.26 us/op 1.02
isKnown best case - 1 super set check 253.00 ns/op 176.00 ns/op 1.44
isKnown normal case - 2 super set checks 201.00 ns/op 171.00 ns/op 1.18
isKnown worse case - 16 super set checks 166.00 ns/op 174.00 ns/op 0.95
validate api signedAggregateAndProof - struct 1.5822 ms/op 1.5409 ms/op 1.03
validate gossip signedAggregateAndProof - struct 1.5966 ms/op 1.5344 ms/op 1.04
batch validate gossip attestation - vc 640000 - chunk 32 114.94 us/op 108.59 us/op 1.06
batch validate gossip attestation - vc 640000 - chunk 64 96.060 us/op 94.203 us/op 1.02
batch validate gossip attestation - vc 640000 - chunk 128 90.433 us/op 87.097 us/op 1.04
batch validate gossip attestation - vc 640000 - chunk 256 86.867 us/op 83.579 us/op 1.04
bytes32 toHexString 304.00 ns/op 302.00 ns/op 1.01
bytes32 Buffer.toString(hex) 175.00 ns/op 167.00 ns/op 1.05
bytes32 Buffer.toString(hex) from Uint8Array 244.00 ns/op 241.00 ns/op 1.01
bytes32 Buffer.toString(hex) + 0x 176.00 ns/op 164.00 ns/op 1.07
Return object 10000 times 0.22090 ns/op 0.21350 ns/op 1.03
Throw Error 10000 times 3.4650 us/op 3.3296 us/op 1.04
toHex 104.18 ns/op 102.62 ns/op 1.02
Buffer.from 90.764 ns/op 93.600 ns/op 0.97
shared Buffer 61.620 ns/op 64.079 ns/op 0.96
fastMsgIdFn sha256 / 200 bytes 1.5160 us/op 1.4840 us/op 1.02
fastMsgIdFn h32 xxhash / 200 bytes 163.00 ns/op 156.00 ns/op 1.04
fastMsgIdFn h64 xxhash / 200 bytes 219.00 ns/op 210.00 ns/op 1.04
fastMsgIdFn sha256 / 1000 bytes 4.8080 us/op 4.7790 us/op 1.01
fastMsgIdFn h32 xxhash / 1000 bytes 254.00 ns/op 247.00 ns/op 1.03
fastMsgIdFn h64 xxhash / 1000 bytes 258.00 ns/op 261.00 ns/op 0.99
fastMsgIdFn sha256 / 10000 bytes 42.587 us/op 42.370 us/op 1.01
fastMsgIdFn h32 xxhash / 10000 bytes 1.2850 us/op 1.2750 us/op 1.01
fastMsgIdFn h64 xxhash / 10000 bytes 825.00 ns/op 838.00 ns/op 0.98
send data - 1000 256B messages 4.3788 ms/op 4.3894 ms/op 1.00
send data - 1000 512B messages 5.4852 ms/op 5.0190 ms/op 1.09
send data - 1000 1024B messages 6.5810 ms/op 5.2715 ms/op 1.25
send data - 1000 1200B messages 6.4748 ms/op 6.7043 ms/op 0.97
send data - 1000 2048B messages 10.294 ms/op 9.3945 ms/op 1.10
send data - 1000 4096B messages 124.95 ms/op 44.818 ms/op 2.79
send data - 1000 16384B messages 522.77 ms/op 451.93 ms/op 1.16
send data - 1000 65536B messages 2.3009 s/op 1.4241 s/op 1.62
enrSubnets - fastDeserialize 64 bits 763.00 ns/op 753.00 ns/op 1.01
enrSubnets - ssz BitVector 64 bits 275.00 ns/op 271.00 ns/op 1.01
enrSubnets - fastDeserialize 4 bits 104.00 ns/op 107.00 ns/op 0.97
enrSubnets - ssz BitVector 4 bits 279.00 ns/op 277.00 ns/op 1.01
prioritizePeers score -10:0 att 32-0.1 sync 2-0 213.29 us/op 207.01 us/op 1.03
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 243.34 us/op 235.33 us/op 1.03
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 360.38 us/op 345.29 us/op 1.04
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 629.44 us/op 604.66 us/op 1.04
prioritizePeers score 0:0 att 64-1 sync 4-1 727.96 us/op 695.91 us/op 1.05
array of 16000 items push then shift 1.3971 us/op 1.3165 us/op 1.06
LinkedList of 16000 items push then shift 7.7100 ns/op 6.8030 ns/op 1.13
array of 16000 items push then pop 85.272 ns/op 69.855 ns/op 1.22
LinkedList of 16000 items push then pop 6.2970 ns/op 6.0600 ns/op 1.04
array of 24000 items push then shift 1.9923 us/op 1.9525 us/op 1.02
LinkedList of 24000 items push then shift 7.3030 ns/op 6.4480 ns/op 1.13
array of 24000 items push then pop 118.60 ns/op 98.316 ns/op 1.21
LinkedList of 24000 items push then pop 6.2620 ns/op 6.0300 ns/op 1.04
intersect bitArray bitLen 8 3.9630 ns/op 3.8900 ns/op 1.02
intersect array and set length 8 30.338 ns/op 30.351 ns/op 1.00
intersect bitArray bitLen 128 24.300 ns/op 23.593 ns/op 1.03
intersect array and set length 128 530.93 ns/op 510.90 ns/op 1.04
bitArray.getTrueBitIndexes() bitLen 128 993.00 ns/op 926.00 ns/op 1.07
bitArray.getTrueBitIndexes() bitLen 248 1.8580 us/op 1.6690 us/op 1.11
bitArray.getTrueBitIndexes() bitLen 512 3.9420 us/op 3.4620 us/op 1.14
Full columns - reconstruct all 6 blobs 200.91 us/op 143.62 us/op 1.40
Full columns - reconstruct half of the blobs out of 6 104.74 us/op 69.023 us/op 1.52
Full columns - reconstruct single blob out of 6 39.720 us/op 37.703 us/op 1.05
Half columns - reconstruct all 6 blobs 411.10 ms/op 383.74 ms/op 1.07
Half columns - reconstruct half of the blobs out of 6 208.62 ms/op 192.25 ms/op 1.09
Half columns - reconstruct single blob out of 6 73.425 ms/op 67.744 ms/op 1.08
Set add up to 64 items then delete first 1.7074 us/op 1.7119 us/op 1.00
OrderedSet add up to 64 items then delete first 2.6024 us/op 2.5726 us/op 1.01
Set add up to 64 items then delete last 1.9158 us/op 1.9242 us/op 1.00
OrderedSet add up to 64 items then delete last 2.8545 us/op 2.8350 us/op 1.01
Set add up to 64 items then delete middle 1.9179 us/op 1.9161 us/op 1.00
OrderedSet add up to 64 items then delete middle 4.3851 us/op 4.3026 us/op 1.02
Set add up to 128 items then delete first 3.8183 us/op 3.8395 us/op 0.99
OrderedSet add up to 128 items then delete first 5.8382 us/op 5.7752 us/op 1.01
Set add up to 128 items then delete last 3.6591 us/op 3.6754 us/op 1.00
OrderedSet add up to 128 items then delete last 5.7970 us/op 5.4639 us/op 1.06
Set add up to 128 items then delete middle 3.9792 us/op 3.7059 us/op 1.07
OrderedSet add up to 128 items then delete middle 12.670 us/op 11.478 us/op 1.10
Set add up to 256 items then delete first 7.5175 us/op 7.6495 us/op 0.98
OrderedSet add up to 256 items then delete first 12.346 us/op 11.726 us/op 1.05
Set add up to 256 items then delete last 7.2291 us/op 7.2666 us/op 0.99
OrderedSet add up to 256 items then delete last 11.534 us/op 11.159 us/op 1.03
Set add up to 256 items then delete middle 7.2565 us/op 8.0053 us/op 0.91
OrderedSet add up to 256 items then delete middle 36.126 us/op 34.541 us/op 1.05
runFastConfirmationRules vc:100000 bc:96 eq:0 4.8554 ms/op 5.1585 ms/op 0.94
runFastConfirmationRules vc:600000 bc:96 eq:0 35.906 ms/op 34.400 ms/op 1.04
runFastConfirmationRules vc:1000000 bc:96 eq:0 59.156 ms/op 57.573 ms/op 1.03
runFastConfirmationRules vc:600000 bc:320 eq:0 36.147 ms/op 34.463 ms/op 1.05
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.2103 s/op 1.1343 s/op 1.07
pass gossip attestations to forkchoice per slot 2.6942 ms/op 2.5680 ms/op 1.05
forkChoice updateHead vc 100000 bc 64 eq 0 442.37 us/op 455.84 us/op 0.97
forkChoice updateHead vc 600000 bc 64 eq 0 2.5412 ms/op 2.7015 ms/op 0.94
forkChoice updateHead vc 1000000 bc 64 eq 0 4.3285 ms/op 4.5334 ms/op 0.95
forkChoice updateHead vc 600000 bc 320 eq 0 2.5410 ms/op 2.7232 ms/op 0.93
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5964 ms/op 2.7376 ms/op 0.95
forkChoice updateHead vc 600000 bc 7200 eq 0 2.9750 ms/op 3.1142 ms/op 0.96
forkChoice updateHead vc 600000 bc 64 eq 1000 2.5789 ms/op 2.7109 ms/op 0.95
forkChoice updateHead vc 600000 bc 64 eq 10000 2.6869 ms/op 2.8142 ms/op 0.95
forkChoice updateHead vc 600000 bc 64 eq 300000 7.2001 ms/op 7.1228 ms/op 1.01
computeDeltas 1400000 validators 0% inactive 12.541 ms/op 13.374 ms/op 0.94
computeDeltas 1400000 validators 10% inactive 11.916 ms/op 12.583 ms/op 0.95
computeDeltas 1400000 validators 20% inactive 11.272 ms/op 11.916 ms/op 0.95
computeDeltas 1400000 validators 50% inactive 9.2400 ms/op 9.6791 ms/op 0.95
computeDeltas 2100000 validators 0% inactive 18.876 ms/op 20.499 ms/op 0.92
computeDeltas 2100000 validators 10% inactive 17.974 ms/op 19.161 ms/op 0.94
computeDeltas 2100000 validators 20% inactive 16.870 ms/op 18.604 ms/op 0.91
computeDeltas 2100000 validators 50% inactive 12.429 ms/op 14.681 ms/op 0.85
altair processAttestation - 250000 vs - 7PWei normalcase 1.7380 ms/op 1.6122 ms/op 1.08
altair processAttestation - 250000 vs - 7PWei worstcase 2.4960 ms/op 2.4348 ms/op 1.03
altair processAttestation - setStatus - 1/6 committees join 103.97 us/op 104.80 us/op 0.99
altair processAttestation - setStatus - 1/3 committees join 207.31 us/op 207.61 us/op 1.00
altair processAttestation - setStatus - 1/2 committees join 286.67 us/op 290.25 us/op 0.99
altair processAttestation - setStatus - 2/3 committees join 368.73 us/op 388.39 us/op 0.95
altair processAttestation - setStatus - 4/5 committees join 524.07 us/op 520.25 us/op 1.01
altair processAttestation - setStatus - 100% committees join 624.49 us/op 623.47 us/op 1.00
altair processBlock - 250000 vs - 7PWei normalcase 3.0502 ms/op 3.0018 ms/op 1.02
altair processBlock - 250000 vs - 7PWei normalcase hashState 13.239 ms/op 12.340 ms/op 1.07
altair processBlock - 250000 vs - 7PWei worstcase 19.779 ms/op 20.823 ms/op 0.95
altair processBlock - 250000 vs - 7PWei worstcase hashState 40.531 ms/op 41.838 ms/op 0.97
phase0 processBlock - 250000 vs - 7PWei normalcase 1.3262 ms/op 1.3243 ms/op 1.00
phase0 processBlock - 250000 vs - 7PWei worstcase 17.658 ms/op 17.252 ms/op 1.02
altair processEth1Data - 250000 vs - 7PWei normalcase 308.29 us/op 299.76 us/op 1.03
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 5.1980 us/op 6.0770 us/op 0.86
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 22.195 us/op 26.779 us/op 0.83
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 7.0970 us/op 6.5890 us/op 1.08
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 4.3110 us/op 5.6140 us/op 0.77
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 103.51 us/op 96.505 us/op 1.07
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4935 ms/op 1.3897 ms/op 1.07
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.9826 ms/op 1.8397 ms/op 1.08
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 2.0400 ms/op 1.8283 ms/op 1.12
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 4.3840 ms/op 3.7648 ms/op 1.16
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.4001 ms/op 2.1032 ms/op 1.14
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.5246 ms/op 4.0732 ms/op 1.11
Tree 40 250000 create 334.76 ms/op 319.19 ms/op 1.05
Tree 40 250000 get(125000) 96.690 ns/op 102.79 ns/op 0.94
Tree 40 250000 set(125000) 1.0841 us/op 1.0834 us/op 1.00
Tree 40 250000 toArray() 25.332 ms/op 9.6809 ms/op 2.62
Tree 40 250000 iterate all - toArray() + loop 20.061 ms/op 9.6985 ms/op 2.07
Tree 40 250000 iterate all - get(i) 52.312 ms/op 36.500 ms/op 1.43
Array 250000 create 2.8063 ms/op 2.2675 ms/op 1.24
Array 250000 clone - spread 865.40 us/op 733.16 us/op 1.18
Array 250000 get(125000) 0.30500 ns/op 0.30500 ns/op 1.00
Array 250000 set(125000) 0.30900 ns/op 0.31300 ns/op 0.99
Array 250000 iterate all - loop 59.378 us/op 59.475 us/op 1.00
phase0 afterProcessEpoch - 250000 vs - 7PWei 41.269 ms/op 42.632 ms/op 0.97
Array.fill - length 1000000 4.1975 ms/op 3.5528 ms/op 1.18
Array push - length 1000000 14.685 ms/op 13.729 ms/op 1.07
Array.get 0.21182 ns/op 0.21917 ns/op 0.97
Uint8Array.get 0.23988 ns/op 0.24762 ns/op 0.97
phase0 beforeProcessEpoch - 250000 vs - 7PWei 17.359 ms/op 12.908 ms/op 1.34
altair processEpoch - mainnet_e81889 350.14 ms/op 294.92 ms/op 1.19
mainnet_e81889 - altair beforeProcessEpoch 42.820 ms/op 41.707 ms/op 1.03
mainnet_e81889 - altair processJustificationAndFinalization 19.913 us/op 11.165 us/op 1.78
mainnet_e81889 - altair processInactivityUpdates 4.0384 ms/op 3.9244 ms/op 1.03
mainnet_e81889 - altair processRewardsAndPenalties 23.313 ms/op 21.856 ms/op 1.07
mainnet_e81889 - altair processRegistryUpdates 762.00 ns/op 1.2990 us/op 0.59
mainnet_e81889 - altair processSlashings 148.00 ns/op 240.00 ns/op 0.62
mainnet_e81889 - altair processEth1DataReset 164.00 ns/op 176.00 ns/op 0.93
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.8079 ms/op 1.3352 ms/op 1.35
mainnet_e81889 - altair processSlashingsReset 757.00 ns/op 1.2860 us/op 0.59
mainnet_e81889 - altair processRandaoMixesReset 2.0270 us/op 1.6530 us/op 1.23
mainnet_e81889 - altair processHistoricalRootsUpdate 142.00 ns/op 219.00 ns/op 0.65
mainnet_e81889 - altair processParticipationFlagUpdates 499.00 ns/op 492.00 ns/op 1.01
mainnet_e81889 - altair processSyncCommitteeUpdates 115.00 ns/op 120.00 ns/op 0.96
mainnet_e81889 - altair afterProcessEpoch 42.505 ms/op 42.012 ms/op 1.01
capella processEpoch - mainnet_e217614 852.77 ms/op 734.97 ms/op 1.16
mainnet_e217614 - capella beforeProcessEpoch 89.085 ms/op 58.151 ms/op 1.53
mainnet_e217614 - capella processJustificationAndFinalization 33.696 ms/op 5.2800 us/op 6381.81
mainnet_e217614 - capella processInactivityUpdates 15.357 ms/op 11.571 ms/op 1.33
mainnet_e217614 - capella processRewardsAndPenalties 94.390 ms/op 85.885 ms/op 1.10
mainnet_e217614 - capella processRegistryUpdates 4.5270 us/op 4.6210 us/op 0.98
mainnet_e217614 - capella processSlashings 145.00 ns/op 122.00 ns/op 1.19
mainnet_e217614 - capella processEth1DataReset 139.00 ns/op 145.00 ns/op 0.96
mainnet_e217614 - capella processEffectiveBalanceUpdates 8.8550 ms/op 6.9471 ms/op 1.27
mainnet_e217614 - capella processSlashingsReset 717.00 ns/op 722.00 ns/op 0.99
mainnet_e217614 - capella processRandaoMixesReset 1.2620 us/op 1.4770 us/op 0.85
mainnet_e217614 - capella processHistoricalRootsUpdate 139.00 ns/op 147.00 ns/op 0.95
mainnet_e217614 - capella processParticipationFlagUpdates 463.00 ns/op 478.00 ns/op 0.97
mainnet_e217614 - capella afterProcessEpoch 112.91 ms/op 111.02 ms/op 1.02
phase0 processEpoch - mainnet_e58758 347.57 ms/op 279.38 ms/op 1.24
mainnet_e58758 - phase0 beforeProcessEpoch 90.895 ms/op 66.266 ms/op 1.37
mainnet_e58758 - phase0 processJustificationAndFinalization 8.3020 us/op 6.9220 us/op 1.20
mainnet_e58758 - phase0 processRewardsAndPenalties 19.096 ms/op 16.333 ms/op 1.17
mainnet_e58758 - phase0 processRegistryUpdates 3.1100 us/op 2.3380 us/op 1.33
mainnet_e58758 - phase0 processSlashings 147.00 ns/op 315.00 ns/op 0.47
mainnet_e58758 - phase0 processEth1DataReset 343.00 ns/op 149.00 ns/op 2.30
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 842.77 us/op 934.17 us/op 0.90
mainnet_e58758 - phase0 processSlashingsReset 1.2440 us/op 904.00 ns/op 1.38
mainnet_e58758 - phase0 processRandaoMixesReset 1.5460 us/op 1.4340 us/op 1.08
mainnet_e58758 - phase0 processHistoricalRootsUpdate 141.00 ns/op 146.00 ns/op 0.97
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.4030 us/op 1.1210 us/op 1.25
mainnet_e58758 - phase0 afterProcessEpoch 34.637 ms/op 34.367 ms/op 1.01
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.1302 ms/op 1.2845 ms/op 0.88
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.7520 ms/op 1.6645 ms/op 1.05
altair processInactivityUpdates - 250000 normalcase 11.390 ms/op 11.059 ms/op 1.03
altair processInactivityUpdates - 250000 worstcase 11.770 ms/op 14.857 ms/op 0.79
phase0 processRegistryUpdates - 250000 normalcase 3.4440 us/op 3.9730 us/op 0.87
phase0 processRegistryUpdates - 250000 badcase_full_deposits 146.68 us/op 151.02 us/op 0.97
phase0 processRegistryUpdates - 250000 worstcase 0.5 65.580 ms/op 60.959 ms/op 1.08
altair processRewardsAndPenalties - 250000 normalcase 16.705 ms/op 13.004 ms/op 1.28
altair processRewardsAndPenalties - 250000 worstcase 15.215 ms/op 15.091 ms/op 1.01
phase0 getAttestationDeltas - 250000 normalcase 5.8427 ms/op 13.359 ms/op 0.44
phase0 getAttestationDeltas - 250000 worstcase 5.9286 ms/op 5.7150 ms/op 1.04
phase0 processSlashings - 250000 worstcase 60.616 us/op 68.426 us/op 0.89
altair processSyncCommitteeUpdates - 250000 10.625 ms/op 11.562 ms/op 0.92
BeaconState.hashTreeRoot - No change 177.00 ns/op 196.00 ns/op 0.90
BeaconState.hashTreeRoot - 1 full validator 71.027 us/op 79.182 us/op 0.90
BeaconState.hashTreeRoot - 32 full validator 804.59 us/op 922.25 us/op 0.87
BeaconState.hashTreeRoot - 512 full validator 6.6216 ms/op 8.3369 ms/op 0.79
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 80.584 us/op 107.10 us/op 0.75
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.3227 ms/op 1.5516 ms/op 0.85
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 15.380 ms/op 18.227 ms/op 0.84
BeaconState.hashTreeRoot - 1 balances 76.495 us/op 81.294 us/op 0.94
BeaconState.hashTreeRoot - 32 balances 699.71 us/op 802.03 us/op 0.87
BeaconState.hashTreeRoot - 512 balances 5.5060 ms/op 6.4903 ms/op 0.85
BeaconState.hashTreeRoot - 250000 balances 119.94 ms/op 139.14 ms/op 0.86
aggregationBits - 2048 els - zipIndexesInBitList 19.932 us/op 21.047 us/op 0.95
regular array get 100000 times 23.342 us/op 23.579 us/op 0.99
wrappedArray get 100000 times 23.318 us/op 23.505 us/op 0.99
arrayWithProxy get 100000 times 15.439 ms/op 10.668 ms/op 1.45
ssz.Root.equals 21.830 ns/op 21.965 ns/op 0.99
byteArrayEquals 21.544 ns/op 21.782 ns/op 0.99
Buffer.compare 8.9270 ns/op 9.0310 ns/op 0.99
processSlot - 1 slots 10.069 us/op 9.8620 us/op 1.02
processSlot - 32 slots 1.9769 ms/op 2.2839 ms/op 0.87
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.5191 ms/op 2.3415 ms/op 1.93
getCommitteeAssignments - req 1 vs - 250000 vc 1.6885 ms/op 1.7343 ms/op 0.97
getCommitteeAssignments - req 100 vs - 250000 vc 3.4329 ms/op 3.5503 ms/op 0.97
getCommitteeAssignments - req 1000 vs - 250000 vc 3.7024 ms/op 3.8479 ms/op 0.96
findModifiedValidators - 10000 modified validators 846.96 ms/op 767.11 ms/op 1.10
findModifiedValidators - 1000 modified validators 633.73 ms/op 528.73 ms/op 1.20
findModifiedValidators - 100 modified validators 444.18 ms/op 370.76 ms/op 1.20
findModifiedValidators - 10 modified validators 359.49 ms/op 386.82 ms/op 0.93
findModifiedValidators - 1 modified validators 239.80 ms/op 275.34 ms/op 0.87
findModifiedValidators - no difference 244.94 ms/op 549.90 ms/op 0.45
migrate state 1500000 validators, 3400 modified, 2000 new 3.1537 s/op 3.6605 s/op 0.86
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7400 ns/op 3.7400 ns/op 1.00
state getBlockRootAtSlot - 250000 vs - 7PWei 414.41 ns/op 436.50 ns/op 0.95
computeProposerIndex 100000 validators 1.3885 ms/op 1.4793 ms/op 0.94
getNextSyncCommitteeIndices 1000 validators 2.8963 ms/op 3.0598 ms/op 0.95
getNextSyncCommitteeIndices 10000 validators 25.198 ms/op 27.587 ms/op 0.91
getNextSyncCommitteeIndices 100000 validators 88.553 ms/op 93.956 ms/op 0.94
computeProposers - vc 250000 560.76 us/op 580.08 us/op 0.97
computeEpochShuffling - vc 250000 41.366 ms/op 42.132 ms/op 0.98
getNextSyncCommittee - vc 250000 9.7030 ms/op 10.325 ms/op 0.94
nodejs block root to RootHex using toHex 100.82 ns/op 103.18 ns/op 0.98
nodejs block root to RootHex using toRootHex 65.238 ns/op 66.320 ns/op 0.98
nodejs fromHex(blob) 842.37 us/op 1.0480 ms/op 0.80
nodejs fromHexInto(blob) 650.98 us/op 664.56 us/op 0.98
nodejs block root to RootHex using the deprecated toHexString 510.09 ns/op 528.17 ns/op 0.97
nodejs byteArrayEquals 32 bytes (block root) 26.401 ns/op 27.342 ns/op 0.97
nodejs byteArrayEquals 48 bytes (pubkey) 38.162 ns/op 39.723 ns/op 0.96
nodejs byteArrayEquals 96 bytes (signature) 37.370 ns/op 38.397 ns/op 0.97
nodejs byteArrayEquals 1024 bytes 46.165 ns/op 44.459 ns/op 1.04
nodejs byteArrayEquals 131072 bytes (blob) 1.7907 us/op 1.8464 us/op 0.97
browser block root to RootHex using toHex 158.93 ns/op 151.60 ns/op 1.05
browser block root to RootHex using toRootHex 143.74 ns/op 137.56 ns/op 1.04
browser fromHex(blob) 1.6412 ms/op 1.9126 ms/op 0.86
browser fromHexInto(blob) 648.12 us/op 657.10 us/op 0.99
browser block root to RootHex using the deprecated toHexString 365.78 ns/op 360.13 ns/op 1.02
browser byteArrayEquals 32 bytes (block root) 28.989 ns/op 29.211 ns/op 0.99
browser byteArrayEquals 48 bytes (pubkey) 40.783 ns/op 41.246 ns/op 0.99
browser byteArrayEquals 96 bytes (signature) 76.155 ns/op 77.203 ns/op 0.99
browser byteArrayEquals 1024 bytes 777.41 ns/op 789.49 ns/op 0.98
browser byteArrayEquals 131072 bytes (blob) 98.795 us/op 99.848 us/op 0.99

by benchmarkbot/action

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

did a quick pass

Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
// 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;
// Keep the previous state if the window has no blocks, there is no data to assess builder health
if (blocksPresent > 0) {

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 like this is the follow up of #9598 (comment) . Are you settled on 0, or just for the time being?

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, we can use a threshold of 4 or whatever for now but if there are so few blocks in the epoch it might be good to enable the circuit breaker also, there was an attack by Toni on devnet-7 which caused missed slots due to slow to process payloads he was broadcasting, so even in that case, we might not wanna accept bids and rather self-build so the network can recover

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

@ensi321 what do you think about the following 1171113, ie. activating without a min sample size, but deactivating only if more than 4 blocks (I think that's the number you wanted?)

@ensi321 ensi321 Aug 14, 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.

I think 4 is good on mainnet, but we probably want 2 for minimal. But if we don't want to complicate things, we can just hardcode MIN_BLOCKS_TO_DEACTIVATE to 2 regardless of what config.

I am actually also okay with how you originally did

    if (blocksPresent > 0) {  
      // 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;
    }

if you want to keep things simple

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

I think 4 is good on mainnet, but we probably want 2 for minimal

even minimal, I would just keep 4, because 2 doesn't give you a meaningful sample for ~25% fault rate

with 2 it's basically

  • 0/2 faults = deactivate
  • 1/2 faults = keep active
  • 2/2 faults = keep active

with 4 you have

  • 0/4 faults = deactive
  • 1/4 faults = deactive
  • 2/4 faults = keep active
  • 3/4 faults = keep active
  • 4/4 faults = keep active

this isn't really meaningful, I would maybe even increase 4 to 8 to make sample more meaningful, also since this only applied for deactivating the circuit breaker, it's more conservative which I like here

Comment thread packages/cli/src/options/beaconNodeOptions/builder.ts
@ensi321

ensi321 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Closes #9678

Does this PR fully close #9678? Any outstanding things that are not fixed in this PR?

@nflaig

nflaig commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Closes #9678

Does this PR fully close #9678? Any outstanding things that are not fixed in this PR?

the issue doesn't document anything meaningful, eg. I left a comment here myself #9598 (comment) which blocks we should count, so right now we don't check if block was strong, but that's a separate topic, not part of the comments you left

there is also open design space like #9598 (comment) and #9598 (comment)

I am not sure we gonna figure this out now until we see how we wanna deal with per-builder blacklisting/whitelisting if at all

so yes, the issue states

Address #9598 (review)

which this PR should fully resolve, bigger design decisions need more broad discussion and iteration on the circuit breaker itself

so I left some breadcrumbs on the PR for that, I'd rather open a proper issue and document open design decisions there

Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
@nflaig
nflaig requested a review from ensi321 August 12, 2026 20:44
Math.max(clockSlot - this.faultInspectionWindow, 0),
clockSlot - 1
);
const faults = blocksPresent - payloadsRevealed;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if there are proposer equivocations, there are 2 blocks per slot, but there is only 1 payload, and it's the canonical
the current implementation counts that as a fault, which is not correct. But I'm not sure if we need to be that precise, the case is so rare.

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.

You're right. getPayloadRevealCounts counts each PENDING beacon block by its own blockRoot with no per-slot dedup (protoArray.ts:711), so two equivocating blocks at the same slot both increment blocksPresent, while only the canonical one gets a FULL variant via onExecutionPayload — so hasPayloadpayloadsRevealed maxes at 1. Net +1 fault for that slot even though the canonical payload was revealed. Confirmed.

Agree it's negligible in practice: equivocation is slashable and rare, and the +1 sits inside the scaled tolerance (faults * window > allowedFaults * blocksPresent, ~25% over a 32-64 block window), so a single spurious fault won't move the breaker unless it's already right at the threshold.

Worth noting it's the same class as the "only count canonical/strong blocks" gap from the anchor thread — the non-canonical equivocation block is exactly one the builder was right not to reveal for. A canonical-only filter would fix both this and the non-canonical side-branch case in one go; a cheaper per-slot dedup would cover just the equivocation. I'd fold it into that follow-up rather than special-case it here, but it's an easy either/or — your + @nflaig's call on whether the precision is worth it now.

@nflaig nflaig Aug 13, 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 there are proposer equivocations, there are 2 blocks per slot, but there is only 1 payload, and it's the canonical

yes this is another problem of going through all nodes in the proto array, I feel like this makes the implementation quite complex, I am thinking about doing getAllAncestorNodes(headRoot, headPayloadStatus) instead as also suggested by @ensi321, this seems much simpler and more easy to get right, and we call builderCircuitBreaker.update(clockSlot) inside prepareNextSlot after recomputeForkChoiceHead so it's seems fine to just consider our canonical branch?

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.

Agree — I think this is the right move: simpler and more correct, and it collapses Thread D + the "canonical-only" follow-up into one change.

Verified on HEAD af6864fb:

It does what you want. getAllAncestorNodes(blockRoot, payloadStatus) (protoArray.ts:1806, wrapped in forkChoice.ts:1307) walks parent pointers via getParentNodeIndexgetParentPayloadStatus, so it returns exactly one node per ancestor block, each as its canonical EMPTY/FULL variant (never PENDING). So blocksPresent = ancestors in window, payloadsRevealed = the FULL ones — no hasPayload/PENDING-dedup dance needed.

Fixes Thread D for free, as you said: one ancestor chain has a single block per slot by construction, so equivocating siblings (and any non-canonical side branch) are off-branch. That's also the entirety of the "only count canonical/strong blocks" follow-up — so this subsumes it rather than being a separate change.

Call order checks out. update(clockSlot) runs at prepareNextSlot.ts:156, after recomputeForkChoiceHead at :84, inside the if (feeRecipient) (we're proposing) block — so the fork-choice head is fresh canonical there; walking forkChoice.getHead() is sound.

A few things to keep/get right:

  • Still window-bound it. getAllAncestorNodes returns the full chain back to the anchor, so keep the [clockSlot - faultInspectionWindow, clockSlot - 1] filter — bonus: ancestors are in strictly decreasing slot order, so you can early-break once slot < fromSlot (the current full scan explicitly can't). Under normal finality the anchor is ≥2 epochs back, so the window never runs past it.
  • Keep the genesis skip (bbce592). Genesis is an ancestor of head and, on a fresh net, in-window; it shows up as EMPTY, so it'd still count as a fault without slot === GENESIS_SLOT.
  • Head variant. Passing the head's PENDING status drops the head node itself (getAllAncestorNodes skips a PENDING start node, protoArray.ts:1823) and starts from its ancestors — which lines up with your existing "exclude clockSlot, reveal may be in flight." Just worth being deliberate about which headPayloadStatus you pass.

One minor semantic upgrade to be aware of (not a problem): the ancestor walk counts a block as revealed only if the canonical child actually built on its FULL (via getParentPayloadStatus), vs the current hasPayload = "a FULL variant exists anywhere." So a payload revealed too late for the chain to build on reads as EMPTY here — arguably the more accurate builder-health signal anyway.

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.

@lodekeeper please review #9815, does that look good?

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.

Looks good — reviewed #9815 end to end, the ancestor-walk approach is correctly implemented and it covers everything from this thread.

Verified (head 5bed75eb):

  • getCanonicalPayloadCounts(fromSlot, toSlot, headRoot, headPayloadStatus) walks getAllAncestorNodes and counts canonical FULL vs EMPTY with faults = empty. The three things I flagged earlier are all handled: window bound kept via the fromSlot/toSlot filter, the genesis skip is retained, and a PENDING head is excluded (start-node drop + the explicit payloadStatus === PENDING guard), with a new test for it.
  • The "keeps EMPTY after a late FULL arrives" test is the important one — it nails the devnet-7 case: a late payload for 0x03 no longer masks the canonical EMPTY that 0x05 built on, and competing siblings (0x04/0x06) drop out for free, so Thread D's equivocation double-count is gone too.
  • !isGloasBlock correctly excludes pre-gloas ancestors at the fork boundary.
  • this.head.payloadStatus wiring matches the existing iterateAncestorNodes call sites, so that's idiomatic.
  • Rename is clean (no dangling getPayloadRevealCounts/payloadsRevealed), and payloads_revealed → payloads_full is safe — nothing references the metric (no dashboards, unreleased). Dropping the startup-log assertion test is fine, it was a brittle low-value check.

Two minor, non-blocking notes:

  1. The genesis skip (node.slot === GENESIS_SLOT) is still load-bearing — on a gloas-genesis net genesis is an in-window ancestor returned as EMPTY, so without it genesis counts as a fault — but its dedicated test ("does not count the genesis block") was dropped in the rewrite. Might be worth re-adding a small one so that guard doesn't silently regress.
  2. Optional: getAllAncestorNodes materializes the whole chain back to the anchor each update; since ancestors are slot-descending and you already have the iterateAncestorNodes generator, you could iterate + break once slot < fromSlot to keep it O(window). Negligible under normal finality, matters more under prolonged non-finality.

Neither blocks it. 👍

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.

@lodekeeper please leave your review on the PR and leave in-line comments there for any feedback

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.

Done — submitted a formal review with inline notes: #9780 (review). No blocking concerns; it summarizes the A–D threads (genesis skip, hysteresis, JSDoc sync, getFaultInspectionParams simplification) and notes the equivocation edge @twoeths raised is handled by the stacked #9815.

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 also meant you should review #9815 and leave comments there please

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.

Done — the review is now on #9815 directly: #9815 (review) (looks good, no blocking concerns + 2 non-blocking inline notes: re-add the dropped genesis-guard test, and an optional early-break via the iterateAncestorNodes generator). Also +1'd your resolution of the codex synthetic-anchor P2.

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

Did a full pass — no blocking concerns from me. The substantive points all got worked through in the inline threads above; summarizing for the record:

  • Genesis skip (Thread A) — narrowing from parent === undefined to slot === GENESIS_SLOT is correct: genesis is the only anchor reliably in-window on a fresh gloas net, it isn't builder-produced, and it's always EMPTY, so counting it would be a spurious fault. Now has a dedicated test.
  • Activate/deactivate hysteresis (Thread B) — asymmetric budget (trip on any breach, recover only with ≥MIN_BLOCKS_TO_DEACTIVATE in-budget observations) reads correctly and is covered by the new "requires a minimum sample to deactivate" test. The exact 4 is @ensi321's call from his thread.
  • JSDoc / option-description sync (Thread C) — options.ts, the CLI --builder.allowedFaults help, and getFaultInspectionParams now line up, and the "~25% budget = window // 4" framing is accurate.
  • getFaultInspectionParams simplification?? Infinity capped at window // 4 is equivalent to the old double-floor and reads cleaner.
  • Equivocation double-count (Thread D, @twoeths) — real but negligible in this all-branches scan (one spurious fault, absorbed by the scaled budget); the canonical-only refinement is the stacked #9815, which I've reviewed separately ("looks good").

LGTM. A few inline notes anchoring the above.

Comment thread packages/fork-choice/src/protoArray/protoArray.ts
Comment thread packages/beacon-node/src/chain/builderCircuitBreaker.ts
Comment thread packages/beacon-node/src/execution/builder/http.ts
@nflaig
nflaig requested a review from twoeths August 13, 2026 21:40
@nflaig
nflaig merged commit 90204c2 into unstable Aug 14, 2026
25 of 27 checks passed
@nflaig
nflaig deleted the nflaig/circuit-breaker-polish branch August 14, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Address post-merge review for #9598

4 participants