Skip to main content

reth_rpc_eth_api/helpers/
spec.rs

1//! Loads chain metadata.
2
3use alloy_primitives::{U256, U64};
4use alloy_rpc_types_eth::{Stage, SyncInfo, SyncStatus};
5use futures::Future;
6use reth_chainspec::ChainInfo;
7use reth_errors::{RethError, RethResult};
8use reth_network_api::NetworkInfo;
9use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment};
10use reth_rpc_convert::RpcTxReq;
11use reth_rpc_eth_types::{EthCapabilities, EthCapabilitiesHead, EthCapabilitiesResource};
12use reth_storage_api::{
13    BlockNumReader, PruneCheckpointReader, StageCheckpointReader, TransactionsProvider,
14};
15
16use crate::{
17    helpers::{EthSigner, EthState},
18    EthApiTypes, RpcNodeCore,
19};
20
21/// `Eth` API trait.
22///
23/// Defines core functionality of the `eth` API implementation.
24#[auto_impl::auto_impl(&, Arc)]
25pub trait EthApiSpec: RpcNodeCore + EthApiTypes {
26    /// Returns the block node is started on.
27    fn starting_block(&self) -> U256;
28
29    /// Returns the current ethereum protocol version.
30    fn protocol_version(&self) -> impl Future<Output = RethResult<U64>> + Send {
31        async move {
32            let status = self.network().network_status().await.map_err(RethError::other)?;
33            Ok(U64::from(status.protocol_version))
34        }
35    }
36
37    /// Returns the chain id
38    fn chain_id(&self) -> U64 {
39        U64::from(self.network().chain_id())
40    }
41
42    /// Returns provider chain info
43    fn chain_info(&self) -> RethResult<ChainInfo> {
44        Ok(self.provider().chain_info()?)
45    }
46
47    /// Returns effective routing capabilities for this node.
48    ///
49    /// The response follows the `eth_capabilities` execution API proposal:
50    /// <https://github.com/ethereum/execution-apis/pull/755>.
51    fn capabilities(&self) -> RethResult<EthCapabilities>
52    where
53        Self: EthState,
54    {
55        let chain_info = self.chain_info()?;
56        let checkpoints = self.provider().get_prune_checkpoints_for_availability()?;
57
58        let state = effective_resource(
59            &checkpoints,
60            &[PruneSegment::AccountHistory, PruneSegment::StorageHistory],
61        );
62        let tx = effective_resource(
63            &checkpoints,
64            &[PruneSegment::TransactionLookup, PruneSegment::Bodies],
65        );
66        let logs =
67            effective_resource(&checkpoints, &[PruneSegment::Receipts, PruneSegment::ContractLogs]);
68        let receipts = effective_resource(
69            &checkpoints,
70            &[PruneSegment::Receipts, PruneSegment::TransactionLookup, PruneSegment::Bodies],
71        );
72        let blocks = effective_resource(&checkpoints, &[PruneSegment::Bodies]);
73
74        let proof_window = self.max_proof_window();
75        let proof_oldest = chain_info
76            .best_number
77            .saturating_sub(proof_window)
78            .max(state.oldest_block.map(|number| number.to::<u64>()).unwrap_or_default());
79        let state_proofs = EthCapabilitiesResource::window(proof_oldest, proof_window);
80
81        Ok(EthCapabilities {
82            head: EthCapabilitiesHead {
83                number: chain_info.best_number,
84                hash: chain_info.best_hash,
85            },
86            state,
87            tx,
88            logs,
89            receipts,
90            blocks,
91            state_proofs,
92        })
93    }
94
95    /// Returns `true` if the network is undergoing sync.
96    fn is_syncing(&self) -> bool {
97        self.network().is_syncing()
98    }
99
100    /// Returns the [`SyncStatus`] of the network
101    fn sync_status(&self) -> RethResult<SyncStatus> {
102        let status = if self.is_syncing() {
103            let current_block = U256::from(
104                self.provider().chain_info().map(|info| info.best_number).unwrap_or_default(),
105            );
106
107            let stages = self
108                .provider()
109                .get_all_checkpoints()
110                .unwrap_or_default()
111                .into_iter()
112                .map(|(name, checkpoint)| Stage { name, block: checkpoint.block_number })
113                .collect();
114
115            SyncStatus::Info(Box::new(SyncInfo {
116                starting_block: self.starting_block(),
117                current_block,
118                highest_block: current_block,
119                warp_chunks_amount: None,
120                warp_chunks_processed: None,
121                stages: Some(stages),
122            }))
123        } else {
124            SyncStatus::None
125        };
126        Ok(status)
127    }
128}
129
130/// Derives effective block availability from checkpoints adjusted for retained data by
131/// [`PruneCheckpointReader::get_prune_checkpoints_for_availability`].
132///
133/// A resource is only available where every required segment is available, so the oldest available
134/// block is the maximum pruned checkpoint across the segments. If any segment is configured with a
135/// distance-based prune mode, or with full pruning that still keeps the segment minimum, expose the
136/// smallest retention window because that is the tightest limit callers can rely on. If full
137/// pruning leaves no reliable window for any required segment, the resource is disabled.
138///
139/// See also: <https://github.com/ethereum/execution-apis/pull/755>
140fn effective_resource(
141    checkpoints: &[(PruneSegment, PruneCheckpoint)],
142    segments: &[PruneSegment],
143) -> EthCapabilitiesResource {
144    let mut oldest_block = 0;
145    let mut retention_blocks = None::<u64>;
146    let mut disabled = false;
147
148    for segment in segments {
149        let Some((_, checkpoint)) = checkpoints.iter().find(|(key, _)| key == segment) else {
150            continue;
151        };
152
153        if let Some(block_number) = checkpoint.block_number {
154            oldest_block = oldest_block.max(block_number.saturating_add(1));
155        }
156
157        match checkpoint.prune_mode {
158            PruneMode::Distance(distance) => {
159                retention_blocks =
160                    Some(retention_blocks.map_or(distance, |current| current.min(distance)));
161            }
162            PruneMode::Full => {
163                let min_blocks = segment.min_blocks();
164                if min_blocks == 0 {
165                    disabled = true;
166                } else {
167                    retention_blocks = Some(
168                        retention_blocks.map_or(min_blocks, |current| current.min(min_blocks)),
169                    );
170                }
171            }
172            PruneMode::Before(_) => {}
173        }
174    }
175
176    if disabled {
177        EthCapabilitiesResource::disabled()
178    } else if let Some(retention_blocks) = retention_blocks {
179        EthCapabilitiesResource::window(oldest_block, retention_blocks)
180    } else {
181        EthCapabilitiesResource::available_from(oldest_block)
182    }
183}
184
185/// A handle to [`EthSigner`]s with its generics set from [`TransactionsProvider`] and
186/// [`reth_rpc_convert::RpcTypes`].
187pub type SignersForRpc<Provider, Rpc> = parking_lot::RwLock<
188    Vec<Box<dyn EthSigner<<Provider as TransactionsProvider>::Transaction, RpcTxReq<Rpc>>>>,
189>;
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn full_prune_segment_without_retention_disables_resource() {
197        let checkpoints = [(
198            PruneSegment::TransactionLookup,
199            PruneCheckpoint {
200                block_number: Some(10),
201                tx_number: None,
202                prune_mode: PruneMode::Full,
203            },
204        )];
205
206        let resource = effective_resource(&checkpoints, &[PruneSegment::TransactionLookup]);
207
208        assert_eq!(resource, EthCapabilitiesResource::disabled());
209    }
210
211    #[test]
212    fn full_prune_segment_with_minimum_retention_uses_window() {
213        let checkpoints = [(
214            PruneSegment::Bodies,
215            PruneCheckpoint {
216                block_number: Some(10),
217                tx_number: None,
218                prune_mode: PruneMode::Full,
219            },
220        )];
221
222        let resource = effective_resource(&checkpoints, &[PruneSegment::Bodies]);
223
224        assert_eq!(
225            resource,
226            EthCapabilitiesResource::window(11, PruneSegment::Bodies.min_blocks())
227        );
228    }
229
230    #[test]
231    fn retained_receipts_preserve_resource_limits() {
232        let checkpoints = [
233            (
234                PruneSegment::Receipts,
235                PruneCheckpoint {
236                    block_number: None,
237                    tx_number: None,
238                    prune_mode: PruneMode::Distance(100),
239                },
240            ),
241            (
242                PruneSegment::ContractLogs,
243                PruneCheckpoint {
244                    prune_mode: PruneMode::Distance(50),
245                    ..PruneCheckpoint::pruned_through(19)
246                },
247            ),
248            (PruneSegment::TransactionLookup, PruneCheckpoint::pruned_through(39)),
249            (PruneSegment::Bodies, PruneCheckpoint::pruned_through(29)),
250        ];
251        for (segments, expected) in [
252            (vec![PruneSegment::Receipts], EthCapabilitiesResource::window(0, 100)),
253            (
254                vec![PruneSegment::Receipts, PruneSegment::ContractLogs],
255                EthCapabilitiesResource::window(20, 50),
256            ),
257            (
258                vec![PruneSegment::Receipts, PruneSegment::TransactionLookup, PruneSegment::Bodies],
259                EthCapabilitiesResource::window(40, 100),
260            ),
261            (vec![PruneSegment::Bodies], EthCapabilitiesResource::available_from(30)),
262            (vec![PruneSegment::AccountHistory], EthCapabilitiesResource::available_from(0)),
263        ] {
264            assert_eq!(effective_resource(&checkpoints, &segments), expected, "{segments:?}");
265        }
266    }
267}