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::{Interval, MissedTickBehavior, Sleep};
23
24/// Window for grouping concurrently-submitted transactions into one instant-mined block.
25/// Scoped to batch/in-process concurrency; not a guarantee for independent external clients.
26const INSTANT_COALESCE_WINDOW: Duration = Duration::from_millis(5);
27
28pub struct Miner<T> {
29    /// The mode this miner currently operates in
30    mode: Arc<RwLock<MiningMode>>,
31    /// Identifies the current mode so stale candidate failures cannot modify its replacement.
32    generation: Arc<AtomicU64>,
33    /// used for task wake up when the mining mode was forcefully changed
34    ///
35    /// This will register the task so we can manually wake it up if the mining mode was changed
36    inner: Arc<MinerInner>,
37    /// Transaction type handled by the associated pool.
38    transaction: PhantomData<fn() -> T>,
39}
40
41impl<T> Clone for Miner<T> {
42    fn clone(&self) -> Self {
43        Self {
44            mode: self.mode.clone(),
45            generation: self.generation.clone(),
46            inner: self.inner.clone(),
47            transaction: PhantomData,
48        }
49    }
50}
51
52impl<T> fmt::Debug for Miner<T> {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.debug_struct("Miner").field("mode", &self.mode).finish_non_exhaustive()
55    }
56}
57
58impl<T> Miner<T> {
59    /// Returns a new miner with that operates in the given `mode`.
60    pub fn new(mode: MiningMode) -> Self {
61        Self {
62            mode: Arc::new(RwLock::new(mode)),
63            generation: Default::default(),
64            inner: Default::default(),
65            transaction: PhantomData,
66        }
67    }
68
69    /// Returns the write lock of the mining mode
70    pub fn mode_write(&self) -> RwLockWriteGuard<'_, RawRwLock, MiningMode> {
71        self.mode.write()
72    }
73
74    /// Returns `true` if auto mining is enabled
75    pub fn is_auto_mine(&self) -> bool {
76        let mode = self.mode.read();
77        matches!(*mode, MiningMode::Auto(_))
78    }
79
80    pub fn get_interval(&self) -> Option<u64> {
81        let mode = self.mode.read();
82        if let MiningMode::FixedBlockTime(ref mm) = *mode {
83            return Some(mm.interval.period().as_secs());
84        }
85        None
86    }
87
88    /// Returns the configured block interval for fixed or mixed mining.
89    pub(crate) fn block_interval(&self) -> Option<Duration> {
90        let mode = self.mode.read();
91        match &*mode {
92            MiningMode::FixedBlockTime(miner) | MiningMode::Mixed(_, miner) => {
93                Some(miner.interval.period())
94            }
95            MiningMode::None | MiningMode::Auto(_) => None,
96        }
97    }
98
99    /// Sets the mining mode to operate in
100    pub fn set_mining_mode(&self, mode: MiningMode) {
101        let new_mode = format!("{mode:?}");
102        let mut current = self.mode_write();
103        let mode = std::mem::replace(&mut *current, mode);
104        self.generation.fetch_add(1, Ordering::Relaxed);
105        drop(current);
106        trace!(target: "miner", "updated mining mode from {:?} to {}", mode, new_mode);
107        self.inner.wake();
108    }
109
110    /// Resets the mode that launched a failed candidate.
111    pub(crate) fn handle_failed_candidate(&self, generation: u64) {
112        let mut mode = self.mode.write();
113        if self.generation.load(Ordering::Relaxed) != generation {
114            if let MiningMode::Auto(miner) | MiningMode::Mixed(miner, _) = &mut *mode {
115                miner.has_pending_txs = Some(true);
116                miner.coalesce = None;
117            }
118            return;
119        }
120        match &mut *mode {
121            MiningMode::Auto(miner) | MiningMode::Mixed(miner, _) => {
122                miner.has_pending_txs = Some(false);
123                miner.coalesce = None;
124            }
125            MiningMode::None | MiningMode::FixedBlockTime(_) => {}
126        }
127        match &mut *mode {
128            MiningMode::FixedBlockTime(miner) | MiningMode::Mixed(_, miner) => {
129                let period = miner.interval.period();
130                *miner = FixedBlockTimeMiner::new(period);
131            }
132            MiningMode::None | MiningMode::Auto(_) => {}
133        }
134    }
135
136    /// polls the [Pool] and returns those transactions that should be put in a block according to
137    /// the current mode.
138    ///
139    /// May return an empty list, if no transactions are ready.
140    pub(crate) fn poll(
141        &mut self,
142        pool: &Arc<Pool<T>>,
143        cx: &mut Context<'_>,
144    ) -> Poll<MiningWork<T>> {
145        self.inner.register(cx);
146        let mut mode = self.mode.write();
147        let generation = self.generation.load(Ordering::Relaxed);
148        mode.poll(pool, cx).map(|transactions| MiningWork { transactions, generation })
149    }
150}
151
152/// Transactions selected by a specific mining mode generation.
153pub(crate) struct MiningWork<T> {
154    pub(crate) transactions: Vec<Arc<PoolTransaction<T>>>,
155    pub(crate) generation: u64,
156}
157
158/// A Mining mode that does nothing
159#[derive(Debug)]
160pub struct MinerInner {
161    waker: AtomicWaker,
162}
163
164impl MinerInner {
165    /// Call the waker again
166    fn wake(&self) {
167        self.waker.wake();
168    }
169
170    fn register(&self, cx: &Context<'_>) {
171        self.waker.register(cx.waker());
172    }
173}
174
175impl Default for MinerInner {
176    fn default() -> Self {
177        Self { waker: AtomicWaker::new() }
178    }
179}
180
181/// Mode of operations for the `Miner`
182#[derive(Debug)]
183pub enum MiningMode {
184    /// A miner that does nothing
185    None,
186    /// A miner that listens for new transactions that are ready.
187    ///
188    /// Either one transaction will be mined per block, or any number of transactions will be
189    /// allowed
190    Auto(ReadyTransactionMiner),
191    /// A miner that constructs a new block every `interval` tick
192    FixedBlockTime(FixedBlockTimeMiner),
193
194    /// A miner that uses both Auto and FixedBlockTime
195    Mixed(ReadyTransactionMiner, FixedBlockTimeMiner),
196}
197
198impl MiningMode {
199    pub fn instant(max_transactions: usize, listener: Receiver<TxHash>) -> Self {
200        Self::Auto(ReadyTransactionMiner {
201            max_transactions,
202            has_pending_txs: None,
203            rx: listener.fuse(),
204            coalesce: None,
205        })
206    }
207
208    pub fn interval(duration: Duration) -> Self {
209        Self::FixedBlockTime(FixedBlockTimeMiner::new(duration))
210    }
211
212    pub fn mixed(max_transactions: usize, listener: Receiver<TxHash>, duration: Duration) -> Self {
213        Self::Mixed(
214            ReadyTransactionMiner {
215                max_transactions,
216                has_pending_txs: None,
217                rx: listener.fuse(),
218                coalesce: None,
219            },
220            FixedBlockTimeMiner::new(duration),
221        )
222    }
223
224    /// polls the [Pool] and returns those transactions that should be put in a block, if any.
225    pub fn poll<T>(
226        &mut self,
227        pool: &Arc<Pool<T>>,
228        cx: &mut Context<'_>,
229    ) -> Poll<Vec<Arc<PoolTransaction<T>>>> {
230        match self {
231            Self::None => Poll::Pending,
232            Self::Auto(miner) => miner.poll(pool, cx),
233            Self::FixedBlockTime(miner) => miner.poll(pool, cx),
234            Self::Mixed(auto, fixed) => {
235                let auto_txs = auto.poll(pool, cx);
236                let fixed_txs = fixed.poll(pool, cx);
237
238                match (auto_txs, fixed_txs) {
239                    // Both auto and fixed transactions are ready, combine them
240                    (Poll::Ready(mut auto_txs), Poll::Ready(fixed_txs)) => {
241                        for tx in fixed_txs {
242                            // filter unique transactions
243                            if auto_txs.iter().any(|auto_tx| auto_tx.hash() == tx.hash()) {
244                                continue;
245                            }
246                            auto_txs.push(tx);
247                        }
248                        Poll::Ready(auto_txs)
249                    }
250                    // Only auto transactions are ready, return them
251                    (Poll::Ready(auto_txs), Poll::Pending) => Poll::Ready(auto_txs),
252                    // Only fixed transactions are ready or both are pending,
253                    // return fixed transactions or pending status
254                    (Poll::Pending, fixed_txs) => fixed_txs,
255                }
256            }
257        }
258    }
259}
260
261/// A miner that's supposed to create a new block every `interval`, mining all transactions that are
262/// ready at that time.
263///
264/// The default blocktime is set to 6 seconds
265#[derive(Debug)]
266pub struct FixedBlockTimeMiner {
267    /// The interval this fixed block time miner operates with
268    interval: Interval,
269}
270
271impl FixedBlockTimeMiner {
272    /// Creates a new instance with an interval of `duration`
273    pub fn new(duration: Duration) -> Self {
274        let start = tokio::time::Instant::now() + duration;
275        let mut interval = tokio::time::interval_at(start, duration);
276        // we use delay here, to ensure ticks are not shortened and to tick at multiples of interval
277        // from when tick was called rather than from start
278        interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
279        Self { interval }
280    }
281
282    fn poll<T>(
283        &mut self,
284        pool: &Arc<Pool<T>>,
285        cx: &mut Context<'_>,
286    ) -> Poll<Vec<Arc<PoolTransaction<T>>>> {
287        if self.interval.poll_tick(cx).is_ready() {
288            // drain the pool
289            return Poll::Ready(pool.ready_transactions().collect());
290        }
291        Poll::Pending
292    }
293}
294
295impl Default for FixedBlockTimeMiner {
296    fn default() -> Self {
297        Self::new(Duration::from_secs(6))
298    }
299}
300
301/// A miner that Listens for new ready transactions
302pub struct ReadyTransactionMiner {
303    /// how many transactions to mine per block
304    max_transactions: usize,
305    /// stores whether there are pending transactions (if known)
306    has_pending_txs: Option<bool>,
307    /// Receives hashes of transactions that are ready
308    rx: Fuse<Receiver<TxHash>>,
309    /// Active [`INSTANT_COALESCE_WINDOW`] timer; while pending, ready txs are accumulated.
310    coalesce: Option<Pin<Box<Sleep>>>,
311}
312
313impl ReadyTransactionMiner {
314    fn poll<T>(
315        &mut self,
316        pool: &Arc<Pool<T>>,
317        cx: &mut Context<'_>,
318    ) -> Poll<Vec<Arc<PoolTransaction<T>>>> {
319        // always drain the notification stream so that we're woken up as soon as there's a new tx
320        let mut saw_new_ready = false;
321        while let Poll::Ready(Some(_hash)) = self.rx.poll_next_unpin(cx) {
322            saw_new_ready = true;
323        }
324
325        // Arm the coalescing window only on fresh notifications to avoid delaying
326        // consecutive chunks when draining a backlog larger than `max_transactions`.
327        if saw_new_ready {
328            self.has_pending_txs = Some(true);
329            if self.coalesce.is_none() {
330                self.coalesce = Some(Box::pin(tokio::time::sleep(INSTANT_COALESCE_WINDOW)));
331            }
332        }
333
334        if self.has_pending_txs == Some(false) {
335            return Poll::Pending;
336        }
337
338        if let Some(sleep) = self.coalesce.as_mut()
339            && sleep.as_mut().poll(cx).is_pending()
340        {
341            return Poll::Pending;
342        }
343        self.coalesce = None;
344
345        let transactions =
346            pool.ready_transactions().take(self.max_transactions).collect::<Vec<_>>();
347
348        // there are pending transactions if we didn't drain the pool
349        self.has_pending_txs = Some(transactions.len() >= self.max_transactions);
350
351        if transactions.is_empty() {
352            self.has_pending_txs = Some(false);
353            return Poll::Pending;
354        }
355
356        Poll::Ready(transactions)
357    }
358}
359
360impl fmt::Debug for ReadyTransactionMiner {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        f.debug_struct("ReadyTransactionMiner")
363            .field("max_transactions", &self.max_transactions)
364            .finish_non_exhaustive()
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use futures::{channel::mpsc, future::poll_fn};
372
373    #[test]
374    fn stale_failure_resumes_replacement_autominer() {
375        let (_tx, rx) = mpsc::channel(1);
376        let miner = Miner::<()>::new(MiningMode::None);
377        miner.set_mining_mode(MiningMode::instant(1, rx));
378
379        miner.handle_failed_candidate(0);
380
381        let mode = miner.mode.read();
382        let MiningMode::Auto(auto) = &*mode else { panic!("expected auto mining") };
383        assert_eq!(auto.has_pending_txs, Some(true));
384    }
385
386    #[tokio::test]
387    async fn failed_fixed_candidate_rearms_interval() {
388        let mut miner = Miner::<()>::new(MiningMode::interval(Duration::from_millis(10)));
389        let pool = Arc::new(Pool::default());
390        tokio::time::timeout(Duration::from_secs(1), poll_fn(|cx| miner.poll(&pool, cx)))
391            .await
392            .unwrap();
393
394        miner.handle_failed_candidate(0);
395
396        tokio::time::timeout(Duration::from_secs(1), poll_fn(|cx| miner.poll(&pool, cx)))
397            .await
398            .unwrap();
399    }
400}