diff --git a/Cargo.lock b/Cargo.lock index feda82f..65cd62b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7269,6 +7269,7 @@ dependencies = [ "revm-database-interface", "revm-primitives", "revm-state", + "serde", "thiserror 1.0.69", "tokio", "tracing", diff --git a/crates/executor/guest/src/error.rs b/crates/executor/guest/src/error.rs index 557bd8e..8093320 100644 --- a/crates/executor/guest/src/error.rs +++ b/crates/executor/guest/src/error.rs @@ -2,6 +2,7 @@ use alloy_primitives::{Address, FixedBytes}; use mpt::Error as MptError; use reth_consensus::ConsensusError; use reth_evm::execute::BlockExecutionError; +use revm_primitives::U256; #[derive(Debug, thiserror::Error)] pub enum ClientError { @@ -31,4 +32,8 @@ pub enum ClientError { FailedToReadGenesisFile(#[from] std::io::Error), #[error("Failed to deserialize the genesis file: {}", .0)] FailedToDeserializeGenesisFile(#[from] serde_json::Error), + #[error("Failed to check slot and value: {}", .0)] + FailedToCheckSlotAndValue(U256), + #[error("Failed to fetch slot and value: {}", .0)] + FailedToFetchSlotAndValue(U256), } diff --git a/crates/executor/guest/src/executor.rs b/crates/executor/guest/src/executor.rs index 2520ab6..910090f 100644 --- a/crates/executor/guest/src/executor.rs +++ b/crates/executor/guest/src/executor.rs @@ -13,9 +13,9 @@ use reth_evm::{ use reth_evm_ethereum::EthEvmConfig; use reth_execution_types::ExecutionOutcome; use reth_primitives_traits::Block; -use reth_trie::KeccakKeyHasher; -use revm::{database::WrapDatabaseRef, install_crypto}; -use revm_primitives::Address; +use reth_trie::{KeccakKeyHasher, TrieAccount, EMPTY_ROOT_HASH}; +use revm::{database::WrapDatabaseRef, install_crypto, DatabaseRef}; +use revm_primitives::{Address, HashMap, U256}; use crate::{ custom::{CustomCrypto, CustomEvmFactory}, @@ -33,6 +33,7 @@ pub const BLOCK_EXECUTION: &str = "block execution"; pub const VALIDATE_HEADER: &str = "validate header"; pub const VALIDATE_EXECUTION: &str = "validate block post-execution"; pub const COMPUTE_STATE_ROOT: &str = "compute state root"; +pub const CHECK_SLOT_AND_VALUE: &str = "check slot and value"; pub type EthClientExecutor = ClientExecutor, ChainSpec>; @@ -55,6 +56,7 @@ where pub fn execute( &self, mut input: ClientExecutorInput, + storage_info: Vec<(Address, U256, U256)>, ) -> Result<(Header, B256), ClientError> { let chain_id: u64 = (&input.genesis).try_into().expect("convert chain id err"); @@ -152,6 +154,69 @@ where requests_hash: input.current_block.header().requests_hash(), }; + if !storage_info.is_empty() { + let check_result: Result<(), ClientError> = profile_report!(CHECK_SLOT_AND_VALUE, { + let state = input.state(); + let db = { + for (hashed_address, storage_trie) in state.storage_tries.iter() { + let account = state + .state_trie + .get_rlp::(hashed_address.as_slice()) + .unwrap(); + let storage_root = account.map_or(EMPTY_ROOT_HASH, |a| a.storage_root); + if storage_root != storage_trie.hash() { + return Err(ClientError::MismatchedStorageRoot); + } + } + + let bytecodes_by_hash = input + .bytecodes() + .map(|code| (code.hash_slow(), code)) + .collect::>(); + + // Verify and build block hashes + let mut block_hashes: HashMap = + HashMap::with_hasher(Default::default()); + for (child_header, parent_header) in input.sealed_headers().tuple_windows() { + if parent_header.number() != child_header.number() - 1 { + return Err(ClientError::InvalidHeaderBlockNumber( + parent_header.number() + 1, + child_header.number(), + )); + } + + let parent_header_hash = parent_header.hash_slow(); + if parent_header_hash != child_header.parent_hash() { + return Err(ClientError::InvalidHeaderParentHash( + parent_header_hash, + child_header.parent_hash(), + )); + } + + block_hashes.insert(parent_header.number(), child_header.parent_hash()); + } + + TrieDB::new(state, block_hashes, bytecodes_by_hash) + }; + + for (contract_address, slot_id, expected_value) in storage_info { + match db.storage_ref(contract_address, slot_id) { + Ok(actual_value) => { + if actual_value != expected_value { + return Err(ClientError::FailedToCheckSlotAndValue(slot_id)); + } + } + _ => { + return Err(ClientError::FailedToFetchSlotAndValue(slot_id)); + } + } + } + Ok(()) + }); + if check_result.is_err() { + return Err(check_result.err().unwrap()); + } + } Ok((header, parent_state_root)) } } diff --git a/crates/executor/guest/src/io.rs b/crates/executor/guest/src/io.rs index 63a4ccc..6fc21bb 100644 --- a/crates/executor/guest/src/io.rs +++ b/crates/executor/guest/src/io.rs @@ -151,16 +151,16 @@ impl DatabaseRef for TrieDB<'_> { let hashed_address = keccak256(address); let hashed_address = hashed_address.as_slice(); - let storage_trie = self - .inner - .storage_tries - .get(hashed_address) - .expect("A storage trie must be provided for each account"); - - Ok(storage_trie - .get_rlp::(keccak256(index.to_be_bytes::<32>()).as_slice()) - .expect("Can get from MPT") - .unwrap_or_default()) + let storage_trie = self.inner.storage_tries.get(hashed_address); + + if let Some(storage_trie) = storage_trie { + Ok(storage_trie + .get_rlp::(keccak256(index.to_be_bytes::<32>()).as_slice()) + .expect("Can get from MPT") + .unwrap_or_default()) + } else { + Ok(U256::ZERO) + } } /// Get block hash by block number. diff --git a/crates/executor/guest/src/lib.rs b/crates/executor/guest/src/lib.rs index bb754b7..3facb8c 100644 --- a/crates/executor/guest/src/lib.rs +++ b/crates/executor/guest/src/lib.rs @@ -27,7 +27,8 @@ pub fn verify_block(input: &[u8]) -> (B256, B256, B256) { Arc::new((&input.genesis).try_into().unwrap()), input.custom_beneficiary, ); - let (header, prev_state_root) = executor.execute(input).expect("failed to execute client"); + let (header, prev_state_root) = + executor.execute(input, vec![]).expect("failed to execute client"); let block_hash = header.hash_slow(); (block_hash, header.state_root, prev_state_root) } diff --git a/crates/executor/host/src/host_executor.rs b/crates/executor/host/src/host_executor.rs index a8d7d23..a3db56e 100644 --- a/crates/executor/host/src/host_executor.rs +++ b/crates/executor/host/src/host_executor.rs @@ -76,6 +76,8 @@ impl HostExecutor { let chain_id: u64 = (&genesis).try_into().unwrap(); tracing::debug!("chain id: {}", chain_id); + let is_goat_testnet = is_goat_testnet(chain_id); + // Fetch the current block and the previous block from the provider. tracing::info!("[{}] fetching the current block and the previous block", block_number); let rpc_block = provider @@ -105,6 +107,7 @@ impl HostExecutor { debug_provider, block_number - 1, previous_block.header().state_root(), + is_goat_testnet, ) .await .map_err(HostError::RpcDbError)?; @@ -149,7 +152,7 @@ impl HostExecutor { &block, self.chain_spec.clone(), &execution_output, - is_goat_testnet(chain_id), + is_goat_testnet, )?; // Accumulate the logs bloom. diff --git a/crates/executor/host/tests/integration.rs b/crates/executor/host/tests/integration.rs index b246c31..8769933 100644 --- a/crates/executor/host/tests/integration.rs +++ b/crates/executor/host/tests/integration.rs @@ -123,7 +123,7 @@ async fn run_e2e( .expect("failed to execute host"); // Execute the client. - client_executor.execute(client_input.clone()).expect("failed to execute client"); + client_executor.execute(client_input.clone(), vec![]).expect("failed to execute client"); // Save the client input to a buffer. let buffer = bincode::serialize(&client_input).unwrap(); diff --git a/crates/mpt/src/execution_witness.rs b/crates/mpt/src/execution_witness.rs index 48cb0d1..b740c74 100644 --- a/crates/mpt/src/execution_witness.rs +++ b/crates/mpt/src/execution_witness.rs @@ -1,6 +1,5 @@ -use alloy_primitives::{keccak256, map::HashMap, B256}; +use alloy_primitives::{keccak256, map::HashMap, Bytes, B256}; use alloy_rlp::Decodable; -use alloy_rpc_types_debug::ExecutionWitness; use reth_trie::TrieAccount; use crate::mpt::{resolve_nodes, MptNode, MptNodeData, MptNodeReference}; @@ -10,7 +9,7 @@ use crate::mpt::{resolve_nodes, MptNode, MptNodeData, MptNodeReference}; // NOTE: This method should be called outside zkVM! In general you construct tries, then // validate them inside zkVM. pub(crate) fn build_validated_tries( - witness: &ExecutionWitness, + state: &Vec, pre_state_root: B256, ) -> Result<(MptNode, HashMap), String> { // Step 1: Decode all RLP-encoded trie nodes and index by hash @@ -19,7 +18,7 @@ pub(crate) fn build_validated_tries( let mut node_by_hash: HashMap = HashMap::default(); let mut root_node: Option = None; - for encoded in &witness.state { + for encoded in state { let node = MptNode::decode(encoded).expect("Valid MPT node in witness"); let hash = keccak256(encoded); if hash == pre_state_root { diff --git a/crates/mpt/src/lib.rs b/crates/mpt/src/lib.rs index 97642df..3cc9acc 100644 --- a/crates/mpt/src/lib.rs +++ b/crates/mpt/src/lib.rs @@ -1,6 +1,4 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] - -use alloy_primitives::{keccak256, map::HashMap, Address, B256}; +use alloy_primitives::{keccak256, map::HashMap, Address, Bytes, B256}; use alloy_rpc_types::EIP1186AccountProofResponse; use reth_trie::{AccountProof, HashedPostState, HashedStorage, TrieAccount}; use serde::{Deserialize, Serialize}; @@ -73,12 +71,9 @@ impl EthereumState { } #[cfg(feature = "execution-witness")] - pub fn from_execution_witness( - witness: &alloy_rpc_types_debug::ExecutionWitness, - pre_state_root: B256, - ) -> Self { + pub fn from_execution_witness(state: &Vec, pre_state_root: B256) -> Self { let (state_trie, storage_tries) = - execution_witness::build_validated_tries(witness, pre_state_root).unwrap(); + execution_witness::build_validated_tries(state, pre_state_root).unwrap(); Self { state_trie, storage_tries } } @@ -93,7 +88,18 @@ impl EthereumState { .get(hashed_address) .cloned() .unwrap_or_else(|| HashedStorage::new(false)); - let storage_root = { + + let storage_root = if state_storage.is_empty() { + // Preserve the existing storage root when witness lacks the storage trie. + self.state_trie + .get_rlp::(hashed_address.as_slice()) + .ok() + .flatten() + .map(|existing| existing.storage_root) + .unwrap_or_else(|| { + self.storage_tries.entry(*hashed_address).or_default().hash() + }) + } else { let storage_trie = self.storage_tries.entry(*hashed_address).or_default(); if state_storage.wiped { diff --git a/crates/storage/rpc-db/Cargo.toml b/crates/storage/rpc-db/Cargo.toml index 4780e6c..cec1420 100644 --- a/crates/storage/rpc-db/Cargo.toml +++ b/crates/storage/rpc-db/Cargo.toml @@ -9,6 +9,7 @@ async-trait.workspace = true tokio.workspace = true thiserror.workspace = true tracing.workspace = true +serde.workspace = true mpt.workspace = true primitives.workspace = true @@ -33,7 +34,7 @@ alloy-trie = { workspace = true, optional = true, features = ["ethereum"] } [features] default = ["execution-witness"] execution-witness = [ - "dep:alloy-consensus", + "dep:alloy-consensus", "dep:alloy-rlp", "dep:alloy-trie", "alloy-provider/debug-api" diff --git a/crates/storage/rpc-db/src/execution_witness.rs b/crates/storage/rpc-db/src/execution_witness.rs index b7e5105..8b39982 100644 --- a/crates/storage/rpc-db/src/execution_witness.rs +++ b/crates/storage/rpc-db/src/execution_witness.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; -use alloy_consensus::Header; -use alloy_primitives::{map::HashMap, Address, B256}; +use alloy_consensus::{private::alloy_eips::BlockNumberOrTag, Header}; +use alloy_primitives::{map::HashMap, Address, Bytes, B256}; use alloy_provider::{ext::DebugApi, Network, Provider}; use alloy_rlp::Decodable; use alloy_trie::TrieAccount; @@ -11,9 +11,18 @@ use reth_storage_errors::ProviderError; use revm_database::{BundleState, DatabaseRef}; use revm_primitives::{keccak256, ruint::aliases::U256, StorageKey, StorageValue}; use revm_state::{AccountInfo, Bytecode}; +use serde::{Deserialize, Serialize}; use crate::{RpcDb, RpcDbError}; +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ExecutionWitnessGoat { + pub state: Vec, + pub codes: Vec, + pub keys: Option>, + pub headers: Vec
, +} + #[derive(Debug)] pub struct ExecutionWitnessRpcDb { /// The provider which fetches data. @@ -30,23 +39,40 @@ pub struct ExecutionWitnessRpcDb { impl + Clone, N: Network> ExecutionWitnessRpcDb { /// Create a new [`ExecutionWitnessRpcDb`]. - pub async fn new(provider: P, block_number: u64, state_root: B256) -> Result { - let execution_witness = provider.debug_execution_witness((block_number + 1).into()).await?; - - let state = EthereumState::from_execution_witness(&execution_witness, state_root); - - let codes = execution_witness - .codes - .iter() - .map(|encoded| (keccak256(encoded), Bytecode::new_raw(encoded.clone()))) - .collect(); - - let ancestor_headers = execution_witness - .headers - .iter() - .map(|encoded| Header::decode(&mut encoded.as_ref()).unwrap()) - .map(|h| (h.number, h)) + pub async fn new( + provider: P, + block_number: u64, + state_root: B256, + is_goat_testnet: bool, + ) -> Result { + let (state, codes, headers) = if is_goat_testnet { + let execution_witness: ExecutionWitnessGoat = provider + .raw_request( + "debug_executionWitness".into(), + (BlockNumberOrTag::Number(block_number + 1),), + ) + .await?; + + (execution_witness.state, execution_witness.codes, execution_witness.headers) + } else { + let execution_witness = + provider.debug_execution_witness((block_number + 1).into()).await?; + let headers = execution_witness + .headers + .iter() + .map(|encoded| Header::decode(&mut encoded.as_ref()).unwrap()) + .collect(); + + (execution_witness.state, execution_witness.codes, headers) + }; + tracing::info!("fetch execution witness for block {}", block_number + 1); + + let state = EthereumState::from_execution_witness(&state, state_root); + let codes = codes + .into_iter() + .map(|encoded| (keccak256(&encoded), Bytecode::new_raw(encoded))) .collect(); + let ancestor_headers = headers.into_iter().map(|h| (h.number, h)).collect(); let db = Self { provider, state, codes, ancestor_headers, phantom: PhantomData };