Skip to content

feat: implement consensus_and_equivocation for blocks and payload envelopes - #9757

Merged
wemeetagain merged 29 commits into
unstablefrom
nflaig/reject-equivocating-payload-envelopes
Aug 7, 2026
Merged

feat: implement consensus_and_equivocation for blocks and payload envelopes#9757
wemeetagain merged 29 commits into
unstablefrom
nflaig/reject-equivocating-payload-envelopes

Conversation

@nflaig

@nflaig nflaig commented Aug 3, 2026

Copy link
Copy Markdown
Member

Follow-up to #9595 that implements broadcast_validation=consensus_and_equivocation for block and execution payload envelope publishing

  • track block roots per slot/proposer once their signatures have been verified through gossip or block import
  • reject equivocating blocks submitted through publishBlockV2 before broadcast
  • reject execution payload envelopes before import or gossip, with a final check after async validation
  • verify and retain the first conflicting repeat proposal without allowing invalid signatures to poison the cache
  • run full signature verification for non-local blocks using consensus or consensus_and_equivocation validation
  • add focused cache, gossip validation, and api regression tests

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: fb93d71 Previous: 65cfb73 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 1.3532 ms/op 1.6303 ms/op 0.83
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 38.939 us/op 41.107 us/op 0.95
BLS verify - blst 724.92 us/op 758.32 us/op 0.96
BLS verifyMultipleSignatures 3 - blst 1.3907 ms/op 1.3689 ms/op 1.02
BLS verifyMultipleSignatures 8 - blst 2.2220 ms/op 2.1904 ms/op 1.01
BLS verifyMultipleSignatures 32 - blst 7.1786 ms/op 6.9492 ms/op 1.03
BLS verifyMultipleSignatures 64 - blst 13.943 ms/op 13.648 ms/op 1.02
BLS verifyMultipleSignatures 128 - blst 26.064 ms/op 25.779 ms/op 1.01
BLS deserializing 10000 signatures 623.88 ms/op 635.03 ms/op 0.98
BLS deserializing 100000 signatures 6.3324 s/op 6.3416 s/op 1.00
BLS verifyMultipleSignatures - same message - 3 - blst 781.05 us/op 777.51 us/op 1.00
BLS verifyMultipleSignatures - same message - 8 - blst 908.45 us/op 893.02 us/op 1.02
BLS verifyMultipleSignatures - same message - 32 - blst 1.4808 ms/op 1.5036 ms/op 0.98
BLS verifyMultipleSignatures - same message - 64 - blst 2.4172 ms/op 2.2863 ms/op 1.06
BLS verifyMultipleSignatures - same message - 128 - blst 4.0767 ms/op 4.0544 ms/op 1.01
BLS aggregatePubkeys 32 - blst 17.840 us/op 17.700 us/op 1.01
BLS aggregatePubkeys 128 - blst 63.306 us/op 63.261 us/op 1.00
getSlashingsAndExits - default max 49.700 us/op 47.059 us/op 1.06
getSlashingsAndExits - 2k 364.48 us/op 341.96 us/op 1.07
proposeBlockBody type=full, size=empty 657.86 us/op 660.01 us/op 1.00
isKnown best case - 1 super set check 162.00 ns/op 174.00 ns/op 0.93
isKnown normal case - 2 super set checks 164.00 ns/op 158.00 ns/op 1.04
isKnown worse case - 16 super set checks 164.00 ns/op 161.00 ns/op 1.02
validate api signedAggregateAndProof - struct 1.5665 ms/op 1.5454 ms/op 1.01
validate gossip signedAggregateAndProof - struct 1.5526 ms/op 1.5269 ms/op 1.02
batch validate gossip attestation - vc 640000 - chunk 32 110.79 us/op 108.10 us/op 1.02
batch validate gossip attestation - vc 640000 - chunk 64 94.682 us/op 94.397 us/op 1.00
batch validate gossip attestation - vc 640000 - chunk 128 88.025 us/op 87.732 us/op 1.00
batch validate gossip attestation - vc 640000 - chunk 256 84.188 us/op 83.138 us/op 1.01
bytes32 toHexString 290.00 ns/op 300.00 ns/op 0.97
bytes32 Buffer.toString(hex) 173.00 ns/op 162.00 ns/op 1.07
bytes32 Buffer.toString(hex) from Uint8Array 242.00 ns/op 231.00 ns/op 1.05
bytes32 Buffer.toString(hex) + 0x 176.00 ns/op 165.00 ns/op 1.07
Return object 10000 times 0.21370 ns/op 0.21220 ns/op 1.01
Throw Error 10000 times 3.3777 us/op 3.6481 us/op 0.93
toHex 102.10 ns/op 99.762 ns/op 1.02
Buffer.from 90.630 ns/op 92.044 ns/op 0.98
shared Buffer 61.209 ns/op 61.866 ns/op 0.99
fastMsgIdFn sha256 / 200 bytes 1.5050 us/op 1.4870 us/op 1.01
fastMsgIdFn h32 xxhash / 200 bytes 163.00 ns/op 147.00 ns/op 1.11
fastMsgIdFn h64 xxhash / 200 bytes 216.00 ns/op 196.00 ns/op 1.10
fastMsgIdFn sha256 / 1000 bytes 4.8700 us/op 4.7870 us/op 1.02
fastMsgIdFn h32 xxhash / 1000 bytes 254.00 ns/op 237.00 ns/op 1.07
fastMsgIdFn h64 xxhash / 1000 bytes 266.00 ns/op 247.00 ns/op 1.08
fastMsgIdFn sha256 / 10000 bytes 42.922 us/op 41.899 us/op 1.02
fastMsgIdFn h32 xxhash / 10000 bytes 1.2890 us/op 1.2630 us/op 1.02
fastMsgIdFn h64 xxhash / 10000 bytes 845.00 ns/op 814.00 ns/op 1.04
send data - 1000 256B messages 4.2485 ms/op 4.3527 ms/op 0.98
send data - 1000 512B messages 5.7349 ms/op 5.7711 ms/op 0.99
send data - 1000 1024B messages 6.1662 ms/op 5.4626 ms/op 1.13
send data - 1000 1200B messages 6.4268 ms/op 6.3265 ms/op 1.02
send data - 1000 2048B messages 10.700 ms/op 8.1056 ms/op 1.32
send data - 1000 4096B messages 40.800 ms/op 50.387 ms/op 0.81
send data - 1000 16384B messages 316.85 ms/op 317.07 ms/op 1.00
send data - 1000 65536B messages 1.7142 s/op 1.6309 s/op 1.05
enrSubnets - fastDeserialize 64 bits 723.00 ns/op 712.00 ns/op 1.02
enrSubnets - ssz BitVector 64 bits 268.00 ns/op 249.00 ns/op 1.08
enrSubnets - fastDeserialize 4 bits 108.00 ns/op 98.000 ns/op 1.10
enrSubnets - ssz BitVector 4 bits 264.00 ns/op 254.00 ns/op 1.04
prioritizePeers score -10:0 att 32-0.1 sync 2-0 197.10 us/op 216.55 us/op 0.91
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 227.75 us/op 231.95 us/op 0.98
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 328.67 us/op 320.39 us/op 1.03
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 577.82 us/op 594.29 us/op 0.97
prioritizePeers score 0:0 att 64-1 sync 4-1 708.49 us/op 659.15 us/op 1.07
array of 16000 items push then shift 1.2985 us/op 1.2047 us/op 1.08
LinkedList of 16000 items push then shift 6.8750 ns/op 6.8150 ns/op 1.01
array of 16000 items push then pop 69.540 ns/op 65.313 ns/op 1.06
LinkedList of 16000 items push then pop 6.2060 ns/op 6.0330 ns/op 1.03
array of 24000 items push then shift 1.9090 us/op 1.9162 us/op 1.00
LinkedList of 24000 items push then shift 6.4520 ns/op 6.3040 ns/op 1.02
array of 24000 items push then pop 94.287 ns/op 93.547 ns/op 1.01
LinkedList of 24000 items push then pop 5.9140 ns/op 5.9790 ns/op 0.99
intersect bitArray bitLen 8 3.7810 ns/op 3.8900 ns/op 0.97
intersect array and set length 8 28.641 ns/op 30.229 ns/op 0.95
intersect bitArray bitLen 128 22.352 ns/op 23.311 ns/op 0.96
intersect array and set length 128 482.48 ns/op 517.99 ns/op 0.93
bitArray.getTrueBitIndexes() bitLen 128 910.00 ns/op 908.00 ns/op 1.00
bitArray.getTrueBitIndexes() bitLen 248 1.6640 us/op 1.6360 us/op 1.02
bitArray.getTrueBitIndexes() bitLen 512 3.5480 us/op 3.4020 us/op 1.04
Full columns - reconstruct all 6 blobs 131.34 us/op 113.97 us/op 1.15
Full columns - reconstruct half of the blobs out of 6 69.947 us/op 80.289 us/op 0.87
Full columns - reconstruct single blob out of 6 30.931 us/op 31.472 us/op 0.98
Half columns - reconstruct all 6 blobs 399.21 ms/op 388.57 ms/op 1.03
Half columns - reconstruct half of the blobs out of 6 198.71 ms/op 195.12 ms/op 1.02
Half columns - reconstruct single blob out of 6 71.431 ms/op 69.706 ms/op 1.02
Set add up to 64 items then delete first 1.5999 us/op 1.6961 us/op 0.94
OrderedSet add up to 64 items then delete first 2.4075 us/op 2.5568 us/op 0.94
Set add up to 64 items then delete last 1.7556 us/op 1.9158 us/op 0.92
OrderedSet add up to 64 items then delete last 2.7296 us/op 2.8247 us/op 0.97
Set add up to 64 items then delete middle 1.7749 us/op 1.9232 us/op 0.92
OrderedSet add up to 64 items then delete middle 4.1381 us/op 4.2700 us/op 0.97
Set add up to 128 items then delete first 3.5651 us/op 3.8218 us/op 0.93
OrderedSet add up to 128 items then delete first 5.6697 us/op 5.8880 us/op 0.96
Set add up to 128 items then delete last 3.4401 us/op 3.6519 us/op 0.94
OrderedSet add up to 128 items then delete last 5.4231 us/op 5.4691 us/op 0.99
Set add up to 128 items then delete middle 3.6477 us/op 3.6763 us/op 0.99
OrderedSet add up to 128 items then delete middle 11.510 us/op 11.305 us/op 1.02
Set add up to 256 items then delete first 7.5986 us/op 7.6924 us/op 0.99
OrderedSet add up to 256 items then delete first 12.005 us/op 11.856 us/op 1.01
Set add up to 256 items then delete last 7.2387 us/op 6.6322 us/op 1.09
OrderedSet add up to 256 items then delete last 11.151 us/op 10.601 us/op 1.05
Set add up to 256 items then delete middle 7.1674 us/op 6.8853 us/op 1.04
OrderedSet add up to 256 items then delete middle 35.272 us/op 32.995 us/op 1.07
runFastConfirmationRules vc:100000 bc:96 eq:0 5.1577 ms/op 4.2375 ms/op 1.22
runFastConfirmationRules vc:600000 bc:96 eq:0 34.997 ms/op 33.126 ms/op 1.06
runFastConfirmationRules vc:1000000 bc:96 eq:0 58.977 ms/op 55.331 ms/op 1.07
runFastConfirmationRules vc:600000 bc:320 eq:0 35.111 ms/op 34.947 ms/op 1.00
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.1285 s/op 1.0749 s/op 1.05
pass gossip attestations to forkchoice per slot 2.6045 ms/op 2.5549 ms/op 1.02
forkChoice updateHead vc 100000 bc 64 eq 0 473.72 us/op 485.08 us/op 0.98
forkChoice updateHead vc 600000 bc 64 eq 0 2.7938 ms/op 2.8290 ms/op 0.99
forkChoice updateHead vc 1000000 bc 64 eq 0 4.6028 ms/op 4.7160 ms/op 0.98
forkChoice updateHead vc 600000 bc 320 eq 0 2.4304 ms/op 2.8593 ms/op 0.85
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5481 ms/op 2.9000 ms/op 0.88
forkChoice updateHead vc 600000 bc 7200 eq 0 3.7402 ms/op 3.2497 ms/op 1.15
forkChoice updateHead vc 600000 bc 64 eq 1000 2.4262 ms/op 2.9085 ms/op 0.83
forkChoice updateHead vc 600000 bc 64 eq 10000 2.5150 ms/op 2.9863 ms/op 0.84
forkChoice updateHead vc 600000 bc 64 eq 300000 6.4167 ms/op 6.8687 ms/op 0.93
computeDeltas 1400000 validators 0% inactive 12.005 ms/op 13.802 ms/op 0.87
computeDeltas 1400000 validators 10% inactive 11.496 ms/op 12.698 ms/op 0.91
computeDeltas 1400000 validators 20% inactive 10.545 ms/op 11.964 ms/op 0.88
computeDeltas 1400000 validators 50% inactive 8.6451 ms/op 9.7682 ms/op 0.89
computeDeltas 2100000 validators 0% inactive 17.935 ms/op 20.030 ms/op 0.90
computeDeltas 2100000 validators 10% inactive 17.071 ms/op 19.067 ms/op 0.90
computeDeltas 2100000 validators 20% inactive 16.155 ms/op 17.936 ms/op 0.90
computeDeltas 2100000 validators 50% inactive 10.717 ms/op 14.555 ms/op 0.74
altair processAttestation - 250000 vs - 7PWei normalcase 2.5349 ms/op 1.7119 ms/op 1.48
altair processAttestation - 250000 vs - 7PWei worstcase 3.4157 ms/op 2.4312 ms/op 1.40
altair processAttestation - setStatus - 1/6 committees join 103.61 us/op 107.99 us/op 0.96
altair processAttestation - setStatus - 1/3 committees join 206.96 us/op 207.69 us/op 1.00
altair processAttestation - setStatus - 1/2 committees join 281.98 us/op 292.97 us/op 0.96
altair processAttestation - setStatus - 2/3 committees join 365.95 us/op 379.96 us/op 0.96
altair processAttestation - setStatus - 4/5 committees join 514.51 us/op 519.66 us/op 0.99
altair processAttestation - setStatus - 100% committees join 636.52 us/op 613.65 us/op 1.04
altair processBlock - 250000 vs - 7PWei normalcase 3.4117 ms/op 2.9042 ms/op 1.17
altair processBlock - 250000 vs - 7PWei normalcase hashState 18.256 ms/op 17.575 ms/op 1.04
altair processBlock - 250000 vs - 7PWei worstcase 21.055 ms/op 19.792 ms/op 1.06
altair processBlock - 250000 vs - 7PWei worstcase hashState 45.588 ms/op 43.673 ms/op 1.04
phase0 processBlock - 250000 vs - 7PWei normalcase 1.5499 ms/op 1.4469 ms/op 1.07
phase0 processBlock - 250000 vs - 7PWei worstcase 18.692 ms/op 16.643 ms/op 1.12
altair processEth1Data - 250000 vs - 7PWei normalcase 314.51 us/op 295.16 us/op 1.07
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 4.4630 us/op 3.3970 us/op 1.31
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 21.820 us/op 20.573 us/op 1.06
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 6.2730 us/op 6.1390 us/op 1.02
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.8100 us/op 3.8660 us/op 0.99
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 92.944 us/op 93.605 us/op 0.99
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4215 ms/op 1.3203 ms/op 1.08
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8677 ms/op 1.7282 ms/op 1.08
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.9362 ms/op 1.8358 ms/op 1.05
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.9051 ms/op 3.4317 ms/op 1.14
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.2241 ms/op 2.0978 ms/op 1.06
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 4.2912 ms/op 4.0758 ms/op 1.05
Tree 40 250000 create 354.85 ms/op 331.16 ms/op 1.07
Tree 40 250000 get(125000) 98.182 ns/op 103.47 ns/op 0.95
Tree 40 250000 set(125000) 1.0540 us/op 1.0730 us/op 0.98
Tree 40 250000 toArray() 12.662 ms/op 11.822 ms/op 1.07
Tree 40 250000 iterate all - toArray() + loop 12.625 ms/op 10.189 ms/op 1.24
Tree 40 250000 iterate all - get(i) 42.400 ms/op 39.906 ms/op 1.06
Array 250000 create 2.2447 ms/op 2.0846 ms/op 1.08
Array 250000 clone - spread 701.09 us/op 648.58 us/op 1.08
Array 250000 get(125000) 0.32300 ns/op 0.30000 ns/op 1.08
Array 250000 set(125000) 0.31400 ns/op 0.30300 ns/op 1.04
Array 250000 iterate all - loop 60.707 us/op 57.715 us/op 1.05
phase0 afterProcessEpoch - 250000 vs - 7PWei 75.187 ms/op 39.683 ms/op 1.89
Array.fill - length 1000000 2.3343 ms/op 2.0853 ms/op 1.12
Array push - length 1000000 9.2108 ms/op 7.4492 ms/op 1.24
Array.get 0.22524 ns/op 0.20942 ns/op 1.08
Uint8Array.get 0.25089 ns/op 0.23887 ns/op 1.05
phase0 beforeProcessEpoch - 250000 vs - 7PWei 17.802 ms/op 18.130 ms/op 0.98
altair processEpoch - mainnet_e81889 431.72 ms/op 305.34 ms/op 1.41
mainnet_e81889 - altair beforeProcessEpoch 21.820 ms/op 35.294 ms/op 0.62
mainnet_e81889 - altair processJustificationAndFinalization 8.2300 us/op 6.2300 us/op 1.32
mainnet_e81889 - altair processInactivityUpdates 4.6465 ms/op 3.6783 ms/op 1.26
mainnet_e81889 - altair processRewardsAndPenalties 18.628 ms/op 18.503 ms/op 1.01
mainnet_e81889 - altair processRegistryUpdates 561.00 ns/op 543.00 ns/op 1.03
mainnet_e81889 - altair processSlashings 138.00 ns/op 131.00 ns/op 1.05
mainnet_e81889 - altair processEth1DataReset 135.00 ns/op 132.00 ns/op 1.02
mainnet_e81889 - altair processEffectiveBalanceUpdates 3.6497 ms/op 5.7760 ms/op 0.63
mainnet_e81889 - altair processSlashingsReset 680.00 ns/op 682.00 ns/op 1.00
mainnet_e81889 - altair processRandaoMixesReset 1.2900 us/op 1.2990 us/op 0.99
mainnet_e81889 - altair processHistoricalRootsUpdate 139.00 ns/op 130.00 ns/op 1.07
mainnet_e81889 - altair processParticipationFlagUpdates 438.00 ns/op 441.00 ns/op 0.99
mainnet_e81889 - altair processSyncCommitteeUpdates 110.00 ns/op 103.00 ns/op 1.07
mainnet_e81889 - altair afterProcessEpoch 41.976 ms/op 40.595 ms/op 1.03
capella processEpoch - mainnet_e217614 982.70 ms/op 1.0442 s/op 0.94
mainnet_e217614 - capella beforeProcessEpoch 77.147 ms/op 63.525 ms/op 1.21
mainnet_e217614 - capella processJustificationAndFinalization 7.1380 us/op 7.5680 us/op 0.94
mainnet_e217614 - capella processInactivityUpdates 11.560 ms/op 11.111 ms/op 1.04
mainnet_e217614 - capella processRewardsAndPenalties 99.148 ms/op 93.640 ms/op 1.06
mainnet_e217614 - capella processRegistryUpdates 4.3230 us/op 4.5510 us/op 0.95
mainnet_e217614 - capella processSlashings 135.00 ns/op 150.00 ns/op 0.90
mainnet_e217614 - capella processEth1DataReset 133.00 ns/op 143.00 ns/op 0.93
mainnet_e217614 - capella processEffectiveBalanceUpdates 14.633 ms/op 16.977 ms/op 0.86
mainnet_e217614 - capella processSlashingsReset 665.00 ns/op 697.00 ns/op 0.95
mainnet_e217614 - capella processRandaoMixesReset 1.4280 us/op 1.5060 us/op 0.95
mainnet_e217614 - capella processHistoricalRootsUpdate 137.00 ns/op 143.00 ns/op 0.96
mainnet_e217614 - capella processParticipationFlagUpdates 440.00 ns/op 472.00 ns/op 0.93
mainnet_e217614 - capella afterProcessEpoch 105.14 ms/op 109.87 ms/op 0.96
phase0 processEpoch - mainnet_e58758 345.13 ms/op 360.94 ms/op 0.96
mainnet_e58758 - phase0 beforeProcessEpoch 83.007 ms/op 70.505 ms/op 1.18
mainnet_e58758 - phase0 processJustificationAndFinalization 7.7800 us/op 7.0010 us/op 1.11
mainnet_e58758 - phase0 processRewardsAndPenalties 17.047 ms/op 16.259 ms/op 1.05
mainnet_e58758 - phase0 processRegistryUpdates 2.3000 us/op 2.2840 us/op 1.01
mainnet_e58758 - phase0 processSlashings 138.00 ns/op 144.00 ns/op 0.96
mainnet_e58758 - phase0 processEth1DataReset 135.00 ns/op 137.00 ns/op 0.99
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 1.0722 ms/op 807.89 us/op 1.33
mainnet_e58758 - phase0 processSlashingsReset 913.00 ns/op 1.1710 us/op 0.78
mainnet_e58758 - phase0 processRandaoMixesReset 1.5730 us/op 1.3660 us/op 1.15
mainnet_e58758 - phase0 processHistoricalRootsUpdate 137.00 ns/op 143.00 ns/op 0.96
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.2860 us/op 1.2530 us/op 1.03
mainnet_e58758 - phase0 afterProcessEpoch 34.552 ms/op 33.091 ms/op 1.04
phase0 processEffectiveBalanceUpdates - 250000 normalcase 982.26 us/op 1.0267 ms/op 0.96
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.5777 ms/op 1.8990 ms/op 0.83
altair processInactivityUpdates - 250000 normalcase 10.680 ms/op 12.016 ms/op 0.89
altair processInactivityUpdates - 250000 worstcase 10.605 ms/op 10.767 ms/op 0.98
phase0 processRegistryUpdates - 250000 normalcase 3.3110 us/op 2.4260 us/op 1.36
phase0 processRegistryUpdates - 250000 badcase_full_deposits 142.23 us/op 147.42 us/op 0.96
phase0 processRegistryUpdates - 250000 worstcase 0.5 63.785 ms/op 65.062 ms/op 0.98
altair processRewardsAndPenalties - 250000 normalcase 15.587 ms/op 15.520 ms/op 1.00
altair processRewardsAndPenalties - 250000 worstcase 15.104 ms/op 16.077 ms/op 0.94
phase0 getAttestationDeltas - 250000 normalcase 5.4216 ms/op 5.3587 ms/op 1.01
phase0 getAttestationDeltas - 250000 worstcase 5.4735 ms/op 5.3969 ms/op 1.01
phase0 processSlashings - 250000 worstcase 62.056 us/op 59.978 us/op 1.03
altair processSyncCommitteeUpdates - 250000 10.189 ms/op 10.285 ms/op 0.99
BeaconState.hashTreeRoot - No change 173.00 ns/op 179.00 ns/op 0.97
BeaconState.hashTreeRoot - 1 full validator 83.597 us/op 70.427 us/op 1.19
BeaconState.hashTreeRoot - 32 full validator 965.74 us/op 952.95 us/op 1.01
BeaconState.hashTreeRoot - 512 full validator 6.5705 ms/op 6.8544 ms/op 0.96
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 98.338 us/op 105.07 us/op 0.94
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 2.3274 ms/op 1.5520 ms/op 1.50
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 17.195 ms/op 14.617 ms/op 1.18
BeaconState.hashTreeRoot - 1 balances 108.78 us/op 84.293 us/op 1.29
BeaconState.hashTreeRoot - 32 balances 961.83 us/op 929.70 us/op 1.03
BeaconState.hashTreeRoot - 512 balances 6.1034 ms/op 5.2085 ms/op 1.17
BeaconState.hashTreeRoot - 250000 balances 161.20 ms/op 133.65 ms/op 1.21
aggregationBits - 2048 els - zipIndexesInBitList 20.440 us/op 19.890 us/op 1.03
regular array get 100000 times 23.829 us/op 23.429 us/op 1.02
wrappedArray get 100000 times 23.838 us/op 23.468 us/op 1.02
arrayWithProxy get 100000 times 18.705 ms/op 10.220 ms/op 1.83
ssz.Root.equals 75.622 ns/op 21.647 ns/op 3.49
byteArrayEquals 21.912 ns/op 21.364 ns/op 1.03
Buffer.compare 9.0100 ns/op 9.0190 ns/op 1.00
processSlot - 1 slots 10.291 us/op 8.5130 us/op 1.21
processSlot - 32 slots 2.7130 ms/op 2.3101 ms/op 1.17
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 3.2228 ms/op 5.8329 ms/op 0.55
getCommitteeAssignments - req 1 vs - 250000 vc 1.6926 ms/op 1.6856 ms/op 1.00
getCommitteeAssignments - req 100 vs - 250000 vc 3.4628 ms/op 3.4580 ms/op 1.00
getCommitteeAssignments - req 1000 vs - 250000 vc 3.6947 ms/op 3.6891 ms/op 1.00
findModifiedValidators - 10000 modified validators 732.07 ms/op 627.91 ms/op 1.17
findModifiedValidators - 1000 modified validators 434.78 ms/op 500.66 ms/op 0.87
findModifiedValidators - 100 modified validators 291.40 ms/op 290.59 ms/op 1.00
findModifiedValidators - 10 modified validators 261.64 ms/op 201.17 ms/op 1.30
findModifiedValidators - 1 modified validators 215.63 ms/op 168.26 ms/op 1.28
findModifiedValidators - no difference 165.60 ms/op 180.47 ms/op 0.92
migrate state 1500000 validators, 3400 modified, 2000 new 4.0756 s/op 3.7807 s/op 1.08
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.8500 ns/op 3.7500 ns/op 1.03
state getBlockRootAtSlot - 250000 vs - 7PWei 427.47 ns/op 436.58 ns/op 0.98
computeProposerIndex 100000 validators 1.3667 ms/op 1.3714 ms/op 1.00
getNextSyncCommitteeIndices 1000 validators 2.8576 ms/op 2.9380 ms/op 0.97
getNextSyncCommitteeIndices 10000 validators 25.443 ms/op 26.005 ms/op 0.98
getNextSyncCommitteeIndices 100000 validators 88.559 ms/op 91.610 ms/op 0.97
computeProposers - vc 250000 557.87 us/op 561.42 us/op 0.99
computeEpochShuffling - vc 250000 39.393 ms/op 41.064 ms/op 0.96
getNextSyncCommittee - vc 250000 9.6022 ms/op 9.6636 ms/op 0.99
nodejs block root to RootHex using toHex 101.08 ns/op 105.13 ns/op 0.96
nodejs block root to RootHex using toRootHex 74.084 ns/op 64.855 ns/op 1.14
nodejs fromHex(blob) 902.38 us/op 913.70 us/op 0.99
nodejs fromHexInto(blob) 668.78 us/op 611.69 us/op 1.09
nodejs block root to RootHex using the deprecated toHexString 503.80 ns/op 436.51 ns/op 1.15
nodejs byteArrayEquals 32 bytes (block root) 26.218 ns/op 24.837 ns/op 1.06
nodejs byteArrayEquals 48 bytes (pubkey) 37.736 ns/op 36.112 ns/op 1.04
nodejs byteArrayEquals 96 bytes (signature) 35.835 ns/op 32.865 ns/op 1.09
nodejs byteArrayEquals 1024 bytes 43.733 ns/op 40.557 ns/op 1.08
nodejs byteArrayEquals 131072 bytes (blob) 1.7728 us/op 1.7380 us/op 1.02
browser block root to RootHex using toHex 146.60 ns/op 140.39 ns/op 1.04
browser block root to RootHex using toRootHex 131.80 ns/op 127.21 ns/op 1.04
browser fromHex(blob) 1.6625 ms/op 1.7763 ms/op 0.94
browser fromHexInto(blob) 613.32 us/op 678.93 us/op 0.90
browser block root to RootHex using the deprecated toHexString 326.38 ns/op 345.52 ns/op 0.94
browser byteArrayEquals 32 bytes (block root) 27.036 ns/op 28.226 ns/op 0.96
browser byteArrayEquals 48 bytes (pubkey) 38.158 ns/op 39.730 ns/op 0.96
browser byteArrayEquals 96 bytes (signature) 71.355 ns/op 75.260 ns/op 0.95
browser byteArrayEquals 1024 bytes 724.69 ns/op 768.63 ns/op 0.94
browser byteArrayEquals 131072 bytes (blob) 94.251 us/op 96.883 us/op 0.97

