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
2 changes: 1 addition & 1 deletion tooling/ef_tests/blockchain/.fixtures_url_zkevm
Original file line number Diff line number Diff line change
@@ -1 +1 @@
https://github.com/ethereum/execution-spec-tests/releases/download/zkevm%40v0.3.0/fixtures_zkevm.tar.gz
https://github.com/ethereum/execution-spec-tests/releases/download/zkevm%40v0.3.3/fixtures_zkevm.tar.gz
5 changes: 3 additions & 2 deletions tooling/ef_tests/blockchain/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ amsterdam-vectors: $(AMSTERDAM_ARTIFACT) $(SPECTEST_VECTORS_DIR)
$(ZKEVM_ARTIFACT): $(ZKEVM_FIXTURES_FILE)
curl -L -o $(ZKEVM_ARTIFACT) $(ZKEVM_URL)

zkevm-vectors: $(ZKEVM_ARTIFACT) $(SPECTEST_VECTORS_DIR)
tar -xzf $(ZKEVM_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) fixtures/blockchain_tests/for_amsterdam/amsterdam/eip8025_optional_proofs
# amsterdam-vectors must run first so witness-bearing zkevm JSONs overlay the bal@v5.6.1 copies.
zkevm-vectors: $(ZKEVM_ARTIFACT) $(SPECTEST_VECTORS_DIR) amsterdam-vectors
tar -xzf $(ZKEVM_ARTIFACT) --strip-components=2 -C $(SPECTEST_VECTORS_DIR) fixtures/blockchain_tests/for_amsterdam

