Skip to content

fix: count canonical FULL and EMPTY blocks in circuit breaker - #9815

Merged
nflaig merged 33 commits into
unstablefrom
nflaig/circuit-breaker-canonical-counts
Aug 14, 2026
Merged

fix: count canonical FULL and EMPTY blocks in circuit breaker#9815
nflaig merged 33 commits into
unstablefrom
nflaig/circuit-breaker-canonical-counts

Conversation

@nflaig

@nflaig nflaig commented Aug 13, 2026

Copy link
Copy Markdown
Member

Motivation

Currently, the builder circuit breaker counts revealed payloads from all branches. On glamsterdam-devnet-7, orphaned payloads masked canonical EMPTY blocks and prevented the circuit breaker from activating. The problem is we only check if payload exists via hasPayload but instead we should walk the ancestry to know whether the node is FULL or EMPTY, but this becomes more complicated if we wanna track all branches, so opting to only track the canonical branch for now.

There was also a discussion here #9598 (comment)

start from head block, and traverse through the ancestors to get the reveal count

which this PR implements for now, I do still think it would be best to track all branches but it requires a more sophisticated algorithm and tracking data, especially if we wanna do per-builder banning, eg. you will need to track the attestation votes of blocks (ie. if block was strong meaning >60% quorum was reached) and you also need to do an ancestor walk for each branch via getAllAncestorNodes

Description

  • count FULL and EMPTY blocks on the canonical branch
  • treat canonical EMPTY blocks as faults
  • expose canonical, FULL, and EMPTY block counts in circuit breaker metrics
  • log the resolved circuit breaker configuration on startup

@nflaig
nflaig marked this pull request as ready for review August 13, 2026 15:12
@nflaig
nflaig requested a review from a team as a code owner August 13, 2026 15:12
@nflaig
nflaig marked this pull request as draft August 13, 2026 15:13

@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: 5bed75eb3e

ℹ️ 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 Outdated

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

