1use std::io::{self, Cursor, Read};
2
3use alloy_eips::eip2718::{Decodable2718, Typed2718};
4use alloy_primitives::{Address, B256, Bytes, U256, keccak256};
5use arb_primitives::{
6 signed_tx::ArbTransactionSigned,
7 tx_types::{ArbContractTx, ArbDepositTx, ArbSubmitRetryableTx, ArbUnsignedTx},
8};
9
10use crate::{
11 arbos_types::{
12 L1_MESSAGE_TYPE_BATCH_FOR_GAS_ESTIMATION, L1_MESSAGE_TYPE_BATCH_POSTING_REPORT,
13 L1_MESSAGE_TYPE_END_OF_BLOCK, L1_MESSAGE_TYPE_ETH_DEPOSIT, L1_MESSAGE_TYPE_INITIALIZE,
14 L1_MESSAGE_TYPE_L2_FUNDED_BY_L1, L1_MESSAGE_TYPE_L2_MESSAGE, L1_MESSAGE_TYPE_ROLLUP_EVENT,
15 L1_MESSAGE_TYPE_SUBMIT_RETRYABLE,
16 },
17 util::{
18 address_from_256_from_reader, address_from_reader, bytestring_from_reader,
19 hash_from_reader, uint64_from_reader, uint256_from_reader,
20 },
21};
22
23pub const L2_MESSAGE_KIND_UNSIGNED_USER_TX: u8 = 0;
25pub const L2_MESSAGE_KIND_CONTRACT_TX: u8 = 1;
26pub const L2_MESSAGE_KIND_NON_MUTATING_CALL: u8 = 2;
27pub const L2_MESSAGE_KIND_BATCH: u8 = 3;
28pub const L2_MESSAGE_KIND_SIGNED_TX: u8 = 4;
29pub const L2_MESSAGE_KIND_HEARTBEAT: u8 = 6;
30pub const L2_MESSAGE_KIND_SIGNED_COMPRESSED_TX: u8 = 7;
31
32pub const HEARTBEATS_DISABLED_AT: u64 = 6;
34
35pub const MAX_L2_MESSAGE_SIZE: usize = 256 * 1024;
37
38#[derive(Debug, Clone)]
40pub enum ParsedTransaction {
41 Signed(Vec<u8>),
43 UnsignedUserTx {
45 from: Address,
46 to: Option<Address>,
47 value: U256,
48 gas: u64,
49 gas_fee_cap: U256,
50 nonce: u64,
51 data: Vec<u8>,
52 },
53 ContractTx {
55 from: Address,
56 to: Option<Address>,
57 value: U256,
58 gas: u64,
59 gas_fee_cap: U256,
60 data: Vec<u8>,
61 request_id: B256,
62 },
63 EthDeposit {
65 from: Address,
66 to: Address,
67 value: U256,
68 request_id: B256,
69 },
70 SubmitRetryable {
72 request_id: B256,
73 l1_base_fee: U256,
74 deposit: U256,
75 callvalue: U256,
76 gas_feature_cap: U256,
77 gas_limit: u64,
78 max_submission_fee: U256,
79 from: Address,
80 to: Option<Address>,
81 fee_refund_addr: Address,
82 beneficiary: Address,
83 data: Vec<u8>,
84 },
85 BatchPostingReport {
87 batch_timestamp: u64,
88 batch_poster: Address,
89 data_hash: B256,
90 batch_number: u64,
91 l1_base_fee_estimate: U256,
92 extra_gas: u64,
93 },
94 InternalStartBlock {
96 l1_block_number: u64,
97 l1_timestamp: u64,
98 },
99}
100
101pub fn parse_l2_transactions(
103 kind: u8,
104 poster: Address,
105 l2_msg: &[u8],
106 request_id: Option<B256>,
107 l1_base_fee: Option<U256>,
108 chain_id: u64,
109) -> Result<Vec<ParsedTransaction>, io::Error> {
110 if l2_msg.len() > MAX_L2_MESSAGE_SIZE {
111 return Err(io::Error::new(
112 io::ErrorKind::InvalidData,
113 "message too large",
114 ));
115 }
116 match kind {
117 L1_MESSAGE_TYPE_L2_MESSAGE => parse_l2_message(l2_msg, poster, request_id, 0, chain_id),
118 L1_MESSAGE_TYPE_END_OF_BLOCK => Ok(vec![]),
119 L1_MESSAGE_TYPE_L2_FUNDED_BY_L1 => {
120 let request_id = request_id.ok_or_else(|| {
121 io::Error::new(
122 io::ErrorKind::InvalidData,
123 "cannot issue L2 funded by L1 tx without L1 request id",
124 )
125 })?;
126 parse_l2_funded_by_l1(l2_msg, poster, request_id)
127 }
128 L1_MESSAGE_TYPE_SUBMIT_RETRYABLE => {
129 let request_id = request_id.ok_or_else(|| {
130 io::Error::new(
131 io::ErrorKind::InvalidData,
132 "cannot issue submit retryable tx without L1 request id",
133 )
134 })?;
135 let l1_base_fee = l1_base_fee.unwrap_or(U256::ZERO);
136 parse_submit_retryable_message(l2_msg, poster, request_id, l1_base_fee)
137 }
138 L1_MESSAGE_TYPE_ETH_DEPOSIT => {
139 let request_id = request_id.ok_or_else(|| {
140 io::Error::new(
141 io::ErrorKind::InvalidData,
142 "cannot issue deposit tx without L1 request id",
143 )
144 })?;
145 parse_eth_deposit_message(l2_msg, poster, request_id)
146 }
147 L1_MESSAGE_TYPE_BATCH_POSTING_REPORT => {
148 let request_id = request_id.unwrap_or(B256::ZERO);
149 parse_batch_posting_report(l2_msg, poster, request_id)
150 }
151 L1_MESSAGE_TYPE_BATCH_FOR_GAS_ESTIMATION => Err(io::Error::new(
152 io::ErrorKind::InvalidData,
153 "L1 message type BatchForGasEstimation is unimplemented",
154 )),
155 L1_MESSAGE_TYPE_INITIALIZE | L1_MESSAGE_TYPE_ROLLUP_EVENT => Ok(vec![]),
156 _ => Ok(vec![]),
157 }
158}
159
160const MAX_L2_MESSAGE_BATCH_DEPTH: u32 = 16;
162
163#[allow(clippy::only_used_in_recursion)]
164fn parse_l2_message(
165 data: &[u8],
166 poster: Address,
167 request_id: Option<B256>,
168 depth: u32,
169 chain_id: u64,
170) -> Result<Vec<ParsedTransaction>, io::Error> {
171 if data.is_empty() {
172 return Err(io::Error::new(
173 io::ErrorKind::UnexpectedEof,
174 "L2 message is empty (missing kind byte)",
175 ));
176 }
177
178 let kind = data[0];
179 let payload = &data[1..];
180
181 match kind {
182 L2_MESSAGE_KIND_SIGNED_COMPRESSED_TX => Err(io::Error::new(
183 io::ErrorKind::InvalidData,
184 "L2 message kind SignedCompressedTx is unimplemented",
185 )),
186 L2_MESSAGE_KIND_SIGNED_TX => {
187 match ArbTransactionSigned::decode_2718(&mut &payload[..]) {
191 Ok(tx) => {
192 let ty = tx.ty();
193 if ty >= 0x64 || ty == 3 {
194 return Err(io::Error::new(
195 io::ErrorKind::InvalidData,
196 format!("unsupported tx type: {ty}"),
197 ));
198 }
199 Ok(vec![ParsedTransaction::Signed(payload.to_vec())])
200 }
201 Err(_) => Err(io::Error::new(
202 io::ErrorKind::InvalidData,
203 "failed to decode signed transaction",
204 )),
205 }
206 }
207 L2_MESSAGE_KIND_UNSIGNED_USER_TX => {
208 let tx = parse_unsigned_tx(payload, poster, request_id, kind)?;
209 Ok(vec![tx])
210 }
211 L2_MESSAGE_KIND_CONTRACT_TX => {
212 let tx = parse_unsigned_tx(payload, poster, request_id, kind)?;
213 Ok(vec![tx])
214 }
215 L2_MESSAGE_KIND_BATCH => {
216 if depth >= MAX_L2_MESSAGE_BATCH_DEPTH {
217 return Err(io::Error::new(
218 io::ErrorKind::InvalidData,
219 "L2 message batches have a max depth of 16",
220 ));
221 }
222 let mut reader = Cursor::new(payload);
223 let mut txs = Vec::new();
224 let mut index: u64 = 0;
225 while let Ok(segment) = bytestring_from_reader(&mut reader, MAX_L2_MESSAGE_SIZE as u64)
226 {
227 if segment.len() > MAX_L2_MESSAGE_SIZE {
228 break;
229 }
230 let sub_request_id = request_id.map(|parent_id| {
231 let mut preimage = [0u8; 64];
232 preimage[..32].copy_from_slice(parent_id.as_slice());
233 preimage[32..].copy_from_slice(&U256::from(index).to_be_bytes::<32>());
234 B256::from(keccak256(preimage))
235 });
236 index += 1;
237 let mut sub_txs =
238 parse_l2_message(&segment, poster, sub_request_id, depth + 1, chain_id)?;
239 txs.append(&mut sub_txs);
240 }
241 Ok(txs)
242 }
243 L2_MESSAGE_KIND_HEARTBEAT => Ok(vec![]),
244 L2_MESSAGE_KIND_NON_MUTATING_CALL => Err(io::Error::new(
245 io::ErrorKind::InvalidData,
246 "L2 message kind NonmutatingCall is unimplemented",
247 )),
248 other => Err(io::Error::new(
249 io::ErrorKind::InvalidData,
250 format!("unknown L2 message kind {other}"),
251 )),
252 }
253}
254
255fn parse_unsigned_tx(
265 data: &[u8],
266 poster: Address,
267 request_id: Option<B256>,
268 kind: u8,
269) -> Result<ParsedTransaction, io::Error> {
270 let mut reader = Cursor::new(data);
271
272 let gas_limit = uint256_from_reader(&mut reader)?;
273 let gas_limit: u64 = gas_limit.try_into().map_err(|_| {
274 io::Error::new(
275 io::ErrorKind::InvalidData,
276 "unsigned user tx gas limit >= 2^64",
277 )
278 })?;
279
280 let max_fee_per_gas = uint256_from_reader(&mut reader)?;
281
282 let nonce = if kind == L2_MESSAGE_KIND_UNSIGNED_USER_TX {
283 let nonce_u256 = uint256_from_reader(&mut reader)?;
284 let n: u64 = nonce_u256.try_into().map_err(|_| {
285 io::Error::new(io::ErrorKind::InvalidData, "unsigned user tx nonce >= 2^64")
286 })?;
287 n
288 } else {
289 0
290 };
291
292 let to = address_from_256_from_reader(&mut reader)?;
293 let destination = if to == Address::ZERO { None } else { Some(to) };
294
295 let value = uint256_from_reader(&mut reader)?;
296
297 let mut calldata = Vec::new();
298 reader.read_to_end(&mut calldata)?;
299
300 match kind {
301 L2_MESSAGE_KIND_UNSIGNED_USER_TX => Ok(ParsedTransaction::UnsignedUserTx {
302 from: poster,
303 to: destination,
304 value,
305 gas: gas_limit,
306 gas_fee_cap: max_fee_per_gas,
307 nonce,
308 data: calldata,
309 }),
310 L2_MESSAGE_KIND_CONTRACT_TX => {
311 let req_id = request_id.ok_or_else(|| {
312 io::Error::new(
313 io::ErrorKind::InvalidData,
314 "cannot issue contract tx without L1 request id",
315 )
316 })?;
317 Ok(ParsedTransaction::ContractTx {
318 from: poster,
319 to: destination,
320 value,
321 gas: gas_limit,
322 gas_fee_cap: max_fee_per_gas,
323 data: calldata,
324 request_id: req_id,
325 })
326 }
327 _ => Err(io::Error::new(
328 io::ErrorKind::InvalidData,
329 "invalid L2 tx type in parseUnsignedTx",
330 )),
331 }
332}
333
334fn parse_l2_funded_by_l1(
335 data: &[u8],
336 poster: Address,
337 request_id: B256,
338) -> Result<Vec<ParsedTransaction>, io::Error> {
339 if data.is_empty() {
340 return Err(io::Error::new(
341 io::ErrorKind::InvalidData,
342 "L2FundedByL1 message has no data",
343 ));
344 }
345
346 let kind = data[0];
347
348 let mut deposit_preimage = [0u8; 64];
350 deposit_preimage[..32].copy_from_slice(request_id.as_slice());
351 let deposit_request_id = B256::from(keccak256(deposit_preimage));
353
354 let mut unsigned_preimage = [0u8; 64];
355 unsigned_preimage[..32].copy_from_slice(request_id.as_slice());
356 unsigned_preimage[63] = 1; let unsigned_request_id = B256::from(keccak256(unsigned_preimage));
358
359 let tx = parse_unsigned_tx(&data[1..], poster, Some(unsigned_request_id), kind)?;
360
361 let tx_value = match &tx {
363 ParsedTransaction::UnsignedUserTx { value, .. } => *value,
364 ParsedTransaction::ContractTx { value, .. } => *value,
365 _ => U256::ZERO,
366 };
367
368 let deposit = ParsedTransaction::EthDeposit {
370 from: Address::ZERO,
371 to: poster,
372 value: tx_value,
373 request_id: deposit_request_id,
374 };
375
376 Ok(vec![deposit, tx])
377}
378
379fn parse_eth_deposit_message(
380 data: &[u8],
381 poster: Address,
382 request_id: B256,
383) -> Result<Vec<ParsedTransaction>, io::Error> {
384 let mut reader = Cursor::new(data);
385 let to = address_from_reader(&mut reader)?;
386 let value = uint256_from_reader(&mut reader)?;
387 Ok(vec![ParsedTransaction::EthDeposit {
388 from: poster,
389 to,
390 value,
391 request_id,
392 }])
393}
394
395fn parse_submit_retryable_message(
396 data: &[u8],
397 poster: Address,
398 request_id: B256,
399 l1_base_fee: U256,
400) -> Result<Vec<ParsedTransaction>, io::Error> {
401 let mut reader = Cursor::new(data);
402
403 let retry_to = address_from_256_from_reader(&mut reader)?;
405 let callvalue = uint256_from_reader(&mut reader)?;
406 let deposit = uint256_from_reader(&mut reader)?;
407 let max_submission_fee = uint256_from_reader(&mut reader)?;
408 let fee_refund_addr = address_from_256_from_reader(&mut reader)?;
409 let beneficiary = address_from_256_from_reader(&mut reader)?;
410 let gas_limit_u256 = uint256_from_reader(&mut reader)?;
411 let gas_limit = gas_limit_u256
412 .try_into()
413 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "gas limit too large"))?;
414 let gas_feature_cap = uint256_from_reader(&mut reader)?;
415
416 let data_length_hash = hash_from_reader(&mut reader)?;
420 let data_length: usize = U256::from_be_bytes(data_length_hash.0)
421 .try_into()
422 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "data length too large"))?;
423 if data_length > MAX_L2_MESSAGE_SIZE {
424 return Err(io::Error::new(
425 io::ErrorKind::InvalidData,
426 format!("data length {data_length} exceeds MAX_L2_MESSAGE_SIZE {MAX_L2_MESSAGE_SIZE}"),
427 ));
428 }
429 let mut calldata = vec![0u8; data_length];
430 if data_length > 0 {
431 let read = io::Read::read(&mut reader, &mut calldata)?;
432 if read == 0 {
433 return Err(io::Error::new(
434 io::ErrorKind::UnexpectedEof,
435 "missing retry data",
436 ));
437 }
438 }
439
440 let to = if retry_to == Address::ZERO {
441 None
442 } else {
443 Some(retry_to)
444 };
445
446 Ok(vec![ParsedTransaction::SubmitRetryable {
447 request_id,
448 l1_base_fee,
449 deposit,
450 callvalue,
451 gas_feature_cap,
452 gas_limit,
453 max_submission_fee,
454 from: poster,
455 to,
456 fee_refund_addr,
457 beneficiary,
458 data: calldata,
459 }])
460}
461
462fn parse_batch_posting_report(
463 data: &[u8],
464 _poster: Address,
465 _request_id: B256,
466) -> Result<Vec<ParsedTransaction>, io::Error> {
467 let mut reader = Cursor::new(data);
468
469 let batch_timestamp_u256 = uint256_from_reader(&mut reader)?;
472 let batch_timestamp: u64 = batch_timestamp_u256
473 .try_into()
474 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch timestamp too large"))?;
475
476 let batch_poster = address_from_reader(&mut reader)?;
477
478 let data_hash = hash_from_reader(&mut reader)?;
479
480 let batch_number_u256 = uint256_from_reader(&mut reader)?;
481 let batch_number: u64 = batch_number_u256
482 .try_into()
483 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch number too large"))?;
484
485 let l1_base_fee_estimate = uint256_from_reader(&mut reader)?;
486
487 let extra_gas = match uint64_from_reader(&mut reader) {
489 Ok(v) => v,
490 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => 0,
491 Err(e) => return Err(e),
492 };
493
494 Ok(vec![ParsedTransaction::BatchPostingReport {
495 batch_timestamp,
496 batch_poster,
497 data_hash,
498 batch_number,
499 l1_base_fee_estimate,
500 extra_gas,
501 }])
502}
503
504pub fn parsed_tx_to_signed(
514 parsed: &ParsedTransaction,
515 chain_id: u64,
516) -> Option<ArbTransactionSigned> {
517 use arb_primitives::signed_tx::ArbTypedTransaction;
518
519 let chain_id_u256 = U256::from(chain_id);
520
521 let tx = match parsed {
522 ParsedTransaction::Signed(rlp_bytes) => {
523 use alloy_eips::Decodable2718;
525 return ArbTransactionSigned::decode_2718(&mut rlp_bytes.as_slice()).ok();
526 }
527 ParsedTransaction::UnsignedUserTx {
528 from,
529 to,
530 value,
531 gas,
532 gas_fee_cap,
533 nonce,
534 data,
535 } => ArbTypedTransaction::Unsigned(ArbUnsignedTx {
536 chain_id: chain_id_u256,
537 from: *from,
538 nonce: *nonce,
539 gas_fee_cap: *gas_fee_cap,
540 gas: *gas,
541 to: *to,
542 value: *value,
543 data: Bytes::copy_from_slice(data),
544 }),
545 ParsedTransaction::ContractTx {
546 from,
547 to,
548 value,
549 gas,
550 gas_fee_cap,
551 data,
552 request_id,
553 } => ArbTypedTransaction::Contract(ArbContractTx {
554 chain_id: chain_id_u256,
555 request_id: *request_id,
556 from: *from,
557 gas_fee_cap: *gas_fee_cap,
558 gas: *gas,
559 to: *to,
560 value: *value,
561 data: Bytes::copy_from_slice(data),
562 }),
563 ParsedTransaction::EthDeposit {
564 from,
565 to,
566 value,
567 request_id,
568 } => ArbTypedTransaction::Deposit(ArbDepositTx {
569 chain_id: chain_id_u256,
570 l1_request_id: *request_id,
571 from: *from,
572 to: *to,
573 value: *value,
574 }),
575 ParsedTransaction::SubmitRetryable {
576 request_id,
577 l1_base_fee,
578 deposit,
579 callvalue,
580 gas_feature_cap,
581 gas_limit,
582 max_submission_fee,
583 from,
584 to,
585 fee_refund_addr,
586 beneficiary,
587 data,
588 } => ArbTypedTransaction::SubmitRetryable(ArbSubmitRetryableTx {
589 chain_id: chain_id_u256,
590 request_id: *request_id,
591 from: *from,
592 l1_base_fee: *l1_base_fee,
593 deposit_value: *deposit,
594 gas_fee_cap: *gas_feature_cap,
595 gas: *gas_limit,
596 retry_to: *to,
597 retry_value: *callvalue,
598 beneficiary: *beneficiary,
599 max_submission_fee: *max_submission_fee,
600 fee_refund_addr: *fee_refund_addr,
601 retry_data: Bytes::copy_from_slice(data),
602 }),
603 ParsedTransaction::BatchPostingReport { .. } => {
604 return None;
607 }
608 ParsedTransaction::InternalStartBlock { .. } => {
609 return None;
611 }
612 };
613
614 let sig = alloy_primitives::Signature::new(U256::ZERO, U256::ZERO, false);
615 Some(ArbTransactionSigned::new_unhashed(tx, sig))
616}