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
|
//! Defines the types for blob transactions, legacy, and other EIP-2718 transactions included in a
|
||||||
//! response to `GetPooledTransactions`.
|
//! response to `GetPooledTransactions`.
|
||||||
use crate::{
|
use crate::{
|
||||||
Address, BlobTransaction, BlobTransactionSidecar, Bytes, Signature, Transaction,
|
Address, BlobTransaction, Bytes, Signature, Transaction, TransactionSigned,
|
||||||
TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxHash, TxLegacy, B256,
|
TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxHash, TxLegacy, B256, EIP4844_TX_TYPE_ID,
|
||||||
EIP4844_TX_TYPE_ID,
|
|
||||||
};
|
};
|
||||||
use alloy_rlp::{Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE};
|
use alloy_rlp::{Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE};
|
||||||
use bytes::Buf;
|
use bytes::Buf;
|
||||||
@ -430,7 +429,7 @@ impl<'a> arbitrary::Arbitrary<'a> for PooledTransactionsElement {
|
|||||||
|
|
||||||
// generate a sidecar for blob txs
|
// generate a sidecar for blob txs
|
||||||
if let PooledTransactionsElement::BlobTransaction(mut tx) = pooled_txs_element {
|
if let PooledTransactionsElement::BlobTransaction(mut tx) = pooled_txs_element {
|
||||||
tx.sidecar = BlobTransactionSidecar::arbitrary(u)?;
|
tx.sidecar = crate::BlobTransactionSidecar::arbitrary(u)?;
|
||||||
Ok(PooledTransactionsElement::BlobTransaction(tx))
|
Ok(PooledTransactionsElement::BlobTransaction(tx))
|
||||||
} else {
|
} else {
|
||||||
Ok(pooled_txs_element)
|
Ok(pooled_txs_element)
|
||||||
@ -441,12 +440,10 @@ impl<'a> arbitrary::Arbitrary<'a> for PooledTransactionsElement {
|
|||||||
#[cfg(any(test, feature = "arbitrary"))]
|
#[cfg(any(test, feature = "arbitrary"))]
|
||||||
impl proptest::arbitrary::Arbitrary for PooledTransactionsElement {
|
impl proptest::arbitrary::Arbitrary for PooledTransactionsElement {
|
||||||
type Parameters = ();
|
type Parameters = ();
|
||||||
type Strategy = proptest::strategy::BoxedStrategy<PooledTransactionsElement>;
|
|
||||||
|
|
||||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||||
use proptest::prelude::{any, Strategy};
|
use proptest::prelude::{any, Strategy};
|
||||||
|
|
||||||
any::<(TransactionSigned, BlobTransactionSidecar)>()
|
any::<(TransactionSigned, crate::BlobTransactionSidecar)>()
|
||||||
.prop_map(move |(transaction, sidecar)| {
|
.prop_map(move |(transaction, sidecar)| {
|
||||||
// this will have an empty sidecar
|
// this will have an empty sidecar
|
||||||
let pooled_txs_element = PooledTransactionsElement::from(transaction);
|
let pooled_txs_element = PooledTransactionsElement::from(transaction);
|
||||||
@ -461,6 +458,8 @@ impl proptest::arbitrary::Arbitrary for PooledTransactionsElement {
|
|||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Strategy = proptest::strategy::BoxedStrategy<PooledTransactionsElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A signed pooled transaction with recovered signer.
|
/// A signed pooled transaction with recovered signer.
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
//! MEV-share bundle type bindings
|
//! MEV bundle type bindings
|
||||||
|
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
use alloy_primitives::{Address, BlockNumber, Bytes, TxHash, B256, U256, U64};
|
use alloy_primitives::{Address, Bytes, TxHash, B256, U256, U64};
|
||||||
use reth_primitives::{BlockId, Log};
|
use reth_primitives::{BlockId, BlockNumberOrTag, Log};
|
||||||
use serde::{
|
use serde::{
|
||||||
ser::{SerializeSeq, Serializer},
|
ser::{SerializeSeq, Serializer},
|
||||||
Deserialize, Deserializer, Serialize,
|
Deserialize, Deserializer, Serialize,
|
||||||
@ -603,7 +604,7 @@ pub struct EthCallBundle {
|
|||||||
/// hex encoded block number for which this bundle is valid on
|
/// hex encoded block number for which this bundle is valid on
|
||||||
pub block_number: U64,
|
pub block_number: U64,
|
||||||
/// Either a hex encoded number or a block tag for which state to base this simulation on
|
/// 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
|
/// the timestamp to use for this bundle simulation, in seconds since the unix epoch
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub timestamp: Option<u64>,
|
pub timestamp: Option<u64>,
|
||||||
@ -613,9 +614,9 @@ pub struct EthCallBundle {
|
|||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EthCallBundleResponse {
|
pub struct EthCallBundleResponse {
|
||||||
|
pub bundle_hash: B256,
|
||||||
#[serde(with = "u256_numeric_string")]
|
#[serde(with = "u256_numeric_string")]
|
||||||
pub bundle_gas_price: U256,
|
pub bundle_gas_price: U256,
|
||||||
pub bundle_hash: String,
|
|
||||||
#[serde(with = "u256_numeric_string")]
|
#[serde(with = "u256_numeric_string")]
|
||||||
pub coinbase_diff: U256,
|
pub coinbase_diff: U256,
|
||||||
#[serde(with = "u256_numeric_string")]
|
#[serde(with = "u256_numeric_string")]
|
||||||
@ -641,9 +642,16 @@ pub struct EthCallBundleTransactionResult {
|
|||||||
#[serde(with = "u256_numeric_string")]
|
#[serde(with = "u256_numeric_string")]
|
||||||
pub gas_price: U256,
|
pub gas_price: U256,
|
||||||
pub gas_used: u64,
|
pub gas_used: u64,
|
||||||
pub to_address: Address,
|
pub to_address: Option<Address>,
|
||||||
pub tx_hash: B256,
|
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 {
|
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.
|
//! `eth` namespace handler implementation.
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
pub mod bundle;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod filter;
|
mod filter;
|
||||||
@ -13,6 +14,7 @@ mod signer;
|
|||||||
pub(crate) mod utils;
|
pub(crate) mod utils;
|
||||||
|
|
||||||
pub use api::{EthApi, EthApiSpec, EthTransactions, TransactionSource, RPC_DEFAULT_GAS_CAP};
|
pub use api::{EthApi, EthApiSpec, EthTransactions, TransactionSource, RPC_DEFAULT_GAS_CAP};
|
||||||
|
pub use bundle::EthBundle;
|
||||||
pub use filter::EthFilter;
|
pub use filter::EthFilter;
|
||||||
pub use id_provider::EthSubscriptionIdProvider;
|
pub use id_provider::EthSubscriptionIdProvider;
|
||||||
pub use pubsub::EthPubSub;
|
pub use pubsub::EthPubSub;
|
||||||
|
|||||||
Reference in New Issue
Block a user