Reviewed the ancestor-walk refactor end to end — looks good, no blocking concerns. (I'd posted this verdict on #9780 earlier; moving it here where it belongs.)

The core is correct: getCanonicalPayloadCounts walks getAllAncestorNodes(head, headPayloadStatus) and counts canonical FULL vs EMPTY with faults = empty. It fixes the real devnet-7 case — a late/orphaned payload no longer masks a canonical EMPTY (the "keeps EMPTY after a late FULL arrives" test nails it) — and drops the equivocation double-count for free (one block/slot per branch). !isGloasBlock correctly excludes pre-gloas ancestors, the this.head.payloadStatus wiring matches the existing iterateAncestorNodes call sites, and the metric rename (payloads_revealedpayloads_full) is safe (no dashboard refs, unreleased).

+1 on your take on the codex P2 (synthetic anchor variants): same class as the genesis case — it self-resolves as the real child syncs (the walk picks the FULL variant once a real child builds on FULL), and the breaker only runs in prepareNextSlot near head, by which point those checkpoint-era synthetic nodes have aged out of the window. Not a blocker.

Two minor, non-blocking notes inline. 👍

Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
Comment thread packages/fork-choice/src/protoArray/protoArray.ts Outdated
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.60%. Comparing base (a7f9aed) to head (5bed75e).
⚠️ Report is 32 commits behind head on nflaig/circuit-breaker-polish.

Additional details and impacted files
@@                      Coverage Diff                       @@
##           nflaig/circuit-breaker-polish    #9815   +/-   ##
==============================================================
  Coverage                          52.60%   52.60%           
==============================================================
  Files                                848      848           
  Lines                              60047    60047           
  Branches                            4424     4424           
==============================================================
  Hits                               31587    31587           
  Misses                             28401    28401           
  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.

@nflaig
nflaig marked this pull request as ready for review August 13, 2026 21:40
@nflaig nflaig added the status-do-not-merge Merging this issue will break the build. Do not merge! label Aug 13, 2026
@nflaig

nflaig commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

this is ready for review but should be merged separately and is currently stacked on top of #9780

@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: 5bed75eb3e

ℹ️ 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/metrics/metrics/beacon.ts Outdated
Base automatically changed from nflaig/circuit-breaker-polish to unstable August 14, 2026 06:45
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Performance Report

🚀🚀 Significant benchmark improvement detected

Benchmark suite Current: 5d7f535 Previous: 2a04194 Ratio
isKnown best case - 1 super set check 163.00 ns/op 631.00 ns/op 0.26
isKnown normal case - 2 super set checks 179.00 ns/op 622.00 ns/op 0.29
isKnown worse case - 16 super set checks 165.00 ns/op 623.00 ns/op 0.26
bytes32 Buffer.toString(hex) 174.00 ns/op 590.00 ns/op 0.29
bytes32 Buffer.toString(hex) + 0x 177.00 ns/op 588.00 ns/op 0.30
fastMsgIdFn h32 xxhash / 200 bytes 158.00 ns/op 582.00 ns/op 0.27
enrSubnets - fastDeserialize 4 bits 102.00 ns/op 508.00 ns/op 0.20
BeaconState.hashTreeRoot - No change 173.00 ns/op 636.00 ns/op 0.27
Full benchmark results
Benchmark suite Current: 5d7f535 Previous: 2a04194 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 884.67 us/op 906.62 us/op 0.98
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 39.927 us/op 39.448 us/op 1.01
BLS verify - blst 755.31 us/op 748.31 us/op 1.01
BLS verifyMultipleSignatures 3 - blst 1.3647 ms/op 1.3862 ms/op 0.98
BLS verifyMultipleSignatures 8 - blst 2.1715 ms/op 2.1776 ms/op 1.00
BLS verifyMultipleSignatures 32 - blst 6.8546 ms/op 6.7784 ms/op 1.01
BLS verifyMultipleSignatures 64 - blst 13.198 ms/op 12.937 ms/op 1.02
BLS verifyMultipleSignatures 128 - blst 25.716 ms/op 25.414 ms/op 1.01
BLS deserializing 10000 signatures 638.34 ms/op 637.19 ms/op 1.00
BLS deserializing 100000 signatures 6.5335 s/op 6.3598 s/op 1.03
BLS verifyMultipleSignatures - same message - 3 - blst 772.82 us/op 807.92 us/op 0.96
BLS verifyMultipleSignatures - same message - 8 - blst 957.37 us/op 909.98 us/op 1.05
BLS verifyMultipleSignatures - same message - 32 - blst 1.5777 ms/op 1.5525 ms/op 1.02
BLS verifyMultipleSignatures - same message - 64 - blst 2.4373 ms/op 2.3750 ms/op 1.03
BLS verifyMultipleSignatures - same message - 128 - blst 4.1124 ms/op 3.9759 ms/op 1.03
BLS aggregatePubkeys 32 - blst 17.931 us/op 17.763 us/op 1.01
BLS aggregatePubkeys 128 - blst 64.158 us/op 62.258 us/op 1.03
getSlashingsAndExits - default max 50.077 us/op 48.691 us/op 1.03
getSlashingsAndExits - 2k 338.20 us/op 335.32 us/op 1.01
proposeBlockBody type=full, size=empty 733.54 us/op 692.54 us/op 1.06
isKnown best case - 1 super set check 163.00 ns/op 631.00 ns/op 0.26
isKnown normal case - 2 super set checks 179.00 ns/op 622.00 ns/op 0.29
isKnown worse case - 16 super set checks 165.00 ns/op 623.00 ns/op 0.26
validate api signedAggregateAndProof - struct 1.5358 ms/op 1.5298 ms/op 1.00
validate gossip signedAggregateAndProof - struct 1.5316 ms/op 1.5317 ms/op 1.00
batch validate gossip attestation - vc 640000 - chunk 32 107.52 us/op 106.07 us/op 1.01
batch validate gossip attestation - vc 640000 - chunk 64 94.158 us/op 93.149 us/op 1.01
batch validate gossip attestation - vc 640000 - chunk 128 87.703 us/op 86.543 us/op 1.01
batch validate gossip attestation - vc 640000 - chunk 256 83.560 us/op 82.465 us/op 1.01
bytes32 toHexString 288.00 ns/op 736.00 ns/op 0.39
bytes32 Buffer.toString(hex) 174.00 ns/op 590.00 ns/op 0.29
bytes32 Buffer.toString(hex) from Uint8Array 244.00 ns/op 662.00 ns/op 0.37
bytes32 Buffer.toString(hex) + 0x 177.00 ns/op 588.00 ns/op 0.30
Return object 10000 times 0.21420 ns/op 0.24700 ns/op 0.87
Throw Error 10000 times 3.3342 us/op 3.2581 us/op 1.02
toHex 99.490 ns/op 97.718 ns/op 1.02
Buffer.from 91.038 ns/op 89.105 ns/op 1.02
shared Buffer 64.226 ns/op 60.547 ns/op 1.06
fastMsgIdFn sha256 / 200 bytes 1.4790 us/op 1.9380 us/op 0.76
fastMsgIdFn h32 xxhash / 200 bytes 158.00 ns/op 582.00 ns/op 0.27
fastMsgIdFn h64 xxhash / 200 bytes 217.00 ns/op 634.00 ns/op 0.34
fastMsgIdFn sha256 / 1000 bytes 4.7250 us/op 5.1950 us/op 0.91
fastMsgIdFn h32 xxhash / 1000 bytes 243.00 ns/op 670.00 ns/op 0.36
fastMsgIdFn h64 xxhash / 1000 bytes 263.00 ns/op 680.00 ns/op 0.39
fastMsgIdFn sha256 / 10000 bytes 41.807 us/op 42.274 us/op 0.99
fastMsgIdFn h32 xxhash / 10000 bytes 1.2820 us/op 1.6900 us/op 0.76
fastMsgIdFn h64 xxhash / 10000 bytes 833.00 ns/op 1.2550 us/op 0.66
send data - 1000 256B messages 4.0999 ms/op 4.2219 ms/op 0.97
send data - 1000 512B messages 5.4060 ms/op 5.7644 ms/op 0.94
send data - 1000 1024B messages 5.3944 ms/op 5.5268 ms/op 0.98
send data - 1000 1200B messages 6.9551 ms/op 6.7255 ms/op 1.03
send data - 1000 2048B messages 9.0808 ms/op 10.865 ms/op 0.84
send data - 1000 4096B messages 63.452 ms/op 85.002 ms/op 0.75
send data - 1000 16384B messages 245.31 ms/op 331.92 ms/op 0.74
send data - 1000 65536B messages 1.0688 s/op 1.1055 s/op 0.97
enrSubnets - fastDeserialize 64 bits 2.7980 us/op 1.2400 us/op 2.26
enrSubnets - ssz BitVector 64 bits 265.00 ns/op 707.00 ns/op 0.37
enrSubnets - fastDeserialize 4 bits 102.00 ns/op 508.00 ns/op 0.20
enrSubnets - ssz BitVector 4 bits 266.00 ns/op 694.00 ns/op 0.38
prioritizePeers score -10:0 att 32-0.1 sync 2-0 203.40 us/op 207.63 us/op 0.98
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 230.53 us/op 235.43 us/op 0.98
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 333.87 us/op 339.63 us/op 0.98
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 592.51 us/op 599.43 us/op 0.99
prioritizePeers score 0:0 att 64-1 sync 4-1 693.53 us/op 699.76 us/op 0.99
array of 16000 items push then shift 1.3194 us/op 1.3238 us/op 1.00
LinkedList of 16000 items push then shift 6.9560 ns/op 7.7270 ns/op 0.90
array of 16000 items push then pop 71.027 ns/op 83.163 ns/op 0.85
LinkedList of 16000 items push then pop 6.0320 ns/op 6.4360 ns/op 0.94
array of 24000 items push then shift 1.9296 us/op 1.9469 us/op 0.99
LinkedList of 24000 items push then shift 6.3950 ns/op 7.0170 ns/op 0.91
array of 24000 items push then pop 97.958 ns/op 114.87 ns/op 0.85
LinkedList of 24000 items push then pop 6.0430 ns/op 6.4320 ns/op 0.94
intersect bitArray bitLen 8 3.9480 ns/op 4.2690 ns/op 0.92
intersect array and set length 8 29.946 ns/op 30.113 ns/op 0.99
intersect bitArray bitLen 128 23.327 ns/op 23.608 ns/op 0.99
intersect array and set length 128 501.01 ns/op 500.73 ns/op 1.00
bitArray.getTrueBitIndexes() bitLen 128 929.00 ns/op 1.3460 us/op 0.69
bitArray.getTrueBitIndexes() bitLen 248 1.6880 us/op 2.1140 us/op 0.80
bitArray.getTrueBitIndexes() bitLen 512 3.5730 us/op 3.9810 us/op 0.90
Full columns - reconstruct all 6 blobs 192.65 us/op 144.66 us/op 1.33
Full columns - reconstruct half of the blobs out of 6 82.597 us/op 95.087 us/op 0.87
Full columns - reconstruct single blob out of 6 33.926 us/op 40.090 us/op 0.85
Half columns - reconstruct all 6 blobs 392.58 ms/op 398.74 ms/op 0.98
Half columns - reconstruct half of the blobs out of 6 193.47 ms/op 199.40 ms/op 0.97
Half columns - reconstruct single blob out of 6 68.495 ms/op 70.757 ms/op 0.97
Set add up to 64 items then delete first 1.6769 us/op 1.6694 us/op 1.00
OrderedSet add up to 64 items then delete first 2.5421 us/op 2.5389 us/op 1.00
Set add up to 64 items then delete last 1.9008 us/op 1.8928 us/op 1.00
OrderedSet add up to 64 items then delete last 2.8584 us/op 2.7797 us/op 1.03
Set add up to 64 items then delete middle 1.9033 us/op 1.8897 us/op 1.01
OrderedSet add up to 64 items then delete middle 4.3019 us/op 4.2336 us/op 1.02
Set add up to 128 items then delete first 3.7937 us/op 3.7645 us/op 1.01
OrderedSet add up to 128 items then delete first 5.7802 us/op 5.7183 us/op 1.01
Set add up to 128 items then delete last 3.6207 us/op 3.5751 us/op 1.01
OrderedSet add up to 128 items then delete last 5.4718 us/op 5.3665 us/op 1.02
Set add up to 128 items then delete middle 3.6177 us/op 3.5881 us/op 1.01
OrderedSet add up to 128 items then delete middle 11.290 us/op 11.283 us/op 1.00
Set add up to 256 items then delete first 7.4298 us/op 7.8394 us/op 0.95
OrderedSet add up to 256 items then delete first 12.115 us/op 12.113 us/op 1.00
Set add up to 256 items then delete last 7.1534 us/op 7.1240 us/op 1.00
OrderedSet add up to 256 items then delete last 12.164 us/op 11.474 us/op 1.06
Set add up to 256 items then delete middle 7.3114 us/op 7.5198 us/op 0.97
OrderedSet add up to 256 items then delete middle 35.766 us/op 35.378 us/op 1.01
runFastConfirmationRules vc:100000 bc:96 eq:0 4.8953 ms/op 4.6596 ms/op 1.05
runFastConfirmationRules vc:600000 bc:96 eq:0 34.959 ms/op 36.480 ms/op 0.96
runFastConfirmationRules vc:1000000 bc:96 eq:0 57.132 ms/op 58.833 ms/op 0.97
runFastConfirmationRules vc:600000 bc:320 eq:0 34.606 ms/op 35.212 ms/op 0.98
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.1259 s/op 1.1491 s/op 0.98
pass gossip attestations to forkchoice per slot 2.6041 ms/op 2.5803 ms/op 1.01
forkChoice updateHead vc 100000 bc 64 eq 0 444.96 us/op 438.20 us/op 1.02
forkChoice updateHead vc 600000 bc 64 eq 0 2.6774 ms/op 2.6017 ms/op 1.03
forkChoice updateHead vc 1000000 bc 64 eq 0 4.4657 ms/op 4.3379 ms/op 1.03
forkChoice updateHead vc 600000 bc 320 eq 0 2.7160 ms/op 2.6380 ms/op 1.03
forkChoice updateHead vc 600000 bc 1200 eq 0 2.7009 ms/op 2.6306 ms/op 1.03
forkChoice updateHead vc 600000 bc 7200 eq 0 3.1285 ms/op 2.9534 ms/op 1.06
forkChoice updateHead vc 600000 bc 64 eq 1000 2.6977 ms/op 2.6406 ms/op 1.02
forkChoice updateHead vc 600000 bc 64 eq 10000 2.7994 ms/op 2.6953 ms/op 1.04
forkChoice updateHead vc 600000 bc 64 eq 300000 7.0444 ms/op 7.1549 ms/op 0.98
computeDeltas 1400000 validators 0% inactive 13.418 ms/op 12.694 ms/op 1.06
computeDeltas 1400000 validators 10% inactive 13.198 ms/op 12.210 ms/op 1.08
computeDeltas 1400000 validators 20% inactive 12.140 ms/op 11.667 ms/op 1.04
computeDeltas 1400000 validators 50% inactive 9.8501 ms/op 9.0770 ms/op 1.09
computeDeltas 2100000 validators 0% inactive 20.117 ms/op 19.223 ms/op 1.05
computeDeltas 2100000 validators 10% inactive 19.176 ms/op 18.241 ms/op 1.05
computeDeltas 2100000 validators 20% inactive 18.041 ms/op 17.243 ms/op 1.05
computeDeltas 2100000 validators 50% inactive 14.616 ms/op 11.643 ms/op 1.26
altair processAttestation - 250000 vs - 7PWei normalcase 1.6875 ms/op 1.7734 ms/op 0.95
altair processAttestation - 250000 vs - 7PWei worstcase 2.4469 ms/op 2.6886 ms/op 0.91
altair processAttestation - setStatus - 1/6 committees join 106.05 us/op 104.62 us/op 1.01
altair processAttestation - setStatus - 1/3 committees join 210.67 us/op 205.02 us/op 1.03
altair processAttestation - setStatus - 1/2 committees join 296.34 us/op 292.55 us/op 1.01
altair processAttestation - setStatus - 2/3 committees join 376.44 us/op 376.86 us/op 1.00
altair processAttestation - setStatus - 4/5 committees join 524.40 us/op 539.55 us/op 0.97
altair processAttestation - setStatus - 100% committees join 616.77 us/op 619.42 us/op 1.00
altair processBlock - 250000 vs - 7PWei normalcase 2.8611 ms/op 3.5501 ms/op 0.81
altair processBlock - 250000 vs - 7PWei normalcase hashState 14.530 ms/op 15.481 ms/op 0.94
altair processBlock - 250000 vs - 7PWei worstcase 19.514 ms/op 21.041 ms/op 0.93
altair processBlock - 250000 vs - 7PWei worstcase hashState 39.158 ms/op 40.957 ms/op 0.96
phase0 processBlock - 250000 vs - 7PWei normalcase 1.4533 ms/op 1.4007 ms/op 1.04
phase0 processBlock - 250000 vs - 7PWei worstcase 17.237 ms/op 17.346 ms/op 0.99
altair processEth1Data - 250000 vs - 7PWei normalcase 303.90 us/op 301.78 us/op 1.01
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 3.3280 us/op 3.7730 us/op 0.88
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 21.273 us/op 21.730 us/op 0.98
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 5.9150 us/op 6.0880 us/op 0.97
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 4.4300 us/op 3.9860 us/op 1.11
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 98.016 us/op 90.569 us/op 1.08
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4445 ms/op 1.4621 ms/op 0.99
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8922 ms/op 1.8939 ms/op 1.00
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.8764 ms/op 1.9684 ms/op 0.95
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.7022 ms/op 3.6489 ms/op 1.01
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.1239 ms/op 2.1292 ms/op 1.00
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.0476 ms/op 3.9255 ms/op 1.03
Tree 40 250000 create 338.35 ms/op 313.72 ms/op 1.08
Tree 40 250000 get(125000) 101.08 ns/op 103.09 ns/op 0.98
Tree 40 250000 set(125000) 1.0542 us/op 1.0412 us/op 1.01
Tree 40 250000 toArray() 9.6300 ms/op 10.958 ms/op 0.88
Tree 40 250000 iterate all - toArray() + loop 10.899 ms/op 9.8958 ms/op 1.10
Tree 40 250000 iterate all - get(i) 41.643 ms/op 39.747 ms/op 1.05
Array 250000 create 2.4067 ms/op 2.5918 ms/op 0.93
Array 250000 clone - spread 770.46 us/op 797.94 us/op 0.97
Array 250000 get(125000) 0.30300 ns/op 0.69400 ns/op 0.44
Array 250000 set(125000) 0.29900 ns/op 0.70300 ns/op 0.43
Array 250000 iterate all - loop 57.968 us/op 57.408 us/op 1.01
phase0 afterProcessEpoch - 250000 vs - 7PWei 52.434 ms/op 52.170 ms/op 1.01
Array.fill - length 1000000 3.8563 ms/op 4.2106 ms/op 0.92
Array push - length 1000000 13.277 ms/op 15.423 ms/op 0.86
Array.get 0.21055 ns/op 0.20808 ns/op 1.01
Uint8Array.get 0.25905 ns/op 0.25690 ns/op 1.01
phase0 beforeProcessEpoch - 250000 vs - 7PWei 14.706 ms/op 15.920 ms/op 0.92
altair processEpoch - mainnet_e81889 264.46 ms/op 266.51 ms/op 0.99
mainnet_e81889 - altair beforeProcessEpoch 15.252 ms/op 37.034 ms/op 0.41
mainnet_e81889 - altair processJustificationAndFinalization 6.1140 us/op 6.6540 us/op 0.92
mainnet_e81889 - altair processInactivityUpdates 3.7456 ms/op 3.6491 ms/op 1.03
mainnet_e81889 - altair processRewardsAndPenalties 18.752 ms/op 20.142 ms/op 0.93
mainnet_e81889 - altair processRegistryUpdates 566.00 ns/op 1.0280 us/op 0.55
mainnet_e81889 - altair processSlashings 143.00 ns/op 610.00 ns/op 0.23
mainnet_e81889 - altair processEth1DataReset 141.00 ns/op 596.00 ns/op 0.24
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.4733 ms/op 1.6726 ms/op 0.88
mainnet_e81889 - altair processSlashingsReset 719.00 ns/op 1.1780 us/op 0.61
mainnet_e81889 - altair processRandaoMixesReset 1.3080 us/op 1.7860 us/op 0.73
mainnet_e81889 - altair processHistoricalRootsUpdate 141.00 ns/op 606.00 ns/op 0.23
mainnet_e81889 - altair processParticipationFlagUpdates 446.00 ns/op 888.00 ns/op 0.50
mainnet_e81889 - altair processSyncCommitteeUpdates 112.00 ns/op 559.00 ns/op 0.20
mainnet_e81889 - altair afterProcessEpoch 42.469 ms/op 41.632 ms/op 1.02
capella processEpoch - mainnet_e217614 825.84 ms/op 810.13 ms/op 1.02
mainnet_e217614 - capella beforeProcessEpoch 65.281 ms/op 60.864 ms/op 1.07
mainnet_e217614 - capella processJustificationAndFinalization 5.6850 us/op 6.0160 us/op 0.94
mainnet_e217614 - capella processInactivityUpdates 12.201 ms/op 14.369 ms/op 0.85
mainnet_e217614 - capella processRewardsAndPenalties 94.750 ms/op 97.088 ms/op 0.98
mainnet_e217614 - capella processRegistryUpdates 4.5920 us/op 4.8710 us/op 0.94
mainnet_e217614 - capella processSlashings 162.00 ns/op 605.00 ns/op 0.27
mainnet_e217614 - capella processEth1DataReset 157.00 ns/op 595.00 ns/op 0.26
mainnet_e217614 - capella processEffectiveBalanceUpdates 5.9584 ms/op 7.4136 ms/op 0.80
mainnet_e217614 - capella processSlashingsReset 716.00 ns/op 1.1480 us/op 0.62
mainnet_e217614 - capella processRandaoMixesReset 1.2810 us/op 1.6470 us/op 0.78
mainnet_e217614 - capella processHistoricalRootsUpdate 161.00 ns/op 594.00 ns/op 0.27
mainnet_e217614 - capella processParticipationFlagUpdates 469.00 ns/op 917.00 ns/op 0.51
mainnet_e217614 - capella afterProcessEpoch 111.45 ms/op 107.21 ms/op 1.04
phase0 processEpoch - mainnet_e58758 299.61 ms/op 317.79 ms/op 0.94
mainnet_e58758 - phase0 beforeProcessEpoch 61.979 ms/op 70.490 ms/op 0.88
mainnet_e58758 - phase0 processJustificationAndFinalization 5.6890 us/op 7.2740 us/op 0.78
mainnet_e58758 - phase0 processRewardsAndPenalties 15.651 ms/op 17.812 ms/op 0.88
mainnet_e58758 - phase0 processRegistryUpdates 2.3540 us/op 2.7460 us/op 0.86
mainnet_e58758 - phase0 processSlashings 157.00 ns/op 611.00 ns/op 0.26
mainnet_e58758 - phase0 processEth1DataReset 155.00 ns/op 610.00 ns/op 0.25
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 870.33 us/op 822.66 us/op 1.06
mainnet_e58758 - phase0 processSlashingsReset 1.1000 us/op 1.3270 us/op 0.83
mainnet_e58758 - phase0 processRandaoMixesReset 1.2360 us/op 1.7580 us/op 0.70
mainnet_e58758 - phase0 processHistoricalRootsUpdate 157.00 ns/op 592.00 ns/op 0.27
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.1450 us/op 1.5710 us/op 0.73
mainnet_e58758 - phase0 afterProcessEpoch 34.145 ms/op 32.761 ms/op 1.04
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.0346 ms/op 1.1616 ms/op 0.89
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.9016 ms/op 1.6957 ms/op 1.12
altair processInactivityUpdates - 250000 normalcase 10.623 ms/op 10.717 ms/op 0.99
altair processInactivityUpdates - 250000 worstcase 10.479 ms/op 10.771 ms/op 0.97
phase0 processRegistryUpdates - 250000 normalcase 2.7730 us/op 3.2210 us/op 0.86
phase0 processRegistryUpdates - 250000 badcase_full_deposits 152.14 us/op 152.16 us/op 1.00
phase0 processRegistryUpdates - 250000 worstcase 0.5 61.828 ms/op 62.630 ms/op 0.99
altair processRewardsAndPenalties - 250000 normalcase 16.845 ms/op 14.100 ms/op 1.19
altair processRewardsAndPenalties - 250000 worstcase 16.289 ms/op 14.654 ms/op 1.11
phase0 getAttestationDeltas - 250000 normalcase 5.5159 ms/op 5.6755 ms/op 0.97
phase0 getAttestationDeltas - 250000 worstcase 5.5236 ms/op 5.7576 ms/op 0.96
phase0 processSlashings - 250000 worstcase 60.544 us/op 66.129 us/op 0.92
altair processSyncCommitteeUpdates - 250000 10.016 ms/op 10.253 ms/op 0.98
BeaconState.hashTreeRoot - No change 173.00 ns/op 636.00 ns/op 0.27
BeaconState.hashTreeRoot - 1 full validator 60.919 us/op 63.645 us/op 0.96
BeaconState.hashTreeRoot - 32 full validator 682.44 us/op 875.43 us/op 0.78
BeaconState.hashTreeRoot - 512 full validator 6.6175 ms/op 6.9622 ms/op 0.95
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 75.042 us/op 104.25 us/op 0.72
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.0968 ms/op 1.4353 ms/op 0.76
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 14.194 ms/op 14.371 ms/op 0.99
BeaconState.hashTreeRoot - 1 balances 59.575 us/op 73.220 us/op 0.81
BeaconState.hashTreeRoot - 32 balances 567.39 us/op 734.13 us/op 0.77
BeaconState.hashTreeRoot - 512 balances 4.7544 ms/op 5.0319 ms/op 0.94
BeaconState.hashTreeRoot - 250000 balances 107.16 ms/op 104.69 ms/op 1.02
aggregationBits - 2048 els - zipIndexesInBitList 19.744 us/op 21.090 us/op 0.94
regular array get 100000 times 22.801 us/op 23.375 us/op 0.98
wrappedArray get 100000 times 22.961 us/op 23.407 us/op 0.98
arrayWithProxy get 100000 times 10.229 ms/op 10.989 ms/op 0.93
ssz.Root.equals 21.569 ns/op 21.869 ns/op 0.99
byteArrayEquals 21.352 ns/op 21.715 ns/op 0.98
Buffer.compare 8.9380 ns/op 9.3300 ns/op 0.96
processSlot - 1 slots 8.5640 us/op 8.9870 us/op 0.95
processSlot - 32 slots 2.0813 ms/op 1.8678 ms/op 1.11
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.2850 ms/op 4.3591 ms/op 0.98
getCommitteeAssignments - req 1 vs - 250000 vc 1.6878 ms/op 1.6982 ms/op 0.99
getCommitteeAssignments - req 100 vs - 250000 vc 3.4397 ms/op 3.4570 ms/op 0.99
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6902 ms/op 3.6725 ms/op 1.00
findModifiedValidators - 10000 modified validators 848.41 ms/op 836.59 ms/op 1.01
findModifiedValidators - 1000 modified validators 622.69 ms/op 707.34 ms/op 0.88
findModifiedValidators - 100 modified validators 395.56 ms/op 400.60 ms/op 0.99
findModifiedValidators - 10 modified validators 332.52 ms/op 307.52 ms/op 1.08
findModifiedValidators - 1 modified validators 200.97 ms/op 258.42 ms/op 0.78
findModifiedValidators - no difference 263.39 ms/op 216.34 ms/op 1.22
migrate state 1500000 validators, 3400 modified, 2000 new 3.3955 s/op 3.6118 s/op 0.94
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7900 ns/op 8.0900 ns/op 0.47
state getBlockRootAtSlot - 250000 vs - 7PWei 316.98 ns/op 366.10 ns/op 0.87
computeProposerIndex 100000 validators 1.3883 ms/op 1.3891 ms/op 1.00
getNextSyncCommitteeIndices 1000 validators 2.9442 ms/op 2.9324 ms/op 1.00
getNextSyncCommitteeIndices 10000 validators 26.285 ms/op 26.351 ms/op 1.00
getNextSyncCommitteeIndices 100000 validators 89.931 ms/op 92.268 ms/op 0.97
computeProposers - vc 250000 561.35 us/op 563.16 us/op 1.00
computeEpochShuffling - vc 250000 40.528 ms/op 40.122 ms/op 1.01
getNextSyncCommittee - vc 250000 9.6981 ms/op 9.7496 ms/op 0.99
nodejs block root to RootHex using toHex 100.11 ns/op 103.64 ns/op 0.97
nodejs block root to RootHex using toRootHex 64.293 ns/op 69.617 ns/op 0.92
nodejs fromHex(blob) 867.55 us/op 913.42 us/op 0.95
nodejs fromHexInto(blob) 657.53 us/op 651.37 us/op 1.01
nodejs block root to RootHex using the deprecated toHexString 513.80 ns/op 523.96 ns/op 0.98
nodejs byteArrayEquals 32 bytes (block root) 26.405 ns/op 26.544 ns/op 0.99
nodejs byteArrayEquals 48 bytes (pubkey) 38.122 ns/op 38.230 ns/op 1.00
nodejs byteArrayEquals 96 bytes (signature) 38.207 ns/op 37.569 ns/op 1.02
nodejs byteArrayEquals 1024 bytes 45.477 ns/op 54.010 ns/op 0.84
nodejs byteArrayEquals 131072 bytes (blob) 1.7801 us/op 1.7799 us/op 1.00
browser block root to RootHex using toHex 147.37 ns/op 147.81 ns/op 1.00
browser block root to RootHex using toRootHex 131.96 ns/op 141.95 ns/op 0.93
browser fromHex(blob) 1.6198 ms/op 1.7124 ms/op 0.95
browser fromHexInto(blob) 659.52 us/op 639.02 us/op 1.03
browser block root to RootHex using the deprecated toHexString 355.27 ns/op 360.28 ns/op 0.99
browser byteArrayEquals 32 bytes (block root) 28.306 ns/op 28.516 ns/op 0.99
browser byteArrayEquals 48 bytes (pubkey) 39.745 ns/op 39.895 ns/op 1.00
browser byteArrayEquals 96 bytes (signature) 74.612 ns/op 74.018 ns/op 1.01
browser byteArrayEquals 1024 bytes 761.08 ns/op 762.83 ns/op 1.00
browser byteArrayEquals 131072 bytes (blob) 96.017 us/op 95.666 us/op 1.00

by benchmarkbot/action

lodekeeper and others added 2 commits August 14, 2026 08:24
…ts (#9824)

Demo requested in #9815
([discussion_r3779437582](#9815 (comment)))
— showing how the O(window) early-break variant of
`getCanonicalPayloadCounts` would look. Targets
`nflaig/circuit-breaker-canonical-counts`, not `unstable`.

### Approach

Instead of materializing the whole canonical chain via
`getAllAncestorNodes()`, walk it newest-first and stop once a node drops
below `fromSlot`. Ancestors are strictly slot-descending, so everything
past that point is already out of the window — the previous full scan
just `continue`d over those.

```ts
const headIndex = this.getNodeIndexByRootAndStatus(headRoot, headPayloadStatus);
const head = headIndex !== undefined ? this.nodes[headIndex] : undefined;
if (head !== undefined && head.slot >= fromSlot) {
  countNode(head);
  for (const node of this.iterateAncestorNodesFromNode(head)) {
    if (node.slot < fromSlot) break;
    countNode(node);
  }
}
```

### The head-inclusion gotcha (your note in r3779406069)

`iterateAncestorNodes()` starts one hop up
(`iterateAncestorNodesFromNode` begins at `.parent`), so a direct swap
drops the resolved head and undercounts by one. Handled here by counting
the head explicitly first, then walking `.parent` via
`iterateAncestorNodesFromNode(head)`. A PENDING head is still skipped by
`countNode` (same as `getAllAncestorNodes` not pushing a PENDING start
node) — it only seeds the ancestor walk.

### Behavior

Identical to the current full-scan: same canonical node sequence (head +
ancestors), same filters (genesis / out-of-window / non-gloas /
PENDING), same `full`/`empty` counts. Only the traversal is lazy with an
early exit. The win (`O(window)` vs `O(chain-to-anchor)`) only shows
under prolonged non-finality, when the chain-to-anchor grows well past
the fault window.

### Tests

Ran the fork-choice protoArray suite locally against this change —
**131/131 pass** (`packages/fork-choice/test/unit/protoArray`, 7 files),
including every `getCanonicalPayloadCounts` case: genesis skip, "keeps
EMPTY after a late FULL arrives", PENDING head, inclusive bounds,
uses-supplied-head-branch. CI here re-runs the full suite.

Not attached to it — happy to close if you'd rather keep the simpler
full-scan; just wanted to show the shape as you asked.

🤖 Generated with AI assistance
@nflaig

nflaig commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

@lodekeeper I merged your changes from #9824, please give this PR another pass

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

Gave it another pass — LGTM, good from my side.

The merged early-break (former #9824) integrated cleanly: getCanonicalPayloadCounts at the head is byte-identical to the version I ran the protoArray suite against (131/131 pass) and that @ensi321 endorsed — the only change since is your tweak comment reword (dd2a3ba), no logic drift.

Confirmed on this pass (head dd2a3ba):

  • Full changed-file set is the expected one (circuit breaker hysteresis + canonical counts + metric names + tests) — nothing unexpected snuck in with the merge.
  • Both notes from my earlier review are addressed and still in place: the genesis-guard test (does not count the genesis block as EMPTY) and the O(window) early-break itself.
  • The genesis / toSlot / !isGloasBlock / PENDING filters and the early-break-on-fromSlot are unchanged; the flat-loop form reads well.

Nothing new to flag — it's the same code already reviewed + tested, just combined. 👍

Comment thread packages/beacon-node/src/chain/builderCircuitBreaker.ts
Comment thread packages/beacon-node/src/chain/builderCircuitBreaker.ts Outdated
@nflaig
nflaig merged commit 32aed64 into unstable Aug 14, 2026
24 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Lodestar Team Coordination Aug 14, 2026
@nflaig
nflaig deleted the nflaig/circuit-breaker-canonical-counts branch August 14, 2026 08:59
@nflaig nflaig removed the status-do-not-merge Merging this issue will break the build. Do not merge! label Aug 14, 2026
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.

3 participants