fix: download proposer duties for the next epoch post-fulu#9380
Conversation
Implements beacon-APIs PR ethereum/beacon-APIs#593 for the gloas fork. Adds the operational ProposerPreferencesPool keyed (slot, dependent_root) and pruned per slot tick, plumbs it into BeaconChain, and exposes it via two new beacon API endpoints: GET /eth/v1/beacon/pool/proposer_preferences (slot-filtered) POST /eth/v1/beacon/pool/proposer_preferences (JSON + SSZ bodies) The POST handler validates each item via the existing gossip validator, adds to the pool, publishes through gossip (network.publishProposerPreferences), emits the new proposer_preferences SSE event, and reports per-item failures with persistInvalidSszValue on REJECT-class errors. The gossip handler mirrors the same persist-and-emit on a successfully validated incoming message. Also resolves the two // TODO GLOAS markers in validateExecutionPayloadBid: - IGNORE if no SignedProposerPreferences exists at (bid.slot, dependent_root), where dependent_root is derived from bid.parentBlockRoot via forkChoice.getAncestor(parentBlockRoot, computeStartSlotAtEpoch(bidEpoch) - 1). - REJECT if bid.fee_recipient != preferences.fee_recipient. - REJECT if bid.gas_limit != preferences.gas_limit. The parentBlockRoot in-fork-choice IGNORE was hoisted to the top of the validator so getAncestor can be called safely; its late copy is removed as unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ProposerPreferencesService that runs every slot (gloas+) and submits
each local validator's SignedProposerPreferences within SLOTS_PER_EPOCH/4
slots of their proposal slot. Re-submits when the proposer dependent root
for an epoch shifts (reorg / dependent-root change), detected by comparing
the dependentRoot reported by BlockDutiesService against the one we last
submitted under. Submitted-slot tracking is updated only after the batch
POST succeeds so transient API failures retry naturally on the next tick.
Lifts BlockDutiesService ownership from BlockProposingService to validator.ts
so the new service can share the existing per-slot proposer-duty poll.
BlockDutiesService gains a setNotifyBlockProductionFn setter for late-binding
the block-production callback, and a public getProposersAtEpoch(epoch) getter
exposing the per-epoch {dependentRoot, data} cache.
Adds ValidatorStore.signProposerPreferences (mirrors signPayloadAttestation)
and SignableMessageType.PROPOSER_PREFERENCES (enum, type union,
requiresForkInfo, serialization switch) for remote-signer support.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for post-Fulu proposer duties and execution payload bid validation by implementing deterministic lookahead logic and updating the validator's polling mechanisms. Changes include the addition of getProposerDutiesV2, updates to the getShufflingDependentRoot utility, and adjustments to the SignedProposerPreferencesListType capacity. Feedback indicates that the new epoch-boundary polling logic for post-Fulu forks might be redundant, potentially causing duplicate API calls, and suggests simplifying the implementation to maintain coverage without overlap.
|
the log is good for finalizedSync.test.ts 8 for this epoch download, 8 for the previous epoch download (1-epoch look ahead) plus 1 at epoch boundary no error found in the log |
|
| Benchmark suite | Current: 2e58976 | Previous: c98da75 | Ratio |
|---|---|---|---|
| enrSubnets - fastDeserialize 4 bits | 320.00 ns/op | 95.000 ns/op | 3.37 |
| Full columns - reconstruct half of the blobs out of 20 | 699.30 us/op | 181.66 us/op | 3.85 |
Full benchmark results
| Benchmark suite | Current: 2e58976 | Previous: c98da75 | Ratio |
|---|---|---|---|
| getPubkeys - index2pubkey - req 1000 vs - 250000 vc | 1.2259 ms/op | 858.07 us/op | 1.43 |
| getPubkeys - validatorsArr - req 1000 vs - 250000 vc | 39.821 us/op | 39.227 us/op | 1.02 |
| BLS verify - blst | 756.43 us/op | 747.44 us/op | 1.01 |
| BLS verifyMultipleSignatures 3 - blst | 1.3881 ms/op | 1.3366 ms/op | 1.04 |
| BLS verifyMultipleSignatures 8 - blst | 2.1920 ms/op | 2.1675 ms/op | 1.01 |
| BLS verifyMultipleSignatures 32 - blst | 6.8917 ms/op | 6.9137 ms/op | 1.00 |
| BLS verifyMultipleSignatures 64 - blst | 13.373 ms/op | 13.676 ms/op | 0.98 |
| BLS verifyMultipleSignatures 128 - blst | 26.019 ms/op | 26.085 ms/op | 1.00 |
| BLS deserializing 10000 signatures | 632.11 ms/op | 640.28 ms/op | 0.99 |
| BLS deserializing 100000 signatures | 6.4112 s/op | 6.3943 s/op | 1.00 |
| BLS verifyMultipleSignatures - same message - 3 - blst | 767.60 us/op | 762.16 us/op | 1.01 |
| BLS verifyMultipleSignatures - same message - 8 - blst | 904.00 us/op | 945.31 us/op | 0.96 |
| BLS verifyMultipleSignatures - same message - 32 - blst | 1.4440 ms/op | 1.5138 ms/op | 0.95 |
| BLS verifyMultipleSignatures - same message - 64 - blst | 2.2408 ms/op | 2.4516 ms/op | 0.91 |
| BLS verifyMultipleSignatures - same message - 128 - blst | 3.9959 ms/op | 4.0438 ms/op | 0.99 |
| BLS aggregatePubkeys 32 - blst | 17.437 us/op | 17.869 us/op | 0.98 |
| BLS aggregatePubkeys 128 - blst | 61.270 us/op | 63.711 us/op | 0.96 |
| getSlashingsAndExits - default max | 52.527 us/op | 44.888 us/op | 1.17 |
| getSlashingsAndExits - 2k | 418.44 us/op | 353.30 us/op | 1.18 |
| proposeBlockBody type=full, size=empty | 750.88 us/op | 775.79 us/op | 0.97 |
| isKnown best case - 1 super set check | 393.00 ns/op | 152.00 ns/op | 2.59 |
| isKnown normal case - 2 super set checks | 386.00 ns/op | 152.00 ns/op | 2.54 |
| isKnown worse case - 16 super set checks | 397.00 ns/op | 152.00 ns/op | 2.61 |
| validate api signedAggregateAndProof - struct | 1.5671 ms/op | 1.5167 ms/op | 1.03 |
| validate gossip signedAggregateAndProof - struct | 1.5681 ms/op | 1.5196 ms/op | 1.03 |
| batch validate gossip attestation - vc 640000 - chunk 32 | 105.58 us/op | 109.35 us/op | 0.97 |
| batch validate gossip attestation - vc 640000 - chunk 64 | 93.457 us/op | 93.878 us/op | 1.00 |
| batch validate gossip attestation - vc 640000 - chunk 128 | 91.277 us/op | 82.425 us/op | 1.11 |
| batch validate gossip attestation - vc 640000 - chunk 256 | 88.711 us/op | 82.828 us/op | 1.07 |
| bytes32 toHexString | 508.00 ns/op | 264.00 ns/op | 1.92 |
| bytes32 Buffer.toString(hex) | 405.00 ns/op | 160.00 ns/op | 2.53 |
| bytes32 Buffer.toString(hex) from Uint8Array | 487.00 ns/op | 222.00 ns/op | 2.19 |
| bytes32 Buffer.toString(hex) + 0x | 406.00 ns/op | 161.00 ns/op | 2.52 |
| Return object 10000 times | 0.23680 ns/op | 0.20140 ns/op | 1.18 |
| Throw Error 10000 times | 3.4560 us/op | 3.1299 us/op | 1.10 |
| toHex | 96.299 ns/op | 87.430 ns/op | 1.10 |
| Buffer.from | 88.099 ns/op | 78.997 ns/op | 1.12 |
| shared Buffer | 60.521 ns/op | 52.597 ns/op | 1.15 |
| fastMsgIdFn sha256 / 200 bytes | 1.7380 us/op | 1.3830 us/op | 1.26 |
| fastMsgIdFn h32 xxhash / 200 bytes | 363.00 ns/op | 142.00 ns/op | 2.56 |
| fastMsgIdFn h64 xxhash / 200 bytes | 420.00 ns/op | 196.00 ns/op | 2.14 |
| fastMsgIdFn sha256 / 1000 bytes | 5.0500 us/op | 4.4770 us/op | 1.13 |
| fastMsgIdFn h32 xxhash / 1000 bytes | 454.00 ns/op | 231.00 ns/op | 1.97 |
| fastMsgIdFn h64 xxhash / 1000 bytes | 478.00 ns/op | 239.00 ns/op | 2.00 |
| fastMsgIdFn sha256 / 10000 bytes | 42.621 us/op | 39.491 us/op | 1.08 |
| fastMsgIdFn h32 xxhash / 10000 bytes | 1.5030 us/op | 1.2100 us/op | 1.24 |
| fastMsgIdFn h64 xxhash / 10000 bytes | 1.0570 us/op | 781.00 ns/op | 1.35 |
| send data - 1000 256B messages | 4.3276 ms/op | 4.1251 ms/op | 1.05 |
| send data - 1000 512B messages | 4.4740 ms/op | 3.9533 ms/op | 1.13 |
| send data - 1000 1024B messages | 4.8635 ms/op | 5.2058 ms/op | 0.93 |
| send data - 1000 1200B messages | 4.7615 ms/op | 4.6482 ms/op | 1.02 |
| send data - 1000 2048B messages | 6.1433 ms/op | 4.9111 ms/op | 1.25 |
| send data - 1000 4096B messages | 6.2062 ms/op | 5.3853 ms/op | 1.15 |
| send data - 1000 16384B messages | 22.700 ms/op | 12.975 ms/op | 1.75 |
| send data - 1000 65536B messages | 264.43 ms/op | 186.62 ms/op | 1.42 |
| enrSubnets - fastDeserialize 64 bits | 1.0300 us/op | 681.00 ns/op | 1.51 |
| enrSubnets - ssz BitVector 64 bits | 500.00 ns/op | 256.00 ns/op | 1.95 |
| enrSubnets - fastDeserialize 4 bits | 320.00 ns/op | 95.000 ns/op | 3.37 |
| enrSubnets - ssz BitVector 4 bits | 504.00 ns/op | 264.00 ns/op | 1.91 |
| prioritizePeers score -10:0 att 32-0.1 sync 2-0 | 218.20 us/op | 196.00 us/op | 1.11 |
| prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 | 259.11 us/op | 259.06 us/op | 1.00 |
| prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 | 357.21 us/op | 371.38 us/op | 0.96 |
| prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 | 608.32 us/op | 651.44 us/op | 0.93 |
| prioritizePeers score 0:0 att 64-1 sync 4-1 | 749.19 us/op | 703.92 us/op | 1.06 |
| array of 16000 items push then shift | 1.2375 us/op | 1.1980 us/op | 1.03 |
| LinkedList of 16000 items push then shift | 8.0340 ns/op | 7.0720 ns/op | 1.14 |
| array of 16000 items push then pop | 71.104 ns/op | 66.817 ns/op | 1.06 |
| LinkedList of 16000 items push then pop | 6.3240 ns/op | 6.0110 ns/op | 1.05 |
| array of 24000 items push then shift | 2.0332 us/op | 1.9486 us/op | 1.04 |
| LinkedList of 24000 items push then shift | 7.6240 ns/op | 6.4540 ns/op | 1.18 |
| array of 24000 items push then pop | 100.97 ns/op | 94.851 ns/op | 1.06 |
| LinkedList of 24000 items push then pop | 6.3180 ns/op | 6.0580 ns/op | 1.04 |
| intersect bitArray bitLen 8 | 4.8090 ns/op | 4.7710 ns/op | 1.01 |
| intersect array and set length 8 | 28.723 ns/op | 29.578 ns/op | 0.97 |
| intersect bitArray bitLen 128 | 23.483 ns/op | 25.149 ns/op | 0.93 |
| intersect array and set length 128 | 497.88 ns/op | 505.54 ns/op | 0.98 |
| bitArray.getTrueBitIndexes() bitLen 128 | 1.3800 us/op | 1.0220 us/op | 1.35 |
| bitArray.getTrueBitIndexes() bitLen 248 | 2.0770 us/op | 1.6960 us/op | 1.22 |
| bitArray.getTrueBitIndexes() bitLen 512 | 4.0980 us/op | 3.5210 us/op | 1.16 |
| Full columns - reconstruct all 6 blobs | 134.15 us/op | 118.54 us/op | 1.13 |
| Full columns - reconstruct half of the blobs out of 6 | 70.151 us/op | 95.687 us/op | 0.73 |
| Full columns - reconstruct single blob out of 6 | 40.243 us/op | 34.285 us/op | 1.17 |
| Half columns - reconstruct all 6 blobs | 400.32 ms/op | 385.70 ms/op | 1.04 |
| Half columns - reconstruct half of the blobs out of 6 | 199.31 ms/op | 194.92 ms/op | 1.02 |
| Half columns - reconstruct single blob out of 6 | 71.794 ms/op | 69.918 ms/op | 1.03 |
| Full columns - reconstruct all 10 blobs | 193.41 us/op | 187.48 us/op | 1.03 |
| Full columns - reconstruct half of the blobs out of 10 | 103.51 us/op | 28.358 ms/op | 0.00 |
| Full columns - reconstruct single blob out of 10 | 31.834 us/op | 26.853 us/op | 1.19 |
| Half columns - reconstruct all 10 blobs | 660.79 ms/op | 636.73 ms/op | 1.04 |
| Half columns - reconstruct half of the blobs out of 10 | 333.86 ms/op | 322.72 ms/op | 1.03 |
| Half columns - reconstruct single blob out of 10 | 72.237 ms/op | 69.804 ms/op | 1.03 |
| Full columns - reconstruct all 20 blobs | 1.3777 ms/op | 570.81 us/op | 2.41 |
| Full columns - reconstruct half of the blobs out of 20 | 699.30 us/op | 181.66 us/op | 3.85 |
| Full columns - reconstruct single blob out of 20 | 32.463 us/op | 98.816 us/op | 0.33 |
| Half columns - reconstruct all 20 blobs | 1.3361 s/op | 1.2640 s/op | 1.06 |
| Half columns - reconstruct half of the blobs out of 20 | 663.85 ms/op | 642.70 ms/op | 1.03 |
| Half columns - reconstruct single blob out of 20 | 73.111 ms/op | 71.514 ms/op | 1.02 |
| Set add up to 64 items then delete first | 2.7256 us/op | 1.9643 us/op | 1.39 |
| OrderedSet add up to 64 items then delete first | 3.5011 us/op | 3.3901 us/op | 1.03 |
| Set add up to 64 items then delete last | 2.4625 us/op | 2.1597 us/op | 1.14 |
| OrderedSet add up to 64 items then delete last | 3.5342 us/op | 3.2937 us/op | 1.07 |
| Set add up to 64 items then delete middle | 2.2480 us/op | 2.1450 us/op | 1.05 |
| OrderedSet add up to 64 items then delete middle | 5.1208 us/op | 4.7800 us/op | 1.07 |
| Set add up to 128 items then delete first | 4.3426 us/op | 4.3514 us/op | 1.00 |
| OrderedSet add up to 128 items then delete first | 6.5093 us/op | 6.5865 us/op | 0.99 |
| Set add up to 128 items then delete last | 4.3481 us/op | 3.9509 us/op | 1.10 |
| OrderedSet add up to 128 items then delete last | 6.2771 us/op | 5.8157 us/op | 1.08 |
| Set add up to 128 items then delete middle | 4.0793 us/op | 3.9594 us/op | 1.03 |
| OrderedSet add up to 128 items then delete middle | 12.602 us/op | 11.765 us/op | 1.07 |
| Set add up to 256 items then delete first | 8.0486 us/op | 7.9622 us/op | 1.01 |
| OrderedSet add up to 256 items then delete first | 12.240 us/op | 12.275 us/op | 1.00 |
| Set add up to 256 items then delete last | 8.1787 us/op | 7.7311 us/op | 1.06 |
| OrderedSet add up to 256 items then delete last | 12.561 us/op | 11.569 us/op | 1.09 |
| Set add up to 256 items then delete middle | 8.0502 us/op | 7.7164 us/op | 1.04 |
| OrderedSet add up to 256 items then delete middle | 38.001 us/op | 35.358 us/op | 1.07 |
| pass gossip attestations to forkchoice per slot | 2.6484 ms/op | 2.5932 ms/op | 1.02 |
| forkChoice updateHead vc 100000 bc 64 eq 0 | 432.16 us/op | 427.71 us/op | 1.01 |
| forkChoice updateHead vc 600000 bc 64 eq 0 | 2.5899 ms/op | 2.5811 ms/op | 1.00 |
| forkChoice updateHead vc 1000000 bc 64 eq 0 | 4.3346 ms/op | 4.3176 ms/op | 1.00 |
| forkChoice updateHead vc 600000 bc 320 eq 0 | 2.6174 ms/op | 2.6224 ms/op | 1.00 |
| forkChoice updateHead vc 600000 bc 1200 eq 0 | 2.6807 ms/op | 2.5775 ms/op | 1.04 |
| forkChoice updateHead vc 600000 bc 7200 eq 0 | 3.3106 ms/op | 2.8908 ms/op | 1.15 |
| forkChoice updateHead vc 600000 bc 64 eq 1000 | 3.1430 ms/op | 3.1863 ms/op | 0.99 |
| forkChoice updateHead vc 600000 bc 64 eq 10000 | 3.2414 ms/op | 3.2553 ms/op | 1.00 |
| forkChoice updateHead vc 600000 bc 64 eq 300000 | 7.3884 ms/op | 7.1000 ms/op | 1.04 |
| computeDeltas 1400000 validators 0% inactive | 13.892 ms/op | 12.920 ms/op | 1.08 |
| computeDeltas 1400000 validators 10% inactive | 13.002 ms/op | 12.174 ms/op | 1.07 |
| computeDeltas 1400000 validators 20% inactive | 11.895 ms/op | 11.242 ms/op | 1.06 |
| computeDeltas 1400000 validators 50% inactive | 8.9384 ms/op | 8.5471 ms/op | 1.05 |
| computeDeltas 2100000 validators 0% inactive | 20.934 ms/op | 19.677 ms/op | 1.06 |
| computeDeltas 2100000 validators 10% inactive | 19.565 ms/op | 18.419 ms/op | 1.06 |
| computeDeltas 2100000 validators 20% inactive | 17.820 ms/op | 16.907 ms/op | 1.05 |
| computeDeltas 2100000 validators 50% inactive | 13.470 ms/op | 10.069 ms/op | 1.34 |
| altair processAttestation - 250000 vs - 7PWei normalcase | 1.9577 ms/op | 2.0061 ms/op | 0.98 |
| altair processAttestation - 250000 vs - 7PWei worstcase | 2.5397 ms/op | 3.4147 ms/op | 0.74 |
| altair processAttestation - setStatus - 1/6 committees join | 90.188 us/op | 99.043 us/op | 0.91 |
| altair processAttestation - setStatus - 1/3 committees join | 184.40 us/op | 191.07 us/op | 0.97 |
| altair processAttestation - setStatus - 1/2 committees join | 286.11 us/op | 268.81 us/op | 1.06 |
| altair processAttestation - setStatus - 2/3 committees join | 375.23 us/op | 349.75 us/op | 1.07 |
| altair processAttestation - setStatus - 4/5 committees join | 514.88 us/op | 475.41 us/op | 1.08 |
| altair processAttestation - setStatus - 100% committees join | 610.40 us/op | 564.04 us/op | 1.08 |
| altair processBlock - 250000 vs - 7PWei normalcase | 3.7083 ms/op | 3.4268 ms/op | 1.08 |
| altair processBlock - 250000 vs - 7PWei normalcase hashState | 16.256 ms/op | 13.746 ms/op | 1.18 |
| altair processBlock - 250000 vs - 7PWei worstcase | 20.848 ms/op | 19.828 ms/op | 1.05 |
| altair processBlock - 250000 vs - 7PWei worstcase hashState | 41.006 ms/op | 41.857 ms/op | 0.98 |
| phase0 processBlock - 250000 vs - 7PWei normalcase | 1.4369 ms/op | 1.4443 ms/op | 0.99 |
| phase0 processBlock - 250000 vs - 7PWei worstcase | 17.085 ms/op | 17.153 ms/op | 1.00 |
| altair processEth1Data - 250000 vs - 7PWei normalcase | 299.66 us/op | 295.14 us/op | 1.02 |
| getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 | 3.4530 us/op | 5.5160 us/op | 0.63 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 | 21.427 us/op | 20.558 us/op | 1.04 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 | 6.1830 us/op | 6.4840 us/op | 0.95 |
| getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 | 3.8560 us/op | 3.8930 us/op | 0.99 |
| getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 | 93.719 us/op | 92.807 us/op | 1.01 |
| getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 | 1.4080 ms/op | 1.3860 ms/op | 1.02 |
| getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 | 1.8511 ms/op | 1.8257 ms/op | 1.01 |
| getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 | 1.8449 ms/op | 1.8264 ms/op | 1.01 |
| getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 | 3.6408 ms/op | 3.7148 ms/op | 0.98 |
| getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 | 2.1001 ms/op | 1.9492 ms/op | 1.08 |
| getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 | 3.9886 ms/op | 3.9747 ms/op | 1.00 |
| Tree 40 250000 create | 359.78 ms/op | 391.87 ms/op | 0.92 |
| Tree 40 250000 get(125000) | 98.215 ns/op | 94.740 ns/op | 1.04 |
| Tree 40 250000 set(125000) | 1.0637 us/op | 944.24 ns/op | 1.13 |
| Tree 40 250000 toArray() | 9.3501 ms/op | 11.613 ms/op | 0.81 |
| Tree 40 250000 iterate all - toArray() + loop | 9.4273 ms/op | 12.281 ms/op | 0.77 |
| Tree 40 250000 iterate all - get(i) | 35.379 ms/op | 33.127 ms/op | 1.07 |
| Array 250000 create | 2.0898 ms/op | 2.0754 ms/op | 1.01 |
| Array 250000 clone - spread | 645.50 us/op | 637.63 us/op | 1.01 |
| Array 250000 get(125000) | 0.48200 ns/op | 0.28400 ns/op | 1.70 |
| Array 250000 set(125000) | 0.49700 ns/op | 0.28700 ns/op | 1.73 |
| Array 250000 iterate all - loop | 57.383 us/op | 54.993 us/op | 1.04 |
| phase0 afterProcessEpoch - 250000 vs - 7PWei | 53.134 ms/op | 49.723 ms/op | 1.07 |
| Array.fill - length 1000000 | 2.1068 ms/op | 2.2306 ms/op | 0.94 |
| Array push - length 1000000 | 7.8129 ms/op | 7.7391 ms/op | 1.01 |
| Array.get | 0.20481 ns/op | 0.20844 ns/op | 0.98 |
| Uint8Array.get | 0.22350 ns/op | 0.23514 ns/op | 0.95 |
| phase0 beforeProcessEpoch - 250000 vs - 7PWei | 14.736 ms/op | 14.334 ms/op | 1.03 |
| altair processEpoch - mainnet_e81889 | 317.39 ms/op | 294.77 ms/op | 1.08 |
| mainnet_e81889 - altair beforeProcessEpoch | 35.924 ms/op | 36.161 ms/op | 0.99 |
| mainnet_e81889 - altair processJustificationAndFinalization | 6.3750 us/op | 6.1160 us/op | 1.04 |
| mainnet_e81889 - altair processInactivityUpdates | 3.4201 ms/op | 3.4154 ms/op | 1.00 |
| mainnet_e81889 - altair processRewardsAndPenalties | 19.185 ms/op | 18.819 ms/op | 1.02 |
| mainnet_e81889 - altair processRegistryUpdates | 759.00 ns/op | 517.00 ns/op | 1.47 |
| mainnet_e81889 - altair processSlashings | 352.00 ns/op | 134.00 ns/op | 2.63 |
| mainnet_e81889 - altair processEth1DataReset | 344.00 ns/op | 126.00 ns/op | 2.73 |
| mainnet_e81889 - altair processEffectiveBalanceUpdates | 1.6511 ms/op | 4.9716 ms/op | 0.33 |
| mainnet_e81889 - altair processSlashingsReset | 942.00 ns/op | 683.00 ns/op | 1.38 |
| mainnet_e81889 - altair processRandaoMixesReset | 1.5790 us/op | 1.4450 us/op | 1.09 |
| mainnet_e81889 - altair processHistoricalRootsUpdate | 366.00 ns/op | 131.00 ns/op | 2.79 |
| mainnet_e81889 - altair processParticipationFlagUpdates | 689.00 ns/op | 414.00 ns/op | 1.66 |
| mainnet_e81889 - altair processSyncCommitteeUpdates | 315.00 ns/op | 110.00 ns/op | 2.86 |
| mainnet_e81889 - altair afterProcessEpoch | 39.512 ms/op | 41.869 ms/op | 0.94 |
| capella processEpoch - mainnet_e217614 | 960.16 ms/op | 945.06 ms/op | 1.02 |
| mainnet_e217614 - capella beforeProcessEpoch | 66.881 ms/op | 56.927 ms/op | 1.17 |
| mainnet_e217614 - capella processJustificationAndFinalization | 6.9830 us/op | 6.2030 us/op | 1.13 |
| mainnet_e217614 - capella processInactivityUpdates | 14.388 ms/op | 11.291 ms/op | 1.27 |
| mainnet_e217614 - capella processRewardsAndPenalties | 97.715 ms/op | 108.87 ms/op | 0.90 |
| mainnet_e217614 - capella processRegistryUpdates | 4.5300 us/op | 4.3050 us/op | 1.05 |
| mainnet_e217614 - capella processSlashings | 344.00 ns/op | 134.00 ns/op | 2.57 |
| mainnet_e217614 - capella processEth1DataReset | 345.00 ns/op | 131.00 ns/op | 2.63 |
| mainnet_e217614 - capella processEffectiveBalanceUpdates | 5.6341 ms/op | 13.208 ms/op | 0.43 |
| mainnet_e217614 - capella processSlashingsReset | 931.00 ns/op | 694.00 ns/op | 1.34 |
| mainnet_e217614 - capella processRandaoMixesReset | 1.6000 us/op | 1.2710 us/op | 1.26 |
| mainnet_e217614 - capella processHistoricalRootsUpdate | 340.00 ns/op | 145.00 ns/op | 2.34 |
| mainnet_e217614 - capella processParticipationFlagUpdates | 655.00 ns/op | 412.00 ns/op | 1.59 |
| mainnet_e217614 - capella afterProcessEpoch | 107.03 ms/op | 104.20 ms/op | 1.03 |
| phase0 processEpoch - mainnet_e58758 | 314.64 ms/op | 323.04 ms/op | 0.97 |
| mainnet_e58758 - phase0 beforeProcessEpoch | 67.895 ms/op | 68.467 ms/op | 0.99 |
| mainnet_e58758 - phase0 processJustificationAndFinalization | 6.7100 us/op | 6.7740 us/op | 0.99 |
| mainnet_e58758 - phase0 processRewardsAndPenalties | 15.853 ms/op | 16.808 ms/op | 0.94 |
| mainnet_e58758 - phase0 processRegistryUpdates | 2.5070 us/op | 2.2270 us/op | 1.13 |
| mainnet_e58758 - phase0 processSlashings | 366.00 ns/op | 135.00 ns/op | 2.71 |
| mainnet_e58758 - phase0 processEth1DataReset | 366.00 ns/op | 360.00 ns/op | 1.02 |
| mainnet_e58758 - phase0 processEffectiveBalanceUpdates | 781.46 us/op | 836.77 us/op | 0.93 |
| mainnet_e58758 - phase0 processSlashingsReset | 1.0900 us/op | 825.00 ns/op | 1.32 |
| mainnet_e58758 - phase0 processRandaoMixesReset | 1.6130 us/op | 1.4040 us/op | 1.15 |
| mainnet_e58758 - phase0 processHistoricalRootsUpdate | 366.00 ns/op | 144.00 ns/op | 2.54 |
| mainnet_e58758 - phase0 processParticipationRecordUpdates | 1.4530 us/op | 1.2330 us/op | 1.18 |
| mainnet_e58758 - phase0 afterProcessEpoch | 35.255 ms/op | 34.899 ms/op | 1.01 |
| phase0 processEffectiveBalanceUpdates - 250000 normalcase | 942.42 us/op | 1.1465 ms/op | 0.82 |
| phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 | 1.2531 ms/op | 1.4874 ms/op | 0.84 |
| altair processInactivityUpdates - 250000 normalcase | 10.573 ms/op | 11.801 ms/op | 0.90 |
| altair processInactivityUpdates - 250000 worstcase | 10.559 ms/op | 12.066 ms/op | 0.88 |
| phase0 processRegistryUpdates - 250000 normalcase | 2.4470 us/op | 2.3070 us/op | 1.06 |
| phase0 processRegistryUpdates - 250000 badcase_full_deposits | 141.96 us/op | 148.38 us/op | 0.96 |
| phase0 processRegistryUpdates - 250000 worstcase 0.5 | 59.778 ms/op | 55.068 ms/op | 1.09 |
| altair processRewardsAndPenalties - 250000 normalcase | 13.436 ms/op | 13.929 ms/op | 0.96 |
| altair processRewardsAndPenalties - 250000 worstcase | 14.577 ms/op | 13.833 ms/op | 1.05 |
| phase0 getAttestationDeltas - 250000 normalcase | 5.0356 ms/op | 13.634 ms/op | 0.37 |
| phase0 getAttestationDeltas - 250000 worstcase | 5.0775 ms/op | 5.1491 ms/op | 0.99 |
| phase0 processSlashings - 250000 worstcase | 56.195 us/op | 61.323 us/op | 0.92 |
| altair processSyncCommitteeUpdates - 250000 | 9.9329 ms/op | 10.526 ms/op | 0.94 |
| BeaconState.hashTreeRoot - No change | 403.00 ns/op | 180.00 ns/op | 2.24 |
| BeaconState.hashTreeRoot - 1 full validator | 78.250 us/op | 70.505 us/op | 1.11 |
| BeaconState.hashTreeRoot - 32 full validator | 753.67 us/op | 882.17 us/op | 0.85 |
| BeaconState.hashTreeRoot - 512 full validator | 6.3589 ms/op | 6.3462 ms/op | 1.00 |
| BeaconState.hashTreeRoot - 1 validator.effectiveBalance | 101.17 us/op | 98.762 us/op | 1.02 |
| BeaconState.hashTreeRoot - 32 validator.effectiveBalance | 1.4473 ms/op | 2.5145 ms/op | 0.58 |
| BeaconState.hashTreeRoot - 512 validator.effectiveBalance | 14.143 ms/op | 14.679 ms/op | 0.96 |
| BeaconState.hashTreeRoot - 1 balances | 77.903 us/op | 89.587 us/op | 0.87 |
| BeaconState.hashTreeRoot - 32 balances | 560.26 us/op | 702.19 us/op | 0.80 |
| BeaconState.hashTreeRoot - 512 balances | 4.8393 ms/op | 6.0376 ms/op | 0.80 |
| BeaconState.hashTreeRoot - 250000 balances | 136.21 ms/op | 165.20 ms/op | 0.82 |
| aggregationBits - 2048 els - zipIndexesInBitList | 18.410 us/op | 19.990 us/op | 0.92 |
| regular array get 100000 times | 21.958 us/op | 22.763 us/op | 0.96 |
| wrappedArray get 100000 times | 21.922 us/op | 22.755 us/op | 0.96 |
| arrayWithProxy get 100000 times | 9.0187 ms/op | 12.664 ms/op | 0.71 |
| ssz.Root.equals | 20.236 ns/op | 21.198 ns/op | 0.95 |
| byteArrayEquals | 20.107 ns/op | 20.860 ns/op | 0.96 |
| Buffer.compare | 8.4180 ns/op | 8.6840 ns/op | 0.97 |
| processSlot - 1 slots | 8.9730 us/op | 10.753 us/op | 0.83 |
| processSlot - 32 slots | 2.0759 ms/op | 2.2217 ms/op | 0.93 |
| getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei | 3.5701 ms/op | 4.6901 ms/op | 0.76 |
| getCommitteeAssignments - req 1 vs - 250000 vc | 1.5849 ms/op | 1.5928 ms/op | 1.00 |
| getCommitteeAssignments - req 100 vs - 250000 vc | 3.2535 ms/op | 3.2958 ms/op | 0.99 |
| getCommitteeAssignments - req 1000 vs - 250000 vc | 3.4843 ms/op | 3.5218 ms/op | 0.99 |
| findModifiedValidators - 10000 modified validators | 539.87 ms/op | 874.23 ms/op | 0.62 |
| findModifiedValidators - 1000 modified validators | 465.01 ms/op | 527.29 ms/op | 0.88 |
| findModifiedValidators - 100 modified validators | 253.58 ms/op | 284.99 ms/op | 0.89 |
| findModifiedValidators - 10 modified validators | 148.41 ms/op | 167.80 ms/op | 0.88 |
| findModifiedValidators - 1 modified validators | 180.42 ms/op | 167.05 ms/op | 1.08 |
| findModifiedValidators - no difference | 157.30 ms/op | 160.02 ms/op | 0.98 |
| migrate state 1500000 validators, 3400 modified, 2000 new | 3.7361 s/op | 3.5157 s/op | 1.06 |
| RootCache.getBlockRootAtSlot - 250000 vs - 7PWei | 5.6200 ns/op | 3.6200 ns/op | 1.55 |
| state getBlockRootAtSlot - 250000 vs - 7PWei | 406.18 ns/op | 435.64 ns/op | 0.93 |
| computeProposerIndex 100000 validators | 1.3336 ms/op | 1.3091 ms/op | 1.02 |
| getNextSyncCommitteeIndices 1000 validators | 2.8338 ms/op | 2.7620 ms/op | 1.03 |
| getNextSyncCommitteeIndices 10000 validators | 25.118 ms/op | 24.481 ms/op | 1.03 |
| getNextSyncCommitteeIndices 100000 validators | 87.347 ms/op | 81.655 ms/op | 1.07 |
| computeProposers - vc 250000 | 544.07 us/op | 531.02 us/op | 1.02 |
| computeEpochShuffling - vc 250000 | 38.854 ms/op | 37.160 ms/op | 1.05 |
| getNextSyncCommittee - vc 250000 | 9.4047 ms/op | 9.1172 ms/op | 1.03 |
| nodejs block root to RootHex using toHex | 89.953 ns/op | 90.470 ns/op | 0.99 |
| nodejs block root to RootHex using toRootHex | 55.354 ns/op | 54.451 ns/op | 1.02 |
| nodejs fromHex(blob) | 862.84 us/op | 820.13 us/op | 1.05 |
| nodejs fromHexInto(blob) | 651.54 us/op | 608.45 us/op | 1.07 |
| nodejs block root to RootHex using the deprecated toHexString | 518.51 ns/op | 438.69 ns/op | 1.18 |
| nodejs byteArrayEquals 32 bytes (block root) | 26.719 ns/op | 25.069 ns/op | 1.07 |
| nodejs byteArrayEquals 48 bytes (pubkey) | 39.111 ns/op | 36.554 ns/op | 1.07 |
| nodejs byteArrayEquals 96 bytes (signature) | 37.059 ns/op | 33.146 ns/op | 1.12 |
| nodejs byteArrayEquals 1024 bytes | 44.015 ns/op | 39.367 ns/op | 1.12 |
| nodejs byteArrayEquals 131072 bytes (blob) | 1.8142 us/op | 1.7041 us/op | 1.06 |
| browser block root to RootHex using toHex | 149.77 ns/op | 139.57 ns/op | 1.07 |
| browser block root to RootHex using toRootHex | 135.84 ns/op | 127.16 ns/op | 1.07 |
| browser fromHex(blob) | 1.6876 ms/op | 1.6078 ms/op | 1.05 |
| browser fromHexInto(blob) | 655.75 us/op | 644.45 us/op | 1.02 |
| browser block root to RootHex using the deprecated toHexString | 363.27 ns/op | 337.98 ns/op | 1.07 |
| browser byteArrayEquals 32 bytes (block root) | 28.712 ns/op | 28.704 ns/op | 1.00 |
| browser byteArrayEquals 48 bytes (pubkey) | 40.628 ns/op | 40.486 ns/op | 1.00 |
| browser byteArrayEquals 96 bytes (signature) | 75.526 ns/op | 75.864 ns/op | 1.00 |
| browser byteArrayEquals 1024 bytes | 774.97 ns/op | 776.86 ns/op | 1.00 |
| browser byteArrayEquals 131072 bytes (blob) | 97.926 us/op | 97.370 us/op | 1.01 |
by benchmarkbot/action
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## unstable #9380 +/- ##
=========================================
Coverage 52.56% 52.56%
=========================================
Files 848 848
Lines 60986 60970 -16
Branches 4495 4491 -4
=========================================
- Hits 32059 32051 -8
+ Misses 28863 28857 -6
+ Partials 64 62 -2 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 287db244a3
ℹ️ 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".
| const res = await this.api.validator.getProposerDuties({epoch}); | ||
| // Post-Fulu the proposer dependent root changed (deterministic proposer lookahead) | ||
| const res = isForkPostFulu(this.config.getForkName(computeStartSlotAtEpoch(epoch))) | ||
| ? await this.api.validator.getProposerDutiesV2({epoch}) |
There was a problem hiding this comment.
this means we will use v2 on mainnet with the next release, this could be problematic as this endpoint was added to the spec retroactively after fulu went live on mainnet, it might be fine but we should check if all clients implement the api already
ideally, we should avoid using v2 until after gloas
There was a problem hiding this comment.
yes this is a valid concern
added fallback mechanism in #9518
There was a problem hiding this comment.
but this is different from what I mean, we shouldn't be calling getProposerDutiesV2 at all from the vc before gloas
There was a problem hiding this comment.
**Motivation** - got this error from vero ``` consensus-1 | Jun-09 21:45:56.328[rest] error: Req req-qi getProposerDuties error - Can only get block root in the past currentSlot=14518127 slot=14518143 consensus-1 | Error: Can only get block root in the past currentSlot=14518127 slot=14518143 consensus-1 | at getBlockRootAtSlot (file:///usr/app/packages/state-transition/src/util/blockRoot.ts:21:11) consensus-1 | at BeaconStateView.getBlockRootAtSlot (file:///usr/app/packages/state-transition/src/stateView/beaconStateView.ts:157:12) consensus-1 | at proposerShufflingDecisionRoot (file:///usr/app/packages/state-transition/src/util/shuffling.ts:36:16) consensus-1 | at Object.getProposerDuties (file:///usr/app/packages/beacon-node/src/api/impl/validator/index.ts:1298:9) consensus-1 | at processTicksAndRejections (node:internal/process/task_queues:104:5) consensus-1 | at Object.<anonymous> (file:///usr/app/packages/api/src/utils/server/handler.ts:105:22) ``` - #9380 was too strict, vero still querying `get_proposer_duties()` v1 **Description** - this is how it was broken for unstable and how it worked for v1.43 ``` ### lodestar unstable vero requested for v1 at epoch 453692, lodestar detected it's not v2 => fork is phase0 => decision epoch = 453692 => decision slot = 14 518 143 => throw error ### lodestar v1.43 did not care about requested epoch => based on state slot 14518127, decisionSlot is 14.518.111, which is luckily correct for requested epoch 453692 (previous slot of epoch 453691) ``` => fallback to`get_proposer_duties()` v1, which is how it worked for v1.43 **AI Assistance Disclosure** - created with the help of Claude Co-authored-by: twoeths <twoeths@users.noreply.github.com>
per #9380 (comment), we cannot use getProposerDutiesV2 before gloas as clients might not have it implemented yet
Summary
Fixes Lodestar's handling of proposer duties under the post-Fulu (EIP-7917) deterministic 1-epoch proposer lookahead. Surfaced while reviewing #9377: the validator was never querying
currentEpoch + 1proposer duties post-Fulu, and the BN's dep_root computation was wrong when serving duties for an epoch other thanstate.epoch.Contains two related stories:
getProposerDutiesV2correctly servescurrentEpoch + 1(andcurrentEpoch + 2near the boundary).AttestationDutiesService, instead of per-slot polling.BN side
proposerShufflingDecisionRootbug fix (state-transition/src/util/shuffling.ts)Previously derived the decision slot from
state.epoch, which gave the wrong dep_root whenever the state was one epoch off the requested epoch (e.g. servingstate.epoch + 1duties from the head state). Now takesproposalEpochexplicitly:dep_root(E) = block@(startSlot(E) - 1)— unchangeddep_root(E) = block@(startSlot(E - 1) - 1)— shifted back one epochgetProposerDuties(beacon-node/src/api/impl/validator/index.ts)Allows
epoch === currentEpoch + 2near the next-epoch boundary post-Fulu. The duties are served from the upcoming-epoch (currentEpoch + 1) checkpoint state'snextProposers, which is populated by theproposer_lookaheadfield. The existingnearNextEpochgate (msToNextEpoch < prepareNextSlotLookAheadMs) determines availability.Validator side
Original draft of this PR added a fork-aware
pollBeaconProposersthat, post-Fulu, pollednextEpochevery slot andnextEpoch + nextEpoch+1at the boundary. That was functional but raised a fair concern in review: why fetch two epochs at the boundary, and why poll next-epoch every slot if its dep_root is stable post-Fulu?The refactor (
refactor(validator): event-driven proposer duties via SSE head events) replaces that with an attester-style model:clock.runEveryEpoch(epoch)epoch(+epoch + 1post-Fulu, using the EIP-7917 lookahead)chainHeaderTracker.runOnNewHead(headEvent)clock.runEverySlot(slot)nextEpoch(its dep_root only stabilizes at the boundary and isn't exposed via SSE)The SSE head event already carries everything needed for both forks via a nice coincidence in the dep_root math:
currentDutyDependentRoot ≡ proposer_dep_root(currentEpoch)previousDutyDependentRoot ≡ proposer_dep_root(currentEpoch),currentDutyDependentRoot ≡ proposer_dep_root(nextEpoch)No spec/event changes required — the same fields the validator already uses for attester duties cover the post-Fulu proposer lookahead window.
A per-slot notification dedup (
notifiedSlot/notifiedProposers) replaces the old "two-pass withdifferenceHex" pattern so any source of cache update (SSE refetch, cold-cache back-fill, epoch tick) only notifies newly discovered proposers and never duplicatescreateAndPublishBlockcalls.Results
In steady state, the validator now makes 2 proposer-duty calls per epoch (current + next epoch pre-fetch) plus refetches only on dep_root changes — matching the per-epoch cadence of
AttestationDutiesService(which previously had been 32× more frequent).Tests
BlockDutiesServiceunit tests covering: post-Fulu pre-fetch of next epoch, pre-Fulu vs post-Fulu fork detection, SSE-driven refetch on dep_root mismatch, no-op on dep_root match, cold-cache back-fill, pre-Fulu boundary scheduling + post-Fulu suppression, signer removal across epochs.getProposerDutiestests updated to exercise the V2 path with a post-Fulu config.proposerBoostReorg,finalizedSync,checkpointSync(Fulu fork crossings, reorgs, checkpoint sync) — all pass, all 30+ block proposals fire correctly, no new errors.Known follow-ups (non-blocking)
Genesis-state dep_root quirk (BN-side, cosmetic). At very early genesis, the BN's
getProposerDutiesreturnsgenesisBlockRootvia thestate.slot === decisionSlotfallback, but later returnsstate.getBlockRootAtSlot(0)for the same epoch — they're cosmetically different roots for the same logical block. The old code didn't observe this because it didn't pre-fetchnextEpochuntil ~1s before the boundary; the new code pre-fetches at the start of epoch 0 and sees one or two spuriousProposer duties re-orgwarnings per VC at startup. Duties are correct — pure metric noise. Worth a small BN-side normalization or a "skip pre-fetch on first epoch tick" guard.Concurrent
pollBeaconProposersrace. IfonNewHeadandrunEveryEpochTaskrace on the same epoch with asymmetric HTTP latencies, last-write-wins can briefly leave a stale dep_root cached. In practice the same BN serves both calls and returns identical payloads. Documented in a code comment; a per-epoch sequence number would harden it if it ever becomes a real problem.Gloas timing.
BLOCK_DUTIES_LOOKAHEAD_BPSmay want to flip from "1s before the boundary" to "1s after" post-Gloas. ExistingTODO GLOAS: re-evaluate timingis preserved.AI disclosure
Refactor designed and implemented with AI assistance.