Skip to content

Commit

Permalink
Sparse trie (#174)
Browse files Browse the repository at this point in the history
## Faster root hash

This pr introduces option to use sparse merkle trie implementation for
the root hash calculation.

This implementation fetches relevant pieces of the trie into memory
making subsequent root hash calculations faster.

On my PC reference implementation takes about 150ms to calculate hash
and the new one when everything is prefetched around 5ms.

Fetching proof for data for one uncached account takes about 0.25ms.

There are things to improve in implementation itself but the basic
interface for the integration into the builder will be the same.


## debug-bench-machine

This PR also redoes debug-bench-machine. Now you need to stop reth node
and provide external rpc for it to work.

The reason is that reth has trie only for the last block. Previous
implementation used last block to generate changes and it applied that
changes to the trie after the last block. Trie would already have these
changes but default reth root hash does not care about that discrepancy
and it would evaluate anyway, this was used as a hack to benchmark trie
even though it would give incorrect results.

Sparse trie implemenation would not work like that because it would
detect errors such as removing key that was already removed from the
trie.

The benefit of this is that we can actually check correctness of the
root hash on historical block.



---

## ✅ I have completed the following steps:

* [ ] Run `make lint`
* [ ] Run `make test`
* [ ] Added tests (if applicable)
  • Loading branch information
dvush authored Sep 24, 2024
1 parent 900aa72 commit 0ac5cf1
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 180 deletions.
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/rbuilder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ mockall = "0.12.1"
shellexpand = "3.1.0"
async-trait = "0.1.80"

eth-sparse-mpt = { git = "https://github.com/flashbots/eth-sparse-mpt", rev = "664759b" }

[build-dependencies]
built = { version = "0.7.1", features = ["git2", "chrono"] }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use crate::building::evm_inspector::SlotKey;
use crate::building::tracers::AccumulatorSimulationTracer;
use crate::building::{BlockBuildingContext, BlockState, PartialBlock, PartialBlockFork};
use crate::primitives::serialize::{RawTx, TxEncoding};
use crate::primitives::TransactionSignedEcRecoveredWithBlobs;
use crate::utils::signed_uint_delta;
use crate::utils::{extract_onchain_block_txs, find_suggested_fee_recipient};
use ahash::{HashMap, HashSet};
use alloy_consensus::TxEnvelope;
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::{Address, B256, I256};
use alloy_primitives::{B256, I256};
use eyre::Context;
use reth_chainspec::ChainSpec;
use reth_db::DatabaseEnv;
Expand Down Expand Up @@ -113,37 +110,3 @@ pub fn sim_historical_block(

Ok(results)
}

fn find_suggested_fee_recipient(
block: &alloy_rpc_types::Block,
txs: &[TransactionSignedEcRecoveredWithBlobs],
) -> Address {
let coinbase = block.header.miner;
let (last_tx_signer, last_tx_to) = if let Some((signer, to)) = txs
.last()
.map(|tx| (tx.signer(), tx.to().unwrap_or_default()))
{
(signer, to)
} else {
return coinbase;
};

if last_tx_signer == coinbase {
last_tx_to
} else {
coinbase
}
}

fn extract_onchain_block_txs(
onchain_block: &alloy_rpc_types::Block,
) -> eyre::Result<Vec<TransactionSignedEcRecoveredWithBlobs>> {
let mut result = Vec::new();
for tx in onchain_block.transactions.clone().into_transactions() {
let tx_envelope: TxEnvelope = tx.try_into()?;
let encoded = tx_envelope.encoded_2718();
let tx = RawTx { tx: encoded.into() }.decode(TxEncoding::NoBlobData)?;
result.push(tx.tx_with_blobs);
}
Ok(result)
}
207 changes: 104 additions & 103 deletions crates/rbuilder/src/bin/debug-bench-machine.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
//! App to benchmark/test the tx block execution.
//! It loads the last landed block and re-executes all the txs in it.
use alloy_primitives::{B256, U256};
//! This only works when reth node is stopped and the chain moved forward form its synced state
//! It downloads block aftre the last one synced and re-executes all the txs in it.
use alloy_provider::Provider;
use clap::Parser;
use eyre::Context;
use itertools::Itertools;
use rbuilder::{
building::{
sim::simulate_all_orders_with_sim_tree, BlockBuildingContext, BlockState, PartialBlock,
},
building::{BlockBuildingContext, BlockState, PartialBlock, PartialBlockFork},
live_builder::{base_config::load_config_toml_and_env, cli::LiveBuilderConfig, config::Config},
primitives::{MempoolTx, Order, TransactionSignedEcRecoveredWithBlobs},
roothash::RootHashMode,
utils::{default_cfg_env, Signer},
utils::{extract_onchain_block_txs, find_suggested_fee_recipient, http_provider},
};
use reth::{
payload::PayloadId,
providers::{BlockNumReader, BlockReader},
};
use reth_payload_builder::{database::CachedReads, EthPayloadBuilderAttributes};
use reth::providers::BlockNumReader;
use reth_payload_builder::database::CachedReads;
use reth_provider::StateProvider;
use revm_primitives::{BlobExcessGasAndPrice, BlockEnv, SpecId};
use std::{path::PathBuf, sync::Arc, time::Instant};
use tracing::{debug, info};

#[derive(Parser, Debug)]
struct Cli {
#[clap(long, help = "bench iterations", default_value = "20")]
iters: usize,
#[clap(help = "Config file path")]
#[clap(
long,
help = "external block provider",
env = "RPC_URL",
default_value = "http://127.0.0.1:8545"
)]
rpc_url: String,
#[clap(long, help = "Config file path", env = "RBUILDER_CONFIG")]
config: PathBuf,
}

