From ff0cf9891dbbbe9ace4977dbb315abd3ba4d72e1 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 13:59:15 -0400 Subject: [PATCH 01/17] feat: sim env --- Cargo.toml | 1 + crates/sim/Cargo.toml | 21 +++ crates/sim/src/built.rs | 127 +++++++++++++++ crates/sim/src/cache.rs | 107 +++++++++++++ crates/sim/src/env.rs | 328 ++++++++++++++++++++++++++++++++++++++ crates/sim/src/inst.rs | 84 ++++++++++ crates/sim/src/lib.rs | 14 ++ crates/sim/src/outcome.rs | 24 +++ 8 files changed, 706 insertions(+) create mode 100644 crates/sim/Cargo.toml create mode 100644 crates/sim/src/built.rs create mode 100644 crates/sim/src/cache.rs create mode 100644 crates/sim/src/env.rs create mode 100644 crates/sim/src/inst.rs create mode 100644 crates/sim/src/lib.rs create mode 100644 crates/sim/src/outcome.rs diff --git a/Cargo.toml b/Cargo.toml index 879017f..e52d6a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ signet-evm = { path = "crates/evm" } signet-extract = { path = "crates/extract" } signet-node = { path = "crates/node" } signet-rpc = { path = "crates/rpc" } +signet-sim = { path = "crates/sim" } signet-types = { path = "crates/types" } signet-zenith = { path = "crates/zenith" } diff --git a/crates/sim/Cargo.toml b/crates/sim/Cargo.toml new file mode 100644 index 0000000..f6f2644 --- /dev/null +++ b/crates/sim/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "signet-sim" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +alloy.workspace = true +dashmap = "6.1.0" +future-utils = "0.12.1" +signet-bundle.workspace = true +signet-evm.workspace = true +signet-types.workspace = true +signet-zenith.workspace = true +tokio.workspace = true +tracing.workspace = true +trevm.workspace = true diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs new file mode 100644 index 0000000..a9a5da9 --- /dev/null +++ b/crates/sim/src/built.rs @@ -0,0 +1,127 @@ +use alloy::{ + consensus::{SidecarBuilder, SidecarCoder, TxEnvelope}, + eips::Decodable2718, + primitives::{keccak256, Bytes, B256}, + rlp::Buf, +}; +use signet_bundle::SignetEthBundle; +use signet_zenith::{encode_txns, Alloy2718Coder, SignedOrder}; +use std::sync::OnceLock; +use tracing::{error, trace}; + +/// A block that has been built by the simulator. +#[derive(Debug, Clone, Default)] +pub struct BuiltBlock { + /// The host fill actions. + pub(crate) host_fills: Vec, + /// Transactions in the block. + pub(crate) transactions: Vec, + + /// Memoized raw encoding of the block. + pub(crate) raw_encoding: OnceLock, + /// Memoized hash of the block. + pub(crate) hash: OnceLock, +} + +impl BuiltBlock { + /// Create a new `BuiltBlock` + pub const fn new() -> Self { + Self { + host_fills: Vec::new(), + transactions: Vec::new(), + raw_encoding: OnceLock::new(), + hash: OnceLock::new(), + } + } + + /// Get the number of transactions in the block. + pub fn len(&self) -> usize { + self.transactions.len() + } + + /// Check if the block is empty. + pub fn is_empty(&self) -> bool { + self.transactions.is_empty() + } + + /// Returns the current list of transactions included in this block + pub fn transactions(&self) -> Vec { + self.transactions.clone() + } + + /// Unseal the block + pub(crate) fn unseal(&mut self) { + self.raw_encoding.take(); + self.hash.take(); + } + + /// Seal the block by encoding the transactions and calculating the contentshash. + pub(crate) fn seal(&self) { + self.raw_encoding.get_or_init(|| encode_txns::(&self.transactions).into()); + self.hash.get_or_init(|| keccak256(self.raw_encoding.get().unwrap().as_ref())); + } + + /// Ingest a transaction into the in-progress block. Fails + pub fn ingest_tx(&mut self, tx: &TxEnvelope) { + trace!(hash = %tx.tx_hash(), "ingesting tx"); + self.unseal(); + self.transactions.push(tx.clone()); + } + + /// Remove a transaction from the in-progress block. + pub fn remove_tx(&mut self, tx: &TxEnvelope) { + trace!(hash = %tx.tx_hash(), "removing tx"); + self.unseal(); + self.transactions.retain(|t| t.tx_hash() != tx.tx_hash()); + } + + /// Ingest a bundle into the in-progress block. + /// Ignores Signed Orders for now. + pub fn ingest_bundle(&mut self, bundle: SignetEthBundle) { + trace!(replacement_uuid = bundle.replacement_uuid(), "adding bundle to block"); + + let txs = bundle + .bundle + .txs + .into_iter() + .map(|tx| TxEnvelope::decode_2718(&mut tx.chunk())) + .collect::, _>>(); + + if let Ok(txs) = txs { + self.unseal(); + // extend the transactions with the decoded transactions. + // As this builder does not provide bundles landing "top of block", its fine to just extend. + self.transactions.extend(txs); + + if let Some(host_fills) = bundle.host_fills { + self.host_fills.push(host_fills); + } + } else { + error!("failed to decode bundle. dropping"); + } + } + + /// Encode the in-progress block + pub(crate) fn encode_raw(&self) -> &Bytes { + self.seal(); + self.raw_encoding.get().unwrap() + } + + /// Calculate the hash of the in-progress block, finishing the block. + pub fn contents_hash(&self) -> B256 { + self.seal(); + *self.hash.get().unwrap() + } + + /// Convert the in-progress block to sign request contents. + pub fn encode_calldata(&self) -> &Bytes { + self.encode_raw() + } + + /// Convert the in-progress block to a blob transaction sidecar. + pub fn encode_blob(&self) -> SidecarBuilder { + let mut coder = SidecarBuilder::::default(); + coder.ingest(self.encode_raw()); + coder + } +} diff --git a/crates/sim/src/cache.rs b/crates/sim/src/cache.rs new file mode 100644 index 0000000..11f9206 --- /dev/null +++ b/crates/sim/src/cache.rs @@ -0,0 +1,107 @@ +use crate::SimItem; +use core::fmt; +use std::{ + collections::BTreeMap, + sync::{Arc, RwLock}, +}; + +/// A cache for the simulator. +/// +/// This cache is used to store the items that are being simulated. +#[derive(Clone)] +pub struct SimCache { + inner: Arc>>>, + capacity: usize, +} + +impl fmt::Debug for SimCache { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SimCache").finish() + } +} + +impl Default for SimCache { + fn default() -> Self { + Self::new() + } +} + +impl SimCache { + /// Create a new `SimCache` instance. + pub fn new() -> Self { + Self { inner: Arc::new(RwLock::new(BTreeMap::new())), capacity: 100 } + } + + /// Create a new `SimCache` instance with a given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { inner: Arc::new(RwLock::new(BTreeMap::new())), capacity } + } + + /// Get an iterator over the best items in the cache. + pub fn read_best(&self, n: usize) -> Vec<(u128, Arc)> { + self.inner.read().unwrap().iter().rev().take(n).map(|(k, v)| (*k, v.clone())).collect() + } + + /// Get an item by key. + pub fn get(&self, key: u128) -> Option> { + self.inner.read().unwrap().get(&key).cloned() + } + + /// Remove an item by key. + pub fn remove(&self, key: u128) -> Option> { + self.inner.write().unwrap().remove(&key) + } + + /// Create a new `SimCache` instance. + pub fn add_item(&self, item: impl Into) { + let item = Arc::new(item.into()); + + // Calculate the total fee for the item. + let mut score = item.calculate_total_fee(); + + let mut inner = self.inner.write().unwrap(); + + // If it has the same score, we decrement (prioritizing earlier items) + while inner.contains_key(&score) { + score -= 1; + } + inner.insert(score, item); + } + + /// Clean the cache by removing bundles that are not valid in the current + /// block. + pub fn clean(&self, block_number: u64, block_timestamp: u64) { + let mut inner = self.inner.write().unwrap(); + + // Trim to capacity by dropping lower fees. + while inner.len() > self.capacity { + inner.pop_first(); + } + + inner.retain(|_, value| { + let SimItem::Bundle(bundle) = value.as_ref() else { + return true; + }; + if bundle.bundle.block_number != block_number { + return false; + } + if let Some(timestamp) = bundle.min_timestamp() { + if timestamp > block_timestamp { + return false; + } + } + if let Some(timestamp) = bundle.max_timestamp() { + if timestamp < block_timestamp { + return false; + } + } + true + }) + } + + /// Clear the cache. + pub fn clear(&self) { + let mut inner = self.inner.write().unwrap(); + inner.clear(); + } +} diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs new file mode 100644 index 0000000..e5840b4 --- /dev/null +++ b/crates/sim/src/env.rs @@ -0,0 +1,328 @@ +use crate::{outcome::SimOutcomeWithCache, SimCache, SimItem}; +use alloy::{consensus::TxEnvelope, primitives::U256}; +use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError}; +use signet_evm::SignetLayered; +use signet_types::config::SignetSystemConstants; +use std::{convert::Infallible, marker::PhantomData, ops::Deref, sync::Arc, time::Instant}; +use tokio::sync::{mpsc, oneshot, watch}; +use trevm::{ + db::{cow::CacheOnWrite, TryCachingDb}, + helpers::Ctx, + inspectors::{Layered, TimeLimit}, + revm::{ + context::result::EVMError, + database::{Cache, CacheDB}, + inspector::NoOpInspector, + DatabaseRef, Inspector, + }, + Block, BundleDriver, Cfg, DbConnect, EvmFactory, +}; + +/// A type alias for the database underlying the simulation. +pub type InnerDb = Arc>; + +/// A type alias for the database used in the simulation. +pub type SimDb = CacheOnWrite>; + +pub struct SimEnv { + inner: Arc>, +} + +impl Deref for SimEnv { + type Target = SimEnvInner; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl SimEnv +where + Db: DatabaseRef + Send + Sync + 'static, + Insp: Inspector>> + Default + Sync + 'static, +{ + /// Run a simulation round, returning the best item. + pub async fn sim_round( + &mut self, + finish_by: std::time::Instant, + ) -> Option<(U256, Arc)> { + let (best_tx, mut best_watcher) = watch::channel(None); + + let (done_tx, done_rx) = oneshot::channel(); + + let this = self.inner.clone(); + + // Spawn a blocking task to run the simulations. + tokio::task::spawn_blocking(move || async move { + // Pull the `n` best items from the cache. + let active_sim = this.sim_items.read_best(this.concurrency_limit); + + // If there are no items to simulate, return None. + let best_score = U256::ZERO; + + std::thread::scope(|scope| { + // Create a channel to send the results back. + let (candidates, mut candidates_rx) = mpsc::channel(this.concurrency_limit); + + // Spawn a thread per bundle to simulate. + for (identifier, item) in active_sim.iter() { + let this_ref = &this; + let c = candidates.clone(); + + scope.spawn(move || { + // If simulation is succesful, send the outcome via the + // channel. + if let Ok(candidate) = this_ref.simulate(*identifier, item) { + let _ = c.blocking_send(candidate); + }; + }); + } + // Drop the TX so that the channel is closed when all threads + // are done. + drop(candidates); + + // Wait for each thread to finish. Find the best outcome. + while let Some(candidate) = candidates_rx.blocking_recv() { + if candidate.score > best_score { + let _ = best_tx.send(Some(candidate)); + } + } + let _ = done_tx.send(()); + }); + }); + + // Either simulation is done, or we time out + tokio::select! { + _ = tokio::time::sleep_until(finish_by.into()) => {}, + _ = done_rx => {}, + } + + // Check what the current best outcome is. + let best = best_watcher.borrow_and_update(); + let outcome = best.as_ref()?; + + // Remove the item from the cache. + let item = self.sim_items.remove(outcome.identifier)?; + // Accept the cache from the simulation. + Arc::get_mut(&mut self.inner) + .expect("sims dropped already") + .accept_cache_ref(&outcome.cache) + .ok()?; + + Some((outcome.score, item)) + } +} + +/// A simulation environment. +pub struct SimEnvInner { + /// The database to use for the simulation. This database will be wrapped + /// in [`CacheOnWrite`] databases for each simulation. + db: InnerDb, + + /// The cache of items to simulate. + sim_items: SimCache, + + /// The system constants for the Signet network. + constants: SignetSystemConstants, + + /// Chain cfg to use for the simulation. + cfg: Box, + + /// Block to use for the simulation. + block: Box, + + /// The instant by which the simulation should finish. + finish_by: std::time::Instant, + + /// The maximum number of concurrent simulations to run. + concurrency_limit: usize, + + /// Spooky ghost inspector. + _pd: PhantomData Insp>, +} + +impl SimEnvInner { + /// Creates a new `SimFactory` instance. + pub fn new( + db: Db, + constants: SignetSystemConstants, + cfg: Box, + block: Box, + finish_by: std::time::Instant, + concurrency_limit: usize, + sim_items: SimCache, + ) -> Self { + Self { + db: Arc::new(CacheDB::new(db)), + constants, + cfg, + block, + finish_by, + concurrency_limit, + sim_items, + _pd: PhantomData, + } + } + + /// Get a reference to the database. + pub fn db_mut(&mut self) -> &mut InnerDb { + &mut self.db + } + + /// Get a reference to the system constants. + pub fn constants(&self) -> &SignetSystemConstants { + &self.constants + } + + /// Get a reference to the chain cfg. + pub fn cfg(&self) -> &dyn Cfg { + &self.cfg + } + + /// Get a reference to the block. + pub fn block(&self) -> &dyn Block { + &self.block + } + + /// Get the exectuion timeout. + pub fn finish_by(&self) -> std::time::Instant { + self.finish_by + } + + /// Set the execution timeout. + pub fn set_finish_by(&mut self, timeout: std::time::Instant) { + self.finish_by = timeout; + } +} + +impl DbConnect for SimEnvInner +where + Db: DatabaseRef + Send + Sync, + Insp: Sync, +{ + type Database = SimDb; + + type Error = Infallible; + + fn connect(&self) -> Result { + Ok(CacheOnWrite::new(self.db.clone())) + } +} + +impl EvmFactory for SimEnvInner +where + Db: DatabaseRef + Send + Sync, + Insp: Inspector>> + Default + Sync, +{ + type Insp = SignetLayered>; + + fn create(&self) -> Result, Self::Error> { + let db = self.connect().unwrap(); + + let inspector = + Layered::new(TimeLimit::new(self.finish_by - Instant::now()), Insp::default()); + + Ok(signet_evm::signet_evm_with_inspector(db, inspector, self.constants)) + } +} + +impl SimEnvInner +where + Db: DatabaseRef + Send + Sync, + Insp: Inspector>> + Default + Sync, +{ + /// Simulates a transaction in the context of a block. + /// + /// This function runs the simulation in a separate thread and waits for + /// the result or the deadline to expire. + fn simulate_tx( + &self, + identifier: u128, + transaction: &TxEnvelope, + ) -> Result>> { + let trevm = self.create_with_block(&self.cfg, &self.block).unwrap(); + + // Get the initial beneficiary balance + let beneificiary = trevm.beneficiary(); + let initial_beneficiary_balance = + trevm.try_read_balance_ref(beneificiary).map_err(EVMError::Database)?; + + // If succesful, take the cache. If failed, return the error. + match trevm.run_tx(transaction) { + Ok(trevm) => { + // Get the beneficiary balance after the transaction and calculate the + // increase + let beneficiary_balance = + trevm.try_read_balance_ref(beneificiary).map_err(EVMError::Database)?; + let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance); + + let cache = trevm.accept_state().into_db().into_cache(); + + // Create the outcome + Ok(SimOutcomeWithCache { identifier, score, cache }) + } + Err(e) => Err(SignetEthBundleError::from(e.into_error())), + } + } + + /// Simulates a bundle on the current environment. + fn simulate_bundle( + &self, + identifier: u128, + bundle: &SignetEthBundle, + ) -> Result>> + where + Insp: Inspector>> + Default + Sync, + { + let mut driver = SignetEthBundleDriver::new(bundle, self.finish_by); + let trevm = self.create_with_block(&self.cfg, &self.block).unwrap(); + + // run the bundle + let trevm = match driver.run_bundle(trevm) { + Ok(result) => result, + Err(e) => return Err(e.into_error()), + }; + + // evaluate the result + let score = driver.beneficiary_balance_increase(); + + let db = trevm.into_db(); + let cache = db.into_cache(); + + Ok(SimOutcomeWithCache { identifier, score, cache }) + } + + /// Simulates a transaction or bundle in the context of a block. + fn simulate( + &self, + identifier: u128, + item: &SimItem, + ) -> Result>> { + match item { + SimItem::Bundle(bundle) => self.simulate_bundle(identifier, bundle), + SimItem::Tx(tx) => self.simulate_tx(identifier, tx), + } + } +} + +impl SimEnvInner +where + Db: DatabaseRef, + Insp: Inspector>> + Default + Sync, +{ + /// Accepts a cache from the simulation and extends the database with it. + pub fn accept_cache( + &mut self, + cache: Cache, + ) -> Result<(), as TryCachingDb>::Error> { + self.db_mut().try_extend(cache) + } + + /// Accepts a cache from the simulation and extends the database with it. + pub fn accept_cache_ref( + &mut self, + cache: &Cache, + ) -> Result<(), as TryCachingDb>::Error> { + self.db_mut().try_extend_ref(cache) + } +} diff --git a/crates/sim/src/inst.rs b/crates/sim/src/inst.rs new file mode 100644 index 0000000..36f3fb0 --- /dev/null +++ b/crates/sim/src/inst.rs @@ -0,0 +1,84 @@ +use crate::BuiltBlock; +use alloy::{ + consensus::{Transaction, TxEnvelope}, + eips::Decodable2718, +}; +use signet_bundle::SignetEthBundle; +use tokio::sync::oneshot; +use trevm::{Block, Cfg}; + +/// Instructions for the simulator. +pub enum SimInstruction { + /// Enroll a bundle for simulation. + AddItem { item: SimItem }, + + /// Drop a bundle from the simulator by its UUID. + DropByUuid { uuid: String }, + + /// Update the configuration of the simulator. This should be used + /// sparingly. + UpdateCfg { cfg: Box, on_update: oneshot::Sender<()> }, + + /// Close the current block, returning the built block + CloseBlock { + next_block_env: Option>, + finish_by: std::time::Instant, + on_close: oneshot::Sender, + }, +} + +pub enum SimItem { + /// A bundle to be simulated. + Bundle(SignetEthBundle), + + /// A transaction to be simulated. + Tx(TxEnvelope), +} + +impl From for SimItem { + fn from(bundle: SignetEthBundle) -> Self { + Self::Bundle(bundle) + } +} + +impl From for SimItem { + fn from(tx: TxEnvelope) -> Self { + Self::Tx(tx) + } +} + +impl SimItem { + /// Get the bundle if it is a bundle. + pub fn as_bundle(&self) -> Option<&SignetEthBundle> { + match self { + Self::Bundle(bundle) => Some(bundle), + Self::Tx(_) => None, + } + } + + /// Get the transaction if it is a transaction. + pub fn as_tx(&self) -> Option<&TxEnvelope> { + match self { + Self::Bundle(_) => None, + Self::Tx(tx) => Some(tx), + } + } + + /// Calculate the maximum gas fee payable, this may be used as a heuristic + /// to determine simulation order. + pub fn calculate_total_fee(&self) -> u128 { + match self { + Self::Bundle(bundle) => { + let mut total_tx_fee = 0; + for tx in bundle.bundle.txs.iter() { + let Ok(tx) = TxEnvelope::decode_2718(&mut tx.as_ref()) else { + continue; + }; + total_tx_fee += tx.effective_gas_price(None) * tx.gas_limit() as u128; + } + total_tx_fee + } + Self::Tx(tx) => tx.effective_gas_price(None) * tx.gas_limit() as u128, + } + } +} diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs new file mode 100644 index 0000000..02a97cd --- /dev/null +++ b/crates/sim/src/lib.rs @@ -0,0 +1,14 @@ +mod built; +pub use built::BuiltBlock; + +mod cache; +pub use cache::SimCache; + +mod env; +pub use env::SimEnv; + +mod inst; +pub use inst::{SimInstruction, SimItem}; + +mod outcome; +pub use outcome::SimOutcome; diff --git a/crates/sim/src/outcome.rs b/crates/sim/src/outcome.rs new file mode 100644 index 0000000..511d90f --- /dev/null +++ b/crates/sim/src/outcome.rs @@ -0,0 +1,24 @@ +use alloy::primitives::U256; +use trevm::revm::database::Cache; + +pub struct SimOutcome { + /// The transaction or bundle that was simulated, as in the cache. + pub identifier: u128, + + /// The score of the simulation, a [`U256`] value that represents the + /// increase in the beneficiary's balance. + pub score: U256, +} + +pub struct SimOutcomeWithCache { + /// The transaction or bundle that was simulated, as in the cache. + pub identifier: u128, + + /// The score of the simulation, a [`U256`] value that represents the + /// increase in the beneficiary's balance. + pub score: U256, + + /// The result of the simulation, a [`Cache`] containing state changes that + /// can be applied. + pub cache: Cache, +} From 5e58d54ca7da990c8b413979d958705cd4c71190 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 14:16:22 -0400 Subject: [PATCH 02/17] chore: remove inst --- crates/sim/src/built.rs | 20 +++++----- crates/sim/src/cache.rs | 12 +++--- crates/sim/src/env.rs | 15 +++++--- crates/sim/src/inst.rs | 84 ----------------------------------------- crates/sim/src/lib.rs | 7 ++-- crates/sim/src/task.rs | 34 +++++++++++++++++ 6 files changed, 63 insertions(+), 109 deletions(-) delete mode 100644 crates/sim/src/inst.rs create mode 100644 crates/sim/src/task.rs diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs index a9a5da9..711c05f 100644 --- a/crates/sim/src/built.rs +++ b/crates/sim/src/built.rs @@ -9,6 +9,8 @@ use signet_zenith::{encode_txns, Alloy2718Coder, SignedOrder}; use std::sync::OnceLock; use tracing::{error, trace}; +use crate::SimItem; + /// A block that has been built by the simulator. #[derive(Debug, Clone, Default)] pub struct BuiltBlock { @@ -62,17 +64,10 @@ impl BuiltBlock { } /// Ingest a transaction into the in-progress block. Fails - pub fn ingest_tx(&mut self, tx: &TxEnvelope) { + pub fn ingest_tx(&mut self, tx: TxEnvelope) { trace!(hash = %tx.tx_hash(), "ingesting tx"); self.unseal(); - self.transactions.push(tx.clone()); - } - - /// Remove a transaction from the in-progress block. - pub fn remove_tx(&mut self, tx: &TxEnvelope) { - trace!(hash = %tx.tx_hash(), "removing tx"); - self.unseal(); - self.transactions.retain(|t| t.tx_hash() != tx.tx_hash()); + self.transactions.push(tx); } /// Ingest a bundle into the in-progress block. @@ -101,6 +96,13 @@ impl BuiltBlock { } } + pub fn ingest(&mut self, item: SimItem) { + match item { + SimItem::Bundle(bundle) => self.ingest_bundle(bundle), + SimItem::Tx(tx) => self.ingest_tx(tx), + } + } + /// Encode the in-progress block pub(crate) fn encode_raw(&self) -> &Bytes { self.seal(); diff --git a/crates/sim/src/cache.rs b/crates/sim/src/cache.rs index 11f9206..2eed36d 100644 --- a/crates/sim/src/cache.rs +++ b/crates/sim/src/cache.rs @@ -10,7 +10,7 @@ use std::{ /// This cache is used to store the items that are being simulated. #[derive(Clone)] pub struct SimCache { - inner: Arc>>>, + inner: Arc>>, capacity: usize, } @@ -38,23 +38,23 @@ impl SimCache { } /// Get an iterator over the best items in the cache. - pub fn read_best(&self, n: usize) -> Vec<(u128, Arc)> { + pub fn read_best(&self, n: usize) -> Vec<(u128, SimItem)> { self.inner.read().unwrap().iter().rev().take(n).map(|(k, v)| (*k, v.clone())).collect() } /// Get an item by key. - pub fn get(&self, key: u128) -> Option> { + pub fn get(&self, key: u128) -> Option { self.inner.read().unwrap().get(&key).cloned() } /// Remove an item by key. - pub fn remove(&self, key: u128) -> Option> { + pub fn remove(&self, key: u128) -> Option { self.inner.write().unwrap().remove(&key) } /// Create a new `SimCache` instance. pub fn add_item(&self, item: impl Into) { - let item = Arc::new(item.into()); + let item = item.into(); // Calculate the total fee for the item. let mut score = item.calculate_total_fee(); @@ -79,7 +79,7 @@ impl SimCache { } inner.retain(|_, value| { - let SimItem::Bundle(bundle) = value.as_ref() else { + let SimItem::Bundle(bundle) = value else { return true; }; if bundle.bundle.block_number != block_number { diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index e5840b4..d070245 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -4,7 +4,10 @@ use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError use signet_evm::SignetLayered; use signet_types::config::SignetSystemConstants; use std::{convert::Infallible, marker::PhantomData, ops::Deref, sync::Arc, time::Instant}; -use tokio::sync::{mpsc, oneshot, watch}; +use tokio::{ + select, + sync::{mpsc, oneshot, watch}, +}; use trevm::{ db::{cow::CacheOnWrite, TryCachingDb}, helpers::Ctx, @@ -42,10 +45,7 @@ where Insp: Inspector>> + Default + Sync + 'static, { /// Run a simulation round, returning the best item. - pub async fn sim_round( - &mut self, - finish_by: std::time::Instant, - ) -> Option<(U256, Arc)> { + pub async fn sim_round(&mut self, finish_by: std::time::Instant) -> Option<(U256, SimItem)> { let (best_tx, mut best_watcher) = watch::channel(None); let (done_tx, done_rx) = oneshot::channel(); @@ -74,6 +74,9 @@ where // channel. if let Ok(candidate) = this_ref.simulate(*identifier, item) { let _ = c.blocking_send(candidate); + } else { + // if the sim fails, remove the item from the cache + this_ref.sim_items.remove(*identifier); }; }); } @@ -92,7 +95,7 @@ where }); // Either simulation is done, or we time out - tokio::select! { + select! { _ = tokio::time::sleep_until(finish_by.into()) => {}, _ = done_rx => {}, } diff --git a/crates/sim/src/inst.rs b/crates/sim/src/inst.rs deleted file mode 100644 index 36f3fb0..0000000 --- a/crates/sim/src/inst.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::BuiltBlock; -use alloy::{ - consensus::{Transaction, TxEnvelope}, - eips::Decodable2718, -}; -use signet_bundle::SignetEthBundle; -use tokio::sync::oneshot; -use trevm::{Block, Cfg}; - -/// Instructions for the simulator. -pub enum SimInstruction { - /// Enroll a bundle for simulation. - AddItem { item: SimItem }, - - /// Drop a bundle from the simulator by its UUID. - DropByUuid { uuid: String }, - - /// Update the configuration of the simulator. This should be used - /// sparingly. - UpdateCfg { cfg: Box, on_update: oneshot::Sender<()> }, - - /// Close the current block, returning the built block - CloseBlock { - next_block_env: Option>, - finish_by: std::time::Instant, - on_close: oneshot::Sender, - }, -} - -pub enum SimItem { - /// A bundle to be simulated. - Bundle(SignetEthBundle), - - /// A transaction to be simulated. - Tx(TxEnvelope), -} - -impl From for SimItem { - fn from(bundle: SignetEthBundle) -> Self { - Self::Bundle(bundle) - } -} - -impl From for SimItem { - fn from(tx: TxEnvelope) -> Self { - Self::Tx(tx) - } -} - -impl SimItem { - /// Get the bundle if it is a bundle. - pub fn as_bundle(&self) -> Option<&SignetEthBundle> { - match self { - Self::Bundle(bundle) => Some(bundle), - Self::Tx(_) => None, - } - } - - /// Get the transaction if it is a transaction. - pub fn as_tx(&self) -> Option<&TxEnvelope> { - match self { - Self::Bundle(_) => None, - Self::Tx(tx) => Some(tx), - } - } - - /// Calculate the maximum gas fee payable, this may be used as a heuristic - /// to determine simulation order. - pub fn calculate_total_fee(&self) -> u128 { - match self { - Self::Bundle(bundle) => { - let mut total_tx_fee = 0; - for tx in bundle.bundle.txs.iter() { - let Ok(tx) = TxEnvelope::decode_2718(&mut tx.as_ref()) else { - continue; - }; - total_tx_fee += tx.effective_gas_price(None) * tx.gas_limit() as u128; - } - total_tx_fee - } - Self::Tx(tx) => tx.effective_gas_price(None) * tx.gas_limit() as u128, - } - } -} diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 02a97cd..53f7baf 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -5,10 +5,9 @@ mod cache; pub use cache::SimCache; mod env; -pub use env::SimEnv; - -mod inst; -pub use inst::{SimInstruction, SimItem}; +pub use env::{InnerDb, SimDb, SimEnv}; mod outcome; pub use outcome::SimOutcome; + +mod task; diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs new file mode 100644 index 0000000..55ba658 --- /dev/null +++ b/crates/sim/src/task.rs @@ -0,0 +1,34 @@ +use crate::{BuiltBlock, SimDb, SimEnv}; +use tokio::select; +use trevm::{ + helpers::Ctx, + revm::{inspector::NoOpInspector, DatabaseRef, Inspector}, +}; + +/// Builds a single block by repeatedly invoking [`SimEnv`]. +pub struct BlockBuild { + env: SimEnv, + block: BuiltBlock, + finish_by: std::time::Instant, +} + +impl BlockBuild +where + Db: DatabaseRef + Send + Sync + 'static, + Insp: Inspector>> + Default + Sync + 'static, +{ + /// Create a new simulation task. + pub const fn new(env: SimEnv) -> Self { + Self { env, block: BuiltBlock::new() } + } + + /// Run a simulation round, and accumulate the results into the block. + pub async fn round(&mut self, finish_by: std::time::Instant) { + if let Some((score, item)) = self.env.sim_round(finish_by).await { + tracing::debug!(%score, "Adding item to block"); + self.block.ingest(item); + } + } + + pub async fn build(&mut self) {} +} From a34d84b0de0bfff60408cf77c6393aa5f2622760 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 14:34:08 -0400 Subject: [PATCH 03/17] chore: gas_limit enforcement in simulation --- crates/sim/src/built.rs | 19 +++++++++--- crates/sim/src/env.rs | 36 +++++++++++++--------- crates/sim/src/item.rs | 63 +++++++++++++++++++++++++++++++++++++++ crates/sim/src/lib.rs | 6 +++- crates/sim/src/outcome.rs | 23 +++++++++----- crates/sim/src/task.rs | 28 ++++++++++++----- 6 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 crates/sim/src/item.rs diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs index 711c05f..984f780 100644 --- a/crates/sim/src/built.rs +++ b/crates/sim/src/built.rs @@ -9,7 +9,7 @@ use signet_zenith::{encode_txns, Alloy2718Coder, SignedOrder}; use std::sync::OnceLock; use tracing::{error, trace}; -use crate::SimItem; +use crate::{outcome::SimulatedItem, SimItem}; /// A block that has been built by the simulator. #[derive(Debug, Clone, Default)] @@ -19,6 +19,9 @@ pub struct BuiltBlock { /// Transactions in the block. pub(crate) transactions: Vec, + /// The amount of gas used by the block so far + pub(crate) gas_used: u64, + /// Memoized raw encoding of the block. pub(crate) raw_encoding: OnceLock, /// Memoized hash of the block. @@ -31,13 +34,19 @@ impl BuiltBlock { Self { host_fills: Vec::new(), transactions: Vec::new(), + gas_used: 0, raw_encoding: OnceLock::new(), hash: OnceLock::new(), } } + /// Get the amount of gas used by the block. + pub fn gas_used(&self) -> u64 { + self.gas_used + } + /// Get the number of transactions in the block. - pub fn len(&self) -> usize { + pub fn tx_count(&self) -> usize { self.transactions.len() } @@ -96,8 +105,10 @@ impl BuiltBlock { } } - pub fn ingest(&mut self, item: SimItem) { - match item { + pub fn ingest(&mut self, item: SimulatedItem) { + self.gas_used += item.gas_used; + + match item.item { SimItem::Bundle(bundle) => self.ingest_bundle(bundle), SimItem::Tx(tx) => self.ingest_tx(tx), } diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index d070245..8c605b5 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -1,4 +1,4 @@ -use crate::{outcome::SimOutcomeWithCache, SimCache, SimItem}; +use crate::{outcome::SimulatedItem, SimCache, SimItem, SimOutcomeWithCache}; use alloy::{consensus::TxEnvelope, primitives::U256}; use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError}; use signet_evm::SignetLayered; @@ -45,7 +45,11 @@ where Insp: Inspector>> + Default + Sync + 'static, { /// Run a simulation round, returning the best item. - pub async fn sim_round(&mut self, finish_by: std::time::Instant) -> Option<(U256, SimItem)> { + pub async fn sim_round( + &mut self, + finish_by: std::time::Instant, + max_gas: u64, + ) -> Option { let (best_tx, mut best_watcher) = watch::channel(None); let (done_tx, done_rx) = oneshot::channel(); @@ -73,11 +77,14 @@ where // If simulation is succesful, send the outcome via the // channel. if let Ok(candidate) = this_ref.simulate(*identifier, item) { - let _ = c.blocking_send(candidate); - } else { - // if the sim fails, remove the item from the cache - this_ref.sim_items.remove(*identifier); + if candidate.gas_used <= max_gas { + let _ = c.blocking_send(candidate); + return; + } }; + // fall through applies to all errors, occurs if + // the simulation fails or the gas limit is exceeded. + this_ref.sim_items.remove(*identifier); }); } // Drop the TX so that the channel is closed when all threads @@ -112,7 +119,7 @@ where .accept_cache_ref(&outcome.cache) .ok()?; - Some((outcome.score, item)) + Some(SimulatedItem { gas_used: outcome.gas_used, score: outcome.score, item }) } } @@ -259,10 +266,12 @@ where trevm.try_read_balance_ref(beneificiary).map_err(EVMError::Database)?; let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance); + // Get the simulation results + let gas_used = trevm.result().gas_used(); let cache = trevm.accept_state().into_db().into_cache(); // Create the outcome - Ok(SimOutcomeWithCache { identifier, score, cache }) + Ok(SimOutcomeWithCache { identifier, score, cache, gas_used }) } Err(e) => Err(SignetEthBundleError::from(e.into_error())), } @@ -280,19 +289,18 @@ where let mut driver = SignetEthBundleDriver::new(bundle, self.finish_by); let trevm = self.create_with_block(&self.cfg, &self.block).unwrap(); - // run the bundle + // Run the bundle let trevm = match driver.run_bundle(trevm) { Ok(result) => result, Err(e) => return Err(e.into_error()), }; - // evaluate the result + // Build the SimOutcome let score = driver.beneficiary_balance_increase(); + let gas_used = driver.total_gas_used(); + let cache = trevm.into_db().into_cache(); - let db = trevm.into_db(); - let cache = db.into_cache(); - - Ok(SimOutcomeWithCache { identifier, score, cache }) + Ok(SimOutcomeWithCache { identifier, score, cache, gas_used }) } /// Simulates a transaction or bundle in the context of a block. diff --git a/crates/sim/src/item.rs b/crates/sim/src/item.rs new file mode 100644 index 0000000..eacd1b6 --- /dev/null +++ b/crates/sim/src/item.rs @@ -0,0 +1,63 @@ +use alloy::{ + consensus::{Transaction, TxEnvelope}, + eips::Decodable2718, +}; +use signet_bundle::SignetEthBundle; + +/// An item that can be simulated. +#[derive(Debug, Clone)] +pub enum SimItem { + /// A bundle to be simulated. + Bundle(SignetEthBundle), + + /// A transaction to be simulated. + Tx(TxEnvelope), +} + +impl From for SimItem { + fn from(bundle: SignetEthBundle) -> Self { + Self::Bundle(bundle) + } +} + +impl From for SimItem { + fn from(tx: TxEnvelope) -> Self { + Self::Tx(tx) + } +} + +impl SimItem { + /// Get the bundle if it is a bundle. + pub fn as_bundle(&self) -> Option<&SignetEthBundle> { + match self { + Self::Bundle(bundle) => Some(bundle), + Self::Tx(_) => None, + } + } + + /// Get the transaction if it is a transaction. + pub fn as_tx(&self) -> Option<&TxEnvelope> { + match self { + Self::Bundle(_) => None, + Self::Tx(tx) => Some(tx), + } + } + + /// Calculate the maximum gas fee payable, this may be used as a heuristic + /// to determine simulation order. + pub fn calculate_total_fee(&self) -> u128 { + match self { + Self::Bundle(bundle) => { + let mut total_tx_fee = 0; + for tx in bundle.bundle.txs.iter() { + let Ok(tx) = TxEnvelope::decode_2718(&mut tx.as_ref()) else { + continue; + }; + total_tx_fee += tx.effective_gas_price(None) * tx.gas_limit() as u128; + } + total_tx_fee + } + Self::Tx(tx) => tx.effective_gas_price(None) * tx.gas_limit() as u128, + } + } +} diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 53f7baf..1c3f80d 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -7,7 +7,11 @@ pub use cache::SimCache; mod env; pub use env::{InnerDb, SimDb, SimEnv}; +mod item; +pub use item::SimItem; + mod outcome; -pub use outcome::SimOutcome; +pub(crate) use outcome::SimOutcomeWithCache; mod task; +pub use task::BlockBuild; diff --git a/crates/sim/src/outcome.rs b/crates/sim/src/outcome.rs index 511d90f..6bce223 100644 --- a/crates/sim/src/outcome.rs +++ b/crates/sim/src/outcome.rs @@ -1,15 +1,11 @@ use alloy::primitives::U256; use trevm::revm::database::Cache; -pub struct SimOutcome { - /// The transaction or bundle that was simulated, as in the cache. - pub identifier: u128, - - /// The score of the simulation, a [`U256`] value that represents the - /// increase in the beneficiary's balance. - pub score: U256, -} +use crate::SimItem; +/// A simulation outcome that includes the score, gas used, and a cache of +/// state changes. +#[derive(Debug, Clone)] pub struct SimOutcomeWithCache { /// The transaction or bundle that was simulated, as in the cache. pub identifier: u128, @@ -21,4 +17,15 @@ pub struct SimOutcomeWithCache { /// The result of the simulation, a [`Cache`] containing state changes that /// can be applied. pub cache: Cache, + + /// The total amount of gas used by the simulation. + pub gas_used: u64, +} + +/// An item after simulation, containing the score and gas used. +#[derive(Debug, Clone)] +pub struct SimulatedItem { + pub score: U256, + pub gas_used: u64, + pub item: SimItem, } diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index 55ba658..970945e 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -10,6 +10,7 @@ pub struct BlockBuild { env: SimEnv, block: BuiltBlock, finish_by: std::time::Instant, + max_gas: u64, } impl BlockBuild @@ -18,17 +19,30 @@ where Insp: Inspector>> + Default + Sync + 'static, { /// Create a new simulation task. - pub const fn new(env: SimEnv) -> Self { - Self { env, block: BuiltBlock::new() } + pub const fn new(env: SimEnv, finish_by: std::time::Instant, max_gas: u64) -> Self { + Self { env, block: BuiltBlock::new(), finish_by, max_gas } } /// Run a simulation round, and accumulate the results into the block. - pub async fn round(&mut self, finish_by: std::time::Instant) { - if let Some((score, item)) = self.env.sim_round(finish_by).await { - tracing::debug!(%score, "Adding item to block"); - self.block.ingest(item); + async fn round(&mut self, finish_by: std::time::Instant) { + let gas_allowed = self.max_gas - self.block.gas_used(); + + if let Some(simulated) = self.env.sim_round(finish_by, gas_allowed).await { + tracing::debug!(score = %simulated.score, gas_used = simulated.gas_used, "Adding item to block"); + self.block.ingest(simulated); } } - pub async fn build(&mut self) {} + /// Run several rounds, building + pub async fn build(mut self) -> BuiltBlock { + // Run until the deadline is reached. + loop { + select! { + _ = tokio::time::sleep_until(self.finish_by.into()) => break, + _ = self.round(self.finish_by) => {} + } + } + + self.block + } } From 28de7976b28a0e937ae78d1b6493ffa3a2fe1795 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 15:59:18 -0400 Subject: [PATCH 04/17] feat: new fn --- crates/sim/src/env.rs | 23 +++++++++++++++++++++++ crates/sim/src/task.rs | 1 + 2 files changed, 24 insertions(+) diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index 8c605b5..1699ac7 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -39,11 +39,34 @@ impl Deref for SimEnv { } } +impl From> for SimEnv +where + Db: DatabaseRef + Send + Sync + 'static, + Insp: Inspector>> + Default + Sync + 'static, +{ + fn from(inner: SimEnvInner) -> Self { + Self { inner: Arc::new(inner) } + } +} + impl SimEnv where Db: DatabaseRef + Send + Sync + 'static, Insp: Inspector>> + Default + Sync + 'static, { + /// Creates a new `SimEnv` instance. + pub fn new( + db: Db, + constants: SignetSystemConstants, + cfg: Box, + block: Box, + finish_by: std::time::Instant, + concurrency_limit: usize, + sim_items: SimCache, + ) -> Self { + SimEnvInner::new(db, constants, cfg, block, finish_by, concurrency_limit, sim_items).into() + } + /// Run a simulation round, returning the best item. pub async fn sim_round( &mut self, diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index 970945e..6f1920b 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -9,6 +9,7 @@ use trevm::{ pub struct BlockBuild { env: SimEnv, block: BuiltBlock, + finish_by: std::time::Instant, max_gas: u64, } From 6e0e93197f00dbfb6d53710902c72fb5e72bae55 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 16:04:46 -0400 Subject: [PATCH 05/17] chore: move aliases up --- crates/sim/src/env.rs | 8 +------- crates/sim/src/lib.rs | 8 +++++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index 1699ac7..2491aaa 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -1,4 +1,4 @@ -use crate::{outcome::SimulatedItem, SimCache, SimItem, SimOutcomeWithCache}; +use crate::{outcome::SimulatedItem, InnerDb, SimCache, SimDb, SimItem, SimOutcomeWithCache}; use alloy::{consensus::TxEnvelope, primitives::U256}; use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError}; use signet_evm::SignetLayered; @@ -21,12 +21,6 @@ use trevm::{ Block, BundleDriver, Cfg, DbConnect, EvmFactory, }; -/// A type alias for the database underlying the simulation. -pub type InnerDb = Arc>; - -/// A type alias for the database used in the simulation. -pub type SimDb = CacheOnWrite>; - pub struct SimEnv { inner: Arc>, } diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 1c3f80d..e02efc7 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -5,7 +5,7 @@ mod cache; pub use cache::SimCache; mod env; -pub use env::{InnerDb, SimDb, SimEnv}; +pub use env::SimEnv; mod item; pub use item::SimItem; @@ -15,3 +15,9 @@ pub(crate) use outcome::SimOutcomeWithCache; mod task; pub use task::BlockBuild; + +/// A type alias for the database underlying the simulation. +pub type InnerDb = std::sync::Arc>; + +/// A type alias for the database used in the simulation. +pub type SimDb = trevm::db::cow::CacheOnWrite>; From 174f3c165a44ee4b9c9f3d650a81a81e22a1a76b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 16:12:14 -0400 Subject: [PATCH 06/17] chore: remove unused deps --- crates/sim/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/sim/Cargo.toml b/crates/sim/Cargo.toml index f6f2644..3440387 100644 --- a/crates/sim/Cargo.toml +++ b/crates/sim/Cargo.toml @@ -10,8 +10,6 @@ repository.workspace = true [dependencies] alloy.workspace = true -dashmap = "6.1.0" -future-utils = "0.12.1" signet-bundle.workspace = true signet-evm.workspace = true signet-types.workspace = true From 6803d0d9a273fd27424c8d34555cbca8e6c2f976 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Apr 2025 18:36:31 -0400 Subject: [PATCH 07/17] lint: add all clippies to sim lib --- crates/sim/src/built.rs | 16 ++++++++++++++-- crates/sim/src/env.rs | 26 ++++++++++++++++++++++++-- crates/sim/src/item.rs | 4 ++-- crates/sim/src/lib.rs | 18 +++++++++++++++++- crates/sim/src/task.rs | 1 + 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs index 984f780..16ec23c 100644 --- a/crates/sim/src/built.rs +++ b/crates/sim/src/built.rs @@ -4,6 +4,7 @@ use alloy::{ primitives::{keccak256, Bytes, B256}, rlp::Buf, }; +use core::fmt; use signet_bundle::SignetEthBundle; use signet_zenith::{encode_txns, Alloy2718Coder, SignedOrder}; use std::sync::OnceLock; @@ -12,7 +13,7 @@ use tracing::{error, trace}; use crate::{outcome::SimulatedItem, SimItem}; /// A block that has been built by the simulator. -#[derive(Debug, Clone, Default)] +#[derive(Clone, Default)] pub struct BuiltBlock { /// The host fill actions. pub(crate) host_fills: Vec, @@ -28,6 +29,16 @@ pub struct BuiltBlock { pub(crate) hash: OnceLock, } +impl fmt::Debug for BuiltBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BuiltBlock") + .field("host_fills", &self.host_fills.len()) + .field("transactions", &self.transactions.len()) + .field("gas_used", &self.gas_used) + .finish() + } +} + impl BuiltBlock { /// Create a new `BuiltBlock` pub const fn new() -> Self { @@ -41,7 +52,7 @@ impl BuiltBlock { } /// Get the amount of gas used by the block. - pub fn gas_used(&self) -> u64 { + pub const fn gas_used(&self) -> u64 { self.gas_used } @@ -105,6 +116,7 @@ impl BuiltBlock { } } + /// Ingest a simulated item, extending the block. pub fn ingest(&mut self, item: SimulatedItem) { self.gas_used += item.gas_used; diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index 2491aaa..f924d06 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -1,5 +1,6 @@ use crate::{outcome::SimulatedItem, InnerDb, SimCache, SimDb, SimItem, SimOutcomeWithCache}; use alloy::{consensus::TxEnvelope, primitives::U256}; +use core::fmt; use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError}; use signet_evm::SignetLayered; use signet_types::config::SignetSystemConstants; @@ -21,10 +22,22 @@ use trevm::{ Block, BundleDriver, Cfg, DbConnect, EvmFactory, }; +/// A simulation environment. +/// +/// Contains enough information to run a simulation. pub struct SimEnv { inner: Arc>, } +impl fmt::Debug for SimEnv { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SimEnv") + .field("finish_by", &self.inner.finish_by) + .field("concurrency_limit", &self.inner.concurrency_limit) + .finish_non_exhaustive() + } +} + impl Deref for SimEnv { type Target = SimEnvInner; @@ -168,6 +181,15 @@ pub struct SimEnvInner { _pd: PhantomData Insp>, } +impl fmt::Debug for SimEnvInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SimEnvInner") + .field("finish_by", &self.finish_by) + .field("concurrency_limit", &self.concurrency_limit) + .finish_non_exhaustive() + } +} + impl SimEnvInner { /// Creates a new `SimFactory` instance. pub fn new( @@ -197,7 +219,7 @@ impl SimEnvInner { } /// Get a reference to the system constants. - pub fn constants(&self) -> &SignetSystemConstants { + pub const fn constants(&self) -> &SignetSystemConstants { &self.constants } @@ -212,7 +234,7 @@ impl SimEnvInner { } /// Get the exectuion timeout. - pub fn finish_by(&self) -> std::time::Instant { + pub const fn finish_by(&self) -> std::time::Instant { self.finish_by } diff --git a/crates/sim/src/item.rs b/crates/sim/src/item.rs index eacd1b6..039e34e 100644 --- a/crates/sim/src/item.rs +++ b/crates/sim/src/item.rs @@ -28,7 +28,7 @@ impl From for SimItem { impl SimItem { /// Get the bundle if it is a bundle. - pub fn as_bundle(&self) -> Option<&SignetEthBundle> { + pub const fn as_bundle(&self) -> Option<&SignetEthBundle> { match self { Self::Bundle(bundle) => Some(bundle), Self::Tx(_) => None, @@ -36,7 +36,7 @@ impl SimItem { } /// Get the transaction if it is a transaction. - pub fn as_tx(&self) -> Option<&TxEnvelope> { + pub const fn as_tx(&self) -> Option<&TxEnvelope> { match self { Self::Bundle(_) => None, Self::Tx(tx) => Some(tx), diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index e02efc7..7697a38 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -1,3 +1,19 @@ +//! Signet Sim +//! +//! A simple parallelized transaction simulation library. + +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + mod built; pub use built::BuiltBlock; @@ -11,7 +27,7 @@ mod item; pub use item::SimItem; mod outcome; -pub(crate) use outcome::SimOutcomeWithCache; +pub use outcome::SimOutcomeWithCache; mod task; pub use task::BlockBuild; diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index 6f1920b..ce132a1 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -6,6 +6,7 @@ use trevm::{ }; /// Builds a single block by repeatedly invoking [`SimEnv`]. +#[derive(Debug)] pub struct BlockBuild { env: SimEnv, block: BuiltBlock, From 5b47a0efe0c263de07593172c0da6d23bc5e8c0a Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Apr 2025 11:00:02 -0400 Subject: [PATCH 08/17] fix: better instantiators, remove duplicated finish_by --- crates/sim/src/env.rs | 45 +++++++++++++++++++++--------------------- crates/sim/src/lib.rs | 2 +- crates/sim/src/task.rs | 42 +++++++++++++++++++++++++++++++-------- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index f924d06..7caca4f 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -25,11 +25,11 @@ use trevm::{ /// A simulation environment. /// /// Contains enough information to run a simulation. -pub struct SimEnv { - inner: Arc>, +pub struct SharedSimEnv { + inner: Arc>, } -impl fmt::Debug for SimEnv { +impl fmt::Debug for SharedSimEnv { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SimEnv") .field("finish_by", &self.inner.finish_by) @@ -38,25 +38,25 @@ impl fmt::Debug for SimEnv { } } -impl Deref for SimEnv { - type Target = SimEnvInner; +impl Deref for SharedSimEnv { + type Target = SimEnv; fn deref(&self) -> &Self::Target { &self.inner } } -impl From> for SimEnv +impl From> for SharedSimEnv where Db: DatabaseRef + Send + Sync + 'static, Insp: Inspector>> + Default + Sync + 'static, { - fn from(inner: SimEnvInner) -> Self { + fn from(inner: SimEnv) -> Self { Self { inner: Arc::new(inner) } } } -impl SimEnv +impl SharedSimEnv where Db: DatabaseRef + Send + Sync + 'static, Insp: Inspector>> + Default + Sync + 'static, @@ -71,15 +71,11 @@ where concurrency_limit: usize, sim_items: SimCache, ) -> Self { - SimEnvInner::new(db, constants, cfg, block, finish_by, concurrency_limit, sim_items).into() + SimEnv::new(db, constants, cfg, block, finish_by, concurrency_limit, sim_items).into() } /// Run a simulation round, returning the best item. - pub async fn sim_round( - &mut self, - finish_by: std::time::Instant, - max_gas: u64, - ) -> Option { + pub async fn sim_round(&mut self, max_gas: u64) -> Option { let (best_tx, mut best_watcher) = watch::channel(None); let (done_tx, done_rx) = oneshot::channel(); @@ -127,13 +123,16 @@ where let _ = best_tx.send(Some(candidate)); } } - let _ = done_tx.send(()); }); + // we drop the Arc BEFORE notifying the listener that the + // simulation is done + drop(this); + let _ = done_tx.send(()); }); // Either simulation is done, or we time out select! { - _ = tokio::time::sleep_until(finish_by.into()) => {}, + _ = tokio::time::sleep_until(self.finish_by.into()) => {}, _ = done_rx => {}, } @@ -154,7 +153,7 @@ where } /// A simulation environment. -pub struct SimEnvInner { +pub struct SimEnv { /// The database to use for the simulation. This database will be wrapped /// in [`CacheOnWrite`] databases for each simulation. db: InnerDb, @@ -181,7 +180,7 @@ pub struct SimEnvInner { _pd: PhantomData Insp>, } -impl fmt::Debug for SimEnvInner { +impl fmt::Debug for SimEnv { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SimEnvInner") .field("finish_by", &self.finish_by) @@ -190,7 +189,7 @@ impl fmt::Debug for SimEnvInner { } } -impl SimEnvInner { +impl SimEnv { /// Creates a new `SimFactory` instance. pub fn new( db: Db, @@ -244,7 +243,7 @@ impl SimEnvInner { } } -impl DbConnect for SimEnvInner +impl DbConnect for SimEnv where Db: DatabaseRef + Send + Sync, Insp: Sync, @@ -258,7 +257,7 @@ where } } -impl EvmFactory for SimEnvInner +impl EvmFactory for SimEnv where Db: DatabaseRef + Send + Sync, Insp: Inspector>> + Default + Sync, @@ -275,7 +274,7 @@ where } } -impl SimEnvInner +impl SimEnv where Db: DatabaseRef + Send + Sync, Insp: Inspector>> + Default + Sync, @@ -355,7 +354,7 @@ where } } -impl SimEnvInner +impl SimEnv where Db: DatabaseRef, Insp: Inspector>> + Default + Sync, diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 7697a38..17af74a 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -21,7 +21,7 @@ mod cache; pub use cache::SimCache; mod env; -pub use env::SimEnv; +pub use env::{SharedSimEnv, SimEnv}; mod item; pub use item::SimItem; diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index ce132a1..df412c3 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -1,17 +1,25 @@ -use crate::{BuiltBlock, SimDb, SimEnv}; +use crate::{env::SimEnv, BuiltBlock, SharedSimEnv, SimCache, SimDb}; +use signet_types::config::SignetSystemConstants; use tokio::select; use trevm::{ helpers::Ctx, revm::{inspector::NoOpInspector, DatabaseRef, Inspector}, + Block, Cfg, }; /// Builds a single block by repeatedly invoking [`SimEnv`]. #[derive(Debug)] pub struct BlockBuild { - env: SimEnv, + /// The simulation environment. + env: SharedSimEnv, + + /// The block being built. block: BuiltBlock, + /// The deadline to produce a block by. finish_by: std::time::Instant, + + /// The maximum amount of gas to use in the built block max_gas: u64, } @@ -20,16 +28,34 @@ where Db: DatabaseRef + Send + Sync + 'static, Insp: Inspector>> + Default + Sync + 'static, { - /// Create a new simulation task. - pub const fn new(env: SimEnv, finish_by: std::time::Instant, max_gas: u64) -> Self { - Self { env, block: BuiltBlock::new(), finish_by, max_gas } + /// Create a new block building process. + pub fn new( + db: Db, + constants: SignetSystemConstants, + cfg: C, + block: B, + finish_by: std::time::Instant, + concurrency_limit: usize, + sim_items: SimCache, + max_gas: u64, + ) -> Self + where + C: Cfg + 'static, + B: Block + 'static, + { + let cfg: Box = Box::new(cfg); + let block: Box = Box::new(block); + + let env = SimEnv::new(db, constants, cfg, block, finish_by, concurrency_limit, sim_items); + let finish_by = env.finish_by(); + Self { env: env.into(), block: BuiltBlock::new(), finish_by, max_gas } } /// Run a simulation round, and accumulate the results into the block. - async fn round(&mut self, finish_by: std::time::Instant) { + async fn round(&mut self) { let gas_allowed = self.max_gas - self.block.gas_used(); - if let Some(simulated) = self.env.sim_round(finish_by, gas_allowed).await { + if let Some(simulated) = self.env.sim_round(gas_allowed).await { tracing::debug!(score = %simulated.score, gas_used = simulated.gas_used, "Adding item to block"); self.block.ingest(simulated); } @@ -41,7 +67,7 @@ where loop { select! { _ = tokio::time::sleep_until(self.finish_by.into()) => break, - _ = self.round(self.finish_by) => {} + _ = self.round() => {} } } From f9b9b96aeeaf34c703cfecebd90fcc657297f67a Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Apr 2025 16:20:58 -0400 Subject: [PATCH 09/17] fix: add a test :) --- crates/evm/src/driver.rs | 4 +- crates/evm/src/lib.rs | 20 ++-- crates/sim/Cargo.toml | 5 + crates/sim/src/cache.rs | 10 ++ crates/sim/src/env.rs | 193 +++++++++++++++++++++++----------- crates/sim/src/task.rs | 31 +++++- crates/sim/tests/basic_sim.rs | 123 ++++++++++++++++++++++ 7 files changed, 314 insertions(+), 72 deletions(-) create mode 100644 crates/sim/tests/basic_sim.rs diff --git a/crates/evm/src/driver.rs b/crates/evm/src/driver.rs index 2be197f..ed4c0e9 100644 --- a/crates/evm/src/driver.rs +++ b/crates/evm/src/driver.rs @@ -927,7 +927,7 @@ mod test { config::{HostConfig, PredeployTokens, RollupConfig}, test_utils::*, }; - use trevm::{revm::database::in_memory_db::InMemoryDB, NoopCfg}; + use trevm::revm::database::in_memory_db::InMemoryDB; /// Make a fake block with a specific number. pub(super) fn fake_block(number: u64) -> RecoveredBlock { @@ -1038,7 +1038,7 @@ mod test { } fn trevm(&self) -> crate::EvmNeedsBlock { - let mut trevm = test_signet_evm().fill_cfg(&NoopCfg); + let mut trevm = test_signet_evm(); for wallet in &self.wallets { let address = wallet.address(); trevm.test_set_balance(address, U256::from(ETH_TO_WEI * 100)); diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 15c8faa..86dc0f6 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -79,18 +79,26 @@ where /// Test utilities for the Signet EVM impl. #[cfg(any(test, feature = "test-utils"))] pub mod test_utils { + use reth::revm::{context::CfgEnv, primitives::hardfork::SpecId}; use signet_types::test_utils::*; use trevm::revm::database::in_memory_db::InMemoryDB; /// Create a new Signet EVM with an in-memory database for testing. - pub fn test_signet_evm() -> super::EvmNeedsCfg + pub fn test_signet_evm() -> super::EvmNeedsBlock { - let mut trevm = super::signet_evm(InMemoryDB::default(), TEST_CONSTANTS); + super::signet_evm(InMemoryDB::default(), TEST_CONSTANTS).fill_cfg(&TestCfg) + } + + /// Test configuration for the Signet EVM. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct TestCfg; - trevm.inner_mut_unchecked().data.ctx.modify_cfg(|cfg| { - cfg.chain_id = TEST_RU_CHAIN_ID; - }); + impl trevm::Cfg for TestCfg { + fn fill_cfg_env(&self, cfg_env: &mut reth::revm::context::CfgEnv) { + let CfgEnv { chain_id, spec, .. } = cfg_env; - trevm + *chain_id = TEST_RU_CHAIN_ID; + *spec = SpecId::default(); + } } } diff --git a/crates/sim/Cargo.toml b/crates/sim/Cargo.toml index 3440387..590abca 100644 --- a/crates/sim/Cargo.toml +++ b/crates/sim/Cargo.toml @@ -17,3 +17,8 @@ signet-zenith.workspace = true tokio.workspace = true tracing.workspace = true trevm.workspace = true + +[dev-dependencies] +signet-types = { workspace = true, features = ["test-utils"] } +signet-evm = { workspace = true, features = ["test-utils"] } +tracing-subscriber.workspace = true diff --git a/crates/sim/src/cache.rs b/crates/sim/src/cache.rs index 2eed36d..c49692a 100644 --- a/crates/sim/src/cache.rs +++ b/crates/sim/src/cache.rs @@ -42,6 +42,16 @@ impl SimCache { self.inner.read().unwrap().iter().rev().take(n).map(|(k, v)| (*k, v.clone())).collect() } + /// Get the number of items in the cache. + pub fn len(&self) -> usize { + self.inner.read().unwrap().len() + } + + /// True if the cache is empty. + pub fn is_empty(&self) -> bool { + self.inner.read().unwrap().is_empty() + } + /// Get an item by key. pub fn get(&self, key: u128) -> Option { self.inner.read().unwrap().get(&key).cloned() diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index 7caca4f..57ca9f6 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -1,5 +1,5 @@ use crate::{outcome::SimulatedItem, InnerDb, SimCache, SimDb, SimItem, SimOutcomeWithCache}; -use alloy::{consensus::TxEnvelope, primitives::U256}; +use alloy::{consensus::TxEnvelope, hex}; use core::fmt; use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError}; use signet_evm::SignetLayered; @@ -7,14 +7,15 @@ use signet_types::config::SignetSystemConstants; use std::{convert::Infallible, marker::PhantomData, ops::Deref, sync::Arc, time::Instant}; use tokio::{ select, - sync::{mpsc, oneshot, watch}, + sync::{mpsc, watch}, }; +use tracing::{instrument, trace, trace_span}; use trevm::{ db::{cow::CacheOnWrite, TryCachingDb}, helpers::Ctx, inspectors::{Layered, TimeLimit}, revm::{ - context::result::EVMError, + context::result::{EVMError, ExecutionResult}, database::{Cache, CacheDB}, inspector::NoOpInspector, DatabaseRef, Inspector, @@ -75,69 +76,28 @@ where } /// Run a simulation round, returning the best item. + #[instrument(skip(self))] pub async fn sim_round(&mut self, max_gas: u64) -> Option { let (best_tx, mut best_watcher) = watch::channel(None); - let (done_tx, done_rx) = oneshot::channel(); - let this = self.inner.clone(); // Spawn a blocking task to run the simulations. - tokio::task::spawn_blocking(move || async move { - // Pull the `n` best items from the cache. - let active_sim = this.sim_items.read_best(this.concurrency_limit); - - // If there are no items to simulate, return None. - let best_score = U256::ZERO; - - std::thread::scope(|scope| { - // Create a channel to send the results back. - let (candidates, mut candidates_rx) = mpsc::channel(this.concurrency_limit); - - // Spawn a thread per bundle to simulate. - for (identifier, item) in active_sim.iter() { - let this_ref = &this; - let c = candidates.clone(); - - scope.spawn(move || { - // If simulation is succesful, send the outcome via the - // channel. - if let Ok(candidate) = this_ref.simulate(*identifier, item) { - if candidate.gas_used <= max_gas { - let _ = c.blocking_send(candidate); - return; - } - }; - // fall through applies to all errors, occurs if - // the simulation fails or the gas limit is exceeded. - this_ref.sim_items.remove(*identifier); - }); - } - // Drop the TX so that the channel is closed when all threads - // are done. - drop(candidates); - - // Wait for each thread to finish. Find the best outcome. - while let Some(candidate) = candidates_rx.blocking_recv() { - if candidate.score > best_score { - let _ = best_tx.send(Some(candidate)); - } - } - }); - // we drop the Arc BEFORE notifying the listener that the - // simulation is done - drop(this); - let _ = done_tx.send(()); - }); + let sim_task = tokio::task::spawn_blocking(move || this.sim_round(max_gas, best_tx)); // Either simulation is done, or we time out select! { - _ = tokio::time::sleep_until(self.finish_by.into()) => {}, - _ = done_rx => {}, + _ = tokio::time::sleep_until(self.finish_by.into()) => { + trace!("Sim round timed out"); + }, + _ = sim_task => { + trace!("Sim round done"); + }, } // Check what the current best outcome is. let best = best_watcher.borrow_and_update(); + trace!(score = %best.as_ref().map(|candidate| candidate.score).unwrap_or_default(), "Read outcome from channel"); let outcome = best.as_ref()?; // Remove the item from the cache. @@ -222,6 +182,11 @@ impl SimEnv { &self.constants } + /// Get a reference to the cache of items to simulate. + pub const fn sim_items(&self) -> &SimCache { + &self.sim_items + } + /// Get a reference to the chain cfg. pub fn cfg(&self) -> &dyn Cfg { &self.cfg @@ -283,6 +248,7 @@ where /// /// This function runs the simulation in a separate thread and waits for /// the result or the deadline to expire. + #[instrument(skip_all, fields(identifier, tx_hash = %transaction.hash()))] fn simulate_tx( &self, identifier: u128, @@ -291,23 +257,44 @@ where let trevm = self.create_with_block(&self.cfg, &self.block).unwrap(); // Get the initial beneficiary balance - let beneificiary = trevm.beneficiary(); + let beneficiary = trevm.beneficiary(); let initial_beneficiary_balance = - trevm.try_read_balance_ref(beneificiary).map_err(EVMError::Database)?; + trevm.try_read_balance_ref(beneficiary).map_err(EVMError::Database)?; // If succesful, take the cache. If failed, return the error. match trevm.run_tx(transaction) { Ok(trevm) => { - // Get the beneficiary balance after the transaction and calculate the - // increase - let beneficiary_balance = - trevm.try_read_balance_ref(beneificiary).map_err(EVMError::Database)?; - let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance); - // Get the simulation results let gas_used = trevm.result().gas_used(); + let success = trevm.result().is_success(); + let reason = trevm.result().output().cloned().map(hex::encode); + let halted = trevm.result().is_halt(); + let halt_reason = if let ExecutionResult::Halt { reason, .. } = trevm.result() { + Some(reason) + } else { + None + } + .cloned(); + let cache = trevm.accept_state().into_db().into_cache(); + let beneficiary_balance = cache + .accounts + .get(&beneficiary) + .map(|acct| acct.info.balance) + .unwrap_or_default(); + let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance); + + trace!( + gas_used = gas_used, + score = %score, + reverted = !success, + halted, + halt_reason = ?if halted { halt_reason } else { None }, + revert_reason = if !success { reason } else { None }, + "Simulation complete" + ); + // Create the outcome Ok(SimOutcomeWithCache { identifier, score, cache, gas_used }) } @@ -316,6 +303,7 @@ where } /// Simulates a bundle on the current environment. + #[instrument(skip_all, fields(identifier, uuid = bundle.replacement_uuid()))] fn simulate_bundle( &self, identifier: u128, @@ -338,6 +326,12 @@ where let gas_used = driver.total_gas_used(); let cache = trevm.into_db().into_cache(); + trace!( + gas_used = gas_used, + score = %score, + "Bundle simulation successful" + ); + Ok(SimOutcomeWithCache { identifier, score, cache, gas_used }) } @@ -352,6 +346,83 @@ where SimItem::Tx(tx) => self.simulate_tx(identifier, tx), } } + + #[instrument(skip_all)] + fn sim_round( + self: Arc, + max_gas: u64, + best_tx: watch::Sender>, + ) { + // Pull the `n` best items from the cache. + let active_sim = self.sim_items.read_best(self.concurrency_limit); + + // Create a channel to send the results back. + let (candidates, mut candidates_rx) = mpsc::channel(self.concurrency_limit); + + let outer = trace_span!("sim_thread", candidates = active_sim.len()); + let outer_ref = &outer; + let _og = outer.enter(); + + // to be used in the scope + let this_ref = &self; + + std::thread::scope(move |scope| { + // Spawn a thread per bundle to simulate. + for (identifier, item) in active_sim.into_iter() { + let c = candidates.clone(); + + scope.spawn(move || { + let _ig = trace_span!(parent: outer_ref, "sim_task", identifier = %identifier) + .entered(); + + // If simulation is succesful, send the outcome via the + // channel. + match this_ref.simulate(identifier, &item) { + Ok(candidate) => { + if candidate.gas_used <= max_gas { + // shortcut return on success + let _ = c.blocking_send(candidate); + return; + } + trace!(gas_used = candidate.gas_used, max_gas, "Gas limit exceeded"); + } + Err(e) => { + trace!(?identifier, ?e, "Simulation failed"); + } + }; + // fall through applies to all errors, occurs if + // the simulation fails or the gas limit is exceeded. + this_ref.sim_items.remove(identifier); + }); + } + // Drop the TX so that the channel is closed when all threads + // are done. + drop(candidates); + + // Wait for each thread to finish. Find the best outcome. + while let Some(candidate) = candidates_rx.blocking_recv() { + // Update the best score and send it to the channel. + let _ = best_tx.send_if_modified(|current| { + let best_score = current.as_ref().map(|c| c.score).unwrap_or_default(); + let current_id = current.as_ref().map(|c| c.identifier); + + if candidate.score > best_score { + trace!( + old_best = ?best_score, + old_identifier = current_id, + new_best = %candidate.score, + identifier = candidate.identifier, + "Found better candidate" + ); + *current = Some(candidate); + true + } else { + false + } + }); + } + }); + } } impl SimEnv diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index df412c3..f47ed31 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -1,6 +1,7 @@ use crate::{env::SimEnv, BuiltBlock, SharedSimEnv, SimCache, SimDb}; use signet_types::config::SignetSystemConstants; use tokio::select; +use tracing::{debug, info_span, trace}; use trevm::{ helpers::Ctx, revm::{inspector::NoOpInspector, DatabaseRef, Inspector}, @@ -29,6 +30,7 @@ where Insp: Inspector>> + Default + Sync + 'static, { /// Create a new block building process. + #[allow(clippy::too_many_arguments)] // sadge but. pub fn new( db: Db, constants: SignetSystemConstants, @@ -46,7 +48,15 @@ where let cfg: Box = Box::new(cfg); let block: Box = Box::new(block); - let env = SimEnv::new(db, constants, cfg, block, finish_by, concurrency_limit, sim_items); + let env = SimEnv::::new( + db, + constants, + cfg, + block, + finish_by, + concurrency_limit, + sim_items, + ); let finish_by = env.finish_by(); Self { env: env.into(), block: BuiltBlock::new(), finish_by, max_gas } } @@ -63,14 +73,29 @@ where /// Run several rounds, building pub async fn build(mut self) -> BuiltBlock { + let mut i = 1; // Run until the deadline is reached. loop { + let _guard = info_span!("build", round = i).entered(); select! { - _ = tokio::time::sleep_until(self.finish_by.into()) => break, - _ = self.round() => {} + _ = tokio::time::sleep_until(self.finish_by.into()) => { + debug!("Deadline reached, stopping sim loop"); + break; + }, + _ = self.round() => { + i+= 1; + let remaining = self.env.sim_items().len(); + trace!(%remaining, round = i, "Round completed"); + if remaining == 0 { + debug!("No more items to simulate, stopping sim loop"); + break; + } + } } } + debug!(rounds = i, transactions = self.block.transactions.len(), "Building completed",); + self.block } } diff --git a/crates/sim/tests/basic_sim.rs b/crates/sim/tests/basic_sim.rs new file mode 100644 index 0000000..264a573 --- /dev/null +++ b/crates/sim/tests/basic_sim.rs @@ -0,0 +1,123 @@ +use alloy::{ + consensus::{ + constants::{ETH_TO_WEI, GWEI_TO_WEI}, + Signed, TxEip1559, TxEnvelope, + }, + network::TxSigner, + primitives::{Address, TxKind, U256}, + signers::Signature, +}; +use signet_evm::test_utils::TestCfg; +use signet_sim::{BlockBuild, SimCache}; +use signet_types::test_utils::*; +use std::sync::Arc; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; +use trevm::{ + revm::{ + database::InMemoryDB, + inspector::NoOpInspector, + state::{Account, AccountInfo, EvmState}, + Database, DatabaseCommit, + }, + NoopBlock, +}; + +#[tokio::test] +pub async fn test_simulator() { + let filter = EnvFilter::from_default_env(); + let fmt = tracing_subscriber::fmt::layer().with_filter(filter); + let registry = tracing_subscriber::registry().with(fmt); + registry.try_init().unwrap(); + + let mut db = InMemoryDB::default(); + + // increment the balance for each test signer + TEST_USERS.iter().copied().for_each(|user| { + modify_account(&mut db, user, |acct| acct.balance = U256::from(1000 * ETH_TO_WEI)).unwrap(); + }); + + let db = Arc::new(db); + + // Set up 10 simple sends with escalating priority fee + let sim_cache = SimCache::new(); + for (i, sender) in TEST_SIGNERS.iter().enumerate() { + sim_cache.add_item( + signed_simple_send( + sender, + TEST_USERS[i], + U256::from(1000), + (10 - i) as u128 * GWEI_TO_WEI as u128, + ) + .await, + ); + } + + // Set up the simulator + let built = BlockBuild::<_, NoOpInspector>::new( + db, + TEST_CONSTANTS, + TestCfg, + NoopBlock, + std::time::Instant::now() + std::time::Duration::from_millis(200), + 10, + sim_cache, + 100_000_000, + ) + .build() + .await; + + assert!(built.transactions().len() == 10); + + // This asserts that the builder has sorted the transactions by priority + // fee. + assert!(built.transactions().windows(2).all(|w| { + let tx1 = w[0].as_eip1559().unwrap().tx().max_priority_fee_per_gas; + let tx2 = w[1].as_eip1559().unwrap().tx().max_priority_fee_per_gas; + tx1 >= tx2 + })); +} + +/// Modify an account with a closure and commit the modified account. +/// +/// This code is reproduced and modified from trevm +fn modify_account(db: &mut Db, addr: Address, f: F) -> Result +where + F: FnOnce(&mut AccountInfo), + Db: Database + DatabaseCommit, +{ + let mut acct: AccountInfo = db.basic(addr)?.unwrap_or_default(); + let old = acct.clone(); + f(&mut acct); + + let mut acct: Account = acct.into(); + acct.mark_touch(); + + let changes: EvmState = [(addr, acct)].into_iter().collect(); + db.commit(changes); + Ok(old) +} + +fn simple_send(to: Address, value: U256, mpfpg: u128) -> TxEip1559 { + TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(to), + value, + chain_id: TEST_RU_CHAIN_ID, + max_fee_per_gas: GWEI_TO_WEI as u128 * 100, + max_priority_fee_per_gas: mpfpg, + ..Default::default() + } +} + +async fn signed_simple_send>( + from: S, + to: Address, + value: U256, + mpfpg: u128, +) -> TxEnvelope { + let mut tx = simple_send(to, value, mpfpg); + let res = from.sign_transaction(&mut tx).await.unwrap(); + + Signed::new_unhashed(tx, res).into() +} From 74f1596a31daec70d421ba52d6832ea127d9c313 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 18 Apr 2025 10:39:28 -0400 Subject: [PATCH 10/17] fix: send fut build --- crates/sim/src/cache.rs | 13 +++++++++- crates/sim/src/task.rs | 47 ++++++++++++++++++++--------------- crates/sim/tests/basic_sim.rs | 4 +++ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/crates/sim/src/cache.rs b/crates/sim/src/cache.rs index c49692a..b7bb31a 100644 --- a/crates/sim/src/cache.rs +++ b/crates/sim/src/cache.rs @@ -1,3 +1,5 @@ +use trevm::MIN_TRANSACTION_GAS; + use crate::SimItem; use core::fmt; use std::{ @@ -69,13 +71,22 @@ impl SimCache { // Calculate the total fee for the item. let mut score = item.calculate_total_fee(); + // Sanity check. This should never be true + if score < MIN_TRANSACTION_GAS as u128 { + return; + } + let mut inner = self.inner.write().unwrap(); // If it has the same score, we decrement (prioritizing earlier items) while inner.contains_key(&score) { - score -= 1; + score.saturating_sub(1); } + inner.insert(score, item); + if inner.len() > self.capacity { + inner.pop_first(); + } } /// Clean the cache by removing bundles that are not valid in the current diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index f47ed31..2ba7cca 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -1,7 +1,9 @@ +use std::future::Future; + use crate::{env::SimEnv, BuiltBlock, SharedSimEnv, SimCache, SimDb}; use signet_types::config::SignetSystemConstants; use tokio::select; -use tracing::{debug, info_span, trace}; +use tracing::{debug, info_span, trace, Instrument}; use trevm::{ helpers::Ctx, revm::{inspector::NoOpInspector, DatabaseRef, Inspector}, @@ -72,30 +74,35 @@ where } /// Run several rounds, building - pub async fn build(mut self) -> BuiltBlock { - let mut i = 1; - // Run until the deadline is reached. - loop { - let _guard = info_span!("build", round = i).entered(); - select! { - _ = tokio::time::sleep_until(self.finish_by.into()) => { - debug!("Deadline reached, stopping sim loop"); - break; - }, - _ = self.round() => { - i+= 1; - let remaining = self.env.sim_items().len(); - trace!(%remaining, round = i, "Round completed"); - if remaining == 0 { - debug!("No more items to simulate, stopping sim loop"); + pub fn build(mut self) -> impl Future + Send { + async move { + let mut i = 1; + // Run until the deadline is reached. + loop { + let span = info_span!("build", round = i); + let finish_by = self.finish_by.into(); + let fut = self.round().instrument(span); + + select! { + _ = tokio::time::sleep_until(finish_by) => { + debug!("Deadline reached, stopping sim loop"); break; + }, + _ = fut => { + i+= 1; + let remaining = self.env.sim_items().len(); + trace!(%remaining, round = i, "Round completed"); + if remaining == 0 { + debug!("No more items to simulate, stopping sim loop"); + break; + } } } } - } - debug!(rounds = i, transactions = self.block.transactions.len(), "Building completed",); + debug!(rounds = i, transactions = self.block.transactions.len(), "Building completed",); - self.block + self.block + } } } diff --git a/crates/sim/tests/basic_sim.rs b/crates/sim/tests/basic_sim.rs index 264a573..cbe0cad 100644 --- a/crates/sim/tests/basic_sim.rs +++ b/crates/sim/tests/basic_sim.rs @@ -52,6 +52,8 @@ pub async fn test_simulator() { ); } + let time = std::time::Instant::now(); + // Set up the simulator let built = BlockBuild::<_, NoOpInspector>::new( db, @@ -75,6 +77,8 @@ pub async fn test_simulator() { let tx2 = w[1].as_eip1559().unwrap().tx().max_priority_fee_per_gas; tx1 >= tx2 })); + + dbg!(time.elapsed()); } /// Modify an account with a closure and commit the modified account. From 4d15d324edcb7f0f793a7b479e458a67f92d5672 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 18 Apr 2025 10:39:57 -0400 Subject: [PATCH 11/17] fix: break loop --- crates/sim/src/cache.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sim/src/cache.rs b/crates/sim/src/cache.rs index b7bb31a..13a152b 100644 --- a/crates/sim/src/cache.rs +++ b/crates/sim/src/cache.rs @@ -79,8 +79,8 @@ impl SimCache { let mut inner = self.inner.write().unwrap(); // If it has the same score, we decrement (prioritizing earlier items) - while inner.contains_key(&score) { - score.saturating_sub(1); + while inner.contains_key(&score) && score != 0 { + score = score.saturating_sub(1); } inner.insert(score, item); From a3ba3c8d8ee9a2d8dc29045587dfc17fa6a467ba Mon Sep 17 00:00:00 2001 From: James Date: Fri, 18 Apr 2025 10:46:16 -0400 Subject: [PATCH 12/17] chore: cleanup and clippy --- crates/sim/Cargo.toml | 3 ++ crates/sim/src/item.rs | 17 +++++++++++- crates/sim/src/task.rs | 63 +++++++++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 26 deletions(-) diff --git a/crates/sim/Cargo.toml b/crates/sim/Cargo.toml index 590abca..a428f19 100644 --- a/crates/sim/Cargo.toml +++ b/crates/sim/Cargo.toml @@ -22,3 +22,6 @@ trevm.workspace = true signet-types = { workspace = true, features = ["test-utils"] } signet-evm = { workspace = true, features = ["test-utils"] } tracing-subscriber.workspace = true + +[features] +test-utils = ["signet-types/test-utils", "signet-evm/test-utils"] \ No newline at end of file diff --git a/crates/sim/src/item.rs b/crates/sim/src/item.rs index 039e34e..7c63d7d 100644 --- a/crates/sim/src/item.rs +++ b/crates/sim/src/item.rs @@ -1,6 +1,7 @@ use alloy::{ - consensus::{Transaction, TxEnvelope}, + consensus::{Signed, Transaction, TxEip1559, TxEnvelope}, eips::Decodable2718, + signers::Signature, }; use signet_bundle::SignetEthBundle; @@ -61,3 +62,17 @@ impl SimItem { } } } + +#[cfg(any(test, feature = "test-utils"))] +impl SimItem { + /// Create an invalid test item. This will be a [`TxEnvelope`] containing + /// an EIP-1559 transaction with an invalid signature and hash. + pub fn invalid_test_item() -> Self { + TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559::default(), + Signature::test_signature(), + Default::default(), + )) + .into() + } +} diff --git a/crates/sim/src/task.rs b/crates/sim/src/task.rs index 2ba7cca..8fc76ad 100644 --- a/crates/sim/src/task.rs +++ b/crates/sim/src/task.rs @@ -1,5 +1,3 @@ -use std::future::Future; - use crate::{env::SimEnv, BuiltBlock, SharedSimEnv, SimCache, SimDb}; use signet_types::config::SignetSystemConstants; use tokio::select; @@ -74,35 +72,50 @@ where } /// Run several rounds, building - pub fn build(mut self) -> impl Future + Send { - async move { - let mut i = 1; - // Run until the deadline is reached. - loop { - let span = info_span!("build", round = i); - let finish_by = self.finish_by.into(); - let fut = self.round().instrument(span); + pub async fn build(mut self) -> BuiltBlock { + let mut i = 1; + // Run until the deadline is reached. + loop { + let span = info_span!("build", round = i); + let finish_by = self.finish_by.into(); + let fut = self.round().instrument(span); - select! { - _ = tokio::time::sleep_until(finish_by) => { - debug!("Deadline reached, stopping sim loop"); + select! { + _ = tokio::time::sleep_until(finish_by) => { + debug!("Deadline reached, stopping sim loop"); + break; + }, + _ = fut => { + i+= 1; + let remaining = self.env.sim_items().len(); + trace!(%remaining, round = i, "Round completed"); + if remaining == 0 { + debug!("No more items to simulate, stopping sim loop"); break; - }, - _ = fut => { - i+= 1; - let remaining = self.env.sim_items().len(); - trace!(%remaining, round = i, "Round completed"); - if remaining == 0 { - debug!("No more items to simulate, stopping sim loop"); - break; - } } } } + } - debug!(rounds = i, transactions = self.block.transactions.len(), "Building completed",); + debug!(rounds = i, transactions = self.block.transactions.len(), "Building completed",); - self.block - } + self.block + } +} + +#[cfg(test)] +mod test { + use std::future::Future; + + use super::*; + + /// Compile-time check to ensure that the block building process is + /// `Send`. + fn _build_fut_is_send(b: BlockBuild) + where + Db: DatabaseRef + Send + Sync + 'static, + Insp: Inspector>> + Default + Sync + 'static, + { + let _: Box + Send> = Box::new(b.build()); } } From a7e626eb0b221ae9818705fa44c1207002df94de Mon Sep 17 00:00:00 2001 From: James Date: Tue, 22 Apr 2025 09:54:40 -0400 Subject: [PATCH 13/17] fix: bad tx scoring --- crates/sim/src/cache.rs | 103 ++++++++++++++++++++++++++++------ crates/sim/src/item.rs | 38 +++++++++---- crates/sim/tests/basic_sim.rs | 1 + 3 files changed, 114 insertions(+), 28 deletions(-) diff --git a/crates/sim/src/cache.rs b/crates/sim/src/cache.rs index 13a152b..fdaf1bc 100644 --- a/crates/sim/src/cache.rs +++ b/crates/sim/src/cache.rs @@ -1,10 +1,8 @@ -use trevm::MIN_TRANSACTION_GAS; - use crate::SimItem; use core::fmt; use std::{ collections::BTreeMap, - sync::{Arc, RwLock}, + sync::{Arc, RwLock, RwLockWriteGuard}, }; /// A cache for the simulator. @@ -64,28 +62,55 @@ impl SimCache { self.inner.write().unwrap().remove(&key) } - /// Create a new `SimCache` instance. - pub fn add_item(&self, item: impl Into) { + fn add_inner( + guard: &mut RwLockWriteGuard<'_, BTreeMap>, + mut score: u128, + item: SimItem, + capacity: usize, + ) { + // If it has the same score, we decrement (prioritizing earlier items) + while guard.contains_key(&score) && score != 0 { + score = score.saturating_sub(1); + } + + if guard.len() >= capacity { + // If we are at capacity, we need to remove the lowest score + guard.pop_first(); + } + + guard.entry(score).or_insert(item); + } + + /// Add an item to the cache. + /// + /// The basefee is used to calculate an estimated fee for the item. + pub fn add_item(&self, item: impl Into, basefee: u64) { let item = item.into(); // Calculate the total fee for the item. - let mut score = item.calculate_total_fee(); - - // Sanity check. This should never be true - if score < MIN_TRANSACTION_GAS as u128 { - return; - } + let score = item.calculate_total_fee(basefee); let mut inner = self.inner.write().unwrap(); - // If it has the same score, we decrement (prioritizing earlier items) - while inner.contains_key(&score) && score != 0 { - score = score.saturating_sub(1); - } + Self::add_inner(&mut inner, score, item, self.capacity); + } - inner.insert(score, item); - if inner.len() > self.capacity { - inner.pop_first(); + /// Add an iterator of items to the cache. This locks the cache only once + pub fn add_items(&self, item: I, basefee: u64) + where + I: IntoIterator, + Item: Into, + { + let iter = item.into_iter().map(|item| { + let item = item.into(); + let score = item.calculate_total_fee(basefee); + (score, item) + }); + + let mut inner = self.inner.write().unwrap(); + + for (score, item) in iter { + Self::add_inner(&mut inner, score, item, self.capacity); } } @@ -126,3 +151,45 @@ impl SimCache { inner.clear(); } } + +#[cfg(test)] +mod test { + use super::*; + use crate::SimItem; + + #[test] + fn test_cache() { + let items = vec![ + SimItem::invalid_item_with_score(100, 1), + SimItem::invalid_item_with_score(100, 2), + SimItem::invalid_item_with_score(100, 3), + ]; + + let cache = SimCache::with_capacity(2); + cache.add_items(items, 0); + + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(300), Some(SimItem::invalid_item_with_score(100, 3))); + assert_eq!(cache.get(200), Some(SimItem::invalid_item_with_score(100, 2))); + assert_eq!(cache.get(100), None); + } + + #[test] + fn overlap_at_zero() { + let items = vec![ + SimItem::invalid_item_with_score(1, 1), + SimItem::invalid_item_with_score(1, 1), + SimItem::invalid_item_with_score(1, 1), + ]; + + let cache = SimCache::with_capacity(2); + cache.add_items(items, 0); + + dbg!(&*cache.inner.read().unwrap()); + + assert_eq!(cache.len(), 2); + assert_eq!(cache.get(0), Some(SimItem::invalid_item_with_score(1, 1))); + assert_eq!(cache.get(1), Some(SimItem::invalid_item_with_score(1, 1))); + assert_eq!(cache.get(2), None); + } +} diff --git a/crates/sim/src/item.rs b/crates/sim/src/item.rs index 7c63d7d..578ac13 100644 --- a/crates/sim/src/item.rs +++ b/crates/sim/src/item.rs @@ -1,12 +1,11 @@ use alloy::{ - consensus::{Signed, Transaction, TxEip1559, TxEnvelope}, + consensus::{Transaction, TxEnvelope}, eips::Decodable2718, - signers::Signature, }; use signet_bundle::SignetEthBundle; /// An item that can be simulated. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum SimItem { /// A bundle to be simulated. Bundle(SignetEthBundle), @@ -46,7 +45,7 @@ impl SimItem { /// Calculate the maximum gas fee payable, this may be used as a heuristic /// to determine simulation order. - pub fn calculate_total_fee(&self) -> u128 { + pub fn calculate_total_fee(&self, basefee: u64) -> u128 { match self { Self::Bundle(bundle) => { let mut total_tx_fee = 0; @@ -54,11 +53,11 @@ impl SimItem { let Ok(tx) = TxEnvelope::decode_2718(&mut tx.as_ref()) else { continue; }; - total_tx_fee += tx.effective_gas_price(None) * tx.gas_limit() as u128; + total_tx_fee += tx.effective_gas_price(Some(basefee)) * tx.gas_limit() as u128; } total_tx_fee } - Self::Tx(tx) => tx.effective_gas_price(None) * tx.gas_limit() as u128, + Self::Tx(tx) => tx.effective_gas_price(Some(basefee)) * tx.gas_limit() as u128, } } } @@ -67,12 +66,31 @@ impl SimItem { impl SimItem { /// Create an invalid test item. This will be a [`TxEnvelope`] containing /// an EIP-1559 transaction with an invalid signature and hash. - pub fn invalid_test_item() -> Self { - TxEnvelope::Eip1559(Signed::new_unchecked( - TxEip1559::default(), - Signature::test_signature(), + pub fn invalid_item() -> Self { + TxEnvelope::Eip1559(alloy::consensus::Signed::new_unchecked( + alloy::consensus::TxEip1559::default(), + alloy::signers::Signature::test_signature(), Default::default(), )) .into() } + + /// Create an invalid test item with a given gas limit and max priority fee + /// per gas. As [`Self::invalid_test_item`] but with a custom gas limit and + /// `max_priority_fee_per_gas`. + pub fn invalid_item_with_score(gas_limit: u64, mpfpg: u128) -> Self { + let tx = alloy::consensus::TxEip1559 { + gas_limit, + max_priority_fee_per_gas: mpfpg, + max_fee_per_gas: alloy::consensus::constants::GWEI_TO_WEI as u128, + ..Default::default() + }; + + let tx = TxEnvelope::Eip1559(alloy::consensus::Signed::new_unchecked( + tx, + alloy::signers::Signature::test_signature(), + Default::default(), + )); + tx.into() + } } diff --git a/crates/sim/tests/basic_sim.rs b/crates/sim/tests/basic_sim.rs index cbe0cad..4d38005 100644 --- a/crates/sim/tests/basic_sim.rs +++ b/crates/sim/tests/basic_sim.rs @@ -49,6 +49,7 @@ pub async fn test_simulator() { (10 - i) as u128 * GWEI_TO_WEI as u128, ) .await, + 0, ); } From 726ea61892c430b9602f3c8b3ed39a25a23472a6 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 22 Apr 2025 16:09:01 -0400 Subject: [PATCH 14/17] docs: more of em --- crates/sim/src/outcome.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/sim/src/outcome.rs b/crates/sim/src/outcome.rs index 6bce223..67a9655 100644 --- a/crates/sim/src/outcome.rs +++ b/crates/sim/src/outcome.rs @@ -25,7 +25,13 @@ pub struct SimOutcomeWithCache { /// An item after simulation, containing the score and gas used. #[derive(Debug, Clone)] pub struct SimulatedItem { + /// The score of the simulation, a [`U256`] value that represents the + /// increase in the beneficiary's balance. pub score: U256, + + /// The total amount of gas used by the simulation. pub gas_used: u64, + + /// The transaction or bundle that was simulated. pub item: SimItem, } From badf52f400e7a5afae4ae29857f28c99a95b2158 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 24 Apr 2025 08:45:12 -0400 Subject: [PATCH 15/17] fix: some docs and built api --- crates/sim/src/built.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs index 16ec23c..8acea11 100644 --- a/crates/sim/src/built.rs +++ b/crates/sim/src/built.rs @@ -66,9 +66,9 @@ impl BuiltBlock { self.transactions.is_empty() } - /// Returns the current list of transactions included in this block - pub fn transactions(&self) -> Vec { - self.transactions.clone() + /// Get the current list of transactions included in this block. + pub fn transactions(&self) -> &[TxEnvelope] { + &self.transactions } /// Unseal the block @@ -77,7 +77,8 @@ impl BuiltBlock { self.hash.take(); } - /// Seal the block by encoding the transactions and calculating the contentshash. + /// Seal the block by encoding the transactions and calculating the hash of + /// the block contents. pub(crate) fn seal(&self) { self.raw_encoding.get_or_init(|| encode_txns::(&self.transactions).into()); self.hash.get_or_init(|| keccak256(self.raw_encoding.get().unwrap().as_ref())); @@ -126,16 +127,16 @@ impl BuiltBlock { } } - /// Encode the in-progress block + /// Encode the in-progress block. pub(crate) fn encode_raw(&self) -> &Bytes { self.seal(); self.raw_encoding.get().unwrap() } /// Calculate the hash of the in-progress block, finishing the block. - pub fn contents_hash(&self) -> B256 { + pub fn contents_hash(&self) -> &B256 { self.seal(); - *self.hash.get().unwrap() + self.hash.get().unwrap() } /// Convert the in-progress block to sign request contents. From 4e11e22af2e61d7981131cfe3536810851bc7498 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 24 Apr 2025 08:49:43 -0400 Subject: [PATCH 16/17] fix: simplify changed --- crates/sim/src/built.rs | 2 +- crates/sim/src/env.rs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs index 8acea11..12f1282 100644 --- a/crates/sim/src/built.rs +++ b/crates/sim/src/built.rs @@ -84,7 +84,7 @@ impl BuiltBlock { self.hash.get_or_init(|| keccak256(self.raw_encoding.get().unwrap().as_ref())); } - /// Ingest a transaction into the in-progress block. Fails + /// Ingest a transaction into the in-progress block. pub fn ingest_tx(&mut self, tx: TxEnvelope) { trace!(hash = %tx.tx_hash(), "ingesting tx"); self.unseal(); diff --git a/crates/sim/src/env.rs b/crates/sim/src/env.rs index 57ca9f6..900988d 100644 --- a/crates/sim/src/env.rs +++ b/crates/sim/src/env.rs @@ -406,7 +406,8 @@ where let best_score = current.as_ref().map(|c| c.score).unwrap_or_default(); let current_id = current.as_ref().map(|c| c.identifier); - if candidate.score > best_score { + let changed = candidate.score > best_score; + if changed { trace!( old_best = ?best_score, old_identifier = current_id, @@ -415,10 +416,8 @@ where "Found better candidate" ); *current = Some(candidate); - true - } else { - false } + changed }); } }); From 80de142f345e71c03cbd53f8abfa844017bd71ab Mon Sep 17 00:00:00 2001 From: James Date: Thu, 24 Apr 2025 08:52:32 -0400 Subject: [PATCH 17/17] lint: clippy --- crates/sim/src/built.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/sim/src/built.rs b/crates/sim/src/built.rs index 12f1282..4080065 100644 --- a/crates/sim/src/built.rs +++ b/crates/sim/src/built.rs @@ -67,6 +67,7 @@ impl BuiltBlock { } /// Get the current list of transactions included in this block. + #[allow(clippy::missing_const_for_fn)] // false positive, const deref pub fn transactions(&self) -> &[TxEnvelope] { &self.transactions }