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.
158pub fn parse_init_message(data: &[u8]) -> io::Result<ParsedInitMessage> {
159    if data.is_empty() {
160        return Ok(ParsedInitMessage {
161            chain_id: U256::ZERO,
162            initial_l1_base_fee: U256::from(DEFAULT_INITIAL_L1_BASE_FEE),
163            serialized_chain_config: Vec::new(),
164        });
165    }
166
167    let mut reader = Cursor::new(data);
168
169    // Version byte
170    let mut version_buf = [0u8; 1];
171    reader.read_exact(&mut version_buf)?;
172    let version = version_buf[0];
173
174    match version {
175        0 => {
176            let chain_id = uint256_from_reader(&mut reader)?;
177            let mut serialized_chain_config = Vec::new();
178            reader.read_to_end(&mut serialized_chain_config)?;
179            Ok(ParsedInitMessage {
180                chain_id,
181                initial_l1_base_fee: U256::from(DEFAULT_INITIAL_L1_BASE_FEE),
182                serialized_chain_config,
183            })
184        }
185        1 => {
186            let chain_id = uint256_from_reader(&mut reader)?;
187            let initial_l1_base_fee = uint256_from_reader(&mut reader)?;
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,
193                serialized_chain_config,
194            })
195        }
196        _ => Err(io::Error::new(
197            io::ErrorKind::InvalidData,
198            format!("unsupported init message version: {version}"),
199        )),
200    }
201}
202
203/// Returns data statistics (total bytes and non-zero byte count).
204pub fn get_data_stats(data: &[u8]) -> BatchDataStats {
205    let non_zeros = data.iter().filter(|&&b| b != 0).count() as u64;
206    BatchDataStats {
207        length: data.len() as u64,
208        non_zeros,
209    }
210}
211
212/// Estimates L1 gas cost using legacy pricing model.
213pub fn legacy_cost_for_stats(stats: &BatchDataStats) -> u64 {
214    let zeros = stats.length.saturating_sub(stats.non_zeros);
215    // Calldata gas: 4 gas per zero byte, 16 gas per non-zero byte.
216    let mut gas = zeros * 4 + stats.non_zeros * 16;
217    // Poster also pays to keccak the batch and write a batch posting report.
218    let keccak_words = stats.length.div_ceil(32);
219    gas += 30 + keccak_words * 6; // Keccak256Gas + words * Keccak256WordGas
220    gas += 2 * 20_000; // 2 × SstoreSetGasEIP2200
221    gas
222}
223
224/// Parses fields from a batch posting report message.
225pub fn parse_batch_posting_report_fields(data: &[u8]) -> io::Result<BatchPostingReportFields> {
226    let mut reader = Cursor::new(data);
227
228    let batch_timestamp_u256 = uint256_from_reader(&mut reader)?;
229    let batch_timestamp: u64 = batch_timestamp_u256
230        .try_into()
231        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch timestamp too large"))?;
232
233    let batch_poster = address_from_reader(&mut reader)?;
234    let data_hash = hash_from_reader(&mut reader)?;
235
236    let batch_number_u256 = uint256_from_reader(&mut reader)?;
237    let batch_number: u64 = batch_number_u256
238        .try_into()
239        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch number too large"))?;
240
241    let l1_base_fee_estimate = uint256_from_reader(&mut reader)?;
242
243    let extra_gas = match uint64_from_reader(&mut reader) {
244        Ok(v) => v,
245        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => 0,
246        Err(e) => return Err(e),
247    };
248
249    Ok(BatchPostingReportFields {
250        batch_timestamp,
251        batch_poster,
252        data_hash,
253        batch_number,
254        l1_base_fee_estimate,
255        extra_gas,
256    })
257}
258
259/// Fields extracted from a batch posting report.
260#[derive(Debug, Clone)]
261pub struct BatchPostingReportFields {
262    pub batch_timestamp: u64,
263    pub batch_poster: Address,
264    pub data_hash: B256,
265    pub batch_number: u64,
266    pub l1_base_fee_estimate: U256,
267    pub extra_gas: u64,
268}