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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ members = [
"supporting_crates/solidity_abi",
"supporting_crates/solidity_abi_derive",
"tests/rig",
"tests/revm_runner",
"tests/instances/transactions",
"tests/instances/erc20",
"tests/instances/forge_tests",
Expand All @@ -30,6 +31,7 @@ members = [
"tests/instances/multiblock_batch",
"tests/instances/header",
"tests/instances/evm",
"tests/instances/block_reexecutor",
"cycle_marker",
"tests/binary_checker",
"supporting_crates/delegated_u256",
Expand Down
2 changes: 1 addition & 1 deletion forward_system/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ zksync_os_interface = { workspace = true, default-features = false }
system_hooks = { path = "../system_hooks", default-features = false }

ruint = { version = "1.12.3", default-features = false, features = ["alloc"] }
alloy = { version = "1", default-features = false, features = ["consensus"] }
alloy = { version = "1", default-features = false, features = ["consensus", "rpc-types-trace"] }
arrayvec = { version = "0.7.4", default-features = false }
hex = { version = "*" }
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
Expand Down
158 changes: 157 additions & 1 deletion forward_system/src/system/tracers/call_tracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
//! missing edge cases, and other limitations that make it unsuitable
//! for production environments.

use alloy::primitives::{Address, Bytes, B256};
use alloy::rpc::types::trace::geth::{
CallFrame as AlloyCallFrame, CallLogFrame as AlloyCallLogFrame,
};
use evm_interpreter::ERGS_PER_GAS;
use ruint::aliases::{B160, U256};
use zk_ee::system::{
Expand Down Expand Up @@ -57,6 +61,22 @@ impl CallType {
},
}
}

fn as_str(&self) -> &'static str {
match self {
CallType::Call => "CALL",
CallType::Create => "CREATE",
CallType::Create2 => "CREATE2",
CallType::Delegate => "DELEGATECALL",
CallType::Static => "STATICCALL",
CallType::DelegateStatic => "DELEGATECALL",
CallType::EVMCallcode => "CALLCODE",
CallType::EVMCallcodeStatic => "CALLCODE",
CallType::ZKVMSystem => "ZKVM_SYSTEM",
CallType::ZKVMSystemStatic => "ZKVM_SYSTEM_STATIC",
CallType::Selfdestruct => "SELFDESTRUCT",
}
}
}

#[derive(Default, Debug)]
Expand Down Expand Up @@ -94,7 +114,7 @@ pub enum CreateType {
Create2,
}

#[derive(Default)]
#[derive(Default, Debug)]
pub struct CallTracer {
pub transactions: Vec<Call>,
pub unfinished_calls: Vec<Call>,
Expand All @@ -106,6 +126,142 @@ pub struct CallTracer {
create_operation_requested: Option<CreateType>,
}

impl std::fmt::Display for CallTracer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, tx) in self.transactions.iter().enumerate() {
writeln!(f, "Transaction {}:", i)?;
tx.fmt_with_indent(f, 2)?;
writeln!(f)?;
}
Ok(())
}
}

impl Call {
fn fmt_with_indent(&self, f: &mut std::fmt::Formatter<'_>, indent: usize) -> std::fmt::Result {
let pad = " ".repeat(indent);
let pad2 = " ".repeat(indent + 2);

let from_formatted = hex::encode(self.from.to_be_bytes_vec());
let to_formatted = hex::encode(self.to.to_be_bytes_vec());

writeln!(
f,
"{}Call from 0x{} to 0x{}",
pad, from_formatted, to_formatted
)?;
writeln!(f, "{}Type: {:?}", pad2, self.call_type)?;
writeln!(f, "{}Value: {}", pad2, self.value)?;
writeln!(f, "{}Gas: {} used {}", pad2, self.gas, self.gas_used)?;
writeln!(f, "{}Reverted: {}", pad2, self.reverted)?;

if let Some(error) = &self.error {
writeln!(f, "{}Error: {:?}", pad2, error)?;
}

writeln!(f, "{}Input: 0x{}", pad2, hex::encode(&self.input))?;
writeln!(f, "{}Output: 0x{}", pad2, hex::encode(&self.output))?;

if !self.logs.is_empty() {
writeln!(f, "{}Logs:", pad2)?;
for log in &self.logs {
writeln!(
f,
"{}- {:?} topics {:?} data 0x{}",
pad2,
log.address,
log.topics,
hex::encode(&log.data)
)?;
}
}

if !self.calls.is_empty() {
writeln!(f, "{}Subcalls:", pad2)?;
for call in &self.calls {
call.fmt_with_indent(f, indent + 4)?;
}
}

Ok(())
}
}

impl std::fmt::Display for Call {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_with_indent(f, 0)
}
}

