Skip to main content

reth_consensus_debug_client/providers/
rpc.rs

1use crate::PayloadProvider;
2use alloy_consensus::BlockHeader;
3use alloy_eips::BlockId;
4use alloy_provider::{
5    network::{primitives::HeaderResponse, AnyNetwork, AnyRpcBlock, AnyRpcHeader, BlockResponse},
6    ConnectionConfig, Provider, ProviderBuilder, WebSocketConfig,
7};
8use alloy_rpc_types_engine::PayloadExtras;
9use alloy_transport::TransportResult;
10use futures::{future::BoxFuture, stream::BoxStream, StreamExt};
11use reth_node_api::ExecutionPayload;
12use std::sync::Arc;
13use tokio::sync::mpsc::Sender;
14use tracing::{debug, warn};
15
16/// Block provider that fetches new blocks from an RPC endpoint using a connection that supports
17/// RPC subscriptions.
18#[derive(derive_more::Debug, Clone)]
19pub struct RpcBlockProvider<ExecutionData> {
20    client: AnyRpcClient,
21    #[debug(skip)]
22    convert: Arc<dyn Fn(AnyRpcBlock, PayloadExtras) -> ExecutionData + Send + Sync>,
23}
24
25impl<ExecutionData> RpcBlockProvider<ExecutionData> {
26    /// Create a new RPC block provider with the given RPC URL.
27    pub async fn new(
28        rpc_url: &str,
29        convert: impl Fn(AnyRpcBlock, PayloadExtras) -> ExecutionData + Send + Sync + 'static,
30    ) -> eyre::Result<Self> {
31        Ok(Self { client: AnyRpcClient::connect(rpc_url).await?, convert: Arc::new(convert) })
32    }
33
34    /// Disables fetching raw block access list bytes.
35    pub const fn without_block_access_lists(mut self) -> Self {
36        self.client.fetch_block_access_list = false;
37        self
38    }
39}
40
41impl<ExecutionData> PayloadProvider for RpcBlockProvider<ExecutionData>
42where
43    ExecutionData: ExecutionPayload,
44{
45    type ExecutionData = ExecutionData;
46
47    async fn subscribe_payloads(&self, tx: Sender<Self::ExecutionData>) {
48        loop {
49            let Ok(mut stream) = self.client.full_block_stream().await.inspect_err(|err| {
50                warn!(
51                    target: "consensus::debug-client",
52                    %err,
53                    url=%self.client.url,
54                    "Failed to subscribe to blocks, retrying in 5s",
55                );
56            }) else {
57                // Exit if the receiver has been dropped (e.g. during shutdown) so we
58                // don't keep retrying after the consumer is gone.
59                if tx.is_closed() {
60                    return;
61                }
62                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
63                continue
64            };
65
66            while let Some(res) = stream.next().await {
67                match res {
68                    Ok(block) => {
69                        let extras = self.client.payload_extras(block.header()).await;
70                        let payload = (self.convert)(block, extras);
71                        if tx.send(payload).await.is_err() {
72                            // Channel closed - receiver dropped, exit completely.
73                            return;
74                        }
75                    }
76                    Err(err) => {
77                        warn!(
78                            target: "consensus::debug-client",
79                            %err,
80                            url=%self.client.url,
81                            "Failed to fetch a block",
82                        );
83                    }
84                }
85            }
86            // if stream terminated we want to re-establish it again
87            debug!(
88                target: "consensus::debug-client",
89                url=%self.client.url,
90                "Re-establishing block subscription",
91            );
92        }
93    }
94
95    async fn get_payload(&self, block_number: u64) -> eyre::Result<Self::ExecutionData> {
96        let (block, extras) = self.client.get_block(block_number).await?;
97        Ok((self.convert)(block, extras))
98    }
99}
100
101type BlockStream = BoxStream<'static, TransportResult<AnyRpcBlock>>;
102
103/// Non-generic connection to an RPC endpoint that returns [`AnyNetwork`] blocks.
104///
105/// The methods called from generic code return boxed futures: the body of an `async fn` is
106/// compiled in every crate that polls it, whereas a boxed future is polled through a vtable built
107/// here. This keeps the alloy provider, pubsub, and transport code out of downstream binaries.
108#[derive(derive_more::Debug, Clone)]
109struct AnyRpcClient {
110    #[debug(skip)]
111    provider: Arc<dyn Provider<AnyNetwork>>,
112    url: String,
113    fetch_block_access_list: bool,
114}
115
116impl AnyRpcClient {
117    fn connect(rpc_url: &str) -> BoxFuture<'_, eyre::Result<Self>> {
118        Box::pin(async move {
119            Ok(Self {
120                provider: Arc::new(
121                    ProviderBuilder::<_, _, AnyNetwork>::default()
122                        .connect_with_config(
123                            rpc_url,
124                            ConnectionConfig::default().with_max_retries(u32::MAX).with_ws_config(
125                                WebSocketConfig::default()
126                                    // allow larger messages/frames for big blocks
127                                    .max_frame_size(Some(128 * 1024 * 1024))
128                                    .max_message_size(Some(128 * 1024 * 1024)),
129                            ),
130                        )
131                        .await?,
132                ),
133                url: rpc_url.to_string(),
134                fetch_block_access_list: true,
135            })
136        })
137    }
138
139    /// Obtains a full block stream.
140    ///
141    /// This first attempts to obtain an `eth_subscribe` subscription, if that fails because the
142    /// connection is not a websocket, this falls back to poll based subscription.
143    fn full_block_stream(&self) -> BoxFuture<'_, TransportResult<BlockStream>> {
144        Box::pin(async move {
145            // first try to obtain a regular subscription
146            match self.provider.subscribe_full_blocks().full().into_stream().await {
147                Ok(sub) => Ok(sub.boxed()),
148                Err(err) => {
149                    debug!(
150                        target: "consensus::debug-client",
151                        %err,
152                        url=%self.url,
153                        "Failed to establish block subscription",
154                    );
155                    Ok(self.provider.watch_full_blocks().await?.full().into_stream().boxed())
156                }
157            }
158        })
159    }
160
161    fn get_block(
162        &self,
163        block_number: u64,
164    ) -> BoxFuture<'_, eyre::Result<(AnyRpcBlock, PayloadExtras)>> {
165        Box::pin(async move {
166            let block = self
167                .provider
168                .get_block_by_number(block_number.into())
169                .full()
170                .await?
171                .ok_or_else(|| eyre::eyre!("block not found by number {}", block_number))?;
172            let extras = self.payload_extras(block.header()).await;
173            Ok((block, extras))
174        })
175    }
176
177    /// Fetches optional payload side data for blocks that advertise a block access list hash.
178    ///
179    /// Block access lists are best effort here: RPC providers may not support
180    /// `eth_getBlockAccessListByHash`, so failed or missing responses fall back to empty extras.
181    fn payload_extras<'a>(&'a self, header: &'a AnyRpcHeader) -> BoxFuture<'a, PayloadExtras> {
182        Box::pin(async move {
183            if !self.fetch_block_access_list {
184                return PayloadExtras::default()
185            }
186
187            let block_hash = header.hash();
188            if header.block_access_list_hash().is_none() {
189                return PayloadExtras::default()
190            };
191
192            let block_access_list = self
193                .provider
194                .get_block_access_list_raw(BlockId::from(block_hash))
195                .await
196                .inspect_err(|err| {
197                    warn!(
198                        target: "consensus::debug-client",
199                        %err,
200                        url=%self.url,
201                        %block_hash,
202                        "Failed to fetch block access list",
203                    );
204                })
205                .ok()
206                .flatten();
207
208            PayloadExtras::from(block_access_list)
209        })
210    }
211}