Skip to content

fix: drop payloadStatus from Checkpoint - #9259

Merged
nflaig merged 5 commits into
unstablefrom
te/drop_checkpoint_payload_status
Apr 23, 2026
Merged

fix: drop payloadStatus from Checkpoint#9259
nflaig merged 5 commits into
unstablefrom
te/drop_checkpoint_payload_status

Conversation

@twoeths

@twoeths twoeths commented Apr 22, 2026

Copy link
Copy Markdown
Member

Motivation

Drop payloadStatus from Checkpoint.

Description

Fork-choice:

  • Drop CheckpointWithPayloadStatus, justifiedPayloadStatus /
    finalizedPayloadStatus store fields, getCheckpointPayloadStatus(), and
    toCheckpointWithPayload().
  • Rename CheckpointWithPayloadAndBalanceCheckpointWithBalance
    (+ CheckpointWithPayloadAndTotalBalanceCheckpointWithTotalBalance).
  • Add getAllAncestorAndNonAncestorBlocksDefaultStatus and
    getBlockHexDefaultStatus — archive-side helpers that resolve the canonical
    variant (FULL pre-Gloas, PENDING for Gloas) without callers needing to pass a
    PayloadStatus.
  • getAllAncestorAndNonAncestorBlocks now also returns
    previousFinalizedFull: ProtoBlock | undefined — the FULL variant of the
    previous finalized block when its execution payload has been received.

Archive:

  • archiveBlocks now archives a post-Gloas finalized block across two runs:
    • first run: SignedBeaconBlock ships (payload may still be PENDING/EMPTY).
    • next run: once the previous finalized block's FULL variant is available,
      its execution payload envelope and data column sidecars ship.
  • Blob sidecars are unaffected (pre-Gloas, arrive with the block).
  • Existing chain query getters (chain.getBlockByRoot, getBlobSidecars,
    getDataColumnSidecars, getExecutionPayloadEnvelope, …) already fall back
    hot→cold, so cross-run intermediate states are transparently handled.

AI Assistance Disclosure

Used Claude Code to refactor the archiver and fork-choice helper, and to draft
tests and comments. Design and review by a human.

// whose SignedBeaconBlock was archived on the previous run). `previousFinalizedFull` is the FULL
// variant of the previous finalized block — present once its execution payload has been received.
//
// By design, a post-Gloas finalized block is archived across two runs because finalization does

@twoeths twoeths Apr 22, 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.

the main thing to review

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 the 2-stage archive + filter approach against pre-gloas / post-gloas.

Pre-gloas path is clean: the FULL filter is a no-op (pre-gloas blocks only have a FULL variant), so migrateDataColumnSidecarsFromHotToColdDb and migrateExecutionPayloadEnvelopesFromHotToColdDb behave the same as before. Boundary block inclusion in finalizedCanonicalBlocks is handled by the getBinary → null ⇒ skip path in migrateBlocksFromHotToColdDb, so re-processing the already-archived previous finalized block doesn't throw — good.

Real gap in the post-gloas path (details in the codex P1 thread on this file): if the newly-finalized block was imported while only its parent's EMPTY variant existed, the ancestor walk during the next-run archive returns EMPTY for that boundary even after onExecutionPayload has added the FULL sibling — because onBlock fixes the child's parent pointer once and never rewires it. The block.payloadStatus === PayloadStatus.FULL filter then drops the boundary permanently, and that specific finalized block's payload envelope + data columns never ship to cold DB.

Options that fit the current shape:

  1. For the boundary block specifically, resolve FULL at root level (forkChoice.hasPayload(root) / protoArray.getNode(root, PayloadStatus.FULL)) before deciding to migrate. Narrow fix, no forkchoice API change.
  2. Your plan 3c — re-process the previous finalized boundary in every archive run and lean on the same null ⇒ skip idempotency for payload/columns that we already have for block data. Matches @wemeetagain's "no special case" ask and covers the EMPTY-boundary case for free.

I'd lean toward 3c — it's uniform across block/payload/columns, doesn't need a new API, and removes the subtle "boundary-was-EMPTY-extended" foot-gun.

Also flagged codex's other P1 about getFinalizedBlock() / getJustifiedBlock() defaulting to PENDING — that's independent of the archive path but regresses FCU finalizedBlockHash / safeBlockHash once the FULL sibling exists for the checkpoint. Reply on that one at forkChoice.ts:1090.

Unrelated: the benchmark-alert "Full columns - reconstruct half of the blobs out of 10" 3.13× regression looks like noise — this PR doesn't touch blob / column reconstruction code (git diff origin/unstable shows no changes in those paths).

@twoeths
twoeths marked this pull request as ready for review April 22, 2026 16:26
@twoeths
twoeths requested a review from a team as a code owner April 22, 2026 16:26

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the fork choice and archiver logic by replacing CheckpointWithPayloadStatus with CheckpointWithHex, removing explicit payload status tracking from checkpoints. It introduces 'DefaultStatus' helper methods in the fork choice to simplify traversals and updates the archiver to support a two-stage migration for post-Gloas blocks, ensuring execution payloads and data columns are archived once their FULL variant becomes available. I have no feedback to provide.

@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Performance Report

✔️ no performance regression detected

