1use 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
24pub(crate) const INSTANT_COALESCE_WINDOW: Duration = Duration::ZERO;
26
27const FAR_FUTURE: Duration = Duration::from_secs(86400 * 365 * 30);
29
30pub struct Miner<T> {
31 mode: Arc<RwLock<MiningMode>>,
33 generation: Arc<AtomicU64>,
36 pause_epoch: Arc<AtomicU64>,
40 inner: Arc<MinerInner>,
44 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 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 pub fn mode_write(&self) -> RwLockWriteGuard<'_, RawRwLock, MiningMode> {
80 self.mode.write()
81 }
82
83 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 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 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 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 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 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 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 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 pub(crate) fn is_paused(&self) -> bool {
192 self.pause_epoch.load(Ordering::SeqCst) % 2 == 1
193 }
194
195 pub(crate) fn is_stale(&self, pause_epoch: u64) -> bool {
197 self.pause_epoch.load(Ordering::SeqCst) != pause_epoch
198 }
199}
200
201pub(crate) struct MiningWork<T> {
203 pub(crate) transactions: Vec<Arc<PoolTransaction<T>>>,
204 pub(crate) generation: u64,
205 pub(crate) pause_epoch: u64,
207}
208
209#[derive(Debug)]
211pub struct MinerInner {
212 waker: AtomicWaker,
213}
214
215impl MinerInner {
216 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#[derive(Debug)]
234pub enum MiningMode {
235 None,
237 Auto(ReadyTransactionMiner),
242 FixedBlockTime(FixedBlockTimeMiner),
244
245 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 #[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 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 (Poll::Ready(mut auto_txs), Poll::Ready(fixed_txs)) => {
305 for tx in fixed_txs {
306 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 (Poll::Ready(auto_txs), Poll::Pending) => Poll::Ready(auto_txs),
316 (Poll::Pending, fixed_txs) => fixed_txs,
319 }
320 }
321 }
322 }
323
324 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 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#[derive(Debug)]
349pub struct FixedBlockTimeMiner {
350 period: Duration,
352 next_tick: Pin<Box<Sleep>>,
357}
358
359impl FixedBlockTimeMiner {
360 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 Poll::Ready(pool.ready_transactions().collect())
379 }
380
381 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
394fn 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
407fn 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
424pub struct ReadyTransactionMiner {
426 max_transactions: usize,
428 has_pending_txs: Option<bool>,
430 rx: Fuse<Receiver<TxHash>>,
432 coalesce_window: Duration,
434 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 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 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 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 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 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 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 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 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 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 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 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}