Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/executor/guest/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
}
71 changes: 68 additions & 3 deletions crates/executor/guest/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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<EthEvmConfig<ChainSpec, CustomEvmFactory>, ChainSpec>;

Expand All @@ -55,6 +56,7 @@ where
pub fn execute(
&self,
mut input: ClientExecutorInput<C::Primitives>,
storage_info: Vec<(Address, U256, U256)>,
) -> Result<(Header, B256), ClientError> {
let chain_id: u64 = (&input.genesis).try_into().expect("convert chain id err");

Expand Down Expand Up @@ -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::<TrieAccount>(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::<HashMap<_, _>>();

// Verify and build block hashes
let mut block_hashes: HashMap<u64, B256> =
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))
}
}
Expand Down
20 changes: 10 additions & 10 deletions crates/executor/guest/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<U256>(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::<U256>(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.
Expand Down
3 changes: 2 additions & 1 deletion crates/executor/guest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 4 additions & 1 deletion crates/executor/host/src/host_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ impl<C: ConfigureEvm, CS> HostExecutor<C, CS> {
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
Expand Down Expand Up @@ -105,6 +107,7 @@ impl<C: ConfigureEvm, CS> HostExecutor<C, CS> {
debug_provider,
block_number - 1,
previous_block.header().state_root(),
is_goat_testnet,
)
.await
.map_err(HostError::RpcDbError)?;
Expand Down Expand Up @@ -149,7 +152,7 @@ impl<C: ConfigureEvm, CS> HostExecutor<C, CS> {
&block,
self.chain_spec.clone(),
&execution_output,
is_goat_testnet(chain_id),
is_goat_testnet,
)?;

// Accumulate the logs bloom.
Expand Down
2 changes: 1 addition & 1 deletion crates/executor/host/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async fn run_e2e<C, CS, N>(
.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();
Expand Down
7 changes: 3 additions & 4 deletions crates/mpt/src/execution_witness.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<Bytes>,
pre_state_root: B256,
) -> Result<(MptNode, HashMap<B256, MptNode>), String> {
// Step 1: Decode all RLP-encoded trie nodes and index by hash
Expand All @@ -19,7 +18,7 @@ pub(crate) fn build_validated_tries(
let mut node_by_hash: HashMap<B256, MptNode> = HashMap::default();
let mut root_node: Option<MptNode> = 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 {
Expand Down
24 changes: 15 additions & 9 deletions crates/mpt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Bytes>, 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 }
}
Expand All @@ -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::<TrieAccount>(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 {
Expand Down
3 changes: 2 additions & 1 deletion crates/storage/rpc-db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
62 changes: 44 additions & 18 deletions crates/storage/rpc-db/src/execution_witness.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Bytes>,
pub codes: Vec<Bytes>,
pub keys: Option<Vec<Bytes>>,
pub headers: Vec<Header>,
}

#[derive(Debug)]
pub struct ExecutionWitnessRpcDb<P, N> {
/// The provider which fetches data.
Expand All @@ -30,23 +39,40 @@ pub struct ExecutionWitnessRpcDb<P, N> {

impl<P: Provider<N> + Clone, N: Network> ExecutionWitnessRpcDb<P, N> {
/// Create a new [`ExecutionWitnessRpcDb`].
pub async fn new(provider: P, block_number: u64, state_root: B256) -> Result<Self, RpcDbError> {
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<Self, RpcDbError> {
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 };

Expand Down