Full benchmark results
Benchmark suite Current: c40afb5 Previous: e5b1322 Ratio
getPubkeys - index2pubkey - req 1000 vs - 250000 vc 903.50 us/op 884.62 us/op 1.02
getPubkeys - validatorsArr - req 1000 vs - 250000 vc 41.660 us/op 40.898 us/op 1.02
BLS verify - blst 688.90 us/op 740.88 us/op 0.93
BLS verifyMultipleSignatures 3 - blst 1.2928 ms/op 1.3952 ms/op 0.93
BLS verifyMultipleSignatures 8 - blst 2.0569 ms/op 2.2259 ms/op 0.92
BLS verifyMultipleSignatures 32 - blst 6.4545 ms/op 7.0466 ms/op 0.92
BLS verifyMultipleSignatures 64 - blst 12.652 ms/op 13.503 ms/op 0.94
BLS verifyMultipleSignatures 128 - blst 24.767 ms/op 26.627 ms/op 0.93
BLS deserializing 10000 signatures 618.10 ms/op 637.79 ms/op 0.97
BLS deserializing 100000 signatures 6.3903 s/op 6.4404 s/op 0.99
BLS verifyMultipleSignatures - same message - 3 - blst 797.84 us/op 803.79 us/op 0.99
BLS verifyMultipleSignatures - same message - 8 - blst 925.40 us/op 913.55 us/op 1.01
BLS verifyMultipleSignatures - same message - 32 - blst 1.5425 ms/op 1.5459 ms/op 1.00
BLS verifyMultipleSignatures - same message - 64 - blst 2.3723 ms/op 2.4316 ms/op 0.98
BLS verifyMultipleSignatures - same message - 128 - blst 4.0468 ms/op 4.1220 ms/op 0.98
BLS aggregatePubkeys 32 - blst 17.862 us/op 17.779 us/op 1.00
BLS aggregatePubkeys 128 - blst 63.799 us/op 63.776 us/op 1.00
getSlashingsAndExits - default max 46.116 us/op 48.623 us/op 0.95
getSlashingsAndExits - 2k 348.51 us/op 345.08 us/op 1.01
proposeBlockBody type=full, size=empty 775.55 us/op 588.23 us/op 1.32
isKnown best case - 1 super set check 165.00 ns/op 179.00 ns/op 0.92
isKnown normal case - 2 super set checks 164.00 ns/op 181.00 ns/op 0.91
isKnown worse case - 16 super set checks 168.00 ns/op 176.00 ns/op 0.95
validate api signedAggregateAndProof - struct 1.4777 ms/op 1.5652 ms/op 0.94
validate gossip signedAggregateAndProof - struct 1.4874 ms/op 1.5695 ms/op 0.95
batch validate gossip attestation - vc 640000 - chunk 32 105.84 us/op 109.50 us/op 0.97
batch validate gossip attestation - vc 640000 - chunk 64 94.963 us/op 99.115 us/op 0.96
batch validate gossip attestation - vc 640000 - chunk 128 89.062 us/op 89.974 us/op 0.99
batch validate gossip attestation - vc 640000 - chunk 256 86.711 us/op 88.213 us/op 0.98
bytes32 toHexString 285.00 ns/op 291.00 ns/op 0.98
bytes32 Buffer.toString(hex) 184.00 ns/op 176.00 ns/op 1.05
bytes32 Buffer.toString(hex) from Uint8Array 251.00 ns/op 248.00 ns/op 1.01
bytes32 Buffer.toString(hex) + 0x 190.00 ns/op 179.00 ns/op 1.06
Return object 10000 times 0.21390 ns/op 0.21840 ns/op 0.98
Throw Error 10000 times 3.2914 us/op 3.4935 us/op 0.94
toHex 90.121 ns/op 97.384 ns/op 0.93
Buffer.from 82.813 ns/op 89.385 ns/op 0.93
shared Buffer 55.587 ns/op 60.411 ns/op 0.92
fastMsgIdFn sha256 / 200 bytes 1.4730 us/op 1.5070 us/op 0.98
fastMsgIdFn h32 xxhash / 200 bytes 155.00 ns/op 155.00 ns/op 1.00
fastMsgIdFn h64 xxhash / 200 bytes 200.00 ns/op 215.00 ns/op 0.93
fastMsgIdFn sha256 / 1000 bytes 4.5730 us/op 4.8270 us/op 0.95
fastMsgIdFn h32 xxhash / 1000 bytes 238.00 ns/op 244.00 ns/op 0.98
fastMsgIdFn h64 xxhash / 1000 bytes 247.00 ns/op 254.00 ns/op 0.97
fastMsgIdFn sha256 / 10000 bytes 40.460 us/op 42.629 us/op 0.95
fastMsgIdFn h32 xxhash / 10000 bytes 1.1990 us/op 1.2680 us/op 0.95
fastMsgIdFn h64 xxhash / 10000 bytes 776.00 ns/op 821.00 ns/op 0.95
send data - 1000 256B messages 4.4891 ms/op 5.0576 ms/op 0.89
send data - 1000 512B messages 4.8960 ms/op 4.2062 ms/op 1.16
send data - 1000 1024B messages 4.7263 ms/op 4.8729 ms/op 0.97
send data - 1000 1200B messages 5.6475 ms/op 4.7226 ms/op 1.20
send data - 1000 2048B messages 6.2124 ms/op 4.7047 ms/op 1.32
send data - 1000 4096B messages 5.8200 ms/op 6.1956 ms/op 0.94
send data - 1000 16384B messages 24.511 ms/op 18.298 ms/op 1.34
send data - 1000 65536B messages 137.26 ms/op 197.97 ms/op 0.69
enrSubnets - fastDeserialize 64 bits 707.00 ns/op 736.00 ns/op 0.96
enrSubnets - ssz BitVector 64 bits 270.00 ns/op 264.00 ns/op 1.02
enrSubnets - fastDeserialize 4 bits 104.00 ns/op 101.00 ns/op 1.03
enrSubnets - ssz BitVector 4 bits 278.00 ns/op 267.00 ns/op 1.04
prioritizePeers score -10:0 att 32-0.1 sync 2-0 203.53 us/op 225.20 us/op 0.90
prioritizePeers score 0:0 att 32-0.25 sync 2-0.25 234.11 us/op 247.81 us/op 0.94
prioritizePeers score 0:0 att 32-0.5 sync 2-0.5 340.57 us/op 357.87 us/op 0.95
prioritizePeers score 0:0 att 64-0.75 sync 4-0.75 595.33 us/op 620.61 us/op 0.96
prioritizePeers score 0:0 att 64-1 sync 4-1 710.66 us/op 749.43 us/op 0.95
array of 16000 items push then shift 1.2667 us/op 1.2303 us/op 1.03
LinkedList of 16000 items push then shift 7.8600 ns/op 7.2910 ns/op 1.08
array of 16000 items push then pop 69.030 ns/op 67.259 ns/op 1.03
LinkedList of 16000 items push then pop 6.0360 ns/op 5.8630 ns/op 1.03
array of 24000 items push then shift 1.8380 us/op 1.8130 us/op 1.01
LinkedList of 24000 items push then shift 8.1960 ns/op 6.9500 ns/op 1.18
array of 24000 items push then pop 99.724 ns/op 93.565 ns/op 1.07
LinkedList of 24000 items push then pop 6.5150 ns/op 5.7710 ns/op 1.13
intersect bitArray bitLen 8 4.6450 ns/op 4.6470 ns/op 1.00
intersect array and set length 8 29.510 ns/op 29.674 ns/op 0.99
intersect bitArray bitLen 128 24.201 ns/op 24.434 ns/op 0.99
intersect array and set length 128 507.56 ns/op 509.00 ns/op 1.00
bitArray.getTrueBitIndexes() bitLen 128 958.00 ns/op 1.0490 us/op 0.91
bitArray.getTrueBitIndexes() bitLen 248 1.7230 us/op 1.7650 us/op 0.98
bitArray.getTrueBitIndexes() bitLen 512 4.0360 us/op 3.6080 us/op 1.12
Full columns - reconstruct all 6 blobs 189.97 us/op 111.71 us/op 1.70
Full columns - reconstruct half of the blobs out of 6 80.487 us/op 63.285 us/op 1.27
Full columns - reconstruct single blob out of 6 32.724 us/op 31.617 us/op 1.04
Half columns - reconstruct all 6 blobs 384.50 ms/op 394.40 ms/op 0.97
Half columns - reconstruct half of the blobs out of 6 197.07 ms/op 198.06 ms/op 0.99
Half columns - reconstruct single blob out of 6 72.049 ms/op 71.029 ms/op 1.01
Full columns - reconstruct all 10 blobs 221.25 us/op 183.68 us/op 1.20
Full columns - reconstruct half of the blobs out of 10 113.69 us/op 91.836 us/op 1.24
Full columns - reconstruct single blob out of 10 33.386 us/op 29.130 us/op 1.15
Half columns - reconstruct all 10 blobs 665.73 ms/op 650.06 ms/op 1.02
Half columns - reconstruct half of the blobs out of 10 337.29 ms/op 328.72 ms/op 1.03
Half columns - reconstruct single blob out of 10 70.525 ms/op 70.898 ms/op 0.99
Full columns - reconstruct all 20 blobs 2.1195 ms/op 1.3296 ms/op 1.59
Full columns - reconstruct half of the blobs out of 20 236.53 us/op 172.58 us/op 1.37
Full columns - reconstruct single blob out of 20 29.444 us/op 28.397 us/op 1.04
Half columns - reconstruct all 20 blobs 1.3250 s/op 1.2988 s/op 1.02
Half columns - reconstruct half of the blobs out of 20 668.26 ms/op 654.12 ms/op 1.02
Half columns - reconstruct single blob out of 20 72.126 ms/op 72.445 ms/op 1.00
Set add up to 64 items then delete first 2.0625 us/op 2.6123 us/op 0.79
OrderedSet add up to 64 items then delete first 3.3151 us/op 3.3717 us/op 0.98
Set add up to 64 items then delete last 2.1600 us/op 2.3861 us/op 0.91
OrderedSet add up to 64 items then delete last 3.4731 us/op 3.3117 us/op 1.05
Set add up to 64 items then delete middle 2.2586 us/op 2.0149 us/op 1.12
OrderedSet add up to 64 items then delete middle 5.4464 us/op 4.5968 us/op 1.18
Set add up to 128 items then delete first 5.0056 us/op 5.0285 us/op 1.00
OrderedSet add up to 128 items then delete first 7.3139 us/op 7.2610 us/op 1.01
Set add up to 128 items then delete last 4.9202 us/op 4.1472 us/op 1.19
OrderedSet add up to 128 items then delete last 6.9561 us/op 5.8380 us/op 1.19
Set add up to 128 items then delete middle 4.7527 us/op 3.9227 us/op 1.21
OrderedSet add up to 128 items then delete middle 14.020 us/op 11.776 us/op 1.19
Set add up to 256 items then delete first 8.8923 us/op 7.9861 us/op 1.11
OrderedSet add up to 256 items then delete first 14.102 us/op 12.270 us/op 1.15
Set add up to 256 items then delete last 8.9638 us/op 7.7501 us/op 1.16
OrderedSet add up to 256 items then delete last 13.396 us/op 11.509 us/op 1.16
Set add up to 256 items then delete middle 10.085 us/op 7.6877 us/op 1.31
OrderedSet add up to 256 items then delete middle 42.124 us/op 34.953 us/op 1.21
pass gossip attestations to forkchoice per slot 2.7189 ms/op 2.5418 ms/op 1.07
forkChoice updateHead vc 100000 bc 64 eq 0 402.62 us/op 386.88 us/op 1.04
forkChoice updateHead vc 600000 bc 64 eq 0 2.4220 ms/op 2.4257 ms/op 1.00
forkChoice updateHead vc 1000000 bc 64 eq 0 3.9999 ms/op 3.8165 ms/op 1.05
forkChoice updateHead vc 600000 bc 320 eq 0 2.4586 ms/op 2.2406 ms/op 1.10
forkChoice updateHead vc 600000 bc 1200 eq 0 2.5228 ms/op 2.3444 ms/op 1.08
forkChoice updateHead vc 600000 bc 7200 eq 0 4.0657 ms/op 2.8336 ms/op 1.43
forkChoice updateHead vc 600000 bc 64 eq 1000 3.0118 ms/op 2.8567 ms/op 1.05
forkChoice updateHead vc 600000 bc 64 eq 10000 3.1224 ms/op 2.9878 ms/op 1.05
forkChoice updateHead vc 600000 bc 64 eq 300000 7.7929 ms/op 6.8184 ms/op 1.14
computeDeltas 1400000 validators 0% inactive 13.039 ms/op 12.553 ms/op 1.04
computeDeltas 1400000 validators 10% inactive 12.051 ms/op 11.423 ms/op 1.05
computeDeltas 1400000 validators 20% inactive 11.038 ms/op 10.413 ms/op 1.06
computeDeltas 1400000 validators 50% inactive 8.5592 ms/op 8.0609 ms/op 1.06
computeDeltas 2100000 validators 0% inactive 19.429 ms/op 18.689 ms/op 1.04
computeDeltas 2100000 validators 10% inactive 18.215 ms/op 17.338 ms/op 1.05
computeDeltas 2100000 validators 20% inactive 16.578 ms/op 15.805 ms/op 1.05
computeDeltas 2100000 validators 50% inactive 12.860 ms/op 9.2803 ms/op 1.39
altair processAttestation - 250000 vs - 7PWei normalcase 2.6560 ms/op 2.4239 ms/op 1.10
altair processAttestation - 250000 vs - 7PWei worstcase 4.0291 ms/op 3.3592 ms/op 1.20
altair processAttestation - setStatus - 1/6 committees join 118.78 us/op 105.56 us/op 1.13
altair processAttestation - setStatus - 1/3 committees join 211.32 us/op 216.81 us/op 0.97
altair processAttestation - setStatus - 1/2 committees join 292.81 us/op 295.81 us/op 0.99
altair processAttestation - setStatus - 2/3 committees join 384.63 us/op 380.63 us/op 1.01
altair processAttestation - setStatus - 4/5 committees join 545.02 us/op 530.54 us/op 1.03
altair processAttestation - setStatus - 100% committees join 631.50 us/op 610.36 us/op 1.03
altair processBlock - 250000 vs - 7PWei normalcase 4.5717 ms/op 3.9094 ms/op 1.17
altair processBlock - 250000 vs - 7PWei normalcase hashState 15.213 ms/op 18.491 ms/op 0.82
altair processBlock - 250000 vs - 7PWei worstcase 23.321 ms/op 20.979 ms/op 1.11
altair processBlock - 250000 vs - 7PWei worstcase hashState 47.173 ms/op 44.350 ms/op 1.06
phase0 processBlock - 250000 vs - 7PWei normalcase 1.3518 ms/op 1.6960 ms/op 0.80
phase0 processBlock - 250000 vs - 7PWei worstcase 18.170 ms/op 18.273 ms/op 0.99
altair processEth1Data - 250000 vs - 7PWei normalcase 300.21 us/op 297.58 us/op 1.01
getExpectedWithdrawals 250000 eb:1,eth1:1,we:0,wn:0,smpl:16 3.7520 us/op 3.9050 us/op 0.96
getExpectedWithdrawals 250000 eb:0.95,eth1:0.1,we:0.05,wn:0,smpl:220 20.879 us/op 22.271 us/op 0.94
getExpectedWithdrawals 250000 eb:0.95,eth1:0.3,we:0.05,wn:0,smpl:43 6.6930 us/op 5.8860 us/op 1.14
getExpectedWithdrawals 250000 eb:0.95,eth1:0.7,we:0.05,wn:0,smpl:19 4.0190 us/op 3.5180 us/op 1.14
getExpectedWithdrawals 250000 eb:0.1,eth1:0.1,we:0,wn:0,smpl:1021 92.442 us/op 96.707 us/op 0.96
getExpectedWithdrawals 250000 eb:0.03,eth1:0.03,we:0,wn:0,smpl:11778 1.3341 ms/op 1.4002 ms/op 0.95
getExpectedWithdrawals 250000 eb:0.01,eth1:0.01,we:0,wn:0,smpl:16384 1.8479 ms/op 1.8516 ms/op 1.00
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,smpl:16384 1.9042 ms/op 1.8003 ms/op 1.06
getExpectedWithdrawals 250000 eb:0,eth1:0,we:0,wn:0,nocache,smpl:16384 7.1210 ms/op 4.0429 ms/op 1.76
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,smpl:16384 2.1694 ms/op 2.0826 ms/op 1.04
getExpectedWithdrawals 250000 eb:0,eth1:1,we:0,wn:0,nocache,smpl:16384 8.4092 ms/op 6.1995 ms/op 1.36
Tree 40 250000 create 342.97 ms/op 371.37 ms/op 0.92
Tree 40 250000 get(125000) 90.003 ns/op 94.053 ns/op 0.96
Tree 40 250000 set(125000) 994.99 ns/op 1.0278 us/op 0.97
Tree 40 250000 toArray() 16.662 ms/op 17.088 ms/op 0.98
Tree 40 250000 iterate all - toArray() + loop 16.230 ms/op 17.734 ms/op 0.92
Tree 40 250000 iterate all - get(i) 39.348 ms/op 41.055 ms/op 0.96
Array 250000 create 2.1810 ms/op 2.3244 ms/op 0.94
Array 250000 clone - spread 666.79 us/op 713.85 us/op 0.93
Array 250000 get(125000) 0.29700 ns/op 0.29700 ns/op 1.00
Array 250000 set(125000) 0.29100 ns/op 0.30300 ns/op 0.96
Array 250000 iterate all - loop 57.332 us/op 58.895 us/op 0.97
phase0 afterProcessEpoch - 250000 vs - 7PWei 61.721 ms/op 41.873 ms/op 1.47
Array.fill - length 1000000 2.1831 ms/op 3.1913 ms/op 0.68
Array push - length 1000000 9.3347 ms/op 11.016 ms/op 0.85
Array.get 0.20381 ns/op 0.21113 ns/op 0.97
Uint8Array.get 0.23363 ns/op 0.24486 ns/op 0.95
phase0 beforeProcessEpoch - 250000 vs - 7PWei 20.782 ms/op 15.281 ms/op 1.36
altair processEpoch - mainnet_e81889 310.27 ms/op 354.05 ms/op 0.88
mainnet_e81889 - altair beforeProcessEpoch 42.744 ms/op 17.752 ms/op 2.41
mainnet_e81889 - altair processJustificationAndFinalization 7.7130 us/op 7.4060 us/op 1.04
mainnet_e81889 - altair processInactivityUpdates 7.7619 ms/op 4.4517 ms/op 1.74
mainnet_e81889 - altair processRewardsAndPenalties 23.645 ms/op 17.750 ms/op 1.33
mainnet_e81889 - altair processRegistryUpdates 622.00 ns/op 564.00 ns/op 1.10
mainnet_e81889 - altair processSlashings 151.00 ns/op 150.00 ns/op 1.01
mainnet_e81889 - altair processEth1DataReset 149.00 ns/op 144.00 ns/op 1.03
mainnet_e81889 - altair processEffectiveBalanceUpdates 9.5131 ms/op 1.7623 ms/op 5.40
mainnet_e81889 - altair processSlashingsReset 746.00 ns/op 745.00 ns/op 1.00
mainnet_e81889 - altair processRandaoMixesReset 1.7050 us/op 1.3580 us/op 1.26
mainnet_e81889 - altair processHistoricalRootsUpdate 163.00 ns/op 152.00 ns/op 1.07
mainnet_e81889 - altair processParticipationFlagUpdates 538.00 ns/op 457.00 ns/op 1.18
mainnet_e81889 - altair processSyncCommitteeUpdates 123.00 ns/op 124.00 ns/op 0.99
mainnet_e81889 - altair afterProcessEpoch 42.216 ms/op 42.688 ms/op 0.99
capella processEpoch - mainnet_e217614 935.93 ms/op 1.0434 s/op 0.90
mainnet_e217614 - capella beforeProcessEpoch 84.614 ms/op 67.047 ms/op 1.26
mainnet_e217614 - capella processJustificationAndFinalization 32.912 ms/op 32.804 ms/op 1.00
mainnet_e217614 - capella processInactivityUpdates 20.338 ms/op 17.535 ms/op 1.16
mainnet_e217614 - capella processRewardsAndPenalties 101.63 ms/op 97.455 ms/op 1.04
mainnet_e217614 - capella processRegistryUpdates 4.5530 us/op 4.6010 us/op 0.99
mainnet_e217614 - capella processSlashings 143.00 ns/op 145.00 ns/op 0.99
mainnet_e217614 - capella processEth1DataReset 149.00 ns/op 147.00 ns/op 1.01
mainnet_e217614 - capella processEffectiveBalanceUpdates 20.729 ms/op 15.546 ms/op 1.33
mainnet_e217614 - capella processSlashingsReset 715.00 ns/op 706.00 ns/op 1.01
mainnet_e217614 - capella processRandaoMixesReset 1.5200 us/op 1.4330 us/op 1.06
mainnet_e217614 - capella processHistoricalRootsUpdate 145.00 ns/op 148.00 ns/op 0.98
mainnet_e217614 - capella processParticipationFlagUpdates 454.00 ns/op 452.00 ns/op 1.00
mainnet_e217614 - capella afterProcessEpoch 107.33 ms/op 108.15 ms/op 0.99
phase0 processEpoch - mainnet_e58758 331.24 ms/op 333.67 ms/op 0.99
mainnet_e58758 - phase0 beforeProcessEpoch 71.402 ms/op 64.214 ms/op 1.11
mainnet_e58758 - phase0 processJustificationAndFinalization 6.2840 us/op 5.8850 us/op 1.07
mainnet_e58758 - phase0 processRewardsAndPenalties 17.344 ms/op 15.957 ms/op 1.09
mainnet_e58758 - phase0 processRegistryUpdates 2.2870 us/op 2.3200 us/op 0.99
mainnet_e58758 - phase0 processSlashings 148.00 ns/op 147.00 ns/op 1.01
mainnet_e58758 - phase0 processEth1DataReset 145.00 ns/op 144.00 ns/op 1.01
mainnet_e58758 - phase0 processEffectiveBalanceUpdates 816.60 us/op 935.41 us/op 0.87
mainnet_e58758 - phase0 processSlashingsReset 906.00 ns/op 922.00 ns/op 0.98
mainnet_e58758 - phase0 processRandaoMixesReset 2.2350 us/op 1.4450 us/op 1.55
mainnet_e58758 - phase0 processHistoricalRootsUpdate 152.00 ns/op 234.00 ns/op 0.65
mainnet_e58758 - phase0 processParticipationRecordUpdates 1.0980 us/op 1.3030 us/op 0.84
mainnet_e58758 - phase0 afterProcessEpoch 34.557 ms/op 35.695 ms/op 0.97
phase0 processEffectiveBalanceUpdates - 250000 normalcase 1.1393 ms/op 1.1275 ms/op 1.01
phase0 processEffectiveBalanceUpdates - 250000 worstcase 0.5 1.5463 ms/op 1.9196 ms/op 0.81
altair processInactivityUpdates - 250000 normalcase 14.230 ms/op 11.270 ms/op 1.26
altair processInactivityUpdates - 250000 worstcase 13.555 ms/op 10.944 ms/op 1.24
phase0 processRegistryUpdates - 250000 normalcase 3.8930 us/op 2.0600 us/op 1.89
phase0 processRegistryUpdates - 250000 badcase_full_deposits 151.60 us/op 148.07 us/op 1.02
phase0 processRegistryUpdates - 250000 worstcase 0.5 65.361 ms/op 62.592 ms/op 1.04
altair processRewardsAndPenalties - 250000 normalcase 17.062 ms/op 16.250 ms/op 1.05
altair processRewardsAndPenalties - 250000 worstcase 16.769 ms/op 15.009 ms/op 1.12
phase0 getAttestationDeltas - 250000 normalcase 5.4898 ms/op 5.4482 ms/op 1.01
phase0 getAttestationDeltas - 250000 worstcase 8.5908 ms/op 5.4543 ms/op 1.58
phase0 processSlashings - 250000 worstcase 63.472 us/op 61.331 us/op 1.03
altair processSyncCommitteeUpdates - 250000 12.729 ms/op 10.360 ms/op 1.23
BeaconState.hashTreeRoot - No change 206.00 ns/op 204.00 ns/op 1.01
BeaconState.hashTreeRoot - 1 full validator 91.505 us/op 93.346 us/op 0.98
BeaconState.hashTreeRoot - 32 full validator 929.61 us/op 1.1677 ms/op 0.80
BeaconState.hashTreeRoot - 512 full validator 9.6307 ms/op 6.8863 ms/op 1.40
BeaconState.hashTreeRoot - 1 validator.effectiveBalance 116.05 us/op 111.88 us/op 1.04
BeaconState.hashTreeRoot - 32 validator.effectiveBalance 1.5778 ms/op 1.6463 ms/op 0.96
BeaconState.hashTreeRoot - 512 validator.effectiveBalance 22.417 ms/op 27.032 ms/op 0.83
BeaconState.hashTreeRoot - 1 balances 88.920 us/op 89.607 us/op 0.99
BeaconState.hashTreeRoot - 32 balances 836.87 us/op 926.25 us/op 0.90
BeaconState.hashTreeRoot - 512 balances 6.9778 ms/op 6.2360 ms/op 1.12
BeaconState.hashTreeRoot - 250000 balances 141.98 ms/op 176.36 ms/op 0.81
aggregationBits - 2048 els - zipIndexesInBitList 19.640 us/op 19.454 us/op 1.01
regular array get 100000 times 22.785 us/op 23.140 us/op 0.98
wrappedArray get 100000 times 22.780 us/op 23.127 us/op 0.98
arrayWithProxy get 100000 times 17.756 ms/op 10.392 ms/op 1.71
ssz.Root.equals 21.306 ns/op 21.929 ns/op 0.97
byteArrayEquals 21.470 ns/op 21.675 ns/op 0.99
Buffer.compare 8.8650 ns/op 10.079 ns/op 0.88
processSlot - 1 slots 10.774 us/op 9.7970 us/op 1.10
processSlot - 32 slots 2.2781 ms/op 2.4579 ms/op 0.93
getEffectiveBalanceIncrementsZeroInactive - 250000 vs - 7PWei 5.7034 ms/op 2.9664 ms/op 1.92
getCommitteeAssignments - req 1 vs - 250000 vc 1.7059 ms/op 1.6730 ms/op 1.02
getCommitteeAssignments - req 100 vs - 250000 vc 3.4641 ms/op 3.4229 ms/op 1.01
getCommitteeAssignments - req 1000 vs - 250000 vc 3.7205 ms/op 3.6725 ms/op 1.01
findModifiedValidators - 10000 modified validators 849.18 ms/op 708.10 ms/op 1.20
findModifiedValidators - 1000 modified validators 491.59 ms/op 461.30 ms/op 1.07
findModifiedValidators - 100 modified validators 261.34 ms/op 296.06 ms/op 0.88
findModifiedValidators - 10 modified validators 162.89 ms/op 165.03 ms/op 0.99
findModifiedValidators - 1 modified validators 161.92 ms/op 168.94 ms/op 0.96
findModifiedValidators - no difference 172.76 ms/op 163.80 ms/op 1.05
migrate state 1500000 validators, 3400 modified, 2000 new 2.9030 s/op 3.5021 s/op 0.83
RootCache.getBlockRootAtSlot - 250000 vs - 7PWei 3.6300 ns/op 3.9300 ns/op 0.92
state getBlockRootAtSlot - 250000 vs - 7PWei 350.07 ns/op 388.14 ns/op 0.90
computeProposerIndex 100000 validators 1.3075 ms/op 1.3537 ms/op 0.97
getNextSyncCommitteeIndices 1000 validators 2.8399 ms/op 2.9211 ms/op 0.97
getNextSyncCommitteeIndices 10000 validators 25.078 ms/op 25.676 ms/op 0.98
getNextSyncCommitteeIndices 100000 validators 86.115 ms/op 90.095 ms/op 0.96
computeProposers - vc 250000 550.16 us/op 555.36 us/op 0.99
computeEpochShuffling - vc 250000 39.040 ms/op 39.946 ms/op 0.98
getNextSyncCommittee - vc 250000 9.3015 ms/op 9.5623 ms/op 0.97
nodejs block root to RootHex using toHex 93.364 ns/op 98.179 ns/op 0.95
nodejs block root to RootHex using toRootHex 55.286 ns/op 63.089 ns/op 0.88
nodejs fromHex(blob) 729.54 us/op 834.82 us/op 0.87
nodejs fromHexInto(blob) 618.64 us/op 642.45 us/op 0.96
nodejs block root to RootHex using the deprecated toHexString 467.19 ns/op 488.36 ns/op 0.96
nodejs byteArrayEquals 32 bytes (block root) 25.749 ns/op 26.696 ns/op 0.96
nodejs byteArrayEquals 48 bytes (pubkey) 37.437 ns/op 38.382 ns/op 0.98
nodejs byteArrayEquals 96 bytes (signature) 34.124 ns/op 37.340 ns/op 0.91
nodejs byteArrayEquals 1024 bytes 40.751 ns/op 44.255 ns/op 0.92
nodejs byteArrayEquals 131072 bytes (blob) 1.7167 us/op 1.9334 us/op 0.89
browser block root to RootHex using toHex 142.23 ns/op 146.50 ns/op 0.97
browser block root to RootHex using toRootHex 128.42 ns/op 127.48 ns/op 1.01
browser fromHex(blob) 1.5633 ms/op 1.6437 ms/op 0.95
browser fromHexInto(blob) 625.93 us/op 641.87 us/op 0.98
browser block root to RootHex using the deprecated toHexString 336.58 ns/op 344.53 ns/op 0.98
browser byteArrayEquals 32 bytes (block root) 28.280 ns/op 28.066 ns/op 1.01
browser byteArrayEquals 48 bytes (pubkey) 40.325 ns/op 39.636 ns/op 1.02
browser byteArrayEquals 96 bytes (signature) 75.853 ns/op 74.152 ns/op 1.02
browser byteArrayEquals 1024 bytes 754.55 ns/op 757.47 ns/op 1.00
browser byteArrayEquals 131072 bytes (blob) 94.910 us/op 95.372 us/op 1.00

