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 evm2::evm::{CacheDB, Db};
9use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
10use reth_evm::{
11    database::StateProviderDatabase, execute::Executor, witness::ExecutionWitnessRecord,
12    ConfigureEvm,
13};
14use reth_primitives_traits::{AlloyBlockHeader, Block};
15use reth_provider::{HeaderProvider, StateProvider, StateProviderFactory};
16use reth_tasks::Runtime;
17use reth_trie_common::ExecutionWitnessMode;
18use std::{future::Future, pin::Pin};
19
20/// Re-executes validated payloads against their parent state for `/payloads/witness`.
21///
22/// The parent state must be available through the provider (persisted, canonical in-memory,
23/// or pending). Parents present only in the engine tree are reported as
24/// [`EngineSszWitnessError::ParentStateUnavailable`]; the route then answers with an internal
25/// error.
26#[derive(Clone, Debug)]
27pub struct EngineSszWitnessGenerator<Provider, Evm> {
28    provider: Provider,
29    evm_config: Evm,
30    task_spawner: Runtime,
31}
32
33impl<Provider, Evm> EngineSszWitnessGenerator<Provider, Evm> {
34    /// Creates a new witness generator.
35    pub const fn new(provider: Provider, evm_config: Evm, task_spawner: Runtime) -> Self {
36        Self { provider, evm_config, task_spawner }
37    }
38}
39
40impl<Provider, Evm> EngineSszWitness for EngineSszWitnessGenerator<Provider, Evm>
41where
42    Provider: HeaderProvider + StateProviderFactory + Clone + 'static,
43    Provider::Header: alloy_rlp::Encodable,
44    Evm: ConfigureEvm<Primitives = EthPrimitives> + 'static,
45{
46    fn generate_witness(
47        &self,
48        payload: ExecutionData,
49    ) -> Pin<
50        Box<
51            dyn Future<Output = Result<ExecutionWitnessV1, EngineSszWitnessError>> + Send + 'static,
52        >,
53    > {
54        let provider = self.provider.clone();
55        let evm_config = self.evm_config.clone();
56        let task_spawner = self.task_spawner.clone();
57
58        Box::pin(async move {
59            task_spawner
60                .spawn_blocking(move || {
61                    // A VALID newPayload need not be canonical or visible through the provider.
62                    let block = payload
63                        .payload
64                        .try_into_block_with_sidecar::<TransactionSigned>(&payload.sidecar)
65                        .map_err(eyre::Report::new)?
66                        .try_into_recovered()
67                        .map_err(eyre::Report::new)?;
68
69                    let block_number = block.header().number;
70                    let parent_hash = block.header().parent_hash;
71                    let state_provider =
72                        provider.state_by_block_hash(parent_hash).map_err(|source| {
73                            EngineSszWitnessError::ParentStateUnavailable {
74                                parent: parent_hash,
75                                source: eyre::Report::new(source),
76                            }
77                        })?;
78                    let mut db = CacheDB::new(Db::new(StateProviderDatabase::new(
79                        state_provider.as_ref().into_evm_state_provider(),
80                    )));
81                    let output =
82                        evm_config.executor(&mut db).execute(&block).map_err(eyre::Report::new)?;
83                    let codes = ExecutionWitnessRecord::commit_execution(&mut db, &output.state);
84                    let first_header = db
85                        .cache
86                        .block_hashes
87                        .keys()
88                        .map(|number| number.saturating_to::<u64>())
89                        .min()
90                        .unwrap_or_else(|| block_number.saturating_sub(1));
91                    let witness = ExecutionWitnessRecord::new(&db)
92                        .with_canonical_codes(codes)
93                        .into_execution_witness_without_headers(
94                            state_provider.as_ref(),
95                            ExecutionWitnessMode::Canonical,
96                        )
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/// for every `VALID` payload status and absent for all other statuses. A failure to generate
207/// the witness is returned as an HTTP error.
208#[derive(Clone, Debug, PartialEq, Eq, ssz_derive::Encode)]
209pub struct PayloadStatusWithWitness {
210    /// Result of processing the submitted payload.
211    pub payload_status: PayloadStatus,
212    /// Execution witness produced for a valid payload.
213    pub witness: Optional<ExecutionWitnessV1>,
214}
215
216impl PayloadStatusWithWitness {
217    /// Creates a response, converting the witness into the REST-SSZ `Optional[T]` representation.
218    pub fn new(payload_status: PayloadStatus, witness: Option<ExecutionWitnessV1>) -> Self {
219        let witness = match payload_status.status {
220            PayloadStatusKind::Valid => witness.into(),
221            _ => Optional::none(),
222        };
223        Self { payload_status, witness }
224    }
225}
226
227/// Backwards-compatible alias for the experimental witness response name.
228pub type NewPayloadWithWitnessResponseV1 = PayloadStatusWithWitness;
229
230impl ssz::Decode for PayloadStatusWithWitness {
231    fn is_ssz_fixed_len() -> bool {
232        false
233    }
234
235    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
236        let mut builder = ssz::SszDecoderBuilder::new(bytes);
237        builder.register_type::<PayloadStatus>()?;
238        builder.register_type::<Optional<ExecutionWitnessV1>>()?;
239        let mut decoder = builder.build()?;
240        let response =
241            Self { payload_status: decoder.decode_next()?, witness: decoder.decode_next()? };
242        if response.witness.is_some() && response.payload_status.status != PayloadStatusKind::Valid
243        {
244            return Err(ssz::DecodeError::BytesInvalid(
245                "execution witness is only valid for VALID payload status".into(),
246            ))
247        }
248        Ok(response)
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use ssz::{Decode, Encode};
256
257    fn assert_roundtrip<T>(value: &T)
258    where
259        T: Encode + Decode + PartialEq + core::fmt::Debug,
260    {
261        assert_eq!(T::from_ssz_bytes(&value.as_ssz_bytes()).unwrap(), *value);
262    }
263
264    #[test]
265    fn witness_response_roundtrips_when_status_is_valid() {
266        let payload_status = PayloadStatus {
267            status: PayloadStatusKind::Valid,
268            latest_valid_hash: Optional::none(),
269            validation_error: Optional::none(),
270        };
271        let witness = ExecutionWitnessV1 {
272            state: vec![vec![1, 2, 3].into()],
273            codes: vec![vec![4, 5].into()],
274            headers: vec![vec![6].into()],
275        };
276        let response = PayloadStatusWithWitness::new(payload_status, Some(witness));
277
278        assert_roundtrip(&response);
279    }
280
281    #[test]
282    fn witness_response_omits_witness_for_non_valid_status() {
283        let payload_status = PayloadStatus {
284            status: PayloadStatusKind::Syncing,
285            latest_valid_hash: Optional::none(),
286            validation_error: Optional::none(),
287        };
288        let response =
289            PayloadStatusWithWitness::new(payload_status, Some(ExecutionWitnessV1::default()));
290
291        assert!(response.witness.is_none());
292        assert_roundtrip(&response);
293    }
294}