by benchmarkbot/action

@nflaig
nflaig marked this pull request as ready for review August 3, 2026 13:45
@nflaig
nflaig requested a review from a team as a code owner August 3, 2026 13:45
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.57%. Comparing base (aa562e6) to head (317e04b).
⚠️ Report is 2 commits behind head on unstable.

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #9757      +/-   ##
============================================
- Coverage     52.57%   52.57%   -0.01%     
============================================
  Files           848      848              
  Lines         60132    60124       -8     
  Branches       4429     4426       -3     
============================================
- Hits          31614    31608       -6     
+ Misses        28459    28457       -2     
  Partials         59       59              
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nflaig nflaig changed the title fix: reject payload envelopes for proposer equivocations feat: reject payload envelopes for proposer equivocations Aug 3, 2026
@nflaig
nflaig marked this pull request as draft August 3, 2026 13:50
@nflaig nflaig changed the title feat: reject payload envelopes for proposer equivocations feat: implement consensus_and_equivocation for payload envelopes Aug 3, 2026
@nflaig
nflaig marked this pull request as ready for review August 3, 2026 16:15

@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: 317e04bab2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/beacon-node/src/chain/blocks/verifyBlock.ts Outdated
@nflaig
nflaig marked this pull request as draft August 3, 2026 16:29
@nflaig nflaig changed the title feat: implement consensus_and_equivocation for payload envelopes feat: implement consensus_and_equivocation for blocks and payload envelopes Aug 3, 2026
Comment thread packages/beacon-node/src/chain/seenCache/seenBlockProposers.ts
await verifyBlockProposerSignature(chain, signedBlock, blockRoot);
chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot);
}
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex});