by benchmarkbot/action

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

ℹ️ 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 thread packages/fork-choice/src/forkChoice/forkChoice.ts
Comment thread packages/fork-choice/src/forkChoice/forkChoice.ts Outdated
const {
ancestors: finalizedCanonicalBlocks,
nonAncestors: finalizedNonCanonicalBlocks,
previousFinalizedFull,

@wemeetagain wemeetagain Apr 22, 2026

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.

Why do we need to treat the previousFinalizedFull differently? It looks like fork choice takes off the final entry in ancestors, only for it to be re-added here?

I think ideally there would be no special-casing for finalized payloads. (Anything that is an ancestor of the finalized checkpoint gets archived. Anything that is not an ancestor of the finalized checkpoint does not get archived)

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.

suppose we finalize block slot 32, we move block data from hot db to cold db. But we cannot do the same thing for payload because payload is technically not finalized

after the 1st finalized process, block slot 32 of PENDING/EMPTY/FULL still stay in forkchoice

1 epoch later, we finalize block slot 64, if it sees 32/FULL as it's ancestor, set previousFinalizedFull, archiveBlocks will migrate payload of block slot 32. If not it will not move there

Why do we need to treat the previousFinalizedFull differently? It looks like fork choice takes off the final entry in ancestors, only for it to be re-added here?

previousFinalizedFull also returns from forkchoice

because the block data was archived/finalized in the last run, but payload was not. There is a long discussion here https://discord.com/channels/595666850260713488/874767108809031740/1490930960236154890

there are some ways to simplify it:

  • 1 - if we do optimistically migrate payload in the 1st run, there is a tiny chance that it's reorged and we serve wrong data for by range request, and get downscore by peers
  • 2 - also delay block data migration to the next run, this makes sure we migrate block data + payload data at the same time. But this is not compliant to what we already have and I'm not sure if it breaks anything.
  • 3 - forkchoice always return all nodes including the previous finalized ProtoBlock to call site, archiveBlocks to decide:
    • a) if pre-gloas, take off the last node, this keeps the old logic. If post-gloas, only archive block, archive payload in the next run (since it's not finalized)
    • b) change the logic from now on for all forks: delay migrating block to the next run so that we always finalize block/payload at the same time
    • c) for block data, process what is finalized for this run and for the previous run too. If data is already migrated, don't throw error

