Skip to content

Commit 11410f6

Browse files
dvushZanCorDX
andauthored
EVM tx cache (#573)
# Motivation Builder constantly builds different blocks for a slot and most of transactions are execution over and over with the same result. Here, we cache execution of transactions and next time we apply tx that was already executed we only check if state that is currently available is the same as for the cached result. # Config * `simulation_use_random_coinbase: bool` if top of block simulation should use random coinbase * `evm_caching_enable: bool` if evm caching should be enabled # Results * Cache hit rate: typically around 80-90% of txs are cached and cache hit rate is around 70-90% * The speedup for EVM part of execution is huge, verifying tx state and applying result is much faster than EVM execution, for most txs its around 10 microseconds while typical execution is around 300 microseconds. There are txs that take multiple milliseconds to execute and for those time saved is huge. ## Speedup of block fill time. To measure this I compare average block fill time for the given block of builder with and without caching. I only look at blocks where average gas used is the same in both builders. The speedup of block fill time is distributed like this: average: 2.5x, median: 2.3x, p10: 1.5x, p90: 3.8x ## Limiting factor The limiting factor of speedup and cache hit rate are txs that are always failing when filling block. We execute txs like that only once, their execution is never cached. Successful txs that we execute over and over are almost perfectly cached. # Caveats I don't recommend enabling it on prod for now because I've seen one root hash related block simulation error that is hard to explain. Currently our dev version builder is broken in that part so that might be it. --------- Co-authored-by: Daniel Xifra <[email protected]>
1 parent fefbedb commit 11410f6

File tree

19 files changed

+939
-42
lines changed

19 files changed

+939
-42
lines changed

crates/rbuilder/src/backtest/build_block/landed_block_from_db.rs

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ impl<ConfigType: LiveBuilderConfig>
125125
self.block_data.winning_bid_trace.proposer_fee_recipient,
126126
Some(signer),
127127
Arc::new(MockRootHasher {}),
128+
self.config.base_config().evm_caching_enable,
128129
))
129130
}
130131

crates/rbuilder/src/backtest/execute.rs

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub fn backtest_prepare_ctx_for_block<P>(
6363
blocklist: BlockList,
6464
sbundle_mergeabe_signers: &[Address],
6565
builder_signer: Signer,
66+
evm_caching_enable: bool,
6667
) -> eyre::Result<BacktestBlockInput>
6768
where
6869
P: StateProviderFactory + Clone + 'static,
@@ -80,6 +81,7 @@ where
8081
block_data.winning_bid_trace.proposer_fee_recipient,
8182
Some(builder_signer),
8283
Arc::from(provider.root_hasher(parent_num_hash)?),
84+
evm_caching_enable,
8385
);
8486
backtest_prepare_ctx_for_block_from_building_context(
8587
ctx,
@@ -145,6 +147,7 @@ where
145147
blocklist,
146148
sbundle_mergeabe_signers,
147149
config.base_config().coinbase_signer()?,
150+
config.base_config().evm_caching_enable,
148151
)?;
149152

150153
let filtered_orders_blocklist_count = sim_errors

crates/rbuilder/src/backtest/restore_landed_orders/resim_landed_block.rs

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ where
5353
suggested_fee_recipient,
5454
None,
5555
Arc::from(provider.root_hasher(parent_num_hash)?),
56+
false,
5657
);
5758

5859
let mut local_ctx = ThreadBlockBuildingContext::default();

crates/rbuilder/src/bin/debug-bench-machine.rs

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ async fn main() -> eyre::Result<()> {
7474
suggested_fee_recipient,
7575
None,
7676
Arc::from(provider_factory.root_hasher(parent_num_hash)?),
77+
config.base_config().evm_caching_enable,
7778
);
7879

