From f8d93b3b443c70b089afad892ae6693d76686440 Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Tue, 11 Feb 2025 21:35:18 +0530 Subject: [PATCH] verifiable_api in config and usage --- cli/src/main.rs | 9 + core/src/client/mod.rs | 3 +- core/src/client/node.rs | 6 +- core/src/execution/client/mod.rs | 23 -- core/src/execution/evm.rs | 27 +- core/src/execution/mod.rs | 293 ++++-------------- core/src/execution/proof.rs | 82 ++++- .../mod.rs => verified_client/api.rs} | 35 ++- core/src/execution/verified_client/mod.rs | 49 +++ core/src/execution/verified_client/rpc.rs | 238 ++++++++++++++ ethereum/src/builder.rs | 16 + ethereum/src/config/cli.rs | 5 + ethereum/src/config/mod.rs | 2 + helios-ts/src/opstack.rs | 1 + opstack/bin/server.rs | 4 + opstack/src/builder.rs | 8 + opstack/src/config.rs | 1 + opstack/src/server/mod.rs | 6 + verifiable-api/client/src/lib.rs | 53 ++-- 19 files changed, 552 insertions(+), 309 deletions(-) delete mode 100644 core/src/execution/client/mod.rs rename core/src/execution/{verifiable_api/mod.rs => verified_client/api.rs} (84%) create mode 100644 core/src/execution/verified_client/mod.rs create mode 100644 core/src/execution/verified_client/rpc.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index 69d25766..f421d693 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -130,6 +130,8 @@ struct EthereumArgs { #[clap(short, long, env, value_parser = parse_url)] execution_rpc: Option, #[clap(short, long, env, value_parser = parse_url)] + verifiable_api: Option, + #[clap(short, long, env, value_parser = parse_url)] consensus_rpc: Option, #[clap(short, long, env)] data_dir: Option, @@ -163,6 +165,7 @@ impl EthereumArgs { CliConfig { checkpoint: self.checkpoint, execution_rpc: self.execution_rpc.clone(), + verifiable_api: self.verifiable_api.clone(), consensus_rpc: self.consensus_rpc.clone(), data_dir: self .data_dir @@ -188,6 +191,8 @@ struct OpStackArgs { #[clap(short, long, env, value_parser = parse_url)] execution_rpc: Option, #[clap(short, long, env, value_parser = parse_url)] + verifiable_api: Option, + #[clap(short, long, env, value_parser = parse_url)] consensus_rpc: Option, #[clap( short = 'w', @@ -227,6 +232,10 @@ impl OpStackArgs { user_dict.insert("execution_rpc", Value::from(rpc.to_string())); } + if let Some(api) = &self.verifiable_api { + user_dict.insert("verifiable_api", Value::from(api.to_string())); + } + if let Some(rpc) = &self.consensus_rpc { user_dict.insert("consensus_rpc", Value::from(rpc.to_string())); } diff --git a/core/src/client/mod.rs b/core/src/client/mod.rs index 79e40afd..d82bb499 100644 --- a/core/src/client/mod.rs +++ b/core/src/client/mod.rs @@ -28,11 +28,12 @@ pub struct Client> { impl> Client { pub fn new( execution_rpc: &str, + verifiable_api: Option<&str>, consensus: C, fork_schedule: ForkSchedule, #[cfg(not(target_arch = "wasm32"))] rpc_address: Option, ) -> Result { - let node = Node::new(execution_rpc, consensus, fork_schedule)?; + let node = Node::new(execution_rpc, verifiable_api, consensus, fork_schedule)?; let node = Arc::new(node); #[cfg(not(target_arch = "wasm32"))] diff --git a/core/src/client/node.rs b/core/src/client/node.rs index 57688e76..4c590940 100644 --- a/core/src/client/node.rs +++ b/core/src/client/node.rs @@ -7,6 +7,7 @@ use alloy::rpc::types::{Filter, FilterChanges, Log, SyncInfo, SyncStatus}; use eyre::{eyre, Result}; use helios_common::{fork_schedule::ForkSchedule, network_spec::NetworkSpec, types::BlockTag}; +use helios_verifiable_api_client::VerifiableApiClient; use crate::consensus::Consensus; use crate::errors::ClientError; @@ -18,7 +19,7 @@ use crate::time::{SystemTime, UNIX_EPOCH}; pub struct Node> { pub consensus: C, - pub execution: Arc>>, + pub execution: Arc, VerifiableApiClient>>, pub history_size: usize, fork_schedule: ForkSchedule, } @@ -26,6 +27,7 @@ pub struct Node> { impl> Node { pub fn new( execution_rpc: &str, + verifiable_api: Option<&str>, mut consensus: C, fork_schedule: ForkSchedule, ) -> Result { @@ -34,7 +36,7 @@ impl> Node { let state = State::new(block_recv, finalized_block_recv, 256, execution_rpc); let execution = Arc::new( - ExecutionClient::new(execution_rpc, state, fork_schedule) + ExecutionClient::new(execution_rpc, verifiable_api, state, fork_schedule) .map_err(ClientError::InternalError)?, ); diff --git a/core/src/execution/client/mod.rs b/core/src/execution/client/mod.rs deleted file mode 100644 index 62a0b547..00000000 --- a/core/src/execution/client/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -use alloy::primitives::{Address, B256, U256}; -use alloy::rpc::types::{Filter, FilterChanges, Log}; -use async_trait::async_trait; -use eyre::Result; - -use helios_common::{ - network_spec::NetworkSpec, - types::{Account, BlockTag}, -}; - -#[async_trait] -pub trait VerifiableMethods: Send + Clone + Sync + 'static { - async fn get_account( - &self, - address: Address, - slots: Option<&[B256]>, - tag: BlockTag, - ) -> Result; - async fn get_transaction_receipt(&self, tx_hash: B256) -> Result>; - async fn get_logs(&self, filter: &Filter) -> Result>; - async fn get_filter_changes(&self, filter_id: U256) -> Result; - async fn get_filter_logs(&self, filter_id: U256) -> Result>; -} diff --git a/core/src/execution/evm.rs b/core/src/execution/evm.rs index 82a2238e..15bf77a2 100644 --- a/core/src/execution/evm.rs +++ b/core/src/execution/evm.rs @@ -6,6 +6,7 @@ use alloy::{ }; use eyre::{Report, Result}; use futures::future::join_all; +use helios_verifiable_api_client::VerifiableApi; use revm::{ primitives::{ address, AccessListItem, AccountInfo, Address, Bytecode, Bytes, Env, ExecutionResult, @@ -24,16 +25,16 @@ use crate::execution::{ ExecutionClient, }; -pub struct Evm> { - execution: Arc>, +pub struct Evm, A: VerifiableApi> { + execution: Arc>, chain_id: u64, tag: BlockTag, fork_schedule: ForkSchedule, } -impl> Evm { +impl, A: VerifiableApi> Evm { pub fn new( - execution: Arc>, + execution: Arc>, chain_id: u64, fork_schedule: ForkSchedule, tag: BlockTag, @@ -119,12 +120,12 @@ impl> Evm { } } -struct ProofDB> { - state: EvmState, +struct ProofDB, A: VerifiableApi> { + state: EvmState, } -impl> ProofDB { - pub fn new(tag: BlockTag, execution: Arc>) -> Self { +impl, A: VerifiableApi> ProofDB { + pub fn new(tag: BlockTag, execution: Arc>) -> Self { let state = EvmState::new(execution.clone(), tag); ProofDB { state } } @@ -136,17 +137,17 @@ enum StateAccess { Storage(Address, U256), } -struct EvmState> { +struct EvmState, A: VerifiableApi> { basic: HashMap, block_hash: HashMap, storage: HashMap>, block: BlockTag, access: Option, - execution: Arc>, + execution: Arc>, } -impl> EvmState { - pub fn new(execution: Arc>, block: BlockTag) -> Self { +impl, A: VerifiableApi> EvmState { + pub fn new(execution: Arc>, block: BlockTag) -> Self { Self { execution, block, @@ -324,7 +325,7 @@ impl> EvmState { } } -impl> Database for ProofDB { +impl, A: VerifiableApi> Database for ProofDB { type Error = Report; fn basic(&mut self, address: Address) -> Result, Report> { diff --git a/core/src/execution/mod.rs b/core/src/execution/mod.rs index a9c6c3a6..104b421a 100644 --- a/core/src/execution/mod.rs +++ b/core/src/execution/mod.rs @@ -1,17 +1,16 @@ -use std::collections::{HashMap, HashSet}; +use std::marker::PhantomData; use alloy::consensus::BlockHeader; use alloy::eips::BlockId; use alloy::network::primitives::HeaderResponse; -use alloy::network::{BlockResponse, ReceiptResponse}; -use alloy::primitives::{keccak256, Address, B256, U256}; -use alloy::rlp; +use alloy::network::BlockResponse; +use alloy::primitives::{Address, B256, U256}; use alloy::rpc::types::{BlockTransactions, Filter, FilterChanges, Log}; -use alloy_trie::root::ordered_trie_root_with_encoder; use eyre::Result; -use futures::future::try_join_all; -use revm::primitives::{BlobExcessGasAndPrice, KECCAK_EMPTY}; -use tracing::warn; +use helios_verifiable_api_client::VerifiableApi; +use proof::{ensure_logs_match_filter, verify_block_receipts}; +use revm::primitives::BlobExcessGasAndPrice; +use tracing::{info, warn}; use helios_common::{ fork_schedule::ForkSchedule, @@ -19,35 +18,52 @@ use helios_common::{ types::{Account, BlockTag}, }; -use self::constants::MAX_SUPPORTED_LOGS_NUMBER; use self::errors::ExecutionError; -use self::proof::{verify_account_proof, verify_storage_proof}; use self::rpc::ExecutionRpc; use self::state::{FilterType, State}; +use self::verified_client::{ + api::VerifiableMethodsApi, rpc::VerifiableMethodsRpc, VerifiableMethods, + VerifiableMethodsClient, +}; -pub mod client; pub mod constants; pub mod errors; pub mod evm; pub mod proof; pub mod rpc; pub mod state; -pub mod verifiable_api; +pub mod verified_client; #[derive(Clone)] -pub struct ExecutionClient> { +pub struct ExecutionClient, A: VerifiableApi> { pub rpc: R, + verified_methods: VerifiableMethodsClient, state: State, fork_schedule: ForkSchedule, + _marker: PhantomData, } -impl> ExecutionClient { - pub fn new(rpc: &str, state: State, fork_schedule: ForkSchedule) -> Result { +impl, A: VerifiableApi> ExecutionClient { + pub fn new( + rpc: &str, + verifiable_api: Option<&str>, + state: State, + fork_schedule: ForkSchedule, + ) -> Result { + let verified_methods = if let Some(verifiable_api) = verifiable_api { + info!(target: "helios::execution", "using Verifiable-API url={}", verifiable_api); + VerifiableMethodsClient::Api(VerifiableMethodsApi::new(verifiable_api, state.clone())?) + } else { + info!(target: "helios::execution", "Using JSON-RPC url={}", rpc); + VerifiableMethodsClient::Rpc(VerifiableMethodsRpc::new(rpc, state.clone())?) + }; let rpc: R = ExecutionRpc::new(rpc)?; - Ok(ExecutionClient:: { + Ok(Self { rpc, + verified_methods, state, fork_schedule, + _marker: PhantomData, }) } @@ -65,46 +81,10 @@ impl> ExecutionClient { slots: Option<&[B256]>, tag: BlockTag, ) -> Result { - let slots = slots.unwrap_or(&[]); - let block = self - .state - .get_block(tag) + self.verified_methods + .client() + .get_account(address, slots, tag) .await - .ok_or(ExecutionError::BlockNotFound(tag))?; - - let proof = self - .rpc - .get_proof(address, slots, block.header().number().into()) - .await?; - - // Verify the account proof - verify_account_proof(&proof, block.header().state_root())?; - // Verify the storage proofs, collecting the slot values - let slot_map = verify_storage_proof(&proof)?; - // Verify the code hash - let code = if proof.code_hash == KECCAK_EMPTY || proof.code_hash == B256::ZERO { - Vec::new() - } else { - let code = self.rpc.get_code(address, block.header().number()).await?; - let code_hash = keccak256(&code); - - if proof.code_hash != code_hash { - return Err( - ExecutionError::CodeHashMismatch(address, code_hash, proof.code_hash).into(), - ); - } - - code - }; - - Ok(Account { - balance: proof.balance, - nonce: proof.nonce, - code, - code_hash: proof.code_hash, - storage_hash: proof.storage_hash, - slots: slot_map, - }) } pub async fn send_raw_transaction(&self, bytes: &[u8]) -> Result { @@ -186,47 +166,10 @@ impl> ExecutionClient { &self, tx_hash: B256, ) -> Result> { - let receipt = self.rpc.get_transaction_receipt(tx_hash).await?; - if receipt.is_none() { - return Ok(None); - } - let receipt = receipt.unwrap(); - - let block_number = receipt.block_number().unwrap(); - let block_id = BlockId::from(block_number); - let tag = BlockTag::Number(block_number); - - let block = self.state.get_block(tag).await; - let block = if let Some(block) = block { - block - } else { - return Ok(None); - }; - - // Fetch all receipts in block, check root and inclusion - let receipts = self - .rpc - .get_block_receipts(block_id) - .await? - .ok_or(eyre::eyre!(ExecutionError::NoReceiptsForBlock(tag)))?; - - let receipts_encoded = receipts.iter().map(N::encode_receipt).collect::>(); - let expected_receipt_root = ordered_trie_root(&receipts_encoded); - - if expected_receipt_root != block.header().receipts_root() - // Note: Some RPC providers return different response in `eth_getTransactionReceipt` vs `eth_getBlockReceipts` - // Primarily due to https://github.com/ethereum/execution-apis/issues/295 not finalized - // Which means that the basic equality check in N::receipt_contains can be flaky - // So as a fallback do equality check on encoded receipts as well - || !( - N::receipt_contains(&receipts, &receipt) - || receipts_encoded.contains(&N::encode_receipt(&receipt)) - ) - { - return Err(ExecutionError::ReceiptRootMismatch(tx_hash).into()); - } - - Ok(Some(receipt)) + self.verified_methods + .client() + .get_transaction_receipt(tx_hash) + .await } pub async fn get_block_receipts( @@ -247,12 +190,7 @@ impl> ExecutionClient { .await? .ok_or(eyre::eyre!(ExecutionError::NoReceiptsForBlock(tag)))?; - let receipts_encoded = receipts.iter().map(N::encode_receipt).collect::>(); - let expected_receipt_root = ordered_trie_root(&receipts_encoded); - - if expected_receipt_root != block.header().receipts_root() { - return Err(ExecutionError::BlockReceiptsRootMismatch(tag).into()); - } + verify_block_receipts::(&receipts, &block)?; Ok(Some(receipts)) } @@ -277,14 +215,8 @@ impl> ExecutionClient { filter }; - let logs = self.rpc.get_logs(&filter).await?; - if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { - return Err( - ExecutionError::TooManyLogsToProve(logs.len(), MAX_SUPPORTED_LOGS_NUMBER).into(), - ); - } - self.ensure_logs_match_filter(&logs, &filter).await?; - self.verify_logs(&logs).await?; + let logs = self.verified_methods.client().get_logs(&filter).await?; + ensure_logs_match_filter(&logs, &filter)?; Ok(logs) } @@ -298,17 +230,13 @@ impl> ExecutionClient { } Some(FilterType::Logs(filter)) => { // underlying RPC takes care of keeping track of changes - let filter_changes = self.rpc.get_filter_changes(filter_id).await?; + let filter_changes = self + .verified_methods + .client() + .get_filter_changes(filter_id) + .await?; let logs = filter_changes.as_logs().unwrap_or(&[]); - if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { - return Err(ExecutionError::TooManyLogsToProve( - logs.len(), - MAX_SUPPORTED_LOGS_NUMBER, - ) - .into()); - } - self.ensure_logs_match_filter(logs, filter).await?; - self.verify_logs(logs).await?; + ensure_logs_match_filter(logs, filter)?; FilterChanges::Logs(logs.to_vec()) } Some(FilterType::NewBlock(last_block_num)) => { @@ -343,16 +271,12 @@ impl> ExecutionClient { match &filter_type { Some(FilterType::Logs(filter)) => { - let logs = self.rpc.get_filter_logs(filter_id).await?; - if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { - return Err(ExecutionError::TooManyLogsToProve( - logs.len(), - MAX_SUPPORTED_LOGS_NUMBER, - ) - .into()); - } - self.ensure_logs_match_filter(&logs, filter).await?; - self.verify_logs(&logs).await?; + let logs = self + .verified_methods + .client() + .get_filter_logs(filter_id) + .await?; + ensure_logs_match_filter(&logs, filter)?; Ok(logs) } _ => { @@ -415,113 +339,4 @@ impl> ExecutionClient { Ok(filter_id) } - - /// Ensure that each log entry in the given array of logs match the given filter. - async fn ensure_logs_match_filter(&self, logs: &[Log], filter: &Filter) -> Result<()> { - fn log_matches_filter(log: &Log, filter: &Filter) -> bool { - if let Some(block_hash) = filter.get_block_hash() { - if log.block_hash.unwrap() != block_hash { - return false; - } - } - if let Some(from_block) = filter.get_from_block() { - if log.block_number.unwrap() < from_block { - return false; - } - } - if let Some(to_block) = filter.get_to_block() { - if log.block_number.unwrap() > to_block { - return false; - } - } - if !filter.address.matches(&log.address()) { - return false; - } - for (i, topic) in filter.topics.iter().enumerate() { - if let Some(log_topic) = log.topics().get(i) { - if !topic.matches(log_topic) { - return false; - } - } else { - // if filter topic is not present in log, it's a mismatch - return false; - } - } - true - } - for log in logs { - if !log_matches_filter(log, filter) { - return Err(ExecutionError::LogFilterMismatch().into()); - } - } - Ok(()) - } - - /// Verify the integrity of each log entry in the given array of logs by - /// checking its inclusion in the corresponding transaction receipt - /// and verifying the transaction receipt itself against the block's receipt root. - async fn verify_logs(&self, logs: &[Log]) -> Result<()> { - // Collect all (unique) block numbers - let block_nums = logs - .iter() - .map(|log| { - log.block_number - .ok_or_else(|| eyre::eyre!("block num not found in log")) - }) - .collect::, _>>()?; - - // Collect all (proven) tx receipts for all block numbers - let blocks_receipts_fut = block_nums.into_iter().map(|block_num| async move { - let tag = BlockTag::Number(block_num); - let receipts = self.get_block_receipts(tag).await; - receipts?.ok_or_else(|| eyre::eyre!(ExecutionError::NoReceiptsForBlock(tag))) - }); - let blocks_receipts = try_join_all(blocks_receipts_fut).await?; - let receipts = blocks_receipts.into_iter().flatten().collect::>(); - - // Map tx hashes to encoded logs - let receipts_logs_encoded = receipts - .into_iter() - .filter_map(|receipt| { - let logs = N::receipt_logs(&receipt); - if logs.is_empty() { - None - } else { - let tx_hash = logs[0].transaction_hash.unwrap(); - let encoded_logs = logs - .iter() - .map(|l| rlp::encode(&l.inner)) - .collect::>(); - Some((tx_hash, encoded_logs)) - } - }) - .collect::>(); - - for log in logs { - // Check if the receipt contains the desired log - // Encoding logs for comparison - let tx_hash = log.transaction_hash.unwrap(); - let log_encoded = rlp::encode(&log.inner); - let receipt_logs_encoded = receipts_logs_encoded.get(&tx_hash).unwrap(); - - if !receipt_logs_encoded.contains(&log_encoded) { - return Err(ExecutionError::MissingLog( - tx_hash, - U256::from(log.log_index.unwrap()), - ) - .into()); - } - } - Ok(()) - } -} - -/// Compute a trie root of a collection of encoded items. -/// Ref: https://github.com/alloy-rs/trie/blob/main/src/root.rs. -fn ordered_trie_root(items: &[Vec]) -> B256 { - fn noop_encoder(item: &Vec, buffer: &mut Vec) { - buffer.extend_from_slice(item); - } - - ordered_trie_root_with_encoder(items, noop_encoder) } diff --git a/core/src/execution/proof.rs b/core/src/execution/proof.rs index 8e63e3bb..1012e1db 100644 --- a/core/src/execution/proof.rs +++ b/core/src/execution/proof.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; -use alloy::network::ReceiptResponse; +use alloy::consensus::BlockHeader; +use alloy::network::{BlockResponse, ReceiptResponse}; use alloy::primitives::{keccak256, Bytes, B256, U256}; use alloy::rlp; -use alloy::rpc::types::EIP1186AccountProofResponse; +use alloy::rpc::types::{EIP1186AccountProofResponse, Filter, Log}; +use alloy_trie::root::ordered_trie_root_with_encoder; use alloy_trie::{ proof::{verify_proof, ProofRetainer}, root::adjust_index_for_rlp, @@ -12,6 +14,7 @@ use alloy_trie::{ use eyre::{eyre, Result}; use helios_common::network_spec::NetworkSpec; +use helios_common::types::BlockTag; use super::errors::ExecutionError; @@ -142,3 +145,78 @@ pub fn verify_receipt_proof( verify_proof(root, key, expected_value, proof).map_err(|e| eyre!(e)) } + +/// Calculate the receipts root hash from given list of receipts +/// and verify it against the given block's receipts root. +pub fn verify_block_receipts( + receipts: &[N::ReceiptResponse], + block: &N::BlockResponse, +) -> Result<()> { + let receipts_encoded = receipts + .into_iter() + .map(N::encode_receipt) + .collect::>(); + let expected_receipt_root = ordered_trie_root_noop_encoder(&receipts_encoded); + + if expected_receipt_root != block.header().receipts_root() { + return Err(ExecutionError::BlockReceiptsRootMismatch(BlockTag::Number( + block.header().number(), + )) + .into()); + } + + Ok(()) +} + +/// Compute a trie root of a collection of encoded items. +/// Ref: https://github.com/alloy-rs/trie/blob/main/src/root.rs. +pub fn ordered_trie_root_noop_encoder(items: &[Vec]) -> B256 { + fn noop_encoder(item: &Vec, buffer: &mut Vec) { + buffer.extend_from_slice(item); + } + + ordered_trie_root_with_encoder(items, noop_encoder) +} + +/// Ensure that each log entry in the given array of logs match the given filter. +pub fn ensure_logs_match_filter(logs: &[Log], filter: &Filter) -> Result<()> { + fn log_matches_filter(log: &Log, filter: &Filter) -> bool { + if let Some(block_hash) = filter.get_block_hash() { + if log.block_hash.unwrap() != block_hash { + return false; + } + } + if let Some(from_block) = filter.get_from_block() { + if log.block_number.unwrap() < from_block { + return false; + } + } + if let Some(to_block) = filter.get_to_block() { + if log.block_number.unwrap() > to_block { + return false; + } + } + if !filter.address.matches(&log.address()) { + return false; + } + for (i, topic) in filter.topics.iter().enumerate() { + if let Some(log_topic) = log.topics().get(i) { + if !topic.matches(log_topic) { + return false; + } + } else { + // if filter topic is not present in log, it's a mismatch + return false; + } + } + true + } + + for log in logs { + if !log_matches_filter(log, filter) { + return Err(ExecutionError::LogFilterMismatch().into()); + } + } + + Ok(()) +} diff --git a/core/src/execution/verifiable_api/mod.rs b/core/src/execution/verified_client/api.rs similarity index 84% rename from core/src/execution/verifiable_api/mod.rs rename to core/src/execution/verified_client/api.rs index 684265e2..0016adcb 100644 --- a/core/src/execution/verifiable_api/mod.rs +++ b/core/src/execution/verified_client/api.rs @@ -15,22 +15,28 @@ use helios_common::{ }; use helios_verifiable_api_client::{types::*, VerifiableApi}; -use crate::execution::client::VerifiableMethods; +use crate::execution::constants::MAX_SUPPORTED_LOGS_NUMBER; use crate::execution::errors::ExecutionError; use crate::execution::proof::{verify_account_proof, verify_receipt_proof, verify_storage_proof}; use crate::execution::rpc::ExecutionRpc; use crate::execution::state::State; +use crate::execution::verified_client::VerifiableMethods; #[derive(Clone)] -pub struct ExecutionVerifiableApiClient, A: VerifiableApi> { +pub struct VerifiableMethodsApi, A: VerifiableApi> { api: A, state: State, } #[async_trait] -impl, A: VerifiableApi> VerifiableMethods - for ExecutionVerifiableApiClient +impl, A: VerifiableApi> VerifiableMethods + for VerifiableMethodsApi { + fn new(url: &str, state: State) -> Result { + let api: A = VerifiableApi::new(url); + Ok(Self { api, state }) + } + async fn get_account( &self, address: Address, @@ -108,6 +114,11 @@ impl, A: VerifiableApi> VerifiableMethods< receipt_proofs, } = self.api.get_logs(filter).await?; + if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { + return Err( + ExecutionError::TooManyLogsToProve(logs.len(), MAX_SUPPORTED_LOGS_NUMBER).into(), + ); + } self.verify_logs_and_receipts(&logs, receipt_proofs).await?; Ok(logs) @@ -122,6 +133,13 @@ impl, A: VerifiableApi> VerifiableMethods< logs, receipt_proofs, }) => { + if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { + return Err(ExecutionError::TooManyLogsToProve( + logs.len(), + MAX_SUPPORTED_LOGS_NUMBER, + ) + .into()); + } self.verify_logs_and_receipts(&logs, receipt_proofs).await?; FilterChanges::Logs(logs) } @@ -134,15 +152,18 @@ impl, A: VerifiableApi> VerifiableMethods< receipt_proofs, } = self.api.get_filter_logs(filter_id).await?; + if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { + return Err( + ExecutionError::TooManyLogsToProve(logs.len(), MAX_SUPPORTED_LOGS_NUMBER).into(), + ); + } self.verify_logs_and_receipts(&logs, receipt_proofs).await?; Ok(logs) } } -impl, A: VerifiableApi> - ExecutionVerifiableApiClient -{ +impl, A: VerifiableApi> VerifiableMethodsApi { async fn verify_logs_and_receipts( &self, logs: &[Log], diff --git a/core/src/execution/verified_client/mod.rs b/core/src/execution/verified_client/mod.rs new file mode 100644 index 00000000..72c18477 --- /dev/null +++ b/core/src/execution/verified_client/mod.rs @@ -0,0 +1,49 @@ +use alloy::primitives::{Address, B256, U256}; +use alloy::rpc::types::{Filter, FilterChanges, Log}; +use async_trait::async_trait; +use eyre::Result; + +use helios_common::{ + network_spec::NetworkSpec, + types::{Account, BlockTag}, +}; +use helios_verifiable_api_client::VerifiableApi; + +use super::rpc::ExecutionRpc; +use super::state::State; + +pub mod api; +pub mod rpc; + +#[async_trait] +pub trait VerifiableMethods> { + fn new(url: &str, state: State) -> Result + where + Self: Sized; + async fn get_account( + &self, + address: Address, + slots: Option<&[B256]>, + tag: BlockTag, + ) -> Result; + async fn get_transaction_receipt(&self, tx_hash: B256) -> Result>; + async fn get_logs(&self, filter: &Filter) -> Result>; + async fn get_filter_changes(&self, filter_id: U256) -> Result; + async fn get_filter_logs(&self, filter_id: U256) -> Result>; +} + +#[derive(Clone)] +pub enum VerifiableMethodsClient, A: VerifiableApi> { + Api(api::VerifiableMethodsApi), + Rpc(rpc::VerifiableMethodsRpc), +} + +impl, A: VerifiableApi> VerifiableMethodsClient { + // Manual dispatch + pub fn client(&self) -> &dyn VerifiableMethods { + match self { + VerifiableMethodsClient::Api(client) => client, + VerifiableMethodsClient::Rpc(client) => client, + } + } +} diff --git a/core/src/execution/verified_client/rpc.rs b/core/src/execution/verified_client/rpc.rs new file mode 100644 index 00000000..d7ba8e21 --- /dev/null +++ b/core/src/execution/verified_client/rpc.rs @@ -0,0 +1,238 @@ +use std::collections::{HashMap, HashSet}; + +use alloy::consensus::BlockHeader; +use alloy::eips::BlockId; +use alloy::network::{BlockResponse, ReceiptResponse}; +use alloy::primitives::{keccak256, Address, B256, U256}; +use alloy::rlp; +use alloy::rpc::types::{Filter, FilterChanges, Log}; +use async_trait::async_trait; +use eyre::Result; +use futures::future::try_join_all; +use revm::primitives::KECCAK_EMPTY; + +use helios_common::{ + network_spec::NetworkSpec, + types::{Account, BlockTag}, +}; + +use crate::execution::constants::MAX_SUPPORTED_LOGS_NUMBER; +use crate::execution::errors::ExecutionError; +use crate::execution::proof::{ + ordered_trie_root_noop_encoder, verify_account_proof, verify_storage_proof, +}; +use crate::execution::rpc::ExecutionRpc; +use crate::execution::state::State; + +use super::VerifiableMethods; + +#[derive(Clone)] +pub struct VerifiableMethodsRpc> { + rpc: R, + state: State, +} + +#[async_trait] +impl> VerifiableMethods for VerifiableMethodsRpc { + fn new(url: &str, state: State) -> Result { + let rpc: R = ExecutionRpc::new(url)?; + Ok(Self { rpc, state }) + } + + async fn get_account( + &self, + address: Address, + slots: Option<&[B256]>, + tag: BlockTag, + ) -> Result { + let slots = slots.unwrap_or(&[]); + let block = self + .state + .get_block(tag) + .await + .ok_or(ExecutionError::BlockNotFound(tag))?; + + let proof = self + .rpc + .get_proof(address, slots, block.header().number().into()) + .await?; + + // Verify the account proof + verify_account_proof(&proof, block.header().state_root())?; + // Verify the storage proofs, collecting the slot values + let slot_map = verify_storage_proof(&proof)?; + // Verify the code hash + let code = if proof.code_hash == KECCAK_EMPTY || proof.code_hash == B256::ZERO { + Vec::new() + } else { + let code = self.rpc.get_code(address, block.header().number()).await?; + let code_hash = keccak256(&code); + + if proof.code_hash != code_hash { + return Err( + ExecutionError::CodeHashMismatch(address, code_hash, proof.code_hash).into(), + ); + } + + code + }; + + Ok(Account { + balance: proof.balance, + nonce: proof.nonce, + code, + code_hash: proof.code_hash, + storage_hash: proof.storage_hash, + slots: slot_map, + }) + } + + async fn get_transaction_receipt(&self, tx_hash: B256) -> Result> { + let receipt = self.rpc.get_transaction_receipt(tx_hash).await?; + if receipt.is_none() { + return Ok(None); + } + let receipt = receipt.unwrap(); + + let block_number = receipt.block_number().unwrap(); + let block_id = BlockId::from(block_number); + let tag = BlockTag::Number(block_number); + + let block = self.state.get_block(tag).await; + let block = if let Some(block) = block { + block + } else { + return Ok(None); + }; + + // Fetch all receipts in block, check root and inclusion + let receipts = self + .rpc + .get_block_receipts(block_id) + .await? + .ok_or(eyre::eyre!(ExecutionError::NoReceiptsForBlock(tag)))?; + + let receipts_encoded = receipts.iter().map(N::encode_receipt).collect::>(); + let expected_receipt_root = ordered_trie_root_noop_encoder(&receipts_encoded); + + if expected_receipt_root != block.header().receipts_root() + // Note: Some RPC providers return different response in `eth_getTransactionReceipt` vs `eth_getBlockReceipts` + // Primarily due to https://github.com/ethereum/execution-apis/issues/295 not finalized + // Which means that the basic equality check in N::receipt_contains can be flaky + // So as a fallback do equality check on encoded receipts as well + || !( + N::receipt_contains(&receipts, &receipt) + || receipts_encoded.contains(&N::encode_receipt(&receipt)) + ) + { + return Err(ExecutionError::ReceiptRootMismatch(tx_hash).into()); + } + + Ok(Some(receipt)) + } + + async fn get_logs(&self, filter: &Filter) -> Result> { + let logs = self.rpc.get_logs(filter).await?; + + if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { + return Err( + ExecutionError::TooManyLogsToProve(logs.len(), MAX_SUPPORTED_LOGS_NUMBER).into(), + ); + } + + self.verify_logs(&logs).await?; + Ok(logs) + } + + async fn get_filter_changes(&self, filter_id: U256) -> Result { + let filter_changes = self.rpc.get_filter_changes(filter_id).await?; + + if filter_changes.is_logs() { + let logs = filter_changes.as_logs().unwrap_or(&[]); + if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { + return Err(ExecutionError::TooManyLogsToProve( + logs.len(), + MAX_SUPPORTED_LOGS_NUMBER, + ) + .into()); + } + self.verify_logs(logs).await?; + } + + Ok(filter_changes) + } + + async fn get_filter_logs(&self, filter_id: U256) -> Result> { + let logs = self.rpc.get_filter_logs(filter_id).await?; + + if logs.len() > MAX_SUPPORTED_LOGS_NUMBER { + return Err( + ExecutionError::TooManyLogsToProve(logs.len(), MAX_SUPPORTED_LOGS_NUMBER).into(), + ); + } + + self.verify_logs(&logs).await?; + Ok(logs) + } +} + +impl> VerifiableMethodsRpc { + /// Verify the integrity of each log entry in the given array of logs by + /// checking its inclusion in the corresponding transaction receipt + /// and verifying the transaction receipt itself against the block's receipt root. + async fn verify_logs(&self, logs: &[Log]) -> Result<()> { + // Collect all (unique) block numbers + let block_nums = logs + .iter() + .map(|log| { + log.block_number + .ok_or_else(|| eyre::eyre!("block num not found in log")) + }) + .collect::, _>>()?; + + // Collect all (proven) tx receipts for all block numbers + let blocks_receipts_fut = block_nums.into_iter().map(|block_num| async move { + let tag = BlockTag::Number(block_num); + // ToDo(@eshaan7): use verified version of `get_block_receipts` + let receipts = self.rpc.get_block_receipts(block_num.into()).await; + receipts?.ok_or_else(|| eyre::eyre!(ExecutionError::NoReceiptsForBlock(tag))) + }); + let blocks_receipts = try_join_all(blocks_receipts_fut).await?; + let receipts = blocks_receipts.into_iter().flatten().collect::>(); + + // Map tx hashes to encoded logs + let receipts_logs_encoded = receipts + .into_iter() + .filter_map(|receipt| { + let logs = N::receipt_logs(&receipt); + if logs.is_empty() { + None + } else { + let tx_hash = logs[0].transaction_hash.unwrap(); + let encoded_logs = logs + .iter() + .map(|l| rlp::encode(&l.inner)) + .collect::>(); + Some((tx_hash, encoded_logs)) + } + }) + .collect::>(); + + for log in logs { + // Check if the receipt contains the desired log + // Encoding logs for comparison + let tx_hash = log.transaction_hash.unwrap(); + let log_encoded = rlp::encode(&log.inner); + let receipt_logs_encoded = receipts_logs_encoded.get(&tx_hash).unwrap(); + + if !receipt_logs_encoded.contains(&log_encoded) { + return Err(ExecutionError::MissingLog( + tx_hash, + U256::from(log.log_index.unwrap()), + ) + .into()); + } + } + Ok(()) + } +} diff --git a/ethereum/src/builder.rs b/ethereum/src/builder.rs index bc02c426..0254168a 100644 --- a/ethereum/src/builder.rs +++ b/ethereum/src/builder.rs @@ -23,6 +23,7 @@ pub struct EthereumClientBuilder { network: Option, consensus_rpc: Option, execution_rpc: Option, + verifiable_api: Option, checkpoint: Option, #[cfg(not(target_arch = "wasm32"))] rpc_bind_ip: Option, @@ -56,6 +57,11 @@ impl EthereumClientBuilder { self } + pub fn verifiable_api(mut self, verifiable_api: Option) -> Self { + self.verifiable_api = verifiable_api; + self + } + pub fn checkpoint(mut self, checkpoint: B256) -> Self { self.checkpoint = Some(checkpoint); self @@ -126,6 +132,14 @@ impl EthereumClientBuilder { .clone() }); + let verifiable_api = self.verifiable_api.or_else(|| { + self.config + .as_ref() + .expect("missing verifiable_api rpc") + .verifiable_api + .clone() + }); + let checkpoint = if let Some(checkpoint) = self.checkpoint { Some(checkpoint) } else if let Some(config) = &self.config { @@ -190,6 +204,7 @@ impl EthereumClientBuilder { let config = Config { consensus_rpc, execution_rpc, + verifiable_api, checkpoint, default_checkpoint, #[cfg(not(target_arch = "wasm32"))] @@ -226,6 +241,7 @@ impl EthereumClientBuilder { Client::>::new( &config.execution_rpc.clone(), + config.verifiable_api.as_deref(), consensus, config.execution_forks.clone(), #[cfg(not(target_arch = "wasm32"))] diff --git a/ethereum/src/config/cli.rs b/ethereum/src/config/cli.rs index 37885084..673ac2fa 100644 --- a/ethereum/src/config/cli.rs +++ b/ethereum/src/config/cli.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct CliConfig { pub execution_rpc: Option, + pub verifiable_api: Option, pub consensus_rpc: Option, pub checkpoint: Option, pub rpc_bind_ip: Option, @@ -28,6 +29,10 @@ impl CliConfig { user_dict.insert("execution_rpc", Value::from(rpc.to_string())); } + if let Some(api) = &self.verifiable_api { + user_dict.insert("verifiable_api", Value::from(api.to_string())); + } + if let Some(rpc) = &self.consensus_rpc { user_dict.insert("consensus_rpc", Value::from(rpc.to_string())); } diff --git a/ethereum/src/config/mod.rs b/ethereum/src/config/mod.rs index 334fb759..4e0753d6 100644 --- a/ethereum/src/config/mod.rs +++ b/ethereum/src/config/mod.rs @@ -28,6 +28,7 @@ mod types; pub struct Config { pub consensus_rpc: String, pub execution_rpc: String, + pub verifiable_api: Option, pub rpc_bind_ip: Option, pub rpc_port: Option, pub default_checkpoint: B256, @@ -102,6 +103,7 @@ impl From for Config { rpc_port: Some(base.rpc_port), consensus_rpc: base.consensus_rpc.unwrap_or_default(), execution_rpc: String::new(), + verifiable_api: None, checkpoint: None, default_checkpoint: base.default_checkpoint, chain: base.chain, diff --git a/helios-ts/src/opstack.rs b/helios-ts/src/opstack.rs index 1c66a863..13102a87 100644 --- a/helios-ts/src/opstack.rs +++ b/helios-ts/src/opstack.rs @@ -42,6 +42,7 @@ impl OpStackClient { let config = Config { execution_rpc: execution_rpc.parse()?, + verifiable_api: None, consensus_rpc, chain: network_config.chain, rpc_socket: None, diff --git a/opstack/bin/server.rs b/opstack/bin/server.rs index c1fc5944..e7729b3e 100644 --- a/opstack/bin/server.rs +++ b/opstack/bin/server.rs @@ -26,6 +26,7 @@ async fn main() -> Result<()> { let gossip_addr = cli.gossip_address; let replica_urls = cli.replica_urls.unwrap_or_default(); let execution_rpc = cli.execution_rpc; + let verifiable_api = cli.verifiable_api; start_server( server_addr, @@ -35,6 +36,7 @@ async fn main() -> Result<()> { system_config_contract, replica_urls, execution_rpc, + verifiable_api, ) .await?; @@ -73,4 +75,6 @@ struct Cli { replica_urls: Option>, #[clap(short, long)] execution_rpc: Url, + #[clap(short, long)] + verifiable_api: Option, } diff --git a/opstack/src/builder.rs b/opstack/src/builder.rs index 4df9e1ff..c986dc41 100644 --- a/opstack/src/builder.rs +++ b/opstack/src/builder.rs @@ -17,6 +17,7 @@ pub struct OpStackClientBuilder { network: Option, consensus_rpc: Option, execution_rpc: Option, + verifiable_api: Option, rpc_socket: Option, verify_unsafe_singer: Option, } @@ -41,6 +42,11 @@ impl OpStackClientBuilder { self } + pub fn verifiable_api(mut self, verifiable_api: Option) -> Self { + self.verifiable_api = verifiable_api.map(|url| url.into_url().unwrap()); + self + } + pub fn rpc_socket(mut self, socket: SocketAddr) -> Self { self.rpc_socket = Some(socket); self @@ -75,6 +81,7 @@ impl OpStackClientBuilder { Config { consensus_rpc, execution_rpc, + verifiable_api: self.verifiable_api, rpc_socket: self.rpc_socket, chain: NetworkConfig::from(network).chain, load_external_fallback: None, @@ -90,6 +97,7 @@ impl OpStackClientBuilder { let consensus = ConsensusClient::new(&config); OpStackClient::new( &config.execution_rpc.to_string(), + config.verifiable_api.map(|url| url.to_string()).as_deref(), consensus, fork_schedule, #[cfg(not(target_arch = "wasm32"))] diff --git a/opstack/src/config.rs b/opstack/src/config.rs index 69b0760e..7027b358 100644 --- a/opstack/src/config.rs +++ b/opstack/src/config.rs @@ -17,6 +17,7 @@ use url::Url; pub struct Config { pub consensus_rpc: Url, pub execution_rpc: Url, + pub verifiable_api: Option, pub rpc_socket: Option, pub chain: ChainConfig, pub load_external_fallback: Option, diff --git a/opstack/src/server/mod.rs b/opstack/src/server/mod.rs index 651d1b8e..e2f642d1 100644 --- a/opstack/src/server/mod.rs +++ b/opstack/src/server/mod.rs @@ -42,6 +42,7 @@ pub async fn start_server( system_config_contract: Address, replica_urls: Vec, execution_rpc: Url, + verifiable_api: Option, ) -> Result<()> { let state = Arc::new(RwLock::new(ServerState::new( gossip_addr, @@ -50,6 +51,7 @@ pub async fn start_server( system_config_contract, replica_urls, execution_rpc, + verifiable_api, )?)); let state_copy = state.clone(); @@ -108,6 +110,8 @@ struct ServerState { commitment_recv: Receiver, latest_commitment: Option<(SequencerCommitment, u64)>, execution_rpc: Url, + #[allow(dead_code)] + verifiable_api: Option, system_config_contract: Address, } @@ -119,6 +123,7 @@ impl ServerState { system_config_contract: Address, replica_urls: Vec, execution_rpc: Url, + verifiable_api: Option, ) -> Result { let (send, commitment_recv) = channel(256); poller::start(replica_urls, signer, chain_id, send.clone()); @@ -131,6 +136,7 @@ impl ServerState { commitment_recv, latest_commitment: None, execution_rpc, + verifiable_api, system_config_contract, }) } diff --git a/verifiable-api/client/src/lib.rs b/verifiable-api/client/src/lib.rs index 53a8c794..f63a6a18 100644 --- a/verifiable-api/client/src/lib.rs +++ b/verifiable-api/client/src/lib.rs @@ -14,7 +14,10 @@ use helios_verifiable_api_types::*; pub use helios_verifiable_api_types as types; #[async_trait] -pub trait VerifiableApi: Send + Clone + Sync + 'static { +pub trait VerifiableApi: Send + Clone + Sync { + fn new(base_url: &str) -> Self + where + Self: Sized; async fn get_account( &self, address: Address, @@ -37,23 +40,29 @@ pub trait VerifiableApi: Send + Clone + Sync + 'static { async fn get_filter_changes(&self, filter_id: U256) -> Result>; } -#[derive(Clone)] pub struct VerifiableApiClient { client: Client, base_url: String, } -impl VerifiableApiClient { - pub fn new(base_url: String) -> Self { +impl Clone for VerifiableApiClient { + fn clone(&self) -> Self { Self { client: Client::new(), - base_url, + base_url: self.base_url.to_string(), } } } #[async_trait] impl VerifiableApi for VerifiableApiClient { + fn new(base_url: &str) -> Self { + Self { + client: Client::new(), + base_url: base_url.to_string(), + } + } + async fn get_account( &self, address: Address, @@ -61,13 +70,14 @@ impl VerifiableApi for VerifiableApiClient { block: Option, ) -> Result { let url = format!("{}/eth/v1/proof/account/{}", self.base_url, address); - let response = self - .client - .get(&url) - .query(&[("block", block)]) - .query(&[("storageKeys", storage_keys)]) - .send() - .await?; + let mut request = self.client.get(&url); + if let Some(block) = block { + request = request.query(&[("block", block.to_string())]); + } + for storage_key in storage_keys { + request = request.query(&[("storageKeys", storage_key)]); + } + let response = request.send().await?; let response = response.json::().await?; Ok(response) } @@ -79,12 +89,11 @@ impl VerifiableApi for VerifiableApiClient { block: Option, ) -> Result { let url = format!("{}/eth/v1/proof/storage/{}/{}", self.base_url, address, key); - let response = self - .client - .get(&url) - .query(&[("block", block)]) - .send() - .await?; + let mut request = self.client.get(&url); + if let Some(block) = block { + request = request.query(&[("block", block.to_string())]); + } + let response = request.send().await?; let response = response.json::().await?; Ok(response) } @@ -113,10 +122,10 @@ impl VerifiableApi for VerifiableApiClient { let mut request = self.client.get(&url); if let Some(from_block) = filter.get_from_block() { - request = request.query(&[("fromBlock", from_block)]); + request = request.query(&[("fromBlock", U256::from(from_block))]); } if let Some(to_block) = filter.get_to_block() { - request = request.query(&[("toBlock", to_block)]); + request = request.query(&[("toBlock", U256::from(to_block))]); } if let Some(block_hash) = filter.get_block_hash() { request = request.query(&[("blockHash", block_hash)]); @@ -129,8 +138,8 @@ impl VerifiableApi for VerifiableApiClient { .iter() .filter_map(|t| t.to_value_or_array()) .collect::>(); - if topics.len() > 0 { - request = request.query(&[("topics", topics)]); + for topic in topics { + request = request.query(&[("topics", topic)]); } let response = request.send().await?;