anvil_zksync_core/node/
debug.rs

1use crate::node::{InMemoryNode, MAX_TX_SIZE};
2use crate::utils::create_debug_output;
3use once_cell::sync::OnceCell;
4use std::sync::Arc;
5use zksync_multivm::interface::storage::StorageView;
6use zksync_multivm::interface::{VmFactory, VmInterface};
7use zksync_multivm::tracers::CallTracer;
8use zksync_multivm::vm_latest::constants::ETH_CALL_GAS_LIMIT;
9use zksync_multivm::vm_latest::{HistoryDisabled, ToTracerPointer, Vm};
10use zksync_types::l2::L2Tx;
11use zksync_types::transaction_request::CallRequest;
12use zksync_types::web3::Bytes;
13use zksync_types::{api, PackedEthSignature, Transaction, H256};
14use zksync_web3_decl::error::Web3Error;
15
16use super::boojumos::BOOJUM_CALL_GAS_LIMIT;
17
18impl InMemoryNode {
19    pub async fn trace_block_impl(
20        &self,
21        block_id: api::BlockId,
22        options: Option<api::TracerConfig>,
23    ) -> anyhow::Result<api::CallTracerBlockResult> {
24        let only_top = options.is_some_and(|o| o.tracer_config.only_top_call);
25        let tx_hashes = self
26            .blockchain
27            .get_block_tx_hashes_by_id(block_id)
28            .await
29            .ok_or_else(|| anyhow::anyhow!("Block (id={block_id}) not found"))?;
30
31        let mut debug_calls = Vec::with_capacity(tx_hashes.len());
32        for tx_hash in tx_hashes {
33            let result = self.blockchain
34                .get_tx_debug_info(&tx_hash, only_top)
35                .await
36                .ok_or_else(|| {
37                    anyhow::anyhow!(
38                        "Unexpectedly transaction (hash={tx_hash}) belongs to a block but could not be found"
39                    )
40                })?;
41            debug_calls.push(api::ResultDebugCall { result });
42        }
43
44        Ok(api::CallTracerBlockResult::CallTrace(debug_calls))
45    }
46
47    pub async fn trace_call_impl(
48        &self,
49        request: CallRequest,
50        block: Option<api::BlockId>,
51        options: Option<api::TracerConfig>,
52    ) -> Result<api::CallTracerResult, Web3Error> {
53        let only_top = options.is_some_and(|o| o.tracer_config.only_top_call);
54        let inner = self.inner.read().await;
55        let system_contracts = self.system_contracts.contracts_for_l2_call();
56        if block.is_some() && !matches!(block, Some(api::BlockId::Number(api::BlockNumber::Latest)))
57        {
58            return Err(Web3Error::InternalError(anyhow::anyhow!(
59                "tracing only supported at `latest` block"
60            )));
61        }
62
63        let mut l2_tx = L2Tx::from_request(
64            request.into(),
65            MAX_TX_SIZE,
66            self.system_contracts.allow_no_target(),
67        )
68        .map_err(Web3Error::SerializationError)?;
69        let execution_mode = zksync_multivm::interface::TxExecutionMode::EthCall;
70
71        // init vm
72        let (mut l1_batch_env, _block_context) = inner.create_l1_batch_env().await;
73
74        // update the enforced_base_fee within l1_batch_env to match the logic in zksync_core
75        l1_batch_env.enforced_base_fee = Some(l2_tx.common_data.fee.max_fee_per_gas.as_u64());
76        let system_env = inner.create_system_env(system_contracts.clone(), execution_mode);
77        let storage = StorageView::new(inner.read_storage()).to_rc_ptr();
78        let mut vm: Vm<_, HistoryDisabled> = Vm::new(l1_batch_env, system_env, storage);
79
80        // We must inject *some* signature (otherwise bootloader code fails to generate hash).
81        if l2_tx.common_data.signature.is_empty() {
82            l2_tx.common_data.signature = PackedEthSignature::default().serialize_packed().into();
83        }
84
85        // Match behavior of zksync_core:
86        // Protection against infinite-loop eth_calls and alike:
87        // limiting the amount of gas the call can use.
88        if self.system_contracts.boojum.use_boojum {
89            l2_tx.common_data.fee.gas_limit = BOOJUM_CALL_GAS_LIMIT.into();
90        } else {
91            l2_tx.common_data.fee.gas_limit = ETH_CALL_GAS_LIMIT.into();
92        }
93
94        let tx: Transaction = l2_tx.clone().into();
95        vm.push_transaction(tx);
96
97        let call_tracer_result = Arc::new(OnceCell::default());
98        let tracer = CallTracer::new(call_tracer_result.clone()).into_tracer_pointer();
99
100        let tx_result = vm.inspect(
101            &mut tracer.into(),
102            zksync_multivm::interface::InspectExecutionMode::OneTx,
103        );
104        let call_traces = if only_top {
105            vec![]
106        } else {
107            Arc::try_unwrap(call_tracer_result)
108                .unwrap()
109                .take()
110                .unwrap_or_default()
111        };
112
113        let debug = create_debug_output(&l2_tx.into(), &tx_result, call_traces)?;
114
115        Ok(api::CallTracerResult::CallTrace(debug))
116    }
117
118    pub async fn trace_transaction_impl(
119        &self,
120        tx_hash: H256,
121        options: Option<api::TracerConfig>,
122    ) -> anyhow::Result<Option<api::CallTracerResult>> {
123        let only_top = options.is_some_and(|o| o.tracer_config.only_top_call);
124        Ok(self
125            .blockchain
126            .get_tx_debug_info(&tx_hash, only_top)
127            .await
128            .map(api::CallTracerResult::CallTrace))
129    }
130
131    pub async fn get_raw_transaction_impl(&self, tx_hash: H256) -> anyhow::Result<Option<Bytes>> {
132        Ok(self.blockchain.get_raw_transaction(tx_hash).await)
133    }
134
135    pub async fn get_raw_transactions_impl(
136        &self,
137        block_number: api::BlockId,
138    ) -> anyhow::Result<Vec<Bytes>> {
139        Ok(self.blockchain.get_raw_transactions(block_number).await)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use alloy::dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt};
146    use alloy::json_abi::{Function, Param, StateMutability};
147    use alloy::primitives::{Address as AlloyAddress, U256 as AlloyU256};
148    use anvil_zksync_config::constants::DEFAULT_ACCOUNT_BALANCE;
149    use zksync_types::{
150        transaction_request::CallRequestBuilder, utils::deployed_address_create, Address,
151        K256PrivateKey, L2BlockNumber, Nonce, H160, U256,
152    };
153
154    use super::*;
155    use crate::{
156        deps::system_contracts::bytecode_from_slice,
157        node::{InMemoryNode, TransactionResult},
158        testing::{self, LogBuilder},
159    };
160
161    async fn deploy_test_contracts(node: &InMemoryNode) -> (Address, Address) {
162        let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xee)).unwrap();
163        let from_account = private_key.address();
164        node.set_rich_account(from_account, U256::from(DEFAULT_ACCOUNT_BALANCE))
165            .await;
166
167        // first, deploy secondary contract
168        let secondary_bytecode = bytecode_from_slice(
169            "Secondary",
170            include_bytes!("../deps/test-contracts/Secondary.json"),
171        );
172        let secondary_deployed_address = deployed_address_create(from_account, U256::zero());
173        let alloy_secondary_address = AlloyAddress::from(secondary_deployed_address.0);
174        let secondary_constructor_calldata =
175            DynSolValue::Uint(AlloyU256::from(2), 256).abi_encode();
176
177        testing::deploy_contract(
178            node,
179            &private_key,
180            secondary_bytecode,
181            Some(secondary_constructor_calldata),
182            Nonce(0),
183        )
184        .await;
185
186        // deploy primary contract using the secondary contract address as a constructor parameter
187        let primary_bytecode = bytecode_from_slice(
188            "Primary",
189            include_bytes!("../deps/test-contracts/Primary.json"),
190        );
191        let primary_deployed_address = deployed_address_create(from_account, U256::one());
192        let primary_constructor_calldata =
193            DynSolValue::Address(alloy_secondary_address).abi_encode();
194
195        testing::deploy_contract(
196            node,
197            &private_key,
198            primary_bytecode,
199            Some(primary_constructor_calldata),
200            Nonce(1),
201        )
202        .await;
203
204        (primary_deployed_address, secondary_deployed_address)
205    }
206
207    #[tokio::test]
208    async fn test_trace_deployed_contract() {
209        let node = InMemoryNode::test(None);
210
211        let (primary_deployed_address, secondary_deployed_address) =
212            deploy_test_contracts(&node).await;
213
214        let func = Function {
215            name: "calculate".to_string(),
216            inputs: vec![Param {
217                name: "value".to_string(),
218                ty: "uint256".to_string(),
219                components: vec![],
220                internal_type: None,
221            }],
222            outputs: vec![Param {
223                name: "".to_string(),
224                ty: "uint256".to_string(),
225                components: vec![],
226                internal_type: None,
227            }],
228            state_mutability: StateMutability::NonPayable,
229        };
230
231        let calldata = func
232            .abi_encode_input(&[DynSolValue::Uint(AlloyU256::from(42), 256)])
233            .expect("failed to encode function input");
234
235        let request = CallRequestBuilder::default()
236            .to(Some(primary_deployed_address))
237            .data(calldata.clone().into())
238            .gas(80_000_000.into())
239            .build();
240        let trace = node
241            .trace_call_impl(request.clone(), None, None)
242            .await
243            .expect("trace call")
244            .unwrap_default();
245
246        // call should not revert
247        assert!(trace.error.is_none());
248        assert!(trace.revert_reason.is_none());
249
250        // check that the call was successful
251        let output = func
252            .abi_decode_output(trace.output.0.as_slice(), true)
253            .expect("failed to decode output");
254        assert_eq!(
255            output[0],
256            DynSolValue::Uint(AlloyU256::from(84), 256),
257            "unexpected output"
258        );
259
260        // find the call to primary contract in the trace
261        let contract_call = trace.calls.last().unwrap().calls.first().unwrap();
262
263        assert_eq!(contract_call.to, primary_deployed_address);
264        assert_eq!(contract_call.input, calldata.into());
265
266        // check that it contains a call to secondary contract
267        let subcall = contract_call.calls.first().unwrap();
268        assert_eq!(subcall.to, secondary_deployed_address);
269        assert_eq!(subcall.from, primary_deployed_address);
270        assert_eq!(
271            subcall.output,
272            func.abi_encode_output(&[DynSolValue::Uint(AlloyU256::from(84), 256)])
273                .expect("failed to encode function output")
274                .into()
275        );
276    }
277
278    #[tokio::test]
279    async fn test_trace_only_top() {
280        let node = InMemoryNode::test(None);
281
282        let (primary_deployed_address, _) = deploy_test_contracts(&node).await;
283
284        // trace a call to the primary contract
285        let func = Function {
286            name: "calculate".to_string(),
287            inputs: vec![Param {
288                name: "value".to_string(),
289                ty: "uint256".to_string(),
290                components: vec![],
291                internal_type: None,
292            }],
293            outputs: vec![],
294            state_mutability: StateMutability::NonPayable,
295        };
296
297        let calldata = func
298            .abi_encode_input(&[DynSolValue::Uint(AlloyU256::from(42), 256)])
299            .expect("failed to encode function input");
300
301        let request = CallRequestBuilder::default()
302            .to(Some(primary_deployed_address))
303            .data(calldata.into())
304            .gas(80_000_000.into())
305            .build();
306
307        // if we trace with onlyTopCall=true, we should get only the top-level call
308        let trace = node
309            .trace_call_impl(
310                request,
311                None,
312                Some(api::TracerConfig {
313                    tracer: api::SupportedTracers::CallTracer,
314                    tracer_config: api::CallTracerConfig {
315                        only_top_call: true,
316                    },
317                }),
318            )
319            .await
320            .expect("trace call")
321            .unwrap_default();
322        // call should not revert
323        assert!(trace.error.is_none());
324        assert!(trace.revert_reason.is_none());
325
326        // call should not contain any subcalls
327        assert!(trace.calls.is_empty());
328    }
329
330    #[tokio::test]
331    async fn test_trace_reverts() {
332        let node = InMemoryNode::test(None);
333
334        let (primary_deployed_address, _) = deploy_test_contracts(&node).await;
335
336        let func = Function {
337            name: "shouldRevert".to_string(),
338            inputs: vec![],
339            outputs: vec![],
340            state_mutability: StateMutability::NonPayable,
341        };
342
343        // trace a call to the primary contract
344        let request = CallRequestBuilder::default()
345            .to(Some(primary_deployed_address))
346            .data(func.selector().to_vec().into())
347            .gas(80_000_000.into())
348            .build();
349        let trace = node
350            .trace_call_impl(request, None, None)
351            .await
352            .expect("trace call")
353            .unwrap_default();
354
355        // call should revert
356        assert!(trace.revert_reason.is_some());
357        // find the call to primary contract in the trace
358        let contract_call = trace.calls.last().unwrap().calls.first().unwrap();
359
360        // the contract subcall should have reverted
361        assert!(contract_call.revert_reason.is_some());
362    }
363
364    #[tokio::test]
365    async fn test_trace_transaction_impl() {
366        let node = InMemoryNode::test(None);
367        {
368            let mut writer = node.inner.write().await;
369            writer
370                .insert_tx_result(
371                    H256::repeat_byte(0x1),
372                    TransactionResult {
373                        info: testing::default_tx_execution_info(),
374                        new_bytecodes: vec![],
375                        receipt: api::TransactionReceipt {
376                            logs: vec![LogBuilder::new()
377                                .set_address(H160::repeat_byte(0xa1))
378                                .build()],
379                            ..Default::default()
380                        },
381                        debug: testing::default_tx_debug_info(),
382                    },
383                )
384                .await;
385        }
386        let result = node
387            .trace_transaction_impl(H256::repeat_byte(0x1), None)
388            .await
389            .unwrap()
390            .unwrap()
391            .unwrap_default();
392        assert_eq!(result.calls.len(), 1);
393    }
394
395    #[tokio::test]
396    async fn test_trace_transaction_only_top() {
397        let node = InMemoryNode::test(None);
398        node.inner
399            .write()
400            .await
401            .insert_tx_result(
402                H256::repeat_byte(0x1),
403                TransactionResult {
404                    info: testing::default_tx_execution_info(),
405                    new_bytecodes: vec![],
406                    receipt: api::TransactionReceipt {
407                        logs: vec![LogBuilder::new()
408                            .set_address(H160::repeat_byte(0xa1))
409                            .build()],
410                        ..Default::default()
411                    },
412                    debug: testing::default_tx_debug_info(),
413                },
414            )
415            .await;
416        let result = node
417            .trace_transaction_impl(
418                H256::repeat_byte(0x1),
419                Some(api::TracerConfig {
420                    tracer: api::SupportedTracers::CallTracer,
421                    tracer_config: api::CallTracerConfig {
422                        only_top_call: true,
423                    },
424                }),
425            )
426            .await
427            .unwrap()
428            .unwrap()
429            .unwrap_default();
430        assert!(result.calls.is_empty());
431    }
432
433    #[tokio::test]
434    async fn test_trace_transaction_not_found() {
435        let node = InMemoryNode::test(None);
436        let result = node
437            .trace_transaction_impl(H256::repeat_byte(0x1), None)
438            .await
439            .unwrap();
440        assert!(result.is_none());
441    }
442
443    #[tokio::test]
444    async fn test_trace_block_by_hash_empty() {
445        let node = InMemoryNode::test(None);
446        let block = api::Block::<api::TransactionVariant>::default();
447        node.inner
448            .write()
449            .await
450            .insert_block(H256::repeat_byte(0x1), block)
451            .await;
452        let result = node
453            .trace_block_impl(api::BlockId::Hash(H256::repeat_byte(0x1)), None)
454            .await
455            .unwrap()
456            .unwrap_default();
457        assert_eq!(result.len(), 0);
458    }
459
460    #[tokio::test]
461    async fn test_trace_block_by_hash_impl() {
462        let node = InMemoryNode::test(None);
463        let tx = api::Transaction::default();
464        let tx_hash = tx.hash;
465        let mut block = api::Block::<api::TransactionVariant>::default();
466        block.transactions.push(api::TransactionVariant::Full(tx));
467        {
468            let mut writer = node.inner.write().await;
469            writer.insert_block(H256::repeat_byte(0x1), block).await;
470            writer
471                .insert_tx_result(
472                    tx_hash,
473                    TransactionResult {
474                        info: testing::default_tx_execution_info(),
475                        new_bytecodes: vec![],
476                        receipt: api::TransactionReceipt::default(),
477                        debug: testing::default_tx_debug_info(),
478                    },
479                )
480                .await;
481        }
482        let result = node
483            .trace_block_impl(api::BlockId::Hash(H256::repeat_byte(0x1)), None)
484            .await
485            .unwrap()
486            .unwrap_default();
487        assert_eq!(result.len(), 1);
488        assert_eq!(result[0].result.calls.len(), 1);
489    }
490
491    #[tokio::test]
492    async fn test_trace_block_by_number_impl() {
493        let node = InMemoryNode::test(None);
494        let tx = api::Transaction::default();
495        let tx_hash = tx.hash;
496        let mut block = api::Block::<api::TransactionVariant>::default();
497        block.transactions.push(api::TransactionVariant::Full(tx));
498        {
499            let mut writer = node.inner.write().await;
500            writer.insert_block(H256::repeat_byte(0x1), block).await;
501            writer
502                .insert_block_hash(L2BlockNumber(0), H256::repeat_byte(0x1))
503                .await;
504            writer
505                .insert_tx_result(
506                    tx_hash,
507                    TransactionResult {
508                        info: testing::default_tx_execution_info(),
509                        new_bytecodes: vec![],
510                        receipt: api::TransactionReceipt::default(),
511                        debug: testing::default_tx_debug_info(),
512                    },
513                )
514                .await;
515        }
516        // check `latest` alias
517        let result = node
518            .trace_block_impl(api::BlockId::Number(api::BlockNumber::Latest), None)
519            .await
520            .unwrap()
521            .unwrap_default();
522        assert_eq!(result.len(), 1);
523        assert_eq!(result[0].result.calls.len(), 1);
524
525        // check block number
526        let result = node
527            .trace_block_impl(
528                api::BlockId::Number(api::BlockNumber::Number(0.into())),
529                None,
530            )
531            .await
532            .unwrap()
533            .unwrap_default();
534        assert_eq!(result.len(), 1);
535        assert_eq!(result[0].result.calls.len(), 1);
536    }
537}