impl Call {
fn to_alloy_call_frame(&self) -> AlloyCallFrame {
let is_create = matches!(self.call_type, CallType::Create | CallType::Create2);

AlloyCallFrame {
from: Address::from(self.from.to_be_bytes()),
gas: U256::from(self.gas),
gas_used: U256::from(self.gas_used),
to: if is_create {
None
} else {
Some(Address::from(self.to.to_be_bytes()))
},
input: Bytes::from(self.input.clone()),
output: Some(Bytes::from(self.output.clone())),
error: self.error.as_ref().map(|error| match error {
CallError::EvmError(err) => format!("{:?}", err),
CallError::FatalError(err) => err.clone(),
}),
revert_reason: None, // TODO skipped for now
calls: self.calls.iter().map(Into::into).collect(),
logs: self.logs.iter().map(Into::into).collect(),
value: Some(self.value),
typ: self.call_type.as_str().to_owned(),
}
}
}

impl CallLogFrame {
fn to_alloy_call_log_frame(&self) -> AlloyCallLogFrame {
AlloyCallLogFrame {
address: Some(Address::from(self.address.to_be_bytes())),
topics: Some(
self.topics
.iter()
.map(|topic| B256::from(topic.as_u8_array()))
.collect(),
),
data: Some(Bytes::from(self.data.clone())),
position: None, // TODO
index: None, // TODO
}
}
}

impl From<&Call> for AlloyCallFrame {
fn from(value: &Call) -> Self {
value.to_alloy_call_frame()
}
}

impl From<Call> for AlloyCallFrame {
fn from(value: Call) -> Self {
(&value).into()
}
}

impl From<&CallLogFrame> for AlloyCallLogFrame {
fn from(value: &CallLogFrame) -> Self {
value.to_alloy_call_log_frame()
}
}

impl From<CallLogFrame> for AlloyCallLogFrame {
fn from(value: CallLogFrame) -> Self {
(&value).into()
}
}

impl CallTracer {
pub fn new_with_config(collect_logs: bool, only_top_call: bool) -> Self {
Self {
Expand Down
6 changes: 6 additions & 0 deletions tests/instances/block_reexecutor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/target
/Cargo.lock

.DS_Store
.idea
.cache
19 changes: 19 additions & 0 deletions tests/instances/block_reexecutor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "block_reexecutor"
version = "0.1.0"
edition = "2021"

# Empty workspace to prevent inheriting from parent workspace
# [workspace]

[dependencies]
rig = { path = "../../rig", features = ["production"] }
zksync_os_revm_runner = { path = "../../revm_runner" }
alloy = { version = "1", features = ["full"] }
ruint = { version = "1.12.3", default-features = false, features = ["alloc"] }
anyhow = "=1.0.89"
clap = { version = "4.4", features = ["derive"] }
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
ureq = { version = "2", features = ["json"] }
66 changes: 66 additions & 0 deletions tests/instances/block_reexecutor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# block_reexecutor

Re-executes a single L2 block against RPC state, validates transaction outputs against RPC receipts, and writes call traces.

## What it does

- Loads block data and metadata from RPC (or from disk cache).
- Executes transactions with `RpcValueOracleFactory`.
- Compares execution results against RPC receipts:
- success/failure status
- gas used
- logs count/content
- Runs REVM on the same block context/state.
- Writes two trace files in geth call tracer format (`CallFrame[]`).

## Run

```bash
RUST_LOG=info cargo run -p block_reexecutor -- --endpoint <rpc> --block-hash <block_hash>
```

### Run With Predefined Transactions

Use state/block metadata from `--block-number`, but execute transactions loaded from a JSON file:

```bash
RUST_LOG=info cargo run -p block_reexecutor -- \
--endpoint <rpc> \
--block-number <block_number> \
--transactions-file <path/to/transactions.json>
```

`transactions.json` must contain RLP tx payload + signer, with tx bytes encoded as hex string:

```json
[
{
"tx": "0x02f86e82853901843b...",
"signer": "0x1111111111111111111111111111111111111111",
"hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
]
```

Also accepted for compatibility:

- `{"rlp":"0x...", "signer":"0x...", "hash":"0x..."}`
- `{"Rlp":["0x...", "0x..."], "hash":"0x..."}`
- previous `EncodedTx` JSON with byte arrays

The tool decodes every RLP payload into an Ethereum transaction for REVM tracing and uses the same `EncodedTx::Rlp` values for block reexecution.
If `hash` is provided for predefined txs, execution results are validated against RPC receipts matched by hash.
If RPC returns `null` for any provided hash, the tool logs it and skips receipt checks.

## Output files

- `tracer_output_<block_number>.json`
- `revm_call_trace_<block_number>.json`

## Cache files

All cache files are stored under:

- `.cache/block_reexecutor/`

If you need a full refetch, delete the cache.
Loading
Loading