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 let l1_block_number = u256_to_u64(&args[32..64], "l1_block_number")?;
215 let l2_block_number = u256_to_u64(&args[64..96], "l2_block_number")?;
216 let time_passed = u256_to_u64(&args[96..128], "time_passed")?;
217 Ok(StartBlockData {
218 l1_base_fee: U256::from_be_slice(&args[0..32]),
219 l1_block_number,
220 l2_block_number,
221 time_passed,
222 })
223}
224
225fn u256_to_u64(slice: &[u8], field: &str) -> Result<u64, String> {
226 U256::from_be_slice(slice)
227 .try_into()
228 .map_err(|_| format!("{field} does not fit in u64"))
229}
230
231fn decode_batch_posting_report(data: &[u8]) -> Result<BatchPostingReportData, String> {
232 if data.len() < 4 + 32 * 5 {
234 return Err(format!(
235 "batch posting report data too short: expected >= 164, got {}",
236 data.len()
237 ));
238 }
239 let args = &data[4..];
240 Ok(BatchPostingReportData {
241 batch_timestamp: u256_to_u64(&args[0..32], "batch_timestamp")?,
242 batch_poster: Address::from_slice(&args[44..64]),
243 batch_data_gas: u256_to_u64(&args[96..128], "batch_data_gas")?,
244 l1_base_fee: U256::from_be_slice(&args[128..160]),
245 })
246}
247
248fn decode_batch_posting_report_v2(data: &[u8]) -> Result<BatchPostingReportV2Data, String> {
249 if data.len() < 4 + 32 * 7 {
251 return Err(format!(
252 "batch posting report v2 data too short: expected >= 228, got {}",
253 data.len()
254 ));
255 }
256 let args = &data[4..];
257 Ok(BatchPostingReportV2Data {
258 batch_timestamp: u256_to_u64(&args[0..32], "batch_timestamp")?,
259 batch_poster: Address::from_slice(&args[44..64]),
260 batch_calldata_length: u256_to_u64(&args[96..128], "batch_calldata_length")?,
261 batch_calldata_non_zeros: u256_to_u64(&args[128..160], "batch_calldata_non_zeros")?,
262 batch_extra_gas: u256_to_u64(&args[160..192], "batch_extra_gas")?,
263 l1_base_fee: U256::from_be_slice(&args[192..224]),
264 })
265}
266
267pub struct InternalTxContext {
273 pub block_number: u64,
274 pub current_time: u64,
275 pub prev_hash: B256,
276}
277
278pub fn apply_internal_tx_update<D: revm::Database, B: Burner, F, G>(
285 data: &[u8],
286 state: &mut ArbosState<D, B>,
287 ctx: &InternalTxContext,
288 mut transfer_fn: F,
289 mut balance_of: G,
290) -> Result<(), String>
291where
292 F: FnMut(Address, Address, U256) -> Result<(), ()>,
293 G: FnMut(Address) -> U256,
294{
295 if data.len() < 4 {
296 return Err(format!(
297 "internal tx data too short ({} bytes, need at least 4)",
298 data.len()
299 ));
300 }
301
302 let selector: [u8; 4] = data[0..4].try_into().unwrap();
303
304 match selector {
305 INTERNAL_TX_START_BLOCK_METHOD_ID => {
306 let inputs = decode_start_block_data(data)?;
307 apply_start_block(inputs, state, ctx, &mut transfer_fn, &mut balance_of)
308 }
309 INTERNAL_TX_BATCH_POSTING_REPORT_METHOD_ID => {
310 let inputs = decode_batch_posting_report(data)?;
311 apply_batch_posting_report(inputs, state, ctx, &mut transfer_fn)
312 }
313 INTERNAL_TX_BATCH_POSTING_REPORT_V2_METHOD_ID => {
314 let inputs = decode_batch_posting_report_v2(data)?;
315 apply_batch_posting_report_v2(inputs, state, ctx, &mut transfer_fn)
316 }
317 _ => Err(format!(
318 "unknown internal tx selector: {:02x}{:02x}{:02x}{:02x}",
319 selector[0], selector[1], selector[2], selector[3]
320 )),
321 }
322}
323
324fn apply_start_block<D: revm::Database, B: Burner, F, G>(
325 inputs: StartBlockData,
326 state: &mut ArbosState<D, B>,
327 ctx: &InternalTxContext,
328 transfer_fn: &mut F,
329 balance_of: &mut G,
330) -> Result<(), String>
331where
332 F: FnMut(Address, Address, U256) -> Result<(), ()>,
333 G: FnMut(Address) -> U256,
334{
335 let arbos_version = state.arbos_version();
336
337 let mut l1_block_number = inputs.l1_block_number;
338 let mut time_passed = inputs.time_passed;
339
340 if arbos_version < arbos_version::ARBOS_VERSION_3 {
342 time_passed = inputs.l2_block_number;
343 }
344
345 if arbos_version < arbos_version::ARBOS_VERSION_8 {
347 l1_block_number = l1_block_number.saturating_add(1);
348 }
349
350 let old_l1_block_number = state
352 .blockhashes
353 .l1_block_number()
354 .map_err(|_| "failed to read l1 block number")?;
355
356 if l1_block_number > old_l1_block_number {
357 state
358 .blockhashes
359 .record_new_l1_block(l1_block_number - 1, ctx.prev_hash, arbos_version)
360 .map_err(|_| "failed to record L1 block")?;
361 }
362
363 let _ = state.retryable_state.try_to_reap_one_retryable(
365 ctx.current_time,
366 &mut *transfer_fn,
367 &mut *balance_of,
368 );
369 let _ = state.retryable_state.try_to_reap_one_retryable(
370 ctx.current_time,
371 &mut *transfer_fn,
372 &mut *balance_of,
373 );
374
375 let _ = state
377 .l2_pricing_state
378 .update_pricing_model(time_passed, arbos_version);
379
380 state
382 .upgrade_arbos_version_if_necessary(ctx.current_time)
383 .map_err(|_| "ArbOS upgrade failed (node may be out of date)")?;
384
385 Ok(())
386}
387
388fn apply_batch_posting_report<D: revm::Database, B: Burner, F>(
389 inputs: BatchPostingReportData,
390 state: &mut ArbosState<D, B>,
391 ctx: &InternalTxContext,
392 transfer_fn: &mut F,
393) -> Result<(), String>
394where
395 F: FnMut(Address, Address, U256) -> Result<(), ()>,
396{
397 let per_batch_gas = state.l1_pricing_state.per_batch_gas_cost().unwrap_or(0);
398
399 let batch_data_gas_i64 = i64::try_from(inputs.batch_data_gas).unwrap_or(i64::MAX);
402 let gas_spent_signed = per_batch_gas.saturating_add(batch_data_gas_i64);
403 let gas_spent = gas_spent_signed.max(0) as u64;
404 let wei_spent = inputs.l1_base_fee.saturating_mul(U256::from(gas_spent));
405
406 if let Err(e) = state.l1_pricing_state.update_for_batch_poster_spending(
407 inputs.batch_timestamp,
408 ctx.current_time,
409 inputs.batch_poster,
410 wei_spent,
411 inputs.l1_base_fee,
412 &mut *transfer_fn,
413 ) {
414 tracing::warn!(error = ?e, "L1 pricing update failed for batch posting report");
415 }
416
417 Ok(())
418}
419
420fn apply_batch_posting_report_v2<D: revm::Database, B: Burner, F>(
421 inputs: BatchPostingReportV2Data,
422 state: &mut ArbosState<D, B>,
423 ctx: &InternalTxContext,
424 transfer_fn: &mut F,
425) -> Result<(), String>
426where
427 F: FnMut(Address, Address, U256) -> Result<(), ()>,
428{
429 let arbos_version = state.arbos_version();
430
431 let mut gas_spent = legacy_cost_for_stats(&BatchDataStats {
433 length: inputs.batch_calldata_length,
434 non_zeros: inputs.batch_calldata_non_zeros,
435 });
436
437 gas_spent = gas_spent.saturating_add(inputs.batch_extra_gas);
438
439 let per_batch_gas = state.l1_pricing_state.per_batch_gas_cost().unwrap_or(0);
441
442 gas_spent = gas_spent.saturating_add(per_batch_gas.max(0) as u64);
443
444 if arbos_version >= arbos_version::ARBOS_VERSION_50 {
446 let gas_floor_per_token = state
447 .l1_pricing_state
448 .parent_gas_floor_per_token()
449 .unwrap_or(0);
450
451 let total_tokens = inputs
452 .batch_calldata_length
453 .saturating_add(inputs.batch_calldata_non_zeros.saturating_mul(3))
454 .saturating_add(FLOOR_GAS_ADDITIONAL_TOKENS);
455
456 let floor_gas_spent = gas_floor_per_token
457 .saturating_mul(total_tokens)
458 .saturating_add(TX_GAS);
459
460 if floor_gas_spent > gas_spent {
461 gas_spent = floor_gas_spent;
462 }
463 }
464
465 let wei_spent = inputs.l1_base_fee.saturating_mul(U256::from(gas_spent));
466
467 if let Err(e) = state.l1_pricing_state.update_for_batch_poster_spending(
468 inputs.batch_timestamp,
469 ctx.current_time,
470 inputs.batch_poster,
471 wei_spent,
472 inputs.l1_base_fee,
473 &mut *transfer_fn,
474 ) {
475 tracing::warn!(error = ?e, "L1 pricing update failed for batch posting report v2");
476 }
477
478 Ok(())
479}