Skip to content

feat: fast confirmation rule#8837

Open
nazarhussain wants to merge 62 commits into
unstablefrom
feature/fast-confirmation
Open

feat: fast confirmation rule#8837
nazarhussain wants to merge 62 commits into
unstablefrom
feature/fast-confirmation

Conversation

@nazarhussain
Copy link
Copy Markdown
Contributor

Motivation

Introduce assumption based fast confirmation rule.

Description

Specs: ethereum/consensus-specs#4747

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @nazarhussain, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new feature: an assumption-based Fast Confirmation Rule (FCR) for the beacon node. The primary goal is to accelerate the confirmation of blocks by implementing a sophisticated algorithm that evaluates various network and validator-related metrics. This involves deep integration into the existing fork choice mechanism, providing a new configurable option for users, and updating how 'safe' blocks are identified within the system. The changes are comprehensive, spanning core logic, configuration, state management, and observability.

Highlights

  • Fast Confirmation Rule (FCR) Implementation: Introduced a new FastConfirmationRule class and its supporting types and metrics, which encapsulates the logic for an assumption-based fast block confirmation mechanism. This rule aims to provide faster block finality by evaluating attestation scores, committee weights, and other factors.
  • Fork Choice Integration: The core ForkChoice class has been updated to optionally enable and integrate the Fast Confirmation Rule. When active, the FCR's logic is executed at the start of each slot to determine and update a 'confirmed root', which can then be queried by other parts of the system.
  • Configurability and CLI Options: A new configuration option, enableFastConfirmation, has been added to the chain options, allowing users to enable or disable this experimental feature via a CLI argument. Additionally, a CONFIRMATION_BYZANTINE_THRESHOLD constant has been introduced to chain configurations, providing a configurable parameter for the FCR.
  • Enhanced Safe Block Determination: The getSafeBeaconBlockRoot and getSafeExecutionBlockHash functions now prioritize the FCR's determined 'confirmed root' if the feature is enabled, falling back to the traditional justified checkpoint if the FCR is not active or doesn't provide a confirmed root. This ensures that the fastest available confirmation is used.
  • State Management and Metrics: The ForkChoiceStore and related interfaces have been extended to store the necessary internal state for the FCR, such as observed justified checkpoints and balances. New Prometheus metrics have also been added to monitor the FCR's performance, confirmed epochs/slots, and any resets.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

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 fast confirmation rule, an experimental feature designed to provide faster block confirmations. The changes are comprehensive, including the core logic in a new FastConfirmationRule class, configuration options to enable it, and integration into the existing ForkChoice and safe block determination logic. The implementation appears to align well with the provided specification. My review includes a couple of minor suggestions to enhance code quality and readability.

