Skip to main content

alloy_reth/
provider.rs

1//! Provider extension trait for the `reth_` RPC namespace.
2
3use crate::types::{
4    BalanceChangesInBlock, GetBlockExecutionOutcomeParams, RethNewPayloadParams, RethPayloadStatus,
5};
6use alloy_network::Network;
7use alloy_provider::Provider;
8use alloy_rpc_types_engine::{ForkchoiceState, ForkchoiceUpdated};
9use alloy_transport::TransportResult;
10
11mod sealed {
12    pub trait Sealed {}
13    impl<T> Sealed for T {}
14}
15
16/// Extension trait for the `reth_` RPC namespace.
17///
18/// Provides access to reth-specific RPC methods through the alloy provider. This trait is
19/// sealed and cannot be implemented outside of this crate.
20#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
21#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
22pub trait RethApi<N: Network>: sealed::Sealed + Send + Sync {
23    /// Returns the balance changes in the given block.
24    ///
25    /// ## JSON-RPC Method
26    ///
27    /// `reth_getBalanceChangesInBlock`
28    async fn reth_get_balance_changes_in_block(
29        &self,
30        params: impl Into<GetBlockExecutionOutcomeParams> + Send,
31    ) -> TransportResult<BalanceChangesInBlock>;
32
33    /// Returns the execution outcome for the given block.
34    ///
35    /// ## JSON-RPC Method
36    ///
37    /// `reth_getBlockExecutionOutcome`
38    async fn reth_get_block_execution_outcome(
39        &self,
40        params: impl Into<GetBlockExecutionOutcomeParams> + Send,
41    ) -> TransportResult<Option<serde_json::Value>>;
42
43    /// Submits a new payload to the engine.
44    ///
45    /// This is an extended version of `engine_newPayload` that accepts standard execution data,
46    /// big-block data, or raw RLP-encoded block bytes, and returns timing information.
47    ///
48    /// The request always contains three positional parameters: the payload, the optional
49    /// persistence wait flag, and the optional cache wait flag.
50    ///
51    /// # Examples
52    ///
53    /// ```ignore
54    /// use alloy_reth::{RethApi, RethNewPayloadInput, RethNewPayloadParams};
55    ///
56    /// let payload = RethNewPayloadInput::execution_data(serde_json::json!({
57    ///     "payload": "0x01",
58    ///     "sidecar": "0x02",
59    /// }));
60    /// provider
61    ///     .reth_new_payload(RethNewPayloadParams::new(payload))
62    ///     .await?;
63    /// # Ok::<(), alloy_transport::TransportError>(())
64    /// ```
65    ///
66    /// ## JSON-RPC Method
67    ///
68    /// `reth_newPayload`
69    async fn reth_new_payload<E>(
70        &self,
71        params: impl Into<RethNewPayloadParams<E>> + Send,
72    ) -> TransportResult<RethPayloadStatus>
73    where
74        E: serde::Serialize + Clone + core::fmt::Debug + Send + Sync + Unpin + 'static;
75
76    /// Updates the forkchoice state.
77    ///
78    /// This Reth-specific endpoint intentionally sends only the forkchoice state. It does not
79    /// accept or send payload attributes.
80    ///
81    /// # Examples
82    ///
83    /// ```ignore
84    /// use alloy_primitives::B256;
85    /// use alloy_reth::RethApi;
86    /// use alloy_rpc_types_engine::ForkchoiceState;
87    ///
88    /// provider
89    ///     .reth_forkchoice_updated(ForkchoiceState {
90    ///         head_block_hash: B256::ZERO,
91    ///         safe_block_hash: B256::ZERO,
92    ///         finalized_block_hash: B256::ZERO,
93    ///     })
94    ///     .await?;
95    /// # Ok::<(), alloy_transport::TransportError>(())
96    /// ```
97    ///
98    /// ## JSON-RPC Method
99    ///
100    /// `reth_forkchoiceUpdated`
101    async fn reth_forkchoice_updated(
102        &self,
103        forkchoice_state: ForkchoiceState,
104    ) -> TransportResult<ForkchoiceUpdated>;
105}
106
107#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
108#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
109impl<N, P> RethApi<N> for P
110where
111    N: Network,
112    P: Provider<N>,
113{
114    async fn reth_get_balance_changes_in_block(
115        &self,
116        params: impl Into<GetBlockExecutionOutcomeParams> + Send,
117    ) -> TransportResult<BalanceChangesInBlock> {
118        let params = params.into();
119        self.client()
120            .request("reth_getBalanceChangesInBlock", (params.block_id,))
121            .await
122    }
123
124    async fn reth_get_block_execution_outcome(
125        &self,
126        params: impl Into<GetBlockExecutionOutcomeParams> + Send,
127    ) -> TransportResult<Option<serde_json::Value>> {
128        let params = params.into();
129        self.client()
130            .request(
131                "reth_getBlockExecutionOutcome",
132                (params.block_id, params.count),
133            )
134            .await
135    }
136
137    async fn reth_new_payload<E>(
138        &self,
139        params: impl Into<RethNewPayloadParams<E>> + Send,
140    ) -> TransportResult<RethPayloadStatus>
141    where
142        E: serde::Serialize + Clone + core::fmt::Debug + Send + Sync + Unpin + 'static,
143    {
144        let params = params.into();
145        self.client()
146            .request(
147                "reth_newPayload",
148                (
149                    params.payload,
150                    params.wait_for_persistence,
151                    params.wait_for_caches,
152                ),
153            )
154            .await
155    }
156
157    async fn reth_forkchoice_updated(
158        &self,
159        forkchoice_state: ForkchoiceState,
160    ) -> TransportResult<ForkchoiceUpdated> {
161        self.client()
162            .request("reth_forkchoiceUpdated", (forkchoice_state,))
163            .await
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::{BigBlockData, RethNewPayloadInput};
171    use alloy_json_rpc::RequestPacket;
172    use alloy_network::Ethereum;
173    use alloy_primitives::{Address, B256, Bytes, U64, U256};
174    use alloy_provider::{Provider, ProviderBuilder};
175    use alloy_rpc_client::RpcClient;
176    use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};
177    use alloy_transport::{TransportError, TransportFut, mock::Asserter};
178    use serde::{Deserialize, Serialize};
179    use serde_json::{Value, json};
180    use std::sync::{Arc, Mutex};
181    use tower::Service;
182
183    #[derive(Debug, Clone, Serialize, Deserialize)]
184    struct TestExecutionData {
185        payload: Bytes,
186        sidecar: Bytes,
187    }
188
189    #[derive(Clone, Debug)]
190    struct RecordingTransport {
191        inner: alloy_transport::mock::MockTransport,
192        requests: Arc<Mutex<Vec<RequestPacket>>>,
193    }
194
195    impl Service<RequestPacket> for RecordingTransport {
196        type Response = alloy_json_rpc::ResponsePacket;
197        type Error = TransportError;
198        type Future = TransportFut<'static>;
199
200        fn poll_ready(
201            &mut self,
202            cx: &mut std::task::Context<'_>,
203        ) -> std::task::Poll<Result<(), Self::Error>> {
204            self.inner.poll_ready(cx)
205        }
206
207        fn call(&mut self, request: RequestPacket) -> Self::Future {
208            self.requests.lock().unwrap().push(request.clone());
209            self.inner.call(request)
210        }
211    }
212
213    fn provider_with_asserter(
214        asserter: Asserter,
215    ) -> (impl Provider<Ethereum>, Arc<Mutex<Vec<RequestPacket>>>) {
216        let requests = Arc::new(Mutex::new(Vec::new()));
217        let transport = RecordingTransport {
218            inner: alloy_transport::mock::MockTransport::new(asserter),
219            requests: Arc::clone(&requests),
220        };
221        let provider = ProviderBuilder::new().connect_client(RpcClient::new(transport, true));
222        (provider, requests)
223    }
224
225    fn take_request(requests: &Arc<Mutex<Vec<RequestPacket>>>) -> Value {
226        let packet = requests.lock().unwrap().remove(0);
227        let request = packet
228            .as_single()
229            .expect("expected a single JSON-RPC request");
230        serde_json::from_str(request.serialized().get()).unwrap()
231    }
232
233    fn assert_request(requests: &Arc<Mutex<Vec<RequestPacket>>>, method: &str, params: Value) {
234        let request = take_request(requests);
235        assert_eq!(request["method"], method);
236        assert_eq!(request["params"], params);
237    }
238
239    fn push_valid_status(asserter: &Asserter, with_timings: bool) {
240        let response = if with_timings {
241            json!({
242                "status": "VALID",
243                "latestValidHash": null,
244                "latency_us": 12,
245                "persistence_wait_us": 3,
246                "execution_cache_wait_us": 4,
247                "sparse_trie_wait_us": 5,
248            })
249        } else {
250            json!({"status": "VALID", "latestValidHash": null})
251        };
252        asserter.push_success(&response);
253    }
254
255    fn execution_data() -> TestExecutionData {
256        TestExecutionData {
257            payload: Bytes::from_static(&[0x01]),
258            sidecar: Bytes::from_static(&[0x02]),
259        }
260    }
261
262    #[tokio::test]
263    async fn balance_changes_uses_exact_reth_method_and_params() {
264        let asserter = Asserter::new();
265        asserter.push_success(&json!({
266            "0x1111111111111111111111111111111111111111": "0x02"
267        }));
268        let (provider, requests) = provider_with_asserter(asserter);
269
270        let changes = provider
271            .reth_get_balance_changes_in_block(alloy_eips::BlockId::latest())
272            .await
273            .unwrap();
274
275        assert_eq!(
276            changes.get(&Address::from([0x11; 20])),
277            Some(&U256::from(2))
278        );
279        assert_request(
280            &requests,
281            "reth_getBalanceChangesInBlock",
282            json!(["latest"]),
283        );
284    }
285
286    #[tokio::test]
287    async fn execution_outcome_uses_exact_reth_method_and_params() {
288        let asserter = Asserter::new();
289        asserter.push_success(&json!({"state": "0x01"}));
290        let (provider, requests) = provider_with_asserter(asserter);
291
292        let outcome = provider
293            .reth_get_block_execution_outcome(
294                GetBlockExecutionOutcomeParams::new(alloy_eips::BlockId::latest())
295                    .with_count(U64::from_limbs([2])),
296            )
297            .await
298            .unwrap();
299
300        assert_eq!(outcome, Some(json!({"state": "0x01"})));
301        assert_request(
302            &requests,
303            "reth_getBlockExecutionOutcome",
304            json!(["latest", "0x2"]),
305        );
306    }
307
308    #[tokio::test]
309    async fn new_payload_sends_all_variants_and_both_wait_flags() {
310        let asserter = Asserter::new();
311        push_valid_status(&asserter, true);
312        push_valid_status(&asserter, false);
313        push_valid_status(&asserter, false);
314        let (provider, requests) = provider_with_asserter(asserter);
315
316        let standard = RethNewPayloadInput::execution_data(execution_data());
317        let status = provider
318            .reth_new_payload(
319                RethNewPayloadParams::new(standard)
320                    .with_wait_for_persistence(true)
321                    .with_wait_for_caches(false),
322            )
323            .await
324            .unwrap();
325        assert_eq!(status.status.status, PayloadStatusEnum::Valid);
326        assert_eq!(status.latency_us, 12);
327        assert_eq!(status.persistence_wait_us, Some(3));
328        assert_eq!(status.execution_cache_wait_us, Some(4));
329        assert_eq!(status.sparse_trie_wait_us, Some(5));
330        assert_request(
331            &requests,
332            "reth_newPayload",
333            json!([{"payload": "0x01", "sidecar": "0x02"}, true, false]),
334        );
335
336        let hash = B256::from([0x11; 32]);
337        let big_block = RethNewPayloadInput::big_block_data(BigBlockData {
338            env_switches: vec![execution_data()],
339            prior_block_hashes: vec![(7, hash)],
340            block_number: 8,
341            merged_block_access_list: None,
342        });
343        let status = provider.reth_new_payload(big_block).await.unwrap();
344        assert_eq!(status.persistence_wait_us, None);
345        assert_eq!(status.execution_cache_wait_us, None);
346        assert_eq!(status.sparse_trie_wait_us, None);
347        let hash = serde_json::to_value(hash).unwrap();
348        assert_request(
349            &requests,
350            "reth_newPayload",
351            json!([{
352                "env_switches": [{"payload": "0x01", "sidecar": "0x02"}],
353                "prior_block_hashes": [[7, hash]],
354                "block_number": 8,
355            }, null, null]),
356        );
357
358        let raw = RethNewPayloadInput::<serde_json::Value>::block_rlp_with_bal(
359            Bytes::from_static(&[0x04]),
360            Bytes::from_static(&[0x05]),
361        );
362        provider
363            .reth_new_payload(RethNewPayloadParams::new(raw).with_wait_for_caches(true))
364            .await
365            .unwrap();
366        assert_request(
367            &requests,
368            "reth_newPayload",
369            json!([{"block": "0x04", "bal": "0x05"}, null, true]),
370        );
371    }
372
373    #[tokio::test]
374    async fn forkchoice_updated_sends_only_state_without_payload_attributes() {
375        let asserter = Asserter::new();
376        asserter.push_success(&json!({
377            "payloadStatus": {"status": "VALID", "latestValidHash": null},
378            "payloadId": null,
379        }));
380        let (provider, requests) = provider_with_asserter(asserter);
381        let state = ForkchoiceState::same_hash(B256::from([0x11; 32]));
382
383        let response = provider.reth_forkchoice_updated(state).await.unwrap();
384        assert!(response.payload_status.is_valid());
385
386        assert_request(
387            &requests,
388            "reth_forkchoiceUpdated",
389            json!([serde_json::to_value(state).unwrap()]),
390        );
391    }
392}