diff --git a/lean_client/containers/src/attestation.rs b/lean_client/containers/src/attestation.rs index cadeced6..637b2e7b 100644 --- a/lean_client/containers/src/attestation.rs +++ b/lean_client/containers/src/attestation.rs @@ -168,11 +168,6 @@ impl AggregationBits { .filter_map(|(i, bit)| if *bit { Some(i as u64) } else { None }) .collect(); - assert!( - !indices.is_empty(), - "Aggregated attestation must reference at least one validator" - ); - indices } } diff --git a/lean_client/containers/src/state.rs b/lean_client/containers/src/state.rs index 5c206ed3..288adf6b 100644 --- a/lean_client/containers/src/state.rs +++ b/lean_client/containers/src/state.rs @@ -368,6 +368,11 @@ impl State { "Block is older than latest header" ); + ensure!( + self.validators.len_u64() > 0, + "Cannot schedule a proposer for an empty validator registry" + ); + ensure!( is_proposer_for(block.proposer_index, self.slot, self.validators.len_u64()), "Incorrect block proposer" @@ -510,6 +515,11 @@ impl State { let target = attestation.data.target.clone(); let head = attestation.data.head.clone(); + ensure!( + attestation.aggregation_bits.0.iter().any(|bit| *bit), + "empty aggregation bits" + ); + if !justified_slots.is_slot_justified(finalized_slot, source.slot)? { info!("skipping attestation, source slot is not justified"); continue; @@ -525,6 +535,15 @@ impl State { continue; } + let chain_length = self.historical_block_hashes.len_u64(); + if source.slot.0 >= chain_length + || target.slot.0 >= chain_length + || head.slot.0 >= chain_length + { + info!("skipping attestation, source/target/head slot past chain view"); + continue; + } + if &source.root != self.historical_block_hashes.get(source.slot.0)? || &target.root != self.historical_block_hashes.get(target.slot.0)? || &head.root != self.historical_block_hashes.get(head.slot.0)? diff --git a/lean_client/containers/tests/test_vectors/runner.rs b/lean_client/containers/tests/test_vectors/runner.rs index 05ec8606..42d4210b 100644 --- a/lean_client/containers/tests/test_vectors/runner.rs +++ b/lean_client/containers/tests/test_vectors/runner.rs @@ -23,7 +23,7 @@ impl From for PracticalCase { pre: case.pre.into(), blocks: case.blocks.map(|v| v.into_iter().map(Into::into).collect()), post: case.post, - expect_exception: case.expect_exception, + expect_exception: case.rejection_reason, info: case.info.unwrap_or_else(default_info), } } @@ -136,12 +136,14 @@ impl TestRunner { // Verify post-state conditions if let Some(post) = test_case.post { - if state.slot != post.slot { - return Err(format!( - "Post-state slot mismatch: expected {:?}, got {:?}", - post.slot, state.slot - ) - .into()); + if let Some(expected_slot) = post.slot { + if state.slot != expected_slot { + return Err(format!( + "Post-state slot mismatch: expected {:?}, got {:?}", + expected_slot, state.slot + ) + .into()); + } } // Only check validator count if specified in post-state @@ -254,12 +256,14 @@ impl TestRunner { // Verify post-state conditions if let Some(post) = test_case.post { - if state.slot != post.slot { - return Err(format!( - "Post-state slot mismatch: expected {:?}, got {:?}", - post.slot, state.slot - ) - .into()); + if let Some(expected_slot) = post.slot { + if state.slot != expected_slot { + return Err(format!( + "Post-state slot mismatch: expected {:?}, got {:?}", + expected_slot, state.slot + ) + .into()); + } } println!("\n✓ All post-state checks passed"); @@ -369,12 +373,14 @@ impl TestRunner { // Verify post-state conditions if let Some(post) = test_case.post { - if state.slot != post.slot { - return Err(format!( - "Post-state slot mismatch: expected {:?}, got {:?}", - post.slot, state.slot - ) - .into()); + if let Some(expected_slot) = post.slot { + if state.slot != expected_slot { + return Err(format!( + "Post-state slot mismatch: expected {:?}, got {:?}", + expected_slot, state.slot + ) + .into()); + } } println!("\n✓ All post-state checks passed"); @@ -648,12 +654,14 @@ impl TestRunner { ) -> Result<(), Box> { if let Some(ref post) = test_case.post { // Verify slot - if state.slot != post.slot { - return Err(format!( - "Post-state slot mismatch: expected {:?}, got {:?}", - post.slot, state.slot - ) - .into()); + if let Some(expected_slot) = post.slot { + if state.slot != expected_slot { + return Err(format!( + "Post-state slot mismatch: expected {:?}, got {:?}", + expected_slot, state.slot + ) + .into()); + } } // Verify validator count if specified diff --git a/lean_client/fork_choice/tests/unit_tests/validator.rs b/lean_client/fork_choice/tests/unit_tests/validator.rs index 70da5388..bf5fe7d5 100644 --- a/lean_client/fork_choice/tests/unit_tests/validator.rs +++ b/lean_client/fork_choice/tests/unit_tests/validator.rs @@ -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(), @@ -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 { diff --git a/lean_client/http_api/src/test_driver.rs b/lean_client/http_api/src/test_driver.rs index 09707888..e2f17a87 100644 --- a/lean_client/http_api/src/test_driver.rs +++ b/lean_client/http_api/src/test_driver.rs @@ -268,10 +268,21 @@ async fn run_state_transition(body: Bytes) -> Json { // slot must be rejected. When that's the shape, exercise // `process_slots(state.slot)` so the invariant fires and the resulting // error surfaces as `succeeded: false`. - if last_err.is_none() && blocks_was_empty && case.expect_exception.is_some() { - let target_slot = state.slot; - if let Err(err) = state.clone().process_slots(target_slot) { - last_err = Some(format!("process_slots({target_slot:?}) failed: {err}")); + if last_err.is_none() && blocks_was_empty && case.rejection_reason.is_some() { + let next_slot = containers::Slot(state.slot.0 + 1); + let probe = containers::Block { + slot: next_slot, + proposer_index: 0, + parent_root: ssz::H256::zero(), + state_root: ssz::H256::zero(), + body: containers::BlockBody::default(), + }; + let result = state + .clone() + .process_slots(next_slot) + .and_then(|advanced| advanced.process_block_header(&probe)); + if let Err(err) = result { + last_err = Some(err.to_string()); } } diff --git a/lean_client/spec_test_fixtures/src/state_transition.rs b/lean_client/spec_test_fixtures/src/state_transition.rs index d7af50ba..4a167d19 100644 --- a/lean_client/spec_test_fixtures/src/state_transition.rs +++ b/lean_client/spec_test_fixtures/src/state_transition.rs @@ -35,7 +35,7 @@ pub struct TestCase { #[serde(default)] pub post: Option, #[serde(default)] - pub expect_exception: Option, + pub rejection_reason: Option, /// `_info` is metadata for traceability; we don't read any sub-field, so /// we keep it fully optional to tolerate fixtures that omit it. #[serde(default, rename = "_info")] @@ -45,7 +45,8 @@ pub struct TestCase { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PostState { - pub slot: Slot, + #[serde(default)] + pub slot: Option, #[serde(default)] pub validator_count: Option, }