From fafcabf68a4d44429dfbeb4e8cee6998028a6e37 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 20 Oct 2023 17:16:33 +0200 Subject: [PATCH] feat: add call bundle (#5100) --- crates/primitives/src/transaction/pooled.rs | 13 +- crates/rpc/rpc-types/src/mev.rs | 22 ++- crates/rpc/rpc/src/eth/bundle.rs | 206 ++++++++++++++++++++ crates/rpc/rpc/src/eth/mod.rs | 2 + 4 files changed, 229 insertions(+), 14 deletions(-) create mode 100644 crates/rpc/rpc/src/eth/bundle.rs diff --git a/crates/primitives/src/transaction/pooled.rs b/crates/primitives/src/transaction/pooled.rs index 493606c84..005f36a05 100644 --- a/crates/primitives/src/transaction/pooled.rs +++ b/crates/primitives/src/transaction/pooled.rs @@ -1,9 +1,8 @@ //! Defines the types for blob transactions, legacy, and other EIP-2718 transactions included in a //! response to `GetPooledTransactions`. use crate::{ - Address, BlobTransaction, BlobTransactionSidecar, Bytes, Signature, Transaction, - TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxHash, TxLegacy, B256, - EIP4844_TX_TYPE_ID, + Address, BlobTransaction, Bytes, Signature, Transaction, TransactionSigned, + TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxHash, TxLegacy, B256, EIP4844_TX_TYPE_ID, }; use alloy_rlp::{Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE}; use bytes::Buf; @@ -430,7 +429,7 @@ impl<'a> arbitrary::Arbitrary<'a> for PooledTransactionsElement { // generate a sidecar for blob txs if let PooledTransactionsElement::BlobTransaction(mut tx) = pooled_txs_element { - tx.sidecar = BlobTransactionSidecar::arbitrary(u)?; + tx.sidecar = crate::BlobTransactionSidecar::arbitrary(u)?; Ok(PooledTransactionsElement::BlobTransaction(tx)) } else { Ok(pooled_txs_element) @@ -441,12 +440,10 @@ impl<'a> arbitrary::Arbitrary<'a> for PooledTransactionsElement { #[cfg(any(test, feature = "arbitrary"))] impl proptest::arbitrary::Arbitrary for PooledTransactionsElement { type Parameters = (); - type Strategy = proptest::strategy::BoxedStrategy; - fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { use proptest::prelude::{any, Strategy}; - any::<(TransactionSigned, BlobTransactionSidecar)>() + any::<(TransactionSigned, crate::BlobTransactionSidecar)>() .prop_map(move |(transaction, sidecar)| { // this will have an empty sidecar let pooled_txs_element = PooledTransactionsElement::from(transaction); @@ -461,6 +458,8 @@ impl proptest::arbitrary::Arbitrary for PooledTransactionsElement { }) .boxed() } + + type Strategy = proptest::strategy::BoxedStrategy; } /// A signed pooled transaction with recovered signer. diff --git a/crates/rpc/rpc-types/src/mev.rs b/crates/rpc/rpc-types/src/mev.rs index 6194d9606..8b082927a 100644 --- a/crates/rpc/rpc-types/src/mev.rs +++ b/crates/rpc/rpc-types/src/mev.rs @@ -1,7 +1,8 @@ -//! MEV-share bundle type bindings +//! MEV bundle type bindings + #![allow(missing_docs)] -use alloy_primitives::{Address, BlockNumber, Bytes, TxHash, B256, U256, U64}; -use reth_primitives::{BlockId, Log}; +use alloy_primitives::{Address, Bytes, TxHash, B256, U256, U64}; +use reth_primitives::{BlockId, BlockNumberOrTag, Log}; use serde::{ ser::{SerializeSeq, Serializer}, Deserialize, Deserializer, Serialize, @@ -603,7 +604,7 @@ pub struct EthCallBundle { /// hex encoded block number for which this bundle is valid on pub block_number: U64, /// Either a hex encoded number or a block tag for which state to base this simulation on - pub state_block_number: BlockNumber, + pub state_block_number: BlockNumberOrTag, /// the timestamp to use for this bundle simulation, in seconds since the unix epoch #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, @@ -613,9 +614,9 @@ pub struct EthCallBundle { #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EthCallBundleResponse { + pub bundle_hash: B256, #[serde(with = "u256_numeric_string")] pub bundle_gas_price: U256, - pub bundle_hash: String, #[serde(with = "u256_numeric_string")] pub coinbase_diff: U256, #[serde(with = "u256_numeric_string")] @@ -641,9 +642,16 @@ pub struct EthCallBundleTransactionResult { #[serde(with = "u256_numeric_string")] pub gas_price: U256, pub gas_used: u64, - pub to_address: Address, + pub to_address: Option
, pub tx_hash: B256, - pub value: Bytes, + /// Contains the return data if the transaction succeeded + /// + /// Note: this is mutually exclusive with `revert` + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Contains the return data if the transaction reverted + #[serde(skip_serializing_if = "Option::is_none")] + pub revert: Option, } mod u256_numeric_string { diff --git a/crates/rpc/rpc/src/eth/bundle.rs b/crates/rpc/rpc/src/eth/bundle.rs new file mode 100644 index 000000000..a4de776bd --- /dev/null +++ b/crates/rpc/rpc/src/eth/bundle.rs @@ -0,0 +1,206 @@ +//! `Eth` bundle implementation and helpers. + +use crate::{ + eth::{ + error::{EthApiError, EthResult, RpcInvalidTransactionError}, + revm_utils::FillableTransaction, + utils::recover_raw_transaction, + EthTransactions, + }, + BlockingTaskGuard, +}; +use reth_primitives::{keccak256, U256}; +use reth_revm::database::StateProviderDatabase; +use reth_rpc_types::{EthCallBundle, EthCallBundleResponse, EthCallBundleTransactionResult}; +use revm::{ + db::CacheDB, + primitives::{Env, ResultAndState, TxEnv}, +}; +use revm_primitives::db::{DatabaseCommit, DatabaseRef}; +use std::sync::Arc; + +/// `Eth` bundle implementation. +pub struct EthBundle { + /// All nested fields bundled together. + inner: Arc>, +} + +impl EthBundle { + /// Create a new `EthBundle` instance. + pub fn new(eth_api: Eth, blocking_task_guard: BlockingTaskGuard) -> Self { + Self { inner: Arc::new(EthBundleInner { eth_api, blocking_task_guard }) } + } +} + +impl EthBundle +where + Eth: EthTransactions + 'static, +{ + /// Simulates a bundle of transactions at the top of a given block number with the state of + /// another (or the same) block. This can be used to simulate future blocks with the current + /// state, or it can be used to simulate a past block. The sender is responsible for signing the + /// transactions and using the correct nonce and ensuring validity + pub async fn call_bundle(&self, bundle: EthCallBundle) -> EthResult { + let EthCallBundle { txs, block_number, state_block_number, timestamp } = bundle; + if txs.is_empty() { + return Err(EthApiError::InvalidParams( + EthBundleError::EmptyBundleTransactions.to_string(), + )) + } + if block_number.to::() == 0 { + return Err(EthApiError::InvalidParams( + EthBundleError::BundleMissingBlockNumber.to_string(), + )) + } + + let transactions = + txs.into_iter().map(recover_raw_transaction).collect::, _>>()?; + + let (cfg, mut block_env, at) = + self.inner.eth_api.evm_env_at(state_block_number.into()).await?; + + // need to adjust the timestamp for the next block + if let Some(timestamp) = timestamp { + block_env.timestamp = U256::from(timestamp); + } else { + block_env.timestamp += U256::from(12); + } + + let state_block_number = block_env.number; + // use the block number of the request + block_env.number = U256::from(block_number); + + self.inner + .eth_api + .spawn_with_state_at_block(at, move |state| { + let coinbase = block_env.coinbase; + let basefee = Some(block_env.basefee.to::()); + let env = Env { cfg, block: block_env, tx: TxEnv::default() }; + let db = CacheDB::new(StateProviderDatabase::new(state)); + + let initial_coinbase = + DatabaseRef::basic(&db, coinbase)?.map(|acc| acc.balance).unwrap_or_default(); + let mut coinbase_balance_before_tx = initial_coinbase; + let mut coinbase_balance_after_tx = initial_coinbase; + let mut total_gas_used = 0u64; + let mut total_gas_fess = U256::ZERO; + let mut hash_bytes = Vec::with_capacity(32 * transactions.len()); + + let mut evm = revm::EVM::with_env(env); + evm.database(db); + + let mut results = Vec::with_capacity(transactions.len()); + let mut transactions = transactions.into_iter().peekable(); + + while let Some(tx) = transactions.next() { + let tx = tx.into_ecrecovered_transaction(); + hash_bytes.extend_from_slice(tx.hash().as_slice()); + let gas_price = tx + .effective_gas_tip(basefee) + .ok_or_else(|| RpcInvalidTransactionError::FeeCapTooLow)?; + tx.try_fill_tx_env(&mut evm.env.tx)?; + let ResultAndState { result, state } = evm.transact()?; + + let gas_used = result.gas_used(); + total_gas_used += gas_used; + + let gas_fees = U256::from(gas_used) * U256::from(gas_price); + total_gas_fess += gas_fees; + + // coinbase is always present in the result state + coinbase_balance_after_tx = + state.get(&coinbase).map(|acc| acc.info.balance).unwrap_or_default(); + let coinbase_diff = + coinbase_balance_after_tx.saturating_sub(coinbase_balance_before_tx); + let eth_sent_to_coinbase = coinbase_diff.saturating_sub(gas_fees); + + // update the coinbase balance + coinbase_balance_before_tx = coinbase_balance_after_tx; + + // set the return data for the response + let (value, revert) = if result.is_success() { + let value = result.into_output().unwrap_or_default(); + (Some(value), None) + } else { + let revert = result.into_output().unwrap_or_default(); + (None, Some(revert)) + }; + + let tx_res = EthCallBundleTransactionResult { + coinbase_diff, + eth_sent_to_coinbase, + from_address: tx.signer(), + gas_fees, + gas_price: U256::from(gas_price), + gas_used, + to_address: tx.to(), + tx_hash: tx.hash(), + value, + revert, + }; + results.push(tx_res); + + // need to apply the state changes of this call before executing the + // next call + if transactions.peek().is_some() { + // need to apply the state changes of this call before executing + // the next call + evm.db.as_mut().expect("is set").commit(state) + } + } + + // populate the response + + let coinbase_diff = coinbase_balance_after_tx.saturating_sub(initial_coinbase); + let eth_sent_to_coinbase = coinbase_diff.saturating_sub(total_gas_fess); + let bundle_gas_price = + coinbase_diff.checked_div(U256::from(total_gas_used)).unwrap_or_default(); + let res = EthCallBundleResponse { + bundle_gas_price, + bundle_hash: keccak256(&hash_bytes), + coinbase_diff, + eth_sent_to_coinbase, + gas_fees: total_gas_fess, + results, + state_block_number: state_block_number.to(), + total_gas_used, + }; + + Ok(res) + }) + .await + } +} + +/// Container type for `EthBundle` internals +#[derive(Debug)] +struct EthBundleInner { + /// Access to commonly used code of the `eth` namespace + eth_api: Eth, + // restrict the number of concurrent tracing calls. + #[allow(unused)] + blocking_task_guard: BlockingTaskGuard, +} + +impl std::fmt::Debug for EthBundle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EthBundle").finish_non_exhaustive() + } +} + +impl Clone for EthBundle { + fn clone(&self) -> Self { + Self { inner: Arc::clone(&self.inner) } + } +} + +/// [EthBundle] specific errors. +#[derive(Debug, thiserror::Error)] +pub enum EthBundleError { + /// Thrown if the bundle does not contain any transactions. + #[error("bundle missing txs")] + EmptyBundleTransactions, + /// Thrown if the bundle does not contain a block number, or block number is 0. + #[error("bundle missing blockNumber")] + BundleMissingBlockNumber, +} diff --git a/crates/rpc/rpc/src/eth/mod.rs b/crates/rpc/rpc/src/eth/mod.rs index 0a9efc560..77baa36e3 100644 --- a/crates/rpc/rpc/src/eth/mod.rs +++ b/crates/rpc/rpc/src/eth/mod.rs @@ -1,6 +1,7 @@ //! `eth` namespace handler implementation. mod api; +pub mod bundle; pub mod cache; pub mod error; mod filter; @@ -13,6 +14,7 @@ mod signer; pub(crate) mod utils; pub use api::{EthApi, EthApiSpec, EthTransactions, TransactionSource, RPC_DEFAULT_GAS_CAP}; +pub use bundle::EthBundle; pub use filter::EthFilter; pub use id_provider::EthSubscriptionIdProvider; pub use pubsub::EthPubSub;