Skip to main content

alloy_reth/
types.rs

1//! Types for the `reth_` RPC namespace.
2
3use alloy_eips::BlockId;
4use alloy_primitives::{Address, B256, Bytes, U64, U256, map::HashMap};
5use alloy_rpc_types_engine::PayloadStatus;
6use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct};
7
8/// Response type for `reth_getBalanceChangesInBlock`.
9///
10/// Maps addresses to their updated balances after block execution.
11pub type BalanceChangesInBlock = HashMap<Address, U256>;
12
13/// The input to `reth_newPayload`.
14///
15/// Reth accepts standard execution data, big-block execution data, and raw RLP-encoded blocks.
16/// Raw blocks are sent as a legacy bare byte string when no block access list is present, and as
17/// an object containing `block` and `bal` when one is present.
18///
19/// # Examples
20///
21/// Standard execution data can use any serializable execution-data type:
22///
23/// ```
24/// use alloy_reth::RethNewPayloadInput;
25///
26/// let input = RethNewPayloadInput::execution_data(serde_json::json!({
27///     "payload": "0x01",
28///     "sidecar": "0x02",
29/// }));
30/// assert!(serde_json::to_value(input).is_ok());
31/// ```
32///
33/// Big-block data carries the constituent execution environments and their block hashes:
34///
35/// ```
36/// use alloy_primitives::B256;
37/// use alloy_reth::{BigBlockData, RethNewPayloadInput};
38///
39/// let input = RethNewPayloadInput::big_block_data(BigBlockData {
40///     env_switches: vec![serde_json::json!({"payload": "0x01"})],
41///     prior_block_hashes: vec![(7, B256::ZERO)],
42///     block_number: 8,
43///     merged_block_access_list: None,
44/// });
45/// assert!(serde_json::to_value(input).is_ok());
46/// ```
47///
48/// Raw RLP data may be sent with an optional merged block access list:
49///
50/// ```
51/// use alloy_primitives::Bytes;
52/// use alloy_reth::RethNewPayloadInput;
53///
54/// let input = RethNewPayloadInput::<serde_json::Value>::block_rlp_with_bal(
55///     Bytes::from_static(&[0x01]),
56///     Bytes::from_static(&[0x02]),
57/// );
58/// assert_eq!(serde_json::to_value(input).unwrap()["block"], "0x01");
59/// ```
60#[allow(clippy::large_enum_variant)]
61#[derive(Debug, Clone)]
62pub enum RethNewPayloadInput<ExecutionData> {
63    /// Standard execution data (payload + sidecar).
64    ExecutionData(ExecutionData),
65    /// Big-block execution data.
66    BigBlockData(Box<BigBlockData<ExecutionData>>),
67    /// Raw RLP-encoded block bytes and an optional merged block access list.
68    BlockRlp {
69        /// RLP-encoded block bytes.
70        block: Bytes,
71        /// Optional merged block access list bytes.
72        bal: Option<Bytes>,
73    },
74}
75
76impl<E> Serialize for RethNewPayloadInput<E>
77where
78    E: Serialize,
79{
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: Serializer,
83    {
84        match self {
85            Self::ExecutionData(data) => data.serialize(serializer),
86            Self::BigBlockData(data) => data.serialize(serializer),
87            Self::BlockRlp { block, bal: None } => block.serialize(serializer),
88            Self::BlockRlp {
89                block,
90                bal: Some(bal),
91            } => {
92                let mut object = serializer.serialize_struct("RethNewPayloadBlockRlp", 2)?;
93                object.serialize_field("block", block)?;
94                object.serialize_field("bal", bal)?;
95                object.end()
96            }
97        }
98    }
99}
100
101impl<'de, E> Deserialize<'de> for RethNewPayloadInput<E>
102where
103    E: Deserialize<'de>,
104{
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: Deserializer<'de>,
108    {
109        #[derive(Deserialize)]
110        #[serde(untagged)]
111        enum Repr<E> {
112            BlockRlp {
113                block: Bytes,
114                #[serde(default)]
115                bal: Option<Bytes>,
116            },
117            LegacyBlockRlp(Bytes),
118            BigBlockData(Box<BigBlockData<E>>),
119            ExecutionData(E),
120        }
121
122        Ok(match Repr::deserialize(deserializer)? {
123            Repr::BlockRlp { block, bal } => Self::BlockRlp { block, bal },
124            Repr::LegacyBlockRlp(block) => Self::BlockRlp { block, bal: None },
125            Repr::BigBlockData(data) => Self::BigBlockData(data),
126            Repr::ExecutionData(data) => Self::ExecutionData(data),
127        })
128    }
129}
130
131/// Big-block execution data accepted by `reth_newPayload`.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct BigBlockData<ExecutionData> {
134    /// Execution data for each constituent environment in the big block.
135    pub env_switches: Vec<ExecutionData>,
136    /// Block numbers and hashes preceding the big block.
137    pub prior_block_hashes: Vec<(u64, B256)>,
138    /// Number of the big block.
139    pub block_number: u64,
140    /// Optional merged block access list.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub merged_block_access_list: Option<Bytes>,
143}
144
145/// Compatibility alias for [`BigBlockData`].
146pub type RethBigBlockData<ExecutionData> = BigBlockData<ExecutionData>;
147
148impl<ExecutionData> BigBlockData<ExecutionData> {
149    /// Creates big-block data without a merged block access list.
150    pub const fn new(
151        env_switches: Vec<ExecutionData>,
152        prior_block_hashes: Vec<(u64, B256)>,
153        block_number: u64,
154    ) -> Self {
155        Self {
156            env_switches,
157            prior_block_hashes,
158            block_number,
159            merged_block_access_list: None,
160        }
161    }
162
163    /// Sets the merged block access list.
164    pub fn with_merged_block_access_list(mut self, bal: impl Into<Bytes>) -> Self {
165        self.merged_block_access_list = Some(bal.into());
166        self
167    }
168}
169
170impl<E> RethNewPayloadInput<E> {
171    /// Creates a new [`RethNewPayloadInput`] from execution data.
172    pub const fn execution_data(data: E) -> Self {
173        Self::ExecutionData(data)
174    }
175
176    /// Creates a new [`RethNewPayloadInput`] from big-block execution data.
177    pub fn big_block_data(data: BigBlockData<E>) -> Self {
178        Self::BigBlockData(Box::new(data))
179    }
180
181    /// Creates a new [`RethNewPayloadInput`] from raw RLP-encoded block bytes.
182    pub fn block_rlp(bytes: impl Into<Bytes>) -> Self {
183        Self::BlockRlp {
184            block: bytes.into(),
185            bal: None,
186        }
187    }
188
189    /// Creates a new [`RethNewPayloadInput`] from raw RLP bytes and a block access list.
190    pub fn block_rlp_with_bal(block: impl Into<Bytes>, bal: impl Into<Bytes>) -> Self {
191        Self::BlockRlp {
192            block: block.into(),
193            bal: Some(bal.into()),
194        }
195    }
196}
197
198/// Parameters for `reth_newPayload`.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200#[non_exhaustive]
201pub struct RethNewPayloadParams<E = serde_json::Value> {
202    /// The payload input.
203    pub payload: RethNewPayloadInput<E>,
204    /// Whether to wait for persistence before returning.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub wait_for_persistence: Option<bool>,
207    /// Whether to wait for caches before returning.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub wait_for_caches: Option<bool>,
210}
211
212impl<E> RethNewPayloadParams<E> {
213    /// Creates new [`RethNewPayloadParams`] with the given payload input.
214    pub const fn new(payload: RethNewPayloadInput<E>) -> Self {
215        Self {
216            payload,
217            wait_for_persistence: None,
218            wait_for_caches: None,
219        }
220    }
221
222    /// Sets whether to wait for persistence.
223    pub const fn with_wait_for_persistence(mut self, wait: bool) -> Self {
224        self.wait_for_persistence = Some(wait);
225        self
226    }
227
228    /// Sets whether to wait for caches.
229    pub const fn with_wait_for_caches(mut self, wait: bool) -> Self {
230        self.wait_for_caches = Some(wait);
231        self
232    }
233}
234
235impl<E> From<RethNewPayloadInput<E>> for RethNewPayloadParams<E> {
236    fn from(payload: RethNewPayloadInput<E>) -> Self {
237        Self::new(payload)
238    }
239}
240
241/// Extended payload status returned by `reth_newPayload`.
242///
243/// Wraps the standard [`PayloadStatus`] with server-side timing information.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[non_exhaustive]
246pub struct RethPayloadStatus {
247    /// The standard payload status.
248    #[serde(flatten)]
249    pub status: PayloadStatus,
250    /// Total execution latency in microseconds.
251    #[serde(default)]
252    pub latency_us: u64,
253    /// Time spent waiting for persistence in microseconds, if applicable.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub persistence_wait_us: Option<u64>,
256    /// Time spent waiting for the execution cache lock in microseconds, if applicable.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub execution_cache_wait_us: Option<u64>,
259    /// Time spent waiting for the sparse trie lock in microseconds, if applicable.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub sparse_trie_wait_us: Option<u64>,
262}
263
264impl RethPayloadStatus {
265    /// Creates a new [`RethPayloadStatus`] with the given status and latency.
266    pub const fn new(status: PayloadStatus, latency_us: u64) -> Self {
267        Self {
268            status,
269            latency_us,
270            persistence_wait_us: None,
271            execution_cache_wait_us: None,
272            sparse_trie_wait_us: None,
273        }
274    }
275
276    /// Sets the persistence wait time.
277    pub const fn with_persistence_wait_us(mut self, us: u64) -> Self {
278        self.persistence_wait_us = Some(us);
279        self
280    }
281
282    /// Sets the execution cache wait time.
283    pub const fn with_execution_cache_wait_us(mut self, us: u64) -> Self {
284        self.execution_cache_wait_us = Some(us);
285        self
286    }
287
288    /// Sets the sparse trie wait time.
289    pub const fn with_sparse_trie_wait_us(mut self, us: u64) -> Self {
290        self.sparse_trie_wait_us = Some(us);
291        self
292    }
293}
294
295impl AsRef<PayloadStatus> for RethPayloadStatus {
296    fn as_ref(&self) -> &PayloadStatus {
297        &self.status
298    }
299}
300
301/// Parameters for `reth_getBlockExecutionOutcome`.
302#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
303#[non_exhaustive]
304pub struct GetBlockExecutionOutcomeParams {
305    /// The block identifier.
306    pub block_id: BlockId,
307    /// Optional number of blocks to include.
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub count: Option<U64>,
310}
311
312impl GetBlockExecutionOutcomeParams {
313    /// Creates new [`GetBlockExecutionOutcomeParams`] for the given block.
314    pub const fn new(block_id: BlockId) -> Self {
315        Self {
316            block_id,
317            count: None,
318        }
319    }
320
321    /// Sets the number of blocks to include.
322    pub fn with_count(mut self, count: impl Into<U64>) -> Self {
323        self.count = Some(count.into());
324        self
325    }
326}
327
328impl From<BlockId> for GetBlockExecutionOutcomeParams {
329    fn from(block_id: BlockId) -> Self {
330        Self::new(block_id)
331    }
332}
333
334/// Notification emitted by `reth_subscribeChainNotifications`.
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336#[serde(tag = "type", rename_all = "lowercase")]
337#[non_exhaustive]
338pub enum CanonStateNotification {
339    /// New chain segment was committed.
340    Commit {
341        /// The newly committed chain segment, serialized as JSON value.
342        new: serde_json::Value,
343    },
344    /// Chain reorganization occurred.
345    Reorg {
346        /// The reverted chain segment, serialized as JSON value.
347        old: serde_json::Value,
348        /// The replacement chain segment, serialized as JSON value.
349        new: serde_json::Value,
350    },
351}
352
353impl CanonStateNotification {
354    /// Creates a [`Commit`](Self::Commit) notification.
355    pub const fn commit(new: serde_json::Value) -> Self {
356        Self::Commit { new }
357    }
358
359    /// Creates a [`Reorg`](Self::Reorg) notification.
360    pub const fn reorg(old: serde_json::Value, new: serde_json::Value) -> Self {
361        Self::Reorg { old, new }
362    }
363
364    /// Returns `true` if this is a [`Commit`](Self::Commit) notification.
365    pub const fn is_commit(&self) -> bool {
366        matches!(self, Self::Commit { .. })
367    }
368
369    /// Returns `true` if this is a [`Reorg`](Self::Reorg) notification.
370    pub const fn is_reorg(&self) -> bool {
371        matches!(self, Self::Reorg { .. })
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use alloy_rpc_types_engine::PayloadStatusEnum;
379    use serde_json::json;
380
381    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
382    struct TestExecutionData {
383        payload: Bytes,
384        sidecar: Bytes,
385    }
386
387    fn execution_data() -> TestExecutionData {
388        TestExecutionData {
389            payload: Bytes::from_static(&[0x01]),
390            sidecar: Bytes::from_static(&[0x02]),
391        }
392    }
393
394    #[test]
395    fn execution_data_round_trips() {
396        let input = RethNewPayloadInput::execution_data(execution_data());
397        let value = serde_json::to_value(&input).unwrap();
398        assert_eq!(value, json!({"payload": "0x01", "sidecar": "0x02"}));
399
400        let decoded: RethNewPayloadInput<TestExecutionData> =
401            serde_json::from_value(value).unwrap();
402        assert!(
403            matches!(decoded, RethNewPayloadInput::ExecutionData(data) if data == execution_data())
404        );
405    }
406
407    #[test]
408    fn big_block_data_round_trips_with_and_without_bal() {
409        let hash = B256::from([0x11; 32]);
410        let data = BigBlockData {
411            env_switches: vec![execution_data()],
412            prior_block_hashes: vec![(7, hash)],
413            block_number: 8,
414            merged_block_access_list: None,
415        };
416        let input = RethNewPayloadInput::big_block_data(data.clone());
417        let value = serde_json::to_value(&input).unwrap();
418        assert_eq!(
419            value,
420            json!({
421                "env_switches": [{"payload": "0x01", "sidecar": "0x02"}],
422                "prior_block_hashes": [[7, hash]],
423                "block_number": 8,
424            })
425        );
426
427        let decoded: RethNewPayloadInput<TestExecutionData> =
428            serde_json::from_value(value).unwrap();
429        assert!(matches!(decoded, RethNewPayloadInput::BigBlockData(decoded) if *decoded == data));
430
431        let with_bal = data.with_merged_block_access_list(Bytes::from_static(&[0x03]));
432        let input = RethNewPayloadInput::big_block_data(with_bal.clone());
433        let value = serde_json::to_value(&input).unwrap();
434        assert_eq!(value["merged_block_access_list"], "0x03");
435        let decoded: RethNewPayloadInput<TestExecutionData> =
436            serde_json::from_value(value).unwrap();
437        assert!(
438            matches!(decoded, RethNewPayloadInput::BigBlockData(decoded) if *decoded == with_bal)
439        );
440    }
441
442    #[test]
443    fn raw_rlp_serializes_as_legacy_bytes_without_bal() {
444        let input =
445            RethNewPayloadInput::<TestExecutionData>::block_rlp(Bytes::from_static(&[0x01, 0x02]));
446        assert_eq!(serde_json::to_value(&input).unwrap(), json!("0x0102"));
447
448        let decoded: RethNewPayloadInput<TestExecutionData> =
449            serde_json::from_value(json!("0x0102")).unwrap();
450        assert!(
451            matches!(decoded, RethNewPayloadInput::BlockRlp { block, bal: None } if block == Bytes::from_static(&[0x01, 0x02]))
452        );
453    }
454
455    #[test]
456    fn raw_rlp_serializes_as_object_with_bal_and_decodes_legacy_object() {
457        let input = RethNewPayloadInput::<TestExecutionData>::block_rlp_with_bal(
458            Bytes::from_static(&[0x01]),
459            Bytes::from_static(&[0x02]),
460        );
461        assert_eq!(
462            serde_json::to_value(&input).unwrap(),
463            json!({"block": "0x01", "bal": "0x02"})
464        );
465
466        for value in [
467            json!({"block": "0x01", "bal": "0x02"}),
468            json!({"block": "0x01"}),
469        ] {
470            let decoded: RethNewPayloadInput<TestExecutionData> =
471                serde_json::from_value(value).unwrap();
472            assert!(
473                matches!(decoded, RethNewPayloadInput::BlockRlp { block, .. } if block == Bytes::from_static(&[0x01]))
474            );
475        }
476    }
477
478    #[test]
479    fn new_payload_params_omit_unset_wait_flags() {
480        let params =
481            RethNewPayloadParams::new(RethNewPayloadInput::execution_data(execution_data()));
482        assert_eq!(
483            serde_json::to_value(params).unwrap(),
484            json!({"payload": {"payload": "0x01", "sidecar": "0x02"}})
485        );
486
487        let params =
488            RethNewPayloadParams::new(RethNewPayloadInput::execution_data(execution_data()))
489                .with_wait_for_persistence(true)
490                .with_wait_for_caches(false);
491        assert_eq!(
492            serde_json::to_value(params).unwrap(),
493            json!({
494                "payload": {"payload": "0x01", "sidecar": "0x02"},
495                "wait_for_persistence": true,
496                "wait_for_caches": false,
497            })
498        );
499    }
500
501    #[test]
502    fn payload_status_preserves_all_timing_fields() {
503        let status: RethPayloadStatus = serde_json::from_value(json!({
504            "status": "VALID",
505            "latestValidHash": null,
506            "latency_us": 12,
507            "persistence_wait_us": 3,
508            "execution_cache_wait_us": 4,
509            "sparse_trie_wait_us": 5,
510        }))
511        .unwrap();
512
513        assert_eq!(status.status.status, PayloadStatusEnum::Valid);
514        assert_eq!(status.latency_us, 12);
515        assert_eq!(status.persistence_wait_us, Some(3));
516        assert_eq!(status.execution_cache_wait_us, Some(4));
517        assert_eq!(status.sparse_trie_wait_us, Some(5));
518    }
519
520    #[test]
521    fn payload_status_accepts_missing_timing_fields() {
522        let status: RethPayloadStatus = serde_json::from_value(json!({
523            "status": "VALID",
524            "latestValidHash": null,
525        }))
526        .unwrap();
527
528        assert_eq!(status.status.status, PayloadStatusEnum::Valid);
529        assert_eq!(status.latency_us, 0);
530        assert_eq!(status.persistence_wait_us, None);
531        assert_eq!(status.execution_cache_wait_us, None);
532        assert_eq!(status.sparse_trie_wait_us, None);
533    }
534}