From cb6eeb14519184b150b5d5f8bb432eae4f0bb61a Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 11 Mar 2026 10:48:16 -0300 Subject: [PATCH 1/4] Remove keys field from ExecutionWitness and change storage_trie_roots to be keyed by H256 (keccak256 of address) instead of Address. Replace the keys-based loop in execution_witness_from_rpc_chain_config with a recursive state trie walker that discovers accounts and their storage roots directly from the trie leaves. This removes the dependency on the keys field which is being phased out of the RPC spec. Keep keys in RpcExecutionWitness with #[serde(default)] for interoperability but always emit it empty. Update GuestProgramState fields (storage_tries, verified_storage_roots, account_hashes_by_address) to use H256 keys consistently. --- crates/blockchain/blockchain.rs | 42 ++-------- .../common/types/block_execution_witness.rs | 68 ++++++++------- .../networking/rpc/debug/execution_witness.rs | 83 ++++++++++++------- 3 files changed, 97 insertions(+), 96 deletions(-) diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index fb79f2e7e60..d94d8515d3d 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -1544,15 +1544,6 @@ impl Blockchain { block_headers_bytes.push(current_header.encode_to_vec()); } - // Create a list of all read/write addresses and storage slots - let mut keys = Vec::new(); - for (address, touched_storage_slots) in touched_account_storage_slots { - keys.push(address.as_bytes().to_vec()); - for slot in touched_storage_slots.iter() { - keys.push(slot.as_bytes().to_vec()); - } - } - // Get initial state trie root and embed the rest of the trie into it let nodes: BTreeMap = used_trie_nodes .into_iter() @@ -1573,12 +1564,9 @@ impl Blockchain { Trie::new_temp() }; let mut storage_trie_roots = BTreeMap::new(); - for key in &keys { - if key.len() != 20 { - continue; // not an address - } - let address = Address::from_slice(key); - let hashed_address = hash_address(&address); + for address in touched_account_storage_slots.keys() { + let hashed_address = hash_address(address); + let hashed_address_h256 = H256::from_slice(&hashed_address); let Some(encoded_account) = state_trie.get(&hashed_address)? else { continue; // empty account, doesn't have a storage trie }; @@ -1595,7 +1583,7 @@ impl Blockchain { "execution witness does not contain non-empty storage trie".to_string(), )); }; - storage_trie_roots.insert(address, (*node).clone()); + storage_trie_roots.insert(hashed_address_h256, (*node).clone()); } Ok(ExecutionWitness { @@ -1605,7 +1593,6 @@ impl Blockchain { chain_config: self.storage.get_chain_config(), state_trie_root, storage_trie_roots, - keys, }) } @@ -1786,15 +1773,6 @@ impl Blockchain { block_headers_bytes.push(current_header.encode_to_vec()); } - // Create a list of all read/write addresses and storage slots - let mut keys = Vec::new(); - for (address, touched_storage_slots) in touched_account_storage_slots { - keys.push(address.as_bytes().to_vec()); - for slot in touched_storage_slots.iter() { - keys.push(slot.as_bytes().to_vec()); - } - } - // Get initial state trie root and embed the rest of the trie into it let nodes: BTreeMap = used_trie_nodes .into_iter() @@ -1815,12 +1793,9 @@ impl Blockchain { Trie::new_temp() }; let mut storage_trie_roots = BTreeMap::new(); - for key in &keys { - if key.len() != 20 { - continue; // not an address - } - let address = Address::from_slice(key); - let hashed_address = hash_address(&address); + for address in touched_account_storage_slots.keys() { + let hashed_address = hash_address(address); + let hashed_address_h256 = H256::from_slice(&hashed_address); let Some(encoded_account) = state_trie.get(&hashed_address)? else { continue; // empty account, doesn't have a storage trie }; @@ -1837,7 +1812,7 @@ impl Blockchain { "execution witness does not contain non-empty storage trie".to_string(), )); }; - storage_trie_roots.insert(address, (*node).clone()); + storage_trie_roots.insert(hashed_address_h256, (*node).clone()); } Ok(ExecutionWitness { @@ -1847,7 +1822,6 @@ impl Blockchain { chain_config: self.storage.get_chain_config(), state_trie_root, storage_trie_roots, - keys, }) } diff --git a/crates/common/types/block_execution_witness.rs b/crates/common/types/block_execution_witness.rs index 28b4f1a913a..09be4a3d8d0 100644 --- a/crates/common/types/block_execution_witness.rs +++ b/crates/common/types/block_execution_witness.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use bytes::Bytes; -use crate::rkyv_utils::H160Wrapper; +use crate::rkyv_utils::H256Wrapper; use crate::serde_utils; use crate::types::{Block, Code, CodeMetadata}; use crate::{ @@ -42,17 +42,17 @@ pub struct GuestProgramState { pub first_block_number: u64, /// The chain configuration. pub chain_config: ChainConfig, - /// Map of storage root hashes to their corresponding storage tries. - pub storage_tries: BTreeMap, + /// Map of hashed addresses to their corresponding storage tries. + pub storage_tries: BTreeMap, /// Map of account addresses to their corresponding hashed addresses. /// This is a convenience map to avoid recomputing the hashed address /// multiple times during guest program execution. /// It is built on-demand during guest program execution, inside the zkVM. - pub account_hashes_by_address: BTreeMap>, - /// Map of account addresses to booleans, indicating whose account's storage tries were + pub account_hashes_by_address: BTreeMap, + /// Map of hashed addresses to booleans, indicating whose account's storage tries were /// verified. /// Verification is done by hashing the trie and comparing the root hash with the account's storage root. - pub verified_storage_roots: BTreeMap, + pub verified_storage_roots: BTreeMap, } /// Witness data produced by the client and consumed by the guest program @@ -76,13 +76,10 @@ pub struct ExecutionWitness { pub chain_config: ChainConfig, /// Root node embedded with the rest of the trie's nodes pub state_trie_root: Option, - /// Root nodes per account storage embedded with the rest of the trie's nodes - #[rkyv(with = MapKV)] - pub storage_trie_roots: BTreeMap, - /// Flattened map of account addresses and storage keys whose values - /// are needed for stateless execution. - #[rkyv(with = crate::rkyv_utils::VecVecWrapper)] - pub keys: Vec>, + /// Root nodes per account storage embedded with the rest of the trie's nodes, + /// keyed by the keccak256 hash of the account address. + #[rkyv(with = MapKV)] + pub storage_trie_roots: BTreeMap, } /// RPC-friendly representation of an execution witness. @@ -98,6 +95,7 @@ pub struct RpcExecutionWitness { )] pub state: Vec, #[serde( + default, serialize_with = "serde_utils::bytes::vec::serialize", deserialize_with = "serde_utils::bytes::vec::deserialize" )] @@ -127,7 +125,7 @@ impl TryFrom for RpcExecutionWitness { } Ok(Self { state: nodes.into_iter().map(Bytes::from).collect(), - keys: value.keys.into_iter().map(Bytes::from).collect(), + keys: Vec::new(), codes: value.codes.into_iter().map(Bytes::from).collect(), headers: value .block_headers_bytes @@ -199,11 +197,11 @@ impl TryFrom for GuestProgramState { state_trie.hash_no_commit(); let mut storage_tries = BTreeMap::new(); - for (address, storage_trie_root) in value.storage_trie_roots { + for (hashed_address, storage_trie_root) in value.storage_trie_roots { // hash storage trie nodes let storage_trie = Trie::new_temp_with_root(storage_trie_root.into()); storage_trie.hash_no_commit(); - storage_tries.insert(address, storage_trie); + storage_tries.insert(hashed_address, storage_trie); } // hash codes @@ -242,18 +240,18 @@ impl GuestProgramState { account_updates: &[AccountUpdate], ) -> Result<(), GuestProgramStateError> { for update in account_updates.iter() { - let hashed_address = self + let hashed_address = *self .account_hashes_by_address .entry(update.address) .or_insert_with(|| hash_address(&update.address)); if update.removed { // Remove account from trie - self.state_trie.remove(hashed_address)?; + self.state_trie.remove(hashed_address.as_bytes())?; } else { // Add or update AccountState in the trie // Fetch current state or create a new state to be inserted - let mut account_state = match self.state_trie.get(hashed_address)? { + let mut account_state = match self.state_trie.get(hashed_address.as_bytes())? { Some(encoded_state) => AccountState::decode(&encoded_state)?, None => AccountState::default(), }; @@ -271,7 +269,7 @@ impl GuestProgramState { } // Store the added storage in the account's storage trie and compute its new root if !update.added_storage.is_empty() { - let storage_trie = self.storage_tries.entry(update.address).or_default(); + let storage_trie = self.storage_tries.entry(hashed_address).or_default(); // Inserts must come before deletes, otherwise deletes might require extra nodes // Example: @@ -295,8 +293,10 @@ impl GuestProgramState { account_state.storage_root = storage_root; } - self.state_trie - .insert(hashed_address.clone(), account_state.encode_to_vec())?; + self.state_trie.insert( + hashed_address.as_bytes().to_vec(), + account_state.encode_to_vec(), + )?; } } Ok(()) @@ -362,12 +362,12 @@ impl GuestProgramState { &mut self, address: Address, ) -> Result, GuestProgramStateError> { - let hashed_address = self + let hashed_address = *self .account_hashes_by_address .entry(address) .or_insert_with(|| hash_address(&address)); - let Ok(Some(encoded_state)) = self.state_trie.get(hashed_address) else { + let Ok(Some(encoded_state)) = self.state_trie.get(hashed_address.as_bytes()) else { return Ok(None); }; let state = AccountState::decode(&encoded_state).map_err(|_| { @@ -500,16 +500,24 @@ impl GuestProgramState { &mut self, address: Address, ) -> Result, GuestProgramStateError> { - let is_storage_verified = *self.verified_storage_roots.get(&address).unwrap_or(&false); + let hashed_address = *self + .account_hashes_by_address + .entry(address) + .or_insert_with(|| hash_address(&address)); + + let is_storage_verified = *self + .verified_storage_roots + .get(&hashed_address) + .unwrap_or(&false); if is_storage_verified { - Ok(self.storage_tries.get(&address)) + Ok(self.storage_tries.get(&hashed_address)) } else { let Some(storage_root) = self.get_account_state(address)?.map(|a| a.storage_root) else { // empty account return Ok(None); }; - let storage_trie = match self.storage_tries.get(&address) { + let storage_trie = match self.storage_tries.get(&hashed_address) { None if storage_root == *EMPTY_TRIE_HASH => return Ok(None), Some(trie) if trie.hash_no_commit() == storage_root => trie, _ => { @@ -518,14 +526,14 @@ impl GuestProgramState { ))); } }; - self.verified_storage_roots.insert(address, true); + self.verified_storage_roots.insert(hashed_address, true); Ok(Some(storage_trie)) } } } -fn hash_address(address: &Address) -> Vec { - keccak_hash(address.to_fixed_bytes()).to_vec() +fn hash_address(address: &Address) -> H256 { + H256(keccak_hash(address.to_fixed_bytes())) } pub fn hash_key(key: &H256) -> Vec { diff --git a/crates/networking/rpc/debug/execution_witness.rs b/crates/networking/rpc/debug/execution_witness.rs index c7459416bf8..c55d4d7c924 100644 --- a/crates/networking/rpc/debug/execution_witness.rs +++ b/crates/networking/rpc/debug/execution_witness.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use bytes::Bytes; use ethrex_common::{ - Address, H256, + H256, types::{ AccountState, BlockHeader, ChainConfig, block_execution_witness::{ExecutionWitness, GuestProgramStateError, RpcExecutionWitness}, @@ -10,8 +10,7 @@ use ethrex_common::{ utils::keccak, }; use ethrex_rlp::{decode::RLPDecode, error::RLPDecodeError}; -use ethrex_storage::hash_address; -use ethrex_trie::{EMPTY_TRIE_HASH, Node, NodeRef, Trie}; +use ethrex_trie::{EMPTY_TRIE_HASH, Nibbles, Node, NodeRef, Trie}; use serde_json::Value; use tracing::debug; @@ -64,36 +63,28 @@ pub fn execution_witness_from_rpc_chain_config( None }; - // get all storage trie roots and embed the rest of the trie into it - let state_trie = if let Some(state_trie_root) = &state_trie_root { - Trie::new_temp_with_root(state_trie_root.clone().into()) - } else { - Trie::new_temp() - }; + // Walk the state trie to discover accounts and their storage roots, + // instead of relying on the keys field which is being removed from the RPC spec. let mut storage_trie_roots = BTreeMap::new(); - for key in &rpc_witness.keys { - if key.len() != 20 { - continue; // not an address - } - let address = Address::from_slice(key); - let hashed_address = hash_address(&address); - let Some(encoded_account) = state_trie.get(&hashed_address)? else { - continue; // empty account, doesn't have a storage trie - }; - let storage_root_hash = AccountState::decode(&encoded_account)?.storage_root; - if storage_root_hash == *EMPTY_TRIE_HASH { - continue; // empty storage trie - } - if !nodes.contains_key(&storage_root_hash) { - continue; // storage trie isn't relevant to this execution + if let Some(state_trie_root) = &state_trie_root { + let mut accounts = Vec::new(); + collect_accounts_from_node(state_trie_root, Nibbles::from_bytes(&[]), &mut accounts); + + for (hashed_address, storage_root_hash) in accounts { + if storage_root_hash == *EMPTY_TRIE_HASH { + continue; // empty storage trie + } + if !nodes.contains_key(&storage_root_hash) { + continue; // storage trie isn't relevant to this execution + } + let node = Trie::get_embedded_root(&nodes, storage_root_hash)?; + let NodeRef::Node(node, _) = node else { + return Err(GuestProgramStateError::Custom( + "execution witness does not contain non-empty storage trie".to_string(), + )); + }; + storage_trie_roots.insert(hashed_address, (*node).clone()); } - let node = Trie::get_embedded_root(&nodes, storage_root_hash)?; - let NodeRef::Node(node, _) = node else { - return Err(GuestProgramStateError::Custom( - "execution witness does not contain non-empty storage trie".to_string(), - )); - }; - storage_trie_roots.insert(address, (*node).clone()); } let witness = ExecutionWitness { @@ -107,12 +98,40 @@ pub fn execution_witness_from_rpc_chain_config( .collect(), state_trie_root, storage_trie_roots, - keys: rpc_witness.keys.into_iter().map(|b| b.to_vec()).collect(), }; Ok(witness) } +/// Recursively walks an embedded state trie node and collects +/// `(hashed_address, storage_root)` pairs from leaf nodes. +fn collect_accounts_from_node(node: &Node, path: Nibbles, accounts: &mut Vec<(H256, H256)>) { + match node { + Node::Branch(branch) => { + for (i, child) in branch.choices.iter().enumerate() { + if let NodeRef::Node(child_node, _) = child { + collect_accounts_from_node(child_node, path.append_new(i as u8), accounts); + } + } + } + Node::Extension(ext) => { + if let NodeRef::Node(child_node, _) = &ext.child { + collect_accounts_from_node(child_node, path.concat(&ext.prefix), accounts); + } + } + Node::Leaf(leaf) => { + let full_path = path.concat(&leaf.partial); + let path_bytes = full_path.to_bytes(); + if path_bytes.len() == 32 { + let hashed_address = H256::from_slice(&path_bytes); + if let Ok(account_state) = AccountState::decode(&leaf.value) { + accounts.push((hashed_address, account_state.storage_root)); + } + } + } + } +} + pub struct ExecutionWitnessRequest { pub from: BlockIdentifier, pub to: Option, From f14ab5af28cbcfbafc8d763ad7e3b19d6c33bd0b Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 13 Mar 2026 12:09:09 -0300 Subject: [PATCH 2/4] Add ExecutionWitness-related paths to L2 integration test workflow triggers so that changes to the witness type definition or RPC endpoint also run L2 tests --- .github/workflows/pr-main_l2.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index 9f81dec502f..feb97d0c8d1 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -45,6 +45,8 @@ jobs: - "fixtures/contracts/**" - "crates/blockchain/dev/**" - "crates/vm/levm/**" + - "crates/common/types/block_execution_witness.rs" + - "crates/networking/rpc/debug/execution_witness.rs" - ".github/workflows/pr-main_l2.yaml" - "cmd/ethrex/l2/**" non_docs: From d8295cef54a5678a6d6910d93da212ca119f867c Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 13 Mar 2026 15:13:56 -0300 Subject: [PATCH 3/4] Add replay-witness-test job to L2 CI workflow that clones ethrex-replay, patches its ethrex dependencies with the current PR's local checkout using Cargo [patch], then sends a transaction to a dev L1 node and replays the block to verify the execution witness end-to-end. --- .github/workflows/pr-main_l2.yaml | 119 +++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index feb97d0c8d1..c5abcdb4e8c 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -928,6 +928,117 @@ jobs: cargo test -p ethrex-test forced_inclusion --release --features l2 -- --nocapture --test-threads=1 killall ethrex -s SIGINT + replay-witness-test: + name: Replay Witness Test + runs-on: ubuntu-latest + needs: [detect-changes, build-docker] + if: ${{ needs.detect-changes.outputs.run_tests == 'true' && github.event_name != 'merge_group' }} + timeout-minutes: 30 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Setup Rust Environment + uses: ./.github/actions/setup-rust + + - name: Download ethrex image artifact + uses: actions/download-artifact@v4 + with: + name: ethrex_image + path: /tmp + + - name: Load ethrex image + run: docker load --input /tmp/ethrex_image.tar + + - name: Start L1 dev node + run: | + cd crates/l2 + docker compose up --detach ethrex_l1 + + - name: Wait for L1 to be ready + run: | + MAX_TRIES=30 + for i in $(seq 1 $MAX_TRIES); do + if curl -sf -X POST http://localhost:8545 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' > /dev/null 2>&1; then + echo "L1 is ready" + exit 0 + fi + echo "Waiting for L1... (attempt $i)" + sleep 2 + done + echo "L1 failed to start" + docker logs ethrex_l1 + exit 1 + + - name: Install rex + run: | + curl -L https://github.com/lambdaclass/rex/releases/download/v8.0.0/rex-linux-x86_64 -o /tmp/rex + chmod +x /tmp/rex + sudo mv /tmp/rex /usr/local/bin/rex + + - name: Clone and build ethrex-replay + run: | + git clone https://github.com/lambdaclass/ethrex-replay.git /tmp/ethrex-replay + cd /tmp/ethrex-replay + + # Override ethrex deps with the local checkout from the current PR. + # This ensures ethrex-replay always builds against the PR's code, + # regardless of what branch ethrex-replay's Cargo.toml points to. + cat >> Cargo.toml << PATCH + + [patch."https://github.com/lambdaclass/ethrex"] + ethrex-common = { path = "$GITHUB_WORKSPACE/crates/common" } + ethrex-config = { path = "$GITHUB_WORKSPACE/crates/common/config" } + ethrex-crypto = { path = "$GITHUB_WORKSPACE/crates/common/crypto" } + ethrex-rlp = { path = "$GITHUB_WORKSPACE/crates/common/rlp" } + ethrex-trie = { path = "$GITHUB_WORKSPACE/crates/common/trie" } + ethrex-storage = { path = "$GITHUB_WORKSPACE/crates/storage" } + ethrex-vm = { path = "$GITHUB_WORKSPACE/crates/vm" } + ethrex-levm = { path = "$GITHUB_WORKSPACE/crates/vm/levm" } + ethrex-blockchain = { path = "$GITHUB_WORKSPACE/crates/blockchain" } + ethrex-rpc = { path = "$GITHUB_WORKSPACE/crates/networking/rpc" } + ethrex-p2p = { path = "$GITHUB_WORKSPACE/crates/networking/p2p" } + ethrex-l2 = { path = "$GITHUB_WORKSPACE/crates/l2" } + ethrex-l2-common = { path = "$GITHUB_WORKSPACE/crates/l2/common" } + ethrex-l2-rpc = { path = "$GITHUB_WORKSPACE/crates/l2/networking/rpc" } + ethrex-storage-rollup = { path = "$GITHUB_WORKSPACE/crates/l2/storage" } + ethrex-sdk = { path = "$GITHUB_WORKSPACE/crates/l2/sdk" } + ethrex-prover = { path = "$GITHUB_WORKSPACE/crates/l2/prover" } + ethrex-guest-program = { path = "$GITHUB_WORKSPACE/crates/guest-program" } + PATCH + + cargo build --release + + - name: Send test transaction and run replay + run: | + # Use a pre-funded dev account (from l1.json genesis) + PRIVATE_KEY="0x850643a0224065ecce3882673c21f56bcf6eef86274cc21cadff15930b59fc8c" + TX_HASH=$(rex send 0x0000000000000000000000000000000000000001 \ + --value 1000000000000000000 \ + -k "$PRIVATE_KEY" \ + --rpc-url http://localhost:8545 2>&1 | head -1) + echo "Sent transaction: $TX_HASH" + + # Wait for receipt and extract block number + sleep 3 + BLOCK_NUMBER=$(curl -sf -X POST http://localhost:8545 \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$TX_HASH\"],\"id\":1}" \ + | python3 -c "import sys,json; r=json.load(sys.stdin); print(int(r['result']['blockNumber'], 16))") + echo "Transaction included in block $BLOCK_NUMBER" + + # Run replay on the block containing the transaction + cd /tmp/ethrex-replay + RUST_LOG=info ./target/release/ethrex-replay block "$BLOCK_NUMBER" \ + --rpc-url http://localhost:8545 \ + --no-zkvm + + - name: Dump L1 logs on failure + if: ${{ failure() }} + run: docker logs ethrex_l1 2>&1 | tail -100 + # The purpose of this job is to add it as a required check in GitHub so that we don't have to add every individual job as a required check all-tests: # "Integration Test L2" is a required check, don't change the name @@ -941,9 +1052,10 @@ jobs: integration-test-tdx, uniswap-swap, integration-test-shared-bridge, + replay-witness-test, ] # Make sure this job runs even if the previous jobs failed or were skipped - if: ${{ needs.detect-changes.outputs.run_tests == 'true' && always() && needs.integration-test.result != 'skipped' && needs.state-diff-test.result != 'skipped' && needs.integration-test-tdx.result != 'skipped' && needs.uniswap-swap.result != 'skipped' && needs.integration-test-shared-bridge.result != 'skipped' }} + if: ${{ needs.detect-changes.outputs.run_tests == 'true' && always() && needs.integration-test.result != 'skipped' && needs.state-diff-test.result != 'skipped' && needs.integration-test-tdx.result != 'skipped' && needs.uniswap-swap.result != 'skipped' && needs.integration-test-shared-bridge.result != 'skipped' && needs.replay-witness-test.result != 'skipped' }} steps: - name: Check if any job failed run: | @@ -966,3 +1078,8 @@ jobs: echo "Job Integration test shared bridge failed" exit 1 fi + + if [ "${{ needs.replay-witness-test.result }}" != "success" ]; then + echo "Job Replay Witness Test failed" + exit 1 + fi From fb44ed7213186649053c3bc7d177ac7836f21d67 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Tue, 17 Mar 2026 10:20:50 -0300 Subject: [PATCH 4/4] Pin ethrex-replay to a specific commit (c6bc19f) in the replay-witness-test CI job instead of cloning HEAD. This prevents unrelated replay regressions from breaking ethrex CI. The pinned SHA should be updated when replay is modified to match new ethrex API changes. --- .github/workflows/pr-main_l2.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pr-main_l2.yaml b/.github/workflows/pr-main_l2.yaml index c5abcdb4e8c..26b28049964 100644 --- a/.github/workflows/pr-main_l2.yaml +++ b/.github/workflows/pr-main_l2.yaml @@ -980,8 +980,13 @@ jobs: - name: Clone and build ethrex-replay run: | + # Pin to a specific ethrex-replay commit to avoid unrelated replay + # regressions breaking ethrex CI. Update this SHA when replay is + # updated to match new ethrex API changes. + REPLAY_REF="c6bc19f" git clone https://github.com/lambdaclass/ethrex-replay.git /tmp/ethrex-replay cd /tmp/ethrex-replay + git checkout "$REPLAY_REF" # Override ethrex deps with the local checkout from the current PR. # This ensures ethrex-replay always builds against the PR's code,