1use std::sync::{Arc, OnceLock};
7
8use alloy_consensus::BlockHeader;
9use alloy_primitives::B256;
10use alloy_rpc_types_eth::BlockNumberOrTag;
11use base64::{
12 Engine as _, alphabet,
13 engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
14};
15use jsonrpsee::core::RpcResult;
16use parking_lot::RwLock;
17use reth_metrics::{
18 Metrics,
19 metrics::{self, Gauge},
20};
21use reth_provider::{BlockNumReader, BlockReaderIdExt, HeaderProvider};
22use tracing::{debug, info, warn};
23
24use crate::{
25 block_producer::{BlockProducer, BlockProductionInput},
26 error::{RpcError, RpcResult as ArbRpcResult},
27 nitro_execution::{
28 NitroExecutionApiServer, RpcConsensusSyncData, RpcFinalityData, RpcMaintenanceStatus,
29 RpcMessageResult, RpcMessageWithMetadata, RpcMessageWithMetadataAndBlockInfo,
30 },
31};
32
33#[derive(Debug, Default)]
35pub struct NitroExecutionState {
36 pub synced: bool,
38 pub max_message_count: u64,
40}
41
42#[derive(Metrics)]
44#[metrics(scope = "arb_sync")]
45struct NitroExecutionMetrics {
46 messages_behind: Gauge,
48}
49
50pub struct NitroExecutionHandler<Provider, BP> {
55 provider: Provider,
56 block_producer: Arc<BP>,
57 state: Arc<RwLock<NitroExecutionState>>,
58 genesis_block_num: u64,
60 metrics: NitroExecutionMetrics,
61}
62
63impl<Provider, BP> NitroExecutionHandler<Provider, BP> {
64 pub fn new(provider: Provider, block_producer: Arc<BP>, genesis_block_num: u64) -> Self {
66 Self {
67 provider,
68 block_producer,
69 state: Arc::new(RwLock::new(NitroExecutionState::default())),
70 genesis_block_num,
71 metrics: NitroExecutionMetrics::default(),
72 }
73 }
74
75 fn message_index_to_block_number(&self, msg_idx: u64) -> u64 {
77 self.genesis_block_num + msg_idx
78 }
79
80 fn block_number_to_message_index(&self, block_num: u64) -> Option<u64> {
82 if block_num < self.genesis_block_num {
83 return None;
84 }
85 Some(block_num - self.genesis_block_num)
86 }
87}
88
89impl<Provider, BP> NitroExecutionHandler<Provider, BP>
90where
91 Provider: BlockReaderIdExt + HeaderProvider,
92{
93 fn get_header(
95 &self,
96 block_num: u64,
97 ) -> ArbRpcResult<
98 Option<reth_primitives_traits::SealedHeader<<Provider as HeaderProvider>::Header>>,
99 > {
100 Ok(self
101 .provider
102 .sealed_header_by_number_or_tag(BlockNumberOrTag::Number(block_num))?)
103 }
104
105 fn send_root_from_header(header: &impl BlockHeader) -> B256 {
107 let extra = header.extra_data();
108 if extra.len() >= 32 {
109 B256::from_slice(&extra[..32])
110 } else {
111 B256::ZERO
112 }
113 }
114}
115
116impl<Provider, BP> NitroExecutionHandler<Provider, BP>
117where
118 Provider: BlockNumReader,
119{
120 fn update_messages_behind(&self) {
122 let best = match self.provider.best_block_number() {
123 Ok(best) => best,
124 Err(err) => {
125 warn!(target: "nitroexecution", %err, "failed to read best block number; skipping messages_behind update");
126 return;
127 }
128 };
129 let Some(head_msg_idx) = self.block_number_to_message_index(best) else {
130 debug!(target: "nitroexecution", best, genesis = self.genesis_block_num, "head below genesis; skipping messages_behind update");
131 return;
132 };
133 let max_message_count = self.state.read().max_message_count;
134 let processed = head_msg_idx.saturating_add(1);
136 self.metrics
137 .messages_behind
138 .set(max_message_count.saturating_sub(processed) as f64);
139 }
140}
141
142fn decode_l2_msg(l2_msg: &Option<String>) -> ArbRpcResult<Vec<u8>> {
147 match l2_msg {
148 Some(s) if !s.is_empty() => base64_decode(s),
149 _ => Ok(vec![]),
150 }
151}
152
153const STANDARD_ALPHABET: &[u8; 64] =
154 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
155
156fn base64_engine() -> &'static GeneralPurpose {
157 static ENGINE: OnceLock<GeneralPurpose> = OnceLock::new();
158 ENGINE.get_or_init(|| {
159 let cfg = GeneralPurposeConfig::new()
160 .with_decode_padding_mode(DecodePaddingMode::Indifferent)
161 .with_decode_allow_trailing_bits(true);
162 GeneralPurpose::new(&alphabet::STANDARD, cfg)
163 })
164}
165
166fn base64_decode(input: &str) -> ArbRpcResult<Vec<u8>> {
167 let stripped = input.trim_end_matches('=');
168 let body_len = stripped.len() & !3;
171 let tail = &stripped.as_bytes()[body_len..];
172 let body = if tail.len() == 1 {
173 let b = tail[0];
174 if !STANDARD_ALPHABET.contains(&b) {
175 return Err(RpcError::base64_decode(format!(
176 "invalid base64 character: {}",
177 b as char
178 )));
179 }
180 &stripped[..body_len]
181 } else {
182 stripped
183 };
184 base64_engine()
185 .decode(body)
186 .map_err(|e| RpcError::base64_decode(format!("invalid base64: {e}")))
187}
188
189#[async_trait::async_trait]
190impl<Provider, BP> NitroExecutionApiServer for NitroExecutionHandler<Provider, BP>
191where
192 Provider: BlockNumReader + BlockReaderIdExt + HeaderProvider + 'static,
193 BP: BlockProducer,
194{
195 async fn digest_message(
196 &self,
197 msg_idx: u64,
198 message: RpcMessageWithMetadata,
199 _message_for_prefetch: Option<RpcMessageWithMetadata>,
200 ) -> RpcResult<RpcMessageResult> {
201 let block_num = self.message_index_to_block_number(msg_idx);
202 let kind = message.message.header.kind;
203 info!(target: "nitroexecution", msg_idx, block_num, kind, "digestMessage called");
204
205 if kind == 11 {
209 let l2_msg = decode_l2_msg(&message.message.l2_msg)?;
210 self.block_producer
211 .cache_init_message(&l2_msg)
212 .map_err(RpcError::from)?;
213
214 let genesis_header = self
215 .get_header(self.genesis_block_num)?
216 .ok_or_else(|| RpcError::not_found("Genesis block not found for Init message"))?;
217 let send_root = Self::send_root_from_header(genesis_header.header());
218 info!(target: "nitroexecution", "Init message cached, returning genesis block");
219 return Ok(RpcMessageResult {
220 block_hash: genesis_header.hash(),
221 send_root,
222 });
223 }
224
225 if let Some(header) = self.get_header(block_num)? {
227 let send_root = Self::send_root_from_header(header.header());
228 debug!(target: "nitroexecution", block_num, ?send_root, "Block already exists");
229 return Ok(RpcMessageResult {
230 block_hash: header.hash(),
231 send_root,
232 });
233 }
234
235 let l2_msg = decode_l2_msg(&message.message.l2_msg)?;
236
237 let batch_data_stats = message
239 .message
240 .batch_data_tokens
241 .as_ref()
242 .map(|s| (s.length, s.nonzeros));
243
244 let input = BlockProductionInput {
246 kind,
247 sender: message.message.header.sender,
248 l1_block_number: message.message.header.block_number,
249 l1_timestamp: message.message.header.timestamp,
250 request_id: message.message.header.request_id,
251 l1_base_fee: message.message.header.base_fee_l1,
252 l2_msg,
253 delayed_messages_read: message.delayed_messages_read,
254 batch_gas_cost: message.message.batch_gas_cost,
255 batch_data_stats,
256 };
257
258 let result = self
259 .block_producer
260 .produce_block(msg_idx, input)
261 .await
262 .map_err(RpcError::from)?;
263
264 self.update_messages_behind();
265
266 Ok(RpcMessageResult {
267 block_hash: result.block_hash,
268 send_root: result.send_root,
269 })
270 }
271
272 async fn reorg(
273 &self,
274 msg_idx_of_first_msg_to_add: u64,
275 new_messages: Vec<RpcMessageWithMetadataAndBlockInfo>,
276 _old_messages: Vec<RpcMessageWithMetadata>,
277 ) -> RpcResult<Vec<RpcMessageResult>> {
278 info!(
279 target: "nitroexecution",
280 msg_idx_of_first_msg_to_add,
281 new_msgs = new_messages.len(),
282 "reorg"
283 );
284
285 let target_block = msg_idx_of_first_msg_to_add
290 .saturating_sub(1)
291 .saturating_add(self.genesis_block_num);
292
293 self.block_producer
294 .reset_to_block(target_block)
295 .await
296 .map_err(RpcError::from)?;
297
298 let mut results = Vec::with_capacity(new_messages.len());
300 for (i, wrapped) in new_messages.into_iter().enumerate() {
301 let msg_idx = msg_idx_of_first_msg_to_add + i as u64;
302 let meta = wrapped.message;
303 let l2_msg = decode_l2_msg(&meta.message.l2_msg)?;
304 let batch_data_stats = meta
305 .message
306 .batch_data_tokens
307 .as_ref()
308 .map(|s| (s.length, s.nonzeros));
309 let input = BlockProductionInput {
310 kind: meta.message.header.kind,
311 sender: meta.message.header.sender,
312 l1_block_number: meta.message.header.block_number,
313 l1_timestamp: meta.message.header.timestamp,
314 request_id: meta.message.header.request_id,
315 l1_base_fee: meta.message.header.base_fee_l1,
316 l2_msg,
317 delayed_messages_read: meta.delayed_messages_read,
318 batch_gas_cost: meta.message.batch_gas_cost,
319 batch_data_stats,
320 };
321 let produced = self
322 .block_producer
323 .produce_block(msg_idx, input)
324 .await
325 .map_err(RpcError::from)?;
326 results.push(RpcMessageResult {
327 block_hash: produced.block_hash,
328 send_root: produced.send_root,
329 });
330 }
331 Ok(results)
332 }
333
334 async fn head_message_index(&self) -> RpcResult<u64> {
335 let best = self.provider.best_block_number().map_err(RpcError::from)?;
336
337 let msg_idx = self.block_number_to_message_index(best).unwrap_or(0);
338 debug!(target: "nitroexecution", best, msg_idx, "headMessageIndex");
339 Ok(msg_idx)
340 }
341
342 async fn result_at_message_index(&self, msg_idx: u64) -> RpcResult<RpcMessageResult> {
343 let block_num = self.message_index_to_block_number(msg_idx);
344
345 let header = self
346 .get_header(block_num)?
347 .ok_or_else(|| RpcError::not_found(format!("Block {block_num} not found")))?;
348
349 let send_root = Self::send_root_from_header(header.header());
350
351 Ok(RpcMessageResult {
352 block_hash: header.hash(),
353 send_root,
354 })
355 }
356
357 fn set_finality_data(
358 &self,
359 safe: Option<RpcFinalityData>,
360 finalized: Option<RpcFinalityData>,
361 validated: Option<RpcFinalityData>,
362 ) -> RpcResult<()> {
363 debug!(target: "nitroexecution", ?safe, ?finalized, ?validated, "setFinalityData");
364 self.block_producer
365 .set_finality(
366 safe.map(|f| f.block_hash),
367 finalized.map(|f| f.block_hash),
368 validated.map(|f| f.block_hash),
369 )
370 .map_err(RpcError::from)?;
371 Ok(())
372 }
373
374 fn set_consensus_sync_data(&self, sync_data: RpcConsensusSyncData) -> RpcResult<()> {
375 {
376 let mut state = self.state.write();
377 state.synced = sync_data.synced;
378 state.max_message_count = sync_data.max_message_count;
379 }
380 debug!(target: "nitroexecution", synced = sync_data.synced, max = sync_data.max_message_count, "setConsensusSyncData");
381 self.update_messages_behind();
382 Ok(())
383 }
384
385 fn mark_feed_start(&self, to: u64) -> RpcResult<()> {
386 debug!(target: "nitroexecution", to, "markFeedStart");
387 Ok(())
388 }
389
390 async fn trigger_maintenance(&self) -> RpcResult<()> {
391 Ok(())
392 }
393
394 async fn should_trigger_maintenance(&self) -> RpcResult<bool> {
395 Ok(false)
396 }
397
398 async fn maintenance_status(&self) -> RpcResult<RpcMaintenanceStatus> {
399 Ok(RpcMaintenanceStatus { is_running: false })
400 }
401
402 async fn arbos_version_for_message_index(&self, msg_idx: u64) -> RpcResult<u64> {
403 let block_num = self.message_index_to_block_number(msg_idx);
404
405 let header = self
406 .get_header(block_num)?
407 .ok_or_else(|| RpcError::not_found(format!("Block {block_num} not found")))?;
408
409 let mix = header.header().mix_hash().unwrap_or_default();
410 let arbos_version = u64::from_be_bytes(mix.0[16..24].try_into().unwrap_or_default());
411
412 Ok(arbos_version)
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use base64::engine::general_purpose::STANDARD as B64;
419
420 use super::*;
421
422 #[test]
423 fn decode_empty_option_is_ok() {
424 assert_eq!(decode_l2_msg(&None).unwrap(), Vec::<u8>::new());
425 assert_eq!(
426 decode_l2_msg(&Some(String::new())).unwrap(),
427 Vec::<u8>::new()
428 );
429 }
430
431 #[test]
432 fn decode_standard_padded() {
433 let encoded = B64.encode(b"Hello, world!");
434 let out = base64_decode(&encoded).unwrap();
435 assert_eq!(out, b"Hello, world!");
436 }
437
438 #[test]
439 fn decode_accepts_unpadded() {
440 let encoded = B64.encode(b"Hello");
441 let stripped = encoded.trim_end_matches('=').to_string();
442 assert_eq!(base64_decode(&stripped).unwrap(), b"Hello");
443 }
444
445 #[test]
446 fn decode_accepts_extra_padding() {
447 assert_eq!(base64_decode("SGVsbG8==").unwrap(), b"Hello");
448 assert_eq!(base64_decode("SGVsbG8====").unwrap(), b"Hello");
449 }
450
451 #[test]
452 fn decode_rejects_invalid_character() {
453 assert!(base64_decode("SG!X").is_err());
454 assert!(base64_decode("a b").is_err());
455 assert!(base64_decode("hello world").is_err());
456 }
457
458 #[test]
459 fn decode_rejects_padding_in_body() {
460 assert!(base64_decode("=SGVs").is_err());
461 assert!(base64_decode("SGVs=bG8").is_err());
462 }
463
464 #[test]
465 fn decode_large_payload_matches_roundtrip() {
466 let bytes: Vec<u8> = (0..32 * 1024).map(|i| (i * 7 + 3) as u8).collect();
467 let encoded = B64.encode(&bytes);
468 assert_eq!(base64_decode(&encoded).unwrap(), bytes);
469 }
470
471 #[test]
472 fn decode_preserves_lenient_padding_tail() {
473 assert_eq!(base64_decode("S").unwrap(), Vec::<u8>::new());
474 assert_eq!(base64_decode("SG").unwrap(), vec![b'H']);
475 assert_eq!(base64_decode("SGV").unwrap(), vec![b'H', b'e']);
476 }
477
478 #[test]
479 fn decode_length_one_tail_validates_alphabet() {
480 assert!(base64_decode("ABCD!").is_err());
481 }
482
483 #[test]
484 fn decode_all_alphabet_characters() {
485 let out = base64_decode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
486 .unwrap();
487 assert_eq!(out.len(), 48);
488 }
489}