Skip to main content

anvil/
service.rs

1//! background service
2
3use crate::{
4    NodeResult,
5    eth::{
6        backend::validate::TransactionValidator, error::BlockchainError, fees::FeeHistoryService,
7        miner::Miner, pool::Pool,
8    },
9    filter::Filters,
10    mem::Backend,
11};
12use alloy_consensus::TxReceipt;
13use alloy_network::Network;
14use foundry_primitives::{FoundryReceiptEnvelope, FoundryTxEnvelope};
15use futures::{FutureExt, Stream, StreamExt};
16use std::{
17    collections::VecDeque,
18    pin::Pin,
19    sync::Arc,
20    task::{Context, Poll},
21};
22use tokio::{task::JoinHandle, time::Interval};
23
24/// The type that drives the blockchain's state
25///
26/// This service is basically an endless future that continuously polls the miner which returns
27/// transactions for the next block, then those transactions are handed off to the backend to
28/// construct a new block, if all transactions were successfully included in a new block they get
29/// purged from the `Pool`.
30pub struct NodeService<N: Network>
31where
32    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
33{
34    /// The pool that holds all transactions.
35    pool: Arc<Pool<N::TxEnvelope>>,
36    /// Creates new blocks.
37    block_producer: BlockProducer<N>,
38    /// The miner responsible to select transactions from the `pool`.
39    miner: Miner<N::TxEnvelope>,
40    /// Maintenance task for fee history related tasks.
41    fee_history: FeeHistoryService<N>,
42    /// Tracks all active filters
43    filters: Filters<N>,
44    /// The interval at which to check for filters that need to be evicted
45    filter_eviction_interval: Interval,
46}
47
48impl<N: Network> NodeService<N>
49where
50    Backend<N>: TransactionValidator<N::TxEnvelope>,
51    N: Network<TxEnvelope = FoundryTxEnvelope, ReceiptEnvelope = FoundryReceiptEnvelope>,
52{
53    pub fn new(
54        pool: Arc<Pool<N::TxEnvelope>>,
55        backend: Arc<Backend<N>>,
56        miner: Miner<N::TxEnvelope>,
57        fee_history: FeeHistoryService<N>,
58        filters: Filters<N>,
59    ) -> Self {
60        let start = tokio::time::Instant::now() + filters.keep_alive();
61        let filter_eviction_interval = tokio::time::interval_at(start, filters.keep_alive());
62        Self {
63            block_producer: BlockProducer::new(backend, pool.clone(), miner.clone()),
64            pool,
65            miner,
66            fee_history,
67            filter_eviction_interval,
68            filters,
69        }
70    }
71}
72
73impl<N: Network> Future for NodeService<N>
74where
75    Backend<N>: TransactionValidator<N::TxEnvelope>,
76    N: Network<TxEnvelope = FoundryTxEnvelope, ReceiptEnvelope = FoundryReceiptEnvelope>,
77{
78    type Output = NodeResult<()>;
79
80    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
81        let pin = self.get_mut();
82
83        // this drives block production and feeds new sets of ready transactions to the block
84        // producer
85        loop {
86            // advance block production until pending
87            while let Poll::Ready(Some(result)) = pin.block_producer.poll_next_unpin(cx) {
88                match result {
89                    BlockProduction::Mined(block_number) => {
90                        trace!(target: "node", "mined block {block_number}");
91                    }
92                    BlockProduction::Failed(generation) => {
93                        pin.miner.handle_failed_candidate(generation);
94                        break;
95                    }
96                    BlockProduction::Skipped => {}
97                }
98            }
99
100            // Do not select snapshots while another candidate is in flight. This leaves newer
101            // ready notifications in the miner so a failed candidate cannot discard their work.
102            if pin.block_producer.is_idle()
103                && let Poll::Ready(work) = pin.miner.poll(&pin.pool, cx)
104            {
105                // miner returned a set of transaction that we feed to the producer
106                pin.block_producer.queued.push_back(work);
107            } else {
108                // no progress made
109                break;
110            }
111        }
112
113        // poll the fee history task
114        let _ = pin.fee_history.poll_unpin(cx);
115
116        if pin.filter_eviction_interval.poll_tick(cx).is_ready() {
117            let filters = pin.filters.clone();
118
119            // evict filters that timed out
120            tokio::task::spawn(async move { filters.evict().await });
121        }
122
123        Poll::Pending
124    }
125}
126
127type MiningResult<N> = (Result<Option<u64>, BlockchainError>, Arc<Backend<N>>, u64);
128
129enum BlockProduction {
130    Mined(u64),
131    Failed(u64),
132    Skipped,
133}
134
135/// A type that exclusively mines one block at a time
136#[must_use = "streams do nothing unless polled"]
137struct BlockProducer<N: Network> {
138    /// Holds the backend if no block is being mined
139    idle_backend: Option<Arc<Backend<N>>>,
140    /// Pool used to select transactions while holding the mining lock.
141    pool: Arc<Pool<N::TxEnvelope>>,
142    /// Miner to wake when a mined block leaves ready transactions behind.
143    miner: Miner<N::TxEnvelope>,
144    /// Single active future that mines a new block
145    block_mining: Option<JoinHandle<MiningResult<N>>>,
146    /// backlog of sets of transactions ready to be mined
147    queued: VecDeque<crate::eth::miner::MiningWork<N::TxEnvelope>>,
148}
149
150impl<N: Network> BlockProducer<N>
151where
152    Backend<N>: TransactionValidator<N::TxEnvelope>,
153    N: Network<TxEnvelope = FoundryTxEnvelope, ReceiptEnvelope = FoundryReceiptEnvelope>,
154{
155    fn new(
156        backend: Arc<Backend<N>>,
157        pool: Arc<Pool<N::TxEnvelope>>,
158        miner: Miner<N::TxEnvelope>,
159    ) -> Self {
160        Self {
161            idle_backend: Some(backend),
162            pool,
163            miner,
164            block_mining: None,
165            queued: Default::default(),
166        }
167    }
168
169    fn is_idle(&self) -> bool {
170        self.idle_backend.is_some() && self.block_mining.is_none() && self.queued.is_empty()
171    }
172}
173
174impl<N: Network> Stream for BlockProducer<N>
175where
176    Backend<N>: TransactionValidator<N::TxEnvelope> + Send + Sync + 'static,
177    N: Network<TxEnvelope = FoundryTxEnvelope, ReceiptEnvelope = FoundryReceiptEnvelope> + 'static,
178{
179    type Item = BlockProduction;
180
181    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
182        let pin = self.get_mut();
183
184        if !pin.queued.is_empty() {
185            // only spawn a building task if there's none in progress already
186            if let Some(backend) = pin.idle_backend.take() {
187                let work = pin.queued.pop_front().expect("not empty; qed");
188                let generation = work.generation;
189                let pause_epoch = work.pause_epoch;
190                let selected = work.transactions.len();
191                let pool = pin.pool.clone();
192                let miner = pin.miner.clone();
193
194                // we spawn this on as blocking task because this can be blocking for a while in
195                // forking mode, because of all the rpc calls to fetch the required state
196                let handle = tokio::runtime::Handle::current();
197                let mining = tokio::task::spawn_blocking(move || {
198                    handle.block_on(async move {
199                        let mining_guard = backend.lock_mining_owned().await;
200                        // Mining was paused after this candidate was selected, so skip it even if
201                        // it has resumed since. Ready transactions are mined by auto mining on
202                        // resume, or by the next tick.
203                        if miner.is_stale(pause_epoch) {
204                            return (Ok(None), backend, generation);
205                        }
206                        let transactions =
207                            pool.ready_transactions().take(selected).collect::<Vec<_>>();
208                        if selected > 0 && transactions.is_empty() {
209                            return (Ok(None), backend, generation);
210                        }
211                        trace!(target: "miner", "creating new block");
212                        let result =
213                            backend.mine_block_locked(transactions, false).await.map(|outcome| {
214                                let block_number = outcome.block_number;
215                                if pool.on_mined_block(outcome) {
216                                    miner.retry_ready_transactions();
217                                }
218                                trace!(target: "miner", "created new block: {block_number}");
219                                Some(block_number)
220                            });
221                        drop(mining_guard);
222                        (result, backend, generation)
223                    })
224                });
225                pin.block_mining = Some(mining);
226            }
227        }
228
229        if let Some(mut mining) = pin.block_mining.take() {
230            if let Poll::Ready(res) = mining.poll_unpin(cx) {
231                return match res {
232                    Ok((Ok(Some(block_number)), backend, _)) => {
233                        pin.idle_backend = Some(backend);
234                        Poll::Ready(Some(BlockProduction::Mined(block_number)))
235                    }
236                    Ok((Ok(None), backend, _)) => {
237                        pin.idle_backend = Some(backend);
238                        Poll::Ready(Some(BlockProduction::Skipped))
239                    }
240                    Ok((Err(error), backend, generation)) => {
241                        pin.idle_backend = Some(backend);
242                        pin.queued.clear();
243                        warn!(target: "miner", %error, "failed to finalize block");
244                        Poll::Ready(Some(BlockProduction::Failed(generation)))
245                    }
246                    Err(err) => {
247                        panic!("miner task failed: {err}");
248                    }
249                };
250            }
251            pin.block_mining = Some(mining)
252        }
253
254        Poll::Pending
255    }
256}