1use alloy_primitives::{Address, B256, U256};
2use arb_chainspec::arbos_version as arb_ver;
3
4use crate::{header::ArbHeaderInfo, internal_tx::L1Info, l2_pricing::GETH_BLOCK_GAS_LIMIT};
5
6const TX_GAS: u64 = 21_000;
8
9#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
16#[error("sequencer rejected transaction: {reason}")]
17pub struct FilterReject {
18 pub reason: String,
20}
21
22impl FilterReject {
23 pub fn new(reason: impl Into<String>) -> Self {
25 Self {
26 reason: reason.into(),
27 }
28 }
29}
30
31#[derive(thiserror::Error, Debug)]
33pub enum BlockProcessorError {
34 #[error("internal start-block tx failed: {reason}")]
37 InternalTxFailed {
38 reason: String,
40 },
41
42 #[error("unexpected balance delta {actual} (expected {expected})")]
45 BalanceDelta {
46 actual: i128,
48 expected: i128,
50 },
51}
52
53#[derive(Debug, Clone, Default)]
59pub struct ConditionalOptions {
60 pub known_accounts: Vec<(Address, Option<B256>)>,
61 pub block_number_min: Option<u64>,
62 pub block_number_max: Option<u64>,
63 pub timestamp_min: Option<u64>,
64 pub timestamp_max: Option<u64>,
65}
66
67pub trait SequencingHooks {
73 fn next_tx_to_sequence(&mut self) -> Option<Vec<u8>>;
75
76 fn pre_tx_filter(&self, tx: &[u8]) -> Result<(), FilterReject>;
78
79 fn post_tx_filter(&self, tx: &[u8], result: &[u8]) -> Result<(), FilterReject>;
81
82 fn discard_invalid_txs_early(&self) -> bool;
84
85 fn block_filter(
87 &self,
88 _header: &NewHeaderResult,
89 _txs: &[Vec<u8>],
90 _receipts: &[Vec<u8>],
91 ) -> Result<(), FilterReject> {
92 Ok(())
93 }
94
95 fn insert_last_tx_error(&mut self, _err: String) {}
97}
98
99pub struct NoopSequencingHooks;
101
102impl SequencingHooks for NoopSequencingHooks {
103 fn next_tx_to_sequence(&mut self) -> Option<Vec<u8>> {
104 None
105 }
106
107 fn pre_tx_filter(&self, _tx: &[u8]) -> Result<(), FilterReject> {
108 Ok(())
109 }
110
111 fn post_tx_filter(&self, _tx: &[u8], _result: &[u8]) -> Result<(), FilterReject> {
112 Ok(())
113 }
114
115 fn discard_invalid_txs_early(&self) -> bool {
116 false
117 }
118}
119
120#[derive(Debug, Clone)]
126pub struct BlockProductionResult {
127 pub l1_info: L1Info,
128 pub num_txs: usize,
129 pub gas_used: u64,
130}
131
132#[derive(Debug, Clone)]
134pub struct NewHeaderParams {
135 pub parent_hash: B256,
136 pub parent_number: u64,
137 pub parent_timestamp: u64,
138 pub parent_extra_data: Vec<u8>,
139 pub parent_mix_hash: B256,
140 pub coinbase: Address,
141 pub timestamp: u64,
142 pub base_fee: U256,
143}
144
145#[derive(Debug, Clone)]
147pub struct NewHeaderResult {
148 pub parent_hash: B256,
149 pub coinbase: Address,
150 pub number: u64,
151 pub gas_limit: u64,
152 pub timestamp: u64,
153 pub extra_data: Vec<u8>,
154 pub mix_hash: B256,
155 pub base_fee: U256,
156 pub difficulty: U256,
157}
158
159pub fn create_new_header(
168 l1_info: Option<&L1Info>,
169 prev_hash: B256,
170 prev_number: u64,
171 prev_timestamp: u64,
172 prev_extra: &[u8],
173 prev_mix_hash: B256,
174 base_fee: U256,
175) -> NewHeaderResult {
176 let mut timestamp = 0u64;
177 let mut coinbase = Address::ZERO;
178
179 if let Some(info) = l1_info {
180 timestamp = info.l1_timestamp;
181 coinbase = info.poster;
182 }
183
184 if timestamp < prev_timestamp {
185 timestamp = prev_timestamp;
186 }
187
188 let mut extra_data = vec![0u8; 32];
189 let copy_len = prev_extra.len().min(32);
190 extra_data[..copy_len].copy_from_slice(&prev_extra[..copy_len]);
191
192 NewHeaderResult {
193 parent_hash: prev_hash,
194 coinbase,
195 number: prev_number + 1,
196 gas_limit: GETH_BLOCK_GAS_LIMIT,
197 timestamp,
198 extra_data,
199 mix_hash: prev_mix_hash,
200 base_fee,
201 difficulty: U256::from(1),
202 }
203}
204
205pub fn finalize_block_header_info(
210 send_root: B256,
211 send_count: u64,
212 l1_block_number: u64,
213 arbos_version: u64,
214 collect_tips: bool,
215) -> ArbHeaderInfo {
216 ArbHeaderInfo {
217 send_root,
218 send_count,
219 l1_block_number,
220 arbos_format_version: arbos_version,
221 collect_tips,
222 }
223}
224
225#[derive(Debug)]
231pub enum TxOutcome {
232 Success(TxResult),
234 Invalid(String),
236}
237
238#[derive(Debug, Clone)]
240pub struct TxResult {
241 pub gas_used: u64,
243 pub data_gas: u64,
245 pub evm_success: bool,
247 pub scheduled_txs: Vec<Vec<u8>>,
249 pub evm_error: Option<String>,
251}
252
253#[derive(Debug)]
255pub enum TxAction {
256 ExecuteStartBlock,
258 ExecuteRedeem(Vec<u8>),
260 ExecuteUserTx(Vec<u8>),
262 Done,
264}
265
266#[derive(Debug)]
273pub struct BlockProductionState {
274 pub block_gas_left: u64,
276 redeems: Vec<Vec<u8>>,
278 start_block_produced: bool,
280 user_txs_processed: usize,
282 pub expected_balance_delta: i128,
284 arbos_version: u64,
286 pub timestamp: u64,
288 pub base_fee: U256,
290}
291
292impl BlockProductionState {
293 pub fn new(
295 per_block_gas_limit: u64,
296 arbos_version: u64,
297 timestamp: u64,
298 base_fee: U256,
299 ) -> Self {
300 Self {
301 block_gas_left: per_block_gas_limit,
302 redeems: Vec::new(),
303 start_block_produced: false,
304 user_txs_processed: 0,
305 expected_balance_delta: 0,
306 arbos_version,
307 timestamp,
308 base_fee,
309 }
310 }
311
312 pub fn next_tx_action<H: SequencingHooks>(&mut self, hooks: &mut H) -> TxAction {
314 if !self.start_block_produced {
315 self.start_block_produced = true;
316 return TxAction::ExecuteStartBlock;
317 }
318
319 if !self.redeems.is_empty() {
321 let redeem = self.redeems.remove(0);
322 return TxAction::ExecuteRedeem(redeem);
323 }
324
325 match hooks.next_tx_to_sequence() {
327 Some(tx_bytes) => {
328 if self.block_gas_left < TX_GAS {
330 hooks.insert_last_tx_error("block gas limit reached".to_string());
331 return TxAction::Done;
332 }
333 TxAction::ExecuteUserTx(tx_bytes)
334 }
335 None => TxAction::Done,
336 }
337 }
338
339 pub fn should_reject_for_block_gas(&self, compute_gas: u64, is_user_tx: bool) -> bool {
345 self.arbos_version < arb_ver::ARBOS_VERSION_50
346 && compute_gas > self.block_gas_left
347 && is_user_tx
348 && self.user_txs_processed > 0
349 }
350
351 pub fn compute_data_gas(poster_cost: U256, base_fee: U256, tx_gas: u64) -> u64 {
353 if base_fee.is_zero() {
354 return 0;
355 }
356
357 let poster_cost_in_l2_gas = poster_cost / base_fee;
358 let data_gas: u64 = poster_cost_in_l2_gas.try_into().unwrap_or(u64::MAX);
359
360 data_gas.min(tx_gas)
362 }
363
364 pub fn record_tx_outcome(
369 &mut self,
370 action: &TxAction,
371 outcome: TxOutcome,
372 ) -> Result<(), BlockProcessorError> {
373 match outcome {
374 TxOutcome::Invalid(err) => {
375 match action {
377 TxAction::ExecuteUserTx(_) => {
378 self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
379 self.user_txs_processed += 1;
380 }
381 _ => {
382 self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
383 }
384 }
385 tracing::debug!(err, "tx invalid, skipped");
386 Ok(())
387 }
388 TxOutcome::Success(result) => {
389 if matches!(action, TxAction::ExecuteStartBlock)
391 && let Some(ref err) = result.evm_error
392 {
393 return Err(BlockProcessorError::InternalTxFailed {
394 reason: err.clone(),
395 });
396 }
397
398 let tx_gas_used = result.gas_used;
399 let data_gas = result.data_gas;
400
401 if self.arbos_version >= arb_ver::ARBOS_VERSION_3 {
403 for scheduled in &result.scheduled_txs {
404 let _ = scheduled; }
409 }
410
411 self.redeems.extend(result.scheduled_txs);
413
414 let compute_used = if tx_gas_used >= data_gas {
416 let c = tx_gas_used - data_gas;
417 if c < TX_GAS { TX_GAS } else { c }
418 } else {
419 tracing::error!(tx_gas_used, data_gas, "tx used less gas than expected");
420 TX_GAS
421 };
422
423 self.block_gas_left = self.block_gas_left.saturating_sub(compute_used);
424
425 if matches!(action, TxAction::ExecuteUserTx(_)) {
426 self.user_txs_processed += 1;
427 }
428
429 Ok(())
430 }
431 }
432 }
433
434 pub fn track_deposit(&mut self, value: U256) {
436 let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
437 self.expected_balance_delta = self.expected_balance_delta.saturating_add(value_i128);
438 }
439
440 pub fn track_withdrawal(&mut self, value: U256) {
442 let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
443 self.expected_balance_delta = self.expected_balance_delta.saturating_sub(value_i128);
444 }
445
446 pub fn set_arbos_version(&mut self, version: u64) {
448 self.arbos_version = version;
449 }
450
451 pub fn verify_balance_delta(
453 &self,
454 actual_balance_delta: i128,
455 debug_mode: bool,
456 ) -> Result<(), BlockProcessorError> {
457 if actual_balance_delta == self.expected_balance_delta {
458 return Ok(());
459 }
460
461 if actual_balance_delta > self.expected_balance_delta || debug_mode {
462 return Err(BlockProcessorError::BalanceDelta {
463 actual: actual_balance_delta,
464 expected: self.expected_balance_delta,
465 });
466 }
467
468 tracing::error!(
470 actual = actual_balance_delta,
471 expected = self.expected_balance_delta,
472 "unexpected balance delta (funds burnt)"
473 );
474 Ok(())
475 }
476
477 pub fn user_txs_processed(&self) -> usize {
479 self.user_txs_processed
480 }
481
482 pub fn arbos_version(&self) -> u64 {
484 self.arbos_version
485 }
486}