@nflaig nflaig Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This returns immediately after verifying the proposer signature, so a conflicting block never reaches normal processing or fork choice. #9233's shouldApplyProposerBoost() scans the ProtoArray for the timely same-proposer block, so it cannot observe equivocations received through the normal gossip path. Can we separate the gossip result from local handling here: still return IGNORE for propagation, but continue full validation locally and, if the block passes consensus, DA, and execution validation, import it into fork choice using its original receive time? A signature-valid but otherwise-invalid conflict should remain only in SeenBlockProposers.

(written by codex)

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.

we can handle that either in #9233 if this PR is merged first, or completely separate as a follow-up

Comment thread packages/beacon-node/src/api/impl/beacon/blocks/index.ts
@matthewkeil matthewkeil added this to the Glamsterdam milestone Aug 5, 2026
@matthewkeil matthewkeil moved this to In Progress in Lodestar Team Coordination Aug 5, 2026
@nflaig
nflaig marked this pull request as ready for review August 5, 2026 18:05

@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: 308d3afa78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await sleep(msToBlockSlot);
}

if (broadcastValidation === routes.beacon.BroadcastValidation.consensusAndEquivocation) {

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 Reject equivocating blinded Fulu blocks

When a validator calls /eth/v2/beacon/blinded_blocks on Fulu with broadcast_validation=consensus_and_equivocation, publishBlindedBlockV2 takes the isForkPostFulu(fork) branch and calls submitBlindedBlockToBuilder directly, so it never reaches this new publishBlockV2 equivocation gate. That can still submit a blinded block to the builder even when seenBlockProposers already has a conflicting root for the same slot and proposer; add the same proposer-signature and getConflictingBlockRoots check before the direct submit path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is pointless for blinded publishing, we submit the block to the builder anyways and broadcast_validation=consensus_and_equivocation is not something proposers use, it's meant to be used by builders to protect them from unbundling attacks

return {skippedSlots};
}

