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