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}