Skip to content

feat: classify range sync batch processing fault#9667

Open
twoeths wants to merge 5 commits into
unstablefrom
te/classify_batch_processing_fault
Open

feat: classify range sync batch processing fault#9667
twoeths wants to merge 5 commits into
unstablefrom
te/classify_batch_processing_fault

Conversation

@twoeths

@twoeths twoeths commented Jul 16, 2026

Copy link
Copy Markdown
Member

Motivation

Description

  • reset MAX_BATCH_PROCESSING_ATTEMPTS to 3 again
  • based on processChainSegment() BlockError code of batch n, we classify if it's batch n error, or batch n - 1 or ambiguous and handle accordingly, see classifyProcessingFault() in batches.ts util
    • to do that, we separate 2 errors: "PARENT_UNKNOWN" block error happen at the 1st block of chain segment
    • if error happen in the middle, it's NON_LINEAR error
  • the other advantage of classifyProcessingFault() is to handle 0-block batch in fix: sync through empty epochs in range sync #9417 (through i want to make sure devnet sync works well first)
  • add Batch.retainForReprocessing() when the current batch itself is good, we retain and invalidate previous batches

AI Assistance Disclosure

  • created with the help of Claude

cc @ensi321

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces fault localization for sync batch processing errors to avoid tearing down the entire sync chain on any error. It adds a classification mechanism (classifyProcessingFault) to determine if a processing failure is due to the current batch, a previous batch, or is ambiguous, allowing the node to re-download only the faulty batch(es). It also adds a retainForReprocessing state transition for batches, increases the maximum batch processing attempts, and gates peer penalization during teardown to finalized sync only. I have no feedback to provide as there are no review comments.