export async function verifyBlockProposerSignature(

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.

verifyBlockProposerSignature has a naming collision with

export async function verifyBlockProposerSignature(

No need to change it I guess but it is good to know.

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.

oh that other one is in backfill that code isn't actively used right now, but the function here is specifically designed for gossip validation

the other function also takes blocks (array of blocks) so the name should be verifyBlockProposerSignatures (plural)

If I would change anything, I would probably delete that other function, or rename it. I do think keeping as is for now is fine.

good catch though

@wemeetagain
wemeetagain merged commit 44e482c into unstable Aug 7, 2026
19 of 21 checks passed
@wemeetagain
wemeetagain deleted the nflaig/reject-equivocating-payload-envelopes branch August 7, 2026 13:09
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Lodestar Team Coordination Aug 7, 2026
matthewkeil pushed a commit that referenced this pull request Aug 7, 2026
Follow-up to #9757 that produces proposer slashings from observed
proposer equivocations

- store the signed block header alongside each observed block root, the
root is derived from the header so no extra hashing
- build a proposer slashing from the two conflicting headers when an
equivocation is observed via gossip or block import
- fully validate the slashing, add it to the local op pool for block
inclusion and publish it on gossip
- do not produce slashings from blocks only observed via the beacon api
so an unpublished block cannot leak equivocation evidence
- add focused seen cache and gossip validation tests

Verified on glamsterdam-devnet-7, a slashing was produced and broadcast
137ms after an adversarial equivocation was published and the validator
got slashed
if (opts.skipVerifyBlockSignatures !== true) {
for (const block of blocks) {
const blockRoot = toRootHex(
this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should be able to get block root from BlockInput instead

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.

import {MapDef} from "@lodestar/utils";

/** Two distinct block roots signed by the same proposer for the same slot are sufficient to establish an equivocation */
const MAX_BLOCK_ROOTS_PER_PROPOSAL = 2;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this name is confusing to me, it feels like allowed block roots per proposal
I think the constant should be 1, rename it to something like MAX_ALLOWED_BLOCK_ROOTS_PER_PROPOSAL = 1, and change all logics accordingly
or name it different way: MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL = 2 without changing the below logic

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I like MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL, agree the name isn't entirely clear, the jsdoc helps though

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.

twoeths pushed a commit that referenced this pull request Aug 11, 2026
Follow-up to #9757 and #9787 based on @twoeths' comments

- remove `skipVerifyBlockSignatures` and always verify block signatures
- reuse the block input root when recording observed proposals
- defer proposer slashing production to the next event loop
- log both conflicting block header roots
- clarify the equivocation root-count constant name
@wemeetagain

Copy link
Copy Markdown
Member

🎉 This PR is included in v1.46.0 🎉

nflaig pushed a commit that referenced this pull request Aug 13, 2026
**Motivation**

- in `lodestar-geth-1` of `glamsterdam-devnet-7`, we saw 2 blocks of the
same slot. The 2nd block got ignored, we ended up having to use
UnknownBlockInput to download it again. And the 2nd block ended up being
the canonical block
- a continuation of #9757

**Description**

- process `REPEAT_PROPOSAL` block, but don't publish it to the network
(to conform to the spec)

part of #9799

**AI Assistance Disclosure**

- created with the help of Claude

---------

Co-authored-by: twoeths <twoeths@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants