arbos/arbos_types/
incoming_message.rs

1use alloy_primitives::{Address, B256, U256};
2use std::io::{self, Cursor, Read};
3
4use crate::util::{
5    address_from_256_from_reader, address_from_reader, hash_from_reader, uint256_from_reader,
6    uint64_from_reader,
7};
8
9/// L1 message type constants.
10pub const L1_MESSAGE_TYPE_L2_MESSAGE: u8 = 3;
11pub const L1_MESSAGE_TYPE_END_OF_BLOCK: u8 = 6;
12pub const L1_MESSAGE_TYPE_L2_FUNDED_BY_L1: u8 = 7;
13pub const L1_MESSAGE_TYPE_ROLLUP_EVENT: u8 = 8;
14pub const L1_MESSAGE_TYPE_SUBMIT_RETRYABLE: u8 = 9;
15pub const L1_MESSAGE_TYPE_BATCH_FOR_GAS_ESTIMATION: u8 = 10;
16pub const L1_MESSAGE_TYPE_INITIALIZE: u8 = 11;
17pub const L1_MESSAGE_TYPE_ETH_DEPOSIT: u8 = 12;
18pub const L1_MESSAGE_TYPE_BATCH_POSTING_REPORT: u8 = 13;
19pub const L1_MESSAGE_TYPE_INVALID: u8 = 0xFF;
20
21/// Maximum size of an L2 message payload.
22pub const MAX_L2_MESSAGE_SIZE: usize = 256 * 1024;
23
24/// Default initial L1 base fee (used when chain config doesn't specify one).
25pub const DEFAULT_INITIAL_L1_BASE_FEE: u64 = 50_000_000_000; // 50 Gwei
26
27/// Header of an L1 incoming message.
28#[derive(Debug, Clone)]
29pub struct L1IncomingMessageHeader {
30    pub kind: u8,
31    pub poster: Address,
32    pub block_number: u64,
33    pub timestamp: u64,
34    pub request_id: Option<B256>,
35    pub l1_base_fee: Option<U256>,
36}
37
38/// Statistics about a batch of data (for L1 cost estimation).
39#[derive(Debug, Clone, Copy, Default)]
40pub struct BatchDataStats {
41    pub length: u64,
42    pub non_zeros: u64,
43}
44
45/// An L1 incoming message containing the header and L2 payload.
46#[derive(Debug, Clone)]
47pub struct L1IncomingMessage {
48    pub header: L1IncomingMessageHeader,
49    pub l2_msg: Vec<u8>,
50    /// Batch-level gas cost fields (filled lazily).
51    pub batch_gas_left: Option<u64>,
52}
53
54/// Parsed initialization message from the first L1 message.
55#[derive(Debug, Clone)]
56pub struct ParsedInitMessage {
57    pub chain_id: U256,
58    pub initial_l1_base_fee: U256,
59    /// Serialized chain config JSON bytes (stored in ArbOS state).
60    pub serialized_chain_config: Vec<u8>,
61}
62
63impl L1IncomingMessageHeader {
64    /// Extracts the sequence number from the RequestId.
65    pub fn seq_num(&self) -> Option<u64> {
66        self.request_id.map(|id| {
67            let bytes = id.as_slice();
68            u64::from_be_bytes(bytes[24..32].try_into().unwrap_or([0; 8]))
69        })
70    }
71}
72
73impl L1IncomingMessage {
74    /// Returns batch numbers this message depends on.
75    ///
76    /// Only BatchPostingReport messages reference past batches; all other
77    /// message types return an empty list.
78    pub fn past_batches_required(&self) -> io::Result<Vec<u64>> {
79        if self.header.kind != L1_MESSAGE_TYPE_BATCH_POSTING_REPORT {
80            return Ok(Vec::new());
81        }
82        let fields = parse_batch_posting_report_fields(&self.l2_msg)?;
83        Ok(vec![fields.batch_number])
84    }
85
86    /// Serializes this message to bytes.
87    pub fn serialize(&self) -> Vec<u8> {
88        let mut buf = Vec::new();
89        buf.push(self.header.kind);
90        // poster (32 bytes, left-padded address)
91        buf.extend_from_slice(B256::left_padding_from(self.header.poster.as_slice()).as_slice());
92        // block number (8 bytes BE)
93        buf.extend_from_slice(&self.header.block_number.to_be_bytes());
94        // timestamp (8 bytes BE)
95        buf.extend_from_slice(&self.header.timestamp.to_be_bytes());
96        // request id (32 bytes, zero if none)
97        match &self.header.request_id {
98            Some(id) => buf.extend_from_slice(id.as_slice()),
99            None => buf.extend_from_slice(&[0u8; 32]),
100        }
101        // l1 base fee (32 bytes BE, zero if none)
102        match &self.header.l1_base_fee {
103            Some(fee) => buf.extend_from_slice(&fee.to_be_bytes::<32>()),
104            None => buf.extend_from_slice(&[0u8; 32]),
105        }
106        // l2 msg
107        buf.extend_from_slice(&self.l2_msg);
108        buf
109    }
110}
111
112/// Parses an L1 incoming message from raw bytes.
113pub fn parse_incoming_l1_message(data: &[u8]) -> io::Result<L1IncomingMessage> {
114    if data.is_empty() {
115        return Err(io::Error::new(io::ErrorKind::InvalidData, "empty message"));
116    }
117    let mut reader = Cursor::new(data);
118
119    let mut kind_buf = [0u8; 1];
120    reader.read_exact(&mut kind_buf)?;
121    let kind = kind_buf[0];
122
123    let poster = address_from_256_from_reader(&mut reader)?;
124    let block_number = uint64_from_reader(&mut reader)?;
125    let timestamp = uint64_from_reader(&mut reader)?;
126    let request_id = hash_from_reader(&mut reader)?;
127    let l1_base_fee = uint256_from_reader(&mut reader)?;
128
129    let request_id = if request_id == B256::ZERO {
130        None
131    } else {
132        Some(request_id)
133    };
134    let l1_base_fee = if l1_base_fee == U256::ZERO {
135        None
136    } else {
137        Some(l1_base_fee)
138    };
139
140    let mut l2_msg = Vec::new();
141    reader.read_to_end(&mut l2_msg)?;
142
143    Ok(L1IncomingMessage {
144        header: L1IncomingMessageHeader {
145            kind,
146            poster,
147            block_number,
148            timestamp,
149            request_id,
150            l1_base_fee,
151        },
152        l2_msg,
153        batch_gas_left: None,
154    })
155}
156
157/// Parses an initialization message to extract chain ID and initial L1 base fee.
158///
159/// Matches Nitro `L1IncomingMessage.ParseInitMessage`:
160///   - len == 32: chain_id only (32 bytes), default base fee, no chain config
161///   - len > 32: chain_id (32) || version (1 byte) || version-specific tail
162///   - version 0: chain_config (rest), default base fee
163///   - version 1: l1_base_fee (32) || chain_config (rest)
164///   - any other length (including empty): error
165pub fn parse_init_message(data: &[u8]) -> io::Result<ParsedInitMessage> {
166    let default_base_fee = U256::from(DEFAULT_INITIAL_L1_BASE_FEE);
167
168    if data.len() == 32 {
169        return Ok(ParsedInitMessage {
170            chain_id: U256::from_be_slice(data),
171            initial_l1_base_fee: default_base_fee,
172            serialized_chain_config: Vec::new(),
173        });
174    }
175    if data.len() < 33 {
176        return Err(io::Error::new(
177            io::ErrorKind::InvalidData,
178            format!("invalid init message length: {}", data.len()),
179        ));
180    }
181
182    let chain_id = U256::from_be_slice(&data[..32]);
183    let version = data[32];
184    let mut reader = Cursor::new(&data[33..]);
185
186    match version {
187        0 => {
188            let mut serialized_chain_config = Vec::new();
189            reader.read_to_end(&mut serialized_chain_config)?;
190            Ok(ParsedInitMessage {
191                chain_id,
192                initial_l1_base_fee: default_base_fee,
193                serialized_chain_config,
194            })
195        }
196        1 => {
197            let initial_l1_base_fee = uint256_from_reader(&mut reader)?;
198            let mut serialized_chain_config = Vec::new();
199            reader.read_to_end(&mut serialized_chain_config)?;
200            Ok(ParsedInitMessage {
201                chain_id,
202                initial_l1_base_fee,
203                serialized_chain_config,
204            })
205        }
206        _ => Err(io::Error::new(
207            io::ErrorKind::InvalidData,
208            format!("unsupported init message version: {version}"),
209        )),
210    }
211}
212
213/// Returns data statistics (total bytes and non-zero byte count).
214pub fn get_data_stats(data: &[u8]) -> BatchDataStats {
215    let non_zeros = data.iter().filter(|&&b| b != 0).count() as u64;
216    BatchDataStats {
217        length: data.len() as u64,
218        non_zeros,
219    }
220}
221
222/// Estimates L1 gas cost using legacy pricing model.
223pub fn legacy_cost_for_stats(stats: &BatchDataStats) -> u64 {
224    let zeros = stats.length.saturating_sub(stats.non_zeros);
225    // Calldata gas: 4 gas per zero byte, 16 gas per non-zero byte.
226    let mut gas = zeros * 4 + stats.non_zeros * 16;
227    // Poster also pays to keccak the batch and write a batch posting report.
228    let keccak_words = stats.length.div_ceil(32);
229    gas += 30 + keccak_words * 6; // Keccak256Gas + words * Keccak256WordGas
230    gas += 2 * 20_000; // 2 × SstoreSetGasEIP2200
231    gas
232}
233
234/// Parses fields from a batch posting report message.
235pub fn parse_batch_posting_report_fields(data: &[u8]) -> io::Result<BatchPostingReportFields> {
236    let mut reader = Cursor::new(data);
237
238    let batch_timestamp_u256 = uint256_from_reader(&mut reader)?;
239    let batch_timestamp: u64 = batch_timestamp_u256
240        .try_into()
241        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch timestamp too large"))?;
242
243    let batch_poster = address_from_reader(&mut reader)?;
244    let data_hash = hash_from_reader(&mut reader)?;
245
246    let batch_number_u256 = uint256_from_reader(&mut reader)?;
247    let batch_number: u64 = batch_number_u256
248        .try_into()
249        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch number too large"))?;
250
251    let l1_base_fee_estimate = uint256_from_reader(&mut reader)?;
252
253    let extra_gas = match uint64_from_reader(&mut reader) {
254        Ok(v) => v,
255        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => 0,
256        Err(e) => return Err(e),
257    };
258
259    Ok(BatchPostingReportFields {
260        batch_timestamp,
261        batch_poster,
262        data_hash,
263        batch_number,
264        l1_base_fee_estimate,
265        extra_gas,
266    })
267}
268
269/// Fields extracted from a batch posting report.
270#[derive(Debug, Clone)]
271pub struct BatchPostingReportFields {
272    pub batch_timestamp: u64,
273    pub batch_poster: Address,
274    pub data_hash: B256,
275    pub batch_number: u64,
276    pub l1_base_fee_estimate: U256,
277    pub extra_gas: u64,
278}