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.unwrap_or(B256::ZERO);
114 parse_l2_funded_by_l1(l2_msg, poster, request_id)
115 }
116 L1_MESSAGE_TYPE_SUBMIT_RETRYABLE => {
117 let request_id = request_id.unwrap_or(B256::ZERO);
118 let l1_base_fee = l1_base_fee.unwrap_or(U256::ZERO);
119 parse_submit_retryable_message(l2_msg, poster, request_id, l1_base_fee)
120 }
121 L1_MESSAGE_TYPE_ETH_DEPOSIT => {
122 let request_id = request_id.unwrap_or(B256::ZERO);
123 parse_eth_deposit_message(l2_msg, poster, request_id)
124 }
125 L1_MESSAGE_TYPE_BATCH_POSTING_REPORT => {
126 let request_id = request_id.unwrap_or(B256::ZERO);
127 parse_batch_posting_report(l2_msg, poster, request_id)
128 }
129 L1_MESSAGE_TYPE_BATCH_FOR_GAS_ESTIMATION => Err(io::Error::new(
130 io::ErrorKind::InvalidData,
131 "L1 message type BatchForGasEstimation is unimplemented",
132 )),
133 L1_MESSAGE_TYPE_INITIALIZE | L1_MESSAGE_TYPE_ROLLUP_EVENT => Ok(vec![]),
134 _ => Ok(vec![]),
135 }
136}
137
138#[allow(clippy::only_used_in_recursion)]
139fn parse_l2_message(
140 data: &[u8],
141 poster: Address,
142 request_id: Option<B256>,
143 depth: u32,
144 chain_id: u64,
145) -> Result<Vec<ParsedTransaction>, io::Error> {
146 const MAX_DEPTH: u32 = 16;
147 if depth > MAX_DEPTH || data.is_empty() {
148 return Ok(vec![]);
149 }
150
151 let kind = data[0];
152 let payload = &data[1..];
153
154 match kind {
155 L2_MESSAGE_KIND_SIGNED_COMPRESSED_TX => Err(io::Error::new(
156 io::ErrorKind::InvalidData,
157 "L2 message kind SignedCompressedTx is unimplemented",
158 )),
159 L2_MESSAGE_KIND_SIGNED_TX => {
160 match ArbTransactionSigned::decode_2718(&mut &payload[..]) {
164 Ok(tx) => {
165 let ty = tx.ty();
166 if ty >= 0x64 || ty == 3 {
167 return Err(io::Error::new(
168 io::ErrorKind::InvalidData,
169 format!("unsupported tx type: {ty}"),
170 ));
171 }
172 Ok(vec![ParsedTransaction::Signed(payload.to_vec())])
173 }
174 Err(_) => Err(io::Error::new(
175 io::ErrorKind::InvalidData,
176 "failed to decode signed transaction",
177 )),
178 }
179 }
180 L2_MESSAGE_KIND_UNSIGNED_USER_TX => {
181 let tx = parse_unsigned_tx(payload, poster, request_id, kind)?;
182 Ok(vec![tx])
183 }
184 L2_MESSAGE_KIND_CONTRACT_TX => {
185 let tx = parse_unsigned_tx(payload, poster, request_id, kind)?;
186 Ok(vec![tx])
187 }
188 L2_MESSAGE_KIND_BATCH => {
189 let mut reader = Cursor::new(payload);
190 let mut txs = Vec::new();
191 let mut index: u64 = 0;
192 while let Ok(segment) = bytestring_from_reader(&mut reader) {
193 if segment.len() > MAX_L2_MESSAGE_SIZE {
194 break;
195 }
196 let sub_request_id = request_id.map(|parent_id| {
198 let mut preimage = [0u8; 64];
199 preimage[..32].copy_from_slice(parent_id.as_slice());
200 preimage[32..].copy_from_slice(&U256::from(index).to_be_bytes::<32>());
201 B256::from(keccak256(preimage))
202 });
203 index += 1;
204 let mut sub_txs =
205 parse_l2_message(&segment, poster, sub_request_id, depth + 1, chain_id)?;
206 txs.append(&mut sub_txs);
207 }
208 Ok(txs)
209 }
210 L2_MESSAGE_KIND_HEARTBEAT => Ok(vec![]),
211 L2_MESSAGE_KIND_NON_MUTATING_CALL => Ok(vec![]),
212 _ => Ok(vec![]),
213 }
214}
215
216fn parse_unsigned_tx(
226 data: &[u8],
227 poster: Address,
228 request_id: Option<B256>,
229 kind: u8,
230) -> Result<ParsedTransaction, io::Error> {
231 let mut reader = Cursor::new(data);
232
233 let gas_limit = uint256_from_reader(&mut reader)?;
234 let gas_limit: u64 = gas_limit.try_into().map_err(|_| {
235 io::Error::new(
236 io::ErrorKind::InvalidData,
237 "unsigned user tx gas limit >= 2^64",
238 )
239 })?;
240
241 let max_fee_per_gas = uint256_from_reader(&mut reader)?;
242
243 let nonce = if kind == L2_MESSAGE_KIND_UNSIGNED_USER_TX {
244 let nonce_u256 = uint256_from_reader(&mut reader)?;
245 let n: u64 = nonce_u256.try_into().map_err(|_| {
246 io::Error::new(io::ErrorKind::InvalidData, "unsigned user tx nonce >= 2^64")
247 })?;
248 n
249 } else {
250 0
251 };
252
253 let to = address_from_256_from_reader(&mut reader)?;
254 let destination = if to == Address::ZERO { None } else { Some(to) };
255
256 let value = uint256_from_reader(&mut reader)?;
257
258 let mut calldata = Vec::new();
259 reader.read_to_end(&mut calldata)?;
260
261 match kind {
262 L2_MESSAGE_KIND_UNSIGNED_USER_TX => Ok(ParsedTransaction::UnsignedUserTx {
263 from: poster,
264 to: destination,
265 value,
266 gas: gas_limit,
267 gas_fee_cap: max_fee_per_gas,
268 nonce,
269 data: calldata,
270 }),
271 L2_MESSAGE_KIND_CONTRACT_TX => {
272 let req_id = request_id.ok_or_else(|| {
273 io::Error::new(
274 io::ErrorKind::InvalidData,
275 "cannot issue contract tx without L1 request id",
276 )
277 })?;
278 Ok(ParsedTransaction::ContractTx {
279 from: poster,
280 to: destination,
281 value,
282 gas: gas_limit,
283 gas_fee_cap: max_fee_per_gas,
284 data: calldata,
285 request_id: req_id,
286 })
287 }
288 _ => Err(io::Error::new(
289 io::ErrorKind::InvalidData,
290 "invalid L2 tx type in parseUnsignedTx",
291 )),
292 }
293}
294
295fn parse_l2_funded_by_l1(
296 data: &[u8],
297 poster: Address,
298 request_id: B256,
299) -> Result<Vec<ParsedTransaction>, io::Error> {
300 if data.is_empty() {
301 return Err(io::Error::new(
302 io::ErrorKind::InvalidData,
303 "L2FundedByL1 message has no data",
304 ));
305 }
306
307 let kind = data[0];
308
309 let mut deposit_preimage = [0u8; 64];
311 deposit_preimage[..32].copy_from_slice(request_id.as_slice());
312 let deposit_request_id = B256::from(keccak256(deposit_preimage));
314
315 let mut unsigned_preimage = [0u8; 64];
316 unsigned_preimage[..32].copy_from_slice(request_id.as_slice());
317 unsigned_preimage[63] = 1; let unsigned_request_id = B256::from(keccak256(unsigned_preimage));
319
320 let tx = parse_unsigned_tx(&data[1..], poster, Some(unsigned_request_id), kind)?;
321
322 let tx_value = match &tx {
324 ParsedTransaction::UnsignedUserTx { value, .. } => *value,
325 ParsedTransaction::ContractTx { value, .. } => *value,
326 _ => U256::ZERO,
327 };
328
329 let deposit = ParsedTransaction::EthDeposit {
332 from: Address::ZERO,
333 to: poster,
334 value: tx_value,
335 request_id: deposit_request_id,
336 };
337
338 Ok(vec![deposit, tx])
339}
340
341fn parse_eth_deposit_message(
342 data: &[u8],
343 poster: Address,
344 request_id: B256,
345) -> Result<Vec<ParsedTransaction>, io::Error> {
346 let mut reader = Cursor::new(data);
347 let to = address_from_reader(&mut reader)?;
348 let value = uint256_from_reader(&mut reader)?;
349 Ok(vec![ParsedTransaction::EthDeposit {
350 from: poster,
351 to,
352 value,
353 request_id,
354 }])
355}
356
357fn parse_submit_retryable_message(
358 data: &[u8],
359 poster: Address,
360 request_id: B256,
361 l1_base_fee: U256,
362) -> Result<Vec<ParsedTransaction>, io::Error> {
363 let mut reader = Cursor::new(data);
364
365 let retry_to = address_from_256_from_reader(&mut reader)?;
367 let callvalue = uint256_from_reader(&mut reader)?;
368 let deposit = uint256_from_reader(&mut reader)?;
369 let max_submission_fee = uint256_from_reader(&mut reader)?;
370 let fee_refund_addr = address_from_256_from_reader(&mut reader)?;
371 let beneficiary = address_from_256_from_reader(&mut reader)?;
372 let gas_limit_u256 = uint256_from_reader(&mut reader)?;
373 let gas_limit = gas_limit_u256
374 .try_into()
375 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "gas limit too large"))?;
376 let gas_feature_cap = uint256_from_reader(&mut reader)?;
377
378 let data_length_hash = hash_from_reader(&mut reader)?;
380 let data_length = U256::from_be_bytes(data_length_hash.0)
381 .try_into()
382 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "data length too large"))?;
383 let mut calldata = vec![0u8; data_length];
384 if data_length > 0 {
385 io::Read::read_exact(&mut reader, &mut calldata)?;
386 }
387
388 let to = if retry_to == Address::ZERO {
389 None
390 } else {
391 Some(retry_to)
392 };
393
394 Ok(vec![ParsedTransaction::SubmitRetryable {
395 request_id,
396 l1_base_fee,
397 deposit,
398 callvalue,
399 gas_feature_cap,
400 gas_limit,
401 max_submission_fee,
402 from: poster,
403 to,
404 fee_refund_addr,
405 beneficiary,
406 data: calldata,
407 }])
408}
409
410fn parse_batch_posting_report(
411 data: &[u8],
412 _poster: Address,
413 _request_id: B256,
414) -> Result<Vec<ParsedTransaction>, io::Error> {
415 let mut reader = Cursor::new(data);
416
417 let batch_timestamp_u256 = uint256_from_reader(&mut reader)?;
420 let batch_timestamp: u64 = batch_timestamp_u256
421 .try_into()
422 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch timestamp too large"))?;
423
424 let batch_poster = address_from_reader(&mut reader)?;
425
426 let data_hash = hash_from_reader(&mut reader)?;
427
428 let batch_number_u256 = uint256_from_reader(&mut reader)?;
429 let batch_number: u64 = batch_number_u256
430 .try_into()
431 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "batch number too large"))?;
432
433 let l1_base_fee_estimate = uint256_from_reader(&mut reader)?;
434
435 let extra_gas = match uint64_from_reader(&mut reader) {
437 Ok(v) => v,
438 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => 0,
439 Err(e) => return Err(e),
440 };
441
442 Ok(vec![ParsedTransaction::BatchPostingReport {
443 batch_timestamp,
444 batch_poster,
445 data_hash,
446 batch_number,
447 l1_base_fee_estimate,
448 extra_gas,
449 }])
450}
451
452pub fn parsed_tx_to_signed(
462 parsed: &ParsedTransaction,
463 chain_id: u64,
464) -> Option<ArbTransactionSigned> {
465 use arb_primitives::signed_tx::ArbTypedTransaction;
466
467 let chain_id_u256 = U256::from(chain_id);
468
469 let tx = match parsed {
470 ParsedTransaction::Signed(rlp_bytes) => {
471 use alloy_eips::Decodable2718;
473 return ArbTransactionSigned::decode_2718(&mut rlp_bytes.as_slice()).ok();
474 }
475 ParsedTransaction::UnsignedUserTx {
476 from,
477 to,
478 value,
479 gas,
480 gas_fee_cap,
481 nonce,
482 data,
483 } => ArbTypedTransaction::Unsigned(ArbUnsignedTx {
484 chain_id: chain_id_u256,
485 from: *from,
486 nonce: *nonce,
487 gas_fee_cap: *gas_fee_cap,
488 gas: *gas,
489 to: *to,
490 value: *value,
491 data: Bytes::copy_from_slice(data),
492 }),
493 ParsedTransaction::ContractTx {
494 from,
495 to,
496 value,
497 gas,
498 gas_fee_cap,
499 data,
500 request_id,
501 } => ArbTypedTransaction::Contract(ArbContractTx {
502 chain_id: chain_id_u256,
503 request_id: *request_id,
504 from: *from,
505 gas_fee_cap: *gas_fee_cap,
506 gas: *gas,
507 to: *to,
508 value: *value,
509 data: Bytes::copy_from_slice(data),
510 }),
511 ParsedTransaction::EthDeposit {
512 from,
513 to,
514 value,
515 request_id,
516 } => ArbTypedTransaction::Deposit(ArbDepositTx {
517 chain_id: chain_id_u256,
518 l1_request_id: *request_id,
519 from: *from,
520 to: *to,
521 value: *value,
522 }),
523 ParsedTransaction::SubmitRetryable {
524 request_id,
525 l1_base_fee,
526 deposit,
527 callvalue,
528 gas_feature_cap,
529 gas_limit,
530 max_submission_fee,
531 from,
532 to,
533 fee_refund_addr,
534 beneficiary,
535 data,
536 } => ArbTypedTransaction::SubmitRetryable(ArbSubmitRetryableTx {
537 chain_id: chain_id_u256,
538 request_id: *request_id,
539 from: *from,
540 l1_base_fee: *l1_base_fee,
541 deposit_value: *deposit,
542 gas_fee_cap: *gas_feature_cap,
543 gas: *gas_limit,
544 retry_to: *to,
545 retry_value: *callvalue,
546 beneficiary: *beneficiary,
547 max_submission_fee: *max_submission_fee,
548 fee_refund_addr: *fee_refund_addr,
549 retry_data: Bytes::copy_from_slice(data),
550 }),
551 ParsedTransaction::BatchPostingReport { .. } => {
552 return None;
555 }
556 ParsedTransaction::InternalStartBlock { .. } => {
557 return None;
559 }
560 };
561
562 let sig = alloy_primitives::Signature::new(U256::ZERO, U256::ZERO, false);
563 Some(ArbTransactionSigned::new_unhashed(tx, sig))
564}