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, storage::MinedBlockOutcome},
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            pool,
64            block_producer: BlockProducer::new(backend),
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(outcome) => {
90                        trace!(target: "node", "mined block {}", outcome.block_number);
91                        pin.pool.on_mined_block(outcome);
92                    }
93                    BlockProduction::Failed(generation) => {
94                        pin.miner.handle_failed_candidate(generation);
95                        break;
96                    }
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> =
128    (Result<MinedBlockOutcome<<N as Network>::TxEnvelope>, BlockchainError>, Arc<Backend<N>>, u64);
129
130enum BlockProduction<T> {
131    Mined(MinedBlockOutcome<T>),
132    Failed(u64),
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    /// Single active future that mines a new block
141    block_mining: Option<JoinHandle<MiningResult<N>>>,
142    /// backlog of sets of transactions ready to be mined
143    queued: VecDeque<crate::eth::miner::MiningWork<N::TxEnvelope>>,
144}
145
146impl<N: Network> BlockProducer<N>
147where
148    Backend<N>: TransactionValidator<N::TxEnvelope>,
149    N: Network<TxEnvelope = FoundryTxEnvelope, ReceiptEnvelope = FoundryReceiptEnvelope>,
150{
151    fn new(backend: Arc<Backend<N>>) -> Self {
152        Self { idle_backend: Some(backend), block_mining: None, queued: Default::default() }
153    }
154
155    fn is_idle(&self) -> bool {
156        self.idle_backend.is_some() && self.block_mining.is_none() && self.queued.is_empty()
157    }
158}
159
160impl<N: Network> Stream for BlockProducer<N>
161where
162    Backend<N>: TransactionValidator<N::TxEnvelope> + Send + Sync + 'static,
163    N: Network<TxEnvelope = FoundryTxEnvelope, ReceiptEnvelope = FoundryReceiptEnvelope> + 'static,
164{
165    type Item = BlockProduction<N::TxEnvelope>;
166
167    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
168        let pin = self.get_mut();
169
170        if !pin.queued.is_empty() {
171            // only spawn a building task if there's none in progress already
172            if let Some(backend) = pin.idle_backend.take() {
173                let work = pin.queued.pop_front().expect("not empty; qed");
174                let generation = work.generation;
175
176                // we spawn this on as blocking task because this can be blocking for a while in
177                // forking mode, because of all the rpc calls to fetch the required state
178                let handle = tokio::runtime::Handle::current();
179                let mining = tokio::task::spawn_blocking(move || {
180                    handle.block_on(async move {
181                        trace!(target: "miner", "creating new block");
182                        let block = backend.mine_block(work.transactions).await;
183                        if let Ok(block) = &block {
184                            trace!(target: "miner", "created new block: {}", block.block_number);
185                        }
186                        (block, backend, generation)
187                    })
188                });
189                pin.block_mining = Some(mining);
190            }
191        }
192
193        if let Some(mut mining) = pin.block_mining.take() {
194            if let Poll::Ready(res) = mining.poll_unpin(cx) {
195                return match res {
196                    Ok((Ok(outcome), backend, _)) => {
197                        pin.idle_backend = Some(backend);
198                        Poll::Ready(Some(BlockProduction::Mined(outcome)))
199                    }
200                    Ok((Err(error), backend, generation)) => {
201                        pin.idle_backend = Some(backend);
202                        pin.queued.clear();
203                        warn!(target: "miner", %error, "failed to finalize block");
204                        Poll::Ready(Some(BlockProduction::Failed(generation)))
205                    }
206                    Err(err) => {
207                        panic!("miner task failed: {err}");
208                    }
209                };
210            }
211            pin.block_mining = Some(mining)
212        }
213
214        Poll::Pending
215    }
216}