edb_engine/analysis/common.rs
1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Common analysis types and functionality for the Ethereum Debug Bridge (EDB) engine.
18//!
19//! This module provides the core data structures and analysis logic for processing Solidity source code
20//! and extracting debugging information. It handles compilation, AST parsing, and step-by-step analysis
21//! of smart contract execution paths.
22//!
23//! # Overview
24//!
25//! The analysis module performs the following key operations:
26//! 1. **Compilation**: Compiles Solidity source code using the Solc compiler
27//! 2. **AST Processing**: Parses and prunes the Abstract Syntax Tree (AST) for analysis
28//! 3. **Step Partitioning**: Divides the source code into executable steps
29//! 4. **Hook Analysis**: Identifies pre and post-execution hooks for each step
30//! 5. **Variable Tracking**: Maps variables to their execution contexts
31//!
32//! # Key Components
33//!
34//! - [`AnalysisResult`]: The main result container holding all analysis data
35//! - [`SourceResult`]: Per-source file analysis results
36//! - [`StepAnalysisResult`]: Individual step analysis with hooks
37//! - [`analyze()`]: Main analysis function that orchestrates the entire process
38//!
39//! # Example Usage
40//!
41//! ```rust
42//! use foundry_compilers::solc::SolcVersionedInput;
43//! use crate::analysis::common::analyze;
44//!
45//! // Create a SolcVersionedInput with your Solidity source
46//! let input = /* your solc input */;
47//!
48//! // Run the analysis
49//! let result = analyze(input)?;
50//!
51//! // Access analysis results
52//! for (path, source_result) in &result.sources {
53//! println!("Analyzed: {}", path.display());
54//! for step in &source_result.steps {
55//! println!("Step: {}", step.source_step.variant_name());
56//! }
57//! }
58//! ```
59
60use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
61use serde::{Deserialize, Serialize};
62use std::collections::HashMap;
63use tracing::warn;
64
65use crate::{
66 analysis::{
67 AnalysisError, Analyzer, ContractRef, FunctionRef, SourceAnalysis, StepRef,
68 UserDefinedTypeRef, UCID, UFID, UTID,
69 },
70 ASTPruner, Artifact, VariableRef, USID, UVID,
71};
72
73/// Main analysis result containing debugging information from source code analysis.
74///
75/// This structure holds the complete analysis results for all source files processed
76/// during the analysis phase. It provides mappings from unique identifiers to their
77/// corresponding source steps and variables for efficient lookup during debugging.
78///
79/// # Fields
80///
81/// - `sources`: Maps file paths to their individual analysis results
82/// - `usid_to_step`: Maps unique step identifiers (USID) to their source step references
83/// - `uvid_to_variable`: Maps unique variable identifiers (UVID) to their variable references
84///
85/// # Example
86///
87/// ```rust
88/// let analysis_result = analyze(input)?;
89///
90/// // Access source results by file path
91/// for (path, source_result) in &analysis_result.sources {
92/// println!("File: {}", path.display());
93/// println!("Steps found: {}", source_result.steps.len());
94/// }
95///
96/// // Look up a specific step by USID
97/// if let Some(step) = analysis_result.usid_to_step.get(&some_usid) {
98/// println!("Found step: {}", step.variant_name());
99/// }
100/// ```
101#[derive(Debug, Default, Clone, Serialize, Deserialize)]
102pub struct AnalysisResult {
103 /// Maps source index to their corresponding source analysis results
104 pub sources: HashMap<u32, SourceAnalysis>,
105 /// Maps unique contract identifiers to their contract references
106 pub ucid_to_contract: HashMap<UCID, ContractRef>,
107 /// Maps unique function identifiers to their function references
108 pub ufid_to_function: HashMap<UFID, FunctionRef>,
109 /// Maps unique step identifiers to their source step references for quick lookup
110 pub usid_to_step: HashMap<USID, StepRef>,
111 /// Maps unique variable identifiers to their variable references (currently unimplemented)
112 pub uvid_to_variable: HashMap<UVID, VariableRef>,
113 /// Maps AST IDs to their corresponding user defined type references
114 pub utid_to_user_defined_type: HashMap<UTID, UserDefinedTypeRef>,
115 /// Maps AST IDs to their corresponding user defined type references
116 pub user_defined_types: HashMap<usize, UserDefinedTypeRef>,
117}
118
119/// Performs comprehensive analysis of Solidity source code.
120///
121/// This is the main entry point for source code analysis. It compiles the provided
122/// Solidity input, processes the AST, and performs step-by-step analysis to extract
123/// debugging information.
124///
125/// # Arguments
126///
127/// * `input` - The Solc versioned input containing source files and compilation settings
128///
129/// # Returns
130///
131/// Returns an `AnalysisResult` containing all analysis data, or an `AnalysisError`
132/// if compilation or analysis fails.
133///
134/// # Process Overview
135///
136/// 1. **Compilation**: Uses the Solc compiler to compile the source code
137/// 2. **AST Processing**: Parses and prunes the AST for each source file
138/// 3. **Parallel Analysis**: Analyzes each source file in parallel
139/// 4. **Step Partitioning**: Divides source code into executable steps
140/// 5. **Hook Generation**: Creates pre and post-execution hooks for each step
141/// 6. **Index Building**: Builds lookup tables for steps and variables
142///
143/// # Example
144///
145/// ```rust
146/// use foundry_compilers::solc::SolcVersionedInput;
147/// use crate::analysis::common::analyze;
148///
149/// // Create your SolcVersionedInput
150/// let input = SolcVersionedInput::build(/* ... */);
151///
152/// // Run the analysis
153/// match analyze(input) {
154/// Ok(result) => {
155/// println!("Analysis completed successfully");
156/// println!("Files analyzed: {}", result.sources.len());
157/// println!("Total steps: {}", result.usid_to_step.len());
158/// }
159/// Err(e) => eprintln!("Analysis failed: {}", e),
160/// }
161/// ```
162///
163/// # Errors
164///
165/// This function can return the following errors:
166/// - `AnalysisError::StepPartitionError`: When step partitioning fails
167pub fn analyze(artifact: &Artifact) -> Result<AnalysisResult, AnalysisError> {
168 let source_results: Vec<SourceAnalysis> = artifact
169 .output
170 .sources
171 .par_iter()
172 .map(|(path, source_result)| {
173 let source_id = source_result.id;
174 let mut source_ast = source_result.ast.clone().ok_or(AnalysisError::MissingAst)?;
175 let source_unit = ASTPruner::convert(&mut source_ast, false)
176 .map_err(AnalysisError::ASTConversionError)?;
177
178 let analyzer = Analyzer::new(source_id);
179 let mut source_result = analyzer.analyze(source_id, path, &source_unit)?;
180
181 // sort steps in reverse order
182 source_result.steps.sort_unstable_by_key(|step| step.read().src.start);
183 source_result.steps.reverse();
184
185 // ensure we do not have overlapped steps
186 // XXX (ZZ): the check failed for the following command, need to investigate
187 // ./target/debug/edb replay 0xd253e3b563bf7b8894da2a69db836a4e98e337157564483d8ac72117df355a9d
188 // overlap happens on USDC (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)
189 for (i, step) in source_result.steps.iter().enumerate() {
190 if i > 0 {
191 let prev = &source_result.steps[i - 1];
192 let end = step
193 .read()
194 .src
195 .start
196 .map(|start| start + step.read().src.length.unwrap_or(0));
197
198 if end > prev.read().src.start {
199 warn!("Overlapping steps detected: {:?}", step);
200 // return Err(AnalysisError::StepPartitionError(eyre::eyre!(
201 // "Overlapping steps detected"
202 // )));
203 }
204 }
205 }
206
207 Ok(source_result)
208 })
209 .collect::<Result<Vec<_>, AnalysisError>>()?;
210
211 // build lookup tables
212 let mut ucid_to_contract = HashMap::new();
213 let mut ufid_to_function = HashMap::new();
214 let mut usid_to_step = HashMap::new();
215 let mut uvid_to_variable = HashMap::new();
216 let mut utid_to_user_defined_type = HashMap::new();
217 let mut user_defined_types = HashMap::new();
218 for result in source_results.iter() {
219 ucid_to_contract.extend(result.contract_table().into_iter());
220 ufid_to_function.extend(result.function_table().into_iter());
221 usid_to_step.extend(result.step_table().into_iter());
222 uvid_to_variable.extend(result.variable_table().into_iter());
223 utid_to_user_defined_type.extend(result.user_defined_type_table().into_iter());
224 user_defined_types.extend(result.user_defined_types().into_iter());
225 }
226 let sources = source_results.into_iter().map(|s| (s.id, s)).collect();
227
228 Ok(AnalysisResult {
229 sources,
230 ucid_to_contract,
231 ufid_to_function,
232 usid_to_step,
233 uvid_to_variable,
234 utid_to_user_defined_type,
235 user_defined_types,
236 })
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use foundry_block_explorers::contract::Metadata;
243 use foundry_compilers::{
244 artifacts::{
245 output_selection::OutputSelection, EvmVersion, Settings, SolcInput, Source, Sources,
246 },
247 solc::{Solc, SolcLanguage},
248 };
249 use std::path::PathBuf;
250
251 #[test]
252 #[allow(clippy::field_reassign_with_default)]
253 fn test_analyze_contract_with_three_statements() {
254 // Create a simple Solidity contract with three statements
255 let contract_source = r#"
256// SPDX-License-Identifier: MIT
257pragma solidity ^0.8.0;
258
259contract SimpleContract {
260 uint256 public value;
261
262 function setValue(uint256 newValue) public {
263 value = newValue; // Statement 1: Assignment
264 emit ValueSet(newValue); // Statement 2: Event emission
265 value = value + 1; // Statement 3: Increment
266 }
267
268 event ValueSet(uint256 value);
269}
270"#;
271
272 // Create the solc input using the correct types
273 let file_path = PathBuf::from("SimpleContract.sol");
274 let sources =
275 Sources::from_iter([(file_path.clone(), Source::new(contract_source.to_string()))]);
276
277 let mut settings = Settings::default();
278 settings.output_selection = OutputSelection::complete_output_selection();
279 // Set a valid EVM version for Solidity 0.8.19
280 settings.evm_version = Some(EvmVersion::Paris);
281
282 let input = SolcInput::new(SolcLanguage::Solidity, sources, settings);
283
284 // Compile using Solc
285 let version = semver::Version::new(0, 8, 19);
286 let compiler = Solc::find_or_install(&version).expect("Failed to find or install Solc");
287 let output = compiler.compile_exact(&input).expect("Compilation failed");
288 println!("input {input:?}");
289 println!("output {output:?}");
290
291 // Create fake metadata for testing
292 // Note: SourceCodeMetadata doesn't have Default impl, so we need to create it manually
293 let source_code_meta = foundry_block_explorers::contract::SourceCodeMetadata::SourceCode(
294 contract_source.to_string(),
295 );
296
297 let meta = Metadata {
298 source_code: source_code_meta,
299 abi: String::new(),
300 contract_name: "SimpleContract".to_string(),
301 compiler_version: "0.8.19".to_string(),
302 optimization_used: 200,
303 runs: 200,
304 constructor_arguments: Default::default(),
305 evm_version: "paris".to_string(),
306 library: String::new(),
307 license_type: String::new(),
308 proxy: 0,
309 implementation: None,
310 swarm_source: String::new(),
311 };
312
313 let artifact = Artifact { meta, input, output };
314
315 // Run the analysis
316 let result = analyze(&artifact).expect("Analysis should succeed");
317
318 // Verify the analysis result
319 assert!(!result.sources.is_empty(), "Should have analyzed at least one source file");
320
321 let source_result = result.sources.get(&0).expect("Should find the source file");
322 assert_eq!(source_result.path, file_path);
323 assert!(!source_result.steps.is_empty(), "Should have analyzed steps in the contract");
324
325 // Verify that we have analysis results for the function
326 // Since we can't directly access source content from SourceStep, we'll check the number of steps
327 let step_count = source_result.steps.len();
328 assert!(step_count > 0, "Should have found steps in the contract");
329 }
330}