arb_rpc/
arb_api.rs

1//! Arbitrum `arb_` RPC namespace.
2//!
3//! Implements the `arb_` JSON-RPC methods: maintenance status,
4//! health checks, and block metadata queries.
5
6use std::sync::Arc;
7
8use alloy_consensus::BlockHeader;
9use alloy_primitives::B256;
10use alloy_rpc_types_eth::BlockNumberOrTag;
11use jsonrpsee::{core::RpcResult, proc_macros::rpc};
12use reth_provider::{BlockNumReader, BlockReaderIdExt, HeaderProvider};
13
14use crate::{
15    stylus_tracer::HostioTraceInfo, ArbBlockInfo, ArbMaintenanceStatus, NumberAndBlockMetadata,
16};
17
18/// Arbitrum `arb_` RPC namespace.
19#[rpc(server, namespace = "arb")]
20pub trait ArbApi {
21    /// Returns the maintenance status of the node.
22    #[method(name = "maintenanceStatus")]
23    fn maintenance_status(&self) -> RpcResult<ArbMaintenanceStatus>;
24
25    /// Publisher health check. Errors when the node is not configured
26    /// as a transaction publisher (the executor-only mode here).
27    #[method(name = "checkPublisherHealth")]
28    fn check_publisher_health(&self) -> RpcResult<()>;
29
30    /// Returns block info for the given block number.
31    #[method(name = "getBlockInfo")]
32    async fn get_block_info(&self, block_num: u64) -> RpcResult<ArbBlockInfo>;
33
34    /// Raw block metadata for blocks in `[fromBlock, toBlock]`. The
35    /// `rawMetadata` field is empty since no sidecar data is tracked.
36    #[method(name = "getRawBlockMetadata")]
37    async fn get_raw_block_metadata(
38        &self,
39        from_block: BlockNumberOrTag,
40        to_block: BlockNumberOrTag,
41    ) -> RpcResult<Vec<NumberAndBlockMetadata>>;
42
43    /// Stylus host-I/O trace for a previously-executed transaction.
44    /// Empty if no Stylus contracts were invoked.
45    #[method(name = "traceStylusHostio")]
46    async fn trace_stylus_hostio(&self, tx_hash: B256) -> RpcResult<Vec<HostioTraceInfo>>;
47
48    /// Currently-set validated block hash. Zero hash when unset.
49    #[method(name = "getValidatedBlock")]
50    fn get_validated_block(&self) -> RpcResult<B256>;
51
52    /// L1 confirmations for the L2 block at `block_num`. Returns 0
53    /// without an L1 reader.
54    #[method(name = "getL1Confirmations")]
55    async fn get_l1_confirmations(&self, block_num: u64) -> RpcResult<u64>;
56
57    /// L1 batch number containing the L2 block at `block_num`. Errors
58    /// without an L1 batch index.
59    #[method(name = "findBatchContainingBlock")]
60    async fn find_batch_containing_block(&self, block_num: u64) -> RpcResult<u64>;
61}
62
63pub struct ArbApiHandler<Provider> {
64    provider: Provider,
65    maintenance_status: Arc<parking_lot::RwLock<ArbMaintenanceStatus>>,
66    validated_block: Arc<parking_lot::RwLock<B256>>,
67}
68
69impl<Provider> ArbApiHandler<Provider> {
70    /// Create a new `ArbApiHandler`.
71    pub fn new(provider: Provider) -> Self {
72        Self {
73            provider,
74            maintenance_status: Arc::new(parking_lot::RwLock::new(ArbMaintenanceStatus::default())),
75            validated_block: Arc::new(parking_lot::RwLock::new(B256::ZERO)),
76        }
77    }
78
79    /// Returns a reference to the maintenance status lock for external updates.
80    pub fn maintenance_status_handle(&self) -> Arc<parking_lot::RwLock<ArbMaintenanceStatus>> {
81        self.maintenance_status.clone()
82    }
83
84    /// Returns a shared handle the block producer uses to push
85    /// validated-block updates from `setFinalityData`.
86    pub fn validated_block_handle(&self) -> Arc<parking_lot::RwLock<B256>> {
87        self.validated_block.clone()
88    }
89}
90
91#[async_trait::async_trait]
92impl<Provider> ArbApiServer for ArbApiHandler<Provider>
93where
94    Provider: BlockNumReader + BlockReaderIdExt + HeaderProvider + 'static,
95{
96    fn maintenance_status(&self) -> RpcResult<ArbMaintenanceStatus> {
97        Ok(self.maintenance_status.read().clone())
98    }
99
100    fn check_publisher_health(&self) -> RpcResult<()> {
101        Err(jsonrpsee::types::ErrorObject::owned(
102            jsonrpsee::types::error::INTERNAL_ERROR_CODE,
103            "publishing transactions not supported by this endpoint",
104            None::<()>,
105        ))
106    }
107
108    async fn get_block_info(&self, block_num: u64) -> RpcResult<ArbBlockInfo> {
109        let header = self
110            .provider
111            .sealed_header_by_number_or_tag(BlockNumberOrTag::Number(block_num))
112            .map_err(|e| {
113                jsonrpsee::types::ErrorObject::owned(
114                    jsonrpsee::types::error::INTERNAL_ERROR_CODE,
115                    e.to_string(),
116                    None::<()>,
117                )
118            })?
119            .ok_or_else(|| {
120                jsonrpsee::types::ErrorObject::owned(
121                    jsonrpsee::types::error::INVALID_PARAMS_CODE,
122                    format!("block {block_num} not found"),
123                    None::<()>,
124                )
125            })?;
126
127        let mix = header.mix_hash().unwrap_or_default();
128        let extra = header.extra_data();
129
130        let send_count = u64::from_be_bytes(mix.0[0..8].try_into().unwrap_or_default());
131        let l1_block_number = u64::from_be_bytes(mix.0[8..16].try_into().unwrap_or_default());
132        let arbos_format_version = u64::from_be_bytes(mix.0[16..24].try_into().unwrap_or_default());
133
134        let send_root = if extra.len() >= 32 {
135            B256::from_slice(&extra[..32])
136        } else {
137            B256::ZERO
138        };
139
140        Ok(ArbBlockInfo {
141            l1_block_number,
142            arbos_format_version,
143            send_count,
144            send_root,
145        })
146    }
147
148    async fn trace_stylus_hostio(
149        &self,
150        tx_hash: B256,
151    ) -> RpcResult<Vec<crate::stylus_tracer::HostioTraceInfo>> {
152        Ok(crate::stylus_tracer::take_cached_trace(tx_hash))
153    }
154
155    fn get_validated_block(&self) -> RpcResult<B256> {
156        Ok(*self.validated_block.read())
157    }
158
159    async fn get_l1_confirmations(&self, _block_num: u64) -> RpcResult<u64> {
160        Ok(0)
161    }
162
163    async fn find_batch_containing_block(&self, block_num: u64) -> RpcResult<u64> {
164        Err(jsonrpsee::types::ErrorObject::owned(
165            jsonrpsee::types::error::INTERNAL_ERROR_CODE,
166            format!("no batch index available for block {block_num}"),
167            None::<()>,
168        ))
169    }
170
171    async fn get_raw_block_metadata(
172        &self,
173        from_block: BlockNumberOrTag,
174        to_block: BlockNumberOrTag,
175    ) -> RpcResult<Vec<NumberAndBlockMetadata>> {
176        let internal = |e: alloy_rpc_types_eth::ConversionError| {
177            jsonrpsee::types::ErrorObject::owned(
178                jsonrpsee::types::error::INVALID_PARAMS_CODE,
179                e.to_string(),
180                None::<()>,
181            )
182        };
183        let provider_err = |e: reth_provider::ProviderError| {
184            jsonrpsee::types::ErrorObject::owned(
185                jsonrpsee::types::error::INTERNAL_ERROR_CODE,
186                e.to_string(),
187                None::<()>,
188            )
189        };
190        let best = self.provider.best_block_number().map_err(provider_err)?;
191        let resolve = |bn: BlockNumberOrTag| -> Result<u64, _> {
192            match bn {
193                BlockNumberOrTag::Number(n) => Ok(n),
194                BlockNumberOrTag::Earliest => Ok(0),
195                BlockNumberOrTag::Latest
196                | BlockNumberOrTag::Safe
197                | BlockNumberOrTag::Finalized
198                | BlockNumberOrTag::Pending => Ok(best),
199            }
200            .map_err(internal)
201        };
202        let from = resolve(from_block)?;
203        let to = resolve(to_block)?;
204        if from > to {
205            return Err(jsonrpsee::types::ErrorObject::owned(
206                jsonrpsee::types::error::INVALID_PARAMS_CODE,
207                format!("from_block ({from}) > to_block ({to})"),
208                None::<()>,
209            ));
210        }
211        const MAX_RANGE: u64 = 5_000;
212        if to.saturating_sub(from) + 1 > MAX_RANGE {
213            return Err(jsonrpsee::types::ErrorObject::owned(
214                jsonrpsee::types::error::INVALID_PARAMS_CODE,
215                format!("range exceeds {MAX_RANGE} blocks"),
216                None::<()>,
217            ));
218        }
219        // No sidecar metadata is tracked — return an empty list so the
220        // wire shape matches a node with no metadata stored.
221        let _ = (from, to, best);
222        Ok(Vec::new())
223    }
224}