Skip to main content

reth_engine_util/
reorg.rs

1//! Stream wrapper that simulates reorgs.
2
3use alloy_consensus::{BlockHeader, Transaction};
4use alloy_primitives::Bytes;
5use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatus};
6use futures::{stream::FuturesUnordered, Stream, StreamExt, TryFutureExt};
7use itertools::Either;
8use reth_chainspec::{ChainSpecProvider, EthChainSpec};
9use reth_engine_primitives::{
10    BeaconEngineMessage, BeaconOnNewPayloadError, ExecutionPayload as _, OnForkChoiceUpdated,
11};
12use reth_engine_tree::tree::EngineValidator;
13use reth_errors::{BlockExecutionError, BlockValidationError, RethError, RethResult};
14use reth_evm::{
15    database::StateProviderDatabase, BlockBuilder, BlockBuilderOutcome, BlockExecutor, ConfigureEvm,
16};
17use reth_payload_primitives::{BuiltPayload, PayloadTypes};
18use reth_primitives_traits::{
19    block::Block as _, BlockBody as _, BlockTy, HeaderTy, SealedBlock, SignedTransaction,
20};
21use reth_storage_api::{errors::ProviderError, BlockReader, StateProvider, StateProviderFactory};
22use std::{
23    collections::VecDeque,
24    future::Future,
25    pin::Pin,
26    task::{ready, Context, Poll},
27};
28use tokio::sync::oneshot;
29use tracing::*;
30
31#[derive(Debug)]
32enum EngineReorgState<T: PayloadTypes> {
33    Forward,
34    Reorg { queue: VecDeque<BeaconEngineMessage<T>> },
35}
36
37type EngineReorgResponse = Result<
38    Either<Result<PayloadStatus, BeaconOnNewPayloadError>, RethResult<OnForkChoiceUpdated>>,
39    oneshot::error::RecvError,
40>;
41
42type ReorgResponseFut = Pin<Box<dyn Future<Output = EngineReorgResponse> + Send + Sync>>;
43
44/// Engine API stream wrapper that simulates reorgs with specified frequency.
45#[derive(Debug)]
46#[pin_project::pin_project]
47pub struct EngineReorg<S, T: PayloadTypes, Provider, Evm, Validator> {
48    /// Underlying stream
49    #[pin]
50    stream: S,
51    /// Database provider.
52    provider: Provider,
53    /// Evm configuration.
54    evm_config: Evm,
55    /// Payload validator.
56    payload_validator: Validator,
57    /// The frequency of reorgs.
58    frequency: usize,
59    /// The depth of reorgs.
60    depth: usize,
61    /// The number of forwarded forkchoice states.
62    /// This is reset after a reorg.
63    forkchoice_states_forwarded: usize,
64    /// Current state of the stream.
65    state: EngineReorgState<T>,
66    /// Last forkchoice state.
67    last_forkchoice_state: Option<ForkchoiceState>,
68    /// Pending engine responses to reorg messages.
69    reorg_responses: FuturesUnordered<ReorgResponseFut>,
70}
71
72impl<S, T: PayloadTypes, Provider, Evm, Validator> EngineReorg<S, T, Provider, Evm, Validator> {
73    /// Creates new [`EngineReorg`] stream wrapper.
74    pub fn new(
75        stream: S,
76        provider: Provider,
77        evm_config: Evm,
78        payload_validator: Validator,
79        frequency: usize,
80        depth: usize,
81    ) -> Self {
82        Self {
83            stream,
84            provider,
85            evm_config,
86            payload_validator,
87            frequency,
88            depth,
89            state: EngineReorgState::Forward,
90            forkchoice_states_forwarded: 0,
91            last_forkchoice_state: None,
92            reorg_responses: FuturesUnordered::new(),
93        }
94    }
95}
96
97impl<S, T, Provider, Evm, Validator> Stream for EngineReorg<S, T, Provider, Evm, Validator>
98where
99    S: Stream<Item = BeaconEngineMessage<T>>,
100    T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = Evm::Primitives>>,
101    Provider: BlockReader<Header = HeaderTy<Evm::Primitives>, Block = BlockTy<Evm::Primitives>>
102        + StateProviderFactory
103        + ChainSpecProvider,
104    Evm: ConfigureEvm,
105    Validator: EngineValidator<T, Evm::Primitives>,
106{
107    type Item = S::Item;
108
109    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
110        let mut this = self.project();
111
112        loop {
113            if let Poll::Ready(Some(response)) = this.reorg_responses.poll_next_unpin(cx) {
114                match response {
115                    Ok(Either::Left(Ok(payload_status))) => {
116                        debug!(target: "engine::stream::reorg", ?payload_status, "Received response for reorg new payload");
117                    }
118                    Ok(Either::Left(Err(payload_error))) => {
119                        error!(target: "engine::stream::reorg", %payload_error, "Error on reorg new payload");
120                    }
121                    Ok(Either::Right(Ok(fcu_status))) => {
122                        debug!(target: "engine::stream::reorg", ?fcu_status, "Received response for reorg forkchoice update");
123                    }
124                    Ok(Either::Right(Err(fcu_error))) => {
125                        error!(target: "engine::stream::reorg", %fcu_error, "Error on reorg forkchoice update");
126                    }
127                    Err(_) => {}
128                };
129                continue
130            }
131
132            if let EngineReorgState::Reorg { queue } = &mut this.state {
133                match queue.pop_front() {
134                    Some(msg) => return Poll::Ready(Some(msg)),
135                    None => {
136                        *this.forkchoice_states_forwarded = 0;
137                        *this.state = EngineReorgState::Forward;
138                    }
139                }
140            }
141
142            let next = ready!(this.stream.poll_next_unpin(cx));
143            let item = match (next, &this.last_forkchoice_state) {
144                (
145                    Some(BeaconEngineMessage::NewPayload { cause, payload, tx }),
146                    Some(last_forkchoice_state),
147                ) if this.forkchoice_states_forwarded > this.frequency &&
148                        // Only enter reorg state if new payload attaches to current head.
149                        last_forkchoice_state.head_block_hash == payload.parent_hash() =>
150                {
151                    // Enter the reorg state.
152                    // The current payload will be immediately forwarded by being in front of the
153                    // queue. Then we attempt to reorg the current head by generating a payload that
154                    // attaches to the head's parent and is based on the non-conflicting
155                    // transactions (txs from block `n + 1` that are valid at block `n` according to
156                    // consensus checks) from the current payload as well as the corresponding
157                    // forkchoice state. We will rely on CL to reorg us back to canonical chain.
158                    // TODO: This is an expensive blocking operation, ideally it's spawned as a task
159                    // so that the stream could yield the control back.
160                    let (reorg_block, encoded_bal) = match create_reorg_head(
161                        this.provider,
162                        this.evm_config,
163                        this.payload_validator,
164                        *this.depth,
165                        payload.clone(),
166                    ) {
167                        Ok(result) => result,
168                        Err(error) => {
169                            error!(target: "engine::stream::reorg", %error, "Error attempting to create reorg head");
170                            // Forward the payload and attempt to create reorg on top of
171                            // the next one
172                            return Poll::Ready(Some(BeaconEngineMessage::NewPayload {
173                                cause,
174                                payload,
175                                tx,
176                            }))
177                        }
178                    };
179                    let reorg_forkchoice_state = ForkchoiceState {
180                        finalized_block_hash: last_forkchoice_state.finalized_block_hash,
181                        safe_block_hash: last_forkchoice_state.safe_block_hash,
182                        head_block_hash: reorg_block.hash(),
183                    };
184
185                    let (reorg_payload_tx, reorg_payload_rx) = oneshot::channel();
186                    let (reorg_fcu_tx, reorg_fcu_rx) = oneshot::channel();
187                    this.reorg_responses.extend([
188                        Box::pin(reorg_payload_rx.map_ok(Either::Left)) as ReorgResponseFut,
189                        Box::pin(reorg_fcu_rx.map_ok(Either::Right)) as ReorgResponseFut,
190                    ]);
191
192                    let queue = VecDeque::from([
193                        // Current payload
194                        BeaconEngineMessage::NewPayload { cause: cause.clone(), payload, tx },
195                        // Reorg payload
196                        BeaconEngineMessage::NewPayload {
197                            cause: cause.clone(),
198                            payload: T::block_to_payload(reorg_block, encoded_bal),
199                            tx: reorg_payload_tx,
200                        },
201                        // Reorg forkchoice state
202                        BeaconEngineMessage::ForkchoiceUpdated {
203                            cause,
204                            state: reorg_forkchoice_state,
205                            payload_attrs: None,
206                            tx: reorg_fcu_tx,
207                        },
208                    ]);
209                    *this.state = EngineReorgState::Reorg { queue };
210                    continue
211                }
212                (
213                    Some(BeaconEngineMessage::ForkchoiceUpdated {
214                        cause,
215                        state,
216                        payload_attrs,
217                        tx,
218                    }),
219                    _,
220                ) => {
221                    // Record last forkchoice state forwarded to the engine.
222                    // We do not care if it's valid since engine should be able to handle
223                    // reorgs that rely on invalid forkchoice state.
224                    *this.last_forkchoice_state = Some(state);
225                    *this.forkchoice_states_forwarded += 1;
226                    Some(BeaconEngineMessage::ForkchoiceUpdated { cause, state, payload_attrs, tx })
227                }
228                (item, _) => item,
229            };
230            return Poll::Ready(item)
231        }
232    }
233}
234
235#[allow(clippy::type_complexity)]
236fn create_reorg_head<Provider, Evm, T, Validator>(
237    provider: &Provider,
238    evm_config: &Evm,
239    payload_validator: &Validator,
240    mut depth: usize,
241    next_payload: T::ExecutionData,
242) -> RethResult<(SealedBlock<BlockTy<Evm::Primitives>>, Option<Bytes>)>
243where
244    Provider: BlockReader<Header = HeaderTy<Evm::Primitives>, Block = BlockTy<Evm::Primitives>>
245        + StateProviderFactory
246        + ChainSpecProvider<ChainSpec: EthChainSpec>,
247    Evm: ConfigureEvm,
248    T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = Evm::Primitives>>,
249    Validator: EngineValidator<T, Evm::Primitives>,
250{
251    // Ensure next payload is valid.
252    let next_block =
253        payload_validator.convert_payload_to_block(next_payload).map_err(RethError::msg)?;
254
255    // Fetch reorg target block depending on its depth and its parent.
256    let mut previous_hash = next_block.parent_hash();
257    let mut candidate_transactions = next_block.into_body().transactions().to_vec();
258    let reorg_target = 'target: {
259        loop {
260            let reorg_target = provider
261                .block_by_hash(previous_hash)?
262                .ok_or_else(|| ProviderError::HeaderNotFound(previous_hash.into()))?;
263            if depth == 0 {
264                break 'target reorg_target.seal_slow()
265            }
266
267            depth -= 1;
268            previous_hash = reorg_target.header().parent_hash();
269            candidate_transactions = reorg_target.into_body().into_transactions();
270        }
271    };
272    let reorg_target_parent = provider
273        .sealed_header_by_hash(reorg_target.header().parent_hash())?
274        .ok_or_else(|| ProviderError::HeaderNotFound(reorg_target.header().parent_hash().into()))?;
275
276    debug!(target: "engine::stream::reorg", number = reorg_target.header().number(), hash = %previous_hash, "Selected reorg target");
277
278    let has_bal = reorg_target.header().block_access_list_hash().is_some();
279    let state_provider = provider.state_by_block_hash(reorg_target.header().parent_hash())?;
280    let evm_env = evm_config.evm_env(reorg_target.header()).map_err(RethError::other)?;
281    let evm = evm_config.evm_with_env(
282        StateProviderDatabase::new(state_provider.as_ref().into_evm_state_provider()),
283        evm_env.clone(),
284    );
285    let ctx = evm_config.context_for_block(&reorg_target).map_err(RethError::other)?;
286    let mut builder = evm_config.create_block_builder(evm, evm_env, &reorg_target_parent, ctx);
287    if has_bal {
288        builder.executor_mut().enable_block_access_list_builder();
289    }
290
291    builder.apply_pre_execution_changes()?;
292
293    for tx in candidate_transactions {
294        // ensure we still have capacity for this transaction
295        if builder.executor_mut().validate_transaction_gas_limit(tx.gas_limit()).is_err() {
296            continue
297        }
298
299        let tx_recovered =
300            tx.try_into_recovered().map_err(|_| ProviderError::SenderRecoveryError)?;
301        match builder.execute_transaction(tx_recovered) {
302            Ok(_) => {}
303            Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx {
304                hash,
305                error,
306            })) => {
307                trace!(target: "engine::stream::reorg", hash = %hash, ?error, "Error executing transaction from next block");
308            }
309            // Treat error as fatal
310            Err(error) => return Err(RethError::Execution(error)),
311        }
312    }
313
314    let BlockBuilderOutcome { block, block_access_list, .. } =
315        builder.finish(state_provider.as_ref(), None)?;
316
317    Ok((block.into_sealed_block(), block_access_list.map(|bal| bal.split().1)))
318}