Skip to content

Commit c991043

Browse files
authored
feat: JS tracer (#569)
This PR adds a JS tracer implementation. It uses Boa JS runtime to call the JS functions when zksync-os execution triggers a corresponding hook. State access is implemented by using the captured StateView and storage overlays in the JS context.
1 parent cd8a611 commit c991043

File tree

16 files changed

+3064
-235
lines changed

16 files changed

+3064
-235
lines changed

Cargo.lock

Lines changed: 694 additions & 195 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ zk_os_forward_system = { package = "forward_system", git = "https://github.com/m
9696
zk_ee = { package = "zk_ee", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
9797
zk_os_basic_system = { package = "basic_system", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
9898
zk_os_api = { package = "zksync_os_api", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
99+
zk_os_evm_interpreter = { package = "evm_interpreter", git = "https://github.com/matter-labs/zksync-os", tag = "v0.2.4" }
99100

100101
#execution_utils = { package = "execution_utils", path = "../zksync-airbender/execution_utils" }
101102
#full_statement_verifier = { package = "full_statement_verifier", path = "../zksync-airbender/full_statement_verifier" }
@@ -164,6 +165,8 @@ url = "2.5.7"
164165
num_enum = "0.7.2"
165166
metrics = "0.24.2"
166167
chrono = { version = "0.4", default-features = false }
168+
boa_engine = { version = "0.21.0" }
169+
boa_gc = { version = "0.21.0" }
167170
structdiff = { version = "0.7.3", features = ["debug_diffs"] }
168171

169172
tracing-opentelemetry = "0.32.0"

integration-tests/test-contracts/src/TracingPrimary.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import "./TracingSecondary.sol";
55
contract TracingPrimary {
66
TracingSecondary secondary;
77

8+
uint256 public lastCalculated;
9+
event CalculationDone(uint256 indexed input, uint256 indexed result);
10+
811
constructor(address _secondary) {
912
secondary = TracingSecondary(_secondary);
1013
}
@@ -14,7 +17,12 @@ contract TracingPrimary {
1417
}
1518

1619
function calculate(uint256 value) public returns (uint) {
17-
return secondary.multiply(value);
20+
uint result = secondary.multiply(value);
21+
lastCalculated = result;
22+
23+
emit CalculationDone(value, result);
24+
25+
return result;
1826
}
1927

2028
function shouldRevert() public view returns (uint) {
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
use alloy::eips::BlockId;
2+
use alloy::network::{EthereumWallet, TransactionBuilder};
3+
use alloy::primitives::U256;
4+
use alloy::providers::ProviderBuilder;
5+
use alloy::providers::ext::DebugApi;
6+
use alloy::rpc::types::TransactionRequest;
7+
use alloy::rpc::types::trace::geth::{GethDebugTracerType, GethDebugTracingCallOptions, GethTrace};
8+
use alloy::signers::local::LocalSigner;
9+
use serde_json::Value;
10+
use std::env;
11+
use std::str::FromStr;
12+
use tracing::info;
13+
use zksync_os_integration_tests::contracts::{TracingPrimary, TracingSecondary};
14+
use zksync_os_integration_tests::dyn_wallet_provider::EthDynProvider;
15+
16+
const ZKSYNC_URL_ENV: &str = "JS_TRACER_ZKSYNC_RPC_URL";
17+
const RETH_URL_ENV: &str = "JS_TRACER_RETH_RPC_URL";
18+
const PRIVATE_KEY_ENV: &str = "JS_TRACER_PRIVATE_KEY";
19+
20+
// This test is left here in case we want to do cross-node JS tracer comparisons in the future.
21+
// It is ignored by default since it requires setting up two nodes and providing their RPC URLs
22+
#[ignore]
23+
#[test_log::test(tokio::test)]
24+
async fn compare_js_tracer_outputs_between_nodes() -> anyhow::Result<()> {
25+
let Some(zksync_url) = env::var(ZKSYNC_URL_ENV).ok() else {
26+
info!("skipping cross-node tracer comparison; {ZKSYNC_URL_ENV} is not set");
27+
return Ok(());
28+
};
29+
let Some(reth_url) = env::var(RETH_URL_ENV).ok() else {
30+
info!("skipping cross-node tracer comparison; {RETH_URL_ENV} is not set");
31+
return Ok(());
32+
};
33+
let Some(private_key) = env::var(PRIVATE_KEY_ENV).ok() else {
34+
info!("skipping cross-node tracer comparison; {PRIVATE_KEY_ENV} is not set");
35+
return Ok(());
36+
};
37+
38+
let wallet = EthereumWallet::new(LocalSigner::from_str(&private_key)?);
39+
let wallet_address = wallet.default_signer().address();
40+
41+
let zksync_provider = ProviderBuilder::new()
42+
.wallet(wallet.clone())
43+
.connect(&zksync_url)
44+
.await?;
45+
let reth_provider = ProviderBuilder::new()
46+
.wallet(wallet.clone())
47+
.connect(&reth_url)
48+
.await?;
49+
50+
let zksync_provider = EthDynProvider::new(zksync_provider);
51+
let reth_provider = EthDynProvider::new(reth_provider);
52+
53+
// Deploy helper contracts on both nodes.
54+
let secondary_init_value = U256::from(7);
55+
let secondary_zksync =
56+
TracingSecondary::deploy(zksync_provider.clone(), secondary_init_value).await?;
57+
let primary_zksync =
58+
TracingPrimary::deploy(zksync_provider.clone(), *secondary_zksync.address()).await?;
59+
60+
let secondary_reth =
61+
TracingSecondary::deploy(reth_provider.clone(), secondary_init_value).await?;
62+
let primary_reth =
63+
TracingPrimary::deploy(reth_provider.clone(), *secondary_reth.address()).await?;
64+
65+
// Prepare calls we want to compare.
66+
let calculate_value = U256::from(3);
67+
let mut calc_zksync_request = primary_zksync
68+
.calculate(calculate_value)
69+
.into_transaction_request();
70+
configure_call_request(&mut calc_zksync_request, wallet_address);
71+
let mut calc_reth_request = primary_reth
72+
.calculate(calculate_value)
73+
.into_transaction_request();
74+
configure_call_request(&mut calc_reth_request, wallet_address);
75+
76+
let call_scenarios = vec![("calculate", calc_zksync_request, calc_reth_request)];
77+
78+
let tracers = vec![
79+
(
80+
"minimal_tracer",
81+
r#"
82+
{
83+
maxSteps: 256,
84+
85+
setup: function () {
86+
this.steps = [];
87+
},
88+
89+
step: function (log, db) {
90+
const rec = {
91+
pc: log.getPC(),
92+
op: log.op.toString(),
93+
gas: log.getGas(),
94+
gasCost: log.getCost(),
95+
depth: log.getDepth(),
96+
error: log.getError ? ("" + log.getError()) : null
97+
};
98+
this.steps.push(rec);
99+
if (this.steps.length > this.maxSteps) this.steps.shift();
100+
},
101+
102+
fault: function (log, db) {
103+
this.faultLog = {
104+
pc: log.getPC(),
105+
op: log.op.toString(),
106+
gas: log.getGas(),
107+
depth: log.getDepth(),
108+
error: "" + log.getError()
109+
};
110+
},
111+
112+
result: function (ctx, db) {
113+
return {
114+
type: "minimal-steps",
115+
lastSteps: this.steps,
116+
fault: this.faultLog || null
117+
};
118+
}
119+
}
120+
"#,
121+
),
122+
(
123+
"value_transfer_tracer",
124+
r#" {
125+
setup: function () {
126+
this.totalCost = 0;
127+
this.byOp = {};
128+
},
129+
130+
step: function (log, db) {
131+
var op = log.op.toString();
132+
var cost = log.getCost();
133+
this.totalCost += cost;
134+
135+
var e = this.byOp[op];
136+
if (!e) this.byOp[op] = { count: 1, cost: cost };
137+
else { e.count += 1; e.cost += cost; }
138+
},
139+
140+
result: function () {
141+
var hot = [];
142+
for (var k in this.byOp) {
143+
hot.push({ op: k, count: this.byOp[k].count, cost: this.byOp[k].cost });
144+
}
145+
hot.sort(function (a, b) { return b.cost - a.cost; });
146+
147+
return {
148+
type: "gas-profiler",
149+
totalIntrinsicCostApprox: this.totalCost,
150+
hotOpcodes: hot
151+
};
152+
}
153+
}
154+
"#,
155+
),
156+
(
157+
"opcode_coverage_tracer",
158+
r#"
159+
{
160+
setup: function () {
161+
this.cover = {};
162+
this.pcs = {};
163+
},
164+
165+
step: function (log, db) {
166+
var op = log.op.toString();
167+
this.cover[op] = true;
168+
169+
var pc = log.getPC();
170+
var m = this.pcs[op];
171+
if (!m) { m = {}; this.pcs[op] = m; }
172+
m[pc] = true;
173+
},
174+
175+
result: function () {
176+
var ops = [];
177+
for (var k in this.cover) {
178+
var pcs = Object.keys(this.pcs[k]).map(function (x) { return Number(x); });
179+
ops.push({ op: k, uniquePCs: pcs.length });
180+
}
181+
ops.sort(function (a, b) { return a.op < b.op ? -1 : 1; });
182+
183+
return { type: "opcode-coverage", ops: ops };
184+
}
185+
}
186+
"#,
187+
),
188+
];
189+
190+
for (scenario_name, zk_request, reth_request) in call_scenarios {
191+
for (tracer_name, tracer_code) in &tracers {
192+
let reth_trace =
193+
trace_with_js(&reth_provider, reth_request.clone(), tracer_code).await?;
194+
let zk_trace = trace_with_js(&zksync_provider, zk_request.clone(), tracer_code).await?;
195+
let mut zk_trace = zk_trace;
196+
let mut reth_trace = reth_trace;
197+
normalize_hex_strings(&mut zk_trace);
198+
normalize_hex_strings(&mut reth_trace);
199+
assert_eq!(
200+
zk_trace, reth_trace,
201+
"JS tracer '{tracer_name}' produced different output for scenario '{scenario_name}'"
202+
);
203+
}
204+
}
205+
206+
Ok(())
207+
}
208+
209+
fn configure_call_request(req: &mut TransactionRequest, from: alloy::primitives::Address) {
210+
req.set_from(from);
211+
req.max_priority_fee_per_gas = Some(1);
212+
req.max_fee_per_gas = Some(u128::MAX);
213+
}
214+
215+
async fn trace_with_js(
216+
provider: &EthDynProvider,
217+
request: TransactionRequest,
218+
tracer_code: &str,
219+
) -> anyhow::Result<Value> {
220+
let mut opts = GethDebugTracingCallOptions::default();
221+
222+
opts.tracing_options.tracer = Some(GethDebugTracerType::JsTracer(tracer_code.to_string()));
223+
let trace = provider
224+
.debug_trace_call(request, BlockId::latest(), opts)
225+
.await?;
226+
match trace {
227+
GethTrace::JS(value) => Ok(value),
228+
other => anyhow::bail!("expected JS trace result, got {other:?}"),
229+
}
230+
}
231+
232+
fn normalize_hex_strings(value: &mut Value) {
233+
match value {
234+
Value::String(data) => {
235+
if data.starts_with("0x") {
236+
*data = data.to_lowercase();
237+
}
238+
}
239+
Value::Array(values) => values.iter_mut().for_each(normalize_hex_strings),
240+
Value::Object(map) => map.values_mut().for_each(normalize_hex_strings),
241+
_ => {}
242+
}
243+
}

0 commit comments

Comments
 (0)