Skip to main content

reth_node_ethereum/
engine_ssz_witness.rs

1//! Execution witness generation and wire types for the SSZ Engine API extension.
2
3use alloy_primitives::{Bytes, B256};
4use alloy_rpc_types_engine::{
5    ssz_engine_types::{Optional, PayloadStatus, PayloadStatusKind},
6    ExecutionData,
7};
8use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
9use reth_evm::{execute::Executor, ConfigureEvm};
10use reth_primitives_traits::{AlloyBlockHeader, Block};
11use reth_provider::{HeaderProvider, StateProvider, StateProviderBox, StateProviderFactory};
12use reth_revm::{database::StateProviderDatabase, witness::ExecutionWitnessRecord};
13use reth_tasks::Runtime;
14use reth_trie_common::ExecutionWitnessMode;
15use std::{future::Future, pin::Pin};
16
17/// Re-executes validated payloads against their parent state for `/payloads/witness`.
18///
19/// The parent state must be available through the provider (persisted, canonical in-memory,
20/// or pending). Parents present only in the engine tree are reported as
21/// [`EngineSszWitnessError::ParentStateUnavailable`]; the route then answers with the payload
22/// status alone.
23#[derive(Clone, Debug)]
24pub struct EngineSszWitnessGenerator<Provider, Evm> {
25    provider: Provider,
26    evm_config: Evm,
27    task_spawner: Runtime,
28}
29
30impl<Provider, Evm> EngineSszWitnessGenerator<Provider, Evm> {
31    /// Creates a new witness generator.
32    pub const fn new(provider: Provider, evm_config: Evm, task_spawner: Runtime) -> Self {
33        Self { provider, evm_config, task_spawner }
34    }
35}
36
37impl<Provider, Evm> EngineSszWitness for EngineSszWitnessGenerator<Provider, Evm>
38where
39    Provider: HeaderProvider + StateProviderFactory + Clone + Send + Sync + 'static,
40    Provider::Header: alloy_rlp::Encodable,
41    Evm: ConfigureEvm<Primitives = EthPrimitives> + 'static,
42{
43    fn generate_witness(
44        &self,
45        payload: ExecutionData,
46    ) -> Pin<
47        Box<
48            dyn Future<Output = Result<ExecutionWitnessV1, EngineSszWitnessError>> + Send + 'static,
49        >,
50    > {
51        let provider = self.provider.clone();
52        let evm_config = self.evm_config.clone();
53        let task_spawner = self.task_spawner.clone();
54
55        Box::pin(async move {
56            task_spawner
57                .spawn_blocking(move || {
58                    // A VALID newPayload need not be canonical or visible through the provider.
59                    let block = payload
60                        .payload
61                        .try_into_block_with_sidecar::<TransactionSigned>(&payload.sidecar)
62                        .map_err(eyre::Report::new)?
63                        .try_into_recovered()
64                        .map_err(eyre::Report::new)?;
65
66                    let block_number = block.header().number;
67                    let parent_hash = block.header().parent_hash;
68                    let state_provider =
69                        provider.state_by_block_hash(parent_hash).map_err(|source| {
70                            EngineSszWitnessError::ParentStateUnavailable {
71                                parent: parent_hash,
72                                source: eyre::Report::new(source),
73                            }
74                        })?;
75                    let block_executor = evm_config.executor(StateProviderDatabase::new(
76                        state_provider.into_evm_state_provider(),
77                    ));
78                    let mut witness = None;
79                    let mut first_header = block_number.saturating_sub(1);
80                    block_executor
81                        .execute_with_state_closure(&block, |statedb: &reth_revm::State<_>| {
82                            if let Some((number, _)) = statedb.block_hashes.lowest() {
83                                first_header = number;
84                            }
85                            witness = Some(
86                                ExecutionWitnessRecord::new(statedb)
87                                    .into_execution_witness_without_headers::<StateProviderBox>(
88                                        &statedb.database,
89                                        ExecutionWitnessMode::Canonical,
90                                    ),
91                            );
92                        })
93                        .map_err(eyre::Report::new)?;
94
95                    let witness = witness
96                        .expect("state closure is called after successful execution")
97                        .map_err(eyre::Report::new)?;
98
99                    // Header numbers may refer to a different canonical ancestor.
100                    let mut headers = Vec::new();
101                    let mut hash = parent_hash;
102                    for _ in first_header..block_number {
103                        let header = provider
104                            .header(hash)
105                            .map_err(eyre::Report::new)?
106                            .ok_or_else(|| eyre::eyre!("ancestor {hash} not found for witness"))?;
107                        hash = header.parent_hash();
108                        headers.push(alloy_rlp::encode(&header).into());
109                    }
110                    headers.reverse();
111
112                    Ok(ExecutionWitnessV1 { state: witness.state, codes: witness.codes, headers })
113                })
114                .await
115                .map_err(eyre::Report::new)?
116        })
117    }
118}
119
120/// Generates an execution witness for a valid payload.
121pub trait EngineSszWitness: Send + Sync + 'static {
122    /// Generates a REST-SSZ execution witness after the submitted payload has been validated.
123    fn generate_witness(
124        &self,
125        payload: ExecutionData,
126    ) -> Pin<
127        Box<
128            dyn Future<Output = Result<ExecutionWitnessV1, EngineSszWitnessError>> + Send + 'static,
129        >,
130    >;
131}
132
133/// Failure to produce a witness for a validated payload.
134#[derive(Debug)]
135pub enum EngineSszWitnessError {
136    /// The parent state is not available through the provider yet.
137    ParentStateUnavailable {
138        /// Parent block whose state is required.
139        parent: B256,
140        /// Provider failure while accessing the state.
141        source: eyre::Report,
142    },
143    /// Witness execution or proof generation failed.
144    Internal(eyre::Report),
145}
146
147impl From<eyre::Report> for EngineSszWitnessError {
148    fn from(error: eyre::Report) -> Self {
149        Self::Internal(error)
150    }
151}
152
153impl std::fmt::Display for EngineSszWitnessError {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            Self::ParentStateUnavailable { parent, source } => {
157                write!(f, "parent state {parent} is unavailable through the provider: {source}")
158            }
159            Self::Internal(error) => std::fmt::Display::fmt(error, f),
160        }
161    }
162}
163
164impl std::error::Error for EngineSszWitnessError {
165    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
166        match self {
167            Self::ParentStateUnavailable { source, .. } | Self::Internal(source) => {
168                Some(source.as_ref())
169            }
170        }
171    }
172}
173
174/// A trie-node byte list in an [`ExecutionWitnessV1`].
175pub type WitnessNodeV1 = Bytes;
176
177/// A contract-code byte list in an [`ExecutionWitnessV1`].
178pub type WitnessCodeV1 = Bytes;
179
180/// An RLP-encoded header byte list in an [`ExecutionWitnessV1`].
181pub type WitnessHeaderV1 = Bytes;
182
183/// Canonical execution witness for `POST /payloads/witness`.
184///
185/// `state` and `codes` are produced in lexicographic ascending byte order. `headers` are
186/// RLP-encoded and ordered by ascending block number; consecutive headers must be parent-linked.
187/// These ordering rules are producer-side requirements from the execution-specs witness builder.
188///
189/// This is a REST-SSZ wire container, not the JSON-RPC debug witness shape.
190#[derive(Clone, Debug, Default, PartialEq, Eq, ssz_derive::Encode, ssz_derive::Decode)]
191pub struct ExecutionWitnessV1 {
192    /// Hashed trie-node preimages required during execution and state-root recomputation.
193    pub state: Vec<WitnessNodeV1>,
194    /// Contract bytecode preimages required from the pre-state.
195    pub codes: Vec<WitnessCodeV1>,
196    /// RLP-encoded ancestor headers used for pre-state and `BLOCKHASH` correctness proofs.
197    pub headers: Vec<WitnessHeaderV1>,
198}
199
200/// Canonical execution witness for `POST /payloads/witness`.
201pub type ExecutionWitness = ExecutionWitnessV1;
202
203/// REST-SSZ response for `POST /payloads/witness`.
204///
205/// The witness uses the Engine REST-SSZ `Optional[T]` encoding from execution-apis and is present
206/// only when the payload status is `VALID`. A `VALID` status without a witness means the parent
207/// state was not yet available through the provider (the parent is only known to the engine
208/// tree); resubmitting the payload once forkchoice has made the parent canonical yields it.
209#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode)]
210pub struct PayloadStatusWithWitness {
211    /// Result of processing the submitted payload.
212    pub payload_status: PayloadStatus,
213    /// Execution witness produced for a valid payload.
214    pub witness: Optional<ExecutionWitnessV1>,
215}
216
217impl PayloadStatusWithWitness {
218    /// Creates a response, converting the witness into the REST-SSZ `Optional[T]` representation.
219    pub fn new(payload_status: PayloadStatus, witness: Option<ExecutionWitnessV1>) -> Self {
220        let witness = match payload_status.status {
221            PayloadStatusKind::Valid => witness.into(),
222            _ => Optional::none(),
223        };
224        Self { payload_status, witness }
225    }
226}
227
228/// Backwards-compatible alias for the experimental witness response name.
229pub type NewPayloadWithWitnessResponseV1 = PayloadStatusWithWitness;
230
231impl ssz::Decode for PayloadStatusWithWitness {
232    fn is_ssz_fixed_len() -> bool {
233        false
234    }
235
236    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
237        let mut builder = ssz::SszDecoderBuilder::new(bytes);
238        builder.register_type::<PayloadStatus>()?;
239        builder.register_type::<Optional<ExecutionWitnessV1>>()?;
240        let mut decoder = builder.build()?;
241        let response =
242            Self { payload_status: decoder.decode_next()?, witness: decoder.decode_next()? };
243        if response.witness.is_some() && response.payload_status.status != PayloadStatusKind::Valid
244        {
245            return Err(ssz::DecodeError::BytesInvalid(
246                "execution witness is only valid for VALID payload status".into(),
247            ))
248        }
249        Ok(response)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use ssz::{Decode, Encode};
257
258    fn assert_roundtrip<T>(value: &T)
259    where
260        T: Encode + Decode + PartialEq + core::fmt::Debug,
261    {
262        assert_eq!(T::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), *value);
263    }
264
265    #[test]
266    fn witness_response_roundtrips_when_status_is_valid() {
267        let payload_status = PayloadStatus {
268            status: PayloadStatusKind::Valid,
269            latest_valid_hash: Optional::none(),
270            validation_error: Optional::none(),
271        };
272        let witness = ExecutionWitnessV1 {
273            state: vec![vec![1, 2, 3].into()],
274            codes: vec![vec![4, 5].into()],
275            headers: vec![vec![6].into()],
276        };
277        let response = PayloadStatusWithWitness::new(payload_status, Some(witness));
278
279        assert_roundtrip(&response);
280    }
281
282    #[test]
283    fn witness_response_omits_witness_for_non_valid_status() {
284        let payload_status = PayloadStatus {
285            status: PayloadStatusKind::Syncing,
286            latest_valid_hash: Optional::none(),
287            validation_error: Optional::none(),
288        };
289        let response =
290            PayloadStatusWithWitness::new(payload_status, Some(ExecutionWitnessV1::default()));
291
292        assert!(response.witness.is_none());
293        assert_roundtrip(&response);
294    }
295}