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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ jobs:

- name: Cache test fixtures
id: cache-fixtures
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: leanSpec/fixtures
key: leanspec-fixtures-${{ steps.lean-spec.outputs.commit }}
save-always: true

# All fixture generation steps are skipped when the cache hits
- name: Checkout leanSpec at pinned commit
Expand Down Expand Up @@ -93,10 +94,11 @@ jobs:
- name: Cache production keys
if: steps.cache-fixtures.outputs.cache-hit != 'true'
id: cache-prod-keys
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: leanSpec/packages/testing/src/consensus_testing/test_keys/prod_scheme
key: prod-keys-${{ steps.prod-keys-url.outputs.hash }}
save-always: true

- name: Download production keys
if: steps.cache-fixtures.outputs.cache-hit != 'true' && steps.cache-prod-keys.outputs.cache-hit != 'true'
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ docker-build: ## 🐳 Build the Docker image
-t ghcr.io/lambdaclass/ethlambda:$(DOCKER_TAG) .
@echo

# 2026-04-20
LEAN_SPEC_COMMIT_HASH:=bc17f7ae8d16caec276f4d304e04fd3c65e6de3c
# 2026-04-28: bump for leanSpec PR #682 (validate_attestation future-slot bound).
LEAN_SPEC_COMMIT_HASH:=62eff6e7e6041a283877a546a07cb3b83f4f7d5b

leanSpec:
git clone https://github.com/leanEthereum/leanSpec.git --single-branch
Expand Down
8 changes: 8 additions & 0 deletions crates/blockchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ pub const MILLISECONDS_PER_SLOT: u64 = MILLISECONDS_PER_INTERVAL * INTERVALS_PER
///
/// See: leanSpec commit 0c9528a (PR #536).
pub const MAX_ATTESTATIONS_DATA: usize = 16;
/// Future-slot tolerance for gossip attestations, expressed in intervals.
///
/// Bounds the clock skew the time check is willing to absorb when admitting a
/// vote whose slot has not yet started locally. One interval is roughly 800 ms,
/// the lean analogue of mainnet's `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
///
/// See: leanSpec PR #682.
pub const GOSSIP_DISPARITY_INTERVALS: u64 = 1;

impl BlockChain {
pub fn spawn(
Expand Down
23 changes: 13 additions & 10 deletions crates/blockchain/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ use ethlambda_types::{
use tracing::{info, trace, warn};

use crate::{
INTERVALS_PER_SLOT, MAX_ATTESTATIONS_DATA, MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT,
metrics,
GOSSIP_DISPARITY_INTERVALS, INTERVALS_PER_SLOT, MAX_ATTESTATIONS_DATA,
MILLISECONDS_PER_INTERVAL, MILLISECONDS_PER_SLOT, metrics,
};

const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3;
Expand Down Expand Up @@ -135,7 +135,7 @@ fn update_safe_target(store: &mut Store) {
/// 2. A vote cannot span backwards in time (source > target).
/// 3. The head must be at least as recent as source and target.
/// 4. Checkpoint slots must match the actual block slots.
/// 5. A vote cannot be for a future slot.
/// 5. The vote's slot must have started locally (a small disparity margin is allowed).
fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<(), StoreError> {
let _timing = metrics::time_attestation_validation();

Expand Down Expand Up @@ -182,13 +182,16 @@ fn validate_attestation_data(store: &Store, data: &AttestationData) -> Result<()
});
}

// Time Check - Validate attestation is not too far in the future.
// We allow a small margin for clock disparity (1 slot), but no further.
let current_slot = store.time() / INTERVALS_PER_SLOT;
if data.slot > current_slot + 1 {
// Time Check - Honest validators emit votes only after their slot has begun.
// Allow a small disparity margin for clock skew between peers.
//
// The bound is in intervals, not slots: a whole-slot margin would let an
// adversary pre-publish next-slot aggregates ahead of any honest validator.
let attestation_start_interval = data.slot.saturating_mul(INTERVALS_PER_SLOT);
if attestation_start_interval > store.time() + GOSSIP_DISPARITY_INTERVALS {
return Err(StoreError::AttestationTooFarInFuture {
attestation_slot: data.slot,
current_slot,
store_time: store.time(),
});
}

Expand Down Expand Up @@ -802,11 +805,11 @@ pub enum StoreError {
},

#[error(
"Attestation slot {attestation_slot} is too far in future (current slot: {current_slot})"
"Attestation slot {attestation_slot} is too far in future (store time: {store_time} intervals)"
)]
AttestationTooFarInFuture {
attestation_slot: u64,
current_slot: u64,
store_time: u64,
},

#[error(
Expand Down
8 changes: 8 additions & 0 deletions crates/blockchain/state_transition/tests/stf_spectests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ fn run(path: &Path) -> datatest_stable::Result<()> {
}
println!("Running test: {}", name);

// Fixtures with no blocks come from spec filler runs that raised
// before any block was constructed (e.g. negative tests where
// `state.process_slots(spec.slot)` aborts pre-build). With nothing
// for ethlambda to replay, the spec framework's verdict stands.
if test.blocks.is_empty() {
continue;
}

let mut pre_state: State = test.pre.into();
let mut result = Ok(());

Expand Down
Loading