Skip to main content

signet_sim/
task.rs

1use crate::{
2    cache::StateSource, env::SimEnv, BuiltBlock, HostEnv, RollupEnv, SharedSimEnv, SimCache, SimDb,
3};
4use std::time::Duration;
5use tokio::select;
6use tracing::{debug, trace};
7use trevm::{
8    helpers::Ctx,
9    revm::{inspector::NoOpInspector, DatabaseRef, Inspector},
10};
11
12/// The amount of time to sleep between simulation rounds when there are no items to simulate.
13pub(crate) const SIM_SLEEP_MS: u64 = 50;
14
15/// Builds a single block by repeatedly invoking [`SimEnv`].
16#[derive(Debug)]
17pub struct BlockBuild<
18    RuDb,
19    HostDb,
20    RuAsync,
21    HostAsync,
22    RuInsp = NoOpInspector,
23    HostInsp = NoOpInspector,
24> {
25    /// The simulation environment.
26    env: SharedSimEnv<RuDb, HostDb, RuInsp, HostInsp>,
27
28    /// Async state source for the rollup chain, used for preflight validity checks.
29    ru_async_source: RuAsync,
30
31    /// Async state source for the host chain, used for preflight validity checks.
32    host_async_source: HostAsync,
33
34    /// The block being built.
35    block: BuiltBlock,
36
37    /// The deadline to produce a block by.
38    finish_by: tokio::time::Instant,
39
40    /// The maximum amount of gas to use in the built block
41    max_gas: u64,
42
43    /// The maximum amount of host gas to use in the user portion of the built
44    /// block, not including overhead for the signet RU block submission.
45    max_host_gas: u64,
46}
47
48impl<RuDb, HostDb, RuAsync, HostAsync, RuInsp, HostInsp>
49    BlockBuild<RuDb, HostDb, RuAsync, HostAsync, RuInsp, HostInsp>
50where
51    RuDb: DatabaseRef + Send + Sync + 'static,
52    RuInsp: Inspector<Ctx<SimDb<RuDb>>> + Default + Sync + 'static,
53    HostDb: DatabaseRef + Send + Sync + 'static,
54    HostInsp: Inspector<Ctx<SimDb<HostDb>>> + Default + Sync + 'static,
55    RuAsync: StateSource,
56    HostAsync: StateSource,
57{
58    /// Create a new block building process.
59    #[expect(clippy::too_many_arguments, reason = "should be refactored to avoid this warning")]
60    pub fn new(
61        rollup: RollupEnv<RuDb, RuInsp>,
62        host: HostEnv<HostDb, HostInsp>,
63        finish_by: tokio::time::Instant,
64        concurrency_limit: usize,
65        sim_items: SimCache,
66        max_gas: u64,
67        max_host_gas: u64,
68        ru_async_source: RuAsync,
69        host_async_source: HostAsync,
70    ) -> Self {
71        let number = rollup.block().number;
72
73        let env = SimEnv::<RuDb, HostDb, RuInsp, HostInsp>::new(
74            rollup,
75            host,
76            finish_by,
77            concurrency_limit,
78            sim_items,
79        );
80        let finish_by = env.finish_by();
81        Self {
82            env: env.into(),
83            ru_async_source,
84            host_async_source,
85            block: BuiltBlock::new(number.to()),
86            finish_by,
87            max_gas,
88            max_host_gas,
89        }
90    }
91
92    /// Get the maximum gas limit for the block being built.
93    pub const fn max_gas(&self) -> u64 {
94        self.max_gas
95    }
96
97    /// Set the maximum gas limit for the block being built.
98    pub const fn set_max_gas(&mut self, max_gas: u64) {
99        self.max_gas = max_gas;
100    }
101
102    /// Get the maximum host gas limit for the block being built.
103    pub const fn max_host_gas(&self) -> u64 {
104        self.max_host_gas
105    }
106
107    /// Set the maximum host gas limit for the block being built.
108    pub const fn set_max_host_gas(&mut self, max_host_gas: u64) {
109        self.max_host_gas = max_host_gas;
110    }
111
112    /// Get a reference the simulation cache used by this builder.
113    pub fn sim_items(&self) -> &SimCache {
114        self.env.sim_items()
115    }
116
117    /// Get a reference to the rollup environment.
118    pub fn rollup_env(&self) -> &RollupEnv<RuDb, RuInsp> {
119        self.env.rollup_env()
120    }
121
122    /// Get a reference to the host environment.
123    pub fn host_env(&self) -> &HostEnv<HostDb, HostInsp> {
124        self.env.host_env()
125    }
126
127    /// Consume the builder and return the built block.
128    ///
129    /// This should generally not be called directly; use [`BlockBuild::build`]
130    /// instead.
131    pub fn into_block(self) -> BuiltBlock {
132        self.block
133    }
134
135    /// Run a simulation round, and accumulate the results into the block.
136    async fn round(&mut self) {
137        let gas_allowed = self.max_gas - self.block.gas_used();
138        let host_gas_allowed = self.max_host_gas - self.block.host_gas_used();
139
140        if let Some(simulated) = self
141            .env
142            .sim_round(
143                gas_allowed,
144                host_gas_allowed,
145                &self.ru_async_source,
146                &self.host_async_source,
147            )
148            .await
149        {
150            debug!(
151                score = %simulated.score,
152                gas_used = simulated.gas_used,
153                host_gas_used = simulated.host_gas_used,
154                identifier = %simulated.item.identifier(),
155                "Adding item to block"
156            );
157            self.block.ingest(simulated);
158        }
159    }
160
161    /// Run several rounds, building a block by iteratively adding simulated
162    /// items.
163    ///
164    /// This version returns self to allow inspection of the building process.
165    /// It does nothing if the block already has transactions (i.e. this
166    /// function should be idempotent).
167    pub async fn run_build(mut self) -> Self {
168        if !self.block.transactions.is_empty() {
169            debug!(
170                transactions = self.block.transactions.len(),
171                "Starting block build with pre-existing transactions",
172            );
173            return self;
174        }
175        let mut i = 1;
176        // Run until the deadline is reached.
177        loop {
178            let finish_by = self.finish_by;
179
180            let next_round_time = tokio::time::Instant::now() + Duration::from_millis(SIM_SLEEP_MS);
181
182            // If the next round time is past the deadline, we stop the simulation loop.
183            // This will stop the simulation even if there are items, but that is an acceptable tradeoff
184            // as we must ensure there's enough time to submit the blob to the host chain.
185            if next_round_time >= finish_by {
186                debug!("Next round time is past the deadline, stopping sim loop");
187                break;
188            }
189
190            // Only simulate if there are items to simulate.
191            // If there are not items, we sleep for [`SIM_SLEEP_MS`] and restart the loop.
192            if self.env.sim_items().is_empty() {
193                tokio::time::sleep_until(next_round_time).await;
194                continue;
195            }
196
197            // If there are items to simulate, we run a simulation round.
198            let prev_tx_count = self.block.transactions.len();
199            let fut = self.round();
200
201            select! {
202                biased;
203                _ = tokio::time::sleep_until(finish_by) => {
204                    // This event is not a round event. It's a control flow
205                    // event for the outer loop. As such it's not in span
206                    debug!("Deadline reached, stopping sim loop");
207                    break;
208                },
209                _ = fut => {
210                    i += 1;
211                    let remaining_items = self.env.sim_items().len();
212                    trace!(remaining_items, "Round completed");
213                }
214            }
215
216            // If the round didn't produce any new transactions (e.g. all
217            // cached items have Future validity), sleep to avoid spinning
218            // and allow new items to arrive.
219            if self.block.transactions.len() == prev_tx_count {
220                tokio::time::sleep_until(next_round_time).await;
221            }
222        }
223
224        debug!(
225            rounds = i,
226            transactions = self.block.transactions.len(),
227            remaining_cache_size = self.env.sim_items().len(),
228            "Building completed",
229        );
230        self
231    }
232
233    /// Run several rounds, building a block by iteratively adding simulated
234    /// items.
235    pub async fn build(self) -> BuiltBlock {
236        self.run_build().await.block
237    }
238}
239
240#[cfg(test)]
241mod test {
242    use super::*;
243    use crate::cache::StateSource;
244    use std::future::Future;
245
246    /// Compile-time check to ensure that the block building process is
247    /// `Send`.
248    fn _build_fut_is_send<RuDb, HostDb, RuAsync, HostAsync, RuInsp, HostInsp>(
249        b: BlockBuild<RuDb, HostDb, RuAsync, HostAsync, RuInsp, HostInsp>,
250    ) where
251        RuDb: DatabaseRef + Send + Sync + 'static,
252        RuInsp: Inspector<Ctx<SimDb<RuDb>>> + Default + Sync + 'static,
253        HostDb: DatabaseRef + Send + Sync + 'static,
254        HostInsp: Inspector<Ctx<SimDb<HostDb>>> + Default + Sync + 'static,
255        RuAsync: StateSource,
256        HostAsync: StateSource,
257    {
258        let _: Box<dyn Future<Output = BuiltBlock> + Send> = Box::new(b.build());
259    }
260}