1use alloy_primitives::{TxHash, B256};
4use alloy_rpc_types_engine::ForkchoiceState;
5use eyre::OptionExt;
6use futures_util::{stream::Fuse, Stream, StreamExt};
7use reth_engine_primitives::ConsensusEngineHandle;
8use reth_payload_builder::PayloadBuilderHandle;
9use reth_payload_primitives::{BuiltPayload, PayloadAttributesBuilder, PayloadKind, PayloadTypes};
10use reth_primitives_traits::{HeaderTy, SealedHeaderFor};
11use reth_storage_api::BlockReader;
12use reth_transaction_pool::TransactionPool;
13use std::{
14 collections::VecDeque,
15 fmt,
16 future::Future,
17 num::NonZeroUsize,
18 pin::Pin,
19 task::{Context, Poll},
20 time::Duration,
21};
22use tokio::time::Interval;
23use tokio_stream::wrappers::ReceiverStream;
24use tracing::error;
25
26pub const DEFAULT_FINALITY_DEPTH: NonZeroUsize = NonZeroUsize::new(64).unwrap();
28
29pub enum MiningMode<Pool: TransactionPool + Unpin> {
31 Instant {
36 pool: Pool,
38 rx: Fuse<ReceiverStream<TxHash>>,
40 max_transactions: Option<usize>,
43 accumulated: usize,
45 },
46 Interval(Interval),
48 Trigger(Pin<Box<dyn Stream<Item = ()> + Send + Sync>>),
53}
54
55impl<Pool: TransactionPool + Unpin> fmt::Debug for MiningMode<Pool> {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::Instant { max_transactions, accumulated, .. } => f
59 .debug_struct("Instant")
60 .field("max_transactions", max_transactions)
61 .field("accumulated", accumulated)
62 .finish(),
63 Self::Interval(interval) => f.debug_tuple("Interval").field(interval).finish(),
64 Self::Trigger(_) => f.debug_tuple("Trigger").finish(),
65 }
66 }
67}
68
69impl<Pool: TransactionPool + Unpin> MiningMode<Pool> {
70 pub fn instant(pool: Pool, max_transactions: Option<usize>) -> Self {
72 let rx = pool.pending_transactions_listener();
73 Self::Instant { pool, rx: ReceiverStream::new(rx).fuse(), max_transactions, accumulated: 0 }
74 }
75
76 pub fn interval(duration: Duration) -> Self {
78 let start = tokio::time::Instant::now() + duration;
79 Self::Interval(tokio::time::interval_at(start, duration))
80 }
81
82 pub fn trigger(trigger: impl Stream<Item = ()> + Send + Sync + 'static) -> Self {
87 Self::Trigger(Box::pin(trigger))
88 }
89}
90
91impl<Pool: TransactionPool + Unpin> Future for MiningMode<Pool> {
92 type Output = ();
93
94 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
95 let this = self.get_mut();
96 match this {
97 Self::Instant { pool, rx, max_transactions, accumulated } => {
98 while let Poll::Ready(Some(_)) = rx.poll_next_unpin(cx) {
100 if pool.pending_and_queued_txn_count().0 == 0 {
101 continue;
102 }
103 if let Some(max_tx) = max_transactions {
104 *accumulated += 1;
105 if *accumulated >= *max_tx {
107 *accumulated = 0; return Poll::Ready(());
109 }
110 } else {
111 return Poll::Ready(());
113 }
114 }
115 }
116 Self::Interval(interval) => {
117 if interval.poll_tick(cx).is_ready() {
118 return Poll::Ready(())
119 }
120 }
121 Self::Trigger(trigger) => {
122 if trigger.poll_next_unpin(cx).is_ready() {
123 return Poll::Ready(())
124 }
125 }
126 }
127 Poll::Pending
128 }
129}
130
131#[derive(Debug)]
133pub struct LocalMiner<T: PayloadTypes, B, Pool: TransactionPool + Unpin> {
134 payload_attributes_builder: B,
136 to_engine: ConsensusEngineHandle<T>,
138 mode: MiningMode<Pool>,
140 payload_builder: PayloadBuilderHandle<T>,
142 last_header: SealedHeaderFor<<T::BuiltPayload as BuiltPayload>::Primitives>,
144 last_block_hashes: VecDeque<B256>,
146 finality_depth: NonZeroUsize,
148 payload_wait_time: Option<Duration>,
153}
154
155impl<T, B, Pool> LocalMiner<T, B, Pool>
156where
157 T: PayloadTypes,
158 B: PayloadAttributesBuilder<
159 T::PayloadAttributes,
160 HeaderTy<<T::BuiltPayload as BuiltPayload>::Primitives>,
161 >,
162 Pool: TransactionPool + Unpin,
163{
164 pub fn new(
166 provider: impl BlockReader<Header = HeaderTy<<T::BuiltPayload as BuiltPayload>::Primitives>>,
167 payload_attributes_builder: B,
168 to_engine: ConsensusEngineHandle<T>,
169 mode: MiningMode<Pool>,
170 payload_builder: PayloadBuilderHandle<T>,
171 ) -> Self {
172 let last_header =
173 provider.sealed_header(provider.best_block_number().unwrap()).unwrap().unwrap();
174
175 Self {
176 payload_attributes_builder,
177 to_engine,
178 mode,
179 payload_builder,
180 last_block_hashes: VecDeque::from([last_header.hash()]),
181 finality_depth: DEFAULT_FINALITY_DEPTH,
182 last_header,
183 payload_wait_time: None,
184 }
185 }
186
187 pub const fn with_finality_depth(mut self, finality_depth: NonZeroUsize) -> Self {
189 self.finality_depth = finality_depth;
190 self
191 }
192
193 pub const fn with_payload_wait_time_opt(mut self, wait_time: Option<Duration>) -> Self {
195 self.payload_wait_time = wait_time;
196 self
197 }
198
199 pub async fn run(mut self) {
201 let mut fcu_interval = tokio::time::interval(Duration::from_secs(1));
202 loop {
203 tokio::select! {
204 _ = &mut self.mode => {
206 if let Err(e) = self.advance().await {
207 error!(target: "engine::local", "Error advancing the chain: {:?}", e);
208 }
209 }
210 _ = fcu_interval.tick() => {
212 if let Err(e) = self.update_forkchoice_state().await {
213 error!(target: "engine::local", "Error updating fork choice: {:?}", e);
214 }
215 }
216 }
217 }
218 }
219
220 fn forkchoice_state(&self) -> ForkchoiceState {
222 let finality_depth = self.finality_depth.get();
223 ForkchoiceState {
224 head_block_hash: block_hash_at_depth(&self.last_block_hashes, 1),
225 safe_block_hash: block_hash_at_depth(
226 &self.last_block_hashes,
227 finality_depth.div_ceil(2),
228 ),
229 finalized_block_hash: block_hash_at_depth(&self.last_block_hashes, finality_depth),
230 }
231 }
232
233 async fn update_forkchoice_state(&self) -> eyre::Result<()> {
235 let state = self.forkchoice_state();
236 let res = self.to_engine.fork_choice_updated(state, None).await?;
237
238 if !res.is_valid() {
239 eyre::bail!("Invalid fork choice update {state:?}: {res:?}");
240 }
241
242 Ok(())
243 }
244
245 async fn advance(&mut self) -> eyre::Result<()> {
248 let res = self
249 .to_engine
250 .fork_choice_updated(
251 self.forkchoice_state(),
252 Some(self.payload_attributes_builder.build(&self.last_header)),
253 )
254 .await?;
255
256 if !res.is_valid() {
257 eyre::bail!("Invalid payload status");
258 }
259
260 let payload_id = res.payload_id.ok_or_eyre("No payload id")?;
261
262 if let Some(wait_time) = self.payload_wait_time {
263 tokio::time::sleep(wait_time).await;
264 }
265
266 let Some(Ok(payload)) =
267 self.payload_builder.resolve_kind(payload_id, PayloadKind::WaitForPending).await
268 else {
269 eyre::bail!("No payload");
270 };
271
272 let header = payload.block().sealed_header().clone();
273 let res = self.to_engine.new_payload(payload.into()).await?;
274
275 if !res.is_valid() {
276 eyre::bail!("Invalid payload");
277 }
278
279 self.last_block_hashes.push_back(header.hash());
280 self.last_header = header;
281 if self.last_block_hashes.len() > self.finality_depth.get() {
283 self.last_block_hashes.pop_front();
284 }
285
286 self.update_forkchoice_state().await?;
287
288 Ok(())
289 }
290}
291
292fn block_hash_at_depth(block_hashes: &VecDeque<B256>, depth: usize) -> B256 {
293 *block_hashes.get(block_hashes.len().saturating_sub(depth)).expect("at least 1 block exists")
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn block_hash_at_configured_depth() {
302 let block_hashes = (0..64).map(B256::with_last_byte).collect();
303
304 assert_eq!(block_hash_at_depth(&block_hashes, 1), B256::with_last_byte(63));
305 assert_eq!(block_hash_at_depth(&block_hashes, 32), B256::with_last_byte(32));
306 assert_eq!(block_hash_at_depth(&block_hashes, 64), B256::with_last_byte(0));
307 assert_eq!(block_hash_at_depth(&block_hashes, 128), B256::with_last_byte(0));
308 }
309}