Skip to main content

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}