I think ideally there would be no special-casing for finalized payloads. (Anything that is an ancestor of the finalized checkpoint gets archived. Anything that is not an ancestor of the finalized checkpoint does not get archived)

seems like 3c which also makes the most sense to me. Forkchoice taking off the final entry is already not ideal, that's the business logic of call site, just also return the finalized ProtoBlock of the last run there

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 on the direction. @twoeths already dropped the previousFinalizedFull special-case field in 496f21c — the forkchoice API now just returns all ancestors (boundary included) + nonAncestors, and the archiver filters by payloadStatus === PayloadStatus.FULL. No re-adding of the last entry.

Edge case that still bites the post-gloas payload/column path (details in the codex P1 thread on forkChoice.ts:null and my review of archiveBlocks.ts): if the boundary ancestor ends up as EMPTY in the ancestor walk even after its FULL sibling later arrives, the filter drops it and we never migrate its payload/columns. Twoeths' plan 3c from the discord thread (re-process the previous finalized boundary every run, rely on the existing null ⇒ skip idempotency) fits your "no special case" ask naturally and covers that case.

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.

Edge case that still bites the post-gloas payload/column path (details in the codex P1 thread on forkChoice.ts:null and my review of archiveBlocks.ts): if the boundary ancestor ends up as EMPTY in the ancestor walk even after its FULL sibling later arrives, the filter drops it and we never migrate its payload/columns.

