diff --git a/core/lib/types/src/debug_flat_call.rs b/core/lib/types/src/debug_flat_call.rs new file mode 100644 index 00000000000..c0ac0661806 --- /dev/null +++ b/core/lib/types/src/debug_flat_call.rs @@ -0,0 +1,258 @@ +use serde::{Deserialize, Serialize}; +use zksync_basic_types::web3::types::{Bytes, U256}; + +use crate::{ + api::{DebugCall, DebugCallType, ResultDebugCall}, + Address, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DebugCallFlat { + pub action: Action, + pub result: CallResult, + pub subtraces: usize, + pub traceaddress: Vec, + pub error: Option, + pub revert_reason: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Action { + pub r#type: DebugCallType, + pub from: Address, + pub to: Address, + pub gas: U256, + pub value: U256, + pub input: Bytes, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallResult { + pub output: Bytes, + pub gas_used: U256, +} + +pub fn flatten_debug_calls(calls: Vec) -> Vec { + let mut flattened_calls = Vec::new(); + for (index, result_debug_call) in calls.into_iter().enumerate() { + let mut trace_address = vec![index]; // Initialize the trace addressees with the index of the top-level call + flatten_call_recursive( + &result_debug_call.result, + &mut flattened_calls, + &mut trace_address, + ); + } + flattened_calls +} + +fn flatten_call_recursive( + call: &DebugCall, + flattened_calls: &mut Vec, + trace_address: &mut Vec, +) { + let flat_call = DebugCallFlat { + action: Action { + r#type: call.r#type.clone(), + from: call.from, + to: call.to, + gas: call.gas, + value: call.value, + input: call.input.clone(), + }, + result: CallResult { + output: call.output.clone(), + gas_used: call.gas_used, + }, + subtraces: call.calls.len(), + traceaddress: trace_address.clone(), // Clone the current trace address + error: call.error.clone(), + revert_reason: call.revert_reason.clone(), + }; + flattened_calls.push(flat_call); + + // Process nested calls + for (index, nested_call) in call.calls.iter().enumerate() { + trace_address.push(index); // Update trace addressees for the nested call + flatten_call_recursive(nested_call, flattened_calls, trace_address); + trace_address.pop(); // Reset trace addressees after processing the nested call (prevent to keep filling the vector) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + api::{DebugCall, DebugCallType, ResultDebugCall}, + vm_trace::Call, + Address, BOOTLOADER_ADDRESS, + }; + + #[test] + fn test_flatten_debug_call() { + let result_debug_trace: Vec = [1, 1] + .map(|_| ResultDebugCall { + result: new_testing_debug_call(), + }) + .into(); + + let debug_call_flat = flatten_debug_calls(result_debug_trace); + let expected_debug_call_flat = expected_flat_trace(); + assert_eq!(debug_call_flat, expected_debug_call_flat); + } + + fn new_testing_debug_call() -> DebugCall { + DebugCall { + r#type: DebugCallType::Call, + from: Address::zero(), + to: BOOTLOADER_ADDRESS, + gas: 1000.into(), + gas_used: 1000.into(), + value: 0.into(), + output: vec![].into(), + input: vec![].into(), + error: None, + revert_reason: None, + calls: new_testing_trace(), + } + } + + fn new_testing_trace() -> Vec { + let first_call_trace = Call { + from: Address::zero(), + to: Address::zero(), + gas: 100, + gas_used: 42, + ..Call::default() + }; + let second_call_trace = Call { + from: Address::zero(), + to: Address::zero(), + value: 123.into(), + gas: 58, + gas_used: 10, + input: b"input".to_vec(), + output: b"output".to_vec(), + ..Call::default() + }; + [first_call_trace, second_call_trace] + .map(|call_trace| call_trace.into()) + .into() + } + + fn expected_flat_trace() -> Vec { + [ + DebugCallFlat { + action: Action { + r#type: DebugCallType::Call, + from: Address::zero(), + to: BOOTLOADER_ADDRESS, + gas: 1000.into(), + value: 0.into(), + input: vec![].into(), + }, + result: CallResult { + output: vec![].into(), + gas_used: 1000.into(), + }, + subtraces: 2, + traceaddress: [0].into(), + error: None, + revert_reason: None, + }, + DebugCallFlat { + action: Action { + r#type: DebugCallType::Call, + from: Address::zero(), + to: Address::zero(), + gas: 100.into(), + value: 0.into(), + input: vec![].into(), + }, + result: CallResult { + output: vec![].into(), + gas_used: 42.into(), + }, + subtraces: 0, + traceaddress: [0, 0].into(), + error: None, + revert_reason: None, + }, + DebugCallFlat { + action: Action { + r#type: DebugCallType::Call, + from: Address::zero(), + to: Address::zero(), + gas: 58.into(), + value: 123.into(), + input: b"input".to_vec().into(), + }, + result: CallResult { + output: b"output".to_vec().into(), + gas_used: 10.into(), + }, + subtraces: 0, + traceaddress: [0, 1].into(), + error: None, + revert_reason: None, + }, + DebugCallFlat { + action: Action { + r#type: DebugCallType::Call, + from: Address::zero(), + to: BOOTLOADER_ADDRESS, + gas: 1000.into(), + value: 0.into(), + input: vec![].into(), + }, + result: CallResult { + output: vec![].into(), + gas_used: 1000.into(), + }, + subtraces: 2, + traceaddress: [1].into(), + error: None, + revert_reason: None, + }, + DebugCallFlat { + action: Action { + r#type: DebugCallType::Call, + from: Address::zero(), + to: Address::zero(), + gas: 100.into(), + value: 0.into(), + input: vec![].into(), + }, + result: CallResult { + output: vec![].into(), + gas_used: 42.into(), + }, + subtraces: 0, + traceaddress: [1, 0].into(), + error: None, + revert_reason: None, + }, + DebugCallFlat { + action: Action { + r#type: DebugCallType::Call, + from: Address::zero(), + to: Address::zero(), + gas: 58.into(), + value: 123.into(), + input: b"input".to_vec().into(), + }, + result: CallResult { + output: b"output".to_vec().into(), + gas_used: 10.into(), + }, + subtraces: 0, + traceaddress: [1, 1].into(), + error: None, + revert_reason: None, + }, + ] + .into() + } +} diff --git a/core/lib/types/src/lib.rs b/core/lib/types/src/lib.rs index 965ad909fa5..ee2f7944f56 100644 --- a/core/lib/types/src/lib.rs +++ b/core/lib/types/src/lib.rs @@ -28,6 +28,7 @@ pub mod block; pub mod circuit; pub mod commitment; pub mod contract_verification_api; +pub mod debug_flat_call; pub mod event; pub mod fee; pub mod fee_model; diff --git a/core/lib/web3_decl/src/namespaces/debug.rs b/core/lib/web3_decl/src/namespaces/debug.rs index 02e75e946b7..c54903d7e3c 100644 --- a/core/lib/web3_decl/src/namespaces/debug.rs +++ b/core/lib/web3_decl/src/namespaces/debug.rs @@ -1,6 +1,7 @@ use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use zksync_types::{ api::{BlockId, BlockNumber, DebugCall, ResultDebugCall, TracerConfig}, + debug_flat_call::DebugCallFlat, transaction_request::CallRequest, }; @@ -25,6 +26,12 @@ pub trait DebugNamespace { block: BlockNumber, options: Option, ) -> RpcResult>; + #[method(name = "traceBlockByNumber.callFlatTracer")] + async fn trace_block_by_number_flat( + &self, + block: BlockNumber, + options: Option, + ) -> RpcResult>; #[method(name = "traceBlockByHash")] async fn trace_block_by_hash( &self, diff --git a/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/debug.rs b/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/debug.rs index 0d8465d39cb..a628c3f8c7b 100644 --- a/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/debug.rs +++ b/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/debug.rs @@ -1,5 +1,6 @@ use zksync_types::{ api::{BlockId, BlockNumber, DebugCall, ResultDebugCall, TracerConfig}, + debug_flat_call::DebugCallFlat, transaction_request::CallRequest, H256, }; @@ -22,6 +23,16 @@ impl DebugNamespaceServer for DebugNamespace { .map_err(|err| self.current_method().map_err(err)) } + async fn trace_block_by_number_flat( + &self, + block: BlockNumber, + options: Option, + ) -> RpcResult> { + self.debug_trace_block_flat_impl(BlockId::Number(block), options) + .await + .map_err(|err| self.current_method().map_err(err)) + } + async fn trace_block_by_hash( &self, hash: H256, diff --git a/core/lib/zksync_core/src/api_server/web3/namespaces/debug.rs b/core/lib/zksync_core/src/api_server/web3/namespaces/debug.rs index 6631622de74..94327c2da69 100644 --- a/core/lib/zksync_core/src/api_server/web3/namespaces/debug.rs +++ b/core/lib/zksync_core/src/api_server/web3/namespaces/debug.rs @@ -6,6 +6,7 @@ use once_cell::sync::OnceCell; use zksync_system_constants::MAX_ENCODED_TX_SIZE; use zksync_types::{ api::{BlockId, BlockNumber, DebugCall, ResultDebugCall, TracerConfig}, + debug_flat_call::{flatten_debug_calls, DebugCallFlat}, fee_model::BatchFeeInput, l2::L2Tx, transaction_request::CallRequest, @@ -92,6 +93,17 @@ impl DebugNamespace { Ok(call_trace) } + #[tracing::instrument(skip(self))] + pub async fn debug_trace_block_flat_impl( + &self, + block_id: BlockId, + options: Option, + ) -> Result, Web3Error> { + let call_trace = self.debug_trace_block_impl(block_id, options).await?; + let call_trace_flat = flatten_debug_calls(call_trace); + Ok(call_trace_flat) + } + #[tracing::instrument(skip(self))] pub async fn debug_trace_transaction_impl( &self, diff --git a/core/lib/zksync_core/src/api_server/web3/tests/debug.rs b/core/lib/zksync_core/src/api_server/web3/tests/debug.rs index 0da58d28cf9..05eeaa5e697 100644 --- a/core/lib/zksync_core/src/api_server/web3/tests/debug.rs +++ b/core/lib/zksync_core/src/api_server/web3/tests/debug.rs @@ -92,6 +92,82 @@ async fn tracing_block() { test_http_server(TraceBlockTest(MiniblockNumber(1))).await; } +#[derive(Debug)] +struct TraceBlockFlatTest(MiniblockNumber); + +#[async_trait] +impl HttpTest for TraceBlockFlatTest { + async fn test(&self, client: &HttpClient, pool: &ConnectionPool) -> anyhow::Result<()> { + let tx_results = [0, 1, 2].map(execute_l2_transaction_with_traces); + let mut storage = pool.access_storage().await?; + let _new_miniblock = store_miniblock(&mut storage, self.0, &tx_results).await?; + drop(storage); + + let block_ids = [ + api::BlockId::Number((*self.0).into()), + api::BlockId::Number(api::BlockNumber::Latest), + ]; + + for block_id in block_ids { + if let api::BlockId::Number(number) = block_id { + let block_traces = client.trace_block_by_number_flat(number, None).await?; + + // A transaction with 2 nested calls will convert into 3 Flattened calls. + // Also in this test, all tx have the same # of nested calls + assert_eq!( + block_traces.len(), + tx_results.len() * (tx_results[0].call_traces.len() + 1) + ); + + // First tx has 2 nested calls, thus 2 sub-traces + assert_eq!(block_traces[0].subtraces, 2); + assert_eq!(block_traces[0].traceaddress, [0]); + // Second flat-call (fist nested call) do not have nested calls + assert_eq!(block_traces[1].subtraces, 0); + assert_eq!(block_traces[1].traceaddress, [0, 0]); + + let top_level_call_indexes = [0, 3, 6]; + let top_level_traces = top_level_call_indexes + .iter() + .map(|&i| block_traces[i].clone()); + + for (top_level_trace, tx_result) in top_level_traces.zip(&tx_results) { + assert_eq!(top_level_trace.action.from, Address::zero()); + assert_eq!(top_level_trace.action.to, BOOTLOADER_ADDRESS); + assert_eq!( + top_level_trace.action.gas, + tx_result.transaction.gas_limit() + ); + } + // TODO: test inner calls + } + } + + let missing_block_number = api::BlockNumber::from(*self.0 + 100); + let error = client + .trace_block_by_number_flat(missing_block_number, None) + .await + .unwrap_err(); + if let ClientError::Call(error) = error { + assert_eq!(error.code(), ErrorCode::InvalidParams.code()); + assert!( + error.message().contains("Block") && error.message().contains("doesn't exist"), + "{error:?}" + ); + assert!(error.data().is_none(), "{error:?}"); + } else { + panic!("Unexpected error: {error:?}"); + } + + Ok(()) + } +} + +#[tokio::test] +async fn tracing_block_flat() { + test_http_server(TraceBlockFlatTest(MiniblockNumber(1))).await; +} + #[derive(Debug)] struct TraceTransactionTest;