arbos/
internal_tx.rs

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
13/// Standard Ethereum base transaction gas.
14const TX_GAS: u64 = 21_000;
15
16/// Errors raised while decoding or applying an ArbOS internal transaction.
17#[derive(thiserror::Error, Debug)]
18pub enum InternalTxDecodeError {
19    /// The raw calldata was shorter than the ABI layout requires.
20    #[error("internal tx data too short: expected at least {expected} bytes, got {got}")]
21    Length {
22        /// Minimum number of bytes the ABI expects.
23        expected: usize,
24        /// Number of bytes actually supplied.
25        got: usize,
26    },
27
28    /// A `uint256` field did not fit in `u64`.
29    #[error("internal tx field `{field}` does not fit in u64")]
30    U256Overflow {
31        /// Name of the offending ABI field.
32        field: &'static str,
33    },
34
35    /// The 4-byte selector did not match any known internal tx method.
36    #[error("unknown internal tx selector: {selector:02x?}")]
37    UnknownSelector {
38        /// The unrecognized 4-byte selector.
39        selector: [u8; 4],
40    },
41
42    /// Reading the L1 block number from the ring buffer failed.
43    #[error(transparent)]
44    Blockhashes(#[from] BlockhashesError),
45
46    /// An ArbOS upgrade step failed (e.g. unsupported scheduled version).
47    #[error(transparent)]
48    ArbosState(#[from] ArbosStateError),
49
50    /// A bare storage failure surfaced from a typed accessor.
51    #[error(transparent)]
52    Storage(#[from] StorageError),
53}
54
55// ---------------------------------------------------------------------------
56// Method selectors (keccak256 of ABI signatures)
57// ---------------------------------------------------------------------------
58
59/// startBlock(uint256,uint64,uint64,uint64)
60pub const INTERNAL_TX_START_BLOCK_METHOD_ID: [u8; 4] = [0x6b, 0xf6, 0xa4, 0x2d];
61
62/// batchPostingReport(uint256,address,uint64,uint64,uint256)
63pub const INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID: [u8; 4] = [0xb6, 0x69, 0x37, 0x71];
64
65/// batchPostingReportV2(uint256,address,uint64,uint64,uint64,uint64,uint256)
66pub const INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID: [u8; 4] = [0x99, 0x98, 0x26, 0x9e];
67
68// ---------------------------------------------------------------------------
69// Well-known system addresses
70// ---------------------------------------------------------------------------
71
72pub 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
85/// Additional tokens in the calldata for floor gas accounting.
86///
87/// Raw batch has a 40-byte header (5 uint64s) that doesn't come from calldata.
88/// The addSequencerL2BatchFromOrigin call has a selector + 5 additional fields.
89/// Token count: 4*4 (selector) + 4*24 (uint64 padding) + 4*12+12 (address) = 172
90pub const FLOOR_GAS_ADDITIONAL_TOKENS: u64 = 172;
91
92// ---------------------------------------------------------------------------
93// L1 block info
94// ---------------------------------------------------------------------------
95
96/// L1 block info passed to internal transactions.
97#[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
114// ---------------------------------------------------------------------------
115// Event IDs
116// ---------------------------------------------------------------------------
117
118pub 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// ---------------------------------------------------------------------------
146// Decoded internal tx data
147// ---------------------------------------------------------------------------
148
149/// Decoded startBlock(uint256 l1BaseFee, uint64 l1BlockNumber, uint64 l2BlockNumber, uint64
150/// timePassed)
151#[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/// Decoded batchPostingReport(uint256, address, uint64, uint64, uint256)
160#[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/// Decoded batchPostingReportV2(uint256, address, uint64, uint64, uint64, uint64, uint256)
169#[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
179// ---------------------------------------------------------------------------
180// ABI encoding
181// ---------------------------------------------------------------------------
182
183/// Creates the ABI-encoded data for a startBlock internal transaction.
184pub 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
199/// Creates the ABI-encoded data for a batchPostingReport internal transaction (v1).
200///
201/// ABI: batchPostingReport(uint256 timestamp, address poster, bytes32 dataHash,
202///                          uint256 batchNum, uint256 l1BaseFee)
203pub 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
220/// Creates the ABI-encoded data for a batchPostingReportV2 internal transaction.
221pub 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
242// ---------------------------------------------------------------------------
243// ABI decoding
244// ---------------------------------------------------------------------------
245
246/// Decode startBlock data from raw internal tx bytes.
247pub 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    // 5 ABI words: uint256, address, uint64, uint64, uint256
277    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    // 7 ABI words: uint256, address, uint64, uint64, uint64, uint64, uint256
297    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
315// ---------------------------------------------------------------------------
316// Dispatch
317// ---------------------------------------------------------------------------
318
319/// Context needed by the internal transaction dispatch from the block executor.
320pub struct InternalTxContext {
321    pub block_number: u64,
322    pub current_time: u64,
323    pub prev_hash: B256,
324}
325
326/// Apply an internal transaction update to ArbOS state.
327///
328/// Dispatches on the 4-byte method selector to handle:
329/// - StartBlock: records L1 block hashes, reaps expired retryables, updates L2 pricing, and checks
330///   for ArbOS upgrades.
331/// - BatchPostingReport (v1 and v2): updates L1 pricing based on batch poster spending.
332pub 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}