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