diff --git a/Cargo.lock b/Cargo.lock index 11feda934..044cf6099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2015,6 +2015,7 @@ dependencies = [ "plonky2", "plonky2_maybe_rayon 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "plonky2_util", + "primitive-types 0.12.2", "rand", "rand_chacha", "ripemd", @@ -4111,6 +4112,8 @@ name = "rpc" version = "0.1.0" dependencies = [ "alloy", + "alloy-primitives", + "alloy-serde", "anyhow", "cargo_metadata", "clap", diff --git a/Cargo.toml b/Cargo.toml index 008257556..858c5604f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,6 @@ criterion = "0.5.1" dotenvy = "0.15.7" either = "1.12.0" enum-as-inner = "0.6.0" -enumn = "0.1.13" env_logger = "0.11.3" eth_trie = "0.4.0" ethereum-types = "0.14.1" @@ -94,7 +93,6 @@ serde = "1.0.203" serde-big-array = "0.5.1" serde_json = "1.0.118" serde_path_to_error = "0.1.16" -serde_with = "3.8.1" sha2 = "0.10.8" static_assertions = "1.1.0" thiserror = "1.0.61" diff --git a/evm_arithmetization/Cargo.toml b/evm_arithmetization/Cargo.toml index c18bea130..2b448b7d9 100644 --- a/evm_arithmetization/Cargo.toml +++ b/evm_arithmetization/Cargo.toml @@ -15,6 +15,7 @@ homepage.workspace = true keywords.workspace = true [dependencies] +__compat_primitive_types = { workspace = true } anyhow = { workspace = true } bytes = { workspace = true } env_logger = { workspace = true } diff --git a/evm_arithmetization/benches/fibonacci_25m_gas.rs b/evm_arithmetization/benches/fibonacci_25m_gas.rs index 21702d2d1..f450276fd 100644 --- a/evm_arithmetization/benches/fibonacci_25m_gas.rs +++ b/evm_arithmetization/benches/fibonacci_25m_gas.rs @@ -195,6 +195,7 @@ fn prepare_setup() -> anyhow::Result { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }) } diff --git a/evm_arithmetization/src/cpu/kernel/interpreter.rs b/evm_arithmetization/src/cpu/kernel/interpreter.rs index f6e9b67ed..8b2e84b2c 100644 --- a/evm_arithmetization/src/cpu/kernel/interpreter.rs +++ b/evm_arithmetization/src/cpu/kernel/interpreter.rs @@ -19,7 +19,9 @@ use crate::cpu::columns::CpuColumnsView; use crate::cpu::kernel::aggregator::KERNEL; use crate::cpu::kernel::constants::global_metadata::GlobalMetadata; use crate::generation::debug_inputs; +use crate::generation::jumpdest::{ContextJumpDests, JumpDestTableProcessed, JumpDestTableWitness}; use crate::generation::mpt::{load_linked_lists_and_txn_and_receipt_mpts, TrieRootPtrs}; +use crate::generation::prover_input::{get_proofs_and_jumpdests, CodeMap}; use crate::generation::rlp::all_rlp_prover_inputs_reversed; use crate::generation::state::{ all_ger_prover_inputs_reversed, all_withdrawals_prover_inputs_reversed, GenerationState, @@ -56,6 +58,7 @@ pub(crate) struct Interpreter { /// Counts the number of appearances of each opcode. For debugging purposes. #[allow(unused)] pub(crate) opcode_count: [usize; 0x100], + /// A table of contexts and their reached JUMPDESTs. jumpdest_table: HashMap>, /// `true` if the we are currently carrying out a jumpdest analysis. pub(crate) is_jumpdest_analysis: bool, @@ -71,9 +74,10 @@ pub(crate) struct Interpreter { pub(crate) fn simulate_cpu_and_get_user_jumps( final_label: &str, state: &GenerationState, -) -> Option>> { + // TODO(einar): remove second component of pair. +) -> (Option, ContextJumpDests) { match state.jumpdest_table { - Some(_) => None, + Some(_) => Default::default(), None => { let halt_pc = KERNEL.global_labels[final_label]; let initial_context = state.registers.context; @@ -94,14 +98,16 @@ pub(crate) fn simulate_cpu_and_get_user_jumps( interpreter .generation_state - .set_jumpdest_analysis_inputs(interpreter.jumpdest_table); + .set_jumpdest_analysis_inputs(interpreter.jumpdest_table.clone()); log::debug!( "Simulated CPU for jumpdest analysis halted after {:?} cycles.", clock ); - - interpreter.generation_state.jumpdest_table + ( + interpreter.generation_state.jumpdest_table, + ContextJumpDests(interpreter.jumpdest_table), + ) } } } @@ -114,7 +120,7 @@ pub(crate) struct ExtraSegmentData { pub(crate) withdrawal_prover_inputs: Vec, pub(crate) ger_prover_inputs: Vec, pub(crate) trie_root_ptrs: TrieRootPtrs, - pub(crate) jumpdest_table: Option>>, + pub(crate) jumpdest_table: Option, pub(crate) next_txn_index: usize, } @@ -148,6 +154,49 @@ pub(crate) fn set_registers_and_run( interpreter.run() } +/// Computes the JUMPDEST proofs for each context. +/// +/// # Arguments +/// +/// - `jumpdest_table_rpc`: The raw table received from RPC. +/// - `code_db`: The corresponding database of contract code used in the trace. +pub(crate) fn set_jumpdest_analysis_inputs_rpc( + jumpdest_table_rpc: &JumpDestTableWitness, + code_map: &CodeMap, +) -> JumpDestTableProcessed { + let ctx_proofs = jumpdest_table_rpc + .0 + .iter() + .flat_map(|(code_addr, ctx_jumpdests)| { + prove_context_jumpdests(&code_map.0[code_addr], ctx_jumpdests) + }) + .collect(); + JumpDestTableProcessed(ctx_proofs) +} + +/// Orchestrates the proving of all contexts in a specific bytecode. +/// +/// # Arguments +/// +/// - `ctx_jumpdests`: Map from `ctx` to its list of offsets to reached +/// `JUMPDEST`s. +/// - `code`: The bytecode for the contexts. This is the same for all contexts. +fn prove_context_jumpdests( + code: &[u8], + ctx_jumpdests: &ContextJumpDests, +) -> HashMap> { + ctx_jumpdests + .0 + .iter() + .map(|(&ctx, jumpdests)| { + let proofs = jumpdests.last().map_or(Vec::default(), |&largest_address| { + get_proofs_and_jumpdests(code, largest_address, jumpdests.clone()) + }); + (ctx, proofs) + }) + .collect() +} + impl Interpreter { /// Returns an instance of `Interpreter` given `GenerationInputs`, and /// assuming we are initializing with the `KERNEL` code. diff --git a/evm_arithmetization/src/cpu/kernel/tests/add11.rs b/evm_arithmetization/src/cpu/kernel/tests/add11.rs index 1840bbc07..3646aa1aa 100644 --- a/evm_arithmetization/src/cpu/kernel/tests/add11.rs +++ b/evm_arithmetization/src/cpu/kernel/tests/add11.rs @@ -196,6 +196,7 @@ fn test_add11_yml() { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let initial_stack = vec![]; @@ -378,6 +379,7 @@ fn test_add11_yml_with_exception() { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let initial_stack = vec![]; diff --git a/evm_arithmetization/src/cpu/kernel/tests/core/jumpdest_analysis.rs b/evm_arithmetization/src/cpu/kernel/tests/core/jumpdest_analysis.rs index b0ef17033..be886893a 100644 --- a/evm_arithmetization/src/cpu/kernel/tests/core/jumpdest_analysis.rs +++ b/evm_arithmetization/src/cpu/kernel/tests/core/jumpdest_analysis.rs @@ -7,6 +7,7 @@ use plonky2::field::goldilocks_field::GoldilocksField as F; use crate::cpu::kernel::aggregator::KERNEL; use crate::cpu::kernel::interpreter::Interpreter; use crate::cpu::kernel::opcodes::{get_opcode, get_push_opcode}; +use crate::generation::jumpdest::JumpDestTableProcessed; use crate::witness::operation::CONTEXT_SCALING_FACTOR; #[test] @@ -67,7 +68,10 @@ fn test_jumpdest_analysis() -> Result<()> { interpreter.generation_state.jumpdest_table, // Context 3 has jumpdest 1, 5, 7. All have proof 0 and hence // the list [proof_0, jumpdest_0, ... ] is [0, 1, 0, 5, 0, 7, 8, 40] - Some(HashMap::from([(3, vec![0, 1, 0, 5, 0, 7, 8, 40])])) + Some(JumpDestTableProcessed(HashMap::from([( + 3, + vec![0, 1, 0, 5, 0, 7, 8, 40] + )]))) ); // Run jumpdest analysis with context = 3 @@ -89,6 +93,7 @@ fn test_jumpdest_analysis() -> Result<()> { .jumpdest_table .as_mut() .unwrap() + .0 .get_mut(&CONTEXT) .unwrap() .pop(); @@ -136,7 +141,8 @@ fn test_packed_verification() -> Result<()> { let mut interpreter: Interpreter = Interpreter::new(write_table_if_jumpdest, initial_stack.clone(), None); interpreter.set_code(CONTEXT, code.clone()); - interpreter.generation_state.jumpdest_table = Some(HashMap::from([(3, vec![1, 33])])); + interpreter.generation_state.jumpdest_table = + Some(JumpDestTableProcessed(HashMap::from([(3, vec![1, 33])]))); interpreter.run()?; @@ -149,7 +155,8 @@ fn test_packed_verification() -> Result<()> { let mut interpreter: Interpreter = Interpreter::new(write_table_if_jumpdest, initial_stack.clone(), None); interpreter.set_code(CONTEXT, code.clone()); - interpreter.generation_state.jumpdest_table = Some(HashMap::from([(3, vec![1, 33])])); + interpreter.generation_state.jumpdest_table = + Some(JumpDestTableProcessed(HashMap::from([(3, vec![1, 33])]))); assert!(interpreter.run().is_err()); diff --git a/evm_arithmetization/src/cpu/kernel/tests/init_exc_stop.rs b/evm_arithmetization/src/cpu/kernel/tests/init_exc_stop.rs index 7e7e6851d..624b44a13 100644 --- a/evm_arithmetization/src/cpu/kernel/tests/init_exc_stop.rs +++ b/evm_arithmetization/src/cpu/kernel/tests/init_exc_stop.rs @@ -111,6 +111,7 @@ fn test_init_exc_stop() { cur_hash: H256::default(), }, global_exit_roots: vec![], + batch_jumpdest_table: None, }; let initial_stack = vec![]; let initial_offset = KERNEL.global_labels["init"]; diff --git a/evm_arithmetization/src/generation/jumpdest.rs b/evm_arithmetization/src/generation/jumpdest.rs new file mode 100644 index 000000000..d4e7d3159 --- /dev/null +++ b/evm_arithmetization/src/generation/jumpdest.rs @@ -0,0 +1,67 @@ +use std::{ + collections::{BTreeSet, HashMap}, + fmt::Display, +}; + +use keccak_hash::H256; +use serde::{Deserialize, Serialize}; + +/// Each `CodeAddress` can be called one or more times, each time creating a new +/// `Context`. Each `Context` will contain one or more offsets of `JUMPDEST`. +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)] +pub struct ContextJumpDests(pub HashMap>); + +/// The result after proving a `JumpDestTableWitness`. +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)] +pub(crate) struct JumpDestTableProcessed(pub HashMap>); + +/// Map `CodeAddress -> (Context -> [JumpDests])` +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, Default)] +pub struct JumpDestTableWitness(pub HashMap); + +impl JumpDestTableWitness { + /// Insert `offset` into `ctx` under the corrresponding `code_hash`. + /// Creates the required `ctx` keys and `code_hash`. Idempotent. + pub fn insert(&mut self, code_hash: &H256, ctx: usize, offset: usize) { + self.0.entry(*code_hash).or_default(); + + self.0.get_mut(code_hash).unwrap().0.entry(ctx).or_default(); + + self.0 + .get_mut(code_hash) + .unwrap() + .0 + .get_mut(&ctx) + .unwrap() + .insert(offset); + + // TODO(einar) remove before publishing PR. + assert!(self.0.contains_key(code_hash)); + assert!(self.0[code_hash].0.contains_key(&ctx)); + assert!(self.0[code_hash].0[&ctx].contains(&offset)); + } +} + +impl Display for JumpDestTableWitness { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "=== JumpDest table ===")?; + + for (code, ctxtbls) in &self.0 { + write!(f, "codehash: {:?}\n{}", code, ctxtbls)?; + } + Ok(()) + } +} + +impl Display for ContextJumpDests { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (ctx, offsets) in &self.0 { + write!(f, " ctx: {}, offsets: [", ctx)?; + for offset in offsets { + write!(f, "{:#10x} ", offset)?; + } + writeln!(f, "]")?; + } + Ok(()) + } +} diff --git a/evm_arithmetization/src/generation/mod.rs b/evm_arithmetization/src/generation/mod.rs index d1bbfbad5..0c19760e8 100644 --- a/evm_arithmetization/src/generation/mod.rs +++ b/evm_arithmetization/src/generation/mod.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use anyhow::anyhow; use ethereum_types::H160; use ethereum_types::{Address, BigEndianHash, H256, U256}; +use jumpdest::JumpDestTableWitness; use keccak_hash::keccak; use log::log_enabled; use mpt_trie::partial_trie::{HashedPartialTrie, PartialTrie}; @@ -34,6 +35,7 @@ use crate::util::{h2u, u256_to_usize}; use crate::witness::memory::{MemoryAddress, MemoryChannel, MemoryState}; use crate::witness::state::RegistersState; +pub mod jumpdest; pub(crate) mod linked_list; pub mod mpt; pub(crate) mod prover_input; @@ -99,6 +101,10 @@ pub struct GenerationInputs { /// The hash of the current block, and a list of the 256 previous block /// hashes. pub block_hashes: BlockHashes, + + /// A table listing each JUMPDESTs reached in each call context under + /// associated code hash. + pub batch_jumpdest_table: Option, } /// A lighter version of [`GenerationInputs`], which have been trimmed @@ -145,6 +151,10 @@ pub struct TrimmedGenerationInputs { /// The hash of the current block, and a list of the 256 previous block /// hashes. pub block_hashes: BlockHashes, + + /// A list of tables listing each JUMPDESTs reached in each call context + /// under associated code hash. + pub batch_jumpdest_table: Option, } #[derive(Clone, Debug, Deserialize, Serialize, Default)] @@ -218,6 +228,7 @@ impl GenerationInputs { burn_addr: self.burn_addr, block_metadata: self.block_metadata.clone(), block_hashes: self.block_hashes.clone(), + batch_jumpdest_table: self.batch_jumpdest_table.clone(), } } } diff --git a/evm_arithmetization/src/generation/prover_input.rs b/evm_arithmetization/src/generation/prover_input.rs index 601e1c525..e0fe1785e 100644 --- a/evm_arithmetization/src/generation/prover_input.rs +++ b/evm_arithmetization/src/generation/prover_input.rs @@ -10,6 +10,7 @@ use num_bigint::BigUint; use plonky2::field::types::Field; use serde::{Deserialize, Serialize}; +use super::jumpdest::JumpDestTableProcessed; use super::linked_list::LinkedList; use super::mpt::load_state_mpt; use crate::cpu::kernel::cancun_constants::KZG_VERSIONED_HASH; @@ -18,7 +19,9 @@ use crate::cpu::kernel::constants::cancun_constants::{ POINT_EVALUATION_PRECOMPILE_RETURN_VALUE, }; use crate::cpu::kernel::constants::context_metadata::ContextMetadata; -use crate::cpu::kernel::interpreter::simulate_cpu_and_get_user_jumps; +use crate::cpu::kernel::interpreter::{ + set_jumpdest_analysis_inputs_rpc, simulate_cpu_and_get_user_jumps, +}; use crate::curve_pairings::{bls381, CurveAff, CyclicGroup}; use crate::extension_tower::{FieldExt, Fp12, Fp2, BLS381, BLS_BASE, BLS_SCALAR, BN254, BN_BASE}; use crate::generation::prover_input::EvmField::{ @@ -35,6 +38,20 @@ use crate::witness::memory::MemoryAddress; use crate::witness::operation::CONTEXT_SCALING_FACTOR; use crate::witness::util::{current_context_peek, stack_peek}; +pub type CodeDb = BTreeSet>; +pub(crate) struct CodeMap(pub HashMap<__compat_primitive_types::H256, Vec>); + +impl From for CodeMap { + fn from(value: CodeDb) -> Self { + Self( + value + .into_iter() + .map(move |code| (keccak_hash::keccak(code.clone()), code)) + .collect(), + ) + } +} + /// Prover input function represented as a scoped function name. /// Example: `PROVER_INPUT(ff::bn254_base::inverse)` is represented as /// `ProverInputFn([ff, bn254_base, inverse])`. @@ -392,12 +409,12 @@ impl GenerationState { )); }; - if let Some(ctx_jumpdest_table) = jumpdest_table.get_mut(&context) + if let Some(ctx_jumpdest_table) = jumpdest_table.0.get_mut(&context) && let Some(next_jumpdest_address) = ctx_jumpdest_table.pop() { Ok((next_jumpdest_address + 1).into()) } else { - jumpdest_table.remove(&context); + jumpdest_table.0.remove(&context); Ok(U256::zero()) } } @@ -411,7 +428,7 @@ impl GenerationState { )); }; - if let Some(ctx_jumpdest_table) = jumpdest_table.get_mut(&context) + if let Some(ctx_jumpdest_table) = jumpdest_table.0.get_mut(&context) && let Some(next_jumpdest_proof) = ctx_jumpdest_table.pop() { Ok(next_jumpdest_proof.into()) @@ -797,7 +814,19 @@ impl GenerationState { fn generate_jumpdest_table(&mut self) -> Result<(), ProgramError> { // Simulate the user's code and (unnecessarily) part of the kernel code, // skipping the validate table call - self.jumpdest_table = simulate_cpu_and_get_user_jumps("terminate_common", self); + + // log::info!("{:?} Generating JUMPDEST tables", tx_hash); + self.jumpdest_table = if let Some(jumpdest_table_rpc) = &self.inputs.batch_jumpdest_table { + let jumpdest_table_processed = set_jumpdest_analysis_inputs_rpc( + jumpdest_table_rpc, + &CodeMap(self.inputs.contract_code.clone()), + ); + Some(jumpdest_table_processed) + } else { + let (jumpdest_table_processed, _) = + simulate_cpu_and_get_user_jumps("terminate_common", self); + jumpdest_table_processed + }; Ok(()) } @@ -809,8 +838,8 @@ impl GenerationState { &mut self, jumpdest_table: HashMap>, ) { - self.jumpdest_table = Some(HashMap::from_iter(jumpdest_table.into_iter().map( - |(ctx, jumpdest_table)| { + self.jumpdest_table = Some(JumpDestTableProcessed(HashMap::from_iter( + jumpdest_table.into_iter().map(|(ctx, jumpdest_table)| { let code = self.get_code(ctx).unwrap(); if let Some(&largest_address) = jumpdest_table.last() { let proofs = get_proofs_and_jumpdests(&code, largest_address, jumpdest_table); @@ -818,7 +847,7 @@ impl GenerationState { } else { (ctx, vec![]) } - }, + }), ))); } @@ -890,7 +919,7 @@ impl GenerationState { /// for which none of the previous 32 bytes in the code (including opcodes /// and pushed bytes) is a PUSHXX and the address is in its range. It returns /// a vector of even size containing proofs followed by their addresses. -fn get_proofs_and_jumpdests( +pub(crate) fn get_proofs_and_jumpdests( code: &[u8], largest_address: usize, jumpdest_table: std::collections::BTreeSet, diff --git a/evm_arithmetization/src/generation/state.rs b/evm_arithmetization/src/generation/state.rs index 96865806a..063bdc251 100644 --- a/evm_arithmetization/src/generation/state.rs +++ b/evm_arithmetization/src/generation/state.rs @@ -8,6 +8,7 @@ use keccak_hash::keccak; use log::Level; use plonky2::field::types::Field; +use super::jumpdest::JumpDestTableProcessed; use super::mpt::TrieRootPtrs; use super::segments::GenerationSegmentData; use super::{TrieInputs, TrimmedGenerationInputs, NUM_EXTRA_CYCLES_AFTER}; @@ -365,7 +366,7 @@ pub struct GenerationState { /// "proof" for a jump destination is either 0 or an address i > 32 in /// the code (not necessarily pointing to an opcode) such that for every /// j in [i, i+32] it holds that code[j] < 0x7f - j + i. - pub(crate) jumpdest_table: Option>>, + pub(crate) jumpdest_table: Option, } impl GenerationState { diff --git a/evm_arithmetization/src/lib.rs b/evm_arithmetization/src/lib.rs index b76953311..3004c567e 100644 --- a/evm_arithmetization/src/lib.rs +++ b/evm_arithmetization/src/lib.rs @@ -206,6 +206,9 @@ pub mod verifier; pub mod generation; pub mod witness; +pub use generation::jumpdest; +pub use generation::prover_input::CodeDb; + // Utility modules pub mod curve_pairings; pub mod extension_tower; diff --git a/evm_arithmetization/tests/add11_yml.rs b/evm_arithmetization/tests/add11_yml.rs index ed5eaaa94..be15b50bd 100644 --- a/evm_arithmetization/tests/add11_yml.rs +++ b/evm_arithmetization/tests/add11_yml.rs @@ -202,6 +202,7 @@ fn get_generation_inputs() -> GenerationInputs { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, } } /// The `add11_yml` test case from https://github.com/ethereum/tests diff --git a/evm_arithmetization/tests/erc20.rs b/evm_arithmetization/tests/erc20.rs index b9f3d6cf0..aed94d663 100644 --- a/evm_arithmetization/tests/erc20.rs +++ b/evm_arithmetization/tests/erc20.rs @@ -196,6 +196,7 @@ fn test_erc20() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/evm_arithmetization/tests/erc721.rs b/evm_arithmetization/tests/erc721.rs index df3099e1f..47c8fd674 100644 --- a/evm_arithmetization/tests/erc721.rs +++ b/evm_arithmetization/tests/erc721.rs @@ -199,6 +199,7 @@ fn test_erc721() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/evm_arithmetization/tests/global_exit_root.rs b/evm_arithmetization/tests/global_exit_root.rs index 69e45bef4..709e9e8de 100644 --- a/evm_arithmetization/tests/global_exit_root.rs +++ b/evm_arithmetization/tests/global_exit_root.rs @@ -98,6 +98,7 @@ fn test_global_exit_root() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/evm_arithmetization/tests/log_opcode.rs b/evm_arithmetization/tests/log_opcode.rs index 8d71e0a19..200e186c7 100644 --- a/evm_arithmetization/tests/log_opcode.rs +++ b/evm_arithmetization/tests/log_opcode.rs @@ -267,6 +267,7 @@ fn test_log_opcodes() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/evm_arithmetization/tests/selfdestruct.rs b/evm_arithmetization/tests/selfdestruct.rs index a4b6aa4f9..b3e9670e3 100644 --- a/evm_arithmetization/tests/selfdestruct.rs +++ b/evm_arithmetization/tests/selfdestruct.rs @@ -170,6 +170,7 @@ fn test_selfdestruct() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/evm_arithmetization/tests/simple_transfer.rs b/evm_arithmetization/tests/simple_transfer.rs index d497142a4..2e6f45823 100644 --- a/evm_arithmetization/tests/simple_transfer.rs +++ b/evm_arithmetization/tests/simple_transfer.rs @@ -162,6 +162,7 @@ fn test_simple_transfer() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/evm_arithmetization/tests/withdrawals.rs b/evm_arithmetization/tests/withdrawals.rs index a8b4c90b9..62431de19 100644 --- a/evm_arithmetization/tests/withdrawals.rs +++ b/evm_arithmetization/tests/withdrawals.rs @@ -106,6 +106,7 @@ fn test_withdrawals() -> anyhow::Result<()> { prev_hashes: vec![H256::default(); 256], cur_hash: H256::default(), }, + batch_jumpdest_table: None, }; let max_cpu_len_log = 20; diff --git a/trace_decoder/src/decoding.rs b/trace_decoder/src/decoding.rs index 758951b45..76931e9d6 100644 --- a/trace_decoder/src/decoding.rs +++ b/trace_decoder/src/decoding.rs @@ -1,4 +1,8 @@ -use std::{cmp::min, collections::HashMap, ops::Range}; +use std::{ + cmp::{max, min}, + collections::HashMap, + ops::Range, +}; use anyhow::{anyhow, Context as _}; use ethereum_types::H160; @@ -8,6 +12,7 @@ use evm_arithmetization::{ mpt::{decode_receipt, AccountRlp}, GenerationInputs, TrieInputs, }, + jumpdest::{ContextJumpDests, JumpDestTableWitness}, proof::{BlockMetadata, ExtraBlockData, TrieRoots}, testing_utils::{ BEACON_ROOTS_CONTRACT_ADDRESS, BEACON_ROOTS_CONTRACT_ADDRESS_HASHED, HISTORY_BUFFER_LENGTH, @@ -82,6 +87,7 @@ pub fn into_txn_proof_gen_ir( .into_iter() .enumerate() .map(|(txn_idx, txn_info)| { + // batch start and end let txn_range = min(txn_idx * batch_size, num_txs)..min(txn_idx * batch_size + batch_size, num_txs); let is_initial_payload = txn_range.start == 0; @@ -583,6 +589,10 @@ fn process_txn_info( true => Some(H160::zero()), false => None, }; + + let jdts = txn_info.meta.iter().map(|tx| &tx.jumpdest_table); + let batch_jumpdest_table: Option = merge_batch_jumpdest_tables(jdts); + let gen_inputs = GenerationInputs { txn_number_before: extra_data.txn_number_before, burn_addr, @@ -611,6 +621,7 @@ fn process_txn_info( block_metadata: other_data.b_data.b_meta.clone(), block_hashes: other_data.b_data.b_hashes.clone(), global_exit_roots: vec![], + batch_jumpdest_table, }; // After processing a transaction, we update the remaining accumulators @@ -621,6 +632,48 @@ fn process_txn_info( Ok(gen_inputs) } +fn merge_batch_jumpdest_tables<'t, T>(jdts: T) -> Option +where + T: Iterator>, +{ + let mut merged_table = JumpDestTableWitness::default(); + + let mut max_batch_ctx = 0; + for jdt in jdts { + let tx_offset = max_batch_ctx; + // Abort if any transaction in the batch came without RPC JumpDestTable. + // TODO: Opportunity for optimization: Simulate to generate only missing + // JUMPDEST tables. + if jdt.is_none() { + return None; + } + for (code_hash, ctx_tbl) in jdt.as_ref().unwrap().0.iter() { + for (ctx, jumpsdests) in ctx_tbl.0.iter() { + let batch_ctx = tx_offset + ctx; + max_batch_ctx = max(max_batch_ctx, batch_ctx); + + merged_table + .0 + .entry(*code_hash) + .or_insert(ContextJumpDests::default()); + + merged_table + .0 + .get_mut(code_hash) + .unwrap() + .0 + .entry(batch_ctx) + .or_insert(jumpsdests.clone()); + + debug_assert!(merged_table.0.contains_key(code_hash)); + debug_assert!(merged_table.0[code_hash].0.contains_key(&batch_ctx)); + } + } + } + + Some(merged_table) +} + impl StateWrite { fn apply_writes_to_state_node( &self, diff --git a/trace_decoder/src/lib.rs b/trace_decoder/src/lib.rs index 2202d58c5..945088bc4 100644 --- a/trace_decoder/src/lib.rs +++ b/trace_decoder/src/lib.rs @@ -99,6 +99,7 @@ mod wire; use std::collections::{BTreeMap, BTreeSet, HashMap}; use ethereum_types::{Address, U256}; +use evm_arithmetization::jumpdest::JumpDestTableWitness; use evm_arithmetization::proof::{BlockHashes, BlockMetadata}; use evm_arithmetization::GenerationInputs; use keccak_hash::keccak as hash; @@ -208,6 +209,9 @@ pub struct TxnMeta { /// Gas used by this txn (Note: not cumulative gas used). pub gas_used: u64, + + /// JumpDest table + pub jumpdest_table: Option, } /// A "trace" specific to an account for a txn. @@ -261,6 +265,17 @@ pub enum ContractCodeUsage { Write(#[serde(with = "crate::hex")] Vec), } +// TODO: Whyt has this has been removed upstream. +impl ContractCodeUsage { + /// Get code hash from a read or write operation of contract code. + pub fn get_code_hash(&self) -> H256 { + match self { + ContractCodeUsage::Read(hash) => *hash, + ContractCodeUsage::Write(bytes) => hash(bytes), + } + } +} + /// Other data that is needed for proof gen. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct OtherBlockData { diff --git a/trace_decoder/src/processed_block_trace.rs b/trace_decoder/src/processed_block_trace.rs index f2e4fcb5e..7b0b597f8 100644 --- a/trace_decoder/src/processed_block_trace.rs +++ b/trace_decoder/src/processed_block_trace.rs @@ -3,6 +3,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use anyhow::{bail, Context as _}; use ethereum_types::{Address, H256, U256}; use evm_arithmetization::generation::mpt::{AccountRlp, LegacyReceiptRlp}; +use evm_arithmetization::jumpdest::JumpDestTableWitness; use itertools::Itertools; use zk_evm_common::EMPTY_TRIE_HASH; @@ -243,6 +244,7 @@ impl TxnInfo { )?, gas_used: txn.meta.gas_used, created_accounts, + jumpdest_table: txn.meta.jumpdest_table.clone(), }); } @@ -295,4 +297,5 @@ pub(crate) struct TxnMetaState { pub receipt_node_bytes: Vec, pub gas_used: u64, pub created_accounts: BTreeSet
, + pub jumpdest_table: Option, } diff --git a/zero_bin/rpc/Cargo.toml b/zero_bin/rpc/Cargo.toml index 046461569..895bd3527 100644 --- a/zero_bin/rpc/Cargo.toml +++ b/zero_bin/rpc/Cargo.toml @@ -32,6 +32,8 @@ itertools = { workspace = true } compat = { workspace = true } zero_bin_common = { workspace = true } prover = { workspace = true } +alloy-serde = "0.3.0" +alloy-primitives = "0.8.0" [build-dependencies] cargo_metadata = { workspace = true } diff --git a/zero_bin/rpc/src/jerigon.rs b/zero_bin/rpc/src/jerigon.rs index f23b804c3..92777e3e1 100644 --- a/zero_bin/rpc/src/jerigon.rs +++ b/zero_bin/rpc/src/jerigon.rs @@ -2,13 +2,15 @@ use alloy::{ primitives::B256, providers::Provider, rpc::types::eth::BlockId, transports::Transport, }; use anyhow::Context as _; +use futures::try_join; use prover::BlockProverInput; use serde::Deserialize; use serde_json::json; -use trace_decoder::{BlockTrace, BlockTraceTriePreImages, CombinedPreImages, TxnInfo}; +use trace_decoder::{BlockTrace, BlockTraceTriePreImages, CombinedPreImages, TxnInfo, TxnMeta}; use zero_bin_common::provider::CachedProvider; use super::fetch_other_block_data; +use crate::native::{self}; /// Transaction traces retrieved from Erigon zeroTracer. #[derive(Debug, Deserialize)] @@ -35,16 +37,57 @@ where "debug_traceBlockByNumber".into(), (target_block_id, json!({"tracer": "zeroTracer"})), ) - .await?; + .await? + .into_iter() + .map(|ztr| ztr.result) + .collect::>(); // Grab block witness info (packed as combined trie pre-images) - let block_witness = cached_provider .get_provider() .await? .raw_request::<_, String>("eth_getWitness".into(), vec![target_block_id]) .await?; + // This is wasteful + let (native_block_trace, _native_other_data) = try_join!( + native::process_block_trace(cached_provider.clone(), target_block_id), + crate::fetch_other_block_data( + cached_provider.clone(), + target_block_id, + checkpoint_state_trie_root, + ) + )?; + + // weave in the JDTs + let txn_info = tx_results + .into_iter() + .zip(native_block_trace.txn_info) + .map( + |( + TxnInfo { + traces, + meta: + TxnMeta { + byte_code, + new_receipt_trie_node_byte, + gas_used, + jumpdest_table: _, + }, + }, + ntx, + )| TxnInfo { + traces, + meta: TxnMeta { + byte_code, + new_receipt_trie_node_byte, + gas_used, + jumpdest_table: ntx.meta.jumpdest_table, + }, + }, + ) + .collect(); + let other_data = fetch_other_block_data(cached_provider, target_block_id, checkpoint_state_trie_root) .await?; @@ -56,7 +99,7 @@ where compact: hex::decode(block_witness.strip_prefix("0x").unwrap_or(&block_witness)) .context("invalid hex returned from call to eth_getWitness")?, }), - txn_info: tx_results.into_iter().map(|it| it.result).collect(), + txn_info, code_db: Default::default(), }, other_data, diff --git a/zero_bin/rpc/src/main.rs b/zero_bin/rpc/src/main.rs index 7ac9db60c..4471730ca 100644 --- a/zero_bin/rpc/src/main.rs +++ b/zero_bin/rpc/src/main.rs @@ -219,6 +219,7 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::Registry::default() .with( tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) .with_ansi(false) .compact() .with_filter(EnvFilter::from_default_env()), diff --git a/zero_bin/rpc/src/native/mod.rs b/zero_bin/rpc/src/native/mod.rs index 728a4f209..554f2a850 100644 --- a/zero_bin/rpc/src/native/mod.rs +++ b/zero_bin/rpc/src/native/mod.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::ops::Deref; use std::sync::Arc; @@ -14,10 +13,9 @@ use trace_decoder::BlockTrace; use zero_bin_common::provider::CachedProvider; mod state; +mod structlogprime; mod txn; -type CodeDb = BTreeSet>; - /// Fetches the prover input for the given BlockId. pub async fn block_prover_input( provider: Arc>, @@ -40,7 +38,7 @@ where } /// Processes the block with the given block number and returns the block trace. -async fn process_block_trace( +pub(crate) async fn process_block_trace( cached_provider: Arc>, block_number: BlockId, ) -> anyhow::Result diff --git a/zero_bin/rpc/src/native/structlogprime.rs b/zero_bin/rpc/src/native/structlogprime.rs new file mode 100644 index 000000000..540d5ec53 --- /dev/null +++ b/zero_bin/rpc/src/native/structlogprime.rs @@ -0,0 +1,112 @@ +//! Geth tracing types. + +use std::collections::BTreeMap; + +use alloy::rpc::types::trace::geth::DefaultFrame; +use alloy_primitives::{Bytes, B256, U256}; +use serde::{ser::SerializeMap as _, Deserialize, Serialize, Serializer}; +use serde_json::Value; + +/// Geth Default struct log trace frame +/// +/// +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DefaultFramePrime { + /// Whether the transaction failed + pub failed: bool, + /// How much gas was used. + pub gas: u64, + /// Output of the transaction + #[serde(serialize_with = "alloy_serde::serialize_hex_string_no_prefix")] + pub return_value: Bytes, + /// Recorded traces of the transaction + pub struct_logs: Vec, +} + +/// Represents a struct log entry in a trace +/// +/// +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +struct StructLogPrime { + /// program counter + pub pc: u64, + /// opcode to be executed + pub op: String, + /// remaining gas + pub gas: u64, + /// cost for executing op + #[serde(rename = "gasCost")] + pub gas_cost: u64, + /// Current call depth + pub depth: u64, + /// Error message if any + #[serde(default, skip)] + pub error: Option, + /// EVM stack + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stack: Option>, + /// Last call's return data. Enabled via enableReturnData + #[serde( + default, + rename = "returnData", + skip_serializing_if = "Option::is_none" + )] + pub return_data: Option, + /// ref + #[serde(default, skip_serializing_if = "Option::is_none")] + pub memory: Option>, + /// Size of memory. + #[serde(default, rename = "memSize", skip_serializing_if = "Option::is_none")] + pub memory_size: Option, + /// Storage slots of current contract read from and written to. Only emitted + /// for SLOAD and SSTORE. Disabled via disableStorage + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_string_storage_map_opt" + )] + pub storage: Option>, + /// Refund counter + #[serde(default, rename = "refund", skip_serializing_if = "Option::is_none")] + pub refund_counter: Option, +} + +/// Serializes a storage map as a list of key-value pairs _without_ 0x-prefix +fn serialize_string_storage_map_opt( + storage: &Option>, + s: S, +) -> Result { + match storage { + None => s.serialize_none(), + Some(storage) => { + let mut m = s.serialize_map(Some(storage.len()))?; + for (key, val) in storage.iter() { + let key = format!("{:?}", key); + let val = format!("{:?}", val); + // skip the 0x prefix + m.serialize_entry(&key.as_str()[2..], &val.as_str()[2..])?; + } + m.end() + } + } +} + +impl TryInto for DefaultFramePrime { + fn try_into(self) -> Result { + let a = serde_json::to_string(&self)?; + let b: DefaultFramePrime = serde_json::from_str(&a)?; + let c = serde_json::to_string(&b)?; + let d: DefaultFrame = serde_json::from_str(&c)?; + Ok(d) + } + + type Error = anyhow::Error; +} + +pub fn try_reserialize(structlog_object: Value) -> Result { + let a = serde_json::to_string(&structlog_object)?; + let b: DefaultFramePrime = serde_json::from_str(&a)?; + let d: DefaultFrame = b.try_into()?; + Ok(d) +} diff --git a/zero_bin/rpc/src/native/txn.rs b/zero_bin/rpc/src/native/txn.rs index 5e3be656a..4c32363d6 100644 --- a/zero_bin/rpc/src/native/txn.rs +++ b/zero_bin/rpc/src/native/txn.rs @@ -1,31 +1,52 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + ops::Not, + sync::OnceLock, +}; use __compat_primitive_types::{H256, U256}; use alloy::{ - primitives::{keccak256, Address, B256}, + primitives::{keccak256, Address, B256, U160}, providers::{ ext::DebugApi as _, network::{eip2718::Encodable2718, Ethereum, Network}, Provider, }, rpc::types::{ - eth::Transaction, - eth::{AccessList, Block}, + eth::{AccessList, Block, Transaction}, trace::geth::{ - AccountState, DiffMode, GethDebugBuiltInTracerType, GethTrace, PreStateConfig, - PreStateFrame, PreStateMode, + AccountState, DiffMode, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethDefaultTracingOptions, GethTrace, PreStateConfig, + PreStateFrame, PreStateMode, StructLog, }, - trace::geth::{GethDebugTracerType, GethDebugTracingOptions}, }, transports::Transport, }; -use anyhow::Context as _; +use anyhow::{ensure, Context as _}; +use evm_arithmetization::{jumpdest::JumpDestTableWitness, CodeDb}; use futures::stream::{FuturesOrdered, TryStreamExt}; use trace_decoder::{ContractCodeUsage, TxnInfo, TxnMeta, TxnTrace}; +use tracing::trace; -use super::CodeDb; +use super::structlogprime::try_reserialize; use crate::Compat; +/// Provides a way to check in constant time if an address points to a +/// precompile. +fn precompiles() -> &'static HashSet
{ + static PRECOMPILES: OnceLock> = OnceLock::new(); + PRECOMPILES.get_or_init(|| { + HashSet::
::from_iter((1..=0xa).map(|x| Address::from(U160::from(x)))) + }) +} + +/// Provides a way to check in constant time if `op` is in the set of normal +/// halting states. They are defined in the Yellowpaper, 9.4.4. Normal Halting. +fn normal_halting() -> &'static HashSet<&'static str> { + static NORMAL_HALTING: OnceLock> = OnceLock::new(); + NORMAL_HALTING.get_or_init(|| HashSet::<&str>::from_iter(["RETURN", "REVERT", "STOP"])) +} + /// Processes the transactions in the given block and updates the code db. pub(super) async fn process_transactions( block: &Block, @@ -63,17 +84,12 @@ where ProviderT: Provider, TransportT: Transport + Clone, { - let (tx_receipt, pre_trace, diff_trace) = fetch_tx_data(provider, &tx.hash).await?; + let (tx_receipt, pre_trace, diff_trace, structlog_trace) = + fetch_tx_data(provider, &tx.hash).await?; let tx_status = tx_receipt.status(); let tx_receipt = tx_receipt.map_inner(rlp::map_receipt_envelope); let access_list = parse_access_list(tx.access_list.as_ref()); - let tx_meta = TxnMeta { - byte_code: ::TxEnvelope::try_from(tx.clone())?.encoded_2718(), - new_receipt_trie_node_byte: alloy::rlp::encode(tx_receipt.inner), - gas_used: tx_receipt.gas_used as u64, - }; - let (code_db, mut tx_traces) = match (pre_trace, diff_trace) { ( GethTrace::PreStateTracer(PreStateFrame::Default(read)), @@ -85,7 +101,28 @@ where // Handle case when transaction failed and a contract creation was reverted if !tx_status && tx_receipt.contract_address.is_some() { tx_traces.insert(tx_receipt.contract_address.unwrap(), TxnTrace::default()); - } + }; + + let struct_logs_opt: Option> = match structlog_trace { + GethTrace::Default(structlog_frame) => Some(structlog_frame.struct_logs), + GethTrace::JS(structlog_js_object) => try_reserialize(structlog_js_object) + .ok() + .map(|s| s.struct_logs), + _ => None, + }; + + let jumpdest_table: Option = struct_logs_opt.and_then(|struct_logs| { + generate_jumpdest_table(tx, &struct_logs, &tx_traces) + .map(Some) + .unwrap_or_default() + }); + + let tx_meta = TxnMeta { + byte_code: ::TxEnvelope::try_from(tx.clone())?.encoded_2718(), + new_receipt_trie_node_byte: alloy::rlp::encode(tx_receipt.inner), + gas_used: tx_receipt.gas_used as u64, + jumpdest_table, + }; Ok(( code_db, @@ -103,7 +140,12 @@ where async fn fetch_tx_data( provider: &ProviderT, tx_hash: &B256, -) -> anyhow::Result<(::ReceiptResponse, GethTrace, GethTrace), anyhow::Error> +) -> anyhow::Result<( + ::ReceiptResponse, + GethTrace, + GethTrace, + GethTrace, +)> where ProviderT: Provider, TransportT: Transport + Clone, @@ -111,14 +153,21 @@ where let tx_receipt_fut = provider.get_transaction_receipt(*tx_hash); let pre_trace_fut = provider.debug_trace_transaction(*tx_hash, prestate_tracing_options(false)); let diff_trace_fut = provider.debug_trace_transaction(*tx_hash, prestate_tracing_options(true)); + let structlog_trace_fut = + provider.debug_trace_transaction(*tx_hash, structlog_tracing_options()); - let (tx_receipt, pre_trace, diff_trace) = - futures::try_join!(tx_receipt_fut, pre_trace_fut, diff_trace_fut,)?; + let (tx_receipt, pre_trace, diff_trace, structlog_trace) = futures::try_join!( + tx_receipt_fut, + pre_trace_fut, + diff_trace_fut, + structlog_trace_fut + )?; Ok(( tx_receipt.context("Transaction receipt not found.")?, pre_trace, diff_trace, + structlog_trace, )) } @@ -344,3 +393,226 @@ fn prestate_tracing_options(diff_mode: bool) -> GethDebugTracingOptions { ..GethDebugTracingOptions::default() } } + +/// Tracing options for the `debug_traceTransaction` call to get structlog. +/// Used for filling JUMPDEST table. +pub(crate) fn structlog_tracing_options() -> GethDebugTracingOptions { + GethDebugTracingOptions { + config: GethDefaultTracingOptions { + disable_stack: Some(false), + disable_memory: Some(true), + disable_storage: Some(true), + ..GethDefaultTracingOptions::default() + }, + tracer: None, + ..GethDebugTracingOptions::default() + } +} + +/// Generate at JUMPDEST table by simulating the call stack in EVM, +/// using a Geth structlog as input. +pub fn generate_jumpdest_table( + tx: &Transaction, + struct_logs: &[StructLog], + tx_traces: &BTreeMap, +) -> anyhow::Result { + trace!("Generating JUMPDEST table for tx: {}", tx.hash); + ensure!(struct_logs.is_empty().not(), "Structlog is empty."); + + let mut jumpdest_table = JumpDestTableWitness::default(); + + let callee_addr_to_code_hash: HashMap = tx_traces + .iter() + .map(|(callee_addr, trace)| (callee_addr, &trace.code_usage)) + .filter(|(_callee_addr, code_usage)| code_usage.is_some()) + .map(|(callee_addr, code_usage)| { + (*callee_addr, code_usage.as_ref().unwrap().get_code_hash()) + }) + .collect(); + + ensure!( + tx.to.is_some(), + format!("No `to`-address for tx: {}.", tx.hash) + ); + let to_address: Address = tx.to.unwrap(); + + // Guard against transactions to a non-contract address. + ensure!( + callee_addr_to_code_hash.contains_key(&to_address), + format!("Callee addr {} is not at contract address", to_address) + ); + let entrypoint_code_hash: H256 = callee_addr_to_code_hash[&to_address]; + + // `None` encodes that previous `entry`` was not a JUMP or JUMPI with true + // condition, `Some(jump_target)` encodes we came from a JUMP or JUMPI with + // true condition and target `jump_target`. + let mut prev_jump = None; + + // Contains the previous op. + let mut prev_op = ""; + + // Call depth of the previous `entry`. We initialize to 0 as this compares + // smaller to 1. + let mut prev_depth = 0; + // The next available context. Starts at 1. Never decrements. + let mut next_ctx_available = 1; + // Immediately use context 1; + let mut call_stack = vec![(entrypoint_code_hash, next_ctx_available)]; + next_ctx_available += 1; + + for (step, entry) in struct_logs.iter().enumerate() { + let op = entry.op.as_str(); + let curr_depth = entry.depth; + + let exception_occurred = prev_entry_caused_exception(prev_op, prev_depth, curr_depth); + if exception_occurred { + ensure!( + call_stack.is_empty().not(), + "Call stack was empty after exception." + ); + // discard callee frame and return control to caller. + call_stack.pop().unwrap(); + } + + ensure!( + entry.depth as usize <= next_ctx_available, + "Structlog is malformed." + ); + ensure!(call_stack.is_empty().not(), "Call stack was empty."); + let (code_hash, ctx) = call_stack.last().unwrap(); + trace!("TX: {:?}", tx.hash); + trace!("STEP: {:?}", step); + trace!("STEPS: {:?}", struct_logs.len()); + trace!("PREV OPCODE: {}", prev_op); + trace!("OPCODE: {}", entry.op.as_str()); + trace!("CODE: {:?}", code_hash); + trace!("CTX: {:?}", ctx); + trace!("EXCEPTION OCCURED: {:?}", exception_occurred); + trace!("PREV_DEPTH: {:?}", prev_depth); + trace!("CURR_DEPTH: {:?}", curr_depth); + trace!("{:#?}\n", entry); + + match op { + "CALL" | "CALLCODE" | "DELEGATECALL" | "STATICCALL" => { + let callee_address = { + // This is the same stack index (i.e. 2nd) for all four opcodes. See https://ethervm.io/#F1 + ensure!(entry.stack.as_ref().is_some(), "No evm stack found."); + let mut evm_stack = entry.stack.as_ref().unwrap().iter().rev(); + + let callee_raw_opt = evm_stack.nth(1); + ensure!( + callee_raw_opt.is_some(), + "Stack must contain at least two values for a CALL instruction." + ); + + // Clear the upper half of the operand. + let callee_raw = *callee_raw_opt.unwrap(); + let (callee_raw, _overflow) = callee_raw.overflowing_shl(128); + let (callee_raw, _overflow) = callee_raw.overflowing_shr(128); + + let lower_20_bytes = U160::from(callee_raw); + Address::from(lower_20_bytes) + }; + + if precompiles().contains(&callee_address) { + trace!("Called precompile at address {}.", &callee_address); + } else if callee_addr_to_code_hash.contains_key(&callee_address) { + let code_hash = callee_addr_to_code_hash[&callee_address]; + call_stack.push((code_hash, next_ctx_available)); + } else { + // This case happens if calling an EOA. This is described + // under opcode `STOP`: https://www.evm.codes/#00?fork=cancun + trace!( + "Callee address {} has no associated `code_hash`.", + &callee_address + ); + } + next_ctx_available += 1; + prev_jump = None; + } + "JUMP" => { + ensure!(entry.stack.as_ref().is_some(), "No evm stack found."); + let mut evm_stack = entry.stack.as_ref().unwrap().iter().rev(); + + let jump_target_opt = evm_stack.next(); + ensure!( + jump_target_opt.is_some(), + "Stack must contain at least one value for a JUMP instruction." + ); + let jump_target = jump_target_opt.unwrap().to::(); + + prev_jump = Some(jump_target); + } + "JUMPI" => { + ensure!(entry.stack.as_ref().is_some(), "No evm stack found."); + let mut evm_stack = entry.stack.as_ref().unwrap().iter().rev(); + + let jump_target_opt = evm_stack.next(); + ensure!( + jump_target_opt.is_some(), + "Stack must contain at least one value for a JUMPI instruction." + ); + let jump_target = jump_target_opt.unwrap().to::(); + + let jump_condition_opt = evm_stack.next(); + ensure!( + jump_condition_opt.is_some(), + "Stack must contain at least two values for a JUMPI instruction." + ); + let jump_condition = jump_condition_opt.unwrap().is_zero().not(); + + prev_jump = if jump_condition { + Some(jump_target) + } else { + None + }; + } + "JUMPDEST" => { + ensure!( + call_stack.is_empty().not(), + "Call stack was empty when a JUMPDEST was encountered." + ); + let (code_hash, ctx) = call_stack.last().unwrap(); + let jumped_here = if let Some(jmp_target) = prev_jump { + ensure!( + jmp_target == entry.pc, + "The structlog seems to make improper JUMPs." + ); + true + } else { + false + }; + let jumpdest_offset = entry.pc as usize; + if jumped_here { + jumpdest_table.insert(code_hash, *ctx, jumpdest_offset); + } + // else: we do not care about JUMPDESTs reached through fall-through. + prev_jump = None; + } + "EXTCODECOPY" | "EXTCODESIZE" => { + next_ctx_available += 1; + prev_jump = None; + } + "RETURN" | "REVERT" | "STOP" => { + ensure!(call_stack.is_empty().not(), "Call stack was empty at POP."); + call_stack.pop().unwrap(); + prev_jump = None; + } + _ => { + prev_jump = None; + } + } + + prev_depth = curr_depth; + prev_op = op; + } + Ok(jumpdest_table) +} + +/// Check if an exception occurred. An exception will cause the current call +/// context at `depth` to yield control to the caller context at `depth-1`. +/// Returning statements, viz. RETURN, REVERT, STOP, do this too, so we need to +/// exclude them. +fn prev_entry_caused_exception(prev_entry: &str, prev_depth: u64, curr_depth: u64) -> bool { + prev_depth > curr_depth && normal_halting().contains(&prev_entry).not() +}