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
29pub 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 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 #[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 let sim_task = tokio::task::spawn_blocking(move || this.sim_round(max_gas, best_tx));
94
95 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 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 let item = self.sim_items.remove(outcome.identifier)?;
112 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
122pub struct SimEnv<Db, Insp = NoOpInspector> {
124 db: InnerDb<Db>,
127
128 sim_items: SimCache,
130
131 constants: SignetSystemConstants,
133
134 cfg: CfgEnv,
136
137 block: BlockEnv,
139
140 finish_by: std::time::Instant,
142
143 concurrency_limit: usize,
145
146 _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 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 pub fn db_mut(&mut self) -> &mut InnerDb<Db> {
193 &mut self.db
194 }
195
196 pub const fn constants(&self) -> &SignetSystemConstants {
198 &self.constants
199 }
200
201 pub const fn sim_items(&self) -> &SimCache {
203 &self.sim_items
204 }
205
206 pub const fn cfg(&self) -> &CfgEnv {
208 &self.cfg
209 }
210
211 pub const fn block(&self) -> &BlockEnv {
213 &self.block
214 }
215
216 pub const fn finish_by(&self) -> std::time::Instant {
218 self.finish_by
219 }
220
221 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 #[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 let beneficiary = trevm.beneficiary();
277 let initial_beneficiary_balance =
278 trevm.try_read_balance_ref(beneficiary).map_err(EVMError::Database)?;
279
280 match trevm.run_tx(transaction) {
282 Ok(trevm) => {
283 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 Ok(SimOutcomeWithCache { identifier, score, cache, gas_used })
316 }
317 Err(e) => Err(SignetEthBundleError::from(e.into_error())),
318 }
319 }
320
321 #[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 let trevm = match driver.run_bundle(trevm) {
336 Ok(result) => result,
337 Err(e) => return Err(e.into_error()),
338 };
339
340 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 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 let active_sim = self.sim_items.read_best(self.concurrency_limit);
374
375 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 let this_ref = &self;
384
385 std::thread::scope(move |scope| {
386 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 match this_ref.simulate(identifier, &item) {
397 Ok(candidate) => {
398 if candidate.gas_used <= max_gas {
399 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 this_ref.sim_items.remove(identifier);
412 });
413 }
414 drop(candidates);
417
418 while let Some(candidate) = candidates_rx.blocking_recv() {
420 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 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 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}