Comment thread packages/fork-choice/src/forkChoice/fastConfirmationRule/fastConfirmationRule.ts Outdated
Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 3, 2026

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: 92d1b6f Previous: c874e6b Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 1.4392 ms/op 1.2589 ms/op 1.14
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 41.949 us/op 40.264 us/op 1.04
BLS verify - blst 759.71 us/op 752.84 us/op 1.01
BLS verifyMultipleSignatures 3 - blst 1.3620 ms/op 1.3706 ms/op 0.99
BLS verifyMultipleSignatures 8 - blst 2.2070 ms/op 2.1867 ms/op 1.01
BLS verifyMultipleSignatures 32 - blst 6.8266 ms/op 7.0087 ms/op 0.97
BLS verifyMultipleSignatures 64 - blst 13.459 ms/op 13.359 ms/op 1.01
BLS verifyMultipleSignatures 128 - blst 25.991 ms/op 26.446 ms/op 0.98
BLS deserializing 10000 signatures 643.14 ms/op 642.03 ms/op 1.00
BLS deserializing 100000 signatures 6.3796 s/op 6.4459 s/op 0.99
BLS verifyMultipleSignatures - same message - 3 - blst 837.36 us/op 764.67 us/op 1.10
BLS verifyMultipleSignatures - same message - 8 - blst 889.16 us/op 901.87 us/op 0.99
BLS verifyMultipleSignatures - same message - 32 - blst 1.5509 ms/op 1.5190 ms/op 1.02
BLS verifyMultipleSignatures - same message - 64 - blst 2.2664 ms/op 2.4585 ms/op 0.92
BLS verifyMultipleSignatures - same message - 128 - blst 4.0332 ms/op 4.0064 ms/op 1.01
BLS aggregatePubkeys 32 - blst 17.858 us/op 17.613 us/op 1.01
BLS aggregatePubkeys 128 - blst 64.147 us/op 62.918 us/op 1.02
getSlashingsAndExits - default max 57.693 us/op 53.460 us/op 1.08
getSlashingsAndExits - 2k 450.01 us/op 418.13 us/op 1.08
proposeBlockBody type=full, size=empty 919.96 us/op 793.53 us/op 1.16
isKnown best case - 1 super set check 181.00 ns/op 172.00 ns/op 1.05
isKnown normal case - 2 super set checks 163.00 ns/op 172.00 ns/op 0.95
isKnown worse case - 16 super set checks 164.00 ns/op 173.00 ns/op 0.95
validate api signedAggregateAndProof - struct 1.5350 ms/op 1.5231 ms/op 1.01
validate gossip signedAggregateAndProof - struct 1.4836 ms/op 1.5196 ms/op 0.98
batch validate gossip attestation - vc 640000 - chunk 32 107.55 us/op 106.92 us/op 1.01
batch validate gossip attestation - vc 640000 - chunk 64 105.34 us/op 93.187 us/op 1.13
batch validate gossip attestation - vc 640000 - chunk 128 98.112 us/op 86.293 us/op 1.14
batch validate gossip attestation - vc 640000 - chunk 256 92.417 us/op 82.934 us/op 1.11
bytes32 toHexString 297.00 ns/op 285.00 ns/op 1.04
bytes32 Buffer.toString(hex) 164.00 ns/op 163.00 ns/op 1.01
bytes32 Buffer.toString(hex) from Uint8Array 231.00 ns/op 227.00 ns/op 1.02
bytes32 Buffer.toString(hex) + 0x 169.00 ns/op 163.00 ns/op 1.04
Return object 10000 times 0.22060 ns/op 0.21250 ns/op 1.04
Throw Error 10000 times 3.7308 us/op 3.3383 us/op 1.12
toHex 94.412 ns/op 92.048 ns/op 1.03
Buffer.from 87.900 ns/op 86.450 ns/op 1.02
shared Buffer 55.950 ns/op 56.857 ns/op 0.98
fastMsgIdFn sha256 / 200 bytes 1.4980 us/op 1.4400 us/op 1.04
fastMsgIdFn h32 xxhash / 200 bytes 159.00 ns/op 147.00 ns/op 1.08
fastMsgIdFn h64 xxhash / 200 bytes 206.00 ns/op 197.00 ns/op 1.05
fastMsgIdFn sha256 / 1000 bytes 4.8560 us/op 4.7360 us/op 1.03
fastMsgIdFn h32 xxhash / 1000 bytes 252.00 ns/op 242.00 ns/op 1.04
fastMsgIdFn h64 xxhash / 1000 bytes 257.00 ns/op 254.00 ns/op 1.01
fastMsgIdFn sha256 / 10000 bytes 42.390 us/op 41.981 us/op 1.01
fastMsgIdFn h32 xxhash / 10000 bytes 1.2710 us/op 1.2590 us/op 1.01
fastMsgIdFn h64 xxhash / 10000 bytes 807.00 ns/op 820.00 ns/op 0.98
send data - 1000 256B messages 4.5753 ms/op 4.1770 ms/op 1.10
send data - 1000 512B messages 4.7536 ms/op 4.1164 ms/op 1.15
send data - 1000 1024B messages 5.1141 ms/op 4.3838 ms/op 1.17
send data - 1000 1200B messages 5.0072 ms/op 4.4315 ms/op 1.13
send data - 1000 2048B messages 5.3610 ms/op 4.5988 ms/op 1.17
send data - 1000 4096B messages 5.9325 ms/op 5.6429 ms/op 1.05
send data - 1000 16384B messages 19.465 ms/op 19.595 ms/op 0.99
send data - 1000 65536B messages 160.46 ms/op 155.07 ms/op 1.03
enrSubnets - fastDeserialize 64 bits 704.00 ns/op 717.00 ns/op 0.98
enrSubnets - ssz BitVector 64 bits 261.00 ns/op 269.00 ns/op 0.97
enrSubnets - fastDeserialize 4 bits 100.00 ns/op 98.000 ns/op 1.02
enrSubnets - ssz BitVector 4 bits 269.00 ns/op 268.00 ns/op 1.00
prioritizePeers score -10:0 att 32-0.1 sync 2-0 202.27 us/op 203.44 us/op 0.99
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 234.09 us/op 232.82 us/op 1.01
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 334.76 us/op 336.46 us/op 0.99
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 598.08 us/op 599.45 us/op 1.00
prioritizePeers score 0:0 att 64-1 sync 4-1 693.52 us/op 694.24 us/op 1.00
array of 16000 items push then shift 1.2691 us/op 1.3033 us/op 0.97
LinkedList of 16000 items push then shift 7.7060 ns/op 6.8830 ns/op 1.12
array of 16000 items push then pop 72.913 ns/op 66.458 ns/op 1.10
LinkedList of 16000 items push then pop 6.5660 ns/op 5.9920 ns/op 1.10
array of 24000 items push then shift 1.8438 us/op 1.9316 us/op 0.95
LinkedList of 24000 items push then shift 8.6180 ns/op 6.6880 ns/op 1.29
array of 24000 items push then pop 104.93 ns/op 94.726 ns/op 1.11
LinkedList of 24000 items push then pop 6.2700 ns/op 6.0250 ns/op 1.04
intersect bitArray bitLen 8 4.7880 ns/op 4.7790 ns/op 1.00
intersect array and set length 8 29.568 ns/op 29.573 ns/op 1.00
intersect bitArray bitLen 128 23.868 ns/op 24.242 ns/op 0.98
intersect array and set length 128 495.68 ns/op 498.27 ns/op 0.99
bitArray.getTrueBitIndexes() bitLen 128 1.1850 us/op 966.00 ns/op 1.23
bitArray.getTrueBitIndexes() bitLen 248 1.9600 us/op 1.7340 us/op 1.13
bitArray.getTrueBitIndexes() bitLen 512 3.9210 us/op 3.5520 us/op 1.10
Full columns - reconstruct all 6 blobs 177.99 us/op 189.85 us/op 0.94
Full columns - reconstruct half of the blobs out of 6 105.69 us/op 67.068 us/op 1.58
Full columns - reconstruct single blob out of 6 30.542 us/op 33.817 us/op 0.90
Half columns - reconstruct all 6 blobs 385.24 ms/op 378.04 ms/op 1.02
Half columns - reconstruct half of the blobs out of 6 189.70 ms/op 189.00 ms/op 1.00
Half columns - reconstruct single blob out of 6 69.202 ms/op 66.514 ms/op 1.04
Full columns - reconstruct all 10 blobs 281.45 us/op 212.02 us/op 1.33
Full columns - reconstruct half of the blobs out of 10 188.47 us/op 96.922 us/op 1.94
Full columns - reconstruct single blob out of 10 32.643 us/op 36.238 us/op 0.90
Half columns - reconstruct all 10 blobs 640.34 ms/op 627.93 ms/op 1.02
Half columns - reconstruct half of the blobs out of 10 321.44 ms/op 315.53 ms/op 1.02
Half columns - reconstruct single blob out of 10 69.529 ms/op 66.404 ms/op 1.05
Full columns - reconstruct all 20 blobs 862.65 us/op 1.4526 ms/op 0.59
Full columns - reconstruct half of the blobs out of 20 443.67 us/op 284.27 us/op 1.56
Full columns - reconstruct single blob out of 20 30.195 us/op 43.217 us/op 0.70
Half columns - reconstruct all 20 blobs 1.2657 s/op 1.2516 s/op 1.01
Half columns - reconstruct half of the blobs out of 20 634.94 ms/op 626.96 ms/op 1.01
Half columns - reconstruct single blob out of 20 68.924 ms/op 66.151 ms/op 1.04
Set add up to 64 items then delete first 2.1719 us/op 2.2149 us/op 0.98
OrderedSet add up to 64 items then delete first 3.4371 us/op 3.4621 us/op 0.99
Set add up to 64 items then delete last 2.1245 us/op 2.1870 us/op 0.97
OrderedSet add up to 64 items then delete last 3.2941 us/op 3.5066 us/op 0.94
Set add up to 64 items then delete middle 2.1226 us/op 2.2253 us/op 0.95
OrderedSet add up to 64 items then delete middle 4.8107 us/op 5.0804 us/op 0.95
Set add up to 128 items then delete first 4.2219 us/op 4.3154 us/op 0.98
OrderedSet add up to 128 items then delete first 6.4394 us/op 6.4648 us/op 1.00
Set add up to 128 items then delete last 3.9362 us/op 4.0431 us/op 0.97
OrderedSet add up to 128 items then delete last 5.8907 us/op 6.1865 us/op 0.95
Set add up to 128 items then delete middle 3.8528 us/op 4.0657 us/op 0.95
OrderedSet add up to 128 items then delete middle 11.553 us/op 12.250 us/op 0.94
Set add up to 256 items then delete first 8.0990 us/op 7.9757 us/op 1.02
OrderedSet add up to 256 items then delete first 12.296 us/op 12.142 us/op 1.01
Set add up to 256 items then delete last 7.7100 us/op 8.0734 us/op 0.95
OrderedSet add up to 256 items then delete last 11.890 us/op 12.466 us/op 0.95
Set add up to 256 items then delete middle 7.7023 us/op 8.0719 us/op 0.95
OrderedSet add up to 256 items then delete middle 37.557 us/op 37.169 us/op 1.01
runFastConfirmationRules vc:100000 bc:96 eq:0 2.8680 us/op
runFastConfirmationRules vc:600000 bc:96 eq:0 19.910 us/op
runFastConfirmationRules vc:1000000 bc:96 eq:0 31.076 us/op
runFastConfirmationRules vc:600000 bc:320 eq:0 23.685 us/op
runFastConfirmationRules vc:600000 bc:1200 eq:0 30.320 us/op
runFastConfirmationRules vc:600000 bc:96 eq:1000 9.9720 us/op
runFastConfirmationRules vc:600000 bc:96 eq:10000 10.703 us/op
runFastConfirmationRules vc:600000 bc:96 eq:300000 21.467 us/op
pass gossip attestations to forkchoice per slot 2.6320 ms/op 2.5780 ms/op 1.02
forkChoice updateHead vc 100000 bc 64 eq 0 403.38 us/op 417.93 us/op 0.97
forkChoice updateHead vc 600000 bc 64 eq 0 2.4031 ms/op 2.4849 ms/op 0.97
forkChoice updateHead vc 1000000 bc 64 eq 0 4.0082 ms/op 4.2161 ms/op 0.95
forkChoice updateHead vc 600000 bc 320 eq 0 2.5129 ms/op 2.5009 ms/op 1.00
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5405 ms/op 2.5193 ms/op 1.01
forkChoice updateHead vc 600000 bc 7200 eq 0 3.2268 ms/op 2.8405 ms/op 1.14
forkChoice updateHead vc 600000 bc 64 eq 1000 2.4149 ms/op 2.9998 ms/op 0.81
forkChoice updateHead vc 600000 bc 64 eq 10000 2.5191 ms/op 3.1265 ms/op 0.81
forkChoice updateHead vc 600000 bc 64 eq 300000 6.7680 ms/op 7.2045 ms/op 0.94
computeDeltas 1400000 validators 0% inactive 12.454 ms/op 12.869 ms/op 0.97
computeDeltas 1400000 validators 10% inactive 11.838 ms/op 12.039 ms/op 0.98
computeDeltas 1400000 validators 20% inactive 10.999 ms/op 11.146 ms/op 0.99
computeDeltas 1400000 validators 50% inactive 8.3629 ms/op 8.4558 ms/op 0.99
computeDeltas 2100000 validators 0% inactive 19.283 ms/op 19.367 ms/op 1.00
computeDeltas 2100000 validators 10% inactive 17.838 ms/op 18.057 ms/op 0.99
computeDeltas 2100000 validators 20% inactive 16.208 ms/op 16.524 ms/op 0.98
computeDeltas 2100000 validators 50% inactive 9.2340 ms/op 9.7183 ms/op 0.95
altair processAttestation - 250000 vs - 7PWei normalcase 2.5504 ms/op 1.6757 ms/op 1.52
altair processAttestation - 250000 vs - 7PWei worstcase 3.4411 ms/op 2.4419 ms/op 1.41
altair processAttestation - setStatus - 1/6 committees join 107.64 us/op 102.71 us/op 1.05
altair processAttestation - setStatus - 1/3 committees join 194.53 us/op 195.30 us/op 1.00
altair processAttestation - setStatus - 1/2 committees join 276.80 us/op 283.85 us/op 0.98
altair processAttestation - setStatus - 2/3 committees join 350.84 us/op 365.88 us/op 0.96
altair processAttestation - setStatus - 4/5 committees join 511.26 us/op 508.12 us/op 1.01
altair processAttestation - setStatus - 100% committees join 596.76 us/op 603.53 us/op 0.99
altair processBlock - 250000 vs - 7PWei normalcase 4.5960 ms/op 2.9518 ms/op 1.56
altair processBlock - 250000 vs - 7PWei normalcase hashState 16.994 ms/op 15.687 ms/op 1.08
altair processBlock - 250000 vs - 7PWei worstcase 24.404 ms/op 20.040 ms/op 1.22
altair processBlock - 250000 vs - 7PWei worstcase hashState 43.202 ms/op 41.054 ms/op 1.05
phase0 processBlock - 250000 vs - 7PWei normalcase 1.5612 ms/op 1.3091 ms/op 1.19
phase0 processBlock - 250000 vs - 7PWei worstcase 19.736 ms/op 17.814 ms/op 1.11
altair processEth1Data - 250000 vs - 7PWei normalcase 323.85 us/op 308.27 us/op 1.05
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 3.2200 us/op 3.3190 us/op 0.97
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 21.048 us/op 22.380 us/op 0.94
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 7.0760 us/op 5.9100 us/op 1.20
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 4.6840 us/op 4.6950 us/op 1.00
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 98.110 us/op 96.705 us/op 1.01
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4263 ms/op 1.4105 ms/op 1.01
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8741 ms/op 1.8354 ms/op 1.02
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.8709 ms/op 1.8297 ms/op 1.02
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.8717 ms/op 3.6837 ms/op 1.05
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.1058 ms/op 2.0874 ms/op 1.01
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.2508 ms/op 4.3609 ms/op 0.97
Tree 40 250000 create 369.46 ms/op 310.19 ms/op 1.19
Tree 40 250000 get(125000) 95.481 ns/op 97.624 ns/op 0.98
Tree 40 250000 set(125000) 1.0281 us/op 1.0278 us/op 1.00
Tree 40 250000 toArray() 16.960 ms/op 9.0300 ms/op 1.88
Tree 40 250000 iterate all - toArray() + loop 16.931 ms/op 9.1937 ms/op 1.84
Tree 40 250000 iterate all - get(i) 43.064 ms/op 34.998 ms/op 1.23
Array 250000 create 2.3422 ms/op 2.0615 ms/op 1.14
Array 250000 clone - spread 753.12 us/op 658.08 us/op 1.14
Array 250000 get(125000) 0.30400 ns/op 0.29500 ns/op 1.03
Array 250000 set(125000) 0.31300 ns/op 0.30400 ns/op 1.03
Array 250000 iterate all - loop 59.469 us/op 57.863 us/op 1.03
phase0 afterProcessEpoch - 250000 vs - 7PWei 41.145 ms/op 51.546 ms/op 0.80
Array.fill - length 1000000 2.3292 ms/op 2.2791 ms/op 1.02
Array push - length 1000000 10.294 ms/op 7.7963 ms/op 1.32
Array.get 0.21453 ns/op 0.20678 ns/op 1.04
Uint8Array.get 0.24968 ns/op 0.24599 ns/op 1.01
phase0 beforeProcessEpoch - 250000 vs - 7PWei 16.477 ms/op 13.524 ms/op 1.22
altair processEpoch - mainnet_e81889 291.35 ms/op 240.32 ms/op 1.21
mainnet_e81889 - altair beforeProcessEpoch 39.216 ms/op 13.177 ms/op 2.98
mainnet_e81889 - altair processJustificationAndFinalization 6.7960 us/op 5.0200 us/op 1.35
mainnet_e81889 - altair processInactivityUpdates 4.2186 ms/op 3.5099 ms/op 1.20
mainnet_e81889 - altair processRewardsAndPenalties 20.742 ms/op 17.456 ms/op 1.19
mainnet_e81889 - altair processRegistryUpdates 576.00 ns/op 540.00 ns/op 1.07
mainnet_e81889 - altair processSlashings 148.00 ns/op 147.00 ns/op 1.01
mainnet_e81889 - altair processEth1DataReset 142.00 ns/op 143.00 ns/op 0.99
mainnet_e81889 - altair processEffectiveBalanceUpdates 2.0420 ms/op 1.8867 ms/op 1.08
mainnet_e81889 - altair processSlashingsReset 716.00 ns/op 701.00 ns/op 1.02
mainnet_e81889 - altair processRandaoMixesReset 1.3700 us/op 1.0460 us/op 1.31
mainnet_e81889 - altair processHistoricalRootsUpdate 141.00 ns/op 147.00 ns/op 0.96
mainnet_e81889 - altair processParticipationFlagUpdates 463.00 ns/op 455.00 ns/op 1.02
mainnet_e81889 - altair processSyncCommitteeUpdates 121.00 ns/op 126.00 ns/op 0.96
mainnet_e81889 - altair afterProcessEpoch 43.498 ms/op 40.491 ms/op 1.07
capella processEpoch - mainnet_e217614 940.29 ms/op 725.54 ms/op 1.30
mainnet_e217614 - capella beforeProcessEpoch 74.547 ms/op 53.446 ms/op 1.39
mainnet_e217614 - capella processJustificationAndFinalization 7.5550 us/op 5.0630 us/op 1.49
mainnet_e217614 - capella processInactivityUpdates 18.096 ms/op 11.042 ms/op 1.64
mainnet_e217614 - capella processRewardsAndPenalties 106.29 ms/op 90.968 ms/op 1.17
mainnet_e217614 - capella processRegistryUpdates 4.6530 us/op 4.6050 us/op 1.01
mainnet_e217614 - capella processSlashings 149.00 ns/op 147.00 ns/op 1.01
mainnet_e217614 - capella processEth1DataReset 142.00 ns/op 143.00 ns/op 0.99
mainnet_e217614 - capella processEffectiveBalanceUpdates 19.663 ms/op 5.9748 ms/op 3.29
mainnet_e217614 - capella processSlashingsReset 738.00 ns/op 698.00 ns/op 1.06
mainnet_e217614 - capella processRandaoMixesReset 1.5470 us/op 1.1180 us/op 1.38
mainnet_e217614 - capella processHistoricalRootsUpdate 141.00 ns/op 145.00 ns/op 0.97
mainnet_e217614 - capella processParticipationFlagUpdates 529.00 ns/op 448.00 ns/op 1.18
mainnet_e217614 - capella afterProcessEpoch 113.95 ms/op 107.50 ms/op 1.06
phase0 processEpoch - mainnet_e58758 355.39 ms/op 255.62 ms/op 1.39
mainnet_e58758 - phase0 beforeProcessEpoch 78.886 ms/op 49.196 ms/op 1.60
mainnet_e58758 - phase0 processJustificationAndFinalization 6.9980 us/op 5.4030 us/op 1.30
mainnet_e58758 - phase0 processRewardsAndPenalties 17.345 ms/op 15.193 ms/op 1.14
mainnet_e58758 - phase0 processRegistryUpdates 2.3420 us/op 2.2580 us/op 1.04
mainnet_e58758 - phase0 processSlashings 145.00 ns/op 145.00 ns/op 1.00
mainnet_e58758 - phase0 processEth1DataReset 143.00 ns/op 241.00 ns/op 0.59
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 1.3743 ms/op 989.86 us/op 1.39
mainnet_e58758 - phase0 processSlashingsReset 982.00 ns/op 838.00 ns/op 1.17
mainnet_e58758 - phase0 processRandaoMixesReset 1.4900 us/op 1.1150 us/op 1.34
mainnet_e58758 - phase0 processHistoricalRootsUpdate 148.00 ns/op 152.00 ns/op 0.97
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.3990 us/op 1.0160 us/op 1.38
mainnet_e58758 - phase0 afterProcessEpoch 35.438 ms/op 33.921 ms/op 1.04
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.3913 ms/op 1.0250 ms/op 1.36
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.6400 ms/op 1.2491 ms/op 1.31
altair processInactivityUpdates - 250000 normalcase 13.112 ms/op 10.618 ms/op 1.23
altair processInactivityUpdates - 250000 worstcase 13.604 ms/op 10.660 ms/op 1.28
phase0 processRegistryUpdates - 250000 normalcase 2.2240 us/op 2.2410 us/op 0.99
phase0 processRegistryUpdates - 250000 badcase_full_deposits 142.49 us/op 147.31 us/op 0.97
phase0 processRegistryUpdates - 250000 worstcase 0.5 59.055 ms/op 49.405 ms/op 1.20
altair processRewardsAndPenalties - 250000 normalcase 17.350 ms/op 13.969 ms/op 1.24
altair processRewardsAndPenalties - 250000 worstcase 16.083 ms/op 13.487 ms/op 1.19
phase0 getAttestationDeltas - 250000 normalcase 5.3738 ms/op 5.4263 ms/op 0.99
phase0 getAttestationDeltas - 250000 worstcase 5.4688 ms/op 5.4567 ms/op 1.00
phase0 processSlashings - 250000 worstcase 60.928 us/op 58.919 us/op 1.03
altair processSyncCommitteeUpdates - 250000 10.686 ms/op 10.226 ms/op 1.04
BeaconState.hashTreeRoot - No change 172.00 ns/op 183.00 ns/op 0.94
BeaconState.hashTreeRoot - 1 full validator 72.920 us/op 59.003 us/op 1.24
BeaconState.hashTreeRoot - 32 full validator 923.12 us/op 664.30 us/op 1.39
BeaconState.hashTreeRoot - 512 full validator 8.5711 ms/op 6.5397 ms/op 1.31
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 107.67 us/op 72.267 us/op 1.49
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.4826 ms/op 1.0700 ms/op 1.39
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 19.351 ms/op 13.213 ms/op 1.46
BeaconState.hashTreeRoot - 1 balances 72.360 us/op 59.538 us/op 1.22
BeaconState.hashTreeRoot - 32 balances 758.45 us/op 582.51 us/op 1.30
BeaconState.hashTreeRoot - 512 balances 5.9814 ms/op 4.8460 ms/op 1.23
BeaconState.hashTreeRoot - 250000 balances 109.81 ms/op 108.16 ms/op 1.02
aggregationBits - 2048 els - zipIndexesInBitList 19.619 us/op 18.801 us/op 1.04
regular array get 100000 times 22.719 us/op 22.868 us/op 0.99
wrappedArray get 100000 times 22.655 us/op 22.858 us/op 0.99
arrayWithProxy get 100000 times 9.8926 ms/op 19.399 ms/op 0.51
ssz.Root.equals 21.697 ns/op 81.430 ns/op 0.27
byteArrayEquals 21.502 ns/op 21.412 ns/op 1.00
Buffer.compare 8.8800 ns/op 8.8940 ns/op 1.00
processSlot - 1 slots 8.9790 us/op 8.1630 us/op 1.10
processSlot - 32 slots 2.4016 ms/op 1.5683 ms/op 1.53
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.2955 ms/op 2.3121 ms/op 1.86
getCommitteeAssignments - req 1 vs - 250000 vc 1.6395 ms/op 1.6784 ms/op 0.98
getCommitteeAssignments - req 100 vs - 250000 vc 3.3864 ms/op 3.4462 ms/op 0.98
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6674 ms/op 3.6700 ms/op 1.00
findModifiedValidators - 10000 modified validators 722.02 ms/op 743.71 ms/op 0.97
findModifiedValidators - 1000 modified validators 384.40 ms/op 495.33 ms/op 0.78
findModifiedValidators - 100 modified validators 303.07 ms/op 306.53 ms/op 0.99
findModifiedValidators - 10 modified validators 203.46 ms/op 219.65 ms/op 0.93
findModifiedValidators - 1 modified validators 141.23 ms/op 161.27 ms/op 0.88
findModifiedValidators - no difference 154.06 ms/op 174.55 ms/op 0.88
migrate state 1500000 validators, 3400 modified, 2000 new 3.2479 s/op 2.9033 s/op 1.12
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7300 ns/op 3.9400 ns/op 0.95
state getBlockRootAtSlot - 250000 vs - 7PWei 419.91 ns/op 376.85 ns/op 1.11
computeProposerIndex 100000 validators 1.3970 ms/op 1.3775 ms/op 1.01
getNextSyncCommitteeIndices 1000 validators 2.9619 ms/op 2.9681 ms/op 1.00
getNextSyncCommitteeIndices 10000 validators 25.650 ms/op 26.180 ms/op 0.98
getNextSyncCommitteeIndices 100000 validators 90.855 ms/op 92.702 ms/op 0.98
computeProposers - vc 250000 553.01 us/op 1.0542 ms/op 0.52
computeEpochShuffling - vc 250000 40.171 ms/op 44.124 ms/op 0.91
getNextSyncCommittee - vc 250000 11.220 ms/op 9.8339 ms/op 1.14
nodejs block root to RootHex using toHex 90.290 ns/op 93.633 ns/op 0.96
nodejs block root to RootHex using toRootHex 52.656 ns/op 55.861 ns/op 0.94
nodejs fromHex(blob) 784.69 us/op 768.94 us/op 1.02
nodejs fromHexInto(blob) 634.85 us/op 671.19 us/op 0.95
nodejs block root to RootHex using the deprecated toHexString 531.77 ns/op 558.73 ns/op 0.95
nodejs byteArrayEquals 32 bytes (block root) 25.477 ns/op 26.523 ns/op 0.96
nodejs byteArrayEquals 48 bytes (pubkey) 37.086 ns/op 38.289 ns/op 0.97
nodejs byteArrayEquals 96 bytes (signature) 33.330 ns/op 37.527 ns/op 0.89
nodejs byteArrayEquals 1024 bytes 40.118 ns/op 44.738 ns/op 0.90
nodejs byteArrayEquals 131072 bytes (blob) 1.7469 us/op 1.7798 us/op 0.98
browser block root to RootHex using toHex 141.96 ns/op 147.28 ns/op 0.96
browser block root to RootHex using toRootHex 128.18 ns/op 132.74 ns/op 0.97
browser fromHex(blob) 1.4746 ms/op 1.5468 ms/op 0.95
browser fromHexInto(blob) 624.87 us/op 635.51 us/op 0.98
browser block root to RootHex using the deprecated toHexString 370.01 ns/op 375.34 ns/op 0.99
browser byteArrayEquals 32 bytes (block root) 27.098 ns/op 28.013 ns/op 0.97
browser byteArrayEquals 48 bytes (pubkey) 38.434 ns/op 39.544 ns/op 0.97
browser byteArrayEquals 96 bytes (signature) 72.060 ns/op 74.236 ns/op 0.97
browser byteArrayEquals 1024 bytes 753.36 ns/op 755.22 ns/op 1.00
browser byteArrayEquals 131072 bytes (blob) 95.056 us/op 96.047 us/op 0.99