@lodekeeper we should not migrate to archive/cold db in that case then, because it's turned out not to be canonical chain. If we persist it, later on a node may send by_range request for us, we may serve it wrong, then peers will downscore us

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.

Right — the by_range serving argument makes it concrete: archiving a non-canonical FULL payload after a canonical EMPTY decision would have us serve wrong data and get downscored. Retracting the concern, the EMPTY-boundary drop is intended behavior.

@twoeths

twoeths commented Apr 23, 2026

Copy link
Copy Markdown
Member Author

@lodekeeper could you review this
mainly the logic in archiveBlocks.ts due to the change from forkchoice getAllAncestorAndNonAncestorBlocksDefaultStatus(): it also return the previous finalized ProtoNode

  • make sure it works the same for pre-gloas
  • post-gloas, given a finalized root of a checkpoint, block data is archived in that same run, but payload + columns are archived in the next run

@twoeths

twoeths commented Apr 23, 2026

Copy link
Copy Markdown
Member Author

I also improved the log to make it easier to debug. This is on mainnet:

verbose: Migrated dataColumnSidecars from hot DB to cold DB currentEpoch=443004, finalizedEpoch=443002, finalizedRoot=0xc9e07bfe11834020643c1ea7dd8ea0c7b309d181cf879f3eedf5c72609fafc9f, migratedEntries=20, slotRange=[14176033-14176037, 14176039-14176044, 14176047, 14176049, 14176053-14176056, 14176058-14176059, 14176061]
debug: DataColumnSidecars in forkchoice but missing in hot db, could be already archived slot=14176032, root=0x5b2998e4939722b9401889965a9cdfd5fd68fd2385b4ad098788646b5b95bd1d

