signet_sim/
env.rs

1use crate::{outcome::SimulatedItem, InnerDb, SimCache, SimDb, SimItem, SimOutcomeWithCache};
2use alloy::{consensus::TxEnvelope, hex};
3use core::fmt;
4use signet_bundle::{SignetEthBundle, SignetEthBundleDriver, SignetEthBundleError};
5use signet_evm::SignetLayered;
6use signet_types::constants::SignetSystemConstants;
7use std::{convert::Infallible, marker::PhantomData, ops::Deref, sync::Arc, time::Instant};
8use tokio::{
9    select,
10    sync::{mpsc, watch},
11};
12use tracing::{instrument, trace, trace_span};
13use trevm::{
14    db::{cow::CacheOnWrite, TryCachingDb},
15    helpers::Ctx,
16    inspectors::{Layered, TimeLimit},
17    revm::{
18        context::{
19            result::{EVMError, ExecutionResult},
20            BlockEnv, CfgEnv,
21        },
22        database::{Cache, CacheDB},
23        inspector::NoOpInspector,
24        DatabaseRef, Inspector,
25    },
26    Block, BundleDriver, Cfg, DbConnect, EvmFactory,
27};
28
29/// A simulation environment.
30///
31/// Contains enough information to run a simulation.
32pub struct SharedSimEnv<Db, Insp = NoOpInspector> {
33    inner: Arc<SimEnv<Db, Insp>>,
34}
35
36impl<Db, Insp> fmt::Debug for SharedSimEnv<Db, Insp> {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.debug_struct("SimEnv")
39            .field("finish_by", &self.inner.finish_by)
40            .field("concurrency_limit", &self.inner.concurrency_limit)
41            .finish_non_exhaustive()
42    }
43}
44
45impl<Db, Insp> Deref for SharedSimEnv<Db, Insp> {
46    type Target = SimEnv<Db, Insp>;
47
48    fn deref(&self) -> &Self::Target {
49        &self.inner
50    }
51}
52
53impl<Db, Insp> From<SimEnv<Db, Insp>> for SharedSimEnv<Db, Insp>
54where
55    Db: DatabaseRef + Send + Sync + 'static,
56    Insp: Inspector<Ctx<SimDb<Db>>> + Default + Sync + 'static,
57{
58    fn from(inner: SimEnv<Db, Insp>) -> Self {
59        Self { inner: Arc::new(inner) }
60    }
61}
62
63impl<Db, Insp> SharedSimEnv<Db, Insp>
64where
65    Db: DatabaseRef + Send + Sync + 'static,
66    Insp: Inspector<Ctx<SimDb<Db>>> + Default + Sync + 'static,
67{
68    /// Creates a new `SimEnv` instance.
69    pub fn new<C, B>(
70        db: Db,
71        constants: SignetSystemConstants,
72        cfg: C,
73        block: B,
74        finish_by: std::time::Instant,
75        concurrency_limit: usize,
76        sim_items: SimCache,
77    ) -> Self
78    where
79        C: Cfg,
80        B: Block,
81    {
82        SimEnv::new(db, constants, cfg, block, finish_by, concurrency_limit, sim_items).into()
83    }
84
85    /// Run a simulation round, returning the best item.
86    #[instrument(skip(self))]
87    pub async fn sim_round(&mut self, max_gas: u64) -> Option<SimulatedItem> {
88        let (best_tx, mut best_watcher) = watch::channel(None);
89
90        let this = self.inner.clone();
91
92        // Spawn a blocking task to run the simulations.
93        let sim_task = tokio::task::spawn_blocking(move || this.sim_round(max_gas, best_tx));
94
95        // Either simulation is done, or we time out
96        select! {
97            _ = tokio::time::sleep_until(self.finish_by.into()) => {
98                trace!("Sim round timed out");
99            },
100            _ = sim_task => {
101                trace!("Sim round done");
102            },
103        }
104
105        // Check what the current best outcome is.
106        let best = best_watcher.borrow_and_update();
107        trace!(score = %best.as_ref().map(|candidate| candidate.score).unwrap_or_default(), "Read outcome from channel");
108        let outcome = best.as_ref()?;
109
110        // Remove the item from the cache.
111        let item = self.sim_items.remove(outcome.identifier)?;
112        // Accept the cache from the simulation.
113        Arc::get_mut(&mut self.inner)
114            .expect("sims dropped already")
115            .accept_cache_ref(&outcome.cache)
116            .ok()?;
117
118        Some(SimulatedItem { gas_used: outcome.gas_used, score: outcome.score, item })
119    }
120}
121
122/// A simulation environment.
123pub struct SimEnv<Db, Insp = NoOpInspector> {
124    /// The database to use for the simulation. This database will be wrapped
125    /// in [`CacheOnWrite`] databases for each simulation.
126    db: InnerDb<Db>,
127
128    /// The cache of items to simulate.
129    sim_items: SimCache,
130
131    /// The system constants for the Signet network.
132    constants: SignetSystemConstants,
133
134    /// Chain cfg to use for the simulation.
135    cfg: CfgEnv,
136
137    /// Block to use for the simulation.
138    block: BlockEnv,
139
140    /// The instant by which the simulation should finish.
141    finish_by: std::time::Instant,
142
143    /// The maximum number of concurrent simulations to run.
144    concurrency_limit: usize,
145
146    /// Spooky ghost inspector.
147    _pd: PhantomData<fn() -> Insp>,
148}
149
150impl<Db, Insp> fmt::Debug for SimEnv<Db, Insp> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_struct("SimEnvInner")
153            .field("finish_by", &self.finish_by)
154            .field("concurrency_limit", &self.concurrency_limit)
155            .finish_non_exhaustive()
156    }
157}
158
159impl<Db, Insp> SimEnv<Db, Insp> {
160    /// Creates a new `SimFactory` instance.
161    pub fn new<C, B>(
162        db: Db,
163        constants: SignetSystemConstants,
164        cfg_ref: C,
165        block_ref: B,
166        finish_by: std::time::Instant,
167        concurrency_limit: usize,
168        sim_items: SimCache,
169    ) -> Self
170    where
171        C: Cfg,
172        B: Block,
173    {
174        let mut cfg = CfgEnv::default();
175        cfg_ref.fill_cfg_env(&mut cfg);
176        let mut block = BlockEnv::default();
177        block_ref.fill_block_env(&mut block);
178
179        Self {
180            db: Arc::new(CacheDB::new(db)),
181            constants,
182            cfg,
183            block,
184            finish_by,
185            concurrency_limit,
186            sim_items,
187            _pd: PhantomData,
188        }
189    }
190
191    /// Get a reference to the database.
192    pub fn db_mut(&mut self) -> &mut InnerDb<Db> {
193        &mut self.db
194    }
195
196    /// Get a reference to the system constants.
197    pub const fn constants(&self) -> &SignetSystemConstants {
198        &self.constants
199    }
200
201    /// Get a reference to the cache of items to simulate.
202    pub const fn sim_items(&self) -> &SimCache {
203        &self.sim_items
204    }
205
206    /// Get a reference to the chain cfg.
207    pub const fn cfg(&self) -> &CfgEnv {
208        &self.cfg
209    }
210
211    /// Get a reference to the block.
212    pub const fn block(&self) -> &BlockEnv {
213        &self.block
214    }
215
216    /// Get the exectuion timeout.
217    pub const fn finish_by(&self) -> std::time::Instant {
218        self.finish_by
219    }
220
221    /// Set the execution timeout.
222    pub fn set_finish_by(&mut self, timeout: std::time::Instant) {
223        self.finish_by = timeout;
224    }
225}
226
227impl<Db, Insp> DbConnect for SimEnv<Db, Insp>
228where
229    Db: DatabaseRef + Send + Sync,
230    Insp: Sync,
231{
232    type Database = SimDb<Db>;
233
234    type Error = Infallible;
235
236    fn connect(&self) -> Result<Self::Database, Self::Error> {
237        Ok(CacheOnWrite::new(self.db.clone()))
238    }
239}
240
241impl<Db, Insp> EvmFactory for SimEnv<Db, Insp>
242where
243    Db: DatabaseRef + Send + Sync,
244    Insp: Inspector<Ctx<SimDb<Db>>> + Default + Sync,
245{
246    type Insp = SignetLayered<Layered<TimeLimit, Insp>>;
247
248    fn create(&self) -> Result<trevm::EvmNeedsCfg<Self::Database, Self::Insp>, Self::Error> {
249        let db = self.connect().unwrap();
250
251        let inspector =
252            Layered::new(TimeLimit::new(self.finish_by - Instant::now()), Insp::default());
253
254        Ok(signet_evm::signet_evm_with_inspector(db, inspector, self.constants))
255    }
256}
257
258impl<Db, Insp> SimEnv<Db, Insp>
259where
260    Db: DatabaseRef + Send + Sync,
261    Insp: Inspector<Ctx<SimDb<Db>>> + Default + Sync,
262{
263    /// Simulates a transaction in the context of a block.
264    ///
265    /// This function runs the simulation in a separate thread and waits for
266    /// the result or the deadline to expire.
267    #[instrument(skip_all, fields(identifier, tx_hash = %transaction.hash()))]
268    fn simulate_tx(
269        &self,
270        identifier: u128,
271        transaction: &TxEnvelope,
272    ) -> Result<SimOutcomeWithCache, SignetEthBundleError<SimDb<Db>>> {
273        let trevm = self.create_with_block(&self.cfg, &self.block).unwrap();
274
275        // Get the initial beneficiary balance
276        let beneficiary = trevm.beneficiary();
277        let initial_beneficiary_balance =
278            trevm.try_read_balance_ref(beneficiary).map_err(EVMError::Database)?;
279
280        // If succesful, take the cache. If failed, return the error.
281        match trevm.run_tx(transaction) {
282            Ok(trevm) => {
283                // Get the simulation results
284                let gas_used = trevm.result().gas_used();
285                let success = trevm.result().is_success();
286                let reason = trevm.result().output().cloned().map(hex::encode);
287                let halted = trevm.result().is_halt();
288                let halt_reason = if let ExecutionResult::Halt { reason, .. } = trevm.result() {
289                    Some(reason)
290                } else {
291                    None
292                }
293                .cloned();
294
295                let cache = trevm.accept_state().into_db().into_cache();
296
297                let beneficiary_balance = cache
298                    .accounts
299                    .get(&beneficiary)
300                    .map(|acct| acct.info.balance)
301                    .unwrap_or_default();
302                let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance);
303
304                trace!(
305                    gas_used = gas_used,
306                    score = %score,
307                    reverted = !success,
308                    halted,
309                    halt_reason = ?if halted { halt_reason } else { None },
310                    revert_reason = if !success { reason } else { None },
311                    "Simulation complete"
312                );
313
314                // Create the outcome
315                Ok(SimOutcomeWithCache { identifier, score, cache, gas_used })
316            }
317            Err(e) => Err(SignetEthBundleError::from(e.into_error())),
318        }
319    }
320
321    /// Simulates a bundle on the current environment.
322    #[instrument(skip_all, fields(identifier, uuid = bundle.replacement_uuid()))]
323    fn simulate_bundle(
324        &self,
325        identifier: u128,
326        bundle: &SignetEthBundle,
327    ) -> Result<SimOutcomeWithCache, SignetEthBundleError<SimDb<Db>>>
328    where
329        Insp: Inspector<Ctx<SimDb<Db>>> + Default + Sync,
330    {
331        let mut driver = SignetEthBundleDriver::new(bundle, self.finish_by);
332        let trevm = self.create_with_block(&self.cfg, &self.block).unwrap();
333
334        // Run the bundle
335        let trevm = match driver.run_bundle(trevm) {
336            Ok(result) => result,
337            Err(e) => return Err(e.into_error()),
338        };
339
340        // Build the SimOutcome
341        let score = driver.beneficiary_balance_increase();
342        let gas_used = driver.total_gas_used();
343        let cache = trevm.into_db().into_cache();
344
345        trace!(
346            gas_used = gas_used,
347            score = %score,
348            "Bundle simulation successful"
349        );
350
351        Ok(SimOutcomeWithCache { identifier, score, cache, gas_used })
352    }
353
354    /// Simulates a transaction or bundle in the context of a block.
355    fn simulate(
356        &self,
357        identifier: u128,
358        item: &SimItem,
359    ) -> Result<SimOutcomeWithCache, SignetEthBundleError<SimDb<Db>>> {
360        match item {
361            SimItem::Bundle(bundle) => self.simulate_bundle(identifier, bundle),
362            SimItem::Tx(tx) => self.simulate_tx(identifier, tx),
363        }
364    }
365
366    #[instrument(skip_all)]
367    fn sim_round(
368        self: Arc<Self>,
369        max_gas: u64,
370        best_tx: watch::Sender<Option<SimOutcomeWithCache>>,
371    ) {
372        // Pull the `n` best items from the cache.
373        let active_sim = self.sim_items.read_best(self.concurrency_limit);
374
375        // Create a channel to send the results back.
376        let (candidates, mut candidates_rx) = mpsc::channel(self.concurrency_limit);
377
378        let outer = trace_span!("sim_thread", candidates = active_sim.len());
379        let outer_ref = &outer;
380        let _og = outer.enter();
381
382        // to be used in the scope
383        let this_ref = &self;
384
385        std::thread::scope(move |scope| {
386            // Spawn a thread per bundle to simulate.
387            for (identifier, item) in active_sim.into_iter() {
388                let c = candidates.clone();
389
390                scope.spawn(move || {
391                    let _ig = trace_span!(parent: outer_ref, "sim_task", identifier = %identifier)
392                        .entered();
393
394                    // If simulation is succesful, send the outcome via the
395                    // channel.
396                    match this_ref.simulate(identifier, &item) {
397                        Ok(candidate) => {
398                            if candidate.gas_used <= max_gas {
399                                // shortcut return on success
400                                let _ = c.blocking_send(candidate);
401                                return;
402                            }
403                            trace!(gas_used = candidate.gas_used, max_gas, "Gas limit exceeded");
404                        }
405                        Err(e) => {
406                            trace!(?identifier, ?e, "Simulation failed");
407                        }
408                    };
409                    // fall through applies to all errors, occurs if
410                    // the simulation fails or the gas limit is exceeded.
411                    this_ref.sim_items.remove(identifier);
412                });
413            }
414            // Drop the TX so that the channel is closed when all threads
415            // are done.
416            drop(candidates);
417
418            // Wait for each thread to finish. Find the best outcome.
419            while let Some(candidate) = candidates_rx.blocking_recv() {
420                // Update the best score and send it to the channel.
421                let _ = best_tx.send_if_modified(|current| {
422                    let best_score = current.as_ref().map(|c| c.score).unwrap_or_default();
423                    let current_id = current.as_ref().map(|c| c.identifier);
424
425                    let changed = candidate.score > best_score;
426                    if changed {
427                        trace!(
428                            old_best = ?best_score,
429                            old_identifier = current_id,
430                            new_best = %candidate.score,
431                            identifier = candidate.identifier,
432                            "Found better candidate"
433                        );
434                        *current = Some(candidate);
435                    }
436                    changed
437                });
438            }
439        });
440    }
441}
442
443impl<Db, Insp> SimEnv<Db, Insp>
444where
445    Db: DatabaseRef,
446    Insp: Inspector<Ctx<SimDb<Db>>> + Default + Sync,
447{
448    /// Accepts a cache from the simulation and extends the database with it.
449    pub fn accept_cache(
450        &mut self,
451        cache: Cache,
452    ) -> Result<(), <InnerDb<Db> as TryCachingDb>::Error> {
453        self.db_mut().try_extend(cache)
454    }
455
456    /// Accepts a cache from the simulation and extends the database with it.
457    pub fn accept_cache_ref(
458        &mut self,
459        cache: &Cache,
460    ) -> Result<(), <InnerDb<Db> as TryCachingDb>::Error> {
461        self.db_mut().try_extend_ref(cache)
462    }
463}