Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions crates/common/consensus/lean/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ impl LeanState {
#[cfg(test)]
mod test {
use super::*;
use crate::vote::Vote;

#[test]
fn get_justifications_empty() {
Expand Down Expand Up @@ -958,6 +959,99 @@ mod test {
);
}

#[test]
fn process_attestations_justification_and_finalization() {
let genesis_state = LeanState::new(10, 0);
let mut state = genesis_state.clone();

// Move to slot 1 to allow producing a block there.
state.process_slots(1).unwrap();

// Create and process the block at slot 1.
let block1 = Block {
slot: 1,
proposer_index: 1,
parent_root: state.latest_block_header.tree_hash_root(),
state_root: B256::ZERO,
body: BlockBody {
attestations: VariableList::empty(),
},
};
state.process_block(&block1).unwrap();

// Move to slot 4 and produce/process a block.
state.process_slots(4).unwrap();
let block4 = Block {
slot: 4,
proposer_index: 4,
parent_root: state.latest_block_header.tree_hash_root(),
state_root: B256::ZERO,
body: BlockBody {
attestations: VariableList::empty(),
},
};
state.process_block(&block4).unwrap();

// Advance to slot 5 so the header at slot 4 caches its state root.
state.process_slots(5).unwrap();

// Process a block at slot 5 to push block4's root into historical_block_hashes.
// This is required by the our implementation based off 3SF-mini which validates that target
// roots exist in historical_block_hashes before accepting votes
// This validation does not exist in leanSpec so the test passes without processing block 5
// We deviate from the leanSpec in this test and process block 5 before testing
// process_attestations for slot 4
let block5 = Block {
slot: 5,
proposer_index: 5,
parent_root: state.latest_block_header.tree_hash_root(),
state_root: B256::ZERO,
body: BlockBody {
attestations: VariableList::empty(),
},
};
state.process_block(&block5).unwrap();

// Define source (genesis) and target (slot 4) checkpoints for voting.
let genesis_checkpoint = Checkpoint {
root: state.historical_block_hashes[0], // Canonical root for slot 0
slot: 0,
};
let checkpoint4 = Checkpoint {
root: state.historical_block_hashes[4], // Root of the block at slot 4
slot: 4,
};

// Create 7 votes from distinct validators (indices 0..6) to reach ≥2/3.
let mut votes_for_4 = VariableList::<SignedVote, U4096>::empty();
for i in 0..7 {
let vote = SignedVote {
validator_id: i,
message: Vote {
slot: 4,
head: checkpoint4.clone(),
target: checkpoint4.clone(),
source: genesis_checkpoint.clone(),
},
signature: Default::default(),
};
votes_for_4.push(vote).unwrap();
}

// Process attestations directly; mutates state in place.
state.process_attestations(&votes_for_4).unwrap();

// The target (slot 4) should now be justified.
assert_eq!(state.latest_justified, checkpoint4);
// The justified bit for slot 4 must be set.
assert!(state.justified_slots.get(4).unwrap_or(false));
// Since no other justifiable slot exists between 0 and 4, genesis is finalized.
assert_eq!(state.latest_finalized, genesis_checkpoint);
// The per-root vote tracker for the justified target has been cleared.
let justifications = state.get_justifications().unwrap();
assert!(!justifications.contains_key(&checkpoint4.root));
}

#[test]
fn state_transition_full() {
let genesis_state = LeanState::new(10, 0);
Expand Down
Loading