Skip to content

fix: bound payload envelope seen cache - #9904

Merged
twoeths merged 3 commits into
unstablefrom
te/bound_payload_envelope_seen_cache
Aug 25, 2026
Merged

fix: bound payload envelope seen cache#9904
twoeths merged 3 commits into
unstablefrom
te/bound_payload_envelope_seen_cache

Conversation

@twoeths

@twoeths twoeths commented Aug 24, 2026

Copy link
Copy Markdown
Member

Motivation

  • all caches in lodestar should be bound, the reason it failed in this previous effort is range sync but we should fix it now
  • to prepare for the upcoming nft devnet

Description

  • for range sync, we should use its own payload envelopes, instead of the seen cache to avoid the previous error
  • add getOrReload() to the seen cache. If, just in case, some old payloads built on a pruned EMPTY PayloadEnvelopeInput, we can reload easily from db
  • use getOrReload() for UnknownBlockInput
  • more metrics + logs

AI Assistance Disclosure

  • created with Claude

@twoeths
twoeths marked this pull request as ready for review August 24, 2026 08:40
@twoeths
twoeths requested a review from a team as a code owner August 24, 2026 08:40

@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: bb66eca801

ℹ️ 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".

Comment on lines +292 to +296
for (const input of this.payloadInputs.values()) {
if (this.payloadInputs.size <= MAX_PAYLOAD_ENVELOPE_INPUT_CACHE_SIZE) {
break;
}
this.evictPayloadInput(input, "cap");

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 Preserve active payload inputs when enforcing the cap

When range sync fills the 96-entry cap and another block is inserted, insertion-order eviction can remove an older-inserted canonical input even if it is recent and still awaiting its envelope or columns. The gossip envelope and payload-column handlers only call seenPayloadEnvelopeInputCache.get(), so after this eviction they ignore valid data with PAYLOAD_ENVELOPE_INPUT_MISSING; they do not recover through getOrReload() as the safety comment claims. This can stall payload processing during concurrent range sync or fork pressure. The cap must exclude active inputs or the gossip paths must reload them.

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 should not happen, the seen cache contains 96 items while gossip only accept messages in the last recent 32 slots, given DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE = 32

even if gossip fails due to this, this is by design, because it will fallback to UnknownBlockInputwhich callsgetOrReload()`

source: PayloadEnvelopeInputSource.reload,
daOutOfRange,
});
this.payloadInputs.set(blockRootHex, input);

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 Recheck the cache after the asynchronous DB read

On the DB path, reloadFromDb() yields while awaiting db.block.get(). A concurrent gossip, API, by-root, or range call can then add the same root and attach an envelope or columns, but this unconditional set() overwrites that live object with an empty reconstructed shell. Callers retaining the first object and callers reading the cache then operate on different inputs, potentially losing received data and duplicating payload processing. Recheck payloadInputs after the await and return the concurrently inserted entry instead.

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.

should not happen too, when the block comes again at either source it will be ignored due to forkchoice check

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: 3a875c9 Previous: e7a3253 Ratio
getPubkeys - native cache - req 1000 vs - 250000 vc 484.62 us/op 448.09 us/op 1.08
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 38.922 us/op 41.699 us/op 0.93
BLS verify - blst 898.31 us/op 912.98 us/op 0.98
BLS verifyMultipleSignatures 3 - blst 1.3324 ms/op 1.3715 ms/op 0.97
BLS verifyMultipleSignatures 8 - blst 2.2554 ms/op 2.2931 ms/op 0.98
BLS verifyMultipleSignatures 32 - blst 7.4085 ms/op 7.6212 ms/op 0.97
BLS verifyMultipleSignatures 64 - blst 14.263 ms/op 14.566 ms/op 0.98
BLS verifyMultipleSignatures 128 - blst 27.025 ms/op 27.270 ms/op 0.99
BLS deserializing 10000 signatures 616.62 ms/op 646.28 ms/op 0.95
BLS deserializing 100000 signatures 6.4225 s/op 6.5097 s/op 0.99
BLS verifyMultipleSignatures - same message - 3 - blst 924.01 us/op 977.44 us/op 0.95
BLS verifyMultipleSignatures - same message - 8 - blst 1.0483 ms/op 1.1073 ms/op 0.95
BLS verifyMultipleSignatures - same message - 32 - blst 1.6566 ms/op 1.7368 ms/op 0.95
BLS verifyMultipleSignatures - same message - 64 - blst 2.4467 ms/op 2.5862 ms/op 0.95
BLS verifyMultipleSignatures - same message - 128 - blst 4.0417 ms/op 4.2968 ms/op 0.94
BLS aggregatePubkeys 32 - blst 18.974 us/op 19.845 us/op 0.96
BLS aggregatePubkeys 128 - blst 68.079 us/op 71.045 us/op 0.96
getSlashingsAndExits - default max 43.496 us/op 45.458 us/op 0.96
getSlashingsAndExits - 2k 348.74 us/op 387.05 us/op 0.90
proposeBlockBody type=full, size=empty 1.3318 ms/op 1.2748 ms/op 1.04
isKnown best case - 1 super set check 172.00 ns/op 173.00 ns/op 0.99
isKnown normal case - 2 super set checks 171.00 ns/op 164.00 ns/op 1.04
isKnown worse case - 16 super set checks 169.00 ns/op 166.00 ns/op 1.02
validate api signedAggregateAndProof - struct 1.5013 ms/op 1.5462 ms/op 0.97
validate gossip signedAggregateAndProof - struct 1.5280 ms/op 1.5429 ms/op 0.99
batch validate gossip attestation - vc 640000 - chunk 32 124.79 us/op 130.36 us/op 0.96
batch validate gossip attestation - vc 640000 - chunk 64 107.20 us/op 109.00 us/op 0.98
batch validate gossip attestation - vc 640000 - chunk 128 96.579 us/op 100.66 us/op 0.96
batch validate gossip attestation - vc 640000 - chunk 256 96.382 us/op 101.48 us/op 0.95
bytes32 toHexString 287.00 ns/op 294.00 ns/op 0.98
bytes32 Buffer.toString(hex) 179.00 ns/op 180.00 ns/op 0.99
bytes32 Buffer.toString(hex) from Uint8Array 239.00 ns/op 245.00 ns/op 0.98
bytes32 Buffer.toString(hex) + 0x 180.00 ns/op 182.00 ns/op 0.99
Return object 10000 times 0.21550 ns/op 0.21760 ns/op 0.99
Throw Error 10000 times 3.3788 us/op 3.3612 us/op 1.01
toHex 97.829 ns/op 98.243 ns/op 1.00
Buffer.from 89.651 ns/op 88.152 ns/op 1.02
shared Buffer 62.197 ns/op 60.100 ns/op 1.03
fastMsgIdFn sha256 / 200 bytes 1.5010 us/op 1.5040 us/op 1.00
fastMsgIdFn h32 xxhash / 200 bytes 163.00 ns/op 155.00 ns/op 1.05
fastMsgIdFn h64 xxhash / 200 bytes 219.00 ns/op 211.00 ns/op 1.04
fastMsgIdFn sha256 / 1000 bytes 4.8620 us/op 4.8440 us/op 1.00
fastMsgIdFn h32 xxhash / 1000 bytes 254.00 ns/op 252.00 ns/op 1.01
fastMsgIdFn h64 xxhash / 1000 bytes 266.00 ns/op 265.00 ns/op 1.00
fastMsgIdFn sha256 / 10000 bytes 43.276 us/op 43.086 us/op 1.00
fastMsgIdFn h32 xxhash / 10000 bytes 1.3370 us/op 1.3410 us/op 1.00
fastMsgIdFn h64 xxhash / 10000 bytes 870.00 ns/op 862.00 ns/op 1.01
send data - 1000 256B messages 4.7092 ms/op 4.4294 ms/op 1.06
send data - 1000 512B messages 5.8233 ms/op 5.4363 ms/op 1.07
send data - 1000 1024B messages 6.5447 ms/op 5.5370 ms/op 1.18
send data - 1000 1200B messages 7.0094 ms/op 6.6294 ms/op 1.06
send data - 1000 2048B messages 11.927 ms/op 13.126 ms/op 0.91
send data - 1000 4096B messages 72.181 ms/op 122.09 ms/op 0.59
send data - 1000 16384B messages 456.45 ms/op 563.92 ms/op 0.81
send data - 1000 65536B messages 1.8444 s/op 2.3736 s/op 0.78
enrSubnets - fastDeserialize 64 bits 750.00 ns/op 783.00 ns/op 0.96
enrSubnets - ssz BitVector 64 bits 271.00 ns/op 269.00 ns/op 1.01
enrSubnets - fastDeserialize 4 bits 102.00 ns/op 103.00 ns/op 0.99
enrSubnets - ssz BitVector 4 bits 276.00 ns/op 273.00 ns/op 1.01
prioritizePeers score -10:0 att 32-0.1 sync 2-0 203.16 us/op 216.86 us/op 0.94
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 229.97 us/op 274.78 us/op 0.84
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 325.34 us/op 358.94 us/op 0.91
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 579.30 us/op 621.52 us/op 0.93
prioritizePeers score 0:0 att 64-1 sync 4-1 707.91 us/op 763.27 us/op 0.93
array of 16000 items push then shift 1.2485 us/op 1.3497 us/op 0.93
LinkedList of 16000 items push then shift 7.2970 ns/op 8.2240 ns/op 0.89
array of 16000 items push then pop 80.471 ns/op 90.801 ns/op 0.89
LinkedList of 16000 items push then pop 5.9060 ns/op 6.4330 ns/op 0.92
array of 24000 items push then shift 1.8573 us/op 1.9958 us/op 0.93
LinkedList of 24000 items push then shift 7.2710 ns/op 7.5240 ns/op 0.97
array of 24000 items push then pop 117.41 ns/op 126.43 ns/op 0.93
LinkedList of 24000 items push then pop 6.0330 ns/op 6.3590 ns/op 0.95
intersect bitArray bitLen 8 3.8410 ns/op 3.9730 ns/op 0.97
intersect array and set length 8 29.583 ns/op 30.783 ns/op 0.96
intersect bitArray bitLen 128 23.242 ns/op 23.911 ns/op 0.97
intersect array and set length 128 496.53 ns/op 519.37 ns/op 0.96
bitArray.getTrueBitIndexes() bitLen 128 934.00 ns/op 996.00 ns/op 0.94
bitArray.getTrueBitIndexes() bitLen 248 1.6960 us/op 1.8260 us/op 0.93
bitArray.getTrueBitIndexes() bitLen 512 3.5880 us/op 3.7860 us/op 0.95
Full columns - reconstruct all 6 blobs 188.50 us/op 161.90 us/op 1.16
Full columns - reconstruct half of the blobs out of 6 102.72 us/op 89.360 us/op 1.15
Full columns - reconstruct single blob out of 6 43.009 us/op 36.467 us/op 1.18
Half columns - reconstruct all 6 blobs 422.98 ms/op 412.60 ms/op 1.03
Half columns - reconstruct half of the blobs out of 6 211.41 ms/op 204.33 ms/op 1.03
Half columns - reconstruct single blob out of 6 76.522 ms/op 71.999 ms/op 1.06
Set add up to 64 items then delete first 2.9167 us/op 2.2829 us/op 1.28
OrderedSet add up to 64 items then delete first 4.3457 us/op 3.6363 us/op 1.20
Set add up to 64 items then delete last 5.6928 us/op 2.4871 us/op 2.29
OrderedSet add up to 64 items then delete last 5.3200 us/op 3.4197 us/op 1.56
Set add up to 64 items then delete middle 3.9813 us/op 2.1795 us/op 1.83
OrderedSet add up to 64 items then delete middle 8.6834 us/op 4.8765 us/op 1.78
Set add up to 128 items then delete first 7.1056 us/op 4.3457 us/op 1.64
OrderedSet add up to 128 items then delete first 11.076 us/op 6.7737 us/op 1.64
Set add up to 128 items then delete last 7.3458 us/op 3.8671 us/op 1.90
OrderedSet add up to 128 items then delete last 11.695 us/op 5.8783 us/op 1.99
Set add up to 128 items then delete middle 7.6151 us/op 3.8819 us/op 1.96
OrderedSet add up to 128 items then delete middle 17.310 us/op 11.791 us/op 1.47
Set add up to 256 items then delete first 12.230 us/op 7.9710 us/op 1.53
OrderedSet add up to 256 items then delete first 20.251 us/op 12.355 us/op 1.64
Set add up to 256 items then delete last 12.259 us/op 7.7266 us/op 1.59
OrderedSet add up to 256 items then delete last 20.233 us/op 11.907 us/op 1.70
Set add up to 256 items then delete middle 12.418 us/op 7.7500 us/op 1.60
OrderedSet add up to 256 items then delete middle 44.433 us/op 35.822 us/op 1.24
runFastConfirmationRules vc:100000 bc:96 eq:0 10.722 ms/op 4.7816 ms/op 2.24
runFastConfirmationRules vc:600000 bc:96 eq:0 50.234 ms/op 34.840 ms/op 1.44
runFastConfirmationRules vc:1000000 bc:96 eq:0 71.168 ms/op 58.489 ms/op 1.22
runFastConfirmationRules vc:600000 bc:320 eq:0 39.310 ms/op 35.841 ms/op 1.10
runFastConfirmationRules vc:100000 bc:96 eq:1000 1.5272 s/op 1.1802 s/op 1.29
pass gossip attestations to forkchoice per slot 2.7602 ms/op 2.6199 ms/op 1.05
forkChoice updateHead vc 100000 bc 64 eq 0 419.86 us/op 415.57 us/op 1.01
forkChoice updateHead vc 600000 bc 64 eq 0 2.4988 ms/op 2.5450 ms/op 0.98
forkChoice updateHead vc 1000000 bc 64 eq 0 4.1542 ms/op 4.1870 ms/op 0.99
forkChoice updateHead vc 600000 bc 320 eq 0 2.5596 ms/op 2.4974 ms/op 1.02
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5602 ms/op 2.6086 ms/op 0.98
forkChoice updateHead vc 600000 bc 7200 eq 0 3.2779 ms/op 2.8846 ms/op 1.14
forkChoice updateHead vc 600000 bc 64 eq 1000 2.5718 ms/op 2.5369 ms/op 1.01
forkChoice updateHead vc 600000 bc 64 eq 10000 2.6607 ms/op 2.6085 ms/op 1.02
forkChoice updateHead vc 600000 bc 64 eq 300000 7.4151 ms/op 7.1947 ms/op 1.03
computeDeltas 1400000 validators 0% inactive 12.759 ms/op 12.409 ms/op 1.03
computeDeltas 1400000 validators 10% inactive 12.055 ms/op 11.752 ms/op 1.03
computeDeltas 1400000 validators 20% inactive 11.399 ms/op 11.265 ms/op 1.01
computeDeltas 1400000 validators 50% inactive 9.2754 ms/op 8.9511 ms/op 1.04
computeDeltas 2100000 validators 0% inactive 19.141 ms/op 18.875 ms/op 1.01
computeDeltas 2100000 validators 10% inactive 18.129 ms/op 17.948 ms/op 1.01
computeDeltas 2100000 validators 20% inactive 17.074 ms/op 16.775 ms/op 1.02
computeDeltas 2100000 validators 50% inactive 13.935 ms/op 10.973 ms/op 1.27
altair processAttestation - 250000 vs - 7PWei normalcase 2.4094 ms/op 1.6302 ms/op 1.48
altair processAttestation - 250000 vs - 7PWei worstcase 3.1571 ms/op 2.4669 ms/op 1.28
altair processAttestation - setStatus - 1/6 committees join 101.77 us/op 99.963 us/op 1.02
altair processAttestation - setStatus - 1/3 committees join 201.44 us/op 204.21 us/op 0.99
altair processAttestation - setStatus - 1/2 committees join 280.83 us/op 280.26 us/op 1.00
altair processAttestation - setStatus - 2/3 committees join 358.58 us/op 372.84 us/op 0.96
altair processAttestation - setStatus - 4/5 committees join 511.25 us/op 533.14 us/op 0.96
altair processAttestation - setStatus - 100% committees join 620.39 us/op 633.25 us/op 0.98
altair processBlock - 250000 vs - 7PWei normalcase 4.6947 ms/op 5.6692 ms/op 0.83
altair processBlock - 250000 vs - 7PWei normalcase hashState 21.999 ms/op 25.439 ms/op 0.86
altair processBlock - 250000 vs - 7PWei worstcase 23.665 ms/op 27.143 ms/op 0.87
altair processBlock - 250000 vs - 7PWei worstcase hashState 52.413 ms/op 56.206 ms/op 0.93
phase0 processBlock - 250000 vs - 7PWei normalcase 1.7878 ms/op 1.4229 ms/op 1.26
phase0 processBlock - 250000 vs - 7PWei worstcase 21.349 ms/op 21.986 ms/op 0.97
altair processEth1Data - 250000 vs - 7PWei normalcase 299.60 us/op 317.43 us/op 0.94
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 5.8020 us/op 3.3380 us/op 1.74
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 20.774 us/op 20.727 us/op 1.00
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 5.9140 us/op 5.8940 us/op 1.00
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 3.6720 us/op 3.9510 us/op 0.93
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 88.152 us/op 92.501 us/op 0.95
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.4331 ms/op 1.4889 ms/op 0.96
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8812 ms/op 1.9630 ms/op 0.96
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.8023 ms/op 2.0538 ms/op 0.88
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 3.6731 ms/op 4.8435 ms/op 0.76
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.1539 ms/op 2.1696 ms/op 0.99
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 3.9134 ms/op 4.0394 ms/op 0.97
Tree 40 250000 create 355.67 ms/op 313.78 ms/op 1.13
Tree 40 250000 get(125000) 89.832 ns/op 98.020 ns/op 0.92
Tree 40 250000 set(125000) 983.84 ns/op 1.0309 us/op 0.95
Tree 40 250000 toArray() 15.315 ms/op 9.7462 ms/op 1.57
Tree 40 250000 iterate all - toArray() + loop 14.580 ms/op 9.8789 ms/op 1.48
Tree 40 250000 iterate all - get(i) 38.259 ms/op 35.018 ms/op 1.09
Array 250000 create 2.5387 ms/op 2.4691 ms/op 1.03
Array 250000 clone - spread 774.12 us/op 795.12 us/op 0.97
Array 250000 get(125000) 0.28900 ns/op 0.29500 ns/op 0.98
Array 250000 set(125000) 0.28700 ns/op 0.29300 ns/op 0.98
Array 250000 iterate all - loop 56.076 us/op 56.876 us/op 0.99
phase0 afterProcessEpoch - 250000 vs - 7PWei 37.632 ms/op 40.163 ms/op 0.94
Array.fill - length 1000000 4.0762 ms/op 4.2048 ms/op 0.97
Array push - length 1000000 14.126 ms/op 13.389 ms/op 1.06
Array.get 0.20568 ns/op 0.20663 ns/op 1.00
Uint8Array.get 0.24487 ns/op 0.25421 ns/op 0.96
phase0 beforeProcessEpoch - 250000 vs - 7PWei 14.246 ms/op 15.051 ms/op 0.95
altair processEpoch - mainnet_e81889 257.82 ms/op 198.90 ms/op 1.30
mainnet_e81889 - altair beforeProcessEpoch 38.457 ms/op 21.484 ms/op 1.79
mainnet_e81889 - altair processJustificationAndFinalization 5.6300 us/op 4.3140 us/op 1.31
mainnet_e81889 - altair processInactivityUpdates 3.6600 ms/op 3.5161 ms/op 1.04
mainnet_e81889 - altair processRewardsAndPenalties 20.114 ms/op 16.665 ms/op 1.21
mainnet_e81889 - altair processRegistryUpdates 567.00 ns/op 557.00 ns/op 1.02
mainnet_e81889 - altair processSlashings 142.00 ns/op 142.00 ns/op 1.00
mainnet_e81889 - altair processEth1DataReset 144.00 ns/op 135.00 ns/op 1.07
mainnet_e81889 - altair processEffectiveBalanceUpdates 1.1920 ms/op 1.6714 ms/op 0.71
mainnet_e81889 - altair processSlashingsReset 696.00 ns/op 707.00 ns/op 0.98
mainnet_e81889 - altair processRandaoMixesReset 974.00 ns/op 941.00 ns/op 1.04
mainnet_e81889 - altair processHistoricalRootsUpdate 144.00 ns/op 138.00 ns/op 1.04
mainnet_e81889 - altair processParticipationFlagUpdates 451.00 ns/op 460.00 ns/op 0.98
mainnet_e81889 - altair processSyncCommitteeUpdates 118.00 ns/op 113.00 ns/op 1.04
mainnet_e81889 - altair afterProcessEpoch 42.754 ms/op 42.469 ms/op 1.01
capella processEpoch - mainnet_e217614 839.64 ms/op 627.46 ms/op 1.34
mainnet_e217614 - capella beforeProcessEpoch 59.458 ms/op 56.493 ms/op 1.05
mainnet_e217614 - capella processJustificationAndFinalization 4.9770 us/op 4.4350 us/op 1.12
mainnet_e217614 - capella processInactivityUpdates 11.533 ms/op 11.511 ms/op 1.00
mainnet_e217614 - capella processRewardsAndPenalties 102.89 ms/op 91.545 ms/op 1.12
mainnet_e217614 - capella processRegistryUpdates 4.6000 us/op 4.5130 us/op 1.02
mainnet_e217614 - capella processSlashings 144.00 ns/op 136.00 ns/op 1.06
mainnet_e217614 - capella processEth1DataReset 140.00 ns/op 132.00 ns/op 1.06
mainnet_e217614 - capella processEffectiveBalanceUpdates 17.749 ms/op 5.5455 ms/op 3.20
mainnet_e217614 - capella processSlashingsReset 683.00 ns/op 678.00 ns/op 1.01
mainnet_e217614 - capella processRandaoMixesReset 1.0390 us/op 933.00 ns/op 1.11
mainnet_e217614 - capella processHistoricalRootsUpdate 145.00 ns/op 134.00 ns/op 1.08
mainnet_e217614 - capella processParticipationFlagUpdates 471.00 ns/op 451.00 ns/op 1.04
mainnet_e217614 - capella afterProcessEpoch 109.01 ms/op 108.24 ms/op 1.01
phase0 processEpoch - mainnet_e58758 238.29 ms/op 188.97 ms/op 1.26
mainnet_e58758 - phase0 beforeProcessEpoch 54.945 ms/op 41.397 ms/op 1.33
mainnet_e58758 - phase0 processJustificationAndFinalization 5.3790 us/op 4.5000 us/op 1.20
mainnet_e58758 - phase0 processRewardsAndPenalties 16.303 ms/op 16.600 ms/op 0.98
mainnet_e58758 - phase0 processRegistryUpdates 2.3400 us/op 2.3170 us/op 1.01
mainnet_e58758 - phase0 processSlashings 148.00 ns/op 139.00 ns/op 1.06
mainnet_e58758 - phase0 processEth1DataReset 143.00 ns/op 240.00 ns/op 0.60
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 848.23 us/op 826.78 us/op 1.03
mainnet_e58758 - phase0 processSlashingsReset 874.00 ns/op 824.00 ns/op 1.06
mainnet_e58758 - phase0 processRandaoMixesReset 1.0990 us/op 991.00 ns/op 1.11
mainnet_e58758 - phase0 processHistoricalRootsUpdate 146.00 ns/op 145.00 ns/op 1.01
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.0510 us/op 957.00 ns/op 1.10
mainnet_e58758 - phase0 afterProcessEpoch 34.420 ms/op 34.112 ms/op 1.01
phase0 processEffectiveBalanceUpdates - 250000 normalcase 995.55 us/op 1.0022 ms/op 0.99
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.6810 ms/op 1.2420 ms/op 1.35
altair processInactivityUpdates - 250000 normalcase 11.406 ms/op 11.138 ms/op 1.02
altair processInactivityUpdates - 250000 worstcase 11.401 ms/op 11.209 ms/op 1.02
phase0 processRegistryUpdates - 250000 normalcase 2.3810 us/op 2.3650 us/op 1.01
phase0 processRegistryUpdates - 250000 badcase_full_deposits 139.75 us/op 148.18 us/op 0.94
phase0 processRegistryUpdates - 250000 worstcase 0.5 62.530 ms/op 68.629 ms/op 0.91
altair processRewardsAndPenalties - 250000 normalcase 14.827 ms/op 16.714 ms/op 0.89
altair processRewardsAndPenalties - 250000 worstcase 14.413 ms/op 15.333 ms/op 0.94
phase0 getAttestationDeltas - 250000 normalcase 5.7694 ms/op 5.7117 ms/op 1.01
phase0 getAttestationDeltas - 250000 worstcase 5.8121 ms/op 6.0488 ms/op 0.96
phase0 processSlashings - 250000 worstcase 57.388 us/op 61.425 us/op 0.93
altair processSyncCommitteeUpdates - 250000 10.100 ms/op 10.209 ms/op 0.99
BeaconState.hashTreeRoot - No change 175.00 ns/op 172.00 ns/op 1.02
BeaconState.hashTreeRoot - 1 full validator 77.501 us/op 78.026 us/op 0.99
BeaconState.hashTreeRoot - 32 full validator 1.0041 ms/op 836.03 us/op 1.20
BeaconState.hashTreeRoot - 512 full validator 6.5774 ms/op 7.4948 ms/op 0.88
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 82.515 us/op 90.746 us/op 0.91
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.3700 ms/op 1.4437 ms/op 0.95
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 18.515 ms/op 15.009 ms/op 1.23
BeaconState.hashTreeRoot - 1 balances 86.632 us/op 64.373 us/op 1.35
BeaconState.hashTreeRoot - 32 balances 799.59 us/op 761.63 us/op 1.05
BeaconState.hashTreeRoot - 512 balances 5.3216 ms/op 5.2328 ms/op 1.02
BeaconState.hashTreeRoot - 250000 balances 166.40 ms/op 116.47 ms/op 1.43
aggregationBits - 2048 els - zipIndexesInBitList 19.444 us/op 19.711 us/op 0.99
regular array get 100000 times 22.660 us/op 23.338 us/op 0.97
wrappedArray get 100000 times 22.727 us/op 23.253 us/op 0.98
arrayWithProxy get 100000 times 9.4328 ms/op 10.359 ms/op 0.91
ssz.Root.equals 21.205 ns/op 21.707 ns/op 0.98
byteArrayEquals 21.097 ns/op 21.474 ns/op 0.98
Buffer.compare 8.7070 ns/op 8.9080 ns/op 0.98
processSlot - 1 slots 12.900 us/op 8.4910 us/op 1.52
processSlot - 32 slots 2.3468 ms/op 1.5953 ms/op 1.47
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 4.1656 ms/op 4.6254 ms/op 0.90
getCommitteeAssignments - req 1 vs - 250000 vc 1.7172 ms/op 1.7115 ms/op 1.00
getCommitteeAssignments - req 100 vs - 250000 vc 3.4963 ms/op 3.4607 ms/op 1.01
getCommitteeAssignments - req 1000 vs - 250000 vc 3.7635 ms/op 3.6852 ms/op 1.02
findModifiedValidators - 10000 modified validators 769.84 ms/op 762.02 ms/op 1.01
findModifiedValidators - 1000 modified validators 538.29 ms/op 731.73 ms/op 0.74
findModifiedValidators - 100 modified validators 363.89 ms/op 362.22 ms/op 1.00
findModifiedValidators - 10 modified validators 300.56 ms/op 299.83 ms/op 1.00
findModifiedValidators - 1 modified validators 229.68 ms/op 286.45 ms/op 0.80
findModifiedValidators - no difference 221.54 ms/op 202.41 ms/op 1.09
migrate state 1500000 validators, 3400 modified, 2000 new 4.1941 s/op 2.7073 s/op 1.55
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.6100 ns/op 3.6900 ns/op 0.98
state getBlockRootAtSlot - 250000 vs - 7PWei 580.53 ns/op 283.46 ns/op 2.05
computeProposerIndex 100000 validators 1.4520 ms/op 1.3869 ms/op 1.05
getNextSyncCommitteeIndices 1000 validators 2.9964 ms/op 2.8776 ms/op 1.04
getNextSyncCommitteeIndices 10000 validators 26.931 ms/op 26.029 ms/op 1.03
getNextSyncCommitteeIndices 100000 validators 93.872 ms/op 88.026 ms/op 1.07
computeProposers - vc 250000 595.73 us/op 542.82 us/op 1.10
computeEpochShuffling - vc 250000 40.358 ms/op 39.096 ms/op 1.03
getNextSyncCommittee - vc 250000 9.8493 ms/op 9.4637 ms/op 1.04
nodejs block root to RootHex using toHex 103.28 ns/op 100.50 ns/op 1.03
nodejs block root to RootHex using toRootHex 65.123 ns/op 64.832 ns/op 1.00
nodejs fromHex(blob) 1.1149 ms/op 876.16 us/op 1.27
nodejs fromHexInto(blob) 634.81 us/op 634.27 us/op 1.00
nodejs block root to RootHex using the deprecated toHexString 609.00 ns/op 641.63 ns/op 0.95
nodejs byteArrayEquals 32 bytes (block root) 26.409 ns/op 26.034 ns/op 1.01
nodejs byteArrayEquals 48 bytes (pubkey) 38.234 ns/op 37.685 ns/op 1.01
nodejs byteArrayEquals 96 bytes (signature) 35.508 ns/op 36.754 ns/op 0.97
nodejs byteArrayEquals 1024 bytes 41.165 ns/op 44.234 ns/op 0.93
nodejs byteArrayEquals 131072 bytes (blob) 1.7991 us/op 1.7851 us/op 1.01
browser block root to RootHex using toHex 146.73 ns/op 146.98 ns/op 1.00
browser block root to RootHex using toRootHex 131.71 ns/op 131.64 ns/op 1.00
browser fromHex(blob) 1.9587 ms/op 1.7564 ms/op 1.12
browser fromHexInto(blob) 633.44 us/op 636.99 us/op 0.99
browser block root to RootHex using the deprecated toHexString 404.93 ns/op 446.39 ns/op 0.91
browser byteArrayEquals 32 bytes (block root) 27.934 ns/op 27.968 ns/op 1.00
browser byteArrayEquals 48 bytes (pubkey) 39.257 ns/op 39.446 ns/op 1.00
browser byteArrayEquals 96 bytes (signature) 73.890 ns/op 74.102 ns/op 1.00
browser byteArrayEquals 1024 bytes 760.45 ns/op 757.14 ns/op 1.00
browser byteArrayEquals 131072 bytes (blob) 95.949 us/op 96.903 us/op 0.99

by benchmarkbot/action

@twoeths

twoeths commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

@lodekeeper please review, given the last error that you know #9489 (comment)

@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 against the #9489 failure I root-caused (insert-time pruneToMaxSize evicting a root mid-batch → cacheByRangeResponses throwing Missing PayloadEnvelopeInput).

Core fix — LGTM, this closes the #9489 hole. Range sync now resolves in-batch payload inputs from the batch-local payloadEnvelopes map (strong refs, seeded in the validatedBlocks loop, round-tripped across retries via Batch.state.payloadEnvelopes), so an in-batch entry is guaranteed present regardless of shared-cache eviction. Only the dangling parent still falls back to seenPayloadEnvelopeInputCache.get(), and that path is isFirstBatchInChain-only at sync-chain start when the cache is well below the 96 cap — no realistic eviction there. Right decoupling: range sync no longer depends on the shared cache for correctness.

On the two Codex P1s — I agree neither is a blocker, two small precision notes:

P1 / pruneToMaxSize (L296): the mechanism is real — eviction is insertion-order (Map.values()), not slot-order, so a concurrent backfill/reorg burst can shed an older-inserted but recent-slot gossip entry. And to be precise, the gossip handlers don't self-heal here: all three consumers (.get() at L492 column, L645 block post-processing, L1196 envelope) IGNORE/throw on miss — none call getOrReload. The recovery is the UnknownBlockInput fallthrough (which does getOrReload), not the gossip path itself. In practice this needs a below-head backfill piling ≥96 inserts after a live gossip entry, and it's recoverable via that fallthrough + req/resp — so bounded, agreed. Only ask: tweak the pruneToMaxSize comment to name the UnknownBlockInput fallthrough as the recovery path rather than implying the gossip handlers reload, so the next reader doesn't over-trust the shared cache.

P1 / reloadFromDb recheck (L240): agree it's effectively unreachable — the reloading map dedups getOrReload×getOrReload, any add() for a fork-choice-known root short-circuits before reloadFromDb, and the EMPTY-over-FULL overwrite is by design with consumers gating on hasPayloadHexUnsafe. A 2-line post-await const existing = this.payloadInputs.get(blockRootHex); if (existing) return existing; before the set() is optional insurance so a future add() caller can't invalidate the invariant — not required.

Nit: reloadFromDb uses Math.floor(Date.now()/1000) while downloadByRoot uses unfloored Date.now()/1000 — harmless.

Nice work bounding this without reintroducing the #9489 regression.

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

One more spot I looked at, resolved — noting for the record, no action needed:

getMissingBlockDependency (unknownBlock.ts L516) drops the seen-cache parent-payload mismatch check. On a closer read this is fine: the authoritative invalidParentPayload branch (forkChoice.hasPayloadHexUnsafe(parentRootHex), L543-544) is retained, so a child referencing a wrong parent block hash is still rejected once the parent payload reveals. The removed parentPayloadInput.getBlockHashHex() comparison was only a pre-reveal shortcut over the now-non-authoritative seen cache, so dropping it is correct.

return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
}

const parentPayloadInput = this.chain.seenPayloadEnvelopeInputCache.get(parentRootHex);

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.

changing this to "getOrReload" would make the consumers to change accordingly
but we don't really need it, it's kind of dead code
we checked this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex) above
so if it reaches here, it really means "parentPayload"

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.

Agreed, this matches what I flagged in the review: that trailing cache.get(parentRootHex) mismatch check was redundant, so dropping it is fine. By the time control reaches the parentPayload return, the hasPayloadHexUnsafe(parentRootHex) guard just above (returning invalidParentPayload) has already caught the revealed-but-mismatched case, so anything past it really is a parent payload. Since the line is gone, .get() vs .getOrReload() there is moot. No change needed.

@twoeths
twoeths requested a review from lodekeeper August 24, 2026 11:30

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

Approving. The core fix correctly closes the #9489 range-sync eviction hole — range sync now resolves in-batch payload inputs from the batch-local payloadEnvelopes map, so it no longer depends on the shared cache for correctness. CI is green and the seenTimestampSec float nit is addressed in both reloadFromDb and the anchor-state path.

The two remaining items from my earlier review are non-blocking and fine as follow-ups if at all: (1) the pruneToMaxSize comment could name the UnknownBlockInput fallthrough as the actual recovery path (the gossip handlers themselves IGNORE on miss), and (2) the optional post-await recheck in reloadFromDb — which we agreed is effectively unreachable given the reloading dedup + fork-choice gate. Nice work bounding this without reintroducing the #9489 regression.

@nflaig nflaig left a comment

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.

Looks pretty good, left some comments, but non-blocking to merge this

Comment on lines +224 to +227
const forkName = this.config.getForkName(block.message.slot);
if (!isForkPostGloas(forkName)) {
return undefined;
}

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.

how can this be reached pre-gloas?

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.

This path doesn't read from the seen cache: reloadFromDb loads the block straight from seenBlockInputCache/db.block by whatever root getOrReload was called with, and only computes the fork after loading (getForkName(block.message.slot)). So a pre-gloas block reaches this guard whenever a caller passes such a root, and the guard then bails. Whether any real getOrReload caller actually passes a pre-gloas root vs this being purely defensive is your call @twoeths.

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.

there should be no pre-gloas flow calling this
it's still nice to have this check through, we need to query db block for subsequent statements anyway

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.

Makes sense, thanks @twoeths. Agreed, worth keeping as a cheap defensive guard since the db block load happens for the following statements anyway.

expect(db.block.get).not.toHaveBeenCalled();
});

it("reconstructs the shell from the hot db when the gloas block is in fork choice", async () => {

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.

"the shell" what does this mean?

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.

"shell" = the PayloadEnvelopeInput that reloadFromDb rebuilds from just the block via createFromBlock, i.e. the block skeleton with no execution payload envelope or data columns attached yet. This test checks that a cache miss on a known gloas block reconstructs that block-only entry from the hot db, so callers can then fill in the envelope/columns.

/**
* Backstop cap for entries pruneBelowParent/pruneFinalized can't reach (non-canonical forks,
* EMPTY/PENDING entries). Evicts by INSERTION ORDER — the Map iterates oldest-inserted first — so a
* just-reloaded old-slot entry (set at the back) survives while genuinely stale forks/shells are shed.

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.

"forks/shells are shed" I find the wording a bit strange

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.

Wording's yours to tweak @twoeths, but for context: "forks/shells" = the two kinds of entry this cap sheds first, stale non-canonical fork entries and the reconstructed block-only "shell" entries (same "shell" as the test thread). Something like "stale non-canonical or reconstructed-only entries are evicted first" might read clearer.

@twoeths
twoeths merged commit 7aa8c9c into unstable Aug 25, 2026
24 of 25 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Lodestar Team Coordination Aug 25, 2026
@twoeths
twoeths deleted the te/bound_payload_envelope_seen_cache branch August 25, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants