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
11const TX_GAS: u64 = 21_000;
13
14pub const INTERNAL_TX_START_BLOCK_METHOD_ID: [u8; 4] = [0x6b, 0xf6, 0xa4, 0x2d];
20
21pub const INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID: [u8; 4] = [0xb6, 0x69, 0x37, 0x71];
23
24pub const INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID: [u8; 4] = [0x99, 0x98, 0x26, 0x9e];
26
27pub 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
44pub const FLOOR_GAS_ADDITIONAL_TOKENS: u64 = 172;
50
51#[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
73pub 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#[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#[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#[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
138pub 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
158pub 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
179pub 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
201pub 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 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 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
258pub struct InternalTxContext {
264 pub block_number: u64,
265 pub current_time: u64,
266 pub prev_hash: B256,
267}
268
269pub 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 if arbos_version < arbos_version::ARBOS_VERSION_3 {
333 time_passed = inputs.l2_block_number;
334 }
335
336 if arbos_version < arbos_version::ARBOS_VERSION_8 {
338 l1_block_number = l1_block_number.saturating_add(1);
339 }
340
341 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 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 let _ = state
368 .l2_pricing_state
369 .update_pricing_model(time_passed, arbos_version);
370
371 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 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 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 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 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}