diff --git a/Cargo.lock b/Cargo.lock index 1faabf3..f475a04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4566,6 +4566,13 @@ dependencies = [ "tracing", ] +[[package]] +name = "odyssey-common" +version = "0.0.0" +dependencies = [ + "alloy-primitives", +] + [[package]] name = "odyssey-e2e-tests" version = "0.0.0" @@ -4575,6 +4582,9 @@ dependencies = [ "alloy-rpc-types", "alloy-signer-local", "ci_info", + "odyssey-common", + "reth-primitives-traits", + "reth-trie-common", "tokio", "url", ] @@ -4586,9 +4596,14 @@ dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", + "alloy-rpc-types", + "alloy-rpc-types-eth", "eyre", + "jsonrpsee", + "odyssey-common", "reth-chainspec", "reth-cli", + "reth-errors", "reth-evm", "reth-network", "reth-network-types", @@ -4601,7 +4616,11 @@ dependencies = [ "reth-payload-builder", "reth-primitives", "reth-revm", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "reth-rpc-types-compat", "reth-transaction-pool", + "reth-trie-common", "reth-trie-db", "revm-precompile", "revm-primitives", diff --git a/Cargo.toml b/Cargo.toml index bec8d4a..dab5fde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "bin/odyssey/", + "crates/common", "crates/node", "crates/e2e-tests", "crates/wallet", @@ -134,6 +135,7 @@ strip = false [workspace.dependencies] # odyssey +odyssey-common = { path = "crates/common" } odyssey-node = { path = "crates/node" } odyssey-wallet = { path = "crates/wallet" } odyssey-walltime = { path = "crates/walltime" } @@ -149,6 +151,7 @@ alloy-eips = "0.6.4" alloy-network = "0.6.4" alloy-primitives = "0.8.11" alloy-rpc-types = "0.6.4" +alloy-rpc-types-eth = "0.6.4" alloy-signer-local = { version = "0.6.4", features = ["mnemonic"] } # tokio @@ -158,6 +161,7 @@ tokio = { version = "1.21", default-features = false } reth-chainspec = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-cli = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-cli-util = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } +reth-errors = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-evm = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-node-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } @@ -181,13 +185,17 @@ reth-payload-builder = { git = "https://github.com/paradigmxyz/reth.git", rev = reth-primitives = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac", features = [ "optimism", ] } +reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-provider = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac", features = [ "optimism", ] } reth-revm = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } +reth-rpc-types-compat = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } +reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-storage-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } +reth-trie-common = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-trie-db = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-network = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } reth-network-types = { git = "https://github.com/paradigmxyz/reth.git", rev = "f211aac" } diff --git a/bin/odyssey/src/main.rs b/bin/odyssey/src/main.rs index 6030d53..0f9755f 100644 --- a/bin/odyssey/src/main.rs +++ b/bin/odyssey/src/main.rs @@ -28,7 +28,11 @@ use alloy_primitives::Address; use alloy_signer_local::PrivateKeySigner; use clap::Parser; use eyre::Context; -use odyssey_node::{chainspec::OdysseyChainSpecParser, node::OdysseyNode}; +use odyssey_node::{ + chainspec::OdysseyChainSpecParser, + node::OdysseyNode, + rpc::{EthApiExt, EthApiOverrideServer}, +}; use odyssey_wallet::{OdysseyWallet, OdysseyWalletApiServer}; use odyssey_walltime::{OdysseyWallTime, OdysseyWallTimeRpcApiServer}; use reth_node_builder::{engine_tree_config::TreeConfig, EngineNodeLauncher}; @@ -56,6 +60,11 @@ fn main() { .with_components(OdysseyNode::components(&rollup_args)) .with_add_ons(OpAddOns::new(rollup_args.sequencer_http)) .extend_rpc_modules(move |ctx| { + // override eth namespace + ctx.modules.replace_configured( + EthApiExt::new(ctx.registry.eth_api().clone()).into_rpc(), + )?; + // register odyssey wallet namespace if let Ok(sk) = std::env::var("EXP1_SK") { let signer: PrivateKeySigner = diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml new file mode 100644 index 0000000..a15a44e --- /dev/null +++ b/crates/common/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "odyssey-common" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +alloy-primitives.workspace = true + +[lints] +workspace = true diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs new file mode 100644 index 0000000..1b175e2 --- /dev/null +++ b/crates/common/src/constants.rs @@ -0,0 +1,8 @@ +//! Odyssey constants. + +use alloy_primitives::{address, Address}; + +/// Withdrawal predeployed contract address. +/// +/// [The L2ToL1MessagePasser](https://specs.optimism.io/protocol/withdrawals.html#the-l2tol1messagepasser-contract) +pub const WITHDRAWAL_CONTRACT: Address = address!("4200000000000000000000000000000000000016"); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs new file mode 100644 index 0000000..5018fe5 --- /dev/null +++ b/crates/common/src/lib.rs @@ -0,0 +1,7 @@ +//! Odyssey common types and constants + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![warn(unused_crate_dependencies)] + +mod constants; +pub use constants::WITHDRAWAL_CONTRACT; diff --git a/crates/e2e-tests/Cargo.toml b/crates/e2e-tests/Cargo.toml index eefd8c6..c840a2b 100644 --- a/crates/e2e-tests/Cargo.toml +++ b/crates/e2e-tests/Cargo.toml @@ -10,6 +10,11 @@ keywords.workspace = true categories.workspace = true [dev-dependencies] +odyssey-common.workspace = true + +reth-primitives-traits.workspace = true +reth-trie-common.workspace = true + alloy.workspace = true alloy-network.workspace = true alloy-rpc-types.workspace = true diff --git a/crates/e2e-tests/src/tests.rs b/crates/e2e-tests/src/tests.rs index b96820c..bbcff25 100644 --- a/crates/e2e-tests/src/tests.rs +++ b/crates/e2e-tests/src/tests.rs @@ -7,8 +7,10 @@ use alloy::{ signers::SignerSync, }; use alloy_network::{TransactionBuilder, TransactionBuilder7702}; -use alloy_rpc_types::TransactionRequest; +use alloy_rpc_types::{Block, BlockNumberOrTag, EIP1186AccountProofResponse, TransactionRequest}; use alloy_signer_local::PrivateKeySigner; +use reth_primitives_traits::Account; +use reth_trie_common::{AccountProof, StorageProof}; use url::Url; static REPLICA_RPC: LazyLock = LazyLock::new(|| { @@ -127,3 +129,61 @@ async fn test_new_wallet_api() -> Result<(), Box> { Ok(()) } + +#[tokio::test] +async fn test_withdrawal_proof_with_fallback() -> Result<(), Box> { + if !ci_info::is_ci() { + return Ok(()); + } + + let provider = ProviderBuilder::new().on_http(REPLICA_RPC.clone()); + let block: Block = provider + .client() + .request("eth_getBlockByNumber", (BlockNumberOrTag::Latest, false)) + .await?; + let block_number = BlockNumberOrTag::Number(block.header.number); + + // Withdrawal contract will return an empty account proof, since it only handles storage proofs + let withdrawal_contract_response: EIP1186AccountProofResponse = provider + .client() + .request( + "eth_getProof", + (odyssey_common::WITHDRAWAL_CONTRACT, vec![B256::ZERO], block_number), + ) + .await?; + + assert!(withdrawal_contract_response.account_proof.is_empty()); + assert!(!withdrawal_contract_response.storage_proof.is_empty()); + + let storage_root = withdrawal_contract_response.storage_hash; + for proof in withdrawal_contract_response.storage_proof { + StorageProof::new(proof.key.as_b256()).with_proof(proof.proof).verify(storage_root)? + } + + // If not targeting the withdrawal contract, it defaults back to the standard getProof + // implementation + let signer = PrivateKeySigner::from_bytes(&b256!( + "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + ))?; + + let eoa_response: EIP1186AccountProofResponse = provider + .client() + .request("eth_getProof", (signer.address(), [0; 0], block_number)) + .await + .unwrap(); + + assert!(!eoa_response.account_proof.is_empty()); + AccountProof { + address: signer.address(), + info: Some(Account { + nonce: eoa_response.nonce, + balance: eoa_response.balance, + bytecode_hash: Some(eoa_response.code_hash), + }), + proof: eoa_response.account_proof, + ..Default::default() + } + .verify(block.header.state_root)?; + + Ok(()) +} diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 8aaf1c9..c631739 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -10,10 +10,13 @@ keywords.workspace = true categories.workspace = true [dependencies] +odyssey-common.workspace = true + revm-precompile.workspace = true revm-primitives.workspace = true reth-cli.workspace = true +reth-errors.workspace = true reth-node-api.workspace = true reth-node-builder.workspace = true reth-optimism-node.workspace = true @@ -25,7 +28,11 @@ reth-payload-builder.workspace = true reth-primitives.workspace = true reth-evm.workspace = true reth-revm.workspace = true +reth-rpc-eth-api.workspace = true +reth-rpc-eth-types.workspace = true +reth-rpc-types-compat.workspace = true reth-transaction-pool.workspace = true +reth-trie-common.workspace = true reth-trie-db.workspace = true reth-network.workspace = true reth-network-types.workspace = true @@ -33,10 +40,14 @@ reth-network-types.workspace = true alloy-consensus.workspace = true alloy-eips.workspace = true alloy-primitives.workspace = true +alloy-rpc-types.workspace = true +alloy-rpc-types-eth.workspace = true + serde_json.workspace = true tracing.workspace = true eyre.workspace = true +jsonrpsee.workspace = true [lints] workspace = true diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index c0e4ee6..6966b27 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -18,3 +18,4 @@ pub mod chainspec; pub mod evm; pub mod node; +pub mod rpc; diff --git a/crates/node/src/rpc.rs b/crates/node/src/rpc.rs new file mode 100644 index 0000000..47ddc8e --- /dev/null +++ b/crates/node/src/rpc.rs @@ -0,0 +1,113 @@ +//! Odyssey rpc logic. +//! +//! `eth_` namespace overrides: +//! +//! - `eth_getProof` will _ONLY_ return the storage proofs _WITHOUT_ an account proof _IF_ targeting +//! the withdrawal contract. Otherwise, it fallbacks to default behaviour. + +use alloy_eips::BlockId; +use alloy_primitives::Address; +use alloy_rpc_types::serde_helpers::JsonStorageKey; +use alloy_rpc_types_eth::EIP1186AccountProofResponse; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + proc_macros::rpc, +}; +use odyssey_common::WITHDRAWAL_CONTRACT; +use reth_errors::RethError; +use reth_rpc_eth_api::{ + helpers::{EthState, FullEthApi}, + FromEthApiError, +}; +use reth_rpc_eth_types::EthApiError; +use reth_rpc_types_compat::proof::from_primitive_account_proof; +use reth_trie_common::AccountProof; +use tracing::trace; + +/// Odyssey `eth_` RPC namespace overrides. +#[cfg_attr(not(test), rpc(server, namespace = "eth"))] +#[cfg_attr(test, rpc(server, client, namespace = "eth"))] +pub trait EthApiOverride { + /// Returns the account and storage values of the specified account including the Merkle-proof. + /// This call can be used to verify that the data you are pulling from is not tampered with. + #[method(name = "getProof")] + async fn get_proof( + &self, + address: Address, + keys: Vec, + block_number: Option, + ) -> RpcResult; +} + +/// Implementation of the `eth_` namespace override +#[derive(Debug)] +pub struct EthApiExt { + eth_api: Eth, +} + +impl EthApiExt { + /// Create a new `EthApiExt` module. + pub const fn new(eth_api: E) -> Self { + Self { eth_api } + } +} + +#[async_trait] +impl EthApiOverrideServer for EthApiExt +where + Eth: FullEthApi + Send + Sync + 'static, +{ + async fn get_proof( + &self, + address: Address, + keys: Vec, + block_number: Option, + ) -> RpcResult { + trace!(target: "rpc::eth", ?address, ?keys, ?block_number, "Serving eth_getProof"); + + // If we are targeting the withdrawal contract, then we only need to provide the storage + // proofs for withdrawal. + if address == WITHDRAWAL_CONTRACT { + let _permit = self + .eth_api + .acquire_owned() + .await + .map_err(RethError::other) + .map_err(EthApiError::Internal)?; + + return self + .eth_api + .spawn_blocking_io(move |this| { + let state = this.state_at_block_id(block_number.unwrap_or_default())?; + let storage_root = state + .storage_root(WITHDRAWAL_CONTRACT, Default::default()) + .map_err(EthApiError::from_eth_err)?; + let storage_proofs = keys + .iter() + .map(|key| { + state.storage_proof( + WITHDRAWAL_CONTRACT, + key.as_b256(), + Default::default(), + ) + }) + .collect::, _>>() + .map_err(EthApiError::from_eth_err)?; + let proof = AccountProof { + address, + storage_root, + storage_proofs, + ..Default::default() + }; + Ok(from_primitive_account_proof(proof, keys)) + }) + .await + .map_err(Into::into); + } + + EthState::get_proof(&self.eth_api, address, keys, block_number) + .map_err(Into::into)? + .await + .map_err(Into::into) + } +}