anvil_polkadot/substrate_node/mining_engine.rs
1use crate::substrate_node::service::TransactionPoolHandle;
2use alloy_rpc_types::anvil::MineOptions;
3use anvil::eth::backend::time::TimeManager;
4use futures::{
5 StreamExt,
6 channel::oneshot,
7 stream::{FusedStream, SelectAll, select_all, unfold},
8 task::AtomicWaker,
9};
10use parking_lot::RwLock;
11use polkadot_sdk::{
12 sc_consensus_manual_seal::{CreatedBlock, EngineCommand, Error as BlockProducingError},
13 sc_service::TransactionPool,
14 sp_core::{self, H256},
15};
16use std::{pin::Pin, sync::Arc};
17use substrate_runtime::Hash;
18use tokio::{
19 sync::mpsc::Sender,
20 time::{Duration, Instant, MissedTickBehavior, interval_at},
21};
22
23// Errors that can happen during the block production.
24#[derive(Debug, thiserror::Error)]
25pub enum MiningError {
26 #[error("Block production failed: {0}")]
27 BlockProducing(#[from] BlockProducingError),
28 #[error("Current mining mode can not answer this query.")]
29 MiningModeMismatch,
30 #[error("Current timestamp is newer.")]
31 Timestamp,
32 #[error("Closed channel")]
33 ClosedChannel,
34}
35
36/// Mining modes supported by the MiningEngine.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum MiningMode {
39 /// Blocks are produced only in response to RPC calls.
40 None,
41 /// Automatic block productiona at fixed time intervals.
42 Interval { tick: Duration },
43 /// Automatic block production triggered by new transactions.
44 AutoMining,
45 /// Hybrid mode combining interval and transaction-based mining.
46 MixedMining { tick: Duration },
47}
48
49impl MiningMode {
50 /// Create a mining mode based on configuration parameters.
51 ///
52 /// This method determines the appropriate mining mode based on
53 /// the provided timing and behavioral preferences.
54 ///
55 /// # Arguments
56 /// * `block_time` - Optional duration between blocks for interval mining
57 /// * `mixed_mining` - Enable mixed mode when block_time is provided
58 /// * `no_mining` - Disable automatic mining when no block_time is set
59 ///
60 /// # Returns
61 /// The appropriate `MiningMode` variant based on input parameters
62 pub fn new(block_time: Option<Duration>, mixed_mining: bool, no_mining: bool) -> Self {
63 block_time.map_or_else(
64 || if no_mining { Self::None } else { Self::AutoMining },
65 |time| {
66 if mixed_mining {
67 Self::MixedMining { tick: time }
68 } else {
69 Self::Interval { tick: time }
70 }
71 },
72 )
73 }
74}
75
76/// Controller for blockchain block production operations.
77///
78/// The `MiningEngine` provides a high-level interface for managing block production
79/// It supports multiple mining modes, time manipulation for testing, and
80/// Ethereum-compatible RPC methods.
81///
82/// The engine coordinates between the transaction pool, consensus layer, and time
83/// management to provide flexible block production strategies suitable for both
84/// development and production environments.
85pub struct MiningEngine {
86 /// Coordination mechanism between the MiningEngine and the background
87 /// task that runs the "polling" loop from `run_mining_engine`.
88 /// Calls to waker.wake() will unpark the background task and force a
89 /// recheck of the current mining mode and rebuild of the polled streams.
90 waker: Arc<AtomicWaker>,
91 mining_mode: Arc<RwLock<MiningMode>>,
92 transaction_pool: Arc<TransactionPoolHandle>,
93 time_manager: Arc<TimeManager>,
94 seal_command_sender: Sender<EngineCommand<sp_core::H256>>,
95}
96
97impl MiningEngine {
98 /// Create a new mining engine controller.
99 ///
100 /// Initializes a mining engine with the specified components and configuration.
101 /// The engine will coordinate block production according to the initial mining mode.
102 ///
103 /// # Arguments
104 /// * `mining_mode` - Initial mining strategy
105 /// * `transaction_pool` - Handle for monitoring transaction pool changes
106 /// * `time_manager` - Component for blockchain time management
107 /// * `seal_command_sender` - Channel for sending block sealing commands
108 ///
109 /// # Returns
110 /// A new `MiningEngine` instance ready for use
111 pub fn new(
112 mining_mode: MiningMode,
113 transaction_pool: Arc<TransactionPoolHandle>,
114 time_manager: Arc<TimeManager>,
115 seal_command_sender: Sender<EngineCommand<sp_core::H256>>,
116 ) -> Self {
117 Self {
118 waker: Default::default(),
119 mining_mode: Arc::new(RwLock::new(mining_mode)),
120 transaction_pool,
121 time_manager,
122 seal_command_sender,
123 }
124 }
125
126 /// Mine a specified number of blocks manually.
127 ///
128 /// This method implements the `anvil_mine` RPC call, allowing manual control
129 /// over block production. Blocks are mined sequentially, with optional time
130 /// advancement between each block.
131 ///
132 /// # Arguments
133 /// * `num_blocks` - Number of blocks to mine (defaults to 1 if None)
134 /// * `interval` - Optional time to advance between blocks (in seconds)
135 ///
136 /// # Returns
137 /// * `Ok(H256)` - The hash of the last block mined successfully.
138 /// * `Err(MiningError)` - Block production failed
139 pub async fn mine(
140 &self,
141 num_blocks: Option<u64>,
142 interval: Option<Duration>,
143 ) -> Result<H256, MiningError> {
144 let blocks = num_blocks.unwrap_or(1);
145 let mut last_hash = H256::zero();
146 for _ in 0..blocks {
147 if let Some(interval) = interval {
148 self.time_manager.increase_time(interval.as_secs());
149 }
150 last_hash = seal_now(&self.seal_command_sender).await?.hash;
151 }
152 Ok(last_hash)
153 }
154
155 /// Ethereum-compatible block mining RPC method.
156 ///
157 /// Implements the `evm_mine` RPC call from the Ethereum JSON-RPC API.
158 /// This method provides compatibility with Ethereum development tools
159 /// and testing frameworks.
160 ///
161 /// # Arguments
162 /// * `opts` - Optional mining parameters including timestamp and block count
163 ///
164 /// # Returns
165 /// * `Ok(H256)` - The hash of the last block mined successfully.
166 /// * `Err(MiningError)` - Mining operation failed
167 pub async fn evm_mine(&self, opts: Option<MineOptions>) -> Result<H256, MiningError> {
168 self.do_evm_mine(opts).await.map(|res| res.1)
169 }
170
171 /// Configure interval-based mining mode.
172 ///
173 /// Sets the mining engine to produce blocks at regular time intervals.
174 /// An interval of 0 disables interval mining. Changes take effect immediately
175 /// and will wake the background mining task.
176 ///
177 /// # Arguments
178 /// * `interval` - Block production interval in seconds (0 to disable)
179 pub fn set_interval_mining(&self, interval: Duration) {
180 let new_mode = if interval.as_secs() == 0 {
181 MiningMode::None
182 } else {
183 MiningMode::Interval { tick: interval }
184 };
185 *self.mining_mode.write() = new_mode;
186 self.wake();
187 }
188
189 /// Get the current interval mining configuration.
190 ///
191 /// Returns the current block production interval if interval or mixed
192 /// mining is active, or None if interval mining is disabled.
193 ///
194 /// # Returns
195 /// * `Some(seconds)` - Interval mining active with specified interval
196 /// * `None` - Interval mining is disabled
197 pub fn get_interval_mining(&self) -> Option<u64> {
198 let mode = *self.mining_mode.read();
199 match mode {
200 MiningMode::Interval { tick } | MiningMode::MixedMining { tick } => {
201 Some(tick.as_secs())
202 }
203 _ => None,
204 }
205 }
206
207 /// Check if automatic mining is enabled.
208 ///
209 /// Returns true if the mining engine will automatically produce blocks
210 /// when new transactions are added to the transaction pool.
211 pub fn is_automine(&self) -> bool {
212 matches!(*self.mining_mode.read(), MiningMode::AutoMining)
213 }
214
215 /// Enable or disable automatic mining mode.
216 ///
217 /// When enabled, the mining engine will automatically produce a new block
218 /// whenever a transaction is added to the transaction pool. This provides
219 /// instant transaction confirmation in development environments.
220 ///
221 /// # Arguments
222 /// * `enabled` - True to enable auto-mining, false to disable
223 pub fn set_auto_mine(&self, enabled: bool) {
224 let mining_mode = match (self.is_automine(), enabled) {
225 (true, false) => Some(MiningMode::None),
226 (false, true) => Some(MiningMode::AutoMining),
227 _ => None,
228 };
229 if let Some(mining_mode) = mining_mode {
230 *self.mining_mode.write() = mining_mode;
231 self.wake();
232 }
233 }
234
235 /// Set the timestamp for the next block to be mined.
236 ///
237 /// Allows precise control over block timestamps for testing scenarios.
238 /// The timestamp must not be older than the current blockchain time.
239 ///
240 /// # Arguments
241 /// * `time_in_seconds` - Unix timestamp in seconds for the next block
242 ///
243 /// # Returns
244 /// * `Ok(())` - Timestamp set successfully
245 /// * `Err(MiningError::Timestamp)` - Invalid timestamp
246 pub fn set_next_block_timestamp(&self, time: Duration) -> Result<(), MiningError> {
247 self.time_manager
248 // this will convert the time_in_seconds in milliseconds. It is transparent
249 // to the user
250 .set_next_block_timestamp(time.as_secs())
251 .map_err(|_| MiningError::Timestamp)
252 }
253
254 /// Advance the blockchain time by a specified duration.
255 ///
256 /// Increases the current blockchain time, affecting timestamps of future blocks.
257 ///
258 /// # Arguments
259 /// * `time_in_seconds` - Duration to advance in seconds
260 ///
261 /// # Returns
262 /// * `new_timestamp` - The new offset to the current time as i64
263 pub fn increase_time(&self, time: Duration) -> i64 {
264 self.time_manager.increase_time(time.as_secs()).saturating_div(1000) as i64
265 }
266
267 /// Set the blockchain time to a specific timestamp.
268 ///
269 /// Resets the blockchain time to the specified timestamp and returns
270 /// the time difference from the previous timestamp.
271 ///
272 /// # Arguments
273 /// * `timestamp` - Target timestamp in seconds since Unix epoch
274 ///
275 /// # Returns
276 /// * `offset_seconds` - Time difference from previous timestamp
277 pub fn set_time(&self, timestamp: Duration) -> u64 {
278 let now = self.time_manager.current_call_timestamp();
279 self.time_manager.reset(timestamp.as_secs());
280 let offset = (timestamp.as_millis() as u64).saturating_sub(now);
281 Duration::from_millis(offset).as_secs()
282 }
283
284 /// Configure automatic timestamp intervals between blocks.
285 ///
286 /// Sets a fixed time interval that will be automatically added to each
287 /// block's timestamp relative to the previous block. This ensures
288 /// consistent time progression in the blockchain.
289 ///
290 /// # Arguments
291 /// * `interval_in_seconds` - Time interval to add between block timestamps
292 pub fn set_block_timestamp_interval(&self, interval_in_seconds: Duration) {
293 self.time_manager.set_block_timestamp_interval(interval_in_seconds.as_secs())
294 }
295
296 /// Remove automatic timestamp intervals between blocks.
297 ///
298 /// Disables the automatic timestamp interval feature, allowing blocks
299 /// to have timestamps based on the actual mining time rather than
300 /// fixed intervals.
301 ///
302 /// # Returns
303 /// * `true` - Timestamp interval was removed
304 /// * `false` - No timestamp interval was configured
305 pub fn remove_block_timestamp_interval(&self) -> bool {
306 self.time_manager.remove_block_timestamp_interval()
307 }
308
309 //---------- Helpers ---------------
310
311 fn wake(&self) {
312 self.waker.wake();
313 }
314
315 pub async fn do_evm_mine(&self, opts: Option<MineOptions>) -> Result<(u64, H256), MiningError> {
316 let mut blocks_to_mine = 1u64;
317 let mut last_hash = H256::zero();
318
319 if let Some(opts) = opts {
320 let timestamp = match opts {
321 MineOptions::Timestamp(timestamp) => timestamp,
322 MineOptions::Options { timestamp, blocks } => {
323 if let Some(blocks) = blocks {
324 blocks_to_mine = blocks;
325 }
326 timestamp
327 }
328 };
329 if let Some(timestamp) = timestamp {
330 // timestamp was explicitly provided to be the next timestamp
331 self.time_manager
332 .set_next_block_timestamp(timestamp)
333 .map_err(|_| MiningError::Timestamp)?;
334 }
335 }
336
337 for _ in 0..blocks_to_mine {
338 last_hash = seal_now(&self.seal_command_sender).await?.hash;
339 }
340
341 Ok((blocks_to_mine, last_hash))
342 }
343}
344
345async fn seal_now(
346 seal_command_sender: &Sender<EngineCommand<sp_core::H256>>,
347) -> Result<CreatedBlock<Hash>, MiningError> {
348 let (sender, receiver) = oneshot::channel();
349 let seal_command = EngineCommand::SealNewBlock {
350 create_empty: true,
351 finalize: true,
352 parent_hash: None,
353 sender: Some(sender),
354 };
355 seal_command_sender.send(seal_command).await.map_err(|_| MiningError::ClosedChannel)?;
356 match receiver.await {
357 Ok(Ok(rx)) => Ok(rx),
358 Ok(Err(e)) => Err(MiningError::BlockProducing(e)),
359 Err(_e) => Err(MiningError::ClosedChannel),
360 }
361}
362
363// --------------- MiningEngine runner
364type SealCommandStream = Pin<Box<dyn FusedStream<Item = ()> + Send>>;
365
366fn build_interval_stream(interval: Duration) -> SealCommandStream {
367 let mut interval_ticker = interval_at(Instant::now() + interval, interval);
368 interval_ticker.set_missed_tick_behavior(MissedTickBehavior::Delay);
369
370 let stream = unfold(interval_ticker, |mut interval_tick| async {
371 interval_tick.tick().await;
372 Some(((), interval_tick))
373 });
374 Box::pin(stream.fuse())
375}
376
377fn build_auto_stream(engine: &Arc<MiningEngine>) -> SealCommandStream {
378 let stream = engine.transaction_pool.import_notification_stream().map(|_| ());
379 Box::pin(stream.fuse())
380}
381
382fn build_streams_for_mode(
383 mode: MiningMode,
384 engine: &Arc<MiningEngine>,
385) -> SelectAll<SealCommandStream> {
386 let mut streams: Vec<SealCommandStream> = Vec::new();
387 if let Some(stream) = match mode {
388 MiningMode::Interval { tick } | MiningMode::MixedMining { tick } => Some(tick),
389 _ => None,
390 }
391 .map(build_interval_stream)
392 {
393 streams.push(stream)
394 }
395 if let Some(stream) = matches!(mode, MiningMode::AutoMining | MiningMode::MixedMining { .. })
396 .then(|| build_auto_stream(engine))
397 {
398 streams.push(stream)
399 }
400 select_all(streams)
401}
402
403async fn wait_for_mode_change(
404 engine: &Arc<MiningEngine>,
405 current: Option<MiningMode>,
406) -> MiningMode {
407 futures::future::poll_fn(|cx| {
408 let mode = *engine.mining_mode.read();
409 if current.as_ref().is_none_or(|m| *m != mode) {
410 return std::task::Poll::Ready(mode);
411 }
412 engine.waker.register(cx.waker());
413 std::task::Poll::Pending
414 })
415 .await
416}
417
418/// Run the mining engine background task.
419///
420/// This is the main event loop that handles block production based on the current
421/// mining mode. It monitors for mining mode changes, manages stream selectors for
422/// different trigger types (intervals, transactions), and coordinates block sealing
423/// operations.
424///
425/// The function runs indefinitely until the mining engine is shut down or a fatal
426/// error occurs.
427///
428/// # Arguments
429/// * `engine` - Shared reference to the mining engine to control
430///
431/// # Behavior
432/// - Monitors for mining mode changes and rebuilds event streams accordingly
433/// - Handles interval-based mining triggers using tokio timers
434/// - Handles transaction-based mining triggers from the transaction pool
435/// - Processes block sealing commands and logs results
436/// - Gracefully handles non-fatal errors and continues operation
437/// - Terminates on fatal errors (communication failures)
438///
439/// # Error Handling
440/// - **Fatal errors** (Canceled, SendError): Breaks the main loop and terminates
441/// - **Non-fatal errors**: Logged and operation continues
442/// - **Successful operations**: Block hash is logged at debug level
443pub async fn run_mining_engine(engine: Arc<MiningEngine>) {
444 let mut current_mode = None;
445 let mut combined_stream: SelectAll<SealCommandStream> = select_all(vec![]);
446
447 loop {
448 tokio::select! {
449 new_mode = wait_for_mode_change(&engine, current_mode) => {
450 current_mode = Some(new_mode);
451 combined_stream = build_streams_for_mode(new_mode, &engine);
452 }
453 Some(_) = combined_stream.next(), if !combined_stream.is_empty() => {
454 match seal_now(&engine.seal_command_sender).await {
455 Ok(block) => {
456 debug!(hash=?block.hash, "sealed");
457 }
458 Err(MiningError::ClosedChannel) => {
459 break; // fatal: break outer loop
460 }
461 Err(e) => {
462 error!(?e, "block production failed");
463 }
464 }
465 }
466 }
467 }
468}