by benchmarkbot/action

@jtraglia
Copy link
Copy Markdown
Contributor

jtraglia commented Feb 3, 2026

Hey @nazarhussain, we chatted about running FCR tests at the meeting today.

This is how you can generate these tests:

  1. Clone Mikhail's fork/branch with FCR
git clone git@github.com:mkalinin/eth2.0-specs.git -b fast-conf-rule
  1. Generate the tests
cd eth2.0-specs
make reftests runner=fast_confirmation

This will write the tests to ../consensus-spec-tests.

Note: There is currently a failing test with Gloas, which sort of breaks test generation. So you could generate the reference tests for a specific fork instead, something like the following. After Mikhail fixes the issue, you can use the command above.

make reftests runner=fast_confirmation fork=fulu

@nazarhussain
Copy link
Copy Markdown
Contributor Author

@nflaig Related to your feedback earlier. I checked that the fast confirmation specs are generated as @with_presets([MINIMAL]) so they do not emit the mainnet specs. For minimal spec it's all passing.

https://github.com/ethereum/consensus-specs/blob/5e5abe333f9d03eca5bd0c756827123e61388cd6/tests/core/pyspec/eth_consensus_specs/test/phase0/fast_confirmation/test_basic.py#L23

@nazarhussain nazarhussain marked this pull request as ready for review May 4, 2026 14:09
@nazarhussain nazarhussain requested a review from a team as a code owner May 4, 2026 14:09
Comment thread packages/fork-choice/src/forkChoice/interface.ts Outdated
Comment thread packages/fork-choice/src/forkChoice/store.ts Outdated
Copy link
Copy Markdown
Contributor