Important

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

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: 1ead8bd Previous: 27da921 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 984.78 us/op 1.2362 ms/op 0.80
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 56.412 us/op 39.670 us/op 1.42
BLS verify - blst 775.69 us/op 751.55 us/op 1.03
BLS verifyMultipleSignatures 3 - blst 1.4051 ms/op 1.3564 ms/op 1.04
BLS verifyMultipleSignatures 8 - blst 2.2303 ms/op 2.1595 ms/op 1.03
BLS verifyMultipleSignatures 32 - blst 7.2266 ms/op 6.8164 ms/op 1.06
BLS verifyMultipleSignatures 64 - blst 13.503 ms/op 13.555 ms/op 1.00
BLS verifyMultipleSignatures 128 - blst 26.529 ms/op 25.874 ms/op 1.03
BLS deserializing 10000 signatures 658.64 ms/op 640.88 ms/op 1.03
BLS deserializing 100000 signatures 6.8012 s/op 6.3913 s/op 1.06
BLS verifyMultipleSignatures - same message - 3 - blst 782.46 us/op 804.75 us/op 0.97
BLS verifyMultipleSignatures - same message - 8 - blst 948.03 us/op 933.10 us/op 1.02
BLS verifyMultipleSignatures - same message - 32 - blst 1.6238 ms/op 1.5457 ms/op 1.05
BLS verifyMultipleSignatures - same message - 64 - blst 2.5354 ms/op 2.3648 ms/op 1.07
BLS verifyMultipleSignatures - same message - 128 - blst 4.2927 ms/op 4.0503 ms/op 1.06
BLS aggregatePubkeys 32 - blst 18.195 us/op 17.580 us/op 1.03
BLS aggregatePubkeys 128 - blst 65.366 us/op 62.618 us/op 1.04
getSlashingsAndExits - default max 58.178 us/op 54.658 us/op 1.06
getSlashingsAndExits - 2k 469.71 us/op 367.74 us/op 1.28
proposeBlockBody type=full, size=empty 746.87 us/op 638.74 us/op 1.17
isKnown best case - 1 super set check 172.00 ns/op 166.00 ns/op 1.04
isKnown normal case - 2 super set checks 178.00 ns/op 164.00 ns/op 1.09
isKnown worse case - 16 super set checks 246.00 ns/op 166.00 ns/op 1.48
validate api signedAggregateAndProof - struct 1.5927 ms/op 1.5062 ms/op 1.06
validate gossip signedAggregateAndProof - struct 1.5797 ms/op 1.5042 ms/op 1.05
batch validate gossip attestation - vc 640000 - chunk 32 142.87 us/op 106.35 us/op 1.34
batch validate gossip attestation - vc 640000 - chunk 64 100.95 us/op 92.630 us/op 1.09
batch validate gossip attestation - vc 640000 - chunk 128 98.111 us/op 86.025 us/op 1.14
batch validate gossip attestation - vc 640000 - chunk 256 88.934 us/op 82.400 us/op 1.08
bytes32 toHexString 294.00 ns/op 296.00 ns/op 0.99
bytes32 Buffer.toString(hex) 183.00 ns/op 161.00 ns/op 1.14
bytes32 Buffer.toString(hex) from Uint8Array 251.00 ns/op 233.00 ns/op 1.08
bytes32 Buffer.toString(hex) + 0x 191.00 ns/op 164.00 ns/op 1.16
Return object 10000 times 0.21430 ns/op 0.21090 ns/op 1.02
Throw Error 10000 times 3.8378 us/op 3.4511 us/op 1.11
toHex 102.21 ns/op 97.261 ns/op 1.05
Buffer.from 92.515 ns/op 88.599 ns/op 1.04
shared Buffer 64.016 ns/op 60.967 ns/op 1.05
fastMsgIdFn sha256 / 200 bytes 1.6250 us/op 1.4680 us/op 1.11
fastMsgIdFn h32 xxhash / 200 bytes 191.00 ns/op 152.00 ns/op 1.26
fastMsgIdFn h64 xxhash / 200 bytes 213.00 ns/op 202.00 ns/op 1.05
fastMsgIdFn sha256 / 1000 bytes 4.8340 us/op 4.7630 us/op 1.01
fastMsgIdFn h32 xxhash / 1000 bytes 254.00 ns/op 243.00 ns/op 1.05
fastMsgIdFn h64 xxhash / 1000 bytes 262.00 ns/op 250.00 ns/op 1.05
fastMsgIdFn sha256 / 10000 bytes 42.282 us/op 41.697 us/op 1.01
fastMsgIdFn h32 xxhash / 10000 bytes 1.2880 us/op 1.2710 us/op 1.01
fastMsgIdFn h64 xxhash / 10000 bytes 834.00 ns/op 830.00 ns/op 1.00
send data - 1000 256B messages 4.5371 ms/op 4.1716 ms/op 1.09
send data - 1000 512B messages 6.0033 ms/op 5.1064 ms/op 1.18
send data - 1000 1024B messages 6.8422 ms/op 5.5459 ms/op 1.23
send data - 1000 1200B messages 9.8465 ms/op 6.0571 ms/op 1.63
send data - 1000 2048B messages 19.105 ms/op 15.662 ms/op 1.22
send data - 1000 4096B messages 69.293 ms/op 54.685 ms/op 1.27
send data - 1000 16384B messages 427.24 ms/op 262.20 ms/op 1.63
send data - 1000 65536B messages 2.1385 s/op 795.72 ms/op 2.69
enrSubnets - fastDeserialize 64 bits 790.00 ns/op 722.00 ns/op 1.09
enrSubnets - ssz BitVector 64 bits 266.00 ns/op 254.00 ns/op 1.05
enrSubnets - fastDeserialize 4 bits 101.00 ns/op 98.000 ns/op 1.03
enrSubnets - ssz BitVector 4 bits 273.00 ns/op 253.00 ns/op 1.08
prioritizePeers score -10:0 att 32-0.1 sync 2-0 219.07 us/op 200.23 us/op 1.09
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 249.59 us/op 228.56 us/op 1.09
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 395.11 us/op 338.72 us/op 1.17
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 644.95 us/op 593.40 us/op 1.09
prioritizePeers score 0:0 att 64-1 sync 4-1 988.20 us/op 688.85 us/op 1.43
array of 16000 items push then shift 1.3368 us/op 1.2873 us/op 1.04
LinkedList of 16000 items push then shift 8.9320 ns/op 6.8720 ns/op 1.30
array of 16000 items push then pop 80.244 ns/op 65.458 ns/op 1.23
LinkedList of 16000 items push then pop 7.1350 ns/op 5.8900 ns/op 1.21
array of 24000 items push then shift 1.9178 us/op 1.9002 us/op 1.01
LinkedList of 24000 items push then shift 9.2640 ns/op 6.5930 ns/op 1.41
array of 24000 items push then pop 108.16 ns/op 91.480 ns/op 1.18
LinkedList of 24000 items push then pop 6.6020 ns/op 5.8720 ns/op 1.12
intersect bitArray bitLen 8 4.8390 ns/op 4.7410 ns/op 1.02
intersect array and set length 8 30.600 ns/op 29.071 ns/op 1.05
intersect bitArray bitLen 128 24.512 ns/op 23.933 ns/op 1.02
intersect array and set length 128 515.12 ns/op 495.49 ns/op 1.04
bitArray.getTrueBitIndexes() bitLen 128 1.1530 us/op 1.0690 us/op 1.08
bitArray.getTrueBitIndexes() bitLen 248 1.9570 us/op 1.8740 us/op 1.04
bitArray.getTrueBitIndexes() bitLen 512 3.9060 us/op 3.7640 us/op 1.04
Full columns - reconstruct all 6 blobs 132.57 us/op 150.78 us/op 0.88
Full columns - reconstruct half of the blobs out of 6 68.058 us/op 93.882 us/op 0.72
Full columns - reconstruct single blob out of 6 35.091 us/op 51.702 us/op 0.68
Half columns - reconstruct all 6 blobs 395.07 ms/op 380.30 ms/op 1.04
Half columns - reconstruct half of the blobs out of 6 198.46 ms/op 189.97 ms/op 1.04
Half columns - reconstruct single blob out of 6 72.292 ms/op 66.915 ms/op 1.08
Set add up to 64 items then delete first 1.6765 us/op 1.6418 us/op 1.02
OrderedSet add up to 64 items then delete first 2.5650 us/op 2.5336 us/op 1.01
Set add up to 64 items then delete last 1.9061 us/op 1.8865 us/op 1.01
OrderedSet add up to 64 items then delete last 2.8702 us/op 2.7940 us/op 1.03
Set add up to 64 items then delete middle 1.9510 us/op 1.8804 us/op 1.04
OrderedSet add up to 64 items then delete middle 4.4284 us/op 4.3006 us/op 1.03
Set add up to 128 items then delete first 3.8285 us/op 3.7476 us/op 1.02
OrderedSet add up to 128 items then delete first 5.9931 us/op 5.7827 us/op 1.04
Set add up to 128 items then delete last 3.8384 us/op 3.7287 us/op 1.03
OrderedSet add up to 128 items then delete last 5.5366 us/op 5.5197 us/op 1.00
Set add up to 128 items then delete middle 3.7525 us/op 3.5942 us/op 1.04
OrderedSet add up to 128 items then delete middle 12.480 us/op 11.467 us/op 1.09
Set add up to 256 items then delete first 7.9597 us/op 7.6934 us/op 1.03
OrderedSet add up to 256 items then delete first 13.088 us/op 11.795 us/op 1.11
Set add up to 256 items then delete last 7.3731 us/op 7.1251 us/op 1.03
OrderedSet add up to 256 items then delete last 11.577 us/op 10.918 us/op 1.06
Set add up to 256 items then delete middle 7.3492 us/op 7.1051 us/op 1.03
OrderedSet add up to 256 items then delete middle 36.247 us/op 34.079 us/op 1.06
runFastConfirmationRules vc:100000 bc:96 eq:0 4.8427 ms/op 4.4876 ms/op 1.08
runFastConfirmationRules vc:600000 bc:96 eq:0 38.798 ms/op 33.985 ms/op 1.14
runFastConfirmationRules vc:1000000 bc:96 eq:0 65.677 ms/op 57.236 ms/op 1.15
runFastConfirmationRules vc:600000 bc:320 eq:0 37.007 ms/op 34.014 ms/op 1.09
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.3714 s/op 1.1447 s/op 1.20
pass gossip attestations to forkchoice per slot 2.6509 ms/op 2.6024 ms/op 1.02
forkChoice updateHead vc 100000 bc 64 eq 0 442.19 us/op 414.07 us/op 1.07
forkChoice updateHead vc 600000 bc 64 eq 0 2.5728 ms/op 2.4896 ms/op 1.03
forkChoice updateHead vc 1000000 bc 64 eq 0 4.4336 ms/op 4.1295 ms/op 1.07
forkChoice updateHead vc 600000 bc 320 eq 0 2.6022 ms/op 2.4877 ms/op 1.05
forkChoice updateHead vc 600000 bc 1200 eq 0 2.7580 ms/op 2.4913 ms/op 1.11
forkChoice updateHead vc 600000 bc 7200 eq 0 3.1361 ms/op 2.8884 ms/op 1.09
forkChoice updateHead vc 600000 bc 64 eq 1000 2.6635 ms/op 2.4493 ms/op 1.09
forkChoice updateHead vc 600000 bc 64 eq 10000 2.7787 ms/op 2.5434 ms/op 1.09
forkChoice updateHead vc 600000 bc 64 eq 300000 7.1512 ms/op 6.5508 ms/op 1.09
computeDeltas 1400000 validators 0% inactive 13.068 ms/op 12.187 ms/op 1.07
computeDeltas 1400000 validators 10% inactive 12.576 ms/op 11.618 ms/op 1.08
computeDeltas 1400000 validators 20% inactive 12.016 ms/op 10.978 ms/op 1.09
computeDeltas 1400000 validators 50% inactive 9.5974 ms/op 8.8811 ms/op 1.08
computeDeltas 2100000 validators 0% inactive 20.081 ms/op 18.362 ms/op 1.09
computeDeltas 2100000 validators 10% inactive 18.758 ms/op 17.647 ms/op 1.06
computeDeltas 2100000 validators 20% inactive 18.031 ms/op 16.526 ms/op 1.09
computeDeltas 2100000 validators 50% inactive 14.517 ms/op 11.024 ms/op 1.32
altair processAttestation - 250000 vs - 7PWei normalcase 1.8189 ms/op 1.6719 ms/op 1.09
altair processAttestation - 250000 vs - 7PWei worstcase 2.7161 ms/op 2.4184 ms/op 1.12
altair processAttestation - setStatus - 1/6 committees join 107.06 us/op 103.86 us/op 1.03
altair processAttestation - setStatus - 1/3 committees join 214.88 us/op 195.30 us/op 1.10
altair processAttestation - setStatus - 1/2 committees join 295.36 us/op 283.62 us/op 1.04
altair processAttestation - setStatus - 2/3 committees join 382.29 us/op 364.24 us/op 1.05
altair processAttestation - setStatus - 4/5 committees join 521.53 us/op 504.04 us/op 1.03
altair processAttestation - setStatus - 100% committees join 630.06 us/op 603.02 us/op 1.04
altair processBlock - 250000 vs - 7PWei normalcase 3.8537 ms/op 2.9146 ms/op 1.32
altair processBlock - 250000 vs - 7PWei normalcase hashState 17.745 ms/op 11.875 ms/op 1.49
altair processBlock - 250000 vs - 7PWei worstcase 22.734 ms/op 19.820 ms/op 1.15
altair processBlock - 250000 vs - 7PWei worstcase hashState 42.633 ms/op 38.342 ms/op 1.11
phase0 processBlock - 250000 vs - 7PWei normalcase 1.5819 ms/op 1.2888 ms/op 1.23
phase0 processBlock - 250000 vs - 7PWei worstcase 17.239 ms/op 16.086 ms/op 1.07
altair processEth1Data - 250000 vs - 7PWei normalcase 297.75 us/op 297.18 us/op 1.00
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 6.3530 us/op 3.1990 us/op 1.99
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 22.034 us/op 20.926 us/op 1.05
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 7.1190 us/op 6.0180 us/op 1.18
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 4.0190 us/op 3.7990 us/op 1.06
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 100.77 us/op 93.085 us/op 1.08
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4236 ms/op 1.4107 ms/op 1.01
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8597 ms/op 1.8769 ms/op 0.99
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.9025 ms/op 1.8836 ms/op 1.01
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.7339 ms/op 3.7262 ms/op 1.00
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 1.9913 ms/op 2.0850 ms/op 0.96
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.1140 ms/op 4.0405 ms/op 1.02
Tree 40 250000 create 359.90 ms/op 310.29 ms/op 1.16
Tree 40 250000 get(125000) 104.08 ns/op 95.067 ns/op 1.09
Tree 40 250000 set(125000) 1.1008 us/op 1.0246 us/op 1.07
Tree 40 250000 toArray() 9.6347 ms/op 9.0260 ms/op 1.07
Tree 40 250000 iterate all - toArray() + loop 9.8446 ms/op 9.2267 ms/op 1.07
Tree 40 250000 iterate all - get(i) 34.447 ms/op 34.305 ms/op 1.00
Array 250000 create 2.2145 ms/op 2.1658 ms/op 1.02
Array 250000 clone - spread 700.31 us/op 671.50 us/op 1.04
Array 250000 get(125000) 0.29200 ns/op 0.29500 ns/op 0.99
Array 250000 set(125000) 0.29400 ns/op 0.29600 ns/op 0.99
Array 250000 iterate all - loop 56.754 us/op 57.806 us/op 0.98
phase0 afterProcessEpoch - 250000 vs - 7PWei 52.051 ms/op 61.007 ms/op 0.85
Array.fill - length 1000000 2.3720 ms/op 2.2202 ms/op 1.07
Array push - length 1000000 9.3291 ms/op 8.1796 ms/op 1.14
Array.get 0.20383 ns/op 0.20575 ns/op 0.99
Uint8Array.get 0.24322 ns/op 0.25414 ns/op 0.96
phase0 beforeProcessEpoch - 250000 vs - 7PWei 17.224 ms/op 16.292 ms/op 1.06
altair processEpoch - mainnet_e81889 334.77 ms/op 261.18 ms/op 1.28
mainnet_e81889 - altair beforeProcessEpoch 19.783 ms/op 19.969 ms/op 0.99
mainnet_e81889 - altair processJustificationAndFinalization 6.4270 us/op 5.6180 us/op 1.14
mainnet_e81889 - altair processInactivityUpdates 3.3765 ms/op 3.4682 ms/op 0.97
mainnet_e81889 - altair processRewardsAndPenalties 19.392 ms/op 18.019 ms/op 1.08
mainnet_e81889 - altair processRegistryUpdates 542.00 ns/op 565.00 ns/op 0.96
mainnet_e81889 - altair processSlashings 130.00 ns/op 156.00 ns/op 0.83
mainnet_e81889 - altair processEth1DataReset 132.00 ns/op 146.00 ns/op 0.90
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.9192 ms/op 1.1921 ms/op 1.61
mainnet_e81889 - altair processSlashingsReset 681.00 ns/op 746.00 ns/op 0.91
mainnet_e81889 - altair processRandaoMixesReset 1.3710 us/op 1.0950 us/op 1.25
mainnet_e81889 - altair processHistoricalRootsUpdate 131.00 ns/op 149.00 ns/op 0.88
mainnet_e81889 - altair processParticipationFlagUpdates 431.00 ns/op 432.00 ns/op 1.00
mainnet_e81889 - altair processSyncCommitteeUpdates 110.00 ns/op 104.00 ns/op 1.06
mainnet_e81889 - altair afterProcessEpoch 40.337 ms/op 43.315 ms/op 0.93
capella processEpoch - mainnet_e217614 933.68 ms/op 764.76 ms/op 1.22
mainnet_e217614 - capella beforeProcessEpoch 65.758 ms/op 55.580 ms/op 1.18
mainnet_e217614 - capella processJustificationAndFinalization 6.6300 us/op 5.4760 us/op 1.21
mainnet_e217614 - capella processInactivityUpdates 11.774 ms/op 12.299 ms/op 0.96
mainnet_e217614 - capella processRewardsAndPenalties 94.833 ms/op 91.491 ms/op 1.04
mainnet_e217614 - capella processRegistryUpdates 4.5670 us/op 4.4520 us/op 1.03
mainnet_e217614 - capella processSlashings 132.00 ns/op 147.00 ns/op 0.90
mainnet_e217614 - capella processEth1DataReset 132.00 ns/op 145.00 ns/op 0.91
mainnet_e217614 - capella processEffectiveBalanceUpdates 6.6987 ms/op 5.7287 ms/op 1.17
mainnet_e217614 - capella processSlashingsReset 679.00 ns/op 721.00 ns/op 0.94
mainnet_e217614 - capella processRandaoMixesReset 1.4310 us/op 1.2290 us/op 1.16
mainnet_e217614 - capella processHistoricalRootsUpdate 128.00 ns/op 147.00 ns/op 0.87
mainnet_e217614 - capella processParticipationFlagUpdates 441.00 ns/op 434.00 ns/op 1.02
mainnet_e217614 - capella afterProcessEpoch 110.85 ms/op 110.10 ms/op 1.01
phase0 processEpoch - mainnet_e58758 344.36 ms/op 278.06 ms/op 1.24
mainnet_e58758 - phase0 beforeProcessEpoch 64.993 ms/op 54.040 ms/op 1.20
mainnet_e58758 - phase0 processJustificationAndFinalization 7.0380 us/op 5.6140 us/op 1.25
mainnet_e58758 - phase0 processRewardsAndPenalties 16.474 ms/op 15.257 ms/op 1.08
mainnet_e58758 - phase0 processRegistryUpdates 2.2190 us/op 4.4560 us/op 0.50
mainnet_e58758 - phase0 processSlashings 133.00 ns/op 144.00 ns/op 0.92
mainnet_e58758 - phase0 processEth1DataReset 148.00 ns/op 145.00 ns/op 1.02
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 794.48 us/op 826.43 us/op 0.96
mainnet_e58758 - phase0 processSlashingsReset 818.00 ns/op 828.00 ns/op 0.99
mainnet_e58758 - phase0 processRandaoMixesReset 1.3760 us/op 1.0910 us/op 1.26
mainnet_e58758 - phase0 processHistoricalRootsUpdate 138.00 ns/op 146.00 ns/op 0.95
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.1860 us/op 1.0110 us/op 1.17
mainnet_e58758 - phase0 afterProcessEpoch 33.740 ms/op 33.596 ms/op 1.00
phase0 processEffectiveBalanceUpdates - 250000 normalcase 951.08 us/op 1.0119 ms/op 0.94
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.5375 ms/op 1.7887 ms/op 0.86
altair processInactivityUpdates - 250000 normalcase 10.329 ms/op 10.632 ms/op 0.97
altair processInactivityUpdates - 250000 worstcase 10.473 ms/op 11.186 ms/op 0.94
phase0 processRegistryUpdates - 250000 normalcase 2.1300 us/op 2.3270 us/op 0.92
phase0 processRegistryUpdates - 250000 badcase_full_deposits 138.27 us/op 140.89 us/op 0.98
phase0 processRegistryUpdates - 250000 worstcase 0.5 59.417 ms/op 61.334 ms/op 0.97
altair processRewardsAndPenalties - 250000 normalcase 15.775 ms/op 15.776 ms/op 1.00
altair processRewardsAndPenalties - 250000 worstcase 14.721 ms/op 15.059 ms/op 0.98
phase0 getAttestationDeltas - 250000 normalcase 5.3704 ms/op 8.7563 ms/op 0.61
phase0 getAttestationDeltas - 250000 worstcase 5.4819 ms/op 8.6709 ms/op 0.63
phase0 processSlashings - 250000 worstcase 60.341 us/op 63.346 us/op 0.95
altair processSyncCommitteeUpdates - 250000 11.637 ms/op 10.235 ms/op 1.14
BeaconState.hashTreeRoot - No change 185.00 ns/op 174.00 ns/op 1.06
BeaconState.hashTreeRoot - 1 full validator 79.068 us/op 71.694 us/op 1.10
BeaconState.hashTreeRoot - 32 full validator 938.42 us/op 859.62 us/op 1.09
BeaconState.hashTreeRoot - 512 full validator 6.6600 ms/op 6.1558 ms/op 1.08
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 103.92 us/op 83.298 us/op 1.25
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.7264 ms/op 1.2731 ms/op 1.36
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 17.142 ms/op 14.652 ms/op 1.17
BeaconState.hashTreeRoot - 1 balances 96.034 us/op 63.223 us/op 1.52
BeaconState.hashTreeRoot - 32 balances 956.74 us/op 741.93 us/op 1.29
BeaconState.hashTreeRoot - 512 balances 6.1078 ms/op 4.7887 ms/op 1.28
BeaconState.hashTreeRoot - 250000 balances 131.32 ms/op 113.88 ms/op 1.15
aggregationBits - 2048 els - zipIndexesInBitList 20.480 us/op 20.060 us/op 1.02
regular array get 100000 times 23.064 us/op 23.476 us/op 0.98
wrappedArray get 100000 times 23.152 us/op 23.452 us/op 0.99
arrayWithProxy get 100000 times 10.982 ms/op 10.484 ms/op 1.05
ssz.Root.equals 21.691 ns/op 21.800 ns/op 0.99
byteArrayEquals 21.503 ns/op 21.544 ns/op 1.00
Buffer.compare 9.1150 ns/op 8.9100 ns/op 1.02
processSlot - 1 slots 10.321 us/op 8.3360 us/op 1.24
processSlot - 32 slots 1.9645 ms/op 1.5354 ms/op 1.28
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.7581 ms/op 2.8076 ms/op 1.69
getCommitteeAssignments - req 1 vs - 250000 vc 1.6905 ms/op 1.6868 ms/op 1.00
getCommitteeAssignments - req 100 vs - 250000 vc 3.4396 ms/op 3.4555 ms/op 1.00
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6813 ms/op 3.7030 ms/op 0.99
findModifiedValidators - 10000 modified validators 636.08 ms/op 757.44 ms/op 0.84
findModifiedValidators - 1000 modified validators 438.27 ms/op 520.40 ms/op 0.84
findModifiedValidators - 100 modified validators 277.82 ms/op 306.47 ms/op 0.91
findModifiedValidators - 10 modified validators 165.25 ms/op 258.46 ms/op 0.64
findModifiedValidators - 1 modified validators 198.53 ms/op 192.58 ms/op 1.03
findModifiedValidators - no difference 197.02 ms/op 157.70 ms/op 1.25
migrate state 1500000 validators, 3400 modified, 2000 new 3.8834 s/op 3.2322 s/op 1.20
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.7000 ns/op 3.7600 ns/op 0.98
state getBlockRootAtSlot - 250000 vs - 7PWei 433.26 ns/op 283.09 ns/op 1.53
computeProposerIndex 100000 validators 1.4059 ms/op 1.4434 ms/op 0.97
getNextSyncCommitteeIndices 1000 validators 2.9055 ms/op 2.9166 ms/op 1.00
getNextSyncCommitteeIndices 10000 validators 26.119 ms/op 27.001 ms/op 0.97
getNextSyncCommitteeIndices 100000 validators 90.830 ms/op 89.673 ms/op 1.01
computeProposers - vc 250000 559.89 us/op 557.58 us/op 1.00
computeEpochShuffling - vc 250000 40.800 ms/op 40.495 ms/op 1.01
getNextSyncCommittee - vc 250000 9.6581 ms/op 9.5625 ms/op 1.01
nodejs block root to RootHex using toHex 102.57 ns/op 96.027 ns/op 1.07
nodejs block root to RootHex using toRootHex 66.078 ns/op 63.244 ns/op 1.04
nodejs fromHex(blob) 903.22 us/op 790.43 us/op 1.14
nodejs fromHexInto(blob) 620.85 us/op 635.07 us/op 0.98
nodejs block root to RootHex using the deprecated toHexString 505.66 ns/op 510.43 ns/op 0.99
nodejs byteArrayEquals 32 bytes (block root) 25.232 ns/op 26.455 ns/op 0.95
nodejs byteArrayEquals 48 bytes (pubkey) 36.794 ns/op 38.224 ns/op 0.96
nodejs byteArrayEquals 96 bytes (signature) 32.971 ns/op 35.431 ns/op 0.93
nodejs byteArrayEquals 1024 bytes 40.751 ns/op 45.413 ns/op 0.90
nodejs byteArrayEquals 131072 bytes (blob) 1.7308 us/op 1.7879 us/op 0.97
browser block root to RootHex using toHex 146.54 ns/op 146.84 ns/op 1.00
browser block root to RootHex using toRootHex 134.05 ns/op 132.88 ns/op 1.01
browser fromHex(blob) 1.6879 ms/op 1.6701 ms/op 1.01
browser fromHexInto(blob) 616.34 us/op 632.02 us/op 0.98
browser block root to RootHex using the deprecated toHexString 515.55 ns/op 350.01 ns/op 1.47
browser byteArrayEquals 32 bytes (block root) 27.639 ns/op 28.442 ns/op 0.97
browser byteArrayEquals 48 bytes (pubkey) 38.936 ns/op 40.033 ns/op 0.97
browser byteArrayEquals 96 bytes (signature) 72.837 ns/op 74.679 ns/op 0.98
browser byteArrayEquals 1024 bytes 755.93 ns/op 767.09 ns/op 0.99
browser byteArrayEquals 131072 bytes (blob) 92.714 us/op 96.522 us/op 0.96

