Skip to content

perf: precompile cache #539

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 28 additions & 29 deletions crates/rbuilder/src/building/mod.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
pub mod block_orders;
pub mod builders;
pub mod built_block_trace;
#[cfg(test)]
pub mod conflict;
pub mod evm_inspector;
pub mod fmt;
pub mod order_commit;
pub mod payout_tx;
pub mod sim;
pub mod testing;
pub mod tracers;
use alloy_consensus::{Header, EMPTY_OMMER_ROOT_HASH};
use alloy_primitives::{Address, Bytes, U256};
use builders::mock_block_building_helper::MockRootHasher;
use reth_primitives::BlockBody;
use reth_primitives_traits::{proofs, Block as _};

use crate::{
live_builder::{block_list_provider::BlockList, payload_events::InternalPayloadId},
primitives::{Order, OrderId, SimValue, SimulatedOrder, TransactionSignedEcRecoveredWithBlobs},
provider::RootHasher,
roothash::RootHashError,
utils::{a2r_withdrawal, default_cfg_env, timestamp_as_u64, Signer},
};
use alloy_consensus::{Header, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::{
eip1559::{calculate_block_gas_limit, ETHEREUM_BLOCK_GAS_LIMIT_30M},
eip4844::BlobTransactionSidecar,
Expand All @@ -32,9 +15,10 @@ use alloy_eips::{
merge::BEACON_NONCE,
};
use alloy_evm::{block::system_calls::SystemCaller, env::EvmEnv, eth::eip6110};
use alloy_primitives::B256;
use alloy_primitives::{Address, Bytes, B256, U256};
use alloy_rpc_types_beacon::events::PayloadAttributesEvent;
use jsonrpsee::core::Serialize;
use precompile_cache::EthCachedEvmFactory;
use reth::{
payload::PayloadId,
primitives::{Block, Receipt, SealedBlock},
Expand All @@ -43,10 +27,12 @@ use reth::{
};
use reth_chainspec::{ChainSpec, EthereumHardforks};
use reth_errors::{BlockExecutionError, BlockValidationError, ProviderError};
use reth_evm::{ConfigureEvm, EthEvmFactory, NextBlockEnvAttributes};
use reth_evm::{ConfigureEvm, NextBlockEnvAttributes};
use reth_evm_ethereum::{revm_spec_by_timestamp_and_block_number, EthEvmConfig};
use reth_node_api::{EngineApiMessageVersion, PayloadBuilderAttributes};
use reth_payload_builder::EthPayloadBuilderAttributes;
use reth_primitives::BlockBody;
use reth_primitives_traits::{proofs, Block as _};
use revm::{
context::BlockEnv,
context_interface::{block::BlobExcessGasAndPrice, result::InvalidTransaction},
Expand All @@ -64,18 +50,31 @@ use std::{
use thiserror::Error;
use time::OffsetDateTime;

use self::tracers::SimulationTracer;
pub use block_orders::*;
pub use built_block_trace::*;
pub mod block_orders;
pub mod builders;
pub mod built_block_trace;
#[cfg(test)]
pub mod conflict;
pub mod evm_inspector;
pub mod fmt;
pub mod order_commit;
pub mod payout_tx;
pub mod precompile_cache;
pub mod sim;
pub mod testing;
pub mod tracers;

pub use self::{
block_orders::*, builders::mock_block_building_helper::MockRootHasher, built_block_trace::*,
order_commit::*, payout_tx::*, sim::simulate_order, tracers::SimulationTracer,
};

#[cfg(test)]
pub use conflict::*;
pub use order_commit::*;
pub use payout_tx::*;
pub use sim::simulate_order;

#[derive(Debug, Clone)]
pub struct BlockBuildingContext {
pub evm_factory: EthEvmFactory,
pub evm_factory: EthCachedEvmFactory,
pub evm_env: EvmEnv,
pub attributes: EthPayloadBuilderAttributes,
pub chain_spec: Arc<ChainSpec>,
Expand Down Expand Up @@ -164,7 +163,7 @@ impl BlockBuildingContext {
)
});
Some(BlockBuildingContext {
evm_factory: EthEvmFactory::default(),
evm_factory: EthCachedEvmFactory::default(),
evm_env,
attributes,
chain_spec,
Expand Down Expand Up @@ -247,7 +246,7 @@ impl BlockBuildingContext {
)
});
BlockBuildingContext {
evm_factory: EthEvmFactory::default(),
evm_factory: EthCachedEvmFactory::default(),
evm_env,
attributes,
chain_spec,
Expand Down
22 changes: 16 additions & 6 deletions crates/rbuilder/src/building/order_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ use alloy_eips::eip4844::{DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK};
use alloy_primitives::{Address, B256, U256};
use reth::revm::{cached::CachedReads, database::StateProviderDatabase};
use reth_errors::ProviderError;
use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
use reth_evm::{Evm, EvmEnv, EvmFactory};
use reth_primitives::Receipt;
use reth_provider::{StateProvider, StateProviderBox};
use revm::{
context::result::ResultAndState,
context::{
result::{HaltReason, ResultAndState},
TxEnv,
},
context_interface::result::{EVMError, ExecutionResult, InvalidTransaction},
database::{states::bundle_state::BundleRetention, BundleState, State, WrapDatabaseRef},
Database, DatabaseCommit,
Expand Down Expand Up @@ -1213,14 +1216,21 @@ fn update_nonce_list_with_updates(
///
/// Gas checks must be done before calling this methods
/// thats why it can't return `TransactionErr::GasLeft` and `TransactionErr::BlobGasLeft`
fn execute_evm(
evm_factory: &EthEvmFactory,
evm_env: EvmEnv,
fn execute_evm<Factory>(
evm_factory: &Factory,
evm_env: EvmEnv<Factory::Spec>,
tx_with_blobs: &TransactionSignedEcRecoveredWithBlobs,
used_state_tracer: Option<&mut UsedStateTrace>,
db: impl Database<Error = ProviderError>,
blocklist: &HashSet<Address>,
) -> Result<Result<ResultAndState, TransactionErr>, CriticalCommitOrderError> {
) -> Result<Result<ResultAndState, TransactionErr>, CriticalCommitOrderError>
where
Factory: EvmFactory<
Tx = TxEnv,
HaltReason = HaltReason,
Error<ProviderError> = EVMError<ProviderError>,
>,
{
let tx = tx_with_blobs.internal_tx_unsecure();
let mut rbuilder_inspector = RBuilderEVMInspector::new(tx, used_state_tracer);

Expand Down
164 changes: 164 additions & 0 deletions crates/rbuilder/src/building/precompile_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use crate::telemetry::{inc_precompile_cache_hits, inc_precompile_cache_misses};
use ahash::HashMap;
use alloy_primitives::{Address, Bytes};
use derive_more::{Deref, DerefMut};
use lru::LruCache;
use parking_lot::Mutex;
use reth_evm::{eth::EthEvmContext, EthEvm, EthEvmFactory, EvmEnv, EvmFactory};
use revm::{
context::{
result::{EVMError, HaltReason},
BlockEnv, Cfg, CfgEnv, ContextTr, TxEnv,
},
handler::{EthPrecompiles, PrecompileProvider},
inspector::NoOpInspector,
interpreter::{interpreter::EthInterpreter, InterpreterResult},
primitives::hardfork::SpecId,
Context, Database, Inspector,
};
use std::{num::NonZeroUsize, sync::Arc};

/// A precompile cache that stores precompile call results by precompile address.
#[derive(Deref, DerefMut, Default, Debug)]
pub struct PrecompileCache(HashMap<Address, PrecompileResultCache>);

/// Precompile result LRU cache stored by `(spec id, input, gas limit)` key.
pub type PrecompileResultCache = LruCache<(SpecId, Bytes, u64), Result<InterpreterResult, String>>;

/// A custom precompile that contains the cache and precompile it wraps.
#[derive(Clone)]
pub struct WrappedPrecompile<P> {
/// The precompile to wrap.
precompile: P,
/// The cache to use.
cache: Arc<Mutex<PrecompileCache>>,
/// The spec id to use.
spec: SpecId,
}

impl<P> WrappedPrecompile<P> {
/// Given a [`PrecompileProvider`] and cache for a specific precompiles, create a
/// wrapper that can be used inside Evm.
pub fn new(precompile: P, cache: Arc<Mutex<PrecompileCache>>) -> Self {
WrappedPrecompile {
precompile,
cache: cache.clone(),
spec: SpecId::LATEST,
}
}
}

impl<CTX: ContextTr, P: PrecompileProvider<CTX, Output = InterpreterResult>> PrecompileProvider<CTX>
for WrappedPrecompile<P>
{
type Output = P::Output;

fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) {
self.precompile.set_spec(spec.clone());
self.spec = spec.into();
}

fn run(
&mut self,
context: &mut CTX,
address: &Address,
bytes: &Bytes,
gas_limit: u64,
) -> Result<Option<Self::Output>, String> {
let key = (self.spec, bytes.clone(), gas_limit);

// get the result if it exists
if let Some(precompiles) = self.cache.lock().get_mut(address) {
if let Some(result) = precompiles.get(&key) {
inc_precompile_cache_hits();
return result.clone().map(Some);
}
}

inc_precompile_cache_misses();

// call the precompile if cache miss
let output = self.precompile.run(context, address, bytes, gas_limit);

if let Some(output) = output.clone().transpose() {
// insert the result into the cache
self.cache
.lock()
.entry(*address)
.or_insert(PrecompileResultCache::new(NonZeroUsize::new(2048).unwrap()))
.put(key, output);
}

output
}

fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
self.precompile.warm_addresses()
}

fn contains(&self, address: &Address) -> bool {
self.precompile.contains(address)
}
}

#[derive(Debug, Clone, Default)]
pub struct EthCachedEvmFactory {
evm_factory: EthEvmFactory,
cache: Arc<Mutex<PrecompileCache>>,
}

impl EvmFactory for EthCachedEvmFactory {
type Evm<DB, I>
= EthEvm<DB, I, WrappedPrecompile<EthPrecompiles>>
where
DB: Database<Error: Send + Sync + 'static>,
I: Inspector<EthEvmContext<DB>>;

type Context<DB>
= Context<BlockEnv, TxEnv, CfgEnv, DB>
where
DB: Database<Error: Send + Sync + 'static>;

type Error<DBError>
= EVMError<DBError>
where
DBError: core::error::Error + Send + Sync + 'static;

type Tx = TxEnv;
type HaltReason = HaltReason;
type Spec = SpecId;

fn create_evm<DB>(&self, db: DB, input: EvmEnv) -> Self::Evm<DB, NoOpInspector>
where
DB: Database<Error: Send + Sync + 'static>,
{
let evm = self
.evm_factory
.create_evm(db, input)
.into_inner()
.with_precompiles(WrappedPrecompile::new(
EthPrecompiles::default(),
self.cache.clone(),
));

EthEvm::new(evm, false)
}

fn create_evm_with_inspector<DB, I>(
&self,
db: DB,
input: EvmEnv,
inspector: I,
) -> Self::Evm<DB, I>
where
DB: Database<Error: Send + Sync + 'static>,
I: Inspector<Self::Context<DB>, EthInterpreter>,
{
EthEvm::new(
self.create_evm(db, input)
.into_inner()
.with_inspector(inspector),
true,
)
}
}
14 changes: 7 additions & 7 deletions crates/rbuilder/src/live_builder/simulation/simulation_job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,14 @@ impl SimulationJob {
info!(
?self.orders_received,
?self.orders_simulated_ok,
bundles_with_replace = self.orders_with_replacement_key,
unique_replace_count = self.unique_replacement_key_bundles.len(),
bundles_with_replace_sim_ok = self.orders_with_replacement_key_sim_ok,
unique_replace_count_sim_ok = self.unique_replacement_key_bundles_sim_ok.len(),
"Stopping simulation job "
bundles_with_replace = self.orders_with_replacement_key,
unique_replace_count = self.unique_replacement_key_bundles.len(),
bundles_with_replace_sim_ok = self.orders_with_replacement_key_sim_ok,
unique_replace_count_sim_ok = self.unique_replacement_key_bundles_sim_ok.len(),
"Stopping simulation job"
);
}

async fn run_no_trace(&mut self) {
let mut new_commands = Vec::new();
let mut new_sim_results = Vec::new();
Expand Down Expand Up @@ -186,8 +187,7 @@ impl SimulationJob {
for sim_result in new_sim_results {
trace!(order_id=?sim_result.simulated_order.order.id(),
sim_duration_mus = sim_result.simulation_time.as_micros(),
profit = format_ether(sim_result.simulated_order.sim_value.coinbase_profit),
"Order simulated");
profit = format_ether(sim_result.simulated_order.sim_value.coinbase_profit), "Order simulated");
self.orders_simulated_ok
.accumulate(&sim_result.simulated_order.order);
if let Some(repl_key) = sim_result.simulated_order.order.replacement_key() {
Expand Down
Loading
Loading