arbos/arbos_types/
incoming_message.rs

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