by benchmarkbot/action

toRootHex(config.getForkTypes(blockN.message.slot).SignedBeaconBlock.hashTreeRoot(blockN))
);
}
export function hashBlocks(blocks: IBlockInput[], payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null): RootHex {

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.

hashing blocks + envlopes are cheap enough thanks to cachePermanentRootStruct option
the downside is we don't include signatures, haven't seen an issue of same block with different signatures for range sync on any networks yet

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.

The cheapness argument holds — blockRootHex is already cached, so this is strictly cheaper than the old SignedBeaconBlock.hashTreeRoot path.

On signatures, I'd frame the risk differently than "same block with different signatures". BLS signing is deterministic, so an honest peer serving the same block message always produces the same signature — you're right that this doesn't arise naturally, and no network would have shown it.

The case that does bite is adversarial: a peer serves the correct block message with a garbage signature. Since blockRootHex is the root of block.message, that attempt now gets the same id as the good one. It fails processing → lands in failedProcessingAttempts → the good attempt later wins with the same hash → attempt.hash !== attemptOk.hash is false at chain.ts:776 → the peer is never scored. The old signature-inclusive hash would have caught it, since a garbage signature changed the id.

That's cheap for a peer to do and now free of consequence at this layer, and it matters slightly more with MAX_BATCH_PROCESSING_ATTEMPTS going 0 → 3 (more free attempts before teardown). I don't think it's blocking — signature-corrupting peers may well get caught elsewhere first — but it might be worth re-wording the tradeoff note to say we no longer score peers that corrupt only the signature, rather than that same-block-different-signature hasn't been observed. Those are different claims, and the second one reads as "low risk" while the first is the one a peer can actually exploit.

@twoeths

twoeths commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Synced with this branch a couple of times on feat3, the last one was with 12h block range

Screenshot 2026-07-17 at 09 53 57

@twoeths
twoeths marked this pull request as ready for review July 17, 2026 02:54
@twoeths
twoeths requested a review from a team as a code owner July 17, 2026 02:54
@twoeths
twoeths requested review from ensi321 and wemeetagain July 17, 2026 02:54
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.49%. Comparing base (242f99e) to head (1a0251f).
⚠️ Report is 3 commits behind head on unstable.

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #9667      +/-   ##
============================================
- Coverage     52.49%   52.49%   -0.01%     
============================================
  Files           848      848              
  Lines         60485    60455      -30     
  Branches       4466     4462       -4     
============================================
- Hits          31754    31737      -17     
+ Misses        28670    28657      -13     
  Partials         61       61              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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: 1a0251f476

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// PARENT_UNKNOWN/PARENT_PAYLOAD_UNKNOWN always fires on the batch's first not-yet-imported block.
const firstBlockSlot = err.signedBlock.message.slot;
// Anchored at the batch start => the missing parent is before this batch => previous-batch fault.
if (firstBlockSlot === batch.startSlot) return ProcessingFaultKind.PreviousBatch;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle first-batch parent faults as unrecoverable here

When this is the first queued batch in a sync chain and its first block fails with PARENT_UNKNOWN/PARENT_PAYLOAD_UNKNOWN, prevBatch is undefined but this still returns PreviousBatch. processBatch() then calls retainForReprocessing() and invalidates no earlier batch, so the same bad downloaded batch is retried without re-downloading or incrementing attempts; for a one-batch target, or until another download happens to retrigger processing, range sync can spin/stall instead of dropping the bad attempt. Treat the no-previous-batch case as this batch's fault/ambiguous rather than previous-batch fault.

Useful? React with 👍 / 👎.

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 — this is real, and I think it's blocking. Traced end to end:

classifyProcessingFault returns PreviousBatch whenever firstBlockSlot === batch.startSlot, without ever checking prevBatch. For the first batch of a sync chain prevBatch is undefined, and then:

  1. batch.retainForReprocessing()ProcessingAwaitingProcessing, blocks retained, no attempt recorded — by design, its docstring says so explicitly.
  2. invalidatePreviousBatches() requires pendingBatch.startEpoch < batch.startEpoch, so it is a no-op — there is nothing earlier to invalidate.
  3. The error branch ends at triggerBatchDownloader() (chain.ts:755), not triggerBatchProcessor().
  4. getNextBatchToProcess returns the first AwaitingProcessing batch — which is this same batch, holding the same bad blocks.

So each subsequent download success re-triggers the processor (chain.ts:646) → same batch → same PARENT_UNKNOWNretainForReprocessing() again. failedProcessingAttempts never grows, so MAX_BATCH_PROCESSING_ATTEMPTS is never reached, the batch never re-downloads, and the chain never tears down.

One refinement to the report: it settles into a hard stall rather than a spin. includeNextBatch counts AwaitingProcessing toward batchesInBuffer, so once BATCH_BUFFER_SIZE(=10)+1 batches are buffered, includeNextBatch returns null → downloads stop → nothing re-triggers the processor → the chain sits in Syncing indefinitely with no progress, no peer report, and no error.

Worth flagging that this is a regression against the exact case the old constant documented: with MAX_BATCH_PROCESSING_ATTEMPTS = 0, processingError() threw MAX_PROCESSING_ATTEMPTS on the first failure → shouldReportPeerOnBatchError → chain dropped and peers reported. That's the #8147 remedy, and the first-batch path no longer reaches it.

Agree with the suggested fix. For the first batch there is no previous batch by definition, so an unknown parent can only mean the peer's segment doesn't chain onto our anchor:

if (firstBlockSlot === batch.startSlot) {
  return prevBatch === undefined ? ProcessingFaultKind.ThisBatch : ProcessingFaultKind.PreviousBatch;
}

I'd pick ThisBatch over AmbiguousAmbiguous would additionally call invalidatePreviousBatches(), which is a no-op here anyway, so ThisBatch states the intent. Worth a unit test that a first batch failing PARENT_UNKNOWN at startSlot re-downloads and eventually throws.

// gloas payloads, see hashBlocks) delivered bad data => penalize its peers
const attemptOk = batch.validationSuccess();
for (const attempt of batch.failedProcessingAttempts) {
for (const attempt of [...batch.failedProcessingAttempts, ...batch.executionErrorAttempts]) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep execution-engine failures out of peer scoring

Including executionErrorAttempts here can downscore peers after transient local EL failures: Batch.processingError() records EXECUTION_ENGINE_ERROR separately via onExecutionEngineError(), and teardown intentionally maps MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS to no peer report, but a later successful retry with a different hash will now report the earlier peer even though UNAVAILABLE/ELERROR was not evidence of bad data. This can penalize honest peers when the execution engine recovers and the batch is retried; only score attempts that failed block validation, or distinguish actual invalid payloads from local EL errors.

Useful? React with 👍 / 👎.

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, though I'd rank it below the P1. The strongest evidence is that the PR is internally inconsistent on this point: shouldReportPeerOnBatchError maps MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS → null ("If peer should not be downscored, returns null") directly alongside MAX_PROCESSING_ATTEMPTS → LowToleranceError. So the teardown path already asserts EL errors are not the peer's fault, while advanceChain now scores exactly those attempts one screen up.

onExecutionEngineError only fires on BlockErrorCode.EXECUTION_ENGINE_ERROR, which is a local fault — the peer's data was never shown invalid, we just couldn't evaluate it.

The attempt.hash !== attemptOk.hash guard narrows this but doesn't close it. onExecutionEngineError wipes state to {blocks: [], payloadEnvelopes: null}AwaitingDownload, so the retry generally comes from a different peer via the balancer — a differing hash is the common case here, not the rare one. Net effect: the EL flaps, and an honest peer that happened to be in flight takes a LowToleranceError ("SyncChainInvalidBatchOther").

The new hashBlocks widens it further: it now folds in payload envelopes and filters on hasPayloadEnvelope(), so two attempts with byte-identical blocks but different envelope availability produce different ids. An EL-error attempt with identical blocks can therefore still mismatch the winner and get scored.

Simplest fix is to drop ...batch.executionErrorAttempts from the loop — that's the same conclusion shouldReportPeerOnBatchError already reached. If the intent was to catch peers serving payloads the EL rejects, that's an invalid-payload signal rather than EXECUTION_ENGINE_ERROR, and is better distinguished inside onExecutionEngineError than at scoring time.

@twoeths

twoeths commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@lodekeeper please review

@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 on request. The fault-localization idea is a clear improvement over tearing the whole chain down on any processing error, and splitting the boundary PARENT_PAYLOAD_UNKNOWN from the new mid-segment NON_LINEAR_PAYLOAD_ROOTS in assertLinearChainSegment is the right shape — classifyProcessingFault leans entirely on that distinction being reliable, so it's good that it's now explicit in the type.

Requesting changes on one confirmed liveness bug. Details in-thread; summarising here plus one cross-cutting note.

Blocking — a first-batch PARENT_UNKNOWN stalls the sync chain permanently (r3600093261). classifyProcessingFault returns PreviousBatch whenever the failing block sits at batch.startSlot, without checking that a previous batch exists. For the first batch of a chain there is none, so retainForReprocessing() keeps the bad blocks without recording an attempt while invalidatePreviousBatches() no-ops. The batch then re-processes the same data on every downstream download, never re-downloads, never increments failedProcessingAttempts, and so never reaches MAX_BATCH_PROCESSING_ATTEMPTS. It ends in a hard stall once BATCH_BUFFER_SIZE+1 batches are buffered, since includeNextBatch counts AwaitingProcessing. This regresses the behaviour MAX_BATCH_PROCESSING_ATTEMPTS = 0 used to guarantee for #8147.

Non-blocking — peer scoring on EL errors (r3600093263). Codex is right, and the tell is internal: shouldReportPeerOnBatchError already maps MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS → null, so scoring executionErrorAttempts in advanceChain contradicts the PR's own stance a few lines down.

Non-blocking — the head-sync safety argument in classifyProcessingFault's doc comment doesn't hold. It ends with:

Head-sync peers may be on different forks; the actions are still safe there (retries can converge onto the target fork), but peer penalization on teardown is gated to finalized sync.

Teardown penalization is not gated by syncType. The catch in sync() calls shouldReportPeerOnBatchError(e.type.code) and reports every peer in the peerset with no fork or syncType check. The only syncType === RangeSyncType.Finalized gate in this file is in the downloadByRange error path (chain.ts:561), which is a different path — and advanceChain's scoring loop isn't gated either. So during head sync, peers legitimately on another fork can still drive a batch to MAX_PROCESSING_ATTEMPTS and get the whole peerset LowToleranceError'd. Either drop that clause or actually add the gate; flagging it because that clause is what the head-sync safety argument rests on, so it shouldn't be load-bearing while untrue.

Doc nitMAX_BATCH_PROCESSING_ATTEMPTS's new comment calls it a "Shared budget (via failedProcessingAttempts)", but it bounds two independent arrays: failedProcessingAttempts (via onProcessingError, reached from both processingError and validationError) and executionErrorAttempts (via onExecutionEngineError). It's two budgets of 3, not one shared budget of 3.

Nice work on the feat3 soak — that plus the new batches.test.ts cases is reassuring for the happy path. The first-batch case above is one a soak wouldn't surface unless a peer actually misbehaves on batch 0, which is why I'd want a unit test pinning it.

@twoeths
twoeths requested a review from lodekeeper July 20, 2026 11:29
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.

3 participants