Skip to content

fix(storage): store fork-choice votes independently - #552

Open
dicethedev wants to merge 1 commit into
lambdaclass:mainfrom
dicethedev:fix/fork-choice-votes
Open

fix(storage): store fork-choice votes independently#552
dicethedev wants to merge 1 commit into
lambdaclass:mainfrom
dicethedev:fix/fork-choice-votes

Conversation

@dicethedev

@dicethedev dicethedev commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🗒️ Description / Motivation

This PR fixes fork-choice vote tracking so validator votes are stored independently from aggregated proofs and gossip signatures.

Previously, fork-choice votes were derived from bounded in-memory proof/signature buffers. When those buffers evicted old entries through FIFO cleanup, a validator’s latest vote could disappear even though the vote should still count for fork choice. This PR adds a separate proof-less vote store so fork-choice weights are not affected by proof/signature retention.

What Changed

  • Updated crates/storage/src/store.rs

    • Added an independent latest-vote store keyed by validator index.
    • Records known fork-choice votes when aggregated payloads become known.
    • Records block-included attestation votes separately from proof storage.
    • Keeps proof buffers for aggregation/proposal behavior only.
    • Prunes stored votes once their target is finalized.
  • Updated crates/blockchain/src/store.rs

    • Inserts block-included attestation votes into the independent vote store during block processing.

Correctness / Behavior Guarantees

  • Fork-choice votes no longer disappear when proof or signature buffers evict entries.
  • Existing proof/signature buffer behavior is preserved for aggregation and block proposal.
  • Latest-vote selection keeps the existing rule: higher slot wins, and same-slot ties use the canonical attestation-data root.
  • Finalized-target votes are pruned once they can no longer affect fork choice.

Tests Added / Run

  • Added regression coverage for votes surviving aggregated proof FIFO eviction.
  • Added coverage for pruning finalized-target votes from the independent vote store.

Related Issues / PRs

✅ Verification Checklist

  • Ran make fmt — clean
  • Ran make lint (clippy with -D warnings) — clean
  • Ran make test (cargo test --workspace --profile release-fast) — all passing

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR separates latest fork-choice votes from bounded proof retention.

  • Adds a per-validator in-memory latest-vote store and deterministic replacement rule.
  • Records votes from known aggregates, promoted aggregates, and block-included attestations.
  • Prunes votes when finalization advances and adds FIFO-eviction and pruning tests.
  • Updates block processing to record included attestation votes independently of proofs.

Confidence Score: 4/5

This PR should not merge until vote pruning preserves latest messages whose heads remain relevant above the finalized checkpoint.

The new pruning predicate uses the attestation target, while LMD-GHOST consumes its head; after finalization this can remove live-branch voting weight from subsequent head calculations.

Files Needing Attention: crates/storage/src/store.rs

Important Files Changed

Filename Overview
crates/storage/src/store.rs Adds the independent latest-vote store and ingestion paths, but pruning by target slot can remove votes whose heads remain relevant to fork choice.
crates/blockchain/src/store.rs Records validated block-included attestation votes after persisting the imported block and state.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Known aggregate] --> V[Independent latest-vote store]
  B[Block-included attestation] --> V
  A --> P[Bounded proof buffer]
  V --> F[LMD-GHOST fork choice]
  C[Finalization advances] --> R[Prune votes by target slot]
  R --> V
Loading
Prompt To Fix All With AI
### Issue 1
crates/storage/src/store.rs:1087-1090
**Target-based pruning drops live votes**

When a validator's latest attestation targets a finalized checkpoint but its head remains on a live post-finalization branch, `prune_known_votes` removes the vote based solely on `target.slot`. LMD-GHOST weights `head.root`, so subsequent head calculations lose that validator's weight and can select the wrong branch until a replacement vote arrives.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(storage): store fork-choice votes in..." | Re-trigger Greptile

Comment on lines +1087 to +1090
for vote in votes.iter_mut() {
let should_prune = vote
.as_ref()
.is_some_and(|data| data.target.slot <= finalized_slot);

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.

P1 Target-based pruning drops live votes

When a validator's latest attestation targets a finalized checkpoint but its head remains on a live post-finalization branch, prune_known_votes removes the vote based solely on target.slot. LMD-GHOST weights head.root, so subsequent head calculations lose that validator's weight and can select the wrong branch until a replacement vote arrives.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/src/store.rs
Line: 1087-1090

Comment:
**Target-based pruning drops live votes**

When a validator's latest attestation targets a finalized checkpoint but its head remains on a live post-finalization branch, `prune_known_votes` removes the vote based solely on `target.slot`. LMD-GHOST weights `head.root`, so subsequent head calculations lose that validator's weight and can select the wrong branch until a replacement vote arrives.

**Knowledge Base Used:**
- [Blockchain core: fork choice, state transition, block building, sync](https://app.greptile.com/lambdaclass/-/custom-context/knowledge-base/lambdaclass/ethlambda/-/docs/blockchain-core.md)
- [Storage (`ethlambda_storage`)](https://app.greptile.com/lambdaclass/-/custom-context/knowledge-base/lambdaclass/ethlambda/-/docs/storage.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@MegaRedHand MegaRedHand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good

Left some comments about pruning. The issue didn't mention it, but the main issue with how we stored votes is that proof are pruned regularly, which shouldn't affect votes: LMD-GHOST uses the latest message from each validator, so otherwise no pruning should happen.

Also, an additional PR is needed to finish addressing the issue, since we still need to replace the payloads and proofs in fork-choice with the new votes.

}

/// Insert proof-less fork-choice votes from block-included attestations.
pub fn insert_known_attestation_votes(&mut self, attestations: &[AggregatedAttestation]) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we embed this inside insert_signed_block?

type StorageKey = Vec<u8>;
type StorageEntry = (StorageKey, Vec<u8>);
type BlockRootIndexChanges = (Vec<StorageKey>, Vec<StorageEntry>);
type VoteStore = Vec<Option<AttestationData>>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's make this a HashMap<usize, AttestationData> instead. That'll make this easier to reason about, and avoid performance edge-cases.

}

/// Prune latest fork-choice votes whose target is at or below `finalized_slot`.
pub fn prune_known_votes(&mut self, finalized_slot: u64) -> usize {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pruning isn't needed for votes, since we always keep the last vote of each validator only

Comment on lines +560 to +561
/// Latest fork-choice vote per validator, independent from bounded proof buffers.
votes: Arc<Mutex<VoteStore>>,

@MegaRedHand MegaRedHand Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's wrap the VoteStore with a ForkChoiceState struct, so we can later extend it as needed without having a mutex for each field.

Also, another thing I forgot about is that we have to store both known and new votes (each in a separate map). Here we are storing known votes only.

}

/// Insert a single proof into the known (fork-choice-active) buffer.
pub fn insert_known_aggregated_payload(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function isn't used anywhere, right? Let's remove it.

Comment on lines +575 to +577
fn new_vote_store() -> Arc<Mutex<VoteStore>> {
Arc::new(Mutex::new(Vec::new()))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is just Default::default(), right?

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.

Store fork-choice votes independently from proofs and signatures

2 participants