help: ## 📚 Show help for each of the Makefile recipes
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down
60 changes: 51 additions & 9 deletions tooling/ef_tests/blockchain/test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,29 +554,71 @@ async fn run_stateless_from_fixture(
let block: CoreBlock = block_data.clone().into();
let block_number = block.header.number;

// Absent bytes means "expected to succeed"; malformed bytes are a hard error.
let expected_valid = match block_data.stateless_output_bytes.as_deref() {
None => true,
Some(bytes) => parse_expected_valid_flag(bytes).map_err(|e| {
format!("Malformed statelessOutputBytes for {test_key} block {block_number}: {e}")
})?,
};

// Parse and conversion errors must always fail; only the execution outcome is
// matched against `expected_valid` so the (false, Err(_)) arm below cannot
// absorb regressions in deserialization or witness conversion.
let rpc_witness: RpcExecutionWitness = serde_json::from_value(witness_json.clone())
.map_err(|e| {
format!("Failed to parse executionWitness for block {block_number}: {e}")
format!("executionWitness parse failed for {test_key} block {block_number}: {e}")
})?;

let execution_witness = rpc_witness
.into_execution_witness(*chain_config, block_number)
.map_err(|e| format!("Witness conversion failed for block {block_number}: {e}"))?;
.map_err(|e| {
format!("witness conversion failed for {test_key} block {block_number}: {e}")
})?;

let program_input = ProgramInput::new(vec![block], execution_witness);

let execute_result = match backend_type {
let exec_result = match backend_type {
BackendType::Exec => ExecBackend::new().execute(program_input),
#[cfg(feature = "sp1")]
BackendType::SP1 => Sp1Backend::new().execute(program_input),
};

if let Err(e) = execute_result {
return Err(format!(
"Stateless execution from fixture failed for {test_key} block {block_number}: {e}"
));
match (expected_valid, exec_result) {
(true, Ok(_)) | (false, Err(_)) => {}
(true, Err(e)) => {
return Err(format!(
"Stateless execution from fixture failed for {test_key} block {block_number}: {e}"
));
}
(false, Ok(_)) => {
return Err(format!(
"Stateless execution from fixture succeeded for {test_key} block \
{block_number} but fixture expected it to fail (invalid executionWitness)"
));
}
}
}

Ok(())
}

/// Decode the `valid` byte (index 32) from a zkevm-fixture `statelessOutputBytes` hex
/// string, encoded as `new_payload_request_root (32 B) ++ valid (1 B) ++ padding`.
#[cfg(feature = "stateless")]
fn parse_expected_valid_flag(hex: &str) -> Result<bool, String> {
let trimmed = hex.strip_prefix("0x").unwrap_or(hex);
let byte_hex = trimmed.get(64..66).ok_or_else(|| {
format!(
"expected at least 33 bytes (66 hex chars), got {} hex chars",
trimmed.len()
)
})?;
let byte = u8::from_str_radix(byte_hex, 16)
.map_err(|e| format!("invalid hex at byte 32 ({byte_hex:?}): {e}"))?;
match byte {
0 => Ok(false),
1 => Ok(true),
n => Err(format!(
"invalid validity byte 0x{n:02x} (expected 0x00 or 0x01)"
)),
}
}
34 changes: 31 additions & 3 deletions tooling/ef_tests/blockchain/tests/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,48 @@ const SKIPPED_BASE: &[&str] = &[
];

// Extra skips added only for prover backends.
#[cfg(feature = "sp1")]
#[cfg(all(feature = "sp1", not(feature = "stateless")))]
const EXTRA_SKIPS: &[&str] = &[
// I believe these tests fail because of how much stress they put into the zkVM, they probably cause an OOM though this should be checked
"static_Call50000",
"Return50000",
"static_Call1MB1024Calldepth",
];
#[cfg(not(feature = "sp1"))]
#[cfg(feature = "stateless")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cfg(feature = "sp1") and cfg(feature = "stateless") aren't mutually exclusive at the cfg level — if both are enabled together, this file fails to compile (two definitions of EXTRA_SKIPS). Either gate the third arm with cfg(all(not(feature="sp1"), not(feature="stateless"))) (which it already does via not(any(...))) AND add a compile_error! in cfg(all(feature="sp1", feature="stateless")) to make the exclusivity explicit. Currently those features may be exclusive in practice, but the constraint isn't documented anywhere.

Copy link
Copy Markdown
Contributor Author

@avilagaston9 avilagaston9 May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compile_error! is at lines 6–7. In ad031d5 I gated the sp1 arms with not(feature = "stateless") so the both-on combo now fails with only that diagnostic (previously two duplicate-definition errors rode along with it).

const EXTRA_SKIPS: &[&str] = &[
// zkevm@v0.3.3 tolerance tests: the fixture's `statelessOutputBytes` declares `valid = 1`
// because the executed path does not actually consume the malformed/extra/missing witness
// entry, but our RpcExecutionWitness conversion eagerly validates the full witness and
// rejects it. Re-enable once the witness conversion is lazy per EIP-8025 §Tolerance.
"validation_headers_malformed_rlp_header",
"validation_headers_missing_oldest_blockhash_ancestor",
"validation_headers_missing_parent_header",
"validation_state_extra_unused_trie_node",
// zkevm@v0.3.3 rejection tests: `statelessOutputBytes` declares `valid = 0` so the guest
// program must reject the deliberately-incomplete witness, but our stateless path runs
// to completion instead of detecting the missing entry. Re-enable once the witness
// completeness checks land (missing delegation/external-code bytecodes, non-contiguous
// header chain detection).
"validation_codes_missing_delegated_code_on_insufficient_balance_call",
"validation_codes_missing_external_code_read_target",
"validation_codes_missing_redelegation_old_marker",
"validation_codes_missing_sender_delegation_marker",
"validation_headers_non_contiguous_chain",
// zkevm@v0.3.3 conversion-time rejection: `statelessOutputBytes` declares `valid = 0` and
// our `into_execution_witness` correctly rejects the witness because it can't extract the
// initial state root without the parent header. Since 5a597e67d the runner treats
// conversion errors as unconditional regressions, so this correct-rejection-at-the-wrong-
// stage trips the test. Re-enable once conversion is lazy enough to defer the parent-
// header check to execution.
"validation_headers_empty_block_missing_mandatory_parent",
];
#[cfg(not(any(feature = "sp1", feature = "stateless")))]
const EXTRA_SKIPS: &[&str] = &[];

// Select backend
#[cfg(feature = "stateless")]
const BACKEND: Option<BackendType> = Some(BackendType::Exec);
#[cfg(feature = "sp1")]
#[cfg(all(feature = "sp1", not(feature = "stateless")))]
const BACKEND: Option<BackendType> = Some(BackendType::SP1);
#[cfg(not(any(feature = "sp1", feature = "stateless")))]
const BACKEND: Option<BackendType> = None;
Expand Down
Loading