diff --git a/Cargo.lock b/Cargo.lock index 4ff7e30..6ac21f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,6 +773,7 @@ name = "alphanet" version = "0.0.0" dependencies = [ "alphanet-node", + "alphanet-wallet", "clap", "reth-cli-util", "reth-node-builder", @@ -844,7 +845,13 @@ dependencies = [ "alloy-primitives", "alloy-rpc-types", "jsonrpsee", + "reth-optimism-rpc", + "reth-primitives", + "reth-storage-api", + "reth-transaction-pool", "serde", + "thiserror", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a23c915..67a5d8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,7 @@ reth-provider = { git = "https://github.com/paradigmxyz/reth.git", rev = "784267 reth-revm = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673", features = [ "optimism", ] } +reth-storage-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" } reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" } reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" } @@ -119,6 +120,7 @@ tracing = "0.1.0" serde = "1" serde_json = "1" once_cell = "1.19" +thiserror = "1" # misc-testing rstest = "0.18.2" diff --git a/bin/alphanet/Cargo.toml b/bin/alphanet/Cargo.toml index a15ef6b..0ac20ae 100644 --- a/bin/alphanet/Cargo.toml +++ b/bin/alphanet/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] alphanet-node.workspace = true +alphanet-wallet.workspace = true tracing.workspace = true reth-cli-util.workspace = true reth-node-builder.workspace = true diff --git a/bin/alphanet/src/main.rs b/bin/alphanet/src/main.rs index d1963e4..083978a 100644 --- a/bin/alphanet/src/main.rs +++ b/bin/alphanet/src/main.rs @@ -24,6 +24,7 @@ //! - `min-trace-logs`: Disables all logs below `trace` level. use alphanet_node::{chainspec::AlphanetChainSpecParser, node::AlphaNetNode}; +use alphanet_wallet::{AlphaNetWallet, AlphaNetWalletApiServer}; use clap::Parser; use reth_node_builder::{engine_tree_config::TreeConfig, EngineNodeLauncher}; use reth_optimism_cli::Cli; @@ -53,12 +54,23 @@ fn main() { .with_components(AlphaNetNode::components(rollup_args.clone())) .with_add_ons(OptimismAddOns::new(rollup_args.sequencer_http.clone())) .extend_rpc_modules(move |ctx| { + let sequencer_client = rollup_args.sequencer_http.map(SequencerClient::new); + // register sequencer tx forwarder - if let Some(sequencer_http) = rollup_args.sequencer_http.clone() { - ctx.registry - .eth_api() - .set_sequencer_client(SequencerClient::new(sequencer_http))?; + if let Some(sequencer_client) = sequencer_client.clone() { + ctx.registry.eth_api().set_sequencer_client(sequencer_client)?; } + // register alphanet wallet namespace + ctx.modules.merge_configured( + AlphaNetWallet::new( + ctx.provider().clone(), + ctx.pool().clone(), + sequencer_client.clone(), + ctx.config().chain.chain().id(), + Vec::new(), + ) + .into_rpc(), + )?; Ok(()) }) diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 44bdf84..53bd193 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -13,7 +13,13 @@ categories.workspace = true alloy-primitives.workspace = true alloy-rpc-types.workspace = true jsonrpsee = { workspace = true, features = ["server", "macros"] } +reth-optimism-rpc.workspace = true +reth-primitives.workspace = true +reth-storage-api.workspace = true +reth-transaction-pool.workspace = true serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true +tracing.workspace = true [lints] workspace = true diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs index 39411e5..46c3953 100644 --- a/crates/wallet/src/lib.rs +++ b/crates/wallet/src/lib.rs @@ -17,10 +17,15 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] -use alloy_primitives::{map::HashMap, Address, ChainId, TxHash}; +use alloy_primitives::{map::HashMap, Address, ChainId, TxHash, TxKind, U256}; use alloy_rpc_types::TransactionRequest; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use reth_optimism_rpc::SequencerClient; +use reth_primitives::revm_primitives::Bytecode; +use reth_storage_api::StateProvider; +use reth_transaction_pool::TransactionPool; use serde::{Deserialize, Serialize}; +use tracing::trace; /// The capability to perform [EIP-7702][eip-7702] delegations, sponsored by the sequencer. /// @@ -45,6 +50,12 @@ pub struct Capabilities { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct WalletCapabilities(pub HashMap); +impl WalletCapabilities { + pub fn get(&self, chain_id: ChainId) -> Option<&Capabilities> { + self.0.get(&chain_id) + } +} + /// AlphaNet `wallet_` RPC namespace. #[cfg_attr(not(test), rpc(server, namespace = "wallet"))] #[cfg_attr(test, rpc(server, client, namespace = "wallet"))] @@ -77,3 +88,128 @@ pub trait AlphaNetWalletApi { #[method(name = "sendTransaction")] fn send_transaction(&self, request: TransactionRequest) -> RpcResult; } + +#[derive(Debug, thiserror::Error)] +pub enum AlphaNetWalletError { + #[error("tx value not zero")] + ValueNotZero, + #[error("tx from field is set")] + FromSet, + #[error("tx nonce is set")] + NonceSet, + #[error("invalid authorization address")] + InvalidAuthorization, + #[error("the authority of an authorization item is the sequencer")] + AuthorityIsSequencer, + #[error("the destination of the transaction is not a delegated account")] + IllegalDestination, +} + +impl From for jsonrpsee::types::error::ErrorObject<'static> { + fn from(error: AlphaNetWalletError) -> Self { + jsonrpsee::types::error::ErrorObject::owned::<()>( + jsonrpsee::types::error::INVALID_PARAMS_CODE, + error.to_string(), + None, + ) + } +} + +pub struct AlphaNetWallet { + provider: Provider, + pool: Pool, + sequencer_client: Option, + chain_id: ChainId, + capabilities: WalletCapabilities, +} + +impl AlphaNetWallet { + pub fn new( + provider: Provider, + pool: Pool, + sequencer_client: Option, + chain_id: ChainId, + valid_designations: Vec
, + ) -> Self { + let mut caps = HashMap::default(); + caps.insert( + chain_id, + Capabilities { delegation: DelegationCapability { addresses: valid_designations } }, + ); + + Self { provider, pool, sequencer_client, chain_id, capabilities: WalletCapabilities(caps) } + } +} + +impl AlphaNetWalletApiServer for AlphaNetWallet +where + Pool: TransactionPool + Clone + 'static, + Provider: StateProvider + Send + Sync + 'static, +{ + fn get_capabilities(&self) -> RpcResult { + trace!(target: "rpc::wallet", "Serving wallet_getCapabilities"); + Ok(self.capabilities.clone()) + } + + fn send_transaction(&self, mut request: TransactionRequest) -> RpcResult { + trace!(target: "rpc::wallet", ?request, "Serving wallet_sendTransaction"); + + // reject transactions that have a non-zero value to prevent draining the sequencer. + if request.value.is_some_and(|val| val > U256::ZERO) { + return Err(AlphaNetWalletError::ValueNotZero.into()); + } + + // reject transactions that have from set, as this will be the sequencer. + if request.from.is_some() { + return Err(AlphaNetWalletError::FromSet.into()); + } + + // reject transaction requests that have nonce set, as this is managed by the sequencer. + if request.nonce.is_some() { + return Err(AlphaNetWalletError::NonceSet.into()); + } + // set nonce and chain id + request.chain_id = Some(self.chain_id); + // gas estimation + + let valid_delegations: &[Address] = self + .capabilities + .get(self.chain_id) + .map(|caps| caps.delegation.addresses.as_ref()) + .unwrap_or_default(); + if let Some(authorizations) = request.authorization_list { + // check that all auth items delegate to a valid address + if authorizations.iter().any(|auth| !valid_delegations.contains(&auth.address)) { + return Err(AlphaNetWalletError::InvalidAuthorization.into()); + } + } else { + // if to is set, ensure that it is an account that delegates to a whitelisted address + // if this is not a 7702 tx + match request.to { + Some(TxKind::Call(addr)) => { + if let Ok(Some(code)) = self.provider.account_code(addr) { + match code.0 { + Bytecode::Eip7702(code) => { + // not a whitelisted address + if !valid_delegations.contains(&code.address()) { + return Err(AlphaNetWalletError::IllegalDestination.into()); + } + } + // not a 7702 account + _ => return Err(AlphaNetWalletError::IllegalDestination.into()), + } + } else { + // no bytecode + return Err(AlphaNetWalletError::IllegalDestination.into()); + } + } + // create tx's disallowed + _ => return Err(AlphaNetWalletError::IllegalDestination.into()), + } + } + + // build and sign + // add to pool, or send to sequencer + todo!() + } +}