Skip to main content

signet_sim/env/
sim_env.rs

1use crate::{env::RollupEnv, HostEnv, SimCache, SimDb, SimItem, SimOutcomeWithCache};
2use alloy::{consensus::TxEnvelope, hex};
3use core::fmt;
4use signet_bundle::{RecoveredBundle, SignetEthBundleDriver, SignetEthBundleError};
5use signet_evm::SignetInspector;
6use signet_types::constants::SignetSystemConstants;
7use std::{
8    borrow::Cow,
9    sync::{
10        atomic::{AtomicU32, Ordering},
11        Arc,
12    },
13};
14use tokio::sync::{mpsc, watch};
15use tracing::{debug, instrument, trace, trace_span};
16use trevm::{
17    helpers::Ctx,
18    revm::{
19        context::result::{EVMError, ExecutionResult},
20        inspector::NoOpInspector,
21        DatabaseRef, Inspector,
22    },
23    BundleDriver,
24};
25
26/// A simulation environment.
27pub struct SimEnv<RuDb, HostDb, RuInsp = NoOpInspector, HostInsp = NoOpInspector> {
28    /// The rollup environment.
29    rollup: RollupEnv<RuDb, RuInsp>,
30
31    /// The host environment.
32    host: HostEnv<HostDb, HostInsp>,
33
34    /// The cache of items to simulate.
35    sim_items: SimCache,
36
37    /// The instant by which the simulation should finish.
38    finish_by: tokio::time::Instant,
39
40    /// The maximum number of concurrent simulations to run.
41    concurrency_limit: usize,
42}
43
44impl<RuDb, HostDb, RuInsp, HostInsp> fmt::Debug for SimEnv<RuDb, HostDb, RuInsp, HostInsp> {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.debug_struct("SimEnv")
47            .field("finish_by", &self.finish_by)
48            .field("concurrency_limit", &self.concurrency_limit)
49            .finish_non_exhaustive()
50    }
51}
52
53impl<RuDb, HostDb, RuInsp, HostInsp> SimEnv<RuDb, HostDb, RuInsp, HostInsp> {
54    /// Create a new `SimEnv` instance.
55    pub const fn new(
56        rollup: RollupEnv<RuDb, RuInsp>,
57        host: HostEnv<HostDb, HostInsp>,
58        finish_by: tokio::time::Instant,
59        concurrency_limit: usize,
60        sim_items: SimCache,
61    ) -> Self {
62        Self { rollup, host, finish_by, concurrency_limit, sim_items }
63    }
64
65    /// Get a reference to the rollup environment.
66    pub const fn rollup_env(&self) -> &RollupEnv<RuDb, RuInsp> {
67        &self.rollup
68    }
69
70    /// Get a mutable reference to the rollup environment.
71    pub const fn rollup_mut(&mut self) -> &mut RollupEnv<RuDb, RuInsp> {
72        &mut self.rollup
73    }
74
75    /// Get a reference to the host environment.
76    pub const fn host_env(&self) -> &HostEnv<HostDb, HostInsp> {
77        &self.host
78    }
79
80    /// Get a mutable reference to the host environment.
81    pub const fn host_mut(&mut self) -> &mut HostEnv<HostDb, HostInsp> {
82        &mut self.host
83    }
84
85    /// Get a reference to the system constants.
86    pub const fn constants(&self) -> &SignetSystemConstants {
87        self.rollup.constants()
88    }
89
90    /// Get a reference to the cache of items to simulate.
91    pub const fn sim_items(&self) -> &SimCache {
92        &self.sim_items
93    }
94
95    /// Get the exectuion timeout.
96    pub const fn finish_by(&self) -> tokio::time::Instant {
97        self.finish_by
98    }
99
100    /// Set the execution timeout.
101    pub const fn set_finish_by(&mut self, timeout: tokio::time::Instant) {
102        self.finish_by = timeout;
103    }
104
105    /// Get the concurrency limit.
106    pub const fn concurrency_limit(&self) -> usize {
107        self.concurrency_limit
108    }
109}
110
111impl<RuDb, HostDb, RuInsp, HostInsp> SimEnv<RuDb, HostDb, RuInsp, HostInsp>
112where
113    RuDb: DatabaseRef<Error: 'static> + Send + Sync,
114    RuInsp: Inspector<Ctx<SimDb<RuDb>>> + Default + Sync,
115    HostDb: DatabaseRef<Error: 'static> + Send + Sync,
116    HostInsp: Inspector<Ctx<SimDb<HostDb>>> + Default + Sync,
117{
118    /// Simulates a transaction in the context of a block.
119    ///
120    /// This function runs the simulation in a separate thread and waits for
121    /// the result or the deadline to expire.
122    #[instrument(skip(self, transaction), fields(tx_hash = %transaction.hash()))]
123    fn simulate_tx(
124        &self,
125        cache_rank: u128,
126        transaction: &TxEnvelope,
127    ) -> Result<SimOutcomeWithCache, SignetEthBundleError<SimDb<RuDb>>> {
128        let trevm = self.rollup.create_evm(self.finish_by);
129
130        // Get the initial beneficiary balance
131        let beneficiary = trevm.beneficiary();
132        let initial_beneficiary_balance =
133            trevm.try_read_balance_ref(beneficiary).map_err(EVMError::Database)?;
134
135        // If succesful, take the cache. If failed, return the error.
136        match trevm.run_tx(transaction) {
137            Ok(mut trevm) => {
138                // Get the simulation results
139                let gas_used = trevm.result().gas_used();
140                let success = trevm.result().is_success();
141                let reason = trevm.result().output().cloned().map(hex::encode);
142                let halted = trevm.result().is_halt();
143                let halt_reason = if let ExecutionResult::Halt { reason, .. } = trevm.result() {
144                    Some(reason.clone())
145                } else {
146                    None
147                };
148
149                // We collect the orders and fills from the inspector, and check
150                // them against the provided fill state. If the fills are
151                // insufficient, we error out. Otherwise we'll return them as
152                // part of the `SimOutcomeWithCache`, to allow _later_ stages to
153                // process them (e.g., to update the fill state).
154                let (bundle_fills, bundle_orders) =
155                    trevm.inner_mut_unchecked().inspector.as_mut_detector().take_aggregates();
156
157                self.rollup.fill_state().check_ru_tx_events(&bundle_fills, &bundle_orders)?;
158
159                // We will later commit these to the trevm DB when the
160                // SimOutcome is accepted.
161                let cache = trevm.accept_state().into_db().into_cache();
162
163                let beneficiary_balance = cache
164                    .accounts
165                    .get(&beneficiary)
166                    .map(|acct| acct.info.balance)
167                    .unwrap_or_default();
168                let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance);
169
170                trace!(
171                    gas_used,
172                    score = %score,
173                    reverted = !success,
174                    halted,
175                    halt_reason = ?if halted { halt_reason } else { None },
176                    revert_reason = if !success { reason } else { None },
177                    "Transaction simulation complete"
178                );
179
180                // Create the outcome
181                Ok(SimOutcomeWithCache {
182                    cache_rank,
183                    score,
184                    rollup_cache: cache,
185                    host_cache: Default::default(),
186                    host_gas_used: 0,
187                    gas_used,
188                    bundle_fills,
189                    bundle_orders,
190                })
191            }
192            Err(e) => Err(SignetEthBundleError::from(e.into_error())),
193        }
194    }
195
196    /// Simulates a bundle on the current environment.
197    #[instrument(skip(self, bundle), fields(uuid = bundle.replacement_uuid()))]
198    fn simulate_bundle(
199        &self,
200        cache_rank: u128,
201        bundle: &RecoveredBundle,
202    ) -> Result<SimOutcomeWithCache, SignetEthBundleError<SimDb<RuDb>>>
203    where
204        RuInsp: Inspector<Ctx<SimDb<RuDb>>> + Default + Sync,
205    {
206        let trevm = self.rollup.create_evm(self.finish_by);
207
208        let mut driver = SignetEthBundleDriver::new_with_fill_state(
209            bundle,
210            self.host.create_evm(self.finish_by),
211            self.finish_by,
212            Cow::Borrowed(self.rollup.fill_state()),
213        );
214
215        // Run the bundle
216        let trevm = match driver.run_bundle(trevm) {
217            Ok(result) => result,
218            Err(e) => return Err(e.into_error()),
219        };
220
221        // Build the SimOutcome
222        let score = driver.beneficiary_balance_increase();
223        let outputs = driver.into_outputs();
224
225        // This is redundant with the driver, however, we double check here.
226        // If perf is hit too much we can remove.
227        self.rollup
228            .fill_state()
229            .check_ru_tx_events(&outputs.bundle_fills, &outputs.bundle_orders)?;
230
231        let host_cache = outputs.host_evm.map(|evm| evm.into_db().into_cache()).unwrap_or_default();
232        trace!(
233            gas_used = outputs.total_gas_used,
234            host_gas_used = outputs.total_host_gas_used,
235            %score,
236            "Bundle simulation successful"
237        );
238
239        Ok(SimOutcomeWithCache {
240            cache_rank,
241            score: score.to(),
242            rollup_cache: trevm.into_db().into_cache(),
243            host_cache,
244            gas_used: outputs.total_gas_used,
245            host_gas_used: outputs.total_host_gas_used,
246            bundle_fills: outputs.bundle_fills,
247            bundle_orders: outputs.bundle_orders,
248        })
249    }
250
251    /// Simulates a transaction or bundle in the context of a block.
252    fn simulate(
253        &self,
254        cache_rank: u128,
255        item: &SimItem,
256    ) -> Result<SimOutcomeWithCache, SignetEthBundleError<SimDb<RuDb>>> {
257        match item {
258            SimItem::Bundle(bundle) => self.simulate_bundle(cache_rank, bundle),
259            SimItem::Tx(tx) => self.simulate_tx(cache_rank, tx),
260        }
261    }
262
263    /// Run a simulation round, returning counts of successful and failed items.
264    pub(crate) fn sim_round(
265        self: Arc<Self>,
266        max_gas: u64,
267        max_host_gas: u64,
268        best_tx: watch::Sender<Option<SimOutcomeWithCache>>,
269        active_sim: Vec<(u128, SimItem)>,
270    ) -> SimRoundCounts {
271        // Create a channel to send the results back.
272        let (candidates, mut candidates_rx) = mpsc::channel(self.concurrency_limit);
273
274        let outer = trace_span!("sim_thread", candidates = active_sim.len()).or_current();
275        let outer_ref = &outer;
276        let _og = outer.enter();
277
278        let ok_count = AtomicU32::new(0);
279        let err_count = AtomicU32::new(0);
280
281        // to be used in the scope
282        let this_ref = self.clone();
283
284        std::thread::scope(|scope| {
285            // Spawn a thread per bundle to simulate.
286            for (cache_rank, item) in active_sim.into_iter() {
287                let c = candidates.clone();
288                let this_ref = this_ref.clone();
289                let ok_ref = &ok_count;
290                let err_ref = &err_count;
291                scope.spawn(move || {
292                    let identifier = item.identifier();
293                    let _ig = trace_span!(parent: outer_ref, "sim_task", %identifier).entered();
294
295                    // If simulation is succesful, send the outcome via the
296                    // channel.
297
298                    match this_ref.simulate(cache_rank, &item) {
299                        Ok(candidate) if candidate.score.is_zero() => {
300                            debug!(
301                                %identifier,
302                                failure_reason = "zero score candidate",
303                                "simulation rejected",
304                            );
305                            err_ref.fetch_add(1, Ordering::Relaxed);
306                        }
307                        Ok(candidate) if candidate.host_gas_used > max_host_gas => {
308                            debug!(
309                                %identifier,
310                                host_gas_used = candidate.host_gas_used,
311                                max_host_gas,
312                                failure_reason = "host gas limit exceeded",
313                                "simulation rejected",
314                            );
315                            err_ref.fetch_add(1, Ordering::Relaxed);
316                        }
317                        Ok(candidate) if candidate.gas_used > max_gas => {
318                            debug!(
319                                %identifier,
320                                gas_used = candidate.gas_used,
321                                max_gas,
322                                failure_reason = "gas limit exceeded",
323                                "simulation rejected",
324                            );
325                            err_ref.fetch_add(1, Ordering::Relaxed);
326                        }
327                        Ok(candidate) => {
328                            ok_ref.fetch_add(1, Ordering::Relaxed);
329                            // shortcut return on success
330                            let _ = c.blocking_send(candidate);
331                            return;
332                        }
333                        Err(error) => {
334                            debug!(
335                                %identifier,
336                                failure_reason = %error,
337                                "simulation failed",
338                            );
339                            err_ref.fetch_add(1, Ordering::Relaxed);
340                        }
341                    };
342                    // fall through applies to all errors, occurs if
343                    // the simulation fails or the gas limit is exceeded.
344                    this_ref.sim_items.remove(cache_rank);
345                });
346            }
347            // Drop the TX so that the channel is closed when all threads
348            // are done.
349            drop(candidates);
350            // Wait for each thread to finish. Find the best outcome.
351            while let Some(candidate) = candidates_rx.blocking_recv() {
352                // Update the best score and send it to the channel.
353                let _ = best_tx.send_if_modified(|current| {
354                    let best_score = current.as_ref().map(|c| c.score).unwrap_or_default();
355                    let current_cache_rank = current.as_ref().map(|c| c.cache_rank);
356
357                    let changed = candidate.score > best_score;
358                    if changed {
359                        trace!(
360                            old_best = ?best_score,
361                            old_cache_rank = current_cache_rank,
362                            new_best = %candidate.score,
363                            new_cache_rank = candidate.cache_rank,
364                            "Found better candidate"
365                        );
366                        *current = Some(candidate);
367                    }
368                    changed
369                });
370            }
371        });
372
373        SimRoundCounts {
374            ok: ok_count.load(Ordering::Relaxed),
375            err: err_count.load(Ordering::Relaxed),
376        }
377    }
378}
379
380/// Counts of items from a simulation round.
381#[derive(Debug, Clone, Copy)]
382pub(crate) struct SimRoundCounts {
383    /// Items that simulated successfully.
384    pub ok: u32,
385    /// Items that failed or were rejected.
386    pub err: u32,
387}