7980
let state_provider = Arc::<dyn StateProvider>::from(

crates/rbuilder/src/bin/dummy-builder.rs

+2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ async fn main() -> eyre::Result<()> {
113113
orderpool_sender,
114114
orderpool_receiver,
115115
sbundle_merger_selected_signers: Default::default(),
116+
evm_caching_enable: false,
117+
simulation_use_random_coinbase: true,
116118
};
117119

118120
let ctrlc = tokio::spawn(async move {

crates/rbuilder/src/building/cached_reads.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ use std::sync::{
99
atomic::{AtomicU64, Ordering},
1010
Arc,
1111
};
12+
13+
use ahash::RandomState;
1214
use tracing::info;
1315

1416
/// Database cache shared bewteen multiple threads.
1517
/// It should be created for unique parent block.
1618
#[derive(Debug, Clone, Default)]
1719
pub struct SharedCachedReads {
18-
pub account_info: DashMap<Address, Option<AccountInfo>>,
19-
pub storage: DashMap<(Address, U256), U256>,
20+
pub account_info: DashMap<Address, Option<AccountInfo>, RandomState>,
21+
pub storage: DashMap<(Address, U256), U256, RandomState>,
2022

21-
pub code_by_hash: DashMap<B256, Bytecode>,
22-
pub block_hash: DashMap<u64, B256>,
23+
pub code_by_hash: DashMap<B256, Bytecode, RandomState>,
24+
pub block_hash: DashMap<u64, B256, RandomState>,
2325

2426
pub local_hit_count: Arc<AtomicU64>,
2527
pub local_miss_count: Arc<AtomicU64>,

crates/rbuilder/src/building/mod.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ use std::{
4949
};
5050
use thiserror::Error;
5151
use time::OffsetDateTime;
52+
use tx_sim_cache::TxExecutionCache;
5253

5354
pub mod block_orders;
5455
pub mod builders;
@@ -65,6 +66,7 @@ pub mod precompile_cache;
6566
pub mod sim;
6667
pub mod testing;
6768
pub mod tracers;
69+
pub mod tx_sim_cache;
6870

6971
pub use self::{
7072
block_orders::*, builders::mock_block_building_helper::MockRootHasher, built_block_trace::*,
@@ -94,6 +96,7 @@ pub struct BlockBuildingContext {
9496
pub root_hasher: Arc<dyn RootHasher>,
9597
pub payload_id: InternalPayloadId,
9698
pub shared_cached_reads: Arc<SharedCachedReads>,
99+
pub tx_execution_cache: Arc<TxExecutionCache>,
97100
}
98101

99102
impl BlockBuildingContext {
@@ -111,6 +114,7 @@ impl BlockBuildingContext {
111114
spec_id: Option<SpecId>,
112115
root_hasher: Arc<dyn RootHasher>,
113116
payload_id: InternalPayloadId,
117+
evm_caching_enable: bool,
114118
) -> Option<BlockBuildingContext> {
115119
let attributes = EthPayloadBuilderAttributes::try_new(
116120
attributes.data.parent_block_hash,
@@ -178,6 +182,7 @@ impl BlockBuildingContext {
178182
root_hasher,
179183
payload_id,
180184
shared_cached_reads: Default::default(),
185+
tx_execution_cache: Arc::new(TxExecutionCache::new(evm_caching_enable)),
181186
})
182187
}
183188

@@ -194,6 +199,7 @@ impl BlockBuildingContext {
194199
suggested_fee_recipient: Address,
195200
builder_signer: Option<Signer>,
196201
root_hasher: Arc<dyn RootHasher>,
202+
evm_caching_enable: bool,
197203
) -> BlockBuildingContext {
198204
let block_number = onchain_block.header.number;
199205

@@ -262,6 +268,7 @@ impl BlockBuildingContext {
262268
root_hasher,
263269
payload_id: 0,
264270
shared_cached_reads: Default::default(),
271+
tx_execution_cache: Arc::new(TxExecutionCache::new(evm_caching_enable)),
265272
}
266273
}
267274

@@ -278,6 +285,7 @@ impl BlockBuildingContext {
278285
Default::default(),
279286
Default::default(),
280287
Arc::new(MockRootHasher {}),
288+
false,
281289
)
282290
}
283291

@@ -415,7 +423,7 @@ pub enum InsertPayoutTxErr {
415423
NoSigner,
416424
}
417425

418-
#[derive(Error, Debug)]
426+
#[derive(Error, Debug, PartialEq, Eq)]
419427
pub enum ExecutionError {
420428
#[error("Order error: {0}")]
421429
OrderError(#[from] OrderErr),

crates/rbuilder/src/building/order_commit.rs

+62-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::{
22
cached_reads::{CachedDB, LocalCachedReads, SharedCachedReads},
33
create_payout_tx,
44
tracers::SimulationTracer,
5+
tx_sim_cache::{CachedExecutionResult, EVMRecordingDatabase},
56
BlockBuildingContext, EstimatePayoutGasErr, ThreadBlockBuildingContext,
67
};
78
use crate::{
@@ -20,6 +21,7 @@ use ahash::HashSet;
2021
use alloy_consensus::{constants::KECCAK_EMPTY, Transaction};
2122
use alloy_eips::eip4844::{DATA_GAS_PER_BLOB, MAX_DATA_GAS_PER_BLOCK};
2223
use alloy_primitives::{Address, B256, U256};
24+
use itertools::Itertools;
2325
use reth::revm::database::StateProviderDatabase;
2426
use reth_errors::ProviderError;
2527
use reth_evm::{Evm, EvmEnv};
@@ -194,7 +196,7 @@ pub struct TransactionOk {
194196
pub receipt: Receipt,
195197
}
196198

197-
#[derive(Error, Debug)]
199+
#[derive(Error, Debug, Clone, PartialEq, Eq)]
198200
pub enum TransactionErr {
199201
#[error("Invalid transaction: {0:?}")]
200202
InvalidTransaction(InvalidTransaction),
@@ -223,7 +225,7 @@ pub struct BundleOk {
223225
pub original_order_ids: Vec<OrderId>,
224226
}
225227

226-
#[derive(Error, Debug)]
228+
#[derive(Error, Debug, PartialEq, Eq)]
227229
pub enum BundleErr {
228230
#[error("Invalid transaction, hash: {0:?}, err: {1}")]
229231
InvalidTransaction(B256, TransactionErr),
@@ -284,7 +286,7 @@ pub struct OrderOk {
284286
pub used_state_trace: Option<UsedStateTrace>,
285287
}
286288

287-
#[derive(Error, Debug)]
289+
#[derive(Error, Debug, PartialEq, Eq)]
288290
pub enum OrderErr {
289291
#[error("Transaction error: {0}")]
290292
Transaction(#[from] TransactionErr),
@@ -436,34 +438,75 @@ impl<'a, 'b, 'c, 'd, Tracer: SimulationTracer> PartialBlockFork<'a, 'b, 'c, 'd,
436438
// evm start
437439
// ====================================================
438440

439-
let used_state_tracer = self.tracer.as_ref().and_then(|tracer| {
440-
if tracer.should_collect_used_state_trace() {
441+
// this is set to true when user of the commit_* function wants to have used state trace,
442+
// on the other hand we always record used state trace when doing evm caching we just can skip showing it
443+
let is_recording_used_state = self
444+
.tracer
445+
.as_ref()
446+
.map(|t| t.should_collect_used_state_trace())
447+
.unwrap_or_default();
448+
let caching_result = self.ctx.tx_execution_cache.get_cached_result(
449+
db.as_mut(),
450+
tx.hash(),
451+
&self.ctx.evm_env.block_env.beneficiary,
452+
)?;
453+
454+
let cached_used_state_trace;
455+
let (res, used_state_trace) = if let Some(result) = caching_result.result {
456+
cached_used_state_trace = Some(caching_result.used_state_trace);
457+
(result, cached_used_state_trace.as_ref().map(|t| t.as_ref()))
458+
} else {
459+
let used_state_tracer = if is_recording_used_state || caching_result.should_cache {
441460
self.tmp_used_state_tracer.clear();
442461
Some(&mut self.tmp_used_state_tracer)
443462
} else {
444463
None
464+
};
465+
466+
let mut db = EVMRecordingDatabase::new(db.as_mut(), caching_result.should_cache);
467+
468+
let res = execute_evm(
469+
&self.ctx.evm_factory,
470+
self.ctx.evm_env.clone(),
471+
tx_with_blobs,
472+
used_state_tracer,
473+
&mut db,
474+
&self.ctx.blocklist,
475+
)?;
476+
477+
if caching_result.should_cache {
478+
self.ctx
479+
.tx_execution_cache
480+
.store_result(CachedExecutionResult {
481+
tx_hash: *tx.hash(),
482+
coinbase: self.ctx.evm_env.block_env.beneficiary,
483+
recorded_trace: db.recorded_trace,
484+
result: res.clone(),
485+
used_state_trace: Arc::new(self.tmp_used_state_tracer.clone()),
486+
});
445487
}
446-
});
447488

448-
let res = execute_evm(
449-
&self.ctx.evm_factory,
450-
self.ctx.evm_env.clone(),
451-
tx_with_blobs,
452-
used_state_tracer,
453-
db.as_mut(),
454-
&self.ctx.blocklist,
455-
)?;
456-
let res = match res {
457-
Ok(res) => res,
458-
Err(err) => return Ok(Err(err)),
489+
let used_state_tracer = if is_recording_used_state {
490+
Some(&self.tmp_used_state_tracer)
491+
} else {
492+
None
493+
};
494+
(res, used_state_tracer)
459495
};
460496

461497
// evm end
462498
// ====================================================
463499

500+
let res = match res {
501+
Ok(res) => res,
502+
Err(err) => return Ok(Err(err)),
503+
};
504+
464505
if let Some(tracer) = &mut self.tracer {
465506
tracer.add_gas_used(res.result.gas_used());
466-
tracer.add_used_state_trace(&self.tmp_used_state_tracer);
507+
if let (true, Some(t)) = (is_recording_used_state, used_state_trace) {
508+
tracer.add_used_state_trace(t)
509+
}
467510
}
468511

469512
db.as_mut().commit(res.state);
@@ -798,7 +841,7 @@ impl<'a, 'b, 'c, 'd, Tracer: SimulationTracer> PartialBlockFork<'a, 'b, 'c, 'd,
798841
let mut insert = res.bundle_ok;
799842

800843
// now pay all kickbacks
801-
for (to, payout) in res.payouts_promissed.into_iter() {
844+
for (to, payout) in res.payouts_promissed.into_iter().sorted_by_key(|(a, _)| *a) {
802845
if let Err(err) = self.insert_refund_payout_tx(payout, to, gas_reserved, &mut insert)? {
803846
return Ok(Err(err));
804847
}

crates/rbuilder/src/building/payout_tx.rs

+31
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ pub enum PayoutTxErr {
4343
NoSigner,
4444
}
4545

46+
impl PartialEq for PayoutTxErr {
47+
fn eq(&self, other: &Self) -> bool {
48+
match (self, other) {
49+
(PayoutTxErr::Reth(_), PayoutTxErr::Reth(_)) => true,
50+
(PayoutTxErr::SignError(a), PayoutTxErr::SignError(b)) => a == b,
51+
(PayoutTxErr::EvmError(_), PayoutTxErr::EvmError(_)) => true,
52+
(PayoutTxErr::NoSigner, PayoutTxErr::NoSigner) => true,
53+
_ => false,
54+
}
55+
}
56+
}
57+
58+
impl Eq for PayoutTxErr {}
59+
4660
pub fn insert_test_payout_tx(
4761
to: Address,
4862
ctx: &BlockBuildingContext,
@@ -95,6 +109,22 @@ pub enum EstimatePayoutGasErr {
95109
#[error("Failed to estimate gas limit")]
96110
FailedToEstimate,
97111
}
112+
113+
impl PartialEq for EstimatePayoutGasErr {
114+
fn eq(&self, other: &Self) -> bool {
115+
match (self, other) {
116+
(EstimatePayoutGasErr::Reth(_), EstimatePayoutGasErr::Reth(_)) => true,
117+
(EstimatePayoutGasErr::PayoutTxErr(a), EstimatePayoutGasErr::PayoutTxErr(b)) => a == b,
118+
(EstimatePayoutGasErr::FailedToEstimate, EstimatePayoutGasErr::FailedToEstimate) => {
119+
true
120+
}
121+
_ => false,
122+
}
123+
}
124+
}
125+
126+
impl Eq for EstimatePayoutGasErr {}
127+
98128
pub fn estimate_payout_gas_limit(
99129
to: Address,
100130
ctx: &BlockBuildingContext,
@@ -192,6 +222,7 @@ mod tests {
192222
proposer,
193223
Some(signer),
194224
Arc::new(MockRootHasher {}),
225+
false,
195226
);
196227
let mut state = BlockState::new(provider_factory.latest().unwrap());
197228
let mut local_ctx = ThreadBlockBuildingContext::default();

0 commit comments

Comments
 (0)