Skip to content
Draft
Show file tree
Hide file tree
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
11 changes: 11 additions & 0 deletions lean_client/fork_choice/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,17 @@ pub fn on_block(
return Ok(BlockOutcome::AlreadyKnown);
}

let attestations = &signed_block.block.body.attestations;
let distinct = attestations
.into_iter()
.map(|attestation| attestation.data.hash_tree_root())
.collect::<HashSet<_>>()
.len();
ensure!(
distinct == attestations.len_usize(),
"block contains duplicate AttestationData"
);

let parent_root = signed_block.block.parent_root;

if !store.states.contains_key(&parent_root) && !parent_root.is_zero() {
Expand Down
21 changes: 2 additions & 19 deletions lean_client/fork_choice/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync::Arc;

use anyhow::{Result, anyhow, ensure};
use containers::{
AggregatedSignatureProof, AttestationData, Block, BlockHeader, Checkpoint, Config,
AggregatedSignatureProof, AttestationData, Block, Checkpoint, Config,
SignatureKey, SignedAggregatedAttestation, SignedAttestation, SignedBlock, Slot, State,
};
use indexmap::IndexMap;
Expand Down Expand Up @@ -230,24 +230,7 @@ pub fn get_forkchoice_store(
let block = anchor_block.block.clone();
let block_slot = block.slot;

// Compute block root differently for genesis vs checkpoint sync:
// - Genesis (slot 0): Use block.hash_tree_root() directly — block and state are consistent.
// - Checkpoint sync (slot > 0): Reconstruct BlockHeader from state.latest_block_header,
// using anchor_state.hash_tree_root() as state_root. This guarantees the root stored
// as the key in store.blocks / store.states is the canonical one committed to by the
// downloaded state, independent of what the real block's state_root field contains.
let block_root = if block_slot.0 == 0 {
block.hash_tree_root()
} else {
let block_header = BlockHeader {
slot: anchor_state.latest_block_header.slot,
proposer_index: anchor_state.latest_block_header.proposer_index,
parent_root: anchor_state.latest_block_header.parent_root,
state_root: anchor_state.hash_tree_root(),
body_root: anchor_state.latest_block_header.body_root,
};
block_header.hash_tree_root()
};
let block_root = block.hash_tree_root();

// Seed both checkpoints from the anchor block itself: (root=anchor_root,
// slot=anchor_slot). The store treats the anchor as the new "genesis" for
Expand Down
10 changes: 8 additions & 2 deletions lean_client/fork_choice/tests/fork_choice_test_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ struct TestDataWrapper<T> {
#[serde(rename_all = "camelCase")]
struct TestValidator {
#[allow(dead_code)]
#[serde(alias = "pubkey")]
#[serde(alias = "pubkey", alias = "attestationPublicKey")]
attestation_pubkey: String,
#[allow(dead_code)]
#[serde(default)]
#[serde(default, alias = "proposalPublicKey")]
proposal_pubkey: Option<String>,
#[allow(dead_code)]
#[serde(default)]
Expand Down Expand Up @@ -473,13 +473,16 @@ fn forkchoice(spec_file: &str) {
let mut cache = BlockCache::new();
let mut block_labels: HashMap<String, H256> = HashMap::new();

block_labels.insert("genesis".to_string(), store.head);

for (step_idx, step) in case.steps.into_iter().enumerate() {
match step {
ForkChoiceStep::Block {
valid,
checks,
block: test_block,
} => {
let block_root_label = test_block.block_root_label.clone();
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
let block: Block = test_block.into();
let signed_block = SignedBlock {
Expand Down Expand Up @@ -519,6 +522,9 @@ fn forkchoice(spec_file: &str) {
}

if valid && result.is_ok() {
if let Some(label) = block_root_label {
block_labels.insert(label, *result.as_ref().unwrap());
}
verify_checks(&store, &checks, &block_labels, step_idx).expect(&format!(
"Step: {step_idx}: Should be valid but checks failed"
));
Expand Down
6 changes: 3 additions & 3 deletions lean_client/fork_choice/tests/unit_tests/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,8 @@ fn produce_and_apply(
let num_validators = store.states[&store.head].validators.len_u64();
let proposer = slot.0 % num_validators;
let _ = keys;
let (_block_root, block, _sigs) =
produce_block_with_signatures(&mut store, slot, proposer, 1, true)
.expect("block production failed");
let (_block_root, block, _sigs) = produce_block_with_signatures(store, slot, proposer, 1, true)
.expect("block production failed");
let signed = SignedBlock {
block,
proof: MultiMessageAggregate::default(),
Expand Down Expand Up @@ -752,6 +751,7 @@ fn test_produce_block_closes_justification_gap() {
&known_block_roots,
&aggregated_payloads,
1,
false,
)
.expect("build_block for sibling block_6 failed");
let signed_block_6 = SignedBlock {
Expand Down
1 change: 1 addition & 0 deletions lean_client/spec_test_fixtures/src/fork_choice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,6 @@ pub struct GossipAggregatedAttestationStep {
#[serde(rename_all = "camelCase")]
pub struct GossipProofJSON {
pub participants: TestAggregationBits,
#[serde(alias = "proof")]
pub proof_data: HexBytesJSON,
}
Loading
Loading