@twoeths twoeths left a comment

Choose a reason for hiding this comment

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

need to remove CheckpointWithPayloadStatus because we removed it for gloas due to ethereum/consensus-specs#5094

@nazarhussain nazarhussain requested a review from twoeths May 19, 2026 08:28
twoeths
twoeths previously approved these changes May 20, 2026
Copy link
Copy Markdown
Contributor

@lodekeeper lodekeeper left a comment

Choose a reason for hiding this comment

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

Multi-persona review — PR #8837 (feat: fast confirmation rule)

🤖 Generated with AI assistance: multi-persona review fan-out (architect, wisdom, devil's advocate, security, bugs). Findings consolidated by @lodekeeper. Reviewed commit: 8066f6b.

@nazarhussain — large but well-structured PR. The algorithm itself looks spec-aligned; the structural feedback below is mostly about API surface, opt-in scope, and code organization. No security findings. Bugs reviewer timed out, so the bug-class is under-covered — flagging that explicitly.

TL;DR

Reviewer Verdict
Security No issues
Architecture 2 coupling concerns (fork-choice ↔ state-view, store interface bloat)
Devil's Advocate RECONSIDER — 3 structural pushbacks (API surface, opt-in scope, PR shape)
Wisdom 12 maintainability findings + 5 smaller notes
Bugs ⚠️ Timed out — not covered. Recommend a focused bug-class pass before merge.

🔴 Structural concerns (devil's advocate)

These are the heaviest items — worth deciding on before merge, not as follow-ups.

1. Public API mismatch with the merged cross-client standard

  • This PR adds GET /eth/v1/lodestar/fast_confirmation_info (polling, Lodestar-namespaced) and the docs position it as the consumer-facing API for bridges/exchanges/custodians.
  • The merged cross-client standard is beacon-APIs#598 (merged 2026-04-20) — a fast_confirmation SSE event on /eth/v1/events with fields {block, slot}. beacon-APIs#611 (open since 2026-05-22) refines same-slot dedup.
  • Nimbus already implemented the standardized event in nimbus-eth2#8479 (merged 2026-05-20).
  • The PR's own docs concede the divergence: "Consumers should not assume that every Ethereum client exposes the same signal or the same API."

Of the 4 fields returned by the polling endpoint, 3 are already available via existing standard endpoints (/eth/v1/beacon/headers/head, /eth/v1/beacon/states/{state_id}/finality_checkpoints). Only confirmed is new. Also: when --chain.fastConfirmation is off, getConfirmedRoot() returns the justified root, so the confirmed field becomes silently misleading (renamed-justified).

Counter-proposal: Implement the standardized fast_confirmation SSE event on /eth/v1/events to match beacon-APIs#598. If a debug/observability polling endpoint is still wanted, ship it explicitly namespaced as Lodestar-internal in the docs and consider deferring it to a follow-up.

2. "Opt-in / experimental" claim is half-true

The --chain.fastConfirmation flag only gates rule execution. The supporting surface lands unconditionally:

Surface Gated by flag?
IForkChoiceStore extends IFastConfirmationStore (9 new fields) ❌ schema change for every store
ForkChoiceStore ctor reads finalized state + initializes 9 FCR fields ❌ runs unconditionally
stateGetter as required ctor argument on every ForkChoice construction ❌ always wired
/eth/v1/lodestar/fast_confirmation_info registration ❌ endpoint always responds
safeBlocks.ts::getSafeBeaconBlockRoot / getSafeExecutionBlockHash routing ❌ now reads getConfirmedRoot() first (falls back to justified, but code path differs from baseline)

Compare Lighthouse sigp/lighthouse#8951: "I have implemented this without touching any production code."

Counter-proposal — pick a side:

  • A — Commit fully: drop the flag, ship on by default. Spec is merged, supporting surface is already always-on.
  • B — Actually isolate (recommended): keep the flag and make every checked row above gated — sub-store composition, optional stateGetter, conditional route registration, baseline-identical safeBlocks.ts when disabled.

3. Single 4549-line PR vs. Nimbus-style stack

  • Nimbus: ~30 incremental PRs Feb–May 2026. Still merging spec follow-ups — nimbus-eth2#8512 ("Ensure confirmed chain includes greatest unrealized checkpoint") merged 2026-05-26, 10 days after spec merge.
  • Lighthouse: single-PR shape, WIP for 3 months.
  • This PR follows the Lighthouse shape and will pay the same costs: review depth on 4 consensus-critical rules in a 4549-line diff, rebase cost across 44 files touching shared interfaces, spec-sync cost when Nimbus-style follow-ups land, bisect cost if a consensus bug surfaces.

Counter-proposal — 5-PR stack:

  1. Config + types + spec test runner (~600 lines) — establishes spec-correctness gate immediately
  2. Pure helpers (~1000 lines) — fastConfirmation/utils.ts minus glue
  3. Snapshot + rules (~500 lines) — the algorithm
  4. ForkChoice wiring (~300 lines) — runFastConfirmation, store schema (ideally as the optional sub-store from #2)
  5. Public surface (~200 lines + dashboard) — SSE event, docs

(I realize a restructure-before-merge ask is a big lift on an already-large PR. If the team's call is "land as-is, fix in follow-ups," at least #1 (API surface) should be settled here so we don't ship a Lodestar-only consumer API that we'd then deprecate.)


🟡 Architecture concerns

A1. Fork-choice now reaches through a full state-transition state view

Files: packages/fork-choice/src/forkChoice/fastConfirmation/{types,utils}.ts, packages/beacon-node/src/chain/chain.ts, packages/state-transition/src/stateView/{interface,beaconStateView}.ts

@lodestar/fork-choice now consumes IBeaconStateView through ForkChoiceStateGetter and calls processSlots(), committee lookup, shuffling access, validator access, and balance extraction. The state-view interface is widened to satisfy these fork-choice calls.

Impact: Fork-choice becomes coupled to the full BeaconStateView surface and to beacon-node's state-cache lifetime semantics. Future state-view or cache changes can break fast confirmation even when fork-choice DAG/vote contracts remain valid.

Recommendation: Keep FCR orchestration in fork-choice, but move state-derived calculations behind a narrow adapter. Have beacon-node/state-transition provide primitive balance / active-validator / committee data through explicit callbacks, without exposing IBeaconStateView or processSlots() to fork-choice.

A2. Optional FCR state merged into core fork-choice store contract

Files: packages/fork-choice/src/forkChoice/store.ts, packages/fork-choice/src/forkChoice/fastConfirmation/types.ts, packages/fork-choice/src/index.ts

IForkChoiceStore now extends IFastConfirmationStore. ForkChoiceStore requires a stateGetter ctor argument. All FCR fields are initialized on every store even when the feature is disabled. The package root exports several FCR internals (FCRBalanceSource, FCRContext, IFCRStore, ...).

Impact: Every fork-choice consumer, mock, and test must now model FCR fields even when disabled. Experimental rule's implementation shape becomes part of the broader package contract.

Recommendation: Compose FastConfirmationStore as a separate, optionally-owned store. Keep IForkChoiceStore focused on canonical fork-choice state. Expose only stable public methods (getConfirmedRoot(), getConfirmedBlock()). Avoid root-level exports of FCR internals.

This pairs with Devil's-Advocate #2 — solving the "actually isolate" Option B naturally resolves A2.


🟢 Readability / maintainability (wisdom — 12 findings)

Pick the ones that resonate; none are merge blockers individually.

  1. loop1Condition / loop2Condition in findLatestConfirmedDescendantpackages/fork-choice/src/forkChoice/fastConfirmation/utils.ts:3184-3300. The logger already uses the better names ("Fast confirmation previous-epoch loop ..." / "... current-epoch loop ..."). Rename to shouldAdvanceThroughPreviousEpoch / shouldAdvanceThroughCurrentEpoch, optionally extract each loop into a helper so the top-level reads as a 4-5 line orchestration.

  2. Stop encoding tuples into string keys in getCurrentTargetScoreutils.ts:2999-3024. Uses `${msg.root}:${msg.epoch}` then lastIndexOf(":") + Number(...) to parse back. Elsewhere in the same module the order is reversed (`${epoch}:${rootHex}`). Use Map<RootHex, Map<Epoch, number>> instead — eliminates parse round-trip, removes the fragile lastIndexOf invariant.

  3. Cache map keyed by generic string instead of literal uniontypes.ts:2298, utils.ts:2667-2723. voteWeightBySource: Map<string, ...> with a doc comment saying "current" | "previous". Let the type system enforce it: type BalanceSourceKey = "current" | "previous". Currently a typo'd "latest" would silently miss the cache.

  4. Duplicated branches in ensureVoteMapsutils.ts:2681-2700. Two near-identical loops differ only by iteration source and slashed-check. Compute iteration set + per-index "is slashed?" predicate up front, run one loop. Cuts ~20 lines, future exclusions only need to be added once.

  5. confirmed_reset reason loses causerules.ts:2129-2152. Three distinct conditions (confirmedEpochBehindHead, notAncestorOfHead, allChildrenNotConfirmed) collapse into a single reason: "confirmed_reset". Emit which guard fired (reset_behind / reset_not_ancestor / reset_chain_unsafe) or include flags in metadata — first question on a prod reset is "which of the three?".

  6. Repeated neutral-threshold object literalutils.ts:2884-2904. Two early-return paths in computeSafetyThreshold produce the same {threshold: POSITIVE_INFINITY, proposerScore: 0, ...}. Name the concept: SAFETY_THRESHOLD_UNREACHABLE (frozen) → return from both early exits.

  7. Optional chaining on a required interface methodpackages/fork-choice/src/forkChoice/safeBlocks.ts:3500, 3520. IForkChoice declares getConfirmedRoot(): RootHex as required (interface.ts:3477), but safeBlocks.ts calls fc.getConfirmedRoot?.(). Drop the ?.. If tests pass partial mocks, fix the mocks.

  8. FCR* re-export aliasespackages/fork-choice/src/index.ts:3617-3627. Internal names are FastConfirmation*; public re-export renames them to FCR* (FCRContext, FCRResult, IFCRStore, ...). Pick one — two names for one concept will fragment grep and rename refactors.

  9. Dead decision.stop fieldtypes.ts:2275-2280, rules.ts:2213-2216. FastConfirmationDecision.stop?: boolean declared and honored with if (decision.stop) break;, but no rule ever sets it. Remove, or document the early-exit hook as part of the public rule API.

  10. getSupportDiscount is a transparent passthroughutils.ts:2861-2869. getSupportDiscount(...) does nothing but call computeEmptySlotSupportDiscount(...) with the same args. Pick one name (or inline at the single caller).

  11. COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR = 5utils.ts:2352. Constant name is good but no spec citation for the 5. Add a spec link/section ("rounded-up safety margin from <spec> §X"). Same applies to the 999 ceiling-division trick in the same function.

  12. Empty meta on FCR failure warningpackages/fork-choice/src/forkChoice/forkChoice.ts:3414. this.logger?.warn("Fast confirmation failed", {}, err as Error). Include slot/head/confirmedRoot in meta so an operator can act without correlating with surrounding logs.

Smaller notes

  • utils.ts:2354-2361 etc. — "has → get ?? default" caching dance repeated 5× (getBlock, getAncestorRoots, getCheckpointState, getSlotCommittee, isDescendantCached). Use const cached = cache.x.get(key); if (cached !== undefined) return cached; to skip the double-hash.
  • utils.ts:2396-2402, 2453-2457try { ctx.getAncestor(...) } catch { return null } — add a one-line comment to make the catch intentional rather than apologetic.
  • fastConfirmationRule.ts:2027-2041updateFastConfirmationMetrics sets confirmedSlot/confirmedEpoch to 0 when block is missing. 0 is a real slot/epoch; consider NaN (Prometheus drops it) or skipping .set.
  • packages/utils/src/metrics.ts:5260-5276EndTimer = (() => number) | undefined is a surprising shape for the type alias; consider moving the optionality to the parameter site.
  • forkChoice.ts:3456-3463getTrackedVotesCount does a linear scan of voteNextIndices each slot. If call frequency increases, maintain a counter alongside the array.

🟢 Security — clean

Reviewed: new endpoint, FCR execution timing & lifecycle, fastConfirmation/*, related safeBlocks.ts changes. Focus on DoS/resource usage, trust boundaries (latest messages, equivocations, execution-status), validator-balance arithmetic, adversarial vote handling.

  • Endpoint is read-only, comparable to existing chain-status surfaces.
  • Disabled by default; rebuild runs on per-slot tick when enabled, not per gossip message.
  • Vote/committee scans use bounded consensus data structures; locally-known equivocators excluded; execution status checked before treating block as one-confirmed.
  • Balance arithmetic in effective-balance increments; well within JS safe-integer limits.

⚠️ Bugs review — TIMED OUT

The bug-class reviewer hit a 50-tool-call timeout and a re-spawn aborted. The other reviews touched correctness incidentally (e.g. wisdom #2 flagging the lastIndexOf invariant) but no dedicated bug pass landed. Given the 4549-line diff and that the rules gate safe_block_hash in engine API, I'd recommend either a targeted manual bug-class pass on fastConfirmation/{rules,utils,data}.ts before merge, or an explicit acknowledgement that bug coverage here came from existing reviewer attention rather than a fresh dedicated pass.


Reports are on disk at ~/.openclaw/workspace/notes/review-reports/pr-8837-*.md if anyone wants the unedited per-reviewer output. Happy to dig deeper on any specific item.

Copy link
Copy Markdown
Contributor

@lodekeeper lodekeeper left a comment

Choose a reason for hiding this comment

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

Inline-comment batch supplementing the earlier structural review (#8837 (review)). These are the file:line-anchored findings from the wisdom + architecture passes. None are individual merge blockers; the structural pushbacks remain the main asks.

🤖 Generated with AI assistance.

Comment on lines +854 to +862
const loop1Condition =
confirmedEpoch !== null &&
confirmedEpoch + 1 === currentEpoch &&
previousSlotVotingSource !== null &&
previousSlotVotingSource.epoch + 2 >= currentEpoch &&
(isStartSlotOfEpoch(snapshot.currentSlot) ||
(willNoConflictingCheckpointBeJustified(ctx, store, cache) &&
((prevSlotJustification !== null && prevSlotJustification.epoch + 1 >= currentEpoch) ||
(headJustification !== null && headJustification.epoch + 1 >= currentEpoch))));
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.

Self-documenting names for the descendant-search loops. The logger already uses the better names ("Fast confirmation previous-epoch loop ..." and "... current-epoch loop ..."). Consider renaming loop1Condition / loop2Condition to shouldAdvanceThroughPreviousEpoch / shouldAdvanceThroughCurrentEpoch so the top-level reads as a 4–5 line orchestration. Optionally extract each loop body into a small helper.

Why: loop1 / loop2 forces every reader to scroll the body to discover what each branch does.

Comment on lines +679 to +689
const groupKey = `${msg.root}:${msg.epoch}`;
voteGroups.set(groupKey, (voteGroups.get(groupKey) ?? 0) + weight);
}

// Check each unique vote group's checkpoint against the target
const targetKey = `${target.epoch}:${target.rootHex}`;
let score = 0;
for (const [groupKey, weight] of voteGroups) {
const sepIdx = groupKey.lastIndexOf(":");
const root = groupKey.slice(0, sepIdx);
const epoch = Number(groupKey.slice(sepIdx + 1)) as Epoch;
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.

Stop encoding tuples into string keys. This uses `${msg.root}:${msg.epoch}` then parses it back with lastIndexOf(":") + Number(...). Elsewhere in the same module the order is reversed (`${epoch}:${rootHex}`).

Suggested:

const voteGroups = new Map<RootHex, Map<Epoch, number>>();
// ...
for (const [root, byEpoch] of voteGroups) {
  for (const [epoch, weight] of byEpoch) {
    const cp = getCheckpointForBlock(ctx, root, epoch);
    if (cp && cp.epoch === target.epoch && cp.rootHex === target.rootHex) score += weight;
  }
}

Why: eliminates a class of bugs (key-order mismatches), removes the manual parse, and avoids relying on the unspoken invariant that RootHex never contains :.

committeeBySlot: Map<Slot, Set<ValidatorIndex>>;
isDescendantByRootPair: Map<string, boolean>;
/** voteRoot -> totalWeight, keyed by sourceKey ("current" | "previous") */
voteWeightBySource: Map<string, Map<RootHex, number>>;
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.

Cache map keyed by generic string instead of the literal union. The call sites already use "current" | "previous" (see utils.ts:341). Encoding it in the type avoids silent cache misses on a typo'd key:

export type BalanceSourceKey = "current" | "previous";
voteWeightBySource: Map<BalanceSourceKey, Map<RootHex, number>>;

Comment on lines +351 to +370
if (activeIndices !== null && state) {
for (const i of activeIndices) {
if (state.getValidator(i).slashed) continue;
if (equivocating.has(i)) continue;
const msg = ctx.getLatestMessage(i);
if (!msg) continue;
const weight = balances[i] ?? 0;
if (weight === 0) continue;
voteMap.set(msg.root, (voteMap.get(msg.root) ?? 0) + weight);
}
} else {
for (let i = 0; i < balances.length; i++) {
const weight = balances[i] ?? 0;
if (weight === 0) continue;
if (equivocating.has(i)) continue;
const msg = ctx.getLatestMessage(i);
if (!msg) continue;
voteMap.set(msg.root, (voteMap.get(msg.root) ?? 0) + weight);
}
}
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.

Duplicated branches in ensureVoteMaps. The two loops differ only by (a) iteration source (activeIndices vs. 0..balances.length) and (b) whether state.getValidator(i).slashed is checked. Compute both up front and run a single loop:

const indices: Iterable<number> = activeIndices ?? balances.keys();
const isSlashed = state ? (i: number) => state.getValidator(i).slashed : () => false;
for (const i of indices) {
  if (isSlashed(i)) continue;
  if (equivocating.has(i)) continue;
  const w = balances[i] ?? 0;
  if (w === 0) continue;
  const m = ctx.getLatestMessage(i);
  if (!m) continue;
  voteMap.set(m.root, (voteMap.get(m.root) ?? 0) + w);
}

Future exclusions then only need to be added once.


if (confirmedEpochBehindHead || notAncestorOfHead || allChildrenNotConfirmed) {
const didReset = decision.didReset || decision.confirmedRoot !== snapshot.finalizedRoot;
return {confirmedRoot: snapshot.finalizedRoot, didReset, reason: "confirmed_reset"};
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.

confirmed_reset reason loses the actual cause. This rule has three distinct guards (confirmedEpochBehindHead, notAncestorOfHead, allChildrenNotConfirmed) that all collapse into a single reason: "confirmed_reset". When an operator sees a reset in production, the first question is "which of the three?".

Either emit a discriminant (reset_behind / reset_not_ancestor / reset_chain_unsafe) or include the three boolean flags in the metadata so post-incident analysis doesn't need a local reproduction with verbose logging.

const result = this.fastConfirmationRule.onSlotStartAfterPastAttestationsApplied(this.fastConfirmationContext);
this.fcStore.confirmedRoot = result.confirmedRoot;
} catch (err) {
this.logger?.warn("Fast confirmation failed", {}, err as Error);
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.

Empty meta object on the FCR failure warning. If this ever fires in production, the empty {} forces the operator to correlate the warn with surrounding logs just to learn the slot. Two extra fields make every report self-contained:

this.logger?.warn(
  "Fast confirmation failed",
  {slot: this.fcStore.currentSlot, head: this.head.blockRoot, confirmedRoot: this.fcStore.confirmedRoot},
  err as Error,
);

Comment on lines +150 to +151
this.metrics.fastConfirmation.confirmedSlot.set(0);
this.metrics.fastConfirmation.confirmedEpoch.set(0);
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.

confirmedSlot=0 / confirmedEpoch=0 is ambiguous on a missing block. 0 is a real slot/epoch, so a dashboard reader can't distinguish "no confirmed block" from "confirmed block at slot 0". Either set Number.NaN (Prometheus drops NaN so the gauge keeps its last value), or skip the .set so the gauge stays at its last value explicitly.

Comment thread packages/utils/src/metrics.ts Outdated
static<Labels extends LabelsGeneric = NoLabels>(config: StaticConfig<Labels>): void;
}

export type EndTimer = (() => number) | undefined;
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.

EndTimer = (() => number) | undefined is a surprising shape for a type alias. Optionality usually belongs at the parameter site, not baked into the type. Consider renaming to MaybeEndTimer, or making EndTimer = () => number and declaring withObservedDuration(endTimer: EndTimer | undefined, ...).

Why: readers of an EndTimer value reasonably expect they can call it directly without a null check.

}),
getFinalizedCheckpoint: () => this.fcStore.finalizedCheckpoint,
getEquivocatingIndices: () => this.fcStore.equivocatingIndices,
getTrackedVotesCount: () => {
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.

getTrackedVotesCount does a linear scan of voteNextIndices each slot. Cheap today, but if the call frequency increases (e.g. exposed via metrics on every slot), prefer maintaining a counter alongside the array.

* - `time` is represented using `Slot` instead of UNIX epoch `u64`.
*/
export interface IForkChoiceStore {
export interface IForkChoiceStore extends IFastConfirmationStore {
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.

Architect concern — optional FCR state is merged into the core fork-choice store contract. IForkChoiceStore extends IFastConfirmationStore makes every fork-choice consumer, mock, and test model 9 FCR fields even when --chain.fastConfirmation is off. The ctor also reads finalized state via stateGetter (line 104) and initializes all FCR fields unconditionally.

Combined with the FCR* root-level exports in packages/fork-choice/src/index.ts, an experimental rule's implementation shape becomes part of the broader fork-choice package contract.

Recommendation: Compose FastConfirmationStore as a separate, optionally-owned store. Keep IForkChoiceStore focused on canonical fork-choice state. Expose only stable public methods (getConfirmedRoot(), getConfirmedBlock()). This also resolves the devil's-advocate "actually isolate" sub-point in the structural review.

@nazarhussain
Copy link
Copy Markdown
Contributor Author

@lodekeeper Most of the inline comments are handled in recent commit.

Following still open, see my responses.

getTrackedVotesCount
Keeping the scan for now. This is currently called once during the FCR slot update for metrics, so the cost is bounded and avoids maintaining another mutable counter alongside voteNextIndices. If we later expose this at higher frequency, we can add a counter.

State getter coupling
I agree the narrower adapter boundary is cleaner, and I did prototype it but after prototyping it the change is broader than I want to include in this PR. I’d prefer to keep this PR focused on spec alignment and open a follow-up issue to move FCR state access behind a narrow provider/adapter.

Optional FCR store isolation:
Agreed on the direction. Fully separating FastConfirmationStore from IForkChoiceStore touches constructor/test setup and public fork-choice shape, so I’d rather handle it as a focused follow-up. This PR keeps runtime execution gated by chain.fastConfirmation; structural isolation can be done separately without blocking spec validation.

SSE event vs Lodestar specific endpoint
Standard SSE event is the right approach to do it and already agreed to do it in separate PR. So in that PR will also remove the lodestar specific endpoint.

@lodekeeper
Copy link
Copy Markdown
Contributor

Thanks for the responses, quick acks + one ask:

  • getTrackedVotesCount: agreed, scan is fine at one call per FCR slot update. The note you left covers the higher-frequency case.
  • State getter coupling: SGTM on follow-up. Could you open a tracking issue for narrowing FCR state access behind a provider/adapter, so the boundary work isn't lost?
  • Optional FCR store isolation: SGTM on focused follow-up — same ask, a tracking issue would be great.
  • SSE event vs Lodestar endpoint: sounds right. When the SSE PR lands, removing /eth/v1/lodestar/fast_confirmation_info + its tests in the same change keeps the divergence story clean.

@nazarhussain
Copy link
Copy Markdown
Contributor Author

@lodekeeper Here are the followup issues #9438 #9437

@lodekeeper
Copy link
Copy Markdown
Contributor

Thanks — #9437 and #9438 both match what was asked for. Tracked.

@nazarhussain
Copy link
Copy Markdown
Contributor Author

@lodekeeper If all points are addressed please approve the RP. :)

@lodekeeper
Copy link
Copy Markdown
Contributor

Walking through my points against the current head (`9a0c038` / `500226c2d8` "as per feedback"):

Inline — all resolved in `500226c2`:

  • `r3329133070` loop conditions → renamed (`shouldAdvanceThroughPreviousEpoch`, ...)
  • `r3329133074` tuple-string keys → `Map<RootHex, Map<Epoch, number>>`
  • `r3329133077` `Map<string, ...>` for sourceKey → `BalanceSourceKey` literal union
  • `r3329133080` duplicated `ensureVoteMaps` branches → unified with `indices` / `isSlashed` predicates
  • `r3329133083` generic `confirmed_reset` reason → `FastConfirmationDecisionReason` enum
  • `r3329133084` repeated neutral-threshold literal → `SAFETY_THRESHOLD_UNREACHABLE`
  • `r3329133087` `fc.getConfirmedRoot?.()` → optional chaining dropped in `safeBlocks.ts`
  • `r3329133089` `FCR*` re-export aliases → exports use canonical `FastConfirmation*` names
  • `r3329133091` dead `decision.stop` field → removed from type and runner
  • `r3329133092` `getSupportDiscount` passthrough → removed
  • `r3329133094` `COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR=5` + `+ 999` → spec link at lines 23-25 + extra comment in `adjustCommitteeWeightEstimateToEnsureSafety` (acked in-thread)
  • `r3329133096` empty meta on FCR failure warn → now includes `slot`/`head`/`confirmedRoot`
  • `r3329133097` `confirmedSlot=0` ambiguity → no-block else-branch removed
  • `r3329133099` `EndTimer` optional-in-type → `EndTimer = () => number`, optionality moved to parameter sites

Issue-level — deferred but tracked:

From my review surface every point I raised is addressed or tracked. One caveat I want to repeat from the original review (`pullrequestreview-4395743466`): the bugs-class reviewer timed out and I flagged "recommend a focused bug-class pass before merge" — that part hasn't happened yet, just noting.

On the formal APPROVE — I'm staying with a COMMENTED review here (that's the lodekeeper pattern across PRs #9350 / #9332 / #9317 / #9430 / #9334). The merge gate is better held by maintainer review — Nico's "did you confirm on mainnet? consistent with Prysm?" question + twoeths' `onFastConfirmation` integration discussion are the active ones from my side.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants