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
26pub struct SimEnv<RuDb, HostDb, RuInsp = NoOpInspector, HostInsp = NoOpInspector> {
28 rollup: RollupEnv<RuDb, RuInsp>,
30
31 host: HostEnv<HostDb, HostInsp>,
33
34 sim_items: SimCache,
36
37 finish_by: tokio::time::Instant,
39
40 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 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 pub const fn rollup_env(&self) -> &RollupEnv<RuDb, RuInsp> {
67 &self.rollup
68 }
69
70 pub const fn rollup_mut(&mut self) -> &mut RollupEnv<RuDb, RuInsp> {
72 &mut self.rollup
73 }
74
75 pub const fn host_env(&self) -> &HostEnv<HostDb, HostInsp> {
77 &self.host
78 }
79
80 pub const fn host_mut(&mut self) -> &mut HostEnv<HostDb, HostInsp> {
82 &mut self.host
83 }
84
85 pub const fn constants(&self) -> &SignetSystemConstants {
87 self.rollup.constants()
88 }
89
90 pub const fn sim_items(&self) -> &SimCache {
92 &self.sim_items
93 }
94
95 pub const fn finish_by(&self) -> tokio::time::Instant {
97 self.finish_by
98 }
99
100 pub const fn set_finish_by(&mut self, timeout: tokio::time::Instant) {
102 self.finish_by = timeout;
103 }
104
105 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 #[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 let beneficiary = trevm.beneficiary();
132 let initial_beneficiary_balance =
133 trevm.try_read_balance_ref(beneficiary).map_err(EVMError::Database)?;
134
135 match trevm.run_tx(transaction) {
137 Ok(mut trevm) => {
138 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 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 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 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 #[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 let trevm = match driver.run_bundle(trevm) {
217 Ok(result) => result,
218 Err(e) => return Err(e.into_error()),
219 };
220
221 let score = driver.beneficiary_balance_increase();
223 let outputs = driver.into_outputs();
224
225 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 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 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 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 let this_ref = self.clone();
283
284 std::thread::scope(|scope| {
285 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 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 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 this_ref.sim_items.remove(cache_rank);
345 });
346 }
347 drop(candidates);
350 while let Some(candidate) = candidates_rx.blocking_recv() {
352 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#[derive(Debug, Clone, Copy)]
382pub(crate) struct SimRoundCounts {
383 pub ok: u32,
385 pub err: u32,
387}