Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
711f317
feat: JS tracer
dimazhornyk Oct 30, 2025
5ee0554
resolve conflicts
dimazhornyk Oct 30, 2025
8fdd8b9
fix test
dimazhornyk Oct 30, 2025
03b3f1e
fix test
dimazhornyk Oct 30, 2025
4c65e9f
refactoring, code cleanup
dimazhornyk Oct 31, 2025
26e718b
Cargo.toml cleanup
dimazhornyk Oct 31, 2025
6d12d27
improve error handing, reuse opcode mapping
dimazhornyk Oct 31, 2025
584a4f2
make the tracing interface more geth compatible
dimazhornyk Nov 3, 2025
b8c5c70
align tracing interface with geth
dimazhornyk Nov 4, 2025
ed930bf
rollback state overlays, clean up the code
dimazhornyk Nov 4, 2025
f1de361
add js wrappers
dimazhornyk Nov 4, 2025
65d25da
Merge branch 'main' of github.com:matter-labs/zksync-os-server into f…
dimazhornyk Nov 5, 2025
1c5f724
change js tracer passing, clean up the code
dimazhornyk Nov 6, 2025
c3690c3
add balance overlays
dimazhornyk Nov 6, 2025
5b12d6d
resolve conflicts
dimazhornyk Nov 6, 2025
9fcb60a
use U256 for balance overlays
dimazhornyk Nov 6, 2025
ebf4a72
add overlay checkpoints
dimazhornyk Nov 7, 2025
9020025
resolve conflicts
dimazhornyk Nov 7, 2025
c61380f
Revert "resolve conflicts"
dimazhornyk Nov 8, 2025
88bc37c
Reapply "resolve conflicts"
dimazhornyk Nov 8, 2025
d7acbcb
fix Cargo.lock
dimazhornyk Nov 8, 2025
1b24930
downgrade aws-sdk-s3
dimazhornyk Nov 10, 2025
7f500c9
rollback some Cargo.lock changes
dimazhornyk Nov 10, 2025
1850933
fix integration tests
dimazhornyk Nov 10, 2025
9507a79
implement stack (no dependencies, expected to fail)
dimazhornyk Nov 12, 2025
52aa5a0
fix dependencies
dimazhornyk Nov 21, 2025
2c38b93
resolve conflicts
dimazhornyk Dec 3, 2025
45dd30c
resolve conflicts
dimazhornyk Dec 3, 2025
f94ab6f
fmt
dimazhornyk Dec 3, 2025
2cc89d0
add code size limit
dimazhornyk Dec 3, 2025
865c95f
fix clippy
dimazhornyk Dec 3, 2025
3bbd8a9
add sanity check in on_storage_read
dimazhornyk Dec 3, 2025
2b58b10
resolve conflicts
dimazhornyk Dec 3, 2025
66eb3af
fmt
dimazhornyk Dec 3, 2025
3d960d7
lint
dimazhornyk Dec 3, 2025
0c845ec
add selfdestruct overlay
dimazhornyk Dec 3, 2025
e34dbb3
update selfdestruct overlays implementation
dimazhornyk Dec 4, 2025
62b20f3
write a flag to an overlay even if it is vacant
dimazhornyk Dec 4, 2025
b193014
fix selfdestruct overlay
dimazhornyk Dec 9, 2025
49a500d
resolve conflicts
dimazhornyk Dec 9, 2025
1aded93
resolve conflicts
dimazhornyk Dec 9, 2025
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
889 changes: 694 additions & 195 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ zk_os_forward_system = { package = "forward_system", git = "https://github.com/m
zk_ee = { package = "zk_ee", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
zk_os_basic_system = { package = "basic_system", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
zk_os_api = { package = "zksync_os_api", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
zk_os_evm_interpreter = { package = "evm_interpreter", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }

#execution_utils = { package = "execution_utils", path = "../zksync-airbender/execution_utils" }
#full_statement_verifier = { package = "full_statement_verifier", path = "../zksync-airbender/full_statement_verifier" }
Expand Down Expand Up @@ -164,6 +165,8 @@ url = "2.5.7"
num_enum = "0.7.2"
metrics = "0.24.2"
chrono = { version = "0.4", default-features = false }
boa_engine = { version = "0.21.0" }
boa_gc = { version = "0.21.0" }
structdiff = { version = "0.7.3", features = ["debug_diffs"] }

tracing-opentelemetry = "0.32.0"
Expand Down
10 changes: 9 additions & 1 deletion integration-tests/test-contracts/src/TracingPrimary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import "./TracingSecondary.sol";
contract TracingPrimary {
TracingSecondary secondary;

uint256 public lastCalculated;
event CalculationDone(uint256 indexed input, uint256 indexed result);

constructor(address _secondary) {
secondary = TracingSecondary(_secondary);
}
Expand All @@ -14,7 +17,12 @@ contract TracingPrimary {
}

function calculate(uint256 value) public returns (uint) {
return secondary.multiply(value);
uint result = secondary.multiply(value);
lastCalculated = result;

emit CalculationDone(value, result);

return result;
}

function shouldRevert() public view returns (uint) {
Expand Down
243 changes: 243 additions & 0 deletions integration-tests/tests/js_tracer_cross_node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
use alloy::eips::BlockId;
use alloy::network::{EthereumWallet, TransactionBuilder};
use alloy::primitives::U256;
use alloy::providers::ProviderBuilder;
use alloy::providers::ext::DebugApi;
use alloy::rpc::types::TransactionRequest;
use alloy::rpc::types::trace::geth::{GethDebugTracerType, GethDebugTracingCallOptions, GethTrace};
use alloy::signers::local::LocalSigner;
use serde_json::Value;
use std::env;
use std::str::FromStr;
use tracing::info;
use zksync_os_integration_tests::contracts::{TracingPrimary, TracingSecondary};
use zksync_os_integration_tests::dyn_wallet_provider::EthDynProvider;

const ZKSYNC_URL_ENV: &str = "JS_TRACER_ZKSYNC_RPC_URL";
const RETH_URL_ENV: &str = "JS_TRACER_RETH_RPC_URL";
const PRIVATE_KEY_ENV: &str = "JS_TRACER_PRIVATE_KEY";

// This test is left here in case we want to do cross-node JS tracer comparisons in the future.
// It is ignored by default since it requires setting up two nodes and providing their RPC URLs
#[ignore]
#[test_log::test(tokio::test)]
async fn compare_js_tracer_outputs_between_nodes() -> anyhow::Result<()> {
let Some(zksync_url) = env::var(ZKSYNC_URL_ENV).ok() else {
info!("skipping cross-node tracer comparison; {ZKSYNC_URL_ENV} is not set");
return Ok(());
};
let Some(reth_url) = env::var(RETH_URL_ENV).ok() else {
info!("skipping cross-node tracer comparison; {RETH_URL_ENV} is not set");
return Ok(());
};
let Some(private_key) = env::var(PRIVATE_KEY_ENV).ok() else {
info!("skipping cross-node tracer comparison; {PRIVATE_KEY_ENV} is not set");
return Ok(());
};

let wallet = EthereumWallet::new(LocalSigner::from_str(&private_key)?);
let wallet_address = wallet.default_signer().address();

let zksync_provider = ProviderBuilder::new()
.wallet(wallet.clone())
.connect(&zksync_url)
.await?;
let reth_provider = ProviderBuilder::new()
.wallet(wallet.clone())
.connect(&reth_url)
.await?;

let zksync_provider = EthDynProvider::new(zksync_provider);
let reth_provider = EthDynProvider::new(reth_provider);

// Deploy helper contracts on both nodes.
let secondary_init_value = U256::from(7);
let secondary_zksync =
TracingSecondary::deploy(zksync_provider.clone(), secondary_init_value).await?;
let primary_zksync =
TracingPrimary::deploy(zksync_provider.clone(), *secondary_zksync.address()).await?;

let secondary_reth =
TracingSecondary::deploy(reth_provider.clone(), secondary_init_value).await?;
let primary_reth =
TracingPrimary::deploy(reth_provider.clone(), *secondary_reth.address()).await?;

// Prepare calls we want to compare.
let calculate_value = U256::from(3);
let mut calc_zksync_request = primary_zksync
.calculate(calculate_value)
.into_transaction_request();
configure_call_request(&mut calc_zksync_request, wallet_address);
let mut calc_reth_request = primary_reth
.calculate(calculate_value)
.into_transaction_request();
configure_call_request(&mut calc_reth_request, wallet_address);

let call_scenarios = vec![("calculate", calc_zksync_request, calc_reth_request)];

let tracers = vec![
(
"minimal_tracer",
r#"
{
maxSteps: 256,

setup: function () {
this.steps = [];
},

step: function (log, db) {
const rec = {
pc: log.getPC(),
op: log.op.toString(),
gas: log.getGas(),
gasCost: log.getCost(),
depth: log.getDepth(),
error: log.getError ? ("" + log.getError()) : null
};
this.steps.push(rec);
if (this.steps.length > this.maxSteps) this.steps.shift();
},

fault: function (log, db) {
this.faultLog = {
pc: log.getPC(),
op: log.op.toString(),
gas: log.getGas(),
depth: log.getDepth(),
error: "" + log.getError()
};
},

result: function (ctx, db) {
return {
type: "minimal-steps",
lastSteps: this.steps,
fault: this.faultLog || null
};
}
}
"#,
),
(
"value_transfer_tracer",
r#" {
setup: function () {
this.totalCost = 0;
this.byOp = {};
},

step: function (log, db) {
var op = log.op.toString();
var cost = log.getCost();
this.totalCost += cost;

var e = this.byOp[op];
if (!e) this.byOp[op] = { count: 1, cost: cost };
else { e.count += 1; e.cost += cost; }
},

result: function () {
var hot = [];
for (var k in this.byOp) {
hot.push({ op: k, count: this.byOp[k].count, cost: this.byOp[k].cost });
}
hot.sort(function (a, b) { return b.cost - a.cost; });

return {
type: "gas-profiler",
totalIntrinsicCostApprox: this.totalCost,
hotOpcodes: hot
};
}
}
"#,
),
(
"opcode_coverage_tracer",
r#"
{
setup: function () {
this.cover = {};
this.pcs = {};
},

step: function (log, db) {
var op = log.op.toString();
this.cover[op] = true;

var pc = log.getPC();
var m = this.pcs[op];
if (!m) { m = {}; this.pcs[op] = m; }
m[pc] = true;
},

result: function () {
var ops = [];
for (var k in this.cover) {
var pcs = Object.keys(this.pcs[k]).map(function (x) { return Number(x); });
ops.push({ op: k, uniquePCs: pcs.length });
}
ops.sort(function (a, b) { return a.op < b.op ? -1 : 1; });

return { type: "opcode-coverage", ops: ops };
}
}
"#,
),
];

for (scenario_name, zk_request, reth_request) in call_scenarios {
for (tracer_name, tracer_code) in &tracers {
let reth_trace =
trace_with_js(&reth_provider, reth_request.clone(), tracer_code).await?;
let zk_trace = trace_with_js(&zksync_provider, zk_request.clone(), tracer_code).await?;
let mut zk_trace = zk_trace;
let mut reth_trace = reth_trace;
normalize_hex_strings(&mut zk_trace);
normalize_hex_strings(&mut reth_trace);
assert_eq!(
zk_trace, reth_trace,
"JS tracer '{tracer_name}' produced different output for scenario '{scenario_name}'"
);
}
}

Ok(())
}

fn configure_call_request(req: &mut TransactionRequest, from: alloy::primitives::Address) {
req.set_from(from);
req.max_priority_fee_per_gas = Some(1);
req.max_fee_per_gas = Some(u128::MAX);
}

async fn trace_with_js(
provider: &EthDynProvider,
request: TransactionRequest,
tracer_code: &str,
) -> anyhow::Result<Value> {
let mut opts = GethDebugTracingCallOptions::default();

opts.tracing_options.tracer = Some(GethDebugTracerType::JsTracer(tracer_code.to_string()));
let trace = provider
.debug_trace_call(request, BlockId::latest(), opts)
.await?;
match trace {
GethTrace::JS(value) => Ok(value),
other => anyhow::bail!("expected JS trace result, got {other:?}"),
}
}

fn normalize_hex_strings(value: &mut Value) {
match value {
Value::String(data) => {
if data.starts_with("0x") {
*data = data.to_lowercase();
}
}
Value::Array(values) => values.iter_mut().for_each(normalize_hex_strings),
Value::Object(map) => map.values_mut().for_each(normalize_hex_strings),
_ => {}
}
}
Loading
Loading