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