Skip to main content

anvil/eth/
miner.rs

1//! Mines transactions
2
3use crate::eth::pool::{Pool, transactions::PoolTransaction};
4use alloy_primitives::TxHash;
5use futures::{
6    channel::mpsc::Receiver,
7    stream::{Fuse, StreamExt},
8    task::AtomicWaker,
9};
10use parking_lot::{RawRwLock, RwLock, lock_api::RwLockWriteGuard};
11use std::{
12    fmt,
13    marker::PhantomData,
14    pin::Pin,
15    sync::{
16        Arc,
17        atomic::{AtomicU64, Ordering},
18    },
19    task::{Context, Poll},
20    time::Duration,
21};
22use tokio::time::{Instant, Sleep};
23
24/// Default delay before instant mining. Zero avoids delaying receipt availability.
25pub(crate) const INSTANT_COALESCE_WINDOW: Duration = Duration::ZERO;
26
27/// How far ahead to schedule a block whose deadline overflows an instant, as tokio's timers do.
28const FAR_FUTURE: Duration = Duration::from_secs(86400 * 365 * 30);
29
30pub struct Miner<T> {
31    /// The mode this miner currently operates in
32    mode: Arc<RwLock<MiningMode>>,
33    /// Identifies the current mode and pause state so stale candidate failures cannot modify the
34    /// schedule that replaced them.
35    generation: Arc<AtomicU64>,
36    /// Incremented whenever automatic block production is paused or resumed, so it is paused
37    /// while odd, see [`Miner::set_paused`]. Candidates selected before a pause compare their
38    /// epoch to skip mining after it, even once mining resumed.
39    pause_epoch: Arc<AtomicU64>,
40    /// used for task wake up when the mining mode was forcefully changed
41    ///
42    /// This will register the task so we can manually wake it up if the mining mode was changed
43    inner: Arc<MinerInner>,
44    /// Transaction type handled by the associated pool.
45    transaction: PhantomData<fn() -> T>,
46}
47
48impl<T> Clone for Miner<T> {
49    fn clone(&self) -> Self {
50        Self {
51            mode: self.mode.clone(),
52            generation: self.generation.clone(),
53            pause_epoch: self.pause_epoch.clone(),
54            inner: self.inner.clone(),
55            transaction: PhantomData,
56        }
57    }
58}
59
60impl<T> fmt::Debug for Miner<T> {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("Miner").field("mode", &self.mode).finish_non_exhaustive()
63    }
64}
65
66impl<T> Miner<T> {
67    /// Returns a new miner with that operates in the given `mode`.
68    pub fn new(mode: MiningMode) -> Self {
69        Self {
70            mode: Arc::new(RwLock::new(mode)),
71            generation: Default::default(),
72            pause_epoch: Default::default(),
73            inner: Default::default(),
74            transaction: PhantomData,
75        }
76    }
77
78    /// Returns the write lock of the mining mode
79    pub fn mode_write(&self) -> RwLockWriteGuard<'_, RawRwLock, MiningMode> {
80        self.mode.write()
81    }
82
83    /// Returns `true` if auto mining is enabled
84    pub fn is_auto_mine(&self) -> bool {
85        let mode = self.mode.read();
86        matches!(*mode, MiningMode::Auto(_))
87    }
88
89    pub fn get_interval(&self) -> Option<u64> {
90        let mode = self.mode.read();
91        if let MiningMode::FixedBlockTime(ref mm) = *mode {
92            return Some(mm.period.as_secs());
93        }
94        None
95    }
96
97    /// Returns the configured block interval for fixed or mixed mining.
98    pub(crate) fn block_interval(&self) -> Option<Duration> {
99        let mode = self.mode.read();
100        match &*mode {
101            MiningMode::FixedBlockTime(miner) | MiningMode::Mixed(_, miner) => Some(miner.period),
102            MiningMode::None | MiningMode::Auto(_) => None,
103        }
104    }
105
106    /// Sets the mining mode to operate in
107    pub fn set_mining_mode(&self, mode: MiningMode) {
108        let new_mode = format!("{mode:?}");
109        let mut current = self.mode_write();
110        let mode = std::mem::replace(&mut *current, mode);
111        self.generation.fetch_add(1, Ordering::Relaxed);
112        drop(current);
113        trace!(target: "miner", "updated mining mode from {:?} to {}", mode, new_mode);
114        self.inner.wake();
115    }
116
117    /// Resets the mode that launched a failed candidate.
118    pub(crate) fn handle_failed_candidate(&self, generation: u64) {
119        let mut mode = self.mode.write();
120        if self.generation.load(Ordering::Relaxed) != generation {
121            if let MiningMode::Auto(miner) | MiningMode::Mixed(miner, _) = &mut *mode {
122                miner.has_pending_txs = Some(true);
123                miner.coalesce = None;
124            }
125            return;
126        }
127        // Selecting the candidate already cleared `has_pending_txs`, so keep any retry requested
128        // since then, e.g. by a block mined via `evm_mine` before this failure was processed.
129        if let MiningMode::Auto(miner) | MiningMode::Mixed(miner, _) = &mut *mode {
130            miner.coalesce = None;
131        }
132        match &mut *mode {
133            MiningMode::FixedBlockTime(miner) | MiningMode::Mixed(_, miner) => {
134                *miner = FixedBlockTimeMiner::new(miner.period);
135            }
136            MiningMode::None | MiningMode::Auto(_) => {}
137        }
138    }
139
140    /// polls the [Pool] and returns those transactions that should be put in a block according to
141    /// the current mode.
142    ///
143    /// May return an empty list, if no transactions are ready.
144    pub(crate) fn poll(
145        &mut self,
146        pool: &Arc<Pool<T>>,
147        cx: &mut Context<'_>,
148    ) -> Poll<MiningWork<T>> {
149        self.inner.register(cx);
150        let mut mode = self.mode.write();
151        if self.is_paused() {
152            mode.poll_paused(cx);
153            return Poll::Pending;
154        }
155        let generation = self.generation.load(Ordering::Relaxed);
156        let pause_epoch = self.pause_epoch.load(Ordering::SeqCst);
157        mode.poll(pool, cx).map(|transactions| MiningWork { transactions, generation, pause_epoch })
158    }
159
160    /// Makes the instant miner select ready transactions again without waiting for a new one.
161    ///
162    /// Used when a mined block left ready transactions behind, e.g. because it ran out of gas.
163    pub(crate) fn retry_ready_transactions(&self) {
164        if let MiningMode::Auto(miner) | MiningMode::Mixed(miner, _) = &mut *self.mode.write() {
165            miner.has_pending_txs = Some(true);
166            self.inner.wake();
167        }
168    }
169
170    /// Pauses or resumes automatic block production.
171    ///
172    /// While paused, the block time interval keeps ticking but blocks that come due are skipped,
173    /// so the next block is mined on the first tick after mining resumes. Ready transactions in
174    /// auto mining mode are mined once mining resumes. The pause persists across mining mode
175    /// changes. Manual mining, e.g. via `evm_mine`, is not affected.
176    pub fn set_paused(&self, paused: bool) {
177        let mut mode = self.mode.write();
178        if paused != self.is_paused() {
179            if !paused {
180                mode.resume();
181            }
182            self.generation.fetch_add(1, Ordering::Relaxed);
183            self.pause_epoch.fetch_add(1, Ordering::SeqCst);
184        }
185        drop(mode);
186        trace!(target: "miner", paused, "updated mining pause");
187        self.inner.wake();
188    }
189
190    /// Returns `true` if automatic block production is paused.
191    pub(crate) fn is_paused(&self) -> bool {
192        self.pause_epoch.load(Ordering::SeqCst) % 2 == 1
193    }
194
195    /// Returns `true` if mining was paused since a candidate was selected at `pause_epoch`.
196    pub(crate) fn is_stale(&self, pause_epoch: u64) -> bool {
197        self.pause_epoch.load(Ordering::SeqCst) != pause_epoch
198    }
199}
200
201/// Transactions selected by a specific mining mode generation.
202pub(crate) struct MiningWork<T> {
203    pub(crate) transactions: Vec<Arc<PoolTransaction<T>>>,
204    pub(crate) generation: u64,
205    /// Pause epoch when the transactions were selected, see [`Miner::is_stale`].
206    pub(crate) pause_epoch: u64,
207}
208
209/// A Mining mode that does nothing
210#[derive(Debug)]
211pub struct MinerInner {
212    waker: AtomicWaker,
213}
214
215impl MinerInner {
216    /// Call the waker again
217    fn wake(&self) {
218        self.waker.wake();
219    }
220
221    fn register(&self, cx: &Context<'_>) {
222        self.waker.register(cx.waker());
223    }
224}
225
226impl Default for MinerInner {
227    fn default() -> Self {
228        Self { waker: AtomicWaker::new() }
229    }
230}
231
232/// Mode of operations for the `Miner`
233#[derive(Debug)]
234pub enum MiningMode {
235    /// A miner that does nothing
236    None,
237    /// A miner that listens for new transactions that are ready.
238    ///
239    /// Either one transaction will be mined per block, or any number of transactions will be
240    /// allowed
241    Auto(ReadyTransactionMiner),
242    /// A miner that constructs a new block every `interval` tick
243    FixedBlockTime(FixedBlockTimeMiner),
244
245    /// A miner that uses both Auto and FixedBlockTime
246    Mixed(ReadyTransactionMiner, FixedBlockTimeMiner),
247}
248
249impl MiningMode {
250    pub fn instant(max_transactions: usize, listener: Receiver<TxHash>) -> Self {
251        Self::Auto(ReadyTransactionMiner {
252            max_transactions,
253            has_pending_txs: None,
254            rx: listener.fuse(),
255            coalesce: None,
256            coalesce_window: INSTANT_COALESCE_WINDOW,
257        })
258    }
259
260    pub fn interval(duration: Duration) -> Self {
261        Self::FixedBlockTime(FixedBlockTimeMiner::new(duration))
262    }
263
264    pub fn mixed(max_transactions: usize, listener: Receiver<TxHash>, duration: Duration) -> Self {
265        Self::Mixed(
266            ReadyTransactionMiner {
267                max_transactions,
268                has_pending_txs: None,
269                rx: listener.fuse(),
270                coalesce: None,
271                coalesce_window: INSTANT_COALESCE_WINDOW,
272            },
273            FixedBlockTimeMiner::new(duration),
274        )
275    }
276
277    /// Sets the window for grouping ready transactions in auto or mixed mining mode.
278    /// A zero window mines ready transactions without a coalescing timer.
279    #[must_use]
280    pub fn with_coalescing_window(mut self, window: Duration) -> Self {
281        if let Self::Auto(miner) | Self::Mixed(miner, _) = &mut self {
282            miner.coalesce_window = window;
283            miner.coalesce = None;
284        }
285        self
286    }
287
288    /// polls the [Pool] and returns those transactions that should be put in a block, if any.
289    pub fn poll<T>(
290        &mut self,
291        pool: &Arc<Pool<T>>,
292        cx: &mut Context<'_>,
293    ) -> Poll<Vec<Arc<PoolTransaction<T>>>> {
294        match self {
295            Self::None => Poll::Pending,
296            Self::Auto(miner) => miner.poll(pool, cx),
297            Self::FixedBlockTime(miner) => miner.poll(pool, cx),
298            Self::Mixed(auto, fixed) => {
299                let auto_txs = auto.poll(pool, cx);
300                let fixed_txs = fixed.poll(pool, cx);
301
302                match (auto_txs, fixed_txs) {
303                    // Both auto and fixed transactions are ready, combine them
304                    (Poll::Ready(mut auto_txs), Poll::Ready(fixed_txs)) => {
305                        for tx in fixed_txs {
306                            // filter unique transactions
307                            if auto_txs.iter().any(|auto_tx| auto_tx.hash() == tx.hash()) {
308                                continue;
309                            }
310                            auto_txs.push(tx);
311                        }
312                        Poll::Ready(auto_txs)
313                    }
314                    // Only auto transactions are ready, return them
315                    (Poll::Ready(auto_txs), Poll::Pending) => Poll::Ready(auto_txs),
316                    // Only fixed transactions are ready or both are pending,
317                    // return fixed transactions or pending status
318                    (Poll::Pending, fixed_txs) => fixed_txs,
319                }
320            }
321        }
322    }
323
324    /// Keeps the mode's notifications moving while mining is paused, without selecting any
325    /// transactions.
326    fn poll_paused(&mut self, cx: &mut Context<'_>) {
327        if let Self::Auto(miner) | Self::Mixed(miner, _) = self {
328            miner.poll_notifications(cx);
329        }
330    }
331
332    /// Skips the block time ticks that came due while mining was paused, and makes auto mining
333    /// select ready transactions again, including any left behind by a failed block.
334    fn resume(&mut self) {
335        if let Self::Auto(miner) | Self::Mixed(miner, _) = self {
336            miner.has_pending_txs = Some(true);
337        }
338        if let Self::FixedBlockTime(miner) | Self::Mixed(_, miner) = self {
339            miner.skip_missed_ticks();
340        }
341    }
342}
343
344/// A miner that's supposed to create a new block every `interval`, mining all transactions that are
345/// ready at that time.
346///
347/// The default blocktime is set to 6 seconds
348#[derive(Debug)]
349pub struct FixedBlockTimeMiner {
350    /// The time between blocks.
351    period: Duration,
352    /// Completes when the next block is due.
353    ///
354    /// This owns the schedule instead of an [`Interval`](tokio::time::Interval) so that resuming
355    /// can skip ticks by their deadline, which the interval does not expose.
356    next_tick: Pin<Box<Sleep>>,
357}
358
359impl FixedBlockTimeMiner {
360    /// Creates a new instance with an interval of `duration`
361    pub fn new(duration: Duration) -> Self {
362        assert!(!duration.is_zero(), "`period` must be non-zero.");
363        let next_tick = Box::pin(tokio::time::sleep(duration));
364        Self { period: duration, next_tick }
365    }
366
367    fn poll<T>(
368        &mut self,
369        pool: &Arc<Pool<T>>,
370        cx: &mut Context<'_>,
371    ) -> Poll<Vec<Arc<PoolTransaction<T>>>> {
372        if self.next_tick.as_mut().poll(cx).is_pending() {
373            return Poll::Pending;
374        }
375        let next = next_deadline(self.next_tick.deadline(), Instant::now(), self.period);
376        self.next_tick.as_mut().reset(next);
377        // Drain the pool.
378        Poll::Ready(pool.ready_transactions().collect())
379    }
380
381    /// Moves the next tick past any ticks that came due while mining was paused, keeping the
382    /// schedule.
383    ///
384    /// This compares deadlines to the current time instead of polling the timer, which may not
385    /// have fired yet for a deadline that already passed.
386    fn skip_missed_ticks(&mut self) {
387        if let Some(next) = resumed_deadline(self.next_tick.deadline(), Instant::now(), self.period)
388        {
389            self.next_tick.as_mut().reset(next);
390        }
391    }
392}
393
394/// Returns the deadline of the tick after the one due at `deadline`, which fired at `now`.
395///
396/// Schedules like an interval with `MissedTickBehavior::Delay`, so that ticks are not shortened
397/// and late ticks shift the schedule rather than producing a burst of blocks.
398fn next_deadline(deadline: Instant, now: Instant, period: Duration) -> Instant {
399    let next = if now > deadline + Duration::from_millis(5) {
400        now.checked_add(period)
401    } else {
402        deadline.checked_add(period)
403    };
404    next.unwrap_or_else(|| now + FAR_FUTURE)
405}
406
407/// Returns the first deadline after `now` on the schedule of `deadline`, or `None` if the tick
408/// due at `deadline` has not come due.
409fn resumed_deadline(deadline: Instant, now: Instant, period: Duration) -> Option<Instant> {
410    if deadline > now {
411        return None;
412    }
413    let late = (now - deadline).as_nanos() % period.as_nanos();
414    let next = now.checked_add(period - Duration::from_nanos_u128(late));
415    Some(next.unwrap_or_else(|| now + FAR_FUTURE))
416}
417
418impl Default for FixedBlockTimeMiner {
419    fn default() -> Self {
420        Self::new(Duration::from_secs(6))
421    }
422}
423
424/// A miner that Listens for new ready transactions
425pub struct ReadyTransactionMiner {
426    /// how many transactions to mine per block
427    max_transactions: usize,
428    /// stores whether there are pending transactions (if known)
429    has_pending_txs: Option<bool>,
430    /// Receives hashes of transactions that are ready
431    rx: Fuse<Receiver<TxHash>>,
432    /// Delay for accumulating ready transactions; zero disables coalescing.
433    coalesce_window: Duration,
434    /// Active coalescing timer; while pending, ready txs are accumulated.
435    coalesce: Option<Pin<Box<Sleep>>>,
436}
437
438impl ReadyTransactionMiner {
439    fn poll<T>(
440        &mut self,
441        pool: &Arc<Pool<T>>,
442        cx: &mut Context<'_>,
443    ) -> Poll<Vec<Arc<PoolTransaction<T>>>> {
444        // Arm the coalescing window only on fresh notifications to avoid delaying
445        // consecutive chunks when draining a backlog larger than `max_transactions`.
446        if self.poll_notifications(cx) && self.coalesce.is_none() && !self.coalesce_window.is_zero()
447        {
448            self.coalesce = Some(Box::pin(tokio::time::sleep(self.coalesce_window)));
449        }
450
451        if self.has_pending_txs == Some(false) {
452            return Poll::Pending;
453        }
454
455        if let Some(sleep) = self.coalesce.as_mut()
456            && sleep.as_mut().poll(cx).is_pending()
457        {
458            return Poll::Pending;
459        }
460        self.coalesce = None;
461
462        let transactions =
463            pool.ready_transactions().take(self.max_transactions).collect::<Vec<_>>();
464
465        // Whether ready txs left behind are worth another block is decided once this block is
466        // mined, see `Miner::retry_ready_transactions`.
467        self.has_pending_txs = Some(false);
468
469        if transactions.is_empty() {
470            return Poll::Pending;
471        }
472
473        Poll::Ready(transactions)
474    }
475
476    /// Drains ready notifications and reports whether any arrived.
477    fn poll_notifications(&mut self, cx: &mut Context<'_>) -> bool {
478        let mut saw_new_ready = false;
479        while let Poll::Ready(Some(_hash)) = self.rx.poll_next_unpin(cx) {
480            saw_new_ready = true;
481        }
482        if saw_new_ready {
483            self.has_pending_txs = Some(true);
484        }
485        saw_new_ready
486    }
487}
488
489impl fmt::Debug for ReadyTransactionMiner {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        f.debug_struct("ReadyTransactionMiner")
492            .field("max_transactions", &self.max_transactions)
493            .finish_non_exhaustive()
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use futures::{channel::mpsc, future::poll_fn};
501
502    fn with_fixed<R>(miner: &Miner<()>, f: impl FnOnce(&mut FixedBlockTimeMiner) -> R) -> R {
503        let mut mode = miner.mode.write();
504        let (MiningMode::FixedBlockTime(fixed) | MiningMode::Mixed(_, fixed)) = &mut *mode else {
505            panic!("expected block time mining")
506        };
507        f(fixed)
508    }
509
510    fn deadline(miner: &Miner<()>) -> Instant {
511        with_fixed(miner, |fixed| fixed.next_tick.deadline())
512    }
513
514    fn set_deadline(miner: &Miner<()>, deadline: Instant) {
515        with_fixed(miner, |fixed| fixed.next_tick.as_mut().reset(deadline));
516    }
517
518    #[test]
519    fn stale_failure_resumes_replacement_autominer() {
520        let (_tx, rx) = mpsc::channel(1);
521        let miner = Miner::<()>::new(MiningMode::None);
522        miner.set_mining_mode(MiningMode::instant(1, rx));
523
524        miner.handle_failed_candidate(0);
525
526        let mode = miner.mode.read();
527        let MiningMode::Auto(auto) = &*mode else { panic!("expected auto mining") };
528        assert_eq!(auto.has_pending_txs, Some(true));
529    }
530
531    #[test]
532    fn failure_keeps_retry_requested_after_candidate_selection() {
533        let (_tx, rx) = mpsc::channel(1);
534        let miner = Miner::<()>::new(MiningMode::instant(1, rx));
535        miner.retry_ready_transactions();
536
537        miner.handle_failed_candidate(0);
538
539        let mode = miner.mode.read();
540        let MiningMode::Auto(auto) = &*mode else { panic!("expected auto mining") };
541        assert_eq!(auto.has_pending_txs, Some(true));
542    }
543
544    #[tokio::test]
545    async fn failed_fixed_candidate_rearms_interval() {
546        let mut miner = Miner::<()>::new(MiningMode::interval(Duration::from_millis(10)));
547        let pool = Arc::new(Pool::default());
548        tokio::time::timeout(Duration::from_secs(1), poll_fn(|cx| miner.poll(&pool, cx)))
549            .await
550            .unwrap();
551
552        miner.handle_failed_candidate(0);
553
554        tokio::time::timeout(Duration::from_secs(1), poll_fn(|cx| miner.poll(&pool, cx)))
555            .await
556            .unwrap();
557    }
558
559    #[tokio::test]
560    async fn coalescing_window_controls_fresh_notifications() {
561        for mixed in [false, true] {
562            for window in [None, Some(Duration::ZERO), Some(Duration::from_secs(60))] {
563                let (mut tx, rx) = mpsc::channel(1);
564                let mode = if mixed {
565                    MiningMode::mixed(1, rx, Duration::from_secs(120))
566                } else {
567                    MiningMode::instant(1, rx)
568                };
569                let mut mode = if let Some(window) = window {
570                    mode.with_coalescing_window(window)
571                } else {
572                    mode
573                };
574                let pool = Arc::new(Pool::<()>::default());
575                tx.try_send(TxHash::ZERO).unwrap();
576                let mut cx = Context::from_waker(futures::task::noop_waker_ref());
577                assert!(mode.poll(&pool, &mut cx).is_pending());
578                let auto = match &mode {
579                    MiningMode::Auto(auto) | MiningMode::Mixed(auto, _) => auto,
580                    _ => unreachable!(),
581                };
582                if window.is_none_or(|window| window.is_zero()) {
583                    assert!(auto.coalesce.is_none());
584                    assert_eq!(auto.has_pending_txs, Some(false));
585                } else {
586                    assert!(auto.coalesce.is_some());
587                    assert_eq!(auto.has_pending_txs, Some(true));
588                }
589            }
590        }
591    }
592
593    #[tokio::test]
594    async fn paused_interval_skips_due_block() {
595        let t0 = Instant::now();
596        let period = Duration::from_secs(1);
597        assert_eq!(resumed_deadline(t0 + period, t0 + Duration::from_millis(500), period), None);
598        // Resume must skip overdue ticks without moving the next tick from 4s to 4.5s.
599        assert_eq!(
600            resumed_deadline(t0 + period, t0 + Duration::from_millis(3500), period),
601            Some(t0 + period * 4)
602        );
603        assert_eq!(resumed_deadline(t0 + period, t0 + period * 4, period), Some(t0 + period * 5));
604
605        let period = Duration::from_millis(100);
606        for mixed in [false, true] {
607            for poll_while_paused in [false, true] {
608                let (_tx, rx) = mpsc::channel(1);
609                let mode = if mixed {
610                    MiningMode::mixed(1, rx, period)
611                } else {
612                    MiningMode::interval(period)
613                };
614                let mut miner = Miner::<()>::new(mode);
615                let pool = Arc::new(Pool::default());
616                let mut cx = Context::from_waker(futures::task::noop_waker_ref());
617                let due = Instant::now() - period * 5 / 2;
618                set_deadline(&miner, due);
619                miner.set_paused(true);
620                if poll_while_paused {
621                    assert!(miner.poll(&pool, &mut cx).is_pending());
622                }
623
624                let before = Instant::now();
625                miner.set_paused(false);
626                let next = deadline(&miner);
627                assert!(next > before, "overdue tick not skipped");
628                assert_eq!((next - due).as_nanos() % period.as_nanos(), 0, "schedule moved");
629            }
630        }
631    }
632
633    #[test]
634    fn paused_autominer_keeps_ready_notifications() {
635        let (mut tx, rx) = mpsc::channel(1);
636        let mut miner = Miner::<()>::new(MiningMode::instant(1, rx));
637        let pool = Arc::new(Pool::default());
638        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
639        miner.set_paused(true);
640        // A channel of size 1 with one sender buffers two notifications.
641        let fill = |tx: &mut mpsc::Sender<TxHash>| {
642            tx.try_send(TxHash::ZERO).unwrap();
643            tx.try_send(TxHash::ZERO).unwrap();
644        };
645        fill(&mut tx);
646
647        assert!(miner.poll(&pool, &mut cx).is_pending());
648        // Polling while paused drained the notifications.
649        fill(&mut tx);
650        let mode = miner.mode.read();
651        let MiningMode::Auto(auto) = &*mode else { panic!("expected auto mining") };
652        assert_eq!(auto.has_pending_txs, Some(true));
653    }
654
655    #[tokio::test]
656    async fn resume_skips_due_tick_before_timer_fires() {
657        let period = Duration::from_millis(100);
658        let mut miner = Miner::<()>::new(MiningMode::interval(period));
659        let pool = Arc::new(Pool::default());
660        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
661        assert!(miner.poll(&pool, &mut cx).is_pending());
662        miner.set_paused(true);
663
664        // Resume decides from the deadline alone, so it skips the tick even though the timer
665        // never got to fire.
666        let due = Instant::now() - period / 2;
667        set_deadline(&miner, due);
668        let before = Instant::now();
669        miner.set_paused(false);
670
671        let next = deadline(&miner);
672        assert!(next > before, "overdue tick not skipped");
673        assert_eq!((next - due).as_nanos() % period.as_nanos(), 0, "schedule moved");
674    }
675
676    #[tokio::test]
677    async fn late_tick_delays_schedule() {
678        let t0 = Instant::now();
679        let period = Duration::from_secs(1);
680        // A tick polled on time keeps the schedule.
681        assert_eq!(next_deadline(t0 + period, t0 + period, period), t0 + period * 2);
682        assert_eq!(
683            next_deadline(t0 + period, t0 + period + Duration::from_millis(5), period),
684            t0 + period * 2
685        );
686        // A late tick delays it by its lateness.
687        assert_eq!(
688            next_deadline(t0 + period * 2, t0 + Duration::from_millis(2500), period),
689            t0 + Duration::from_millis(3500)
690        );
691
692        let period = Duration::from_millis(100);
693        let mut miner = Miner::<()>::new(MiningMode::interval(period));
694        let pool = Arc::new(Pool::default());
695        set_deadline(&miner, Instant::now() - period * 3 / 2);
696        let before = Instant::now();
697        poll_fn(|cx| miner.poll(&pool, cx)).await;
698        let after = Instant::now();
699        let next = deadline(&miner);
700        assert!(next >= before + period && next <= after + period, "late tick kept schedule");
701    }
702
703    #[tokio::test]
704    async fn failure_selected_before_pause_keeps_schedule() {
705        let period = Duration::from_secs(3600);
706        let miner = Miner::<()>::new(MiningMode::interval(period));
707        let next = Instant::now() + period * 2;
708        set_deadline(&miner, next);
709        miner.set_paused(true);
710        miner.set_paused(false);
711
712        // The candidate selected at generation 0, before the pause, fails after mining resumed.
713        miner.handle_failed_candidate(0);
714
715        assert_eq!(deadline(&miner), next);
716    }
717
718    #[test]
719    fn resume_retries_ready_transactions_after_failure() {
720        let (_tx, rx) = mpsc::channel(1);
721        let mut miner = Miner::<()>::new(MiningMode::instant(1, rx));
722        let pool = Arc::new(Pool::default());
723        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
724        // The candidate failed before the pause, so its transactions are no longer retried.
725        assert!(miner.poll(&pool, &mut cx).is_pending());
726        miner.handle_failed_candidate(0);
727        miner.set_paused(true);
728        miner.set_paused(false);
729
730        let mode = miner.mode.read();
731        let MiningMode::Auto(auto) = &*mode else { panic!("expected auto mining") };
732        assert_eq!(auto.has_pending_txs, Some(true));
733    }
734
735    #[test]
736    fn overflowing_tick_waits_indefinitely() {
737        let t0 = Instant::now();
738        let now = t0 + Duration::from_secs(1);
739        assert_eq!(next_deadline(t0, now, Duration::MAX), now + FAR_FUTURE);
740        assert_eq!(resumed_deadline(t0, now, Duration::MAX), Some(now + FAR_FUTURE));
741    }
742}