Expand All @@ -36,107 +38,106 @@ async fn main() -> eyre::Result<()> {
let config: Config = load_config_toml_and_env(cli.config)?;
config.base_config().setup_tracing_subsriber()?;

let chain = config.base_config().chain_spec()?;
let rpc = http_provider(cli.rpc_url.parse()?);

let chain_spec = config.base_config().chain_spec()?;

let factory = config
.base_config()
.provider_factory()?
.provider_factory_unchecked();

let last_block = factory.last_block_number()?;
let block_data = factory
.block_by_number(last_block)?
.ok_or_else(|| eyre::eyre!("Block not found"))?;

let signer = Signer::try_from_secret(B256::random())?;

let cfg_env = default_cfg_env(&chain, block_data.timestamp);

let ctx = BlockBuildingContext {
block_env: BlockEnv {
number: U256::from(block_data.number),
coinbase: signer.address,
timestamp: U256::from(block_data.timestamp),
gas_limit: U256::from(block_data.gas_limit),
basefee: U256::from(block_data.base_fee_per_gas.unwrap_or_default()),
difficulty: Default::default(),
prevrandao: Some(block_data.difficulty.into()),
blob_excess_gas_and_price: block_data.excess_blob_gas.map(BlobExcessGasAndPrice::new),
},
initialized_cfg: cfg_env,
attributes: EthPayloadBuilderAttributes {
id: PayloadId::new([0u8; 8]),
parent: block_data.parent_hash,
timestamp: block_data.timestamp,
suggested_fee_recipient: Default::default(),
prev_randao: Default::default(),
withdrawals: block_data.withdrawals.clone().unwrap_or_default(),
parent_beacon_block_root: block_data.parent_beacon_block_root,
},
chain_spec: chain.clone(),
builder_signer: Some(signer),
extra_data: Vec::new(),
blocklist: Default::default(),
excess_blob_gas: block_data.excess_blob_gas,
spec_id: SpecId::LATEST,
};

// Get the landed orders (all Order::Tx) from the block
let orders = block_data
.body
.iter()
.map(|tx| {
let tx = tx
.try_ecrecovered()
.ok_or_else(|| eyre::eyre!("Failed to recover tx"))?;
let tx = TransactionSignedEcRecoveredWithBlobs::new_for_testing(tx);
let tx = MempoolTx::new(tx);
Ok::<_, eyre::Error>(Order::Tx(tx))
})
.collect::<Result<Vec<_>, _>>()?;

let mut state_provider =
Arc::<dyn StateProvider>::from(factory.history_by_block_number(block_data.number - 1)?);
let (sim_orders, _) = simulate_all_orders_with_sim_tree(factory.clone(), &ctx, &orders, false)?;

tracing::info!(
"Block: {}, simulated orders: {}",
block_data.number,
sim_orders.len()
let onchain_block = rpc
.get_block_by_number((last_block + 1).into(), true)
.await?
.ok_or_else(|| eyre::eyre!("block not found on rpc"))?;

let txs = extract_onchain_block_txs(&onchain_block)?;
let suggested_fee_recipient = find_suggested_fee_recipient(&onchain_block, &txs);
info!(
"Block number: {}, txs: {}",
onchain_block.header.number,
txs.len()
);

let coinbase = onchain_block.header.miner;

let ctx = BlockBuildingContext::from_onchain_block(
onchain_block,
chain_spec,
None,
Default::default(),
coinbase,
suggested_fee_recipient,
None,
);

let mut build_times_mus = Vec::new();
let mut finalize_time_mus = Vec::new();
// let signer = Signer::try_from_secret(B256::random())?;

let state_provider =
Arc::<dyn StateProvider>::from(factory.history_by_block_number(last_block)?);

let mut build_times_ms = Vec::new();
let mut finalize_time_ms = Vec::new();
let mut cached_reads = Some(CachedReads::default());
for _ in 0..cli.iters {
let mut partial_block = PartialBlock::new(true, None);
let mut block_state =
BlockState::new_arc(state_provider).with_cached_reads(cached_reads.unwrap_or_default());
let build_time = Instant::now();
partial_block.pre_block_call(&ctx, &mut block_state)?;
for order in &sim_orders {
let _ = partial_block.commit_order(order, &ctx, &mut block_state)?;
}
let build_time = build_time.elapsed();

let finalize_time = Instant::now();
let finalized_block = partial_block.finalize(
&mut block_state,
&ctx,
factory.clone(),
RootHashMode::IgnoreParentHash,
config.base_config().root_hash_task_pool()?,
)?;
let finalize_time = finalize_time.elapsed();

cached_reads = Some(finalized_block.cached_reads);

build_times_mus.push(build_time.as_micros());
finalize_time_mus.push(finalize_time.as_micros());
state_provider = block_state.into_provider();
let ctx = ctx.clone();
let txs = txs.clone();
let state_provider = state_provider.clone();
let factory = factory.clone();
let config = config.clone();
let root_hash_config = config.base_config.live_root_hash_config()?;
let (new_cached_reads, build_time, finalize_time) =
tokio::task::spawn_blocking(move || -> eyre::Result<_> {
let partial_block = PartialBlock::new(true, None);
let mut state = BlockState::new_arc(state_provider)
.with_cached_reads(cached_reads.unwrap_or_default());

let build_time = Instant::now();

let mut cumulative_gas_used = 0;
let mut cumulative_blob_gas_used = 0;
for (idx, tx) in txs.into_iter().enumerate() {
let result = {
let mut fork = PartialBlockFork::new(&mut state);
fork.commit_tx(&tx, &ctx, cumulative_gas_used, 0, cumulative_blob_gas_used)?
.with_context(|| {
format!("Failed to commit tx: {} {:?}", idx, tx.hash())
})?
};
cumulative_gas_used += result.gas_used;
cumulative_blob_gas_used += result.blob_gas_used;
}

let build_time = build_time.elapsed();

let finalize_time = Instant::now();
let finalized_block = partial_block.finalize(
&mut state,
&ctx,
factory.clone(),
root_hash_config.clone(),
config.base_config().root_hash_task_pool()?,
)?;
let finalize_time = finalize_time.elapsed();

debug!(
"Calculated root hash: {:?}",
finalized_block.sealed_block.state_root
);

Ok((finalized_block.cached_reads, build_time, finalize_time))
})
.await??;

cached_reads = Some(new_cached_reads);
build_times_ms.push(build_time.as_millis());
finalize_time_ms.push(finalize_time.as_millis());
}
report_time_data("build", &build_times_mus);
report_time_data("finalize", &finalize_time_mus);
report_time_data("build", &build_times_ms);
report_time_data("finalize", &finalize_time_ms);

Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions crates/rbuilder/src/bin/dummy-builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use rbuilder::{
mev_boost::{MevBoostRelay, RelayConfig},
SimulatedOrder,
},
roothash::RootHashMode,
roothash::RootHashConfig,
utils::Signer,
};
use reth::{providers::ProviderFactory, tasks::pool::BlockingTaskPool};
Expand Down Expand Up @@ -199,7 +199,7 @@ impl DummyBuildingAlgorithm {
let mut block_building_helper = BlockBuildingHelperFromDB::new(
provider_factory.clone(),
self.root_hash_task_pool.clone(),
RootHashMode::CorrectRoot,
RootHashConfig::live_config(false, false),
ctx.clone(),
None,
BUILDER_NAME.to_string(),
Expand Down
Loading

0 comments on commit 0ac5cf1

Please sign in to comment.