mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
feat: add call bundle (#5100)
This commit is contained in:
@ -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<PooledTransactionsElement>;
|
||||
|
||||
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<PooledTransactionsElement>;
|
||||
}
|
||||
|
||||
/// A signed pooled transaction with recovered signer.
|
||||
|
||||
@ -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<u64>,
|
||||
@ -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<Address>,
|
||||
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<Bytes>,
|
||||
/// Contains the return data if the transaction reverted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub revert: Option<Bytes>,
|
||||
}
|
||||
|
||||
mod u256_numeric_string {
|
||||
|
||||
206
crates/rpc/rpc/src/eth/bundle.rs
Normal file
206
crates/rpc/rpc/src/eth/bundle.rs
Normal file
@ -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<Eth> {
|
||||
/// All nested fields bundled together.
|
||||
inner: Arc<EthBundleInner<Eth>>,
|
||||
}
|
||||
|
||||
impl<Eth> EthBundle<Eth> {
|
||||
/// 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<Eth> EthBundle<Eth>
|
||||
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<EthCallBundleResponse> {
|
||||
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::<u64>() == 0 {
|
||||
return Err(EthApiError::InvalidParams(
|
||||
EthBundleError::BundleMissingBlockNumber.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
let transactions =
|
||||
txs.into_iter().map(recover_raw_transaction).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
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::<u64>());
|
||||
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<Eth> {
|
||||
/// 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<Eth> std::fmt::Debug for EthBundle<Eth> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EthBundle").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Eth> Clone for EthBundle<Eth> {
|
||||
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,
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user