arbos/
internal_tx.rs

1use alloy_primitives::{Address, B256, U256};
2
3use arb_chainspec::arbos_version;
4
5use crate::{
6    arbos_state::ArbosState,
7    arbos_types::{legacy_cost_for_stats, BatchDataStats},
8    burn::Burner,
9};
10
11/// Standard Ethereum base transaction gas.
12const TX_GAS: u64 = 21_000;
13
14// ---------------------------------------------------------------------------
15// Method selectors (keccak256 of ABI signatures)
16// ---------------------------------------------------------------------------
17
18/// startBlock(uint256,uint64,uint64,uint64)
19pub const INTERNAL_TX_START_BLOCK_METHOD_ID: [u8; 4] = [0x6b, 0xf6, 0xa4, 0x2d];
20
21/// batchPostingReport(uint256,address,uint64,uint64,uint256)
22pub const INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID: [u8; 4] = [0xb6, 0x69, 0x37, 0x71];
23
24/// batchPostingReportV2(uint256,address,uint64,uint64,uint64,uint64,uint256)
25pub const INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID: [u8; 4] = [0x99, 0x98, 0x26, 0x9e];
26
27// ---------------------------------------------------------------------------
28// Well-known system addresses
29// ---------------------------------------------------------------------------
30
31pub const ARB_RETRYABLE_TX_ADDRESS: Address = {
32    let mut bytes = [0u8; 20];
33    bytes[18] = 0x00;
34    bytes[19] = 0x6e;
35    Address::new(bytes)
36};
37
38pub const ARB_SYS_ADDRESS: Address = {
39    let mut bytes = [0u8; 20];
40    bytes[19] = 0x64;
41    Address::new(bytes)
42};
43
44/// Additional tokens in the calldata for floor gas accounting.
45///
46/// Raw batch has a 40-byte header (5 uint64s) that doesn't come from calldata.
47/// The addSequencerL2BatchFromOrigin call has a selector + 5 additional fields.
48/// Token count: 4*4 (selector) + 4*24 (uint64 padding) + 4*12+12 (address) = 172
49pub const FLOOR_GAS_ADDITIONAL_TOKENS: u64 = 172;
50
51// ---------------------------------------------------------------------------
52// L1 block info
53// ---------------------------------------------------------------------------
54
55/// L1 block info passed to internal transactions.
56#[derive(Debug, Clone)]
57pub struct L1Info {
58    pub poster: Address,
59    pub l1_block_number: u64,
60    pub l1_timestamp: u64,
61}
62
63impl L1Info {
64    pub fn new(poster: Address, l1_block_number: u64, l1_timestamp: u64) -> Self {
65        Self {
66            poster,
67            l1_block_number,
68            l1_timestamp,
69        }
70    }
71}
72
73// ---------------------------------------------------------------------------
74// Event IDs
75// ---------------------------------------------------------------------------
76
77pub const L2_TO_L1_TRANSACTION_EVENT_ID: B256 = {
78    let bytes: [u8; 32] = [
79        0x5b, 0xaa, 0xa8, 0x7d, 0xb3, 0x86, 0x36, 0x5b, 0x5c, 0x16, 0x1b, 0xe3, 0x77, 0xbc, 0x3d,
80        0x8e, 0x31, 0x7e, 0x8d, 0x98, 0xd7, 0x1a, 0x3c, 0xa7, 0xed, 0x7d, 0x55, 0x53, 0x40, 0xc8,
81        0xf7, 0x67,
82    ];
83    B256::new(bytes)
84};
85
86pub const L2_TO_L1_TX_EVENT_ID: B256 = {
87    let bytes: [u8; 32] = [
88        0x3e, 0x7a, 0xaf, 0xa7, 0x7d, 0xbf, 0x18, 0x6b, 0x7f, 0xd4, 0x88, 0x00, 0x6b, 0xef, 0xf8,
89        0x93, 0x74, 0x4c, 0xaa, 0x3c, 0x4f, 0x6f, 0x29, 0x9e, 0x8a, 0x70, 0x9f, 0xa2, 0x08, 0x73,
90        0x74, 0xfc,
91    ];
92    B256::new(bytes)
93};
94
95pub const REDEEM_SCHEDULED_EVENT_ID: B256 = {
96    let bytes: [u8; 32] = [
97        0x5c, 0xcd, 0x00, 0x95, 0x02, 0x50, 0x9c, 0xf2, 0x87, 0x62, 0xc6, 0x78, 0x58, 0x99, 0x4d,
98        0x85, 0xb1, 0x63, 0xbb, 0x6e, 0x45, 0x1f, 0x5e, 0x9d, 0xf7, 0xc5, 0xe1, 0x8c, 0x9c, 0x2e,
99        0x12, 0x3e,
100    ];
101    B256::new(bytes)
102};
103
104// ---------------------------------------------------------------------------
105// Decoded internal tx data
106// ---------------------------------------------------------------------------
107
108/// Decoded startBlock(uint256 l1BaseFee, uint64 l1BlockNumber, uint64 l2BlockNumber, uint64
109/// timePassed)
110#[derive(Debug, Clone)]
111pub struct StartBlockData {
112    pub l1_base_fee: U256,
113    pub l1_block_number: u64,
114    pub l2_block_number: u64,
115    pub time_passed: u64,
116}
117
118/// Decoded batchPostingReport(uint256, address, uint64, uint64, uint256)
119#[derive(Debug, Clone)]
120pub struct BatchPostingReportData {
121    pub batch_timestamp: u64,
122    pub batch_poster: Address,
123    pub batch_data_gas: u64,
124    pub l1_base_fee: U256,
125}
126
127/// Decoded batchPostingReportV2(uint256, address, uint64, uint64, uint64, uint64, uint256)
128#[derive(Debug, Clone)]
129pub struct BatchPostingReportV2Data {
130    pub batch_timestamp: u64,
131    pub batch_poster: Address,
132    pub batch_calldata_length: u64,
133    pub batch_calldata_non_zeros: u64,
134    pub batch_extra_gas: u64,
135    pub l1_base_fee: U256,
136}
137
138// ---------------------------------------------------------------------------
139// ABI encoding
140// ---------------------------------------------------------------------------
141
142/// Creates the ABI-encoded data for a startBlock internal transaction.
143pub fn encode_start_block(
144    l1_base_fee: U256,
145    l1_block_number: u64,
146    l2_block_number: u64,
147    time_passed: u64,
148) -> Vec<u8> {
149    let mut data = Vec::with_capacity(4 + 32 * 4);
150    data.extend_from_slice(&INTERNAL_TX_START_BLOCK_METHOD_ID);
151    data.extend_from_slice(&l1_base_fee.to_be_bytes::<32>());
152    data.extend_from_slice(&B256::left_padding_from(&l1_block_number.to_be_bytes()).0);
153    data.extend_from_slice(&B256::left_padding_from(&l2_block_number.to_be_bytes()).0);
154    data.extend_from_slice(&B256::left_padding_from(&time_passed.to_be_bytes()).0);
155    data
156}
157
158/// Creates the ABI-encoded data for a batchPostingReport internal transaction (v1).
159///
160/// ABI: batchPostingReport(uint256 timestamp, address poster, bytes32 dataHash,
161///                          uint256 batchNum, uint256 l1BaseFee)
162pub fn encode_batch_posting_report(
163    batch_timestamp: u64,
164    batch_poster: Address,
165    batch_number: u64,
166    batch_data_gas: u64,
167    l1_base_fee: U256,
168) -> Vec<u8> {
169    let mut data = Vec::with_capacity(4 + 32 * 5);
170    data.extend_from_slice(&INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID);
171    data.extend_from_slice(&B256::left_padding_from(&batch_timestamp.to_be_bytes()).0);
172    data.extend_from_slice(&B256::left_padding_from(batch_poster.as_slice()).0);
173    data.extend_from_slice(&B256::left_padding_from(&batch_number.to_be_bytes()).0);
174    data.extend_from_slice(&B256::left_padding_from(&batch_data_gas.to_be_bytes()).0);
175    data.extend_from_slice(&l1_base_fee.to_be_bytes::<32>());
176    data
177}
178
179/// Creates the ABI-encoded data for a batchPostingReportV2 internal transaction.
180pub fn encode_batch_posting_report_v2(
181    batch_timestamp: u64,
182    batch_poster: Address,
183    batch_number: u64,
184    batch_calldata_length: u64,
185    batch_calldata_non_zeros: u64,
186    batch_extra_gas: u64,
187    l1_base_fee: U256,
188) -> Vec<u8> {
189    let mut data = Vec::with_capacity(4 + 32 * 7);
190    data.extend_from_slice(&INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID);
191    data.extend_from_slice(&B256::left_padding_from(&batch_timestamp.to_be_bytes()).0);
192    data.extend_from_slice(&B256::left_padding_from(batch_poster.as_slice()).0);
193    data.extend_from_slice(&B256::left_padding_from(&batch_number.to_be_bytes()).0);
194    data.extend_from_slice(&B256::left_padding_from(&batch_calldata_length.to_be_bytes()).0);
195    data.extend_from_slice(&B256::left_padding_from(&batch_calldata_non_zeros.to_be_bytes()).0);
196    data.extend_from_slice(&B256::left_padding_from(&batch_extra_gas.to_be_bytes()).0);
197    data.extend_from_slice(&l1_base_fee.to_be_bytes::<32>());
198    data
199}
200
201// ---------------------------------------------------------------------------
202// ABI decoding
203// ---------------------------------------------------------------------------
204
205/// Decode startBlock data from raw internal tx bytes.
206pub fn decode_start_block_data(data: &[u8]) -> Result<StartBlockData, String> {
207    if data.len() < 4 + 32 * 4 {
208        return Err(format!(
209            "start block data too short: expected >= 132, got {}",
210            data.len()
211        ));
212    }
213    let args = &data[4..];
214    Ok(StartBlockData {
215        l1_base_fee: U256::from_be_slice(&args[0..32]),
216        l1_block_number: U256::from_be_slice(&args[32..64]).to::<u64>(),
217        l2_block_number: U256::from_be_slice(&args[64..96]).to::<u64>(),
218        time_passed: U256::from_be_slice(&args[96..128]).to::<u64>(),
219    })
220}
221
222fn decode_batch_posting_report(data: &[u8]) -> Result<BatchPostingReportData, String> {
223    // 5 ABI words: uint256, address, uint64, uint64, uint256
224    if data.len() < 4 + 32 * 5 {
225        return Err(format!(
226            "batch posting report data too short: expected >= 164, got {}",
227            data.len()
228        ));
229    }
230    let args = &data[4..];
231    Ok(BatchPostingReportData {
232        batch_timestamp: U256::from_be_slice(&args[0..32]).to::<u64>(),
233        batch_poster: Address::from_slice(&args[44..64]),
234        batch_data_gas: U256::from_be_slice(&args[96..128]).to::<u64>(),
235        l1_base_fee: U256::from_be_slice(&args[128..160]),
236    })
237}
238
239fn decode_batch_posting_report_v2(data: &[u8]) -> Result<BatchPostingReportV2Data, String> {
240    // 7 ABI words: uint256, address, uint64, uint64, uint64, uint64, uint256
241    if data.len() < 4 + 32 * 7 {
242        return Err(format!(
243            "batch posting report v2 data too short: expected >= 228, got {}",
244            data.len()
245        ));
246    }
247    let args = &data[4..];
248    Ok(BatchPostingReportV2Data {
249        batch_timestamp: U256::from_be_slice(&args[0..32]).to::<u64>(),
250        batch_poster: Address::from_slice(&args[44..64]),
251        batch_calldata_length: U256::from_be_slice(&args[96..128]).to::<u64>(),
252        batch_calldata_non_zeros: U256::from_be_slice(&args[128..160]).to::<u64>(),
253        batch_extra_gas: U256::from_be_slice(&args[160..192]).to::<u64>(),
254        l1_base_fee: U256::from_be_slice(&args[192..224]),
255    })
256}
257
258// ---------------------------------------------------------------------------
259// Dispatch
260// ---------------------------------------------------------------------------
261
262/// Context needed by the internal transaction dispatch from the block executor.
263pub struct InternalTxContext {
264    pub block_number: u64,
265    pub current_time: u64,
266    pub prev_hash: B256,
267}
268
269/// Apply an internal transaction update to ArbOS state.
270///
271/// Dispatches on the 4-byte method selector to handle:
272/// - StartBlock: records L1 block hashes, reaps expired retryables, updates L2 pricing, and checks
273///   for ArbOS upgrades.
274/// - BatchPostingReport (v1 and v2): updates L1 pricing based on batch poster spending.
275pub fn apply_internal_tx_update<D: revm::Database, B: Burner, F, G>(
276    data: &[u8],
277    state: &mut ArbosState<D, B>,
278    ctx: &InternalTxContext,
279    mut transfer_fn: F,
280    mut balance_of: G,
281) -> Result<(), String>
282where
283    F: FnMut(Address, Address, U256) -> Result<(), ()>,
284    G: FnMut(Address) -> U256,
285{
286    if data.len() < 4 {
287        return Err(format!(
288            "internal tx data too short ({} bytes, need at least 4)",
289            data.len()
290        ));
291    }
292
293    let selector: [u8; 4] = data[0..4].try_into().unwrap();
294
295    match selector {
296        INTERNAL_TX_START_BLOCK_METHOD_ID => {
297            let inputs = decode_start_block_data(data)?;
298            apply_start_block(inputs, state, ctx, &mut transfer_fn, &mut balance_of)
299        }
300        INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID => {
301            let inputs = decode_batch_posting_report(data)?;
302            apply_batch_posting_report(inputs, state, ctx, &mut transfer_fn)
303        }
304        INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID => {
305            let inputs = decode_batch_posting_report_v2(data)?;
306            apply_batch_posting_report_v2(inputs, state, ctx, &mut transfer_fn)
307        }
308        _ => Err(format!(
309            "unknown internal tx selector: {:02x}{:02x}{:02x}{:02x}",
310            selector[0], selector[1], selector[2], selector[3]
311        )),
312    }
313}
314
315fn apply_start_block<D: revm::Database, B: Burner, F, G>(
316    inputs: StartBlockData,
317    state: &mut ArbosState<D, B>,
318    ctx: &InternalTxContext,
319    transfer_fn: &mut F,
320    balance_of: &mut G,
321) -> Result<(), String>
322where
323    F: FnMut(Address, Address, U256) -> Result<(), ()>,
324    G: FnMut(Address) -> U256,
325{
326    let arbos_version = state.arbos_version();
327
328    let mut l1_block_number = inputs.l1_block_number;
329    let mut time_passed = inputs.time_passed;
330
331    // Before ArbOS v3, incorrectly used the L2 block number as time_passed.
332    if arbos_version < arbos_version::ARBOS_VERSION_3 {
333        time_passed = inputs.l2_block_number;
334    }
335
336    // Before ArbOS v8, incorrectly used L1 block number one too high.
337    if arbos_version < arbos_version::ARBOS_VERSION_8 {
338        l1_block_number = l1_block_number.saturating_add(1);
339    }
340
341    // Record L1 block hashes if L1 block number advanced.
342    let old_l1_block_number = state
343        .blockhashes
344        .l1_block_number()
345        .map_err(|_| "failed to read l1 block number")?;
346
347    if l1_block_number > old_l1_block_number {
348        state
349            .blockhashes
350            .record_new_l1_block(l1_block_number - 1, ctx.prev_hash, arbos_version)
351            .map_err(|_| "failed to record L1 block")?;
352    }
353
354    // Try to reap 2 expired retryables.
355    let _ = state.retryable_state.try_to_reap_one_retryable(
356        ctx.current_time,
357        &mut *transfer_fn,
358        &mut *balance_of,
359    );
360    let _ = state.retryable_state.try_to_reap_one_retryable(
361        ctx.current_time,
362        &mut *transfer_fn,
363        &mut *balance_of,
364    );
365
366    // Update L2 pricing model.
367    let _ = state
368        .l2_pricing_state
369        .update_pricing_model(time_passed, arbos_version);
370
371    // Check for scheduled ArbOS upgrade.
372    state
373        .upgrade_arbos_version_if_necessary(ctx.current_time)
374        .map_err(|_| "ArbOS upgrade failed (node may be out of date)")?;
375
376    Ok(())
377}
378
379fn apply_batch_posting_report<D: revm::Database, B: Burner, F>(
380    inputs: BatchPostingReportData,
381    state: &mut ArbosState<D, B>,
382    ctx: &InternalTxContext,
383    transfer_fn: &mut F,
384) -> Result<(), String>
385where
386    F: FnMut(Address, Address, U256) -> Result<(), ()>,
387{
388    let per_batch_gas = state.l1_pricing_state.per_batch_gas_cost().unwrap_or(0);
389
390    // gasSpent = SaturatingAdd(perBatchGas, SaturatingCast[int64](batchDataGas))
391    // Then SaturatingUCast[uint64](gasSpent) — clamps negative result to 0.
392    let batch_data_gas_i64 = i64::try_from(inputs.batch_data_gas).unwrap_or(i64::MAX);
393    let gas_spent_signed = per_batch_gas.saturating_add(batch_data_gas_i64);
394    let gas_spent = gas_spent_signed.max(0) as u64;
395    let wei_spent = inputs.l1_base_fee.saturating_mul(U256::from(gas_spent));
396
397    if let Err(e) = state.l1_pricing_state.update_for_batch_poster_spending(
398        inputs.batch_timestamp,
399        ctx.current_time,
400        inputs.batch_poster,
401        wei_spent,
402        inputs.l1_base_fee,
403        &mut *transfer_fn,
404    ) {
405        tracing::warn!(error = ?e, "L1 pricing update failed for batch posting report");
406    }
407
408    Ok(())
409}
410
411fn apply_batch_posting_report_v2<D: revm::Database, B: Burner, F>(
412    inputs: BatchPostingReportV2Data,
413    state: &mut ArbosState<D, B>,
414    ctx: &InternalTxContext,
415    transfer_fn: &mut F,
416) -> Result<(), String>
417where
418    F: FnMut(Address, Address, U256) -> Result<(), ()>,
419{
420    let arbos_version = state.arbos_version();
421
422    // Compute gas from calldata stats (legacy cost model).
423    let mut gas_spent = legacy_cost_for_stats(&BatchDataStats {
424        length: inputs.batch_calldata_length,
425        non_zeros: inputs.batch_calldata_non_zeros,
426    });
427
428    gas_spent = gas_spent.saturating_add(inputs.batch_extra_gas);
429
430    // Add per-batch gas overhead.
431    let per_batch_gas = state.l1_pricing_state.per_batch_gas_cost().unwrap_or(0);
432
433    gas_spent = gas_spent.saturating_add(per_batch_gas.max(0) as u64);
434
435    // Floor gas computation (ArbOS v50+).
436    if arbos_version >= arbos_version::ARBOS_VERSION_50 {
437        let gas_floor_per_token = state
438            .l1_pricing_state
439            .parent_gas_floor_per_token()
440            .unwrap_or(0);
441
442        let total_tokens = inputs
443            .batch_calldata_length
444            .saturating_add(inputs.batch_calldata_non_zeros.saturating_mul(3))
445            .saturating_add(FLOOR_GAS_ADDITIONAL_TOKENS);
446
447        let floor_gas_spent = gas_floor_per_token
448            .saturating_mul(total_tokens)
449            .saturating_add(TX_GAS);
450
451        if floor_gas_spent > gas_spent {
452            gas_spent = floor_gas_spent;
453        }
454    }
455
456    let wei_spent = inputs.l1_base_fee.saturating_mul(U256::from(gas_spent));
457
458    if let Err(e) = state.l1_pricing_state.update_for_batch_poster_spending(
459        inputs.batch_timestamp,
460        ctx.current_time,
461        inputs.batch_poster,
462        wei_spent,
463        inputs.l1_base_fee,
464        &mut *transfer_fn,
465    ) {
466        tracing::warn!(error = ?e, "L1 pricing update failed for batch posting report v2");
467    }
468
469    Ok(())
470}