Skip to content
Merged
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
5 changes: 0 additions & 5 deletions lean_client/containers/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
19 changes: 19 additions & 0 deletions lean_client/containers/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand All @@ -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)?
Expand Down
58 changes: 33 additions & 25 deletions lean_client/containers/tests/test_vectors/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl From<TestCase> 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),
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -648,12 +654,14 @@ impl TestRunner {
) -> Result<(), Box<dyn std::error::Error>> {
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
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
19 changes: 15 additions & 4 deletions lean_client/http_api/src/test_driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,21 @@ async fn run_state_transition(body: Bytes) -> Json<StateTransitionResponse> {
// 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());
}
}

Expand Down
5 changes: 3 additions & 2 deletions lean_client/spec_test_fixtures/src/state_transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub struct TestCase {
#[serde(default)]
pub post: Option<PostState>,
#[serde(default)]
pub expect_exception: Option<String>,
pub rejection_reason: Option<String>,
/// `_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")]
Expand All @@ -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<Slot>,
#[serde(default)]
pub validator_count: Option<usize>,
}
Expand Down
Loading