verbose: Migrated blocks from hot DB to cold DB currentEpoch=443004, finalizedEpoch=443002, finalizedRoot=0xc9e07bfe11834020643c1ea7dd8ea0c7b309d181cf879f3eedf5c72609fafc9f, fromSlot=14176064, toSlot=14176032, size=33, migratedEntries=32, slotRange=[14176033-14176064]
debug: Block in forkchoice but missing in hot db, could be already archived slot=14176032, root=0x5b2998e4939722b9401889965a9cdfd5fd68fd2385b4ad098788646b5b95bd1d

it all makes sense to me, given the previous finalized root is at slot 14176032
Block migration:

  • size=33, migratedEntries=32, slotRange=[14176033-14176064]
  • One skipped (boundary): slot=14176032, logged at debug — matches expectation. That block’s SignedBeaconBlock was archived on the previous run; getBinary returned null; the skip path logged and moved on.

Column migration:

  • migratedEntries=20, slotRange=[… 7 gaps inside 14176033-14176061 …]
  • Boundary slot=14176032 again skipped at debug (same root, same reason).

@lodekeeper

Copy link
Copy Markdown
Contributor

Already reviewed at 6a8955c187; posted the archiveBlocks walkthrough at discussion_r3129907873 and the two codex P1 threads (FCU / getFinalizedBlock, archive EMPTY-boundary).

Your mainnet logs confirm the pre-gloas path behaves as expected:

  • Block migration: walk size 33 (32 new + 1 boundary), migratedEntries 32, slotRange [14176033-14176064]. Boundary 14176032 correctly skipped at debug via the getBinary → null ⇒ skip idempotency in migrateBlocksFromHotToColdDb.
  • Data column migration: 20 entries in [14176033-14176061]; the 12 gaps are blob-less blocks (normal — mainnet post-fulu without blobs), plus the boundary 14176032 skipped at debug. migratedEntries 32 blocks − 12 blob-less = 20 columns. Matches.

So pre-gloas is validated by real logs. The one outstanding concern in the post-gloas path is in the codex-P1-2 thread I replied in: the ancestor walk's parent pointer is frozen at onBlock time, so if the new finalized block was imported while only the boundary's EMPTY variant existed, the walk returns EMPTY for that boundary even after onExecutionPayload has added the FULL sibling — the FULL-filter in migrateDataColumnSidecarsFromHotToColdDb / migrateExecutionPayloadEnvelopesFromHotToColdDb then drops it forever. Your plan 3c from the discord thread plugs it naturally by re-processing the previous finalized boundary each run and reusing the same null ⇒ skip idempotency we already have for block data.

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

LGTM — all my concerns (both P1s on forkChoice.ts / archiveBlocks.ts) retracted after @twoeths clarified the Gloas spec invariants: Checkpoint is always PENDING/EMPTY by spec, and the PTC vote is canonical for payload status, so dropping non-canonical FULL siblings at finalized EMPTY boundaries is the intended behavior (and avoids serving wrong data in by_range responses).

Pre-gloas path verified clean — FULL filter is a no-op there. Post-gloas archive flow for blocks / payloads / data-columns walks correctly through the 2-stage (finalize → filter by payloadStatus) approach. Improved migration logs also helpful for debugging on mainnet.

CI all green.

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

based on the review I did with @wemeetagain yesterday, all concerns are addressed

@nflaig
nflaig merged commit ca1fc40 into unstable Apr 23, 2026
23 of 25 checks passed
@nflaig
nflaig deleted the te/drop_checkpoint_payload_status branch April 23, 2026 11:54
@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.53%. Comparing base (e5b1322) to head (6a8955c).
⚠️ Report is 1 commits behind head on unstable.

Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #9259      +/-   ##
============================================
- Coverage     52.54%   52.53%   -0.01%     
============================================
  Files           848      848              
  Lines         61388    61328      -60     
  Branches       4519     4511       -8     
============================================
- Hits          32254    32218      -36     
+ Misses        29069    29045      -24     
  Partials         65       65              
🚀 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.

@wemeetagain

Copy link
Copy Markdown
Member

🎉 This PR is included in v1.43.0 🎉

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants