Skip to content
Draft
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
126 changes: 125 additions & 1 deletion .github/workflows/pr-main_l2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -926,6 +928,122 @@ 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: |
# 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,
# 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
Expand All @@ -939,9 +1057,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: |
Expand All @@ -964,3 +1083,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
42 changes: 8 additions & 34 deletions crates/blockchain/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H256, Node> = used_trie_nodes
.into_iter()
Expand All @@ -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
};
Expand All @@ -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 {
Expand All @@ -1605,7 +1593,6 @@ impl Blockchain {
chain_config: self.storage.get_chain_config(),
state_trie_root,
storage_trie_roots,
keys,
})
}

Expand Down Expand Up @@ -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<H256, Node> = used_trie_nodes
.into_iter()
Expand All @@ -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
};
Expand All @@ -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 {
Expand All @@ -1847,7 +1822,6 @@ impl Blockchain {
chain_config: self.storage.get_chain_config(),
state_trie_root,
storage_trie_roots,
keys,
})
}

Expand Down
Loading
Loading