Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ docker-build: ## 🐳 Build the Docker image
-t ghcr.io/lambdaclass/ethlambda:$(DOCKER_TAG) .
@echo

LEAN_SPEC_COMMIT_HASH:=45e87fd8a56ac3849ae25906e96960cc116f8d81
# 2026-04-14
LEAN_SPEC_COMMIT_HASH:=76d4792ecd9d5bbcab60bfb022b72b590946b511

leanSpec:
git clone https://github.com/leanEthereum/leanSpec.git --single-branch
Expand Down
87 changes: 84 additions & 3 deletions crates/blockchain/state_transition/tests/stf_spectests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
use std::collections::HashMap;
use std::path::Path;

use ethlambda_state_transition::state_transition;
use ethlambda_types::{block::Block, state::State};
use ethlambda_types::{
block::Block,
primitives::{H256, HashTreeRoot as _},
state::State,
};

use crate::types::PostState;

Expand All @@ -24,8 +29,13 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
let mut pre_state: State = test.pre.into();
let mut result = Ok(());

for block in test.blocks {
// Build a block registry mapping "block_N" labels to hash tree roots.
// Labels are 1-indexed: "block_1" is the first block in the array.
let mut block_registry: HashMap<String, H256> = HashMap::new();
for (i, block) in test.blocks.into_iter().enumerate() {
let block: Block = block.into();
let label = format!("block_{}", i + 1);
block_registry.insert(label, block.hash_tree_root());
result = state_transition(&mut pre_state, &block);
if result.is_err() {
break;
Expand All @@ -34,7 +44,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
let post_state = pre_state;
match (result, test.post) {
(Ok(_), Some(expected_post)) => {
compare_post_states(&post_state, &expected_post)?;
compare_post_states(&post_state, &expected_post, &block_registry)?;
}
(Ok(_), None) => {
return Err(
Expand All @@ -55,9 +65,24 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
Ok(())
}

fn resolve_label(
label: &str,
block_registry: &HashMap<String, H256>,
) -> datatest_stable::Result<H256> {
block_registry.get(label).copied().ok_or_else(|| {
format!(
"label '{}' not found in block registry. Available: {:?}",
label,
block_registry.keys().collect::<Vec<_>>()
)
.into()
})
}

fn compare_post_states(
post_state: &State,
expected_post: &PostState,
block_registry: &HashMap<String, H256>,
) -> datatest_stable::Result<()> {
let PostState {
config_genesis_time,
Expand All @@ -77,6 +102,11 @@ fn compare_post_states(
justifications_roots,
justifications_validators,
validator_count,
latest_justified_root_label,
latest_finalized_root_label,
justifications_roots_labels,
justifications_roots_count,
justifications_validators_count,
} = expected_post;
if let Some(config_genesis_time) = config_genesis_time
&& post_state.config.genesis_time != *config_genesis_time
Expand Down Expand Up @@ -237,6 +267,57 @@ fn compare_post_states(
.into());
}
}
if let Some(label) = latest_justified_root_label {
let expected = resolve_label(label, block_registry)?;
if post_state.latest_justified.root != expected {
return Err(format!(
"latest_justified.root mismatch (via label '{label}'): expected {expected:?}, got {:?}",
post_state.latest_justified.root
)
.into());
}
}
if let Some(label) = latest_finalized_root_label {
let expected = resolve_label(label, block_registry)?;
if post_state.latest_finalized.root != expected {
return Err(format!(
"latest_finalized.root mismatch (via label '{label}'): expected {expected:?}, got {:?}",
post_state.latest_finalized.root
)
.into());
}
}
if let Some(labels) = justifications_roots_labels {
let expected_roots: Vec<H256> = labels
.iter()
.map(|label| resolve_label(label, block_registry))
.collect::<datatest_stable::Result<Vec<_>>>()?;
let post_roots: Vec<_> = post_state.justifications_roots.iter().copied().collect();
if post_roots != expected_roots {
return Err(format!(
"justifications_roots mismatch (via labels {labels:?}): expected {expected_roots:?}, got {post_roots:?}",
)
.into());
}
}
if let Some(expected_count) = justifications_roots_count {
let count = post_state.justifications_roots.len() as u64;
if count != *expected_count {
return Err(format!(
"justifications_roots count mismatch: expected {expected_count}, got {count}",
)
.into());
}
}
if let Some(expected_count) = justifications_validators_count {
let count = post_state.justifications_validators.len() as u64;
if count != *expected_count {
return Err(format!(
"justifications_validators count mismatch: expected {expected_count}, got {count}",
)
.into());
}
}
Ok(())
}

Expand Down
14 changes: 14 additions & 0 deletions crates/blockchain/state_transition/tests/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,18 @@ pub struct PostState {

#[serde(rename = "validatorCount")]
pub validator_count: Option<u64>,

// 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<String>,
#[serde(rename = "latestFinalizedRootLabel")]
pub latest_finalized_root_label: Option<String>,
#[serde(rename = "justificationsRootsLabels")]
pub justifications_roots_labels: Option<Vec<String>>,

// Count checks for variable-length collections.
#[serde(rename = "justificationsRootsCount")]
pub justifications_roots_count: Option<u64>,
#[serde(rename = "justificationsValidatorsCount")]
pub justifications_validators_count: Option<u64>,
}
16 changes: 13 additions & 3 deletions crates/common/test-fixtures/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub struct TestState {
#[serde(rename = "historicalBlockHashes")]
pub historical_block_hashes: Container<H256>,
#[serde(rename = "justifiedSlots")]
pub justified_slots: Container<u64>,
pub justified_slots: Container<bool>,
pub validators: Container<Validator>,
#[serde(rename = "justificationsRoots")]
pub justifications_roots: Container<H256>,
Expand All @@ -154,17 +154,27 @@ impl From<TestState> for State {
.unwrap();
let justifications_roots = SszList::try_from(value.justifications_roots.data).unwrap();

let mut justified_slots = JustifiedSlots::new();
for &b in &value.justified_slots.data {
justified_slots.push(b).unwrap();
}

let mut justifications_validators = JustificationValidators::new();
for &b in &value.justifications_validators.data {
justifications_validators.push(b).unwrap();
}

State {
config: value.config.into(),
slot: value.slot,
latest_block_header: value.latest_block_header.into(),
latest_justified: value.latest_justified.into(),
latest_finalized: value.latest_finalized.into(),
historical_block_hashes,
justified_slots: JustifiedSlots::new(),
justified_slots,
validators,
justifications_roots,
justifications_validators: JustificationValidators::new(),
justifications_validators,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/common/types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ libssz-types.workspace = true
serde_json.workspace = true
serde_yaml_ng.workspace = true
rand.workspace = true
ethlambda-test-fixtures.workspace = true

datatest-stable = "0.3.3"

[[test]]
name = "ssz_spectests"
path = "tests/ssz_spectests.rs"
harness = false
4 changes: 2 additions & 2 deletions crates/common/types/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub struct AttestationData {
}

/// Validator attestation bundled with its signature.
#[derive(Debug, Clone, SszEncode, SszDecode)]
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
pub struct SignedAttestation {
/// The index of the validator making the attestation.
pub validator_id: u64,
Expand Down Expand Up @@ -79,7 +79,7 @@ pub fn validator_indices(bits: &AggregationBits) -> impl Iterator<Item = u64> +
}

/// Aggregated attestation with its signature proof, used for gossip on the aggregation topic.
#[derive(Debug, Clone, SszEncode, SszDecode)]
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
pub struct SignedAggregatedAttestation {
pub data: AttestationData,
pub proof: AggregatedSignatureProof,
Expand Down
6 changes: 3 additions & 3 deletions crates/common/types/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
use primitives::HashTreeRoot as _;

/// Envelope carrying a block and its aggregated signatures.
#[derive(Clone, SszEncode, SszDecode)]
#[derive(Clone, SszEncode, SszDecode, HashTreeRoot)]
pub struct SignedBlock {
/// The block being signed.
pub message: Block,
Expand All @@ -35,7 +35,7 @@ impl core::fmt::Debug for SignedBlock {
}

/// Signature payload for the block.
#[derive(Clone, SszEncode, SszDecode)]
#[derive(Clone, SszEncode, SszDecode, HashTreeRoot)]
pub struct BlockSignatures {
/// Attestation signatures for the aggregated attestations in the block body.
///
Expand Down Expand Up @@ -69,7 +69,7 @@ pub type AttestationSignatures = SszList<AggregatedSignatureProof, 4096>;
/// The proof can verify that all participants signed the same message in the
/// same epoch, using a single verification operation instead of checking
/// each signature individually.
#[derive(Debug, Clone, SszEncode, SszDecode)]
#[derive(Debug, Clone, SszEncode, SszDecode, HashTreeRoot)]
pub struct AggregatedSignatureProof {
/// Bitfield indicating which validators' signatures are included.
pub participants: AggregationBits,
Expand Down
Loading
Loading