diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b66bf99..e8ee9f2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,7 +7,7 @@ on: merge_group: push: branches: [main] - + env: CARGO_TERM_COLOR: always @@ -30,29 +30,29 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Install Kurtosis - run: | - echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list - sudo apt update - sudo apt install kurtosis-cli + run: | + echo "deb [trusted=yes] https://apt.fury.io/kurtosis-tech/ /" | sudo tee /etc/apt/sources.list.d/kurtosis.list + sudo apt update + sudo apt install kurtosis-cli - name: Build Odyssey run: | - cargo build --profile release --locked --bin odyssey && - mkdir dist/ && - cp ./target/release/odyssey dist/odyssey && - docker buildx build . --load -f .github/assets/Dockerfile -t ghcr.io/ithacaxyz/odyssey:latest + cargo build --profile release --locked --bin odyssey && + mkdir dist/ && + cp ./target/release/odyssey dist/odyssey && + docker buildx build . --load -f .github/assets/Dockerfile -t ghcr.io/ithacaxyz/odyssey:latest - name: Run enclave id: kurtosis run: | - kurtosis engine start - kurtosis run --enclave op-devnet github.com/ethpandaops/optimism-package --args-file ./etc/kurtosis.yaml - ENCLAVE_ID=$(curl http://127.0.0.1:9779/api/enclaves | jq --raw-output 'keys[0]') - SEQUENCER_EL_PORT=$(curl "http://127.0.0.1:9779/api/enclaves/$ENCLAVE_ID/services" | jq '."op-el-1-op-reth-op-node-op-kurtosis".public_ports.rpc.number') - REPLICA_EL_PORT=$(curl "http://127.0.0.1:9779/api/enclaves/$ENCLAVE_ID/services" | jq '."op-el-2-op-reth-op-node-op-kurtosis".public_ports.rpc.number') - echo "SEQUENCER_RPC=http://127.0.0.1:$SEQUENCER_EL_PORT" >> $GITHUB_ENV - echo "REPLICA_RPC=http://127.0.0.1:$REPLICA_EL_PORT" >> $GITHUB_ENV + kurtosis engine start + kurtosis run --enclave op-devnet github.com/ethpandaops/optimism-package --args-file ./etc/kurtosis.yaml + ENCLAVE_ID=$(curl http://127.0.0.1:9779/api/enclaves | jq --raw-output 'keys[0]') + SEQUENCER_EL_PORT=$(curl "http://127.0.0.1:9779/api/enclaves/$ENCLAVE_ID/services" | jq '."op-el-1-op-reth-op-node-op-kurtosis".public_ports.rpc.number') + REPLICA_EL_PORT=$(curl "http://127.0.0.1:9779/api/enclaves/$ENCLAVE_ID/services" | jq '."op-el-2-op-reth-op-node-op-kurtosis".public_ports.rpc.number') + echo "SEQUENCER_RPC=http://127.0.0.1:$SEQUENCER_EL_PORT" >> $GITHUB_ENV + echo "REPLICA_RPC=http://127.0.0.1:$REPLICA_EL_PORT" >> $GITHUB_ENV - name: Run E2E tests run: | - cargo nextest run \ - --locked \ - --workspace \ - -E "package(odyssey-e2e-tests)" + cargo nextest run \ + --locked \ + --workspace \ + -E "package(odyssey-e2e-tests)" diff --git a/Cargo.lock b/Cargo.lock index b8a93b4..2ca5aa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,11 +105,9 @@ dependencies = [ "alloy-contract", "alloy-core", "alloy-eips", - "alloy-genesis", "alloy-network", "alloy-provider", "alloy-rpc-client", - "alloy-serde", "alloy-signer", "alloy-transport", "alloy-transport-http", @@ -176,7 +174,6 @@ dependencies = [ "alloy-dyn-abi", "alloy-json-abi", "alloy-primitives", - "alloy-rlp", "alloy-sol-types", ] @@ -2666,21 +2663,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -3187,22 +3169,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.10" @@ -4338,23 +4304,6 @@ dependencies = [ "unsigned-varint", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -4552,6 +4501,8 @@ version = "0.0.0" dependencies = [ "alloy-network", "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", "alloy-signer-local", "clap", "eyre", @@ -4629,21 +4580,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "odyssey-relay" +version = "0.0.0" +dependencies = [ + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-signer-local", + "clap", + "eyre", + "jsonrpsee", + "odyssey-wallet", + "reth-tracing", + "tokio", + "tracing", + "url", +] + [[package]] name = "odyssey-wallet" version = "0.0.0" dependencies = [ - "alloy-eips", "alloy-network", "alloy-primitives", + "alloy-provider", "alloy-rpc-types", + "alloy-transport", + "eyre", "jsonrpsee", "metrics 0.23.0", "metrics-derive", "reth-optimism-rpc", "reth-rpc-eth-api", "reth-storage-api", - "revm-primitives", "serde", "serde_json", "thiserror 1.0.69", @@ -4786,50 +4756,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" -dependencies = [ - "bitflags 2.6.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "openssl-sys" -version = "0.9.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -5527,13 +5459,11 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -5547,7 +5477,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tokio-native-tls", "tokio-rustls", "tower-service", "url", @@ -9527,16 +9456,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" @@ -10072,12 +9991,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "vergen" version = "8.3.2" diff --git a/Cargo.toml b/Cargo.toml index d763557..98f3d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [workspace] members = [ "bin/odyssey/", + "bin/relay/", "crates/common", "crates/node", "crates/e2e-tests", "crates/wallet", "crates/walltime", ] -default-members = ["bin/odyssey/"] +default-members = ["bin/odyssey/", "bin/relay/"] resolver = "2" [workspace.package] @@ -145,19 +146,29 @@ alloy = { version = "0.6.4", features = [ "providers", "provider-http", "signers", -] } -alloy-consensus = "0.6.4" -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" + "reqwest-rustls-tls", +], default-features = false } +alloy-consensus = { version = "0.6.4", default-features = false } +alloy-eips = { version = "0.6.4", default-features = false } +alloy-network = { version = "0.6.4", default-features = false } +alloy-primitives = { version = "0.8.11", default-features = false } +alloy-provider = { version = "0.6.4", default-features = false } +alloy-rpc-client = { version = "0.6.4", default-features = false } +alloy-rpc-types = { version = "0.6.4", default-features = false } +alloy-rpc-types-eth = { version = "0.6.4", default-features = false } alloy-signer-local = { version = "0.6.4", features = ["mnemonic"] } +alloy-transport = { version = "0.6.4", default-features = false } +alloy-transport-http = { version = "0.6.4", default-features = false, features = [ + "reqwest", + "reqwest-rustls-tls", +] } +reqwest = { version = "0.12.9", default-features = false, features = [ + "rustls-tls", +] } # tokio tokio = { version = "1.21", default-features = false } -# reth reth-chainspec = { git = "https://github.com/paradigmxyz/reth.git", rev = "38cf6c9" } reth-cli = { git = "https://github.com/paradigmxyz/reth.git", rev = "38cf6c9" } reth-cli-util = { git = "https://github.com/paradigmxyz/reth.git", rev = "38cf6c9" } @@ -218,6 +229,7 @@ serde = "1" serde_json = "1" thiserror = "1" futures = "0.3" +url = "2.5" # misc-testing rstest = "0.18.2" diff --git a/bin/odyssey/Cargo.toml b/bin/odyssey/Cargo.toml index c8fb97c..fd3de54 100644 --- a/bin/odyssey/Cargo.toml +++ b/bin/odyssey/Cargo.toml @@ -15,6 +15,8 @@ workspace = true alloy-signer-local.workspace = true alloy-network.workspace = true alloy-primitives.workspace = true +alloy-provider.workspace = true +alloy-rpc-client.workspace = true odyssey-node.workspace = true odyssey-wallet.workspace = true odyssey-walltime.workspace = true diff --git a/bin/odyssey/src/main.rs b/bin/odyssey/src/main.rs index 5b6656f..a874f3a 100644 --- a/bin/odyssey/src/main.rs +++ b/bin/odyssey/src/main.rs @@ -33,7 +33,7 @@ use odyssey_node::{ node::OdysseyNode, rpc::{EthApiExt, EthApiOverrideServer}, }; -use odyssey_wallet::{OdysseyWallet, OdysseyWalletApiServer}; +use odyssey_wallet::{OdysseyWallet, OdysseyWalletApiServer, RethUpstream}; use odyssey_walltime::{OdysseyWallTime, OdysseyWallTimeRpcApiServer}; use reth_node_builder::{engine_tree_config::TreeConfig, EngineNodeLauncher, NodeComponents}; use reth_optimism_cli::Cli; @@ -92,9 +92,11 @@ fn main() { if let Some(wallet) = wallet { ctx.modules.merge_configured( OdysseyWallet::new( - ctx.provider().clone(), - wallet, - ctx.registry.eth_api().clone(), + RethUpstream::new( + ctx.provider().clone(), + ctx.registry.eth_api().clone(), + wallet, + ), ctx.config().chain.chain().id(), ) .into_rpc(), diff --git a/bin/relay/Cargo.toml b/bin/relay/Cargo.toml new file mode 100644 index 0000000..42bdbc9 --- /dev/null +++ b/bin/relay/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "odyssey-relay" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Odyssey Relay is an EIP-7702 native transaction batcher and sponsor." + +[lints] +workspace = true + +[dependencies] +alloy-signer-local.workspace = true +alloy-primitives.workspace = true +alloy-provider.workspace = true +alloy-rpc-client.workspace = true +odyssey-wallet.workspace = true +eyre.workspace = true +jsonrpsee = { workspace = true, features = ["server"] } +tracing.workspace = true +reth-tracing.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +url.workspace = true +tokio = { workspace = true, features = ["rt", "macros"] } + +[features] +default = [] +min-error-logs = ["tracing/release_max_level_error"] +min-warn-logs = ["tracing/release_max_level_warn"] +min-info-logs = ["tracing/release_max_level_info"] +min-debug-logs = ["tracing/release_max_level_debug"] +min-trace-logs = ["tracing/release_max_level_trace"] + +[[bin]] +name = "relay" +path = "src/main.rs" diff --git a/bin/relay/src/main.rs b/bin/relay/src/main.rs new file mode 100644 index 0000000..994c1c6 --- /dev/null +++ b/bin/relay/src/main.rs @@ -0,0 +1,77 @@ +//! # Odyssey Relay +//! +//! TBD + +use alloy_provider::{network::EthereumWallet, Provider, ProviderBuilder}; +use alloy_rpc_client::RpcClient; +use alloy_signer_local::PrivateKeySigner; +use clap::Parser; +use eyre::Context; +use jsonrpsee::server::Server; +use odyssey_wallet::{AlloyUpstream, OdysseyWallet, OdysseyWalletApiServer}; +use reth_tracing::Tracer; +use std::net::{IpAddr, Ipv4Addr}; +use tracing::info; +use url::Url; + +/// The Odyssey relayer service sponsors transactions for EIP-7702 accounts. +#[derive(Debug, Parser)] +#[command(author, about = "Relay", long_about = None)] +struct Args { + /// The address to serve the RPC on. + #[arg(long = "http.addr", value_name = "ADDR", default_value_t = IpAddr::V4(Ipv4Addr::LOCALHOST))] + address: IpAddr, + /// The port to serve the RPC on. + #[arg(long = "http.port", value_name = "PORT", default_value_t = 9119)] + port: u16, + /// The RPC endpoint of the chain to send transactions to. + #[arg(long, value_name = "RPC_ENDPOINT")] + upstream: Url, + /// The secret key to sponsor transactions with. + #[arg(long, value_name = "SECRET_KEY", env = "RELAY_SK")] + secret_key: String, +} + +impl Args { + /// Run the relayer service. + async fn run(self) -> eyre::Result<()> { + let _guard = reth_tracing::RethTracer::new().init()?; + + // construct provider + let signer: PrivateKeySigner = self.secret_key.parse().wrap_err("Invalid signing key")?; + let wallet = EthereumWallet::from(signer); + let rpc_client = RpcClient::new_http(self.upstream).boxed(); + let provider = + ProviderBuilder::new().with_recommended_fillers().wallet(wallet).on_client(rpc_client); + + // get chain id + let chain_id = provider.get_chain_id().await?; + + // construct rpc module + let rpc = OdysseyWallet::new(AlloyUpstream::new(provider), chain_id).into_rpc(); + + // start server + let server = Server::builder().http_only().build((self.address, self.port)).await?; + info!(addr = ?server.local_addr().unwrap(), "Started relay service"); + + let handle = server.start(rpc); + handle.stopped().await; + + Ok(()) + } +} + +#[doc(hidden)] +#[tokio::main] +async fn main() { + // Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided. + if std::env::var_os("RUST_BACKTRACE").is_none() { + std::env::set_var("RUST_BACKTRACE", "1"); + } + + let args = Args::parse(); + if let Err(err) = args.run().await { + eprint!("Error: {err:?}"); + std::process::exit(1); + } +} diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 221b51b..4be86f1 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -10,20 +10,20 @@ keywords.workspace = true categories.workspace = true [dependencies] -alloy-eips.workspace = true alloy-network.workspace = true alloy-primitives.workspace = true +alloy-provider.workspace = true alloy-rpc-types.workspace = true +alloy-transport.workspace = true -reth-storage-api.workspace = true -reth-rpc-eth-api.workspace = true reth-optimism-rpc.workspace = true - -revm-primitives.workspace = true +reth-rpc-eth-api.workspace = true +reth-storage-api.workspace = true jsonrpsee = { workspace = true, features = ["server", "macros"] } serde = { workspace = true, features = ["derive"] } thiserror.workspace = true +eyre.workspace = true tracing.workspace = true tokio = { workspace = true, features = ["sync"] } diff --git a/crates/wallet/src/lib.rs b/crates/wallet/src/lib.rs index ae8a413..61af625 100644 --- a/crates/wallet/src/lib.rs +++ b/crates/wallet/src/lib.rs @@ -2,14 +2,13 @@ //! //! Implementations of a custom `wallet_` namespace for Odyssey experiment 1. //! -//! - `odyssey_sendTransaction` that can perform sequencer-sponsored [EIP-7702][eip-7702] -//! delegations and send other sequencer-sponsored transactions on behalf of EOAs with delegated -//! code. +//! - `odyssey_sendTransaction` that can perform service-sponsored [EIP-7702][eip-7702] delegations +//! and send other service-sponsored transactions on behalf of EOAs with delegated code. //! //! # Restrictions //! //! `odyssey_sendTransaction` has additional verifications in place to prevent some -//! rudimentary abuse of the sequencer's funds. For example, transactions cannot contain any +//! rudimentary abuse of the service's funds. For example, transactions cannot contain any //! `value`. //! //! [eip-5792]: https://eips.ethereum.org/EIPS/eip-5792 @@ -17,31 +16,196 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] -use alloy_eips::BlockId; use alloy_network::{ eip2718::Encodable2718, Ethereum, EthereumWallet, NetworkWallet, TransactionBuilder, }; -use alloy_primitives::{Address, ChainId, TxHash, TxKind, U256}; -use alloy_rpc_types::TransactionRequest; +use alloy_primitives::{Address, Bytes, ChainId, TxHash, TxKind, U256}; +use alloy_provider::{utils::Eip1559Estimation, Provider, WalletProvider}; +use alloy_rpc_types::{BlockId, TransactionRequest}; +use alloy_transport::Transport; use jsonrpsee::{ core::{async_trait, RpcResult}, proc_macros::rpc, }; use metrics::Counter; use metrics_derive::Metrics; + use reth_rpc_eth_api::helpers::{EthCall, EthTransactions, FullEthApi, LoadFee, LoadState}; -use reth_storage_api::{StateProvider, StateProviderFactory}; -use revm_primitives::Bytecode; +use reth_storage_api::StateProviderFactory; use serde::{Deserialize, Serialize}; -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; use tracing::{trace, warn}; use reth_optimism_rpc as _; use tokio::sync::Mutex; -/// The capability to perform [EIP-7702][eip-7702] delegations, sponsored by the sequencer. +/// An upstream is capable of estimating, signing, and propagating signed transactions for a +/// specific chain. +#[async_trait] +pub trait Upstream { + /// Get the address of the account that sponsors transactions. + fn default_signer_address(&self) -> Address; + + /// Get the code at a specific address. + async fn get_code(&self, address: Address) -> Result; + + /// Estimate the transaction request's gas usage and fees. + async fn estimate( + &self, + tx: &TransactionRequest, + ) -> Result<(u64, Eip1559Estimation), OdysseyWalletError>; + + /// Sign the transaction request and send it to the upstream. + async fn sign_and_send(&self, tx: TransactionRequest) -> Result; +} + +/// A wrapper around an Alloy provider for signing and sending sponsored transactions. +#[derive(Debug)] +pub struct AlloyUpstream { + provider: P, + _transport: PhantomData, +} + +impl AlloyUpstream { + /// Create a new [`AlloyUpstream`] + pub const fn new(provider: P) -> Self { + Self { provider, _transport: PhantomData } + } +} + +#[async_trait] +impl Upstream for AlloyUpstream +where + P: Provider + WalletProvider, + T: Transport + Clone, +{ + fn default_signer_address(&self) -> Address { + self.provider.default_signer_address() + } + + async fn get_code(&self, address: Address) -> Result { + self.provider + .get_code_at(address) + .await + .map_err(|err| OdysseyWalletError::InternalError(err.into())) + } + + async fn estimate( + &self, + tx: &TransactionRequest, + ) -> Result<(u64, Eip1559Estimation), OdysseyWalletError> { + let (estimate, fee_estimate) = + tokio::join!(self.provider.estimate_gas(tx), self.provider.estimate_eip1559_fees(None)); + + Ok(( + estimate.map_err(|err| OdysseyWalletError::InternalError(err.into()))?, + fee_estimate.map_err(|err| OdysseyWalletError::InternalError(err.into()))?, + )) + } + + async fn sign_and_send(&self, tx: TransactionRequest) -> Result { + self.provider + .send_transaction(tx) + .await + .map_err(|err| OdysseyWalletError::InternalError(err.into())) + .map(|pending| *pending.tx_hash()) + } +} + +/// A handle to a Reth upstream that signs transactions and injects them directly into the +/// transaction pool. +#[derive(Debug)] +pub struct RethUpstream { + provider: Provider, + eth_api: Eth, + wallet: EthereumWallet, +} + +impl RethUpstream { + /// Create a new [`RethUpstream`]. + pub const fn new(provider: Provider, eth_api: Eth, wallet: EthereumWallet) -> Self { + Self { provider, eth_api, wallet } + } +} + +#[async_trait] +impl Upstream for RethUpstream +where + Provider: StateProviderFactory + Send + Sync, + Eth: FullEthApi + Send + Sync, +{ + fn default_signer_address(&self) -> Address { + NetworkWallet::::default_signer_address(&self.wallet) + } + + async fn get_code(&self, address: Address) -> Result { + let state = + self.provider.latest().map_err(|err| OdysseyWalletError::InternalError(err.into()))?; + + Ok(state + .account_code(address) + .ok() + .flatten() + .map(|code| code.0.bytes()) + .unwrap_or_default()) + } + + async fn estimate( + &self, + tx: &TransactionRequest, + ) -> Result<(u64, Eip1559Estimation), OdysseyWalletError> { + let (estimate, fee_estimate) = tokio::join!( + EthCall::estimate_gas_at(&self.eth_api, tx.clone(), BlockId::latest(), None), + LoadFee::eip1559_fees(&self.eth_api, None, None) + ); + + Ok(( + estimate + .map(|estimate| estimate.to()) + .map_err(|err| OdysseyWalletError::InternalError(eyre::Report::new(err)))?, + fee_estimate + .map(|(base, prio)| Eip1559Estimation { + max_fee_per_gas: (base + prio).to(), + max_priority_fee_per_gas: prio.to(), + }) + .map_err(|err| OdysseyWalletError::InternalError(eyre::Report::new(err)))?, + )) + } + + async fn sign_and_send( + &self, + mut tx: TransactionRequest, + ) -> Result { + let next_nonce = LoadState::next_available_nonce( + &self.eth_api, + NetworkWallet::::default_signer_address(&self.wallet), + ) + .await + .map_err(|err| OdysseyWalletError::InternalError(eyre::Report::new(err)))?; + tx.nonce = Some(next_nonce); + + // build and sign + let envelope = + >::build::( + tx, + &self.wallet, + ) + .await + .map_err(|err| OdysseyWalletError::InternalError(err.into()))?; + + // this uses the internal `OpEthApi` to either forward the tx to the sequencer, or add it to + // the txpool + // + // see: https://github.com/paradigmxyz/reth/blob/b67f004fbe8e1b7c05f84f314c4c9f2ed9be1891/crates/optimism/rpc/src/eth/transaction.rs#L35-L57 + EthTransactions::send_raw_transaction(&self.eth_api, envelope.encoded_2718().into()) + .await + .map_err(|err| OdysseyWalletError::InternalError(eyre::Report::new(err))) + } +} + +/// The capability to perform [EIP-7702][eip-7702] delegations, sponsored by the service. /// -/// The sequencer will only perform delegations, and act on behalf of delegated accounts, if the +/// The service will only perform delegations, and act on behalf of delegated accounts, if the /// account delegates to one of the addresses specified within this capability. /// /// [eip-7702]: https://eips.ethereum.org/EIPS/eip-7702 @@ -55,7 +219,7 @@ pub struct DelegationCapability { #[cfg_attr(not(test), rpc(server, namespace = "wallet"))] #[cfg_attr(test, rpc(server, client, namespace = "wallet"))] pub trait OdysseyWalletApi { - /// Send a sequencer-sponsored transaction. + /// Send a sponsored transaction. /// /// The transaction will only be processed if: /// @@ -64,8 +228,8 @@ pub trait OdysseyWalletApi { /// delegated to one of the addresses above /// - The value in the transaction is exactly 0. /// - /// The sequencer will sign the transaction and inject it into the transaction pool, provided it - /// is valid. The nonce is managed by the sequencer. + /// The service will sign the transaction and inject it into the transaction pool, provided it + /// is valid. The nonce is managed by the service. /// /// [eip-7702]: https://eips.ethereum.org/EIPS/eip-7702 /// [eip-1559]: https://eips.ethereum.org/EIPS/eip-1559 @@ -74,22 +238,22 @@ pub trait OdysseyWalletApi { } /// Errors returned by the wallet API. -#[derive(Debug, Eq, PartialEq, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum OdysseyWalletError { /// The transaction value is not 0. /// - /// The value should be 0 to prevent draining the sequencer. + /// The value should be 0 to prevent draining the service. #[error("tx value not zero")] ValueNotZero, /// The from field is set on the transaction. /// /// Requests with the from field are rejected, since it is implied that it will always be the - /// sequencer. + /// service. #[error("tx from field is set")] FromSet, /// The nonce field is set on the transaction. /// - /// Requests with the nonce field set are rejected, as this is managed by the sequencer. + /// Requests with the nonce field set are rejected, as this is managed by the service. #[error("tx nonce is set")] NonceSet, /// The to field of the transaction was invalid. @@ -102,20 +266,20 @@ pub enum OdysseyWalletError { IllegalDestination, /// The transaction request was invalid. /// - /// This is likely an internal error, as most of the request is built by the sequencer. + /// This is likely an internal error, as most of the request is built by the service. #[error("invalid tx request")] InvalidTransactionRequest, /// The request was estimated to consume too much gas. /// - /// The gas usage by each request is limited to counteract draining the sequencers funds. + /// The gas usage by each request is limited to counteract draining the services funds. #[error("request would use too much gas: estimated {estimate}")] GasEstimateTooHigh { /// The amount of gas the request was estimated to consume. estimate: u64, }, /// An internal error occurred. - #[error("internal error")] - InternalError, + #[error(transparent)] + InternalError(#[from] eyre::Error), } impl From for jsonrpsee::types::error::ErrorObject<'static> { @@ -130,22 +294,15 @@ impl From for jsonrpsee::types::error::ErrorObject<'static> /// Implementation of the Odyssey `wallet_` namespace. #[derive(Debug)] -pub struct OdysseyWallet { - inner: Arc>, +pub struct OdysseyWallet { + inner: Arc>, } -impl OdysseyWallet { +impl OdysseyWallet { /// Create a new Odyssey wallet module. - pub fn new( - provider: Provider, - wallet: EthereumWallet, - eth_api: Eth, - chain_id: ChainId, - ) -> Self { + pub fn new(upstream: T, chain_id: ChainId) -> Self { let inner = OdysseyWalletInner { - provider, - wallet, - eth_api, + upstream, chain_id, permit: Default::default(), metrics: WalletMetrics::default(), @@ -159,10 +316,9 @@ impl OdysseyWallet { } #[async_trait] -impl OdysseyWalletApiServer for OdysseyWallet +impl OdysseyWalletApiServer for OdysseyWallet where - Provider: StateProviderFactory + Send + Sync + 'static, - Eth: FullEthApi + Send + Sync + 'static, + T: Upstream + Sync + Send + 'static, { async fn send_transaction(&self, mut request: TransactionRequest) -> RpcResult { trace!(target: "rpc::wallet", ?request, "Serving odyssey_sendTransaction"); @@ -178,24 +334,22 @@ where // if this is an eip-1559 tx, ensure that it is an account that delegates to a // whitelisted address (false, Some(TxKind::Call(addr))) => { - let state = self.inner.provider.latest().map_err(|_| { - self.inner.metrics.invalid_send_transaction_calls.increment(1); - OdysseyWalletError::InternalError - })?; - let delegated_address = state - .account_code(addr) - .ok() - .flatten() - .and_then(|code| match code.0 { - Bytecode::Eip7702(code) => Some(code.address()), - _ => None, - }) - .unwrap_or_default(); - - // not eip-7702 bytecode - if delegated_address == Address::ZERO { - self.inner.metrics.invalid_send_transaction_calls.increment(1); - return Err(OdysseyWalletError::IllegalDestination.into()); + let code = self.inner.upstream.get_code(addr).await?; + match code.as_ref() { + // A valid EIP-7702 delegation + [0xef, 0x01, 0x00, address @ ..] => { + let addr = Address::from_slice(address); + // the delegation was cleared + if addr.is_zero() { + self.inner.metrics.invalid_send_transaction_calls.increment(1); + return Err(OdysseyWalletError::IllegalDestination.into()); + } + } + // Not an EIP-7702 delegation, or an empty (cleared) delegation + _ => { + self.inner.metrics.invalid_send_transaction_calls.increment(1); + return Err(OdysseyWalletError::IllegalDestination.into()); + } } } // if it's an eip-7702 tx, let it through @@ -210,82 +364,43 @@ where // we acquire the permit here so that all following operations are performed exclusively let _permit = self.inner.permit.lock().await; - // set nonce - let next_nonce = LoadState::next_available_nonce( - &self.inner.eth_api, - NetworkWallet::::default_signer_address(&self.inner.wallet), - ) - .await - .map_err(|err| { - self.inner.metrics.invalid_send_transaction_calls.increment(1); - err.into() - })?; - request.nonce = Some(next_nonce); - // set chain id request.chain_id = Some(self.chain_id()); // set gas limit // note: we also set the `from` field here to correctly estimate for contracts that use e.g. // `tx.origin` - request.from = Some(NetworkWallet::::default_signer_address(&self.inner.wallet)); - let (estimate, base_fee) = tokio::join!( - EthCall::estimate_gas_at(&self.inner.eth_api, request.clone(), BlockId::latest(), None), - LoadFee::eip1559_fees(&self.inner.eth_api, None, None) - ); - let estimate = estimate.map_err(|err| { - self.inner.metrics.invalid_send_transaction_calls.increment(1); - err.into() - })?; - - if estimate >= U256::from(350_000) { + request.from = Some(self.inner.upstream.default_signer_address()); + let (estimate, fee_estimate) = self + .inner + .upstream + .estimate(&request) + .await + .inspect_err(|_| self.inner.metrics.invalid_send_transaction_calls.increment(1))?; + if estimate >= 350_000 { self.inner.metrics.invalid_send_transaction_calls.increment(1); - return Err(OdysseyWalletError::GasEstimateTooHigh { estimate: estimate.to() }.into()); + return Err(OdysseyWalletError::GasEstimateTooHigh { estimate }.into()); } - request.gas = Some(estimate.to()); + request.gas = Some(estimate); // set gas price - let (base_fee, _) = base_fee.map_err(|_| { - self.inner.metrics.invalid_send_transaction_calls.increment(1); - OdysseyWalletError::InvalidTransactionRequest - })?; - let max_priority_fee_per_gas = 1_000_000_000; // 1 gwei - request.max_fee_per_gas = Some(base_fee.to::() + max_priority_fee_per_gas); - request.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + request.max_fee_per_gas = Some(fee_estimate.max_fee_per_gas); + request.max_priority_fee_per_gas = Some(fee_estimate.max_priority_fee_per_gas); request.gas_price = None; - // build and sign - let envelope = - >::build::( - request, - &self.inner.wallet, - ) - .await - .map_err(|_| { - self.inner.metrics.invalid_send_transaction_calls.increment(1); - OdysseyWalletError::InvalidTransactionRequest - })?; - // all checks passed, increment the valid calls counter self.inner.metrics.valid_send_transaction_calls.increment(1); - // this uses the internal `OpEthApi` to either forward the tx to the sequencer, or add it to - // the txpool - // - // see: https://github.com/paradigmxyz/reth/blob/b67f004fbe8e1b7c05f84f314c4c9f2ed9be1891/crates/optimism/rpc/src/eth/transaction.rs#L35-L57 - EthTransactions::send_raw_transaction(&self.inner.eth_api, envelope.encoded_2718().into()) - .await - .inspect_err(|err| warn!(target: "rpc::wallet", ?err, "Error adding sequencer-sponsored tx to pool")) - .map_err(Into::into) + Ok(self.inner.upstream.sign_and_send(request).await.inspect_err( + |err| warn!(target: "rpc::wallet", ?err, "Error adding sponsored tx to pool"), + )?) } } /// Implementation of the Odyssey `wallet_` namespace. #[derive(Debug)] -struct OdysseyWalletInner { - provider: Provider, - eth_api: Eth, - wallet: EthereumWallet, +struct OdysseyWalletInner { + upstream: T, chain_id: ChainId, /// Used to guard tx signing permit: Mutex<()>, @@ -294,17 +409,17 @@ struct OdysseyWalletInner { } fn validate_tx_request(request: &TransactionRequest) -> Result<(), OdysseyWalletError> { - // reject transactions that have a non-zero value to prevent draining the sequencer. + // reject transactions that have a non-zero value to prevent draining the service. if request.value.is_some_and(|val| val > U256::ZERO) { return Err(OdysseyWalletError::ValueNotZero); } - // reject transactions that have from set, as this will be the sequencer. + // reject transactions that have from set, as this will be the service. if request.from.is_some() { return Err(OdysseyWalletError::FromSet); } - // reject transaction requests that have nonce set, as this is managed by the sequencer. + // reject transaction requests that have nonce set, as this is managed by the service. if request.nonce.is_some() { return Err(OdysseyWalletError::NonceSet); } @@ -327,36 +442,37 @@ mod tests { use crate::{validate_tx_request, OdysseyWalletError}; use alloy_primitives::{Address, U256}; use alloy_rpc_types::TransactionRequest; + #[test] fn no_value_allowed() { - assert_eq!( + assert!(matches!( validate_tx_request(&TransactionRequest::default().value(U256::from(1))), Err(OdysseyWalletError::ValueNotZero) - ); + )); - assert_eq!( + assert!(matches!( validate_tx_request(&TransactionRequest::default().value(U256::from(0))), Ok(()) - ); + )); } #[test] fn no_from_allowed() { - assert_eq!( + assert!(matches!( validate_tx_request(&TransactionRequest::default().from(Address::ZERO)), Err(OdysseyWalletError::FromSet) - ); + )); - assert_eq!(validate_tx_request(&TransactionRequest::default()), Ok(())); + assert!(matches!(validate_tx_request(&TransactionRequest::default()), Ok(()))); } #[test] fn no_nonce_allowed() { - assert_eq!( + assert!(matches!( validate_tx_request(&TransactionRequest::default().nonce(1)), Err(OdysseyWalletError::NonceSet) - ); + )); - assert_eq!(validate_tx_request(&TransactionRequest::default()), Ok(())); + assert!(matches!(validate_tx_request(&TransactionRequest::default()), Ok(()))); } } diff --git a/deny.toml b/deny.toml index 8d7bf3e..3b60910 100644 --- a/deny.toml +++ b/deny.toml @@ -9,6 +9,7 @@ notice = "warn" multiple-versions = "warn" wildcards = "deny" highlight = "all" +deny = [{ name = "openssl" }] [licenses] unlicensed = "deny"