1use alloy_primitives::{Address, B256, U256};
2use arb_chainspec::arbos_version;
3use arb_storage::{StorageBackend, StorageError};
4
5use crate::{
6 arbos_state::{ArbosState, ArbosStateError},
7 arbos_types::{BatchDataStats, legacy_cost_for_stats},
8 blockhash::BlockhashesError,
9 burn::Burner,
10 util::BalanceError,
11};
12
13const TX_GAS: u64 = 21_000;
15
16#[derive(thiserror::Error, Debug)]
18pub enum InternalTxDecodeError {
19 #[error("internal tx data too short: expected at least {expected} bytes, got {got}")]
21 Length {
22 expected: usize,
24 got: usize,
26 },
27
28 #[error("internal tx field `{field}` does not fit in u64")]
30 U256Overflow {
31 field: &'static str,
33 },
34
35 #[error("unknown internal tx selector: {selector:02x?}")]
37 UnknownSelector {
38 selector: [u8; 4],
40 },
41
42 #[error(transparent)]
44 Blockhashes(#[from] BlockhashesError),
45
46 #[error(transparent)]
48 ArbosState(#[from] ArbosStateError),
49
50 #[error(transparent)]
52 Storage(#[from] StorageError),
53}
54
55pub const INTERNAL_TX_START_BLOCK_METHOD_ID: [u8; 4] = [0x6b, 0xf6, 0xa4, 0x2d];
61
62pub const INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID: [u8; 4] = [0xb6, 0x69, 0x37, 0x71];
64
65pub const INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID: [u8; 4] = [0x99, 0x98, 0x26, 0x9e];
67
68pub const ARB_RETRYABLE_TX_ADDRESS: Address = {
73 let mut bytes = [0u8; 20];
74 bytes[18] = 0x00;
75 bytes[19] = 0x6e;
76 Address::new(bytes)
77};
78
79pub const ARB_SYS_ADDRESS: Address = {
80 let mut bytes = [0u8; 20];
81 bytes[19] = 0x64;
82 Address::new(bytes)
83};
84
85pub const FLOOR_GAS_ADDITIONAL_TOKENS: u64 = 172;
91
92#[derive(Debug, Clone)]
98pub struct L1Info {
99 pub poster: Address,
100 pub l1_block_number: u64,
101 pub l1_timestamp: u64,
102}
103
104impl L1Info {
105 pub fn new(poster: Address, l1_block_number: u64, l1_timestamp: u64) -> Self {
106 Self {
107 poster,
108 l1_block_number,
109 l1_timestamp,
110 }
111 }
112}
113
114pub const L2_TO_L1_TRANSACTION_EVENT_ID: B256 = {
119 let bytes: [u8; 32] = [
120 0x5b, 0xaa, 0xa8, 0x7d, 0xb3, 0x86, 0x36, 0x5b, 0x5c, 0x16, 0x1b, 0xe3, 0x77, 0xbc, 0x3d,
121 0x8e, 0x31, 0x7e, 0x8d, 0x98, 0xd7, 0x1a, 0x3c, 0xa7, 0xed, 0x7d, 0x55, 0x53, 0x40, 0xc8,
122 0xf7, 0x67,
123 ];
124 B256::new(bytes)
125};
126
127pub const L2_TO_L1_TX_EVENT_ID: B256 = {
128 let bytes: [u8; 32] = [
129 0x3e, 0x7a, 0xaf, 0xa7, 0x7d, 0xbf, 0x18, 0x6b, 0x7f, 0xd4, 0x88, 0x00, 0x6b, 0xef, 0xf8,
130 0x93, 0x74, 0x4c, 0xaa, 0x3c, 0x4f, 0x6f, 0x29, 0x9e, 0x8a, 0x70, 0x9f, 0xa2, 0x08, 0x73,
131 0x74, 0xfc,
132 ];
133 B256::new(bytes)
134};
135
136pub const REDEEM_SCHEDULED_EVENT_ID: B256 = {
137 let bytes: [u8; 32] = [
138 0x5c, 0xcd, 0x00, 0x95, 0x02, 0x50, 0x9c, 0xf2, 0x87, 0x62, 0xc6, 0x78, 0x58, 0x99, 0x4d,
139 0x85, 0xb1, 0x63, 0xbb, 0x6e, 0x45, 0x1f, 0x5e, 0x9d, 0xf7, 0xc5, 0xe1, 0x8c, 0x9c, 0x2e,
140 0x12, 0x3e,
141 ];
142 B256::new(bytes)
143};
144
145#[derive(Debug, Clone)]
152pub struct StartBlockData {
153 pub l1_base_fee: U256,
154 pub l1_block_number: u64,
155 pub l2_block_number: u64,
156 pub time_passed: u64,
157}
158
159#[derive(Debug, Clone)]
161pub struct BatchPostingReportData {
162 pub batch_timestamp: u64,
163 pub batch_poster: Address,
164 pub batch_data_gas: u64,
165 pub l1_base_fee: U256,
166}
167
168#[derive(Debug, Clone)]
170pub struct BatchPostingReportV2Data {
171 pub batch_timestamp: u64,
172 pub batch_poster: Address,
173 pub batch_calldata_length: u64,
174 pub batch_calldata_non_zeros: u64,
175 pub batch_extra_gas: u64,
176 pub l1_base_fee: U256,
177}
178
179pub fn encode_start_block(
185 l1_base_fee: U256,
186 l1_block_number: u64,
187 l2_block_number: u64,
188 time_passed: u64,
189) -> Vec<u8> {
190 let mut data = Vec::with_capacity(4 + 32 * 4);
191 data.extend_from_slice(&INTERNAL_TX_START_BLOCK_METHOD_ID);
192 data.extend_from_slice(&l1_base_fee.to_be_bytes::<32>());
193 data.extend_from_slice(&B256::left_padding_from(&l1_block_number.to_be_bytes()).0);
194 data.extend_from_slice(&B256::left_padding_from(&l2_block_number.to_be_bytes()).0);
195 data.extend_from_slice(&B256::left_padding_from(&time_passed.to_be_bytes()).0);
196 data
197}
198
199pub fn encode_batch_posting_report(
204 batch_timestamp: u64,
205 batch_poster: Address,
206 batch_number: u64,
207 batch_data_gas: u64,
208 l1_base_fee: U256,
209) -> Vec<u8> {
210 let mut data = Vec::with_capacity(4 + 32 * 5);
211 data.extend_from_slice(&INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID);
212 data.extend_from_slice(&B256::left_padding_from(&batch_timestamp.to_be_bytes()).0);
213 data.extend_from_slice(&B256::left_padding_from(batch_poster.as_slice()).0);
214 data.extend_from_slice(&B256::left_padding_from(&batch_number.to_be_bytes()).0);
215 data.extend_from_slice(&B256::left_padding_from(&batch_data_gas.to_be_bytes()).0);
216 data.extend_from_slice(&l1_base_fee.to_be_bytes::<32>());
217 data
218}
219
220pub fn encode_batch_posting_report_v2(
222 batch_timestamp: u64,
223 batch_poster: Address,
224 batch_number: u64,
225 batch_calldata_length: u64,
226 batch_calldata_non_zeros: u64,
227 batch_extra_gas: u64,
228 l1_base_fee: U256,
229) -> Vec<u8> {
230 let mut data = Vec::with_capacity(4 + 32 * 7);
231 data.extend_from_slice(&INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID);
232 data.extend_from_slice(&B256::left_padding_from(&batch_timestamp.to_be_bytes()).0);
233 data.extend_from_slice(&B256::left_padding_from(batch_poster.as_slice()).0);
234 data.extend_from_slice(&B256::left_padding_from(&batch_number.to_be_bytes()).0);
235 data.extend_from_slice(&B256::left_padding_from(&batch_calldata_length.to_be_bytes()).0);
236 data.extend_from_slice(&B256::left_padding_from(&batch_calldata_non_zeros.to_be_bytes()).0);
237 data.extend_from_slice(&B256::left_padding_from(&batch_extra_gas.to_be_bytes()).0);
238 data.extend_from_slice(&l1_base_fee.to_be_bytes::<32>());
239 data
240}
241
242pub fn decode_start_block_data(data: &[u8]) -> Result<StartBlockData, InternalTxDecodeError> {
248 const EXPECTED: usize = 4 + 32 * 4;
249 if data.len() < EXPECTED {
250 return Err(InternalTxDecodeError::Length {
251 expected: EXPECTED,
252 got: data.len(),
253 });
254 }
255 let args = &data[4..];
256 let l1_block_number = u256_to_u64(&args[32..64], "l1_block_number")?;
257 let l2_block_number = u256_to_u64(&args[64..96], "l2_block_number")?;
258 let time_passed = u256_to_u64(&args[96..128], "time_passed")?;
259 Ok(StartBlockData {
260 l1_base_fee: U256::from_be_slice(&args[0..32]),
261 l1_block_number,
262 l2_block_number,
263 time_passed,
264 })
265}
266
267fn u256_to_u64(slice: &[u8], field: &'static str) -> Result<u64, InternalTxDecodeError> {
268 U256::from_be_slice(slice)
269 .try_into()
270 .map_err(|_| InternalTxDecodeError::U256Overflow { field })
271}
272
273fn decode_batch_posting_report(
274 data: &[u8],
275) -> Result<BatchPostingReportData, InternalTxDecodeError> {
276 const EXPECTED: usize = 4 + 32 * 5;
278 if data.len() < EXPECTED {
279 return Err(InternalTxDecodeError::Length {
280 expected: EXPECTED,
281 got: data.len(),
282 });
283 }
284 let args = &data[4..];
285 Ok(BatchPostingReportData {
286 batch_timestamp: u256_to_u64(&args[0..32], "batch_timestamp")?,
287 batch_poster: Address::from_slice(&args[44..64]),
288 batch_data_gas: u256_to_u64(&args[96..128], "batch_data_gas")?,
289 l1_base_fee: U256::from_be_slice(&args[128..160]),
290 })
291}
292
293fn decode_batch_posting_report_v2(
294 data: &[u8],
295) -> Result<BatchPostingReportV2Data, InternalTxDecodeError> {
296 const EXPECTED: usize = 4 + 32 * 7;
298 if data.len() < EXPECTED {
299 return Err(InternalTxDecodeError::Length {
300 expected: EXPECTED,
301 got: data.len(),
302 });
303 }
304 let args = &data[4..];
305 Ok(BatchPostingReportV2Data {
306 batch_timestamp: u256_to_u64(&args[0..32], "batch_timestamp")?,
307 batch_poster: Address::from_slice(&args[44..64]),
308 batch_calldata_length: u256_to_u64(&args[96..128], "batch_calldata_length")?,
309 batch_calldata_non_zeros: u256_to_u64(&args[128..160], "batch_calldata_non_zeros")?,
310 batch_extra_gas: u256_to_u64(&args[160..192], "batch_extra_gas")?,
311 l1_base_fee: U256::from_be_slice(&args[192..224]),
312 })
313}
314
315pub struct InternalTxContext {
321 pub block_number: u64,
322 pub current_time: u64,
323 pub prev_hash: B256,
324}
325
326pub fn apply_internal_tx_update<D: revm::Database, B: Burner, F, G, C>(
333 backend: &mut C,
334 data: &[u8],
335 state: &mut ArbosState<'_, D, B>,
336 ctx: &InternalTxContext,
337 mut transfer_fn: F,
338 mut balance_of: G,
339) -> Result<(), InternalTxDecodeError>
340where
341 F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
342 G: FnMut(Address) -> U256,
343 C: StorageBackend,
344{
345 let Some(selector) = data.first_chunk::<4>().copied() else {
346 return Err(InternalTxDecodeError::Length {
347 expected: 4,
348 got: data.len(),
349 });
350 };
351
352 match selector {
353 INTERNAL_TX_START_BLOCK_METHOD_ID => {
354 let inputs = decode_start_block_data(data)?;
355 apply_start_block(
356 backend,
357 inputs,
358 state,
359 ctx,
360 &mut transfer_fn,
361 &mut balance_of,
362 )
363 }
364 INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID => {
365 let inputs = decode_batch_posting_report(data)?;
366 apply_batch_posting_report(backend, inputs, state, ctx, &mut transfer_fn)
367 }
368 INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID => {
369 let inputs = decode_batch_posting_report_v2(data)?;
370 apply_batch_posting_report_v2(backend, inputs, state, ctx, &mut transfer_fn)
371 }
372 _ => Err(InternalTxDecodeError::UnknownSelector { selector }),
373 }
374}
375
376fn apply_start_block<D: revm::Database, B: Burner, F, G, C>(
377 backend: &mut C,
378 inputs: StartBlockData,
379 state: &mut ArbosState<'_, D, B>,
380 ctx: &InternalTxContext,
381 transfer_fn: &mut F,
382 balance_of: &mut G,
383) -> Result<(), InternalTxDecodeError>
384where
385 F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
386 G: FnMut(Address) -> U256,
387 C: StorageBackend,
388{
389 let arbos_version = state.arbos_version();
390
391 let mut l1_block_number = inputs.l1_block_number;
392 let mut time_passed = inputs.time_passed;
393
394 if arbos_version < arbos_version::ARBOS_VERSION_3 {
395 time_passed = inputs.l2_block_number;
396 }
397
398 if arbos_version < arbos_version::ARBOS_VERSION_8 {
399 l1_block_number = l1_block_number.saturating_add(1);
400 }
401
402 let old_l1_block_number = state.blockhashes.l1_block_number(backend)?;
403
404 if l1_block_number > old_l1_block_number {
405 state.blockhashes.record_new_l1_block(
406 backend,
407 l1_block_number - 1,
408 ctx.prev_hash,
409 arbos_version,
410 )?;
411 }
412
413 let _ = state.retryable_state.try_to_reap_one_retryable(
414 backend,
415 ctx.current_time,
416 &mut *transfer_fn,
417 &mut *balance_of,
418 );
419 let _ = state.retryable_state.try_to_reap_one_retryable(
420 backend,
421 ctx.current_time,
422 &mut *transfer_fn,
423 &mut *balance_of,
424 );
425
426 let _ = state
427 .l2_pricing_state
428 .update_pricing_model(backend, time_passed, arbos_version);
429
430 state.upgrade_arbos_version_if_necessary(backend, ctx.current_time)?;
431
432 Ok(())
433}
434
435fn apply_batch_posting_report<D: revm::Database, B: Burner, F, C>(
436 backend: &mut C,
437 inputs: BatchPostingReportData,
438 state: &mut ArbosState<'_, D, B>,
439 ctx: &InternalTxContext,
440 transfer_fn: &mut F,
441) -> Result<(), InternalTxDecodeError>
442where
443 F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
444 C: StorageBackend,
445{
446 let per_batch_gas = state
447 .l1_pricing_state
448 .per_batch_gas_cost(backend)
449 .unwrap_or(0);
450
451 let batch_data_gas_i64 = i64::try_from(inputs.batch_data_gas).unwrap_or(i64::MAX);
452 let gas_spent_signed = per_batch_gas.saturating_add(batch_data_gas_i64);
453 let gas_spent = gas_spent_signed.max(0) as u64;
454 let wei_spent = inputs.l1_base_fee.saturating_mul(U256::from(gas_spent));
455
456 if let Err(e) = state.l1_pricing_state.update_for_batch_poster_spending(
457 backend,
458 inputs.batch_timestamp,
459 ctx.current_time,
460 inputs.batch_poster,
461 wei_spent,
462 inputs.l1_base_fee,
463 &mut *transfer_fn,
464 ) {
465 tracing::warn!(error = ?e, "L1 pricing update failed for batch posting report");
466 }
467
468 Ok(())
469}
470
471fn apply_batch_posting_report_v2<D: revm::Database, B: Burner, F, C>(
472 backend: &mut C,
473 inputs: BatchPostingReportV2Data,
474 state: &mut ArbosState<'_, D, B>,
475 ctx: &InternalTxContext,
476 transfer_fn: &mut F,
477) -> Result<(), InternalTxDecodeError>
478where
479 F: FnMut(Address, Address, U256) -> Result<(), BalanceError>,
480 C: StorageBackend,
481{
482 let arbos_version = state.arbos_version();
483
484 let mut gas_spent = legacy_cost_for_stats(&BatchDataStats {
485 length: inputs.batch_calldata_length,
486 non_zeros: inputs.batch_calldata_non_zeros,
487 });
488
489 gas_spent = gas_spent.saturating_add(inputs.batch_extra_gas);
490
491 let per_batch_gas = state
492 .l1_pricing_state
493 .per_batch_gas_cost(backend)
494 .unwrap_or(0);
495
496 gas_spent = gas_spent.saturating_add(per_batch_gas.max(0) as u64);
497
498 if arbos_version >= arbos_version::ARBOS_VERSION_50 {
499 let gas_floor_per_token = state
500 .l1_pricing_state
501 .parent_gas_floor_per_token(backend)
502 .unwrap_or(0);
503
504 let total_tokens = inputs
505 .batch_calldata_length
506 .saturating_add(inputs.batch_calldata_non_zeros.saturating_mul(3))
507 .saturating_add(FLOOR_GAS_ADDITIONAL_TOKENS);
508
509 let floor_gas_spent = gas_floor_per_token
510 .saturating_mul(total_tokens)
511 .saturating_add(TX_GAS);
512
513 if floor_gas_spent > gas_spent {
514 gas_spent = floor_gas_spent;
515 }
516 }
517
518 let wei_spent = inputs.l1_base_fee.saturating_mul(U256::from(gas_spent));
519
520 if let Err(e) = state.l1_pricing_state.update_for_batch_poster_spending(
521 backend,
522 inputs.batch_timestamp,
523 ctx.current_time,
524 inputs.batch_poster,
525 wei_spent,
526 inputs.l1_base_fee,
527 &mut *transfer_fn,
528 ) {
529 tracing::warn!(error = ?e, "L1 pricing update failed for batch posting report v2");
530 }
531
532 Ok(())
533}