From 243e266241a5ef29ee22551875a6f160e7fd2c72 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 16 May 2025 12:22:31 +0200 Subject: [PATCH 01/15] changed vector to map --- lib/bytecode_verification/parse_json.rs | 60 +++++++++++++------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/bytecode_verification/parse_json.rs b/lib/bytecode_verification/parse_json.rs index 0da4060..344b519 100644 --- a/lib/bytecode_verification/parse_json.rs +++ b/lib/bytecode_verification/parse_json.rs @@ -326,15 +326,17 @@ impl ProjectInfo { .unwrap() .to_string(); if identifier.starts_with("t_struct") { - let struct_slots: Vec<(u64, U256, Option)> = vec![( - type_name - .get("referencedDeclaration") - .unwrap() - .as_u64() - .unwrap(), + let struct_slots: HashMap)> = HashMap::from([( U256::from_str("0x0").unwrap(), // this won't be used as we only have to add the types - None, - )]; + ( + type_name + .get("referencedDeclaration") + .unwrap() + .as_u64() + .unwrap(), + None, + ), + )]); let mut storage: Vec = vec![]; // this won't be used as we only have to add the types for source in sources.values() { if let Some(ast) = source.ast.clone() { @@ -440,14 +442,14 @@ impl ProjectInfo { sources: &BTreeMap, node: &EAstNode, type_defs: &Types, - struct_slots: &Vec<(u64, U256, Option)>, + struct_slots: &HashMap)>, types: &mut HashMap, storage: &mut Vec, ) { if node.node_type == NodeType::StructDefinition && node.id.is_some() { let mut storage_var_id: Option = None; // parse all struct definitions for each struct -> slot pair. - for (struct_id, slot, name) in struct_slots { + for (slot, (struct_id,name)) in struct_slots { let struct_id = *struct_id; let node_id = node.id.unwrap() as u64; if node_id == struct_id { @@ -593,7 +595,7 @@ impl ProjectInfo { types: &mut HashMap, ) { // Tuple: (struct AST ID, slot, name of variable containing the slot) - let mut struct_slots: Vec<(u64, U256, Option)> = vec![]; + let mut struct_slots: HashMap)> = HashMap::new(); // find pairs (storage slot => struct AST ID) for source in sources.values() { if let Some(ast) = source.ast.clone() { @@ -626,7 +628,7 @@ impl ProjectInfo { sources: &BTreeMap, node: &EAstNode, exported_ids: &Vec, - struct_slots: &mut Vec<(u64, U256, Option)>, + struct_slots: &mut HashMap)>, ) { if node.node_type == NodeType::ContractDefinition && node.id.is_some() @@ -700,7 +702,7 @@ impl ProjectInfo { top_node, stmt_ref["declaration"].as_u64().unwrap() ) { - struct_slots.push((struct_id, var_slot, Some(var_name))); + struct_slots.insert(var_slot, (struct_id, Some(var_name))); // if no variable declaration can be found, try to find // functions with the variable as parameter. } else if let Some((_, _, function_id, param_id)) @@ -754,8 +756,8 @@ impl ProjectInfo { top_node, var_ref_id.as_u64().unwrap() ) { - if !struct_slots.iter().any(|(_, slot, _)| slot.eq(&var_slot)) { - struct_slots.push((struct_id, var_slot, Some(var_name))); + if !struct_slots.contains_key(&var_slot) { + struct_slots.insert(var_slot, (struct_id, Some(var_name))); } } } @@ -764,11 +766,11 @@ impl ProjectInfo { // as we have no associated variable for the slot, // we use the name of the outer function. let var_slot = U256::from_str(slot_value.as_str().unwrap()).unwrap(); - if !struct_slots.iter().any(|(_, slot, _)| slot.eq(&var_slot)) { - struct_slots.push( + if !struct_slots.contains_key(&var_slot) { + struct_slots.insert( + var_slot, ( struct_id, - var_slot, Some(format!("[{}]", outer_function)) ) ); @@ -810,16 +812,18 @@ impl ProjectInfo { } else { function_name = None; } - struct_slots.push(( - struct_id, - U256::from_str( - slot_value - .as_str() - .unwrap(), - ) - .unwrap(), - function_name, - )); + let var_slot = U256::from_str( + slot_value + .as_str() + .unwrap(), + ) + .unwrap(); + if !struct_slots.contains_key(&var_slot) { + struct_slots.insert( + var_slot, + (struct_id, function_name) + ); + } } } } From 1937be92231a17d70cb2d40cc915440386b53e26 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 16 May 2025 13:56:37 +0200 Subject: [PATCH 02/15] added another uniqueness check --- src/dvf.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dvf.rs b/src/dvf.rs index 28ec65e..9d3eb8a 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -1004,7 +1004,12 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { ); contract_state.add_forge_inspect(&implementation_layout); - storage.extend(tmp_project_info.storage.clone()); + // Extend storage with implementation storage variables, ensuring unique slots + for storage_var in &tmp_project_info.storage { + if !storage.iter().any(|existing| existing.slot == storage_var.slot) { + storage.push(storage_var.clone()); + } + } types.extend(tmp_project_info.types.clone()); imp_project_info = Some(tmp_project_info); } From ce00cdbc902f7fac92f0896bb1d19b98dc0032a6 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 16 May 2025 15:06:53 +0200 Subject: [PATCH 03/15] fmt + clippy --- lib/bytecode_verification/parse_json.rs | 28 ++++++++----------------- src/dvf.rs | 5 ++++- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/bytecode_verification/parse_json.rs b/lib/bytecode_verification/parse_json.rs index 344b519..c5054ff 100644 --- a/lib/bytecode_verification/parse_json.rs +++ b/lib/bytecode_verification/parse_json.rs @@ -449,7 +449,7 @@ impl ProjectInfo { if node.node_type == NodeType::StructDefinition && node.id.is_some() { let mut storage_var_id: Option = None; // parse all struct definitions for each struct -> slot pair. - for (slot, (struct_id,name)) in struct_slots { + for (slot, (struct_id, name)) in struct_slots { let struct_id = *struct_id; let node_id = node.id.unwrap() as u64; if node_id == struct_id { @@ -756,9 +756,7 @@ impl ProjectInfo { top_node, var_ref_id.as_u64().unwrap() ) { - if !struct_slots.contains_key(&var_slot) { - struct_slots.insert(var_slot, (struct_id, Some(var_name))); - } + struct_slots.entry(var_slot).or_insert((struct_id, Some(var_name))); } } } else if let Some(slot_value) = arg[param_id].get("value") { @@ -766,15 +764,7 @@ impl ProjectInfo { // as we have no associated variable for the slot, // we use the name of the outer function. let var_slot = U256::from_str(slot_value.as_str().unwrap()).unwrap(); - if !struct_slots.contains_key(&var_slot) { - struct_slots.insert( - var_slot, - ( - struct_id, - Some(format!("[{}]", outer_function)) - ) - ); - } + struct_slots.entry(var_slot).or_insert((struct_id, Some(outer_function))); } } } @@ -818,12 +808,12 @@ impl ProjectInfo { .unwrap(), ) .unwrap(); - if !struct_slots.contains_key(&var_slot) { - struct_slots.insert( - var_slot, - (struct_id, function_name) - ); - } + struct_slots + .entry(var_slot) + .or_insert(( + struct_id, + function_name, + )); } } } diff --git a/src/dvf.rs b/src/dvf.rs index 9d3eb8a..e3cff68 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -1006,7 +1006,10 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { // Extend storage with implementation storage variables, ensuring unique slots for storage_var in &tmp_project_info.storage { - if !storage.iter().any(|existing| existing.slot == storage_var.slot) { + if !storage + .iter() + .any(|existing| existing.slot == storage_var.slot) + { storage.push(storage_var.clone()); } } From c684037570ded809f48425f4f53f3dddc69cc6e1 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 4 Jul 2025 10:36:46 +0200 Subject: [PATCH 04/15] fixed ordering --- lib/bytecode_verification/parse_json.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/bytecode_verification/parse_json.rs b/lib/bytecode_verification/parse_json.rs index c5054ff..1258602 100644 --- a/lib/bytecode_verification/parse_json.rs +++ b/lib/bytecode_verification/parse_json.rs @@ -326,7 +326,7 @@ impl ProjectInfo { .unwrap() .to_string(); if identifier.starts_with("t_struct") { - let struct_slots: HashMap)> = HashMap::from([( + let struct_slots_vec: Vec<(U256, (u64, Option))> = Vec::from([( U256::from_str("0x0").unwrap(), // this won't be used as we only have to add the types ( type_name @@ -345,7 +345,7 @@ impl ProjectInfo { sources, top_node, type_defs, - &struct_slots, + &struct_slots_vec, types, &mut storage, ); @@ -442,7 +442,7 @@ impl ProjectInfo { sources: &BTreeMap, node: &EAstNode, type_defs: &Types, - struct_slots: &HashMap)>, + struct_slots: &Vec<(U256, (u64, Option))>, types: &mut HashMap, storage: &mut Vec, ) { @@ -604,6 +604,11 @@ impl ProjectInfo { } } } + + // Order struct_slots by key for deterministic results + let mut struct_slots_vec: Vec<(U256, (u64, Option))> = struct_slots.iter().map(|(k, v)| (*k, v.clone())).collect(); + struct_slots_vec.sort_by(|a, b| a.0.cmp(&b.0)); + // parse the struct members + types for source in sources.values() { if let Some(ast) = source.ast.clone() { @@ -612,7 +617,7 @@ impl ProjectInfo { sources, node, type_defs, - &struct_slots, + &struct_slots_vec, types, storage, ); From d03d90963703bf4d02410967e9cac9b89085d9e1 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 4 Jul 2025 11:25:32 +0200 Subject: [PATCH 05/15] fixed test case file --- lib/bytecode_verification/compare_bytecodes.rs | 2 +- lib/bytecode_verification/parse_json.rs | 3 ++- lib/bytecode_verification/verify_bytecode.rs | 16 ++++------------ src/dvf.rs | 4 ++-- tests/expected_dvfs/CrazyHiddenStruct.dvf.json | 4 ++-- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/bytecode_verification/compare_bytecodes.rs b/lib/bytecode_verification/compare_bytecodes.rs index 6553dec..876d700 100644 --- a/lib/bytecode_verification/compare_bytecodes.rs +++ b/lib/bytecode_verification/compare_bytecodes.rs @@ -297,7 +297,7 @@ impl CompareInitCode { for (arg, value) in project_info.constructor_args.iter_mut().zip(&decoded_args) { let encoded_value = value.abi_encode_packed(); - if encoded_value.len() == 0 { + if encoded_value.is_empty() { // Here we keep the arg.type_string we previous extracted from the ABI // This happens with empty arrays arg.value = "0x".to_string(); diff --git a/lib/bytecode_verification/parse_json.rs b/lib/bytecode_verification/parse_json.rs index 1258602..db28c4f 100644 --- a/lib/bytecode_verification/parse_json.rs +++ b/lib/bytecode_verification/parse_json.rs @@ -606,7 +606,8 @@ impl ProjectInfo { } // Order struct_slots by key for deterministic results - let mut struct_slots_vec: Vec<(U256, (u64, Option))> = struct_slots.iter().map(|(k, v)| (*k, v.clone())).collect(); + let mut struct_slots_vec: Vec<(U256, (u64, Option))> = + struct_slots.iter().map(|(k, v)| (*k, v.clone())).collect(); struct_slots_vec.sort_by(|a, b| a.0.cmp(&b.0)); // parse the struct members + types diff --git a/lib/bytecode_verification/verify_bytecode.rs b/lib/bytecode_verification/verify_bytecode.rs index 5f0b05d..782af68 100644 --- a/lib/bytecode_verification/verify_bytecode.rs +++ b/lib/bytecode_verification/verify_bytecode.rs @@ -24,7 +24,7 @@ pub fn print_verification_summary( contract_address: &Address, status: CompareBytecode, project_info: &ProjectInfo, - on_chain_bytecode: &String, + on_chain_bytecode: &str, ) { let mut table = Table::new(); @@ -70,11 +70,7 @@ pub fn print_verification_summary( table.printstd(); } -pub fn write_out_bytecodes( - project_info: &ProjectInfo, - on_chain_bytecode: &String, - table: &mut Table, -) { +pub fn write_out_bytecodes(project_info: &ProjectInfo, on_chain_bytecode: &str, table: &mut Table) { let mut compiled_file = File::create("compiled_bytecode.txt").expect("Could not create file"); let mut on_chain_file = File::create("on_chain_bytecode.txt").expect("Could not create file"); @@ -96,11 +92,7 @@ pub fn write_out_bytecodes( ]); } -pub fn write_out_initcodes( - project_info: &ProjectInfo, - on_chain_initcode: &String, - table: &mut Table, -) { +pub fn write_out_initcodes(project_info: &ProjectInfo, on_chain_initcode: &str, table: &mut Table) { let mut compiled_file = File::create("compiled_initcode.txt").expect("Could not create file"); let mut on_chain_file = File::create("on_chain_initcode.txt").expect("Could not create file"); @@ -123,7 +115,7 @@ pub fn print_generation_summary( contract_address: &Address, status: CompareBytecode, project_info: &ProjectInfo, - on_chain_bytecode: &String, + on_chain_bytecode: &str, pretty_printer: &PrettyPrinter, ) { let mut table = Table::new(); diff --git a/src/dvf.rs b/src/dvf.rs index e3cff68..bf79746 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -164,7 +164,7 @@ fn validate_dvf( return Err(ValidationError::from("Different codehash.")); } - let pretty_printer = PrettyPrinter::new(&config, Some(®istry)); + let pretty_printer = PrettyPrinter::new(config, Some(registry)); // Validate Storage slots print_progress("Validating Storage Variables.", &mut pc, &progress_mode); @@ -181,7 +181,7 @@ fn validate_dvf( if !storage_variable.compare(¤t_val[start_index..end_index]) { let message = get_mismatch_msg( &pretty_printer, - &storage_variable, + storage_variable, ¤t_val[start_index..end_index], ); if continue_on_mismatch { diff --git a/tests/expected_dvfs/CrazyHiddenStruct.dvf.json b/tests/expected_dvfs/CrazyHiddenStruct.dvf.json index 9fcc21a..cf2bd21 100644 --- a/tests/expected_dvfs/CrazyHiddenStruct.dvf.json +++ b/tests/expected_dvfs/CrazyHiddenStruct.dvf.json @@ -1,6 +1,6 @@ { "version": "0.9.1", - "id": "0x71f27bd042cf53e6783d03336d86171f7d728e61f867e60071ae32818de10da2", + "id": "0x294a145622ea4fa416a4fe6395b3a307e7d692e6956c2fd0e2192831c7e776fa", "contract_name": "CrazyHiddenStruct", "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "chain_id": 1337, @@ -484,7 +484,7 @@ { "slot": "0xa83659c989cfe332581a2ed207e0e6d23d9199b0de773442a1e23a9b8c5138f0", "offset": 0, - "var_name": "CrazyHiddenStruct.[_setOwner].AddressSlot.value", + "var_name": "CrazyHiddenStruct._setOwner.AddressSlot.value", "var_type": "t_address", "value": "0x5fbdb2315678afecb367f032d93f642f64180aa3", "comparison_operator": "Equal" From 61b908b45d0355f75d61a9b74a61604fa89878ac Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Wed, 25 Feb 2026 15:21:56 +0100 Subject: [PATCH 06/15] fmt --- lib/dvf/discovery.rs | 2 +- tests/with_metadata/src/PullPayment.sol | 2 +- tests/with_metadata/src/PureFactory.sol | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/dvf/discovery.rs b/lib/dvf/discovery.rs index 8c7f49a..7559904 100644 --- a/lib/dvf/discovery.rs +++ b/lib/dvf/discovery.rs @@ -166,7 +166,7 @@ pub fn discover_storage_and_events( ); contract_state.add_forge_inspect(&fi_impl_layout, &fi_impl_ir); - // Extend storage with implementation storage variables, ensuring unique slots + // Extend storage with implementation storage variables, ensuring unique slots for storage_var in &tmp_project_info.storage { if !storage_layout .iter() diff --git a/tests/with_metadata/src/PullPayment.sol b/tests/with_metadata/src/PullPayment.sol index 8aa6261..32c329c 100644 --- a/tests/with_metadata/src/PullPayment.sol +++ b/tests/with_metadata/src/PullPayment.sol @@ -105,6 +105,7 @@ abstract contract Ownable is Context { emit OwnershipTransferred(oldOwner, newOwner); } } + // OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) /** @@ -474,4 +475,3 @@ contract PullPayment { function dummy() external {} } - \ No newline at end of file diff --git a/tests/with_metadata/src/PureFactory.sol b/tests/with_metadata/src/PureFactory.sol index 4223daf..7e4c0d9 100644 --- a/tests/with_metadata/src/PureFactory.sol +++ b/tests/with_metadata/src/PureFactory.sol @@ -23,4 +23,3 @@ contract PureDeployer { function dummy() external {} } - \ No newline at end of file From bc00496080cdcfae15bb68c6c97f5778a76edb01 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Wed, 25 Feb 2026 17:01:50 +0100 Subject: [PATCH 07/15] added clean to ci tests --- tests/ci_tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ci_tests.sh b/tests/ci_tests.sh index bddd580..64ff676 100755 --- a/tests/ci_tests.sh +++ b/tests/ci_tests.sh @@ -24,6 +24,7 @@ pkill geth || true pkill anvil || true pkill cached_proxy || true rm -rf /tmp/uni-factory /tmp/usdc_implementation2 +cargo clean cargo build cargo clippy mkdir -p /tmp/dvfs From 67b0ff1fe342a8c59e6581238b97417a04714bff Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Thu, 26 Feb 2026 11:19:17 +0100 Subject: [PATCH 08/15] fixed ci cache keys --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97b092b..c414ee7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,10 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ - - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }} restore-keys: | + ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}- ${{ runner.os }}-cargo- - name: Install dependencies From 9a23565f4c1d06217280fd2864d2673a5e347c2f Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Thu, 26 Feb 2026 11:38:07 +0100 Subject: [PATCH 09/15] merge main --- lib/bytecode_verification/parse_json.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/bytecode_verification/parse_json.rs b/lib/bytecode_verification/parse_json.rs index d9a5c49..186b6f3 100644 --- a/lib/bytecode_verification/parse_json.rs +++ b/lib/bytecode_verification/parse_json.rs @@ -324,12 +324,14 @@ impl ProjectInfo { ); // add base type if base_identifier.starts_with("t_struct") { - let struct_slots: Vec<(u64, U256, Option)> = vec![( - type_name["baseType"]["referencedDeclaration"] - .as_u64() - .unwrap(), + let struct_slots_vec: Vec<(U256, (u64, Option))> = vec![( U256::from(0), - None, + ( + type_name["baseType"]["referencedDeclaration"] + .as_u64() + .unwrap(), + None, + ), )]; // we only need the types, so we use a dummy storage vector let mut storage: Vec = vec![]; @@ -340,7 +342,7 @@ impl ProjectInfo { sources, node, type_defs, - &struct_slots, + &struct_slots_vec, types, &mut storage, ); From fdccd0a10e64920c097c658b06354507e0f8f517 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Thu, 26 Feb 2026 14:29:45 +0100 Subject: [PATCH 10/15] updated test dvfs and changed test name --- tests/expected_dvfs/PullPayment.dvf.json | 2 +- tests/expected_dvfs/PureDeployer.dvf.json | 2 +- tests/test_end_to_end.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/expected_dvfs/PullPayment.dvf.json b/tests/expected_dvfs/PullPayment.dvf.json index 803c02e..b978e06 100644 --- a/tests/expected_dvfs/PullPayment.dvf.json +++ b/tests/expected_dvfs/PullPayment.dvf.json @@ -7,7 +7,7 @@ "deployment_block_num": 2, "init_block_num": 3, "deployment_tx": "0x42d75b67073ebd107239df89b1f512be1972205d5bc728bcafeb3dc5675d0d15", - "codehash": "0x0d737bb1623f9f2c92d870330a5e6e657949fc96c0cd7b09d93be7565d352580", + "codehash": "0x2f5330b21d5be1ae2b7e5e526f3a309b0bb64480c1cf0d9f6be0794a5a7f6ce4", "insecure": false, "immutables": [ { diff --git a/tests/expected_dvfs/PureDeployer.dvf.json b/tests/expected_dvfs/PureDeployer.dvf.json index 83b64af..0b110bb 100644 --- a/tests/expected_dvfs/PureDeployer.dvf.json +++ b/tests/expected_dvfs/PureDeployer.dvf.json @@ -7,7 +7,7 @@ "deployment_block_num": 2, "init_block_num": 3, "deployment_tx": "0xc3f03c210e501eaca1459141ce84bb36675ca2a1cc28d6d716c9794d2b9d4e88", - "codehash": "0xaf3123d8bc1c73c93f3c9b0176b44bd11c0db94032b5a1326f28b7ce6967a06b", + "codehash": "0x6347979e9c068d544e8acaff418347562606d8f9ee014ac90cf06c0c7cb0ea93", "insecure": false, "immutables": [], "constructor_args": [], diff --git a/tests/test_end_to_end.rs b/tests/test_end_to_end.rs index e53d806..bfe6295 100644 --- a/tests/test_end_to_end.rs +++ b/tests/test_end_to_end.rs @@ -1507,7 +1507,7 @@ mod tests { } #[test] - fn test_pure_factory() { + fn test_e2e_pure_factory() { let port = 8553u16; let config_file = match DVFConfig::test_config_file(Some(port)) { Ok(config) => config, From a59b8db309bbfc669ac5586f0f0550dd93224fed Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Thu, 26 Feb 2026 15:26:16 +0100 Subject: [PATCH 11/15] updated another test dvf --- test1.json | 53 +++++++++++++++++++ .../script/Deploy_ConstructorFactory.s.sol | 21 ++++++++ tests/Contracts/src/ConstructorFactory.sol | 37 +++++++++++++ tests/expected_dvfs/PureChild.dvf.json | 2 +- 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 test1.json create mode 100644 tests/Contracts/script/Deploy_ConstructorFactory.s.sol create mode 100644 tests/Contracts/src/ConstructorFactory.sol diff --git a/test1.json b/test1.json new file mode 100644 index 0000000..e73753a --- /dev/null +++ b/test1.json @@ -0,0 +1,53 @@ +{ + "version": "0.9.1", + "id": "0x9c82796f5193c4db0965d63046ed83572b2e8140f2be2f9587125b37e8d8710c", + "contract_name": "ConstructorFactory", + "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "chain_id": 31337, + "deployment_block_num": 1, + "init_block_num": 2, + "deployment_tx": "0x9f852dc8e8b0ad7dee5533ba7854bb676f3f270a3720c7501bbd0a65b1194663", + "codehash": "0x035d6d745ec488636551786986451c4f4d368d1fd9facc2c4f11b8b3733f5aa9", + "insecure": false, + "immutables": [], + "constructor_args": [], + "critical_storage_variables": [ + { + "slot": "0x0", + "offset": 0, + "var_name": "children.length", + "var_type": "t_uint256", + "value": "0x0000000000000000000000000000000000000000000000000000000000000002", + "value_hint": "2", + "comparison_operator": "Equal" + }, + { + "slot": "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563", + "offset": 0, + "var_name": "children[0]", + "var_type": "t_address", + "value": "0xa16e02e87b7454126e5e10d957a927a7f5b5d2be", + "comparison_operator": "Equal" + }, + { + "slot": "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564", + "offset": 0, + "var_name": "children[1]", + "var_type": "t_address", + "value": "0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968", + "comparison_operator": "Equal" + } + ], + "critical_events": [], + "unvalidated_metadata": { + "author_name": "Author", + "description": "System Description", + "hardfork": [ + "paris", + "shanghai" + ], + "audit_report": "https://example.org/report.pdf", + "source_url": "https://github.com/source/code", + "security_contact": "security@example.org" + } +} \ No newline at end of file diff --git a/tests/Contracts/script/Deploy_ConstructorFactory.s.sol b/tests/Contracts/script/Deploy_ConstructorFactory.s.sol new file mode 100644 index 0000000..0c0284f --- /dev/null +++ b/tests/Contracts/script/Deploy_ConstructorFactory.s.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.8.12; + +import "forge-std/Script.sol"; +import "../src/ConstructorFactory.sol"; + +contract Deploy is Script { + function run() external { + uint256 anvilDefaultKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + vm.startBroadcast(anvilDefaultKey); + + ConstructorFactory factory = new ConstructorFactory(); + factory.createChild(100); + factory.createChild(200); + + for (uint256 i = 0; i < 5; i++) { + factory.dummy(); + } + + vm.stopBroadcast(); + } +} diff --git a/tests/Contracts/src/ConstructorFactory.sol b/tests/Contracts/src/ConstructorFactory.sol new file mode 100644 index 0000000..6571ed3 --- /dev/null +++ b/tests/Contracts/src/ConstructorFactory.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Child { + address public owner; + uint256 public value; + + constructor(address _owner, uint256 _value) { + owner = _owner; + value = _value; + } + + function setValue(uint256 _value) external { + value = _value; + } +} + +contract ConstructorFactory { + address[] public children; + + constructor() { + Child child = new Child(msg.sender, 42); + children.push(address(child)); + } + + function createChild(uint256 _value) external returns (address) { + Child child = new Child(msg.sender, _value); + children.push(address(child)); + return address(child); + } + + function getChildrenCount() external view returns (uint256) { + return children.length; + } + + function dummy() external {} +} diff --git a/tests/expected_dvfs/PureChild.dvf.json b/tests/expected_dvfs/PureChild.dvf.json index 88d5a01..6f3f0ee 100644 --- a/tests/expected_dvfs/PureChild.dvf.json +++ b/tests/expected_dvfs/PureChild.dvf.json @@ -7,7 +7,7 @@ "deployment_block_num": 2, "init_block_num": 3, "deployment_tx": "0xc3f03c210e501eaca1459141ce84bb36675ca2a1cc28d6d716c9794d2b9d4e88", - "codehash": "0x58f7c91b706c7b2e5694cdb86ba16504d4d8851df9522b29299fe8bb95403d57", + "codehash": "0x96e74684860511bd54dc85352acf15d678b3e88a6ab3bc7414aebbaec5571159", "insecure": false, "immutables": [], "constructor_args": [], From a69441051d4e2a7e94a1ae4d757fbaf54f789aa1 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 27 Feb 2026 13:37:00 +0100 Subject: [PATCH 12/15] added streaming for debug trace transactions --- .gitignore | 1 + docs/config.md | 2 +- lib/dvf/discovery.rs | 80 +++-- lib/state/contract_state.rs | 237 ++++++++++++++- lib/utils/progress.rs | 2 +- lib/web3.rs | 565 +++++++++++++++++++++++++++++++++++- src/dvf.rs | 52 ++-- 7 files changed, 870 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 302d322..5726310 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ on_chain_bytecode.txt .env tests/hardhat/package-lock.json .dv_config.json +/.claude diff --git a/docs/config.md b/docs/config.md index 7a3fcfe..a40e492 100644 --- a/docs/config.md +++ b/docs/config.md @@ -23,7 +23,7 @@ When running the `dvf` command, the default configuration file is expected at `$ | - - `api_url` | Chain-specific Blockscout API URL | | - - `api_key` | Chain-specific Blockscout API Key | | `max_blocks_per_event_query` | Number of blocks that can be queried at once in `getLogs`, optional, defaults to 9999 | -| `web3_timeout` | Timeout in seconds for web3 RPC queries, optional, defaults to 5000 | +| `web3_timeout` | Timeout in milliseconds for web3 RPC queries, optional, defaults to 5000 | | `signer` | Configuration on how to sign, optional | | - `wallet_address` | Address which is used to sign | | - `wallet_type` | Can have different structure | diff --git a/lib/dvf/discovery.rs b/lib/dvf/discovery.rs index b028fb9..ac0bad2 100644 --- a/lib/dvf/discovery.rs +++ b/lib/dvf/discovery.rs @@ -19,10 +19,10 @@ use crate::state::forge_inspect; use crate::utils::pretty::PrettyPrinter; use crate::utils::progress::{print_progress, ProgressMode}; use crate::utils::read_write_file::get_project_paths; +use crate::state::contract_state::MappingUsages; use crate::web3; use crate::web3::stop_anvil_instance; -use crate::web3::TraceWithAddress; -use alloy_node_bindings::AnvilInstance; + pub struct DiscoveryParams<'a> { pub config: &'a DVFConfig, @@ -46,9 +46,8 @@ pub struct DiscoveryParams<'a> { pub progress_mode: &'a ProgressMode, pub use_storage_range: bool, pub tx_hashes: Option>, - // Optional cache (used by inspect-tx): reuse an already computed trace and config - pub cached_traces: Option>, - pub cached_anvil_config: Option<&'a DVFConfig>, + // Optional cache (used by inspect-tx): reuse pre-computed mapping usages + pub cached_mapping_usages: Option, } pub struct DiscoveryResult { @@ -211,47 +210,48 @@ pub fn discover_storage_and_events( )?; print_progress("Getting relevant traces.", params.pc, params.progress_mode); - let mut seen_transactions = HashSet::new(); let mut missing_traces = false; - for (index, tx_hash) in tx_hashes.iter().enumerate() { - if seen_transactions.contains(tx_hash) { - continue; - } - seen_transactions.insert(tx_hash); - - info!("Getting trace for {}", tx_hash); - // Use cached trace if provided (inspect-tx), otherwise fetch - let fetched = if let Some(ref cached) = params.cached_traces { - debug!("Using cached trace at index {} of {}", index, cached.len()); - Ok(( - cached[index].clone(), - None::, - None::, - )) - } else { - web3::get_eth_debug_trace_sim(params.config, tx_hash) - }; - match fetched { - Ok((trace, anvil_config, anvil_instance)) => { - let record_traces_config: &DVFConfig = if params.cached_traces.is_some() { - params.cached_anvil_config.unwrap_or(params.config) - } else { - anvil_config.as_ref().unwrap_or(params.config) - }; - if let Err(err) = contract_state.record_traces(record_traces_config, vec![trace]) { + if let Some(cached_usages) = params.cached_mapping_usages { + // Inject pre-computed mapping usages directly (from inspect-tx) + debug!("Using cached mapping usages"); + contract_state.inject_mapping_usages(cached_usages); + } else { + // Stream traces to collect mapping usages + let mut seen_transactions = HashSet::new(); + for tx_hash in tx_hashes.iter() { + if seen_transactions.contains(tx_hash) { + continue; + } + seen_transactions.insert(tx_hash); + + info!("Getting trace for {}", tx_hash); + let trace_address = match web3::get_receipt_address(params.config, tx_hash) { + Ok(addr) => addr, + Err(err) => { missing_traces = true; info!("Warning. The trace for {tx_hash} cannot be obtained. Some mapping slots might not be decodable. You can try to increase the timeout in the config. Error: {}", err); + continue; } - if params.cached_traces.is_none() { + }; + let mut processor = crate::state::contract_state::MappingUsageProcessor::new( + *params.address, + trace_address, + params.config, + tx_hash.clone(), + true, + ); + match web3::stream_eth_debug_trace_sim(params.config, tx_hash, &mut processor) { + Ok((_metadata, _address, _anvil_config, anvil_instance)) => { + contract_state.inject_mapping_usages(processor.mapping_usages); if let Some(anvil_instance) = anvil_instance { stop_anvil_instance(anvil_instance); } } - } - Err(err) => { - missing_traces = true; - info!("Warning. The trace for {tx_hash} cannot be obtained. Some mapping slots might not be decodable. You can try to increase the timeout in the config. Error: {}", err); + Err(err) => { + missing_traces = true; + info!("Warning. The trace for {tx_hash} cannot be obtained. Some mapping slots might not be decodable. You can try to increase the timeout in the config. Error: {}", err); + } } } } @@ -546,8 +546,7 @@ pub fn create_discovery_params_for_init<'a>( progress_mode, use_storage_range: true, tx_hashes: None, - cached_traces: None, - cached_anvil_config: None, + cached_mapping_usages: None, } } @@ -593,7 +592,6 @@ pub fn create_discovery_params_for_update<'a>( progress_mode, use_storage_range: false, // cannot use storage range here as we are only trying to get a subset of the state tx_hashes: None, - cached_traces: None, - cached_anvil_config: None, + cached_mapping_usages: None, } } diff --git a/lib/state/contract_state.rs b/lib/state/contract_state.rs index 781c57f..97589ba 100644 --- a/lib/state/contract_state.rs +++ b/lib/state/contract_state.rs @@ -17,7 +17,9 @@ use crate::state::forge_inspect::{ ForgeInspectIrOptimized, ForgeInspectLayoutStorage, StateVariable, TypeDescription, }; use crate::utils::pretty::PrettyPrinter; -use crate::web3::{get_internal_create_addresses, StorageSnapshot, TraceWithAddress}; +use crate::web3::{ + get_internal_create_addresses, StorageSnapshot, StructLogProcessor, TraceWithAddress, +}; fn hash_u256(u: &U256) -> B256 { keccak256(u.to_be_bytes::<32>()) @@ -388,6 +390,16 @@ impl<'a> ContractState<'a> { Ok(()) } + /// Inject pre-computed mapping usages directly, skipping trace processing. + pub fn inject_mapping_usages(&mut self, usages: HashMap>) { + for (index, entries) in usages { + self.mapping_usages + .entry(index) + .or_default() + .extend(entries); + } + } + fn add_to_table(storage_entry: &parse::DVFStorageEntry, table: &mut Table) { PrettyPrinter::add_formatted_to_table( &storage_entry.var_name, @@ -931,3 +943,226 @@ impl<'a> ContractState<'a> { var_type.starts_with("t_userDefinedValueType") } } + +/// Type alias for mapping usages: storage index -> set of (key, derived_slot). +pub type MappingUsages = HashMap>; + +/// Helper: extract mapping key from memory at a SHA3/KECCAK256 opcode. +/// Returns `Some((key_hex, storage_index))` if the input looks like a mapping hash. +fn extract_mapping_key_from_sha3( + stack: &[U256], + memory: &[String], +) -> Option<(String, U256)> { + let length_in_bytes = stack[stack.len() - 2]; + if length_in_bytes < U256::from(32_u64) || length_in_bytes >= U256::from(usize::MAX / 2) { + return None; + } + let mem_str: String = memory.iter().cloned().collect(); + let start_idx = stack[stack.len() - 1].to::() * 2; + let length = length_in_bytes.to::() * 2; + let sha3_input = format!("0x{}", &mem_str[start_idx..(start_idx + length)]); + + let usize_str_length = length_in_bytes.to::() * 2 + 2; + assert!(sha3_input.len() == usize_str_length); + let key = sha3_input[2..usize_str_length - 64].to_string(); + let index = U256::from_str_radix(&sha3_input[usize_str_length - 64..], 16).ok()?; + Some((key, index)) +} + +/// Processor that collects mapping usages for a single address from trace logs. +/// Extracted from `ContractState::record_traces()`. +pub struct MappingUsageProcessor<'a> { + address: Address, + config: &'a DVFConfig, + tx_id: String, + is_first_trace: bool, + depth_to_address: HashMap, + create_addresses: Option>, + key: Option, + index: U256, + pub mapping_usages: MappingUsages, + failed: bool, +} + +impl<'a> MappingUsageProcessor<'a> { + pub fn new( + address: Address, + trace_address: Address, + config: &'a DVFConfig, + tx_id: String, + is_first_trace: bool, + ) -> Self { + let mut depth_to_address: HashMap = HashMap::new(); + depth_to_address.insert(1, trace_address); + MappingUsageProcessor { + address, + config, + tx_id, + is_first_trace, + depth_to_address, + create_addresses: None, + key: None, + index: U256::from(1), + mapping_usages: HashMap::new(), + failed: false, + } + } +} + +impl<'a> StructLogProcessor for MappingUsageProcessor<'a> { + fn process_log(&mut self, log: alloy_rpc_types_trace::geth::StructLog) -> Result<(), crate::dvf::parse::ValidationError> { + if log.stack.is_none() { + return Ok(()); + } + let stack = log.stack.unwrap(); + + if log.op == "CREATE" || log.op == "CREATE2" { + if self.is_first_trace { + if self.create_addresses.is_none() { + self.create_addresses = + Some(get_internal_create_addresses(self.config, &self.tx_id)?); + } + if let Some(ref mut create_ref) = self.create_addresses { + self.depth_to_address + .insert(log.depth + 1, create_ref.remove(0)); + } + } else { + self.depth_to_address + .insert(log.depth + 1, Address::from([0; 20])); + } + } + + if log.op == "CALL" || log.op == "STATICCALL" { + let address_bytes = stack[stack.len() - 2].to_be_bytes::<32>(); + let a = Address::from_slice(&address_bytes[12..]); + self.depth_to_address.insert(log.depth + 1, a); + } + + if log.op == "DELEGATECALL" || log.op == "CALLCODE" { + self.depth_to_address + .insert(log.depth + 1, self.depth_to_address[&log.depth]); + } + + if self.depth_to_address[&log.depth] == self.address { + if let Some(key_in) = self.key.take() { + let target_slot = &stack[stack.len() - 1]; + self.mapping_usages + .entry(self.index) + .or_default() + .insert((key_in, *target_slot)); + } + + if log.op == "KECCAK256" || log.op == "SHA3" { + if let Some(memory) = &log.memory { + if let Some((key, index)) = extract_mapping_key_from_sha3(&stack, memory) { + debug!("Found key {} for index {}.", key, index); + self.key = Some(key); + self.index = index; + } + } + } + } + + Ok(()) + } + + fn trace_failed(&mut self) { + self.failed = true; + self.mapping_usages.clear(); + } +} + +/// Processor that collects mapping usages for ALL addresses encountered in a trace. +/// Used by `inspect-tx` to process the trace once for all contracts. +pub struct MultiAddressMappingProcessor<'a> { + config: &'a DVFConfig, + tx_id: String, + depth_to_address: HashMap, + create_addresses: Option>, + key: Option, + index: U256, + /// Per-address mapping usages. + pub all_mapping_usages: HashMap, + failed: bool, +} + +impl<'a> MultiAddressMappingProcessor<'a> { + pub fn new( + trace_address: Address, + config: &'a DVFConfig, + tx_id: String, + ) -> Self { + let mut depth_to_address: HashMap = HashMap::new(); + depth_to_address.insert(1, trace_address); + MultiAddressMappingProcessor { + config, + tx_id, + depth_to_address, + create_addresses: None, + key: None, + index: U256::from(1), + all_mapping_usages: HashMap::new(), + failed: false, + } + } +} + +impl<'a> StructLogProcessor for MultiAddressMappingProcessor<'a> { + fn process_log(&mut self, log: alloy_rpc_types_trace::geth::StructLog) -> Result<(), crate::dvf::parse::ValidationError> { + if log.stack.is_none() { + return Ok(()); + } + let stack = log.stack.unwrap(); + + if log.op == "CREATE" || log.op == "CREATE2" { + if self.create_addresses.is_none() { + self.create_addresses = + Some(get_internal_create_addresses(self.config, &self.tx_id)?); + } + if let Some(ref mut create_ref) = self.create_addresses { + self.depth_to_address + .insert(log.depth + 1, create_ref.remove(0)); + } + } + + if log.op == "CALL" || log.op == "STATICCALL" { + let address_bytes = stack[stack.len() - 2].to_be_bytes::<32>(); + let a = Address::from_slice(&address_bytes[12..]); + self.depth_to_address.insert(log.depth + 1, a); + } + + if log.op == "DELEGATECALL" || log.op == "CALLCODE" { + self.depth_to_address + .insert(log.depth + 1, self.depth_to_address[&log.depth]); + } + + let current_address = self.depth_to_address[&log.depth]; + + if let Some(key_in) = self.key.take() { + let target_slot = &stack[stack.len() - 1]; + self.all_mapping_usages + .entry(current_address) + .or_default() + .entry(self.index) + .or_default() + .insert((key_in, *target_slot)); + } + + if log.op == "KECCAK256" || log.op == "SHA3" { + if let Some(memory) = &log.memory { + if let Some((key, index)) = extract_mapping_key_from_sha3(&stack, memory) { + debug!("Found key {} for index {}.", key, index); + self.key = Some(key); + self.index = index; + } + } + } + + Ok(()) + } + + fn trace_failed(&mut self) { + self.failed = true; + self.all_mapping_usages.clear(); + } +} diff --git a/lib/utils/progress.rs b/lib/utils/progress.rs index d8f81b9..584cd2c 100644 --- a/lib/utils/progress.rs +++ b/lib/utils/progress.rs @@ -23,7 +23,7 @@ pub fn print_progress(s: &str, i: &mut u64, pm: &ProgressMode) { ProgressMode::BytecodeCheck => 3, ProgressMode::GenerateBuildCache => 1, ProgressMode::ListEvents => 1, - ProgressMode::InspectTx => 10, + ProgressMode::InspectTx => 3, ProgressMode::InspectTxSub => 10, ProgressMode::InspectTxSubNoconf => 4, }; diff --git a/lib/web3.rs b/lib/web3.rs index 848c6b6..1779bf4 100644 --- a/lib/web3.rs +++ b/lib/web3.rs @@ -98,6 +98,126 @@ pub struct TraceWithAddress { pub tx_id: String, } +/// Metadata from a debug trace (everything except structLogs). +#[derive(Debug, Clone)] +pub struct TraceMetadata { + pub failed: bool, + pub gas: u64, + pub return_value: Bytes, +} + +/// Trait for processing StructLog entries one at a time during streaming deserialization. +pub trait StructLogProcessor { + fn process_log(&mut self, log: StructLog) -> Result<(), ValidationError>; + /// Called after all logs have been processed if the trace's `failed` flag is true. + /// Allows the processor to discard results from a failed trace. + fn trace_failed(&mut self); +} + +/// Processor that builds a storage snapshot from trace logs. +/// Extracted from `StorageSnapshot::add_trace()`. +pub struct StorageSnapshotProcessor<'a> { + snapshot: &'a mut HashMap, + address: Address, + config: &'a DVFConfig, + tx_id: String, + depth_to_address: HashMap, + last_storage: HashMap>, + last_depth: u64, + create_addresses: Option>, + failed: bool, +} + +impl<'a> StorageSnapshotProcessor<'a> { + pub fn new( + snapshot: &'a mut HashMap, + address: Address, + config: &'a DVFConfig, + tx_id: String, + ) -> Self { + let mut depth_to_address: HashMap = HashMap::new(); + depth_to_address.insert(1, address); + StorageSnapshotProcessor { + snapshot, + address, + config, + tx_id, + depth_to_address, + last_storage: HashMap::new(), + last_depth: 1, + create_addresses: None, + failed: false, + } + } + + /// Finalize: commit depth-1 storage (we know depth 1 succeeded if trace didn't fail). + pub fn finalize(self) -> Result<(), ValidationError> { + if self.failed { + return Ok(()); + } + commit_storage_to_snapshot(&self.last_storage, 0u64, self.snapshot); + // Check that we used all addresses + if let Some(addrs) = self.create_addresses { + assert_eq!(addrs.len(), 0); + } + Ok(()) + } +} + +impl<'a> StructLogProcessor for StorageSnapshotProcessor<'a> { + fn process_log(&mut self, log: StructLog) -> Result<(), ValidationError> { + if log.stack.is_none() { + return Ok(()); + } + let stack = log.stack.unwrap(); + + if log.op == "CREATE" || log.op == "CREATE2" { + if self.create_addresses.is_none() { + self.create_addresses = + Some(get_internal_create_addresses(self.config, &self.tx_id)?); + } + if let Some(ref mut create_ref) = self.create_addresses { + self.depth_to_address + .insert(log.depth + 1, create_ref.remove(0)); + } + } + + if log.op == "CALL" || log.op == "STATICCALL" { + let address_bytes = stack[stack.len() - 2].to_be_bytes::<32>(); + let a = Address::from_slice(&address_bytes[12..]); + self.depth_to_address.insert(log.depth + 1, a); + } + + if log.op == "DELEGATECALL" || log.op == "CALLCODE" { + self.depth_to_address + .insert(log.depth + 1, self.depth_to_address[&log.depth]); + } + + if &self.depth_to_address[&log.depth] == &self.address && log.op == "SSTORE" { + let last_store = self.last_storage.entry(log.depth).or_default(); + let value = stack[stack.len() - 2]; + let slot = stack[stack.len() - 1]; + last_store.insert(slot, value); + } + + if log.op == "STOP" || log.op == "RETURN" { + commit_storage_to_snapshot(&self.last_storage, log.depth, self.snapshot); + } + if log.depth < self.last_depth { + for depth in log.depth..self.last_depth + 1 { + self.last_storage.remove(&depth); + } + } + self.last_depth = log.depth; + + Ok(()) + } + + fn trace_failed(&mut self) { + self.failed = true; + } +} + pub fn get_block_traces( config: &DVFConfig, block_num: u64, @@ -296,6 +416,37 @@ fn create_trace_with_address( } } +/// Get the target address for a transaction from its receipt. +/// Extracted from `create_trace_with_address()` for reuse with streaming. +pub fn get_receipt_address( + config: &DVFConfig, + tx_id: &str, +) -> Result { + let request_body = json!({ + "jsonrpc": "2.0", + "method": "eth_getTransactionReceipt", + "params": [tx_id], + "id": 1 + }); + let result = send_blocking_web3_post(config, &request_body)?; + let receipt: TransactionReceipt = serde_json::from_value(result)?; + + if let Some(address) = receipt.to { + Ok(address) + } else if let Some(address) = receipt.contract_address { + Ok(address) + } else { + println!( + "[DEBUG] get_receipt_address: No address found in receipt: {:?}", + receipt + ); + Err(ValidationError::from(format!( + "Found no address for tx {}", + tx_id + ))) + } +} + pub fn get_eth_debug_trace( config: &DVFConfig, tx_id: &str, @@ -476,6 +627,405 @@ pub fn get_eth_debug_trace_sim( } } +/// Module for streaming deserialization of debug_traceTransaction responses. +/// Instead of materializing the entire `structLogs` array, we deserialize and process +/// one `StructLog` at a time, keeping memory usage bounded. +mod streaming_trace { + use super::*; + use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; + use std::fmt; + + + /// Seed that drives streaming: navigates the JSON-RPC envelope and processes structLogs + /// one element at a time via the provided `StructLogProcessor`. + struct ResultFieldSeed<'a, P> { + processor: &'a mut P, + metadata: &'a mut TraceMetadata, + } + + /// Visitor for the `result` object inside JSON-RPC response. + /// Handles fields: `failed`, `gas`, `returnValue`, `structLogs`. + struct ResultObjectVisitor<'a, P> { + processor: &'a mut P, + metadata: &'a mut TraceMetadata, + } + + impl<'de, P: StructLogProcessor> Visitor<'de> for ResultObjectVisitor<'_, P> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a trace result object with structLogs") + } + + fn visit_map>(self, mut map: A) -> Result<(), A::Error> { + let mut got_struct_logs = false; + while let Some(key) = map.next_key::()? { + match key.as_str() { + "failed" => { + self.metadata.failed = map.next_value()?; + } + "gas" => { + // Handle both integer and hex string (pathological RPC) + let value: serde_json::Value = map.next_value()?; + self.metadata.gas = match value { + serde_json::Value::Number(n) => n + .as_u64() + .ok_or_else(|| de::Error::custom("Invalid gas number"))?, + serde_json::Value::String(s) => { + u64::from_str_radix(s.trim_start_matches("0x"), 16) + .map_err(de::Error::custom)? + } + _ => return Err(de::Error::custom("Expected gas as hex string or integer")), + }; + } + "returnValue" => { + self.metadata.return_value = map.next_value()?; + } + "structLogs" => { + // Stream the array: deserialize each StructLog and hand to processor + map.next_value_seed(StructLogArraySeed { + processor: self.processor, + })?; + got_struct_logs = true; + } + _ => { + // Skip unknown fields + let _ = map.next_value::()?; + } + } + } + if !got_struct_logs { + return Err(de::Error::custom("missing structLogs field in trace result")); + } + // If `failed` was encountered after `structLogs`, notify the processor + if self.metadata.failed { + self.processor.trace_failed(); + } + Ok(()) + } + } + + impl<'de, P: StructLogProcessor> DeserializeSeed<'de> for ResultFieldSeed<'_, P> { + type Value = (); + + fn deserialize>(self, deserializer: D) -> Result<(), D::Error> { + deserializer.deserialize_map(ResultObjectVisitor { + processor: self.processor, + metadata: self.metadata, + }) + } + } + + /// Seed for streaming deserialization of the `structLogs` array. + struct StructLogArraySeed<'a, P> { + processor: &'a mut P, + } + + struct StructLogArrayVisitor<'a, P> { + processor: &'a mut P, + } + + impl<'de, P: StructLogProcessor> Visitor<'de> for StructLogArrayVisitor<'_, P> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an array of StructLog entries") + } + + fn visit_seq>(self, mut seq: A) -> Result<(), A::Error> { + while let Some(log) = seq.next_element::()? { + self.processor + .process_log(log) + .map_err(de::Error::custom)?; + } + Ok(()) + } + } + + impl<'de, P: StructLogProcessor> DeserializeSeed<'de> for StructLogArraySeed<'_, P> { + type Value = (); + + fn deserialize>(self, deserializer: D) -> Result<(), D::Error> { + deserializer.deserialize_seq(StructLogArrayVisitor { + processor: self.processor, + }) + } + } + + /// Visitor for the top-level JSON-RPC response envelope. + struct RpcEnvelopeVisitor<'a, P> { + processor: &'a mut P, + metadata: &'a mut TraceMetadata, + } + + impl<'de, P: StructLogProcessor> Visitor<'de> for RpcEnvelopeVisitor<'_, P> { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a JSON-RPC response object") + } + + fn visit_map>(self, mut map: A) -> Result<(), A::Error> { + let mut got_result = false; + while let Some(key) = map.next_key::()? { + match key.as_str() { + "result" => { + map.next_value_seed(ResultFieldSeed { + processor: self.processor, + metadata: self.metadata, + })?; + got_result = true; + } + "error" => { + let error: serde_json::Value = map.next_value()?; + return Err(de::Error::custom(format!("Web3Error: {:?}", error))); + } + _ => { + // Skip jsonrpc, id, etc. + let _ = map.next_value::()?; + } + } + } + if !got_result { + return Err(de::Error::custom("No result field in JSON-RPC response")); + } + Ok(()) + } + } + + struct RpcEnvelopeSeed<'a, P> { + processor: &'a mut P, + metadata: &'a mut TraceMetadata, + } + + impl<'de, P: StructLogProcessor> DeserializeSeed<'de> for RpcEnvelopeSeed<'_, P> { + type Value = (); + + fn deserialize>(self, deserializer: D) -> Result<(), D::Error> { + deserializer.deserialize_map(RpcEnvelopeVisitor { + processor: self.processor, + metadata: self.metadata, + }) + } + } + + /// Stream a debug_traceTransaction response, processing each StructLog via the processor. + /// Returns TraceMetadata (failed, gas, returnValue) without materializing the full array. + pub fn stream_trace_response( + response: reqwest::blocking::Response, + processor: &mut P, + ) -> Result { + let mut metadata = TraceMetadata { + failed: false, + gas: 0, + return_value: Bytes::new(), + }; + + let reader = std::io::BufReader::new(response); + let mut deserializer = serde_json::Deserializer::from_reader(reader); + + RpcEnvelopeSeed { + processor, + metadata: &mut metadata, + } + .deserialize(&mut deserializer) + .map_err(|e| ValidationError::from(format!("Streaming trace deserialization error: {}", e)))?; + + Ok(metadata) + } +} + +/// Send a JSON-RPC POST and return the raw response (not deserialized). +fn send_blocking_web3_post_raw( + config: &DVFConfig, + request_body: &serde_json::Value, +) -> Result { + let client = Client::builder() + .timeout(Duration::from_millis(config.web3_timeout)) + .build() + .unwrap(); + + let node_url = config.get_rpc_url()?; + + debug!("Web3 request_body (streaming): {:?}", request_body); + let response = client.post(node_url).json(&request_body).send()?; + + Ok(response) +} + +/// Stream a debug_traceTransaction RPC call, processing each StructLog one at a time. +/// Returns (TraceMetadata, target_address). +pub fn stream_debug_trace( + config: &DVFConfig, + tx_id: &str, + processor: &mut P, +) -> Result<(TraceMetadata, Address), ValidationError> { + debug!("Streaming debug trace for {}", tx_id); + let request_body = json!({ + "jsonrpc": "2.0", + "method": "debug_traceTransaction", + "params": [tx_id, {"enableMemory": true, "enableStorage": true, "enableReturnData": false}], + "id": 1 + }); + println!("a {:?}", request_body); + + let response = send_blocking_web3_post_raw(config, &request_body)?; + let metadata = streaming_trace::stream_trace_response(response, processor)?; + let address = get_receipt_address(config, tx_id)?; + + Ok((metadata, address)) +} + +/// Streaming variant of `get_eth_debug_trace_sim()` with Anvil fallback. +/// Returns (TraceMetadata, target_address, Option, Option). +pub fn stream_eth_debug_trace_sim( + config: &DVFConfig, + tx_id: &str, + processor: &mut P, +) -> Result<(TraceMetadata, Address, Option, Option), ValidationError> { + debug!("Streaming debug trace (with sim fallback)."); + + let request_body = json!({ + "jsonrpc": "2.0", + "method": "debug_traceTransaction", + "params": [tx_id, {"enableMemory": true, "enableStorage": true, "enableReturnData": false}], + "id": 1 + }); + println!("b {:?}", request_body); + + // Try direct streaming first + match send_blocking_web3_post_raw(config, &request_body) { + Ok(response) => { + match streaming_trace::stream_trace_response(response, processor) { + Ok(metadata) => { + let address = get_receipt_address(config, tx_id)?; + return Ok((metadata, address, None, None)); + } + Err(e) => { + info!("Direct streaming trace failed, trying fallback with anvil: {}", e); + } + } + } + Err(_) => { + info!("Initial debug_traceTransaction request failed, trying fallback with anvil"); + } + } + + // Fallback: replay via Anvil, then stream from Anvil + let (block_num, tx_index, tx_result) = get_transaction_details(config, tx_id)?; + let rpc_url = config.get_rpc_url()?; + + let (fork_transaction_hash, fork_block_number) = + match get_previous_transaction_in_block(config, block_num, tx_index)? { + Some(prev_tx_hash) => (Some(prev_tx_hash), None), + None => (None, Some(block_num - 1)), + }; + + let anvil_instance = start_anvil( + &rpc_url, + fork_transaction_hash.as_deref(), + fork_block_number, + get_eth_block_timestamp(config, block_num)?, + )?; + + let mut anvil_config = DVFConfig::default(); + anvil_config + .rpc_urls + .insert(config.active_chain_id.unwrap(), anvil_instance.endpoint()); + anvil_config.active_chain_id = config.active_chain_id; + anvil_config.web3_timeout = config.web3_timeout; + + let from = tx_result["from"].as_str().unwrap(); + let to = tx_result["to"].as_str().unwrap_or("null"); + let value = tx_result["value"].as_str().unwrap(); + let data = tx_result["input"].as_str().unwrap(); + let gas = tx_result["gas"].as_str().unwrap(); + let gas_price = tx_result["gasPrice"].as_str().unwrap(); + + let impersonate_body = json!({ + "jsonrpc": "2.0", + "method": "anvil_impersonateAccount", + "params": [from], + "id": 1 + }); + let _ = send_blocking_web3_post(&anvil_config, &impersonate_body); + + let balance_body = json!({ + "jsonrpc": "2.0", + "method": "anvil_setBalance", + "params": [from, "0x56bc75e2d63100000"], + "id": 1 + }); + let _ = send_blocking_web3_post(&anvil_config, &balance_body); + + let send_tx_body = json!({ + "jsonrpc": "2.0", + "method": "eth_sendTransaction", + "params": [ + { + "from": from, + "to": if to == "null" { serde_json::Value::Null } else { json!(to) }, + "value": value, + "input": data, + "gas": gas, + "gasPrice": gas_price + } + ], + "id": 1 + }); + + let anvil_tx_result = match send_blocking_web3_post(&anvil_config, &send_tx_body) { + Ok(result) => result, + Err(_) => { + let send_tx_body_wo_gas = json!({ + "jsonrpc": "2.0", + "method": "eth_sendTransaction", + "params": [ + { + "from": from, + "to": if to == "null" { serde_json::Value::Null } else { json!(to) }, + "value": value, + "input": data, + } + ], + "id": 1 + }); + match send_blocking_web3_post(&anvil_config, &send_tx_body_wo_gas) { + Ok(result) => result, + Err(e) => { + stop_anvil_instance(anvil_instance); + return Err(e); + } + } + } + }; + + let ts_body = json!({ + "jsonrpc": "2.0", + "method": "evm_setNextBlockTimestamp", + "params": [get_eth_block_timestamp(config, block_num)?], + "id": 1 + }); + let _ = send_blocking_web3_post(&anvil_config, &ts_body); + + let mine_body = json!({ + "jsonrpc": "2.0", + "method": "evm_mine", + "params": [], + "id": 1 + }); + let _ = send_blocking_web3_post(&anvil_config, &mine_body); + + let anvil_tx_id = anvil_tx_result.as_str().unwrap(); + match stream_debug_trace(&anvil_config, anvil_tx_id, processor) { + Ok((metadata, address)) => Ok((metadata, address, Some(anvil_config), Some(anvil_instance))), + Err(e) => { + stop_anvil_instance(anvil_instance); + Err(e) + } + } +} + // Returns create addresses of internal calls, if the initial call is a create, then it is not included // Reverting creates are indicated with a Zero address fn extract_create_addresses_from_call_frame( @@ -1707,11 +2257,20 @@ impl StorageSnapshot { address: &Address, tx_hashes: &Vec, ) -> Result, ValidationError> { - debug!("Constructing snapshot from TX Ids."); + debug!("Constructing snapshot from TX Ids (streaming)."); let mut snapshot: HashMap = HashMap::new(); for tx_hash in tx_hashes { - let trace_w_a = get_eth_debug_trace(config, tx_hash)?; - Self::add_trace(&mut snapshot, config, address, &trace_w_a)?; + let mut processor = StorageSnapshotProcessor::new( + &mut snapshot, + *address, + config, + tx_hash.clone(), + ); + let (metadata, _) = stream_debug_trace(config, tx_hash, &mut processor)?; + if metadata.failed { + processor.trace_failed(); + } + processor.finalize()?; } Ok(snapshot) } diff --git a/src/dvf.rs b/src/dvf.rs index 2e021a4..da6421f 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -1659,21 +1659,34 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { for address in &call_addresses { println!("- {}", address); } - println!("The transaction created the following contracts:"); - for address in &create_addresses { - println!("- {}", address); + if create_addresses.len() > 0 { + println!("The transaction created the following contracts:"); + for address in &create_addresses { + println!("- {}", address); + } } let registry = registry::Registry::from_config(&config)?; let pretty_printer = PrettyPrinter::new(&config, Some(®istry)); - // Fetch and cache debug trace once; keep anvil alive until end - let (cached_trace, cached_anvil_config, cached_anvil_instance) = - web3::get_eth_debug_trace_sim(&config, tx_hash)?; + // Stream the trace once, collecting mapping usages for ALL addresses + let trace_address = web3::get_receipt_address(&config, tx_hash)?; + let mut multi_processor = + dvf_libs::state::contract_state::MultiAddressMappingProcessor::new( + trace_address, + &config, + tx_hash.clone(), + ); + let (_metadata, _address, _anvil_config, anvil_instance) = + web3::stream_eth_debug_trace_sim(&config, tx_hash, &mut multi_processor)?; + + // Cache the tiny per-address mapping usages (not the raw trace) + let all_mapping_usages = multi_processor.all_mapping_usages; print_progress("Checking called contracts.", &mut pc, &progress_mode); for address in &call_addresses { println!("Checking contract: {}", address); + let usages_for_address = all_mapping_usages.get(address).cloned(); inspect_called_contract( &config, chain_id, @@ -1681,16 +1694,14 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { block_num, tx_hash, &pretty_printer, - &mut pc, - &progress_mode, - Some(vec![cached_trace.clone()]), - cached_anvil_config.as_ref(), + usages_for_address, )?; } print_progress("Checking created contracts.", &mut pc, &progress_mode); for address in &create_addresses { println!("Checking contract: {}", address); + let usages_for_address = all_mapping_usages.get(address).cloned(); inspect_called_contract( &config, chain_id, @@ -1698,15 +1709,16 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { block_num, tx_hash, &pretty_printer, - &mut pc, - &progress_mode, - Some(vec![cached_trace.clone()]), - cached_anvil_config.as_ref(), + usages_for_address, )?; } + + if create_addresses.len() == 0 { + println!("No contracts created during transaction - skipping") + } // After all inspections, stop cached anvil instance if present - if let Some(anvil_instance) = cached_anvil_instance { + if let Some(anvil_instance) = anvil_instance { web3::stop_anvil_instance(anvil_instance); } @@ -1823,10 +1835,7 @@ fn inspect_called_contract( block_num: u64, tx_hash: &String, pretty_printer: &PrettyPrinter, - pc: &mut u64, - progress_mode: &ProgressMode, - cached_traces: Option>, - cached_anvil_config: Option<&DVFConfig>, + cached_mapping_usages: Option, ) -> Result<(), ValidationError> { let project_config = config.get_project_config(address, chain_id); if let Some(project_config) = project_config { @@ -1869,7 +1878,7 @@ fn inspect_called_contract( None => "Compiling local code.", Some(_) => "Loading build cache.", }; - print_progress(compile_output, pc, progress_mode); + print_progress(compile_output, &mut pc_sub, &progress_mode_sub); let mut project_info = ProjectInfo::new( &project_config.contract_name, Path::new(&project_config.project_path), @@ -1950,8 +1959,7 @@ fn inspect_called_contract( progress_mode: &progress_mode_sub, use_storage_range: false, tx_hashes: Some(vec![tx_hash.clone()]), - cached_traces, - cached_anvil_config, + cached_mapping_usages, })?; dumped.critical_storage_variables = critical_storage_variables; From f6758edcf0b0495e1736cbae3e3fbe40843e7ee8 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 27 Feb 2026 14:36:28 +0100 Subject: [PATCH 13/15] changed web3 timeout handling --- lib/web3.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/web3.rs b/lib/web3.rs index 1779bf4..33ea7d7 100644 --- a/lib/web3.rs +++ b/lib/web3.rs @@ -836,12 +836,16 @@ mod streaming_trace { } /// Send a JSON-RPC POST and return the raw response (not deserialized). +/// Uses a fixed 120-second per-read timeout (via `.timeout()`) instead of +/// the user-configured `web3_timeout`, so that arbitrarily large streaming +/// responses can be consumed while still detecting server stalls promptly. fn send_blocking_web3_post_raw( config: &DVFConfig, request_body: &serde_json::Value, ) -> Result { let client = Client::builder() - .timeout(Duration::from_millis(config.web3_timeout)) + .connect_timeout(Duration::from_millis(config.web3_timeout)) + .timeout(Duration::from_secs(120)) .build() .unwrap(); From 23b668d926077310fda1b27976af1e2c9132d9c5 Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 27 Feb 2026 14:42:11 +0100 Subject: [PATCH 14/15] fmt + clippy --- lib/dvf/discovery.rs | 3 +- lib/state/contract_state.rs | 21 ++++++----- lib/web3.rs | 69 +++++++++++++++++++++---------------- src/dvf.rs | 6 ++-- 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/lib/dvf/discovery.rs b/lib/dvf/discovery.rs index ac0bad2..7261b61 100644 --- a/lib/dvf/discovery.rs +++ b/lib/dvf/discovery.rs @@ -15,15 +15,14 @@ use crate::dvf::config::DVFConfig; use crate::dvf::parse::{self, ValidationError}; use crate::dvf::registry; use crate::state::contract_state::ContractState; +use crate::state::contract_state::MappingUsages; use crate::state::forge_inspect; use crate::utils::pretty::PrettyPrinter; use crate::utils::progress::{print_progress, ProgressMode}; use crate::utils::read_write_file::get_project_paths; -use crate::state::contract_state::MappingUsages; use crate::web3; use crate::web3::stop_anvil_instance; - pub struct DiscoveryParams<'a> { pub config: &'a DVFConfig, pub contract_name: &'a str, diff --git a/lib/state/contract_state.rs b/lib/state/contract_state.rs index 97589ba..723d153 100644 --- a/lib/state/contract_state.rs +++ b/lib/state/contract_state.rs @@ -949,10 +949,7 @@ pub type MappingUsages = HashMap>; /// Helper: extract mapping key from memory at a SHA3/KECCAK256 opcode. /// Returns `Some((key_hex, storage_index))` if the input looks like a mapping hash. -fn extract_mapping_key_from_sha3( - stack: &[U256], - memory: &[String], -) -> Option<(String, U256)> { +fn extract_mapping_key_from_sha3(stack: &[U256], memory: &[String]) -> Option<(String, U256)> { let length_in_bytes = stack[stack.len() - 2]; if length_in_bytes < U256::from(32_u64) || length_in_bytes >= U256::from(usize::MAX / 2) { return None; @@ -1010,7 +1007,10 @@ impl<'a> MappingUsageProcessor<'a> { } impl<'a> StructLogProcessor for MappingUsageProcessor<'a> { - fn process_log(&mut self, log: alloy_rpc_types_trace::geth::StructLog) -> Result<(), crate::dvf::parse::ValidationError> { + fn process_log( + &mut self, + log: alloy_rpc_types_trace::geth::StructLog, + ) -> Result<(), crate::dvf::parse::ValidationError> { if log.stack.is_none() { return Ok(()); } @@ -1087,11 +1087,7 @@ pub struct MultiAddressMappingProcessor<'a> { } impl<'a> MultiAddressMappingProcessor<'a> { - pub fn new( - trace_address: Address, - config: &'a DVFConfig, - tx_id: String, - ) -> Self { + pub fn new(trace_address: Address, config: &'a DVFConfig, tx_id: String) -> Self { let mut depth_to_address: HashMap = HashMap::new(); depth_to_address.insert(1, trace_address); MultiAddressMappingProcessor { @@ -1108,7 +1104,10 @@ impl<'a> MultiAddressMappingProcessor<'a> { } impl<'a> StructLogProcessor for MultiAddressMappingProcessor<'a> { - fn process_log(&mut self, log: alloy_rpc_types_trace::geth::StructLog) -> Result<(), crate::dvf::parse::ValidationError> { + fn process_log( + &mut self, + log: alloy_rpc_types_trace::geth::StructLog, + ) -> Result<(), crate::dvf::parse::ValidationError> { if log.stack.is_none() { return Ok(()); } diff --git a/lib/web3.rs b/lib/web3.rs index 33ea7d7..dfbae10 100644 --- a/lib/web3.rs +++ b/lib/web3.rs @@ -193,7 +193,7 @@ impl<'a> StructLogProcessor for StorageSnapshotProcessor<'a> { .insert(log.depth + 1, self.depth_to_address[&log.depth]); } - if &self.depth_to_address[&log.depth] == &self.address && log.op == "SSTORE" { + if self.depth_to_address[&log.depth] == self.address && log.op == "SSTORE" { let last_store = self.last_storage.entry(log.depth).or_default(); let value = stack[stack.len() - 2]; let slot = stack[stack.len() - 1]; @@ -418,10 +418,7 @@ fn create_trace_with_address( /// Get the target address for a transaction from its receipt. /// Extracted from `create_trace_with_address()` for reuse with streaming. -pub fn get_receipt_address( - config: &DVFConfig, - tx_id: &str, -) -> Result { +pub fn get_receipt_address(config: &DVFConfig, tx_id: &str) -> Result { let request_body = json!({ "jsonrpc": "2.0", "method": "eth_getTransactionReceipt", @@ -635,7 +632,6 @@ mod streaming_trace { use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; use std::fmt; - /// Seed that drives streaming: navigates the JSON-RPC envelope and processes structLogs /// one element at a time via the provided `StructLogProcessor`. struct ResultFieldSeed<'a, P> { @@ -675,7 +671,11 @@ mod streaming_trace { u64::from_str_radix(s.trim_start_matches("0x"), 16) .map_err(de::Error::custom)? } - _ => return Err(de::Error::custom("Expected gas as hex string or integer")), + _ => { + return Err(de::Error::custom( + "Expected gas as hex string or integer", + )) + } }; } "returnValue" => { @@ -695,7 +695,9 @@ mod streaming_trace { } } if !got_struct_logs { - return Err(de::Error::custom("missing structLogs field in trace result")); + return Err(de::Error::custom( + "missing structLogs field in trace result", + )); } // If `failed` was encountered after `structLogs`, notify the processor if self.metadata.failed { @@ -734,9 +736,7 @@ mod streaming_trace { fn visit_seq>(self, mut seq: A) -> Result<(), A::Error> { while let Some(log) = seq.next_element::()? { - self.processor - .process_log(log) - .map_err(de::Error::custom)?; + self.processor.process_log(log).map_err(de::Error::custom)?; } Ok(()) } @@ -829,7 +829,9 @@ mod streaming_trace { metadata: &mut metadata, } .deserialize(&mut deserializer) - .map_err(|e| ValidationError::from(format!("Streaming trace deserialization error: {}", e)))?; + .map_err(|e| { + ValidationError::from(format!("Streaming trace deserialization error: {}", e)) + })?; Ok(metadata) } @@ -886,7 +888,15 @@ pub fn stream_eth_debug_trace_sim( config: &DVFConfig, tx_id: &str, processor: &mut P, -) -> Result<(TraceMetadata, Address, Option, Option), ValidationError> { +) -> Result< + ( + TraceMetadata, + Address, + Option, + Option, + ), + ValidationError, +> { debug!("Streaming debug trace (with sim fallback)."); let request_body = json!({ @@ -899,17 +909,18 @@ pub fn stream_eth_debug_trace_sim( // Try direct streaming first match send_blocking_web3_post_raw(config, &request_body) { - Ok(response) => { - match streaming_trace::stream_trace_response(response, processor) { - Ok(metadata) => { - let address = get_receipt_address(config, tx_id)?; - return Ok((metadata, address, None, None)); - } - Err(e) => { - info!("Direct streaming trace failed, trying fallback with anvil: {}", e); - } + Ok(response) => match streaming_trace::stream_trace_response(response, processor) { + Ok(metadata) => { + let address = get_receipt_address(config, tx_id)?; + return Ok((metadata, address, None, None)); } - } + Err(e) => { + info!( + "Direct streaming trace failed, trying fallback with anvil: {}", + e + ); + } + }, Err(_) => { info!("Initial debug_traceTransaction request failed, trying fallback with anvil"); } @@ -1022,7 +1033,9 @@ pub fn stream_eth_debug_trace_sim( let anvil_tx_id = anvil_tx_result.as_str().unwrap(); match stream_debug_trace(&anvil_config, anvil_tx_id, processor) { - Ok((metadata, address)) => Ok((metadata, address, Some(anvil_config), Some(anvil_instance))), + Ok((metadata, address)) => { + Ok((metadata, address, Some(anvil_config), Some(anvil_instance))) + } Err(e) => { stop_anvil_instance(anvil_instance); Err(e) @@ -2264,12 +2277,8 @@ impl StorageSnapshot { debug!("Constructing snapshot from TX Ids (streaming)."); let mut snapshot: HashMap = HashMap::new(); for tx_hash in tx_hashes { - let mut processor = StorageSnapshotProcessor::new( - &mut snapshot, - *address, - config, - tx_hash.clone(), - ); + let mut processor = + StorageSnapshotProcessor::new(&mut snapshot, *address, config, tx_hash.clone()); let (metadata, _) = stream_debug_trace(config, tx_hash, &mut processor)?; if metadata.failed { processor.trace_failed(); diff --git a/src/dvf.rs b/src/dvf.rs index da6421f..0473ba6 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -1659,7 +1659,7 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { for address in &call_addresses { println!("- {}", address); } - if create_addresses.len() > 0 { + if !create_addresses.is_empty() { println!("The transaction created the following contracts:"); for address in &create_addresses { println!("- {}", address); @@ -1712,8 +1712,8 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { usages_for_address, )?; } - - if create_addresses.len() == 0 { + + if create_addresses.is_empty() { println!("No contracts created during transaction - skipping") } From f3b4de59fa19c5c8aa2c38bbeefc399e76cd511d Mon Sep 17 00:00:00 2001 From: Stefan Effenberger Date: Fri, 27 Feb 2026 17:53:19 +0100 Subject: [PATCH 15/15] some fixes --- lib/web3.rs | 69 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/lib/web3.rs b/lib/web3.rs index dfbae10..9d5bd55 100644 --- a/lib/web3.rs +++ b/lib/web3.rs @@ -132,11 +132,12 @@ impl<'a> StorageSnapshotProcessor<'a> { pub fn new( snapshot: &'a mut HashMap, address: Address, + tx_to_address: Address, config: &'a DVFConfig, tx_id: String, ) -> Self { let mut depth_to_address: HashMap = HashMap::new(); - depth_to_address.insert(1, address); + depth_to_address.insert(1, tx_to_address); StorageSnapshotProcessor { snapshot, address, @@ -201,10 +202,21 @@ impl<'a> StructLogProcessor for StorageSnapshotProcessor<'a> { } if log.op == "STOP" || log.op == "RETURN" { - commit_storage_to_snapshot(&self.last_storage, log.depth, self.snapshot); + // Propagate committed storage to parent depth instead of writing + // directly to the snapshot. This ensures that if a parent depth + // later REVERTs, the child's storage changes are correctly rolled + // back (the parent's storage, including propagated values, is + // discarded on depth-decrease cleanup). It also handles reentrancy + // naturally: child values override parent values for the same slots. + if let Some(committed) = self.last_storage.remove(&log.depth) { + let parent = self.last_storage.entry(log.depth - 1).or_default(); + for (slot, value) in committed { + parent.insert(slot, value); + } + } } if log.depth < self.last_depth { - for depth in log.depth..self.last_depth + 1 { + for depth in (log.depth + 1)..=self.last_depth { self.last_storage.remove(&depth); } } @@ -873,7 +885,6 @@ pub fn stream_debug_trace( "params": [tx_id, {"enableMemory": true, "enableStorage": true, "enableReturnData": false}], "id": 1 }); - println!("a {:?}", request_body); let response = send_blocking_web3_post_raw(config, &request_body)?; let metadata = streaming_trace::stream_trace_response(response, processor)?; @@ -905,7 +916,6 @@ pub fn stream_eth_debug_trace_sim( "params": [tx_id, {"enableMemory": true, "enableStorage": true, "enableReturnData": false}], "id": 1 }); - println!("b {:?}", request_body); // Try direct streaming first match send_blocking_web3_post_raw(config, &request_body) { @@ -1599,21 +1609,33 @@ pub fn get_all_txs_for_contract( start_block: u64, end_block: u64, ) -> Result, ValidationError> { - if let Ok(all_txs) = - get_all_txs_for_contract_from_blockscout(config, address, start_block, end_block) - { - return Ok(all_txs); - } else if end_block - start_block <= 100 { + // For manageable ranges, prefer trace-based approaches: they capture ALL + // internal calls to the contract, whereas blockscout may miss some. + // Parity traces: 1 RPC call per block. + if end_block - start_block <= 500 { if let Ok(all_txs) = get_all_txs_for_contract_from_parity_traces(config, address, start_block, end_block) { + debug!("Found {} txs via parity traces", all_txs.len()); return Ok(all_txs); - } else if let Ok(all_txs) = + } + } + // Geth traces: 1 RPC call per transaction (only feasible for small ranges). + if end_block - start_block <= 100 { + if let Ok(all_txs) = get_all_txs_for_contract_from_geth_traces(config, address, start_block, end_block) { + debug!("Found {} txs via geth traces", all_txs.len()); return Ok(all_txs); } } + // Fallback: blockscout API (fast but may miss internal-call-only transactions). + if let Ok(all_txs) = + get_all_txs_for_contract_from_blockscout(config, address, start_block, end_block) + { + debug!("Found {} txs via blockscout", all_txs.len()); + return Ok(all_txs); + } Err(ValidationError::from(format!( "Could not find transactions for {:?} from {} to {}.", address, start_block, end_block @@ -2277,8 +2299,14 @@ impl StorageSnapshot { debug!("Constructing snapshot from TX Ids (streaming)."); let mut snapshot: HashMap = HashMap::new(); for tx_hash in tx_hashes { - let mut processor = - StorageSnapshotProcessor::new(&mut snapshot, *address, config, tx_hash.clone()); + let tx_to_address = get_receipt_address(config, tx_hash)?; + let mut processor = StorageSnapshotProcessor::new( + &mut snapshot, + *address, + tx_to_address, + config, + tx_hash.clone(), + ); let (metadata, _) = stream_debug_trace(config, tx_hash, &mut processor)?; if metadata.failed { processor.trace_failed(); @@ -2365,16 +2393,19 @@ impl StorageSnapshot { // ); } - // Save upon successful return + // Propagate committed storage to parent depth on successful return if log.op == "STOP" || log.op == "RETURN" { - commit_storage_to_snapshot(&last_storage, log.depth, snapshot); + if let Some(committed) = last_storage.remove(&log.depth) { + let parent = last_storage.entry(log.depth - 1).or_default(); + for (slot, value) in committed { + parent.insert(slot, value); + } + } } // Clean failed storages if log.depth < last_depth { - for depth in log.depth..last_depth + 1 { - if last_storage.contains_key(&depth) { - last_storage.remove(&depth); - } + for depth in (log.depth + 1)..=last_depth { + last_storage.remove(&depth); } } }