arbos_types/
incoming_message.rs

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