Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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)"
)),
}
}
27 changes: 24 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,41 @@ 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.

@avilagaston9 avilagaston9 May 4, 2026

Copy link
Copy Markdown
Contributor Author

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",
];
#[cfg(not(any(feature = "sp1", feature = "stateless")))]
const EXTRA_SKIPS: &[&str] = &[];
Comment on lines -31 to 60

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.

if sp1 is set but stateless isn't this won't compile, and if sp1 implies stateless then why specify both?

@avilagaston9 avilagaston9 May 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sp1 alone compiles (verified). The sp1 + stateless combo is rejected by the compile_error! at lines 6–7. They are not redundant: stateless uses the in-process Exec backend; sp1 runs the guest ELF inside the SP1 zkVM executor.


// 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