Repository navigation
fix(storage): grow the beacon vote table exactly instead of doubling - #740
MegaRedHand wants to merge 8 commits into
Conversation
Store::latest_messages is a dense table indexed by validator index, and set_latest_message grew it with resize, which at least doubles the capacity when an index lands past it. A registry that keeps gaining validators crosses that edge soon after the table is first sized, so on the mainnet follower the table had room for about twice the registry and roughly half of its 256 MB was never used. Growth now reserves the new index plus a fixed headroom, so a registry that gains a few validators an epoch still reallocates rarely, but the table stays within the headroom of the registry's size. That frees about 120 MB on the mainnet follower.
🤖 Claude Code ReviewReview of PR 740: grow the latest-message table exactly rather than by doubling The change looks correct and I found nothing blocking. Correctness
Performance
Tests
Style
Approve. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code ReviewI found one correctness concern.
No fork-choice / attestation / justification / SSZ logic changes are in this PR; the production change is a localized memory-usage optimization and otherwise looks reasonable. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Kimi Code ReviewI'll review this PR which optimizes memory growth for the Overall AssessmentThis is a well-scoped optimization PR that addresses a real memory inefficiency. The logic is mostly sound, but I have several concerns ranging from edge cases to code clarity. Detailed Findings1. Potential Panic on Integer Overflow (Lines 4288-4290)if messages.capacity() <= slot {
messages.reserve_exact(slot + 1 + LATEST_MESSAGES_HEADROOM - messages.len());
}Bug: Fix: Use let new_cap = slot
.checked_add(1 + LATEST_MESSAGES_HEADROOM)
.expect("validator index + headroom fits in usize");
messages.reserve_exact(new_cap.saturating_sub(messages.len()));2.
|
| Aspect | Status |
|---|---|
latest_message read path unchanged |
✓ No diff |
None initialization preserves "no vote" semantics |
✓ |
slot bounds check (messages.len() <= slot) preserved |
✓ |
MAX_PLAUSIBLE_VALIDATOR_INDEX still enforced elsewhere |
✓ (unchanged) |
Recommendations Summary
| Priority | Item | Location |
|---|---|---|
| Medium | Fix potential usize overflow in capacity calculation |
store.rs:4289 |
| Low | Clarify reserve_exact + resize interaction with comment |
store.rs:4289-4291 |
| Low | Rename test for accuracy | store.rs:9132 |
| Low | Strengthen test assertions against fragility | store.rs:9154-9159 |
| Trivial | Fix comment grammar ("name" → "reference") | store.rs:4285 |
The core optimization is sound and the test coverage is appreciated. Address the overflow check (Item 1) before merge.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
…740) Store::latest_messages is a dense table indexed by validator index, and set_latest_message grew it with resize, which at least doubles the capacity when an index lands past it. A registry that keeps gaining validators crosses that edge soon after the table is first sized, so on the mainnet follower the table had room for about twice the registry and roughly half of its 256 MB was never used. Growth now reserves the new index plus a fixed headroom, so a registry that gains a few validators an epoch still reallocates rarely, but the table stays within the headroom of the registry's size. That frees about 120 MB on the mainnet follower. Cherry-picked from fix/storage-latest-messages-exact-growth at 83aabfc.
|
@MegaRedHand could you address the Kimi review before this goes in? The items
Items 5 and 7 in Kimi's list (index bound, mutex held across the allocation) |
The sum was safe only because a debug assertion bounds the index, so the line could not be read as correct on its own. Name the target and check the addition.
The reserve above it is what makes the growth exact; without the note, the resize reads as if it could still double the table.
…ises reserve_exact guarantees at least the requested capacity, not exactly it, so the old name claimed more than the code does.
`grown < 2 * sized` held only because of the indices the test picked. Check what the code promises instead: the highest index inside the headroom fits without a reallocation, and growth past it lands at the new index plus the headroom, as an upper bound.
An indexed attestation's attesters are sorted ascending. With exact growth, each index past the headroom reallocated and copied the whole table, so the first aggregate spanning the registry after a start copied a registry-sized table hundreds of times while holding the beacon lock. update_latest_messages now reserves room for the batch's highest index first, so the batch reallocates once at most. Indices are taken by max, not by position, so an unsorted caller is still covered.
|
@pablodeymo all five are in, one commit each:
Kimi's items 5 and 7 stay out, as you said. One more from a second review pass, |
🗒️ Description / Motivation
Store::latest_messages(the beacon fork choice's latest vote per validator) is a dense table indexed by validator index.set_latest_messagegrew it withresize, which at least doubles the capacity whenever an index lands past it. A registry that keeps gaining validators crosses that edge soon after the table is first sized. So on our mainnet follower the table had room for about twice the registry, and roughly half of its 256 MB (heap profile) was never used.Exact growth has its own trap: an indexed attestation's attesters are sorted ascending. Growing the table one index at a time, each index past the headroom reallocated and copied the whole table, so the first aggregate spanning the registry after a start copied a registry-sized table hundreds of times while holding the beacon lock.
What Changed
crates/storage/src/store.rs:LATEST_MESSAGES_HEADROOM(4096) spare entries withreserve_exact, thenresizes as before. The capacity arithmetic is checked, and the growth lives in one helper,reserve_latest_message_slot.Store::reserve_latest_messages(highest): makes room for every index up tohighestin one reallocation at most.crates/blockchain/state_transition/src/beacon/fork_choice.rs:update_latest_messagesreserves for the batch's highest index before recording it. The highest is taken bymax, not by position, so an unsorted caller is still covered.Correctness / Behavior Guarantees
Tests Added / Run
the_latest_message_table_grows_with_headroom_rather_than_doubling:a_reserved_batch_of_ascending_votes_never_moves_the_table: indices spaced wider than the headroom, recorded in ascending order after one reservation, never move the table's buffer or change its capacity.cargo test --profile release-fast -p ethlambda-storage --lib: 212 passed, 1 ignored.cargo test --profile release-fast -p ethlambda-state-transition --lib fork_choice: 93 passed.Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — clean. Rancargo clippy --locked -p ethlambda-storage -p ethlambda-state-transition --all-targets -- -D warnings(clean), not the full workspace targetmake test(test-consensusplustest-node, atrelease-fast) — all passing. Ran only the tests above