diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 8fd1b36c..07c278d8 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -52,7 +52,13 @@ pub fn update_head(store: &mut Store, log_tree: bool) { metrics::observe_fork_choice_reorg_depth(depth); info!(%old_head, %new_head, depth, "Fork choice reorg detected"); } - store.update_checkpoints(ForkCheckpoints::head_only(new_head)); + + // Override the store's latest finalized with the head state's + let finalized = store + .get_state(&new_head) + .map(|state| state.latest_finalized) + .filter(|finalized| store.get_block_header(&finalized.root).is_some()); + store.update_checkpoints(ForkCheckpoints::new(new_head, None, finalized)); if old_head != new_head { let old_slot = store @@ -406,6 +412,9 @@ fn on_gossip_aggregated_attestation_core( let num_validators = validators.len() as u64; let participant_indices: Vec = aggregated.proof.participant_indices().collect(); + if participant_indices.is_empty() { + return Err(StoreError::EmptyAggregationBits); + } if participant_indices.iter().any(|&vid| vid >= num_validators) { return Err(StoreError::InvalidValidatorIndex); } @@ -547,14 +556,14 @@ fn on_block_core( let state_root = block.state_root; post_state.latest_block_header.state_root = state_root; - // Update justified/finalized checkpoints if they have higher slots + // Advance the justified checkpoint when the post-state names a higher one + // (leanSpec `advance_to`: monotonic by slot). Finalized is intentionally not + // latched here; it is recomputed from the head's chain in `update_head`. let justified = (post_state.latest_justified.slot > store.latest_justified().slot) .then_some(post_state.latest_justified); - let finalized = (post_state.latest_finalized.slot > store.latest_finalized().slot) - .then_some(post_state.latest_finalized); - if justified.is_some() || finalized.is_some() { - store.update_checkpoints(ForkCheckpoints::new(store.head(), justified, finalized)); + if let Some(justified) = justified { + store.update_checkpoints(ForkCheckpoints::new(store.head(), Some(justified), None)); } // Store signed block and state @@ -866,6 +875,9 @@ pub enum StoreError { #[error("Missing target state for block: {0}")] MissingTargetState(H256), + #[error("Aggregated attestation references no participants")] + EmptyAggregationBits, + #[error("Validator {validator_index} is not the proposer for slot {slot}")] NotProposer { validator_index: u64, slot: u64 }, diff --git a/crates/blockchain/state_transition/tests/stf_spectests.rs b/crates/blockchain/state_transition/tests/stf_spectests.rs index 669ea835..8a92fd4e 100644 --- a/crates/blockchain/state_transition/tests/stf_spectests.rs +++ b/crates/blockchain/state_transition/tests/stf_spectests.rs @@ -110,6 +110,7 @@ fn compare_post_states( justifications_roots, justifications_validators, validator_count, + validators, latest_justified_root_label, latest_finalized_root_label, justifications_roots_labels, @@ -275,6 +276,39 @@ fn compare_post_states( .into()); } } + if let Some(validators) = validators { + let expected = &validators.data; + let post_validators: Vec<_> = post_state.validators.iter().collect(); + if post_validators.len() != expected.len() { + return Err(format!( + "validators count mismatch: expected {}, got {}", + expected.len(), + post_validators.len() + ) + .into()); + } + for (i, expected_validator) in expected.iter().enumerate() { + let expected_domain: ethlambda_types::state::Validator = + expected_validator.clone().into(); + let actual = post_validators[i]; + if actual.index != expected_domain.index + || actual.attestation_pubkey != expected_domain.attestation_pubkey + || actual.proposal_pubkey != expected_domain.proposal_pubkey + { + return Err(format!( + "validator[{i}] mismatch: expected index={} att={} prop={}, \ + got index={} att={} prop={}", + expected_domain.index, + hex::encode(expected_domain.attestation_pubkey), + hex::encode(expected_domain.proposal_pubkey), + actual.index, + hex::encode(actual.attestation_pubkey), + hex::encode(actual.proposal_pubkey), + ) + .into()); + } + } + } if let Some(label) = latest_justified_root_label { let expected = resolve_label(label, block_registry)?; if post_state.latest_justified.root != expected { diff --git a/crates/blockchain/state_transition/tests/types.rs b/crates/blockchain/state_transition/tests/types.rs index dab07f68..e8e1d60a 100644 --- a/crates/blockchain/state_transition/tests/types.rs +++ b/crates/blockchain/state_transition/tests/types.rs @@ -82,6 +82,10 @@ pub struct PostState { #[serde(rename = "validatorCount")] pub validator_count: Option, + /// Full validator registry check: each entry's index and public keys must + /// match the post-state registry exactly. + pub validators: Option>, + // Label-based root checks: "block_N" labels resolved to hash_tree_root of the Nth block. #[serde(rename = "latestJustifiedRootLabel")] pub latest_justified_root_label: Option,