|
| 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