diff --git a/Cargo.lock b/Cargo.lock index 040e90404f4..b3a68a1cbdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10411,7 +10411,9 @@ dependencies = [ "reth-ethereum-consensus", "reth-execution-types", "reth-primitives-traits", + "reth-scroll-chainspec", "reth-scroll-primitives", + "scroll-alloy-consensus", "scroll-alloy-hardforks", "thiserror 2.0.12", "tracing", diff --git a/crates/scroll/alloy/consensus/src/lib.rs b/crates/scroll/alloy/consensus/src/lib.rs index c6eb0dad6ea..061a6d6969f 100644 --- a/crates/scroll/alloy/consensus/src/lib.rs +++ b/crates/scroll/alloy/consensus/src/lib.rs @@ -13,8 +13,8 @@ extern crate alloc as std; mod transaction; pub use transaction::{ ScrollAdditionalInfo, ScrollL1MessageTransactionFields, ScrollPooledTransaction, - ScrollTransactionInfo, ScrollTxEnvelope, ScrollTxType, ScrollTypedTransaction, TxL1Message, - L1_MESSAGE_TRANSACTION_TYPE, L1_MESSAGE_TX_TYPE_ID, + ScrollTransaction, ScrollTransactionInfo, ScrollTxEnvelope, ScrollTxType, + ScrollTypedTransaction, TxL1Message, L1_MESSAGE_TRANSACTION_TYPE, L1_MESSAGE_TX_TYPE_ID, }; mod receipt; diff --git a/crates/scroll/alloy/consensus/src/transaction/envelope.rs b/crates/scroll/alloy/consensus/src/transaction/envelope.rs index 4fd4b8d1e2b..2905667012f 100644 --- a/crates/scroll/alloy/consensus/src/transaction/envelope.rs +++ b/crates/scroll/alloy/consensus/src/transaction/envelope.rs @@ -424,6 +424,30 @@ impl ScrollTxEnvelope { } } +/// A Scroll chain transaction. +pub trait ScrollTransaction { + /// Returns true if the transaction is a L1 message. + fn is_l1_message(&self) -> bool; + /// Returns the queue index if the transaction is a L1 message, None otherwise. + fn queue_index(&self) -> Option; +} + +impl ScrollTransaction for ScrollTxEnvelope { + fn is_l1_message(&self) -> bool { + match self { + Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) | Self::Eip7702(_) => false, + Self::L1Message(_) => true, + } + } + + fn queue_index(&self) -> Option { + match self { + Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) | Self::Eip7702(_) => None, + Self::L1Message(tx) => Some(tx.queue_index), + } + } +} + #[cfg(feature = "reth-codec")] impl ToTxCompact for ScrollTxEnvelope { fn to_tx_compact(&self, buf: &mut (impl BufMut + AsMut<[u8]>)) { diff --git a/crates/scroll/alloy/consensus/src/transaction/mod.rs b/crates/scroll/alloy/consensus/src/transaction/mod.rs index c43aab42897..b2f08383ce6 100644 --- a/crates/scroll/alloy/consensus/src/transaction/mod.rs +++ b/crates/scroll/alloy/consensus/src/transaction/mod.rs @@ -4,7 +4,7 @@ mod tx_type; pub use tx_type::{ScrollTxType, L1_MESSAGE_TX_TYPE_ID}; mod envelope; -pub use envelope::ScrollTxEnvelope; +pub use envelope::{ScrollTransaction, ScrollTxEnvelope}; mod l1_message; pub use l1_message::{ScrollL1MessageTransactionFields, TxL1Message, L1_MESSAGE_TRANSACTION_TYPE}; diff --git a/crates/scroll/consensus/Cargo.toml b/crates/scroll/consensus/Cargo.toml index f9c6c1319f2..0456665bafa 100644 --- a/crates/scroll/consensus/Cargo.toml +++ b/crates/scroll/consensus/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # alloy alloy-consensus.workspace = true -alloy-primitives.workspace = true +alloy-primitives = { workspace = true, features = ["getrandom"] } # reth reth-chainspec.workspace = true @@ -26,6 +26,7 @@ reth-primitives-traits.workspace = true # scroll reth-scroll-primitives = { workspace = true, default-features = false } +scroll-alloy-consensus.workspace = true scroll-alloy-hardforks.workspace = true # misc @@ -34,3 +35,6 @@ tracing.workspace = true [package.metadata.cargo-udeps.ignore] normal = ["reth-primitives"] + +[dev-dependencies] +reth-scroll-chainspec.workspace = true diff --git a/crates/scroll/consensus/src/constants.rs b/crates/scroll/consensus/src/constants.rs index 88c6f798d0a..188aeb896d5 100644 --- a/crates/scroll/consensus/src/constants.rs +++ b/crates/scroll/consensus/src/constants.rs @@ -2,3 +2,12 @@ use alloy_primitives::U256; /// The maximum value Rollup fee. pub const MAX_ROLLUP_FEE: U256 = U256::from_limbs([u64::MAX, 0, 0, 0]); + +/// The block difficulty for in turn signing in the Clique consensus. +pub const CLIQUE_IN_TURN_DIFFICULTY: U256 = U256::from_limbs([2, 0, 0, 0]); + +/// The block difficulty for out of turn signing in the Clique consensus. +pub const CLIQUE_NO_TURN_DIFFICULTY: U256 = U256::from_limbs([1, 0, 0, 0]); + +/// Maximum allowed base fee. We would only go above this if L1 base fee hits 2931 Gwei. +pub const SCROLL_MAXIMUM_BASE_FEE: u64 = 10000000000; diff --git a/crates/scroll/consensus/src/error.rs b/crates/scroll/consensus/src/error.rs index 39bae3f2eaf..09eca6d9b71 100644 --- a/crates/scroll/consensus/src/error.rs +++ b/crates/scroll/consensus/src/error.rs @@ -1,3 +1,6 @@ +use crate::constants::SCROLL_MAXIMUM_BASE_FEE; + +use alloy_primitives::{Address, B256, B64, U256}; use reth_consensus::ConsensusError; /// Scroll consensus error. @@ -6,6 +9,42 @@ pub enum ScrollConsensusError { /// L1 [`ConsensusError`], that also occurs on L2. #[error(transparent)] Eth(#[from] ConsensusError), + /// Invalid L1 messages order. + #[error("invalid L1 message order")] + InvalidL1MessageOrder, + /// Block has non zero coinbase. + #[error("block coinbase not zero: {0}")] + CoinbaseNotZero(Address), + /// Block has non zero nonce. + #[error("block nonce not zero: {0:?}")] + NonceNotZero(Option), + /// Block has invalid clique nonce. + #[error("block nonce should be 0x0 or 0xffffffffffffffff: {0:?}")] + InvalidCliqueNonce(Option), + /// Block has non zero mix hash. + #[error("block mix hash not zero: {0:?}")] + MixHashNotZero(Option), + /// Block difficulty is not one. + #[error("block difficulty not one: {0}")] + DifficultyNotOne(U256), + /// Block has invalid clique difficulty. + #[error("block difficulty should be 1 or 2: {0}")] + InvalidCliqueDifficulty(U256), + /// Block extra data missing vanity. + #[error("block extra data missing vanity")] + MissingVanity, + /// Block extra data missing signature. + #[error("block extra data missing signature")] + MissingSignature, + /// Block extra data with invalid checkpoint signers. + #[error("block extra data contains invalid checkpoint signers")] + InvalidCheckpointSigners, + /// Block base fee present before Curie. + #[error("block base fee is set before Curie fork activation")] + UnexpectedBaseFee, + /// Block base fee over limit. + #[error("block base fee is over limit of {SCROLL_MAXIMUM_BASE_FEE}")] + BaseFeeOverLimit, /// Block body has non-empty withdrawals list. #[error("non-empty block body withdrawals list")] WithdrawalsNonEmpty, @@ -13,3 +52,12 @@ pub enum ScrollConsensusError { #[error("unexpected blob params at timestamp")] UnexpectedBlobParams, } + +impl From for ConsensusError { + fn from(value: ScrollConsensusError) -> Self { + match value { + ScrollConsensusError::Eth(eth) => eth, + err => Self::Other(err.to_string()), + } + } +} diff --git a/crates/scroll/consensus/src/lib.rs b/crates/scroll/consensus/src/lib.rs index 3483ae79067..4e29d6c461a 100644 --- a/crates/scroll/consensus/src/lib.rs +++ b/crates/scroll/consensus/src/lib.rs @@ -3,7 +3,9 @@ extern crate alloc; mod constants; -pub use constants::MAX_ROLLUP_FEE; +pub use constants::{ + CLIQUE_IN_TURN_DIFFICULTY, CLIQUE_NO_TURN_DIFFICULTY, MAX_ROLLUP_FEE, SCROLL_MAXIMUM_BASE_FEE, +}; mod error; pub use error::ScrollConsensusError; diff --git a/crates/scroll/consensus/src/validation.rs b/crates/scroll/consensus/src/validation.rs index adcd2706f85..552b9b63e81 100644 --- a/crates/scroll/consensus/src/validation.rs +++ b/crates/scroll/consensus/src/validation.rs @@ -1,9 +1,12 @@ -use crate::error::ScrollConsensusError; +use crate::{ + constants::SCROLL_MAXIMUM_BASE_FEE, error::ScrollConsensusError, CLIQUE_IN_TURN_DIFFICULTY, + CLIQUE_NO_TURN_DIFFICULTY, +}; use alloc::sync::Arc; -use core::fmt::Debug; use alloy_consensus::{BlockHeader as _, TxReceipt, EMPTY_OMMER_ROOT_HASH}; -use alloy_primitives::B256; +use alloy_primitives::{b64, Address, B256, B64, U256}; +use core::fmt::Debug; use reth_chainspec::{EthChainSpec, EthereumHardforks}; use reth_consensus::{ validate_state_root, Consensus, ConsensusError, FullConsensus, HeaderValidator, @@ -13,11 +16,14 @@ use reth_consensus_common::validation::{ }; use reth_execution_types::BlockExecutionResult; use reth_primitives_traits::{ - receipt::gas_spent_by_transactions, Block, BlockBody, BlockHeader, GotExpected, NodePrimitives, - RecoveredBlock, SealedBlock, SealedHeader, + constants::{GAS_LIMIT_BOUND_DIVISOR, MINIMUM_GAS_LIMIT}, + receipt::gas_spent_by_transactions, + Block, BlockBody, BlockHeader, GotExpected, NodePrimitives, RecoveredBlock, SealedBlock, + SealedHeader, SignedTransaction, }; use reth_scroll_primitives::ScrollReceipt; -use scroll_alloy_hardforks::{ScrollHardfork, ScrollHardforks}; +use scroll_alloy_consensus::ScrollTransaction; +use scroll_alloy_hardforks::ScrollHardforks; /// Scroll consensus implementation. /// @@ -35,8 +41,10 @@ impl ScrollBeaconConsensus { } } -impl> - FullConsensus for ScrollBeaconConsensus +impl< + ChainSpec: EthChainSpec + ScrollHardforks, + N: NodePrimitives, + > FullConsensus for ScrollBeaconConsensus { fn validate_block_post_execution( &self, @@ -75,8 +83,16 @@ impl Consensus - for ScrollBeaconConsensus +/// Following fields should be checked on body: +/// - Verify no ommers are present and hash to the header ommer root. +/// - Verify transactions trie root is valid. +/// - Validate L1 messages: should be at the start of the list of transactions and been continuous +/// in regard to the queue index. +impl Consensus for ScrollBeaconConsensus +where + B: Block, + ::Transaction: ScrollTransaction, + ChainSpec: EthChainSpec + ScrollHardforks, { type Error = ConsensusError; @@ -89,6 +105,12 @@ impl Consensus } fn validate_block_pre_execution(&self, block: &SealedBlock) -> Result<(), ConsensusError> { + // Check no ommers. + let ommers_len = block.body().ommers().map(|o| o.len()).unwrap_or_default(); + if ommers_len > 0 { + return Err(ConsensusError::Other("uncles not allowed".to_string())) + } + // Check ommers hash let ommers_hash = block.body().calculate_ommers_root(); if Some(block.ommers_hash()) != ommers_hash { @@ -111,6 +133,13 @@ impl Consensus return Err(ConsensusError::Other(ScrollConsensusError::WithdrawalsNonEmpty.to_string())) } + // Check L1 messages. + let ts = block.header().timestamp(); + validate_l1_messages( + block.body().transactions(), + self.chain_spec.is_euclid_v2_active_at_timestamp(ts), + )?; + Ok(()) } } @@ -119,12 +148,11 @@ impl HeaderValidator< for ScrollBeaconConsensus { fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { - if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH { - return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty) - } - + validate_header_timestamp(header.header())?; + validate_header_fields(header.header(), &self.chain_spec)?; validate_header_gas(header.header())?; - validate_header_base_fee(header.header(), &self.chain_spec) + validate_header_base_fee(header.header(), &self.chain_spec)?; + Ok(()) } fn validate_header_against_parent( @@ -134,10 +162,7 @@ impl HeaderValidator< ) -> Result<(), ConsensusError> { validate_against_parent_hash_number(header.header(), parent)?; validate_against_parent_timestamp(header.header(), parent.header())?; - - // TODO(scroll): we should have a way to validate the base fee from the header - // against the parent header using - // + validate_against_parent_gas_limit(header.header(), parent.header())?; // ensure that the blob gas fields for this block if self.chain_spec.blob_params_at_timestamp(header.timestamp()).is_some() { @@ -158,16 +183,120 @@ impl HeaderValidator< } } +#[inline] +fn validate_header_fields( + header: &H, + chain_spec: ChainSpec, +) -> Result<(), ScrollConsensusError> { + // Common checks to pre and post Euclid v2. + if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH { + return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty.into()) + } + if header.mix_hash() != Some(B256::ZERO) { + return Err(ScrollConsensusError::MixHashNotZero(header.mix_hash())) + } + + if chain_spec.is_euclid_v2_active_at_timestamp(header.timestamp()) { + verify_header_fields_post_euclid_v2(header)?; + } else { + let clique_config = + chain_spec.genesis().config.clique.expect("clique config required pre euclid v2"); + let epoch = clique_config.epoch.expect("epoch required pre euclid v2"); + verify_header_fields_pre_euclid_v2(header, epoch)?; + } + + Ok(()) +} + +/// Verify the header's field for post Euclid v2 blocks. +#[inline] +fn verify_header_fields_post_euclid_v2( + header: &H, +) -> Result<(), ScrollConsensusError> { + if header.beneficiary() != Address::ZERO { + return Err(ScrollConsensusError::CoinbaseNotZero(header.beneficiary())) + } + if header.nonce() != Some(B64::ZERO) { + return Err(ScrollConsensusError::NonceNotZero(header.nonce())) + } + if header.difficulty() != U256::ONE { + return Err(ScrollConsensusError::DifficultyNotOne(header.difficulty())) + } + if !header.extra_data().is_empty() { + return Err(ConsensusError::ExtraDataExceedsMax { len: header.extra_data().len() }.into()) + } + + Ok(()) +} + +/// Verify the header's field for pre Euclid v2 blocks. +#[inline] +fn verify_header_fields_pre_euclid_v2( + header: &H, + epoch: u64, +) -> Result<(), ScrollConsensusError> { + let is_checkpoint = header.number().is_multiple_of(epoch); + if is_checkpoint && header.beneficiary() != Address::ZERO { + return Err(ScrollConsensusError::CoinbaseNotZero(header.beneficiary())) + } + if header.nonce() != Some(B64::ZERO) && header.nonce() != Some(b64!("ffffffffffffffff")) { + return Err(ScrollConsensusError::InvalidCliqueNonce(header.nonce())) + } + if is_checkpoint && header.nonce() != Some(B64::ZERO) { + return Err(ScrollConsensusError::NonceNotZero(header.nonce())) + } + if header.extra_data().len() < 32 { + return Err(ScrollConsensusError::MissingVanity) + } + if header.extra_data().len() < 32 + 65 { + return Err(ScrollConsensusError::MissingSignature) + } + let signer_bytes = header.extra_data().len() - 32 - 65; + if !is_checkpoint && signer_bytes > 0 { + return Err(ScrollConsensusError::InvalidCheckpointSigners) + } + let difficulty = header.difficulty(); + if difficulty != CLIQUE_IN_TURN_DIFFICULTY && difficulty != CLIQUE_NO_TURN_DIFFICULTY { + return Err(ScrollConsensusError::InvalidCliqueDifficulty(difficulty)) + } + + Ok(()) +} + +/// Validates the timestamp of the header, which should not be in the future. +#[inline] +fn validate_header_timestamp(header: &H) -> Result<(), ConsensusError> { + let now = std::time::SystemTime::now(); + let since_unix_epoch = now.duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs(); + if header.timestamp() > since_unix_epoch { + return Err(ConsensusError::TimestampIsInPast { + parent_timestamp: since_unix_epoch, + timestamp: header.timestamp(), + }) + } + Ok(()) +} + /// Ensure the EIP-1559 base fee is set if the Curie hardfork is active. #[inline] fn validate_header_base_fee( header: &H, chain_spec: &ChainSpec, -) -> Result<(), ConsensusError> { - if chain_spec.scroll_fork_activation(ScrollHardfork::Curie).active_at_block(header.number()) && - header.base_fee_per_gas().is_none() +) -> Result<(), ScrollConsensusError> { + if chain_spec.is_curie_active_at_block(header.number()) { + if header.base_fee_per_gas().is_none() { + return Err(ConsensusError::BaseFeeMissing.into()) + } + // note: we do not verify L2 base fee, the sequencer has the + // right to set any base fee below the maximum. L2 base fee + // is not subject to L2 consensus or zk verification. + if header.base_fee_per_gas().expect("checked") > SCROLL_MAXIMUM_BASE_FEE { + return Err(ScrollConsensusError::BaseFeeOverLimit) + } + } + if !chain_spec.is_curie_active_at_block(header.number()) && header.base_fee_per_gas().is_some() { - return Err(ConsensusError::BaseFeeMissing) + return Err(ScrollConsensusError::UnexpectedBaseFee) } Ok(()) } @@ -189,3 +318,524 @@ fn validate_against_parent_timestamp( } Ok(()) } + +/// Validates the gas limit of the block against the parent. +#[inline] +fn validate_against_parent_gas_limit( + header: &H, + parent: &H, +) -> Result<(), ConsensusError> { + let diff = header.gas_limit().abs_diff(parent.gas_limit()); + let limit = parent.gas_limit() / GAS_LIMIT_BOUND_DIVISOR; + if diff > limit { + return if header.gas_limit() > parent.gas_limit() { + Err(ConsensusError::GasLimitInvalidIncrease { + parent_gas_limit: parent.gas_limit(), + child_gas_limit: parent.gas_limit(), + }) + } else { + Err(ConsensusError::GasLimitInvalidDecrease { + parent_gas_limit: parent.gas_limit(), + child_gas_limit: parent.gas_limit(), + }) + } + } + // Check that the gas limit is above the minimum allowed gas limit. + if header.gas_limit() < MINIMUM_GAS_LIMIT { + return Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: header.gas_limit() }) + } + Ok(()) +} + +/// Validate the L1 messages by checking they are only present that the start of the block and only +/// have increasing queue index. +#[inline] +fn validate_l1_messages( + txs: &[Tx], + is_euclid_v2: bool, +) -> Result<(), ScrollConsensusError> { + // Check L1 messages are only at the start of the block and correctly ordered. + let mut saw_l2_transaction = false; + let mut queue_index = txs + .iter() + .find(|tx| tx.is_l1_message()) + .and_then(|tx| tx.queue_index()) + .unwrap_or_default(); + + // starting at EuclidV2, we don't skip L1 messages. + let l1_message_index_check: fn(u64, u64) -> bool = if is_euclid_v2 { + |tx_queue_index, queue_index| tx_queue_index != queue_index + } else { + |tx_queue_index, queue_index| tx_queue_index < queue_index + }; + + for tx in txs { + // Check index is strictly increasing pre EuclidV2 and sequential post EuclidV2. + if tx.is_l1_message() { + let tx_queue_index = tx.queue_index().expect("is_l1_message"); + if l1_message_index_check(tx_queue_index, queue_index) { + return Err(ScrollConsensusError::InvalidL1MessageOrder); + } + queue_index = tx_queue_index + 1; + } + + // Check correct ordering. + if tx.is_l1_message() && saw_l2_transaction { + return Err(ScrollConsensusError::InvalidL1MessageOrder); + } + saw_l2_transaction = !tx.is_l1_message(); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ScrollConsensusError; + + use alloy_consensus::{Header, Signed, TxEip1559}; + use alloy_primitives::{b64, Address, Bloom, Bytes, Signature, B256, U256}; + use reth_consensus::ConsensusError; + use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MINIMUM_GAS_LIMIT}; + use reth_scroll_chainspec::SCROLL_MAINNET; + use scroll_alloy_consensus::{ScrollTxEnvelope, TxL1Message}; + + fn create_test_header() -> Header { + Header { + parent_hash: B256::random(), + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: Address::ZERO, + state_root: B256::random(), + transactions_root: B256::random(), + receipts_root: B256::random(), + logs_bloom: Bloom::default(), + difficulty: U256::ONE, + number: 1, + gas_limit: 30000000, + gas_used: 0, + timestamp: 1000, + extra_data: Bytes::new(), + mix_hash: B256::ZERO, + nonce: B64::ZERO, + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + } + } + + #[test] + fn test_validate_header_timestamp_success() { + let header = create_test_header(); + assert!(validate_header_timestamp(&header).is_ok()); + } + + #[test] + fn test_validate_header_timestamp_future() { + let mut header = create_test_header(); + // timestamp in the future. + header.timestamp = u64::MAX; + + let result = validate_header_timestamp(&header); + assert!(matches!(result, Err(ConsensusError::TimestampIsInPast { .. }))); + } + + #[test] + fn test_verify_header_fields_post_euclid_v2_success() { + let header = create_test_header(); + assert!(verify_header_fields_post_euclid_v2(&header).is_ok()); + } + + #[test] + fn test_verify_header_fields_post_euclid_v2_coinbase_not_zero() { + let mut header = create_test_header(); + header.beneficiary = Address::random(); + + let result = verify_header_fields_post_euclid_v2(&header); + assert!(matches!(result, Err(ScrollConsensusError::CoinbaseNotZero(_)))); + } + + #[test] + fn test_verify_header_fields_post_euclid_v2_nonce_not_zero() { + let mut header = create_test_header(); + header.nonce = b64!("0123456789abcdef"); + + let result = verify_header_fields_post_euclid_v2(&header); + assert!(matches!(result, Err(ScrollConsensusError::NonceNotZero(_)))); + } + + #[test] + fn test_verify_header_fields_post_euclid_v2_difficulty_not_one() { + let mut header = create_test_header(); + header.difficulty = U256::from(2); + + let result = verify_header_fields_post_euclid_v2(&header); + assert!(matches!(result, Err(ScrollConsensusError::DifficultyNotOne(_)))); + } + + #[test] + fn test_verify_header_fields_post_euclid_v2_extra_data_not_empty() { + let mut header = create_test_header(); + header.extra_data = Bytes::from(vec![1, 2, 3]); + + let result = verify_header_fields_post_euclid_v2(&header); + assert!(matches!( + result, + Err(ScrollConsensusError::Eth(ConsensusError::ExtraDataExceedsMax { .. })) + )); + } + + #[test] + fn test_verify_header_fields_pre_euclid_v2_success() { + let mut header = create_test_header(); + // valid extra data (32 bytes vanity + 65 bytes signature). + let mut extra_data = vec![0u8; 32]; + extra_data.extend_from_slice(&[0u8; 65]); + header.extra_data = Bytes::from(extra_data); + + assert!(verify_header_fields_pre_euclid_v2(&header, 30000).is_ok()); + } + + #[test] + fn test_verify_header_fields_pre_euclid_v2_checkpoint_coinbase_not_zero() { + let mut header = create_test_header(); + // checkpoint block. + header.number = 0; + header.beneficiary = Address::random(); + let mut extra_data = vec![0u8; 32]; + extra_data.extend_from_slice(&[0u8; 65]); + header.extra_data = Bytes::from(extra_data); + + let result = verify_header_fields_pre_euclid_v2(&header, 30000); + assert!(matches!(result, Err(ScrollConsensusError::CoinbaseNotZero(_)))); + } + + #[test] + fn test_verify_header_fields_pre_euclid_v2_invalid_nonce() { + let mut header = create_test_header(); + // invalid nonce. + header.nonce = b64!("1234567890abcdef"); + let mut extra_data = vec![0u8; 32]; + extra_data.extend_from_slice(&[0u8; 65]); + header.extra_data = Bytes::from(extra_data); + + let result = verify_header_fields_pre_euclid_v2(&header, 30000); + assert!(matches!(result, Err(ScrollConsensusError::InvalidCliqueNonce(_)))); + } + + #[test] + fn test_verify_header_fields_pre_euclid_v2_missing_vanity() { + let mut header = create_test_header(); + // vanity too short. + header.extra_data = Bytes::from(vec![0u8; 31]); + + let result = verify_header_fields_pre_euclid_v2(&header, 30000); + assert!(matches!(result, Err(ScrollConsensusError::MissingVanity))); + } + + #[test] + fn test_verify_header_fields_pre_euclid_v2_missing_signature() { + let mut header = create_test_header(); + // signature too short. + header.extra_data = Bytes::from(vec![0u8; 32]); + + let result = verify_header_fields_pre_euclid_v2(&header, 30000); + assert!(matches!(result, Err(ScrollConsensusError::MissingSignature))); + } + + #[test] + fn test_verify_header_fields_pre_euclid_v2_invalid_difficulty() { + let mut header = create_test_header(); + // invalid difficulty. + header.difficulty = U256::from(3); + let mut extra_data = vec![0u8; 32]; + extra_data.extend_from_slice(&[0u8; 65]); + header.extra_data = Bytes::from(extra_data); + + let result = verify_header_fields_pre_euclid_v2(&header, 30000); + assert!(matches!(result, Err(ScrollConsensusError::InvalidCliqueDifficulty(_)))); + } + + #[test] + fn test_validate_against_parent_timestamp_success() { + let parent = create_test_header(); + let mut header = create_test_header(); + header.timestamp = parent.timestamp + 1; + + assert!(validate_against_parent_timestamp(&header, &parent).is_ok()); + } + + #[test] + fn test_validate_against_parent_timestamp_same_time() { + let parent = create_test_header(); + let header = create_test_header(); + + assert!(validate_against_parent_timestamp(&header, &parent).is_ok()); + } + + #[test] + fn test_validate_against_parent_timestamp_in_past() { + let parent = create_test_header(); + let mut header = create_test_header(); + header.timestamp = parent.timestamp - 1; + + let result = validate_against_parent_timestamp(&header, &parent); + assert!(matches!(result, Err(ConsensusError::TimestampIsInPast { .. }))); + } + + #[test] + fn test_validate_against_parent_gas_limit_success() { + let parent = create_test_header(); + let mut header = create_test_header(); + // small gas increase. + header.gas_limit = parent.gas_limit + 100; + + assert!(validate_against_parent_gas_limit(&header, &parent).is_ok()); + } + + #[test] + fn test_validate_against_parent_gas_limit_too_high_increase() { + let parent = create_test_header(); + let mut header = create_test_header(); + header.gas_limit = parent.gas_limit + parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR + 1; + + let result = validate_against_parent_gas_limit(&header, &parent); + assert!(matches!(result, Err(ConsensusError::GasLimitInvalidIncrease { .. }))); + } + + #[test] + fn test_validate_against_parent_gas_limit_too_high_decrease() { + let parent = create_test_header(); + let mut header = create_test_header(); + header.gas_limit = parent.gas_limit - parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR - 1; + + let result = validate_against_parent_gas_limit(&header, &parent); + assert!(matches!(result, Err(ConsensusError::GasLimitInvalidDecrease { .. }))); + } + + #[test] + fn test_validate_against_parent_gas_limit_below_minimum() { + let mut parent = create_test_header(); + let mut header = create_test_header(); + parent.gas_limit = MINIMUM_GAS_LIMIT + 1; + header.gas_limit = MINIMUM_GAS_LIMIT - 1; + + let result = validate_against_parent_gas_limit(&header, &parent); + dbg!(&result); + assert!(matches!(result, Err(ConsensusError::GasLimitInvalidMinimum { .. }))); + } + + #[test] + fn test_validate_l1_messages_success() { + let txs: Vec = vec![ + TxL1Message { queue_index: 0, ..Default::default() }.into(), + TxL1Message { queue_index: 1, ..Default::default() }.into(), + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + ]; + + assert!(validate_l1_messages(&txs, true).is_ok()); + assert!(validate_l1_messages(&txs, false).is_ok()); + } + + #[test] + fn test_validate_l1_messages_empty() { + let txs: Vec = vec![]; + assert!(validate_l1_messages(&txs, true).is_ok()); + assert!(validate_l1_messages(&txs, false).is_ok()); + } + + #[test] + fn test_validate_l1_messages_only_l2() { + let txs: Vec = vec![ + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + ]; + + assert!(validate_l1_messages(&txs, true).is_ok()); + assert!(validate_l1_messages(&txs, false).is_ok()); + } + + #[test] + fn test_validate_l1_messages_invalid_order() { + let txs: Vec = vec![ + Signed::new_unchecked( + TxEip1559::default(), + Signature::new(U256::ZERO, U256::ZERO, false), + B256::random(), + ) + .into(), + TxL1Message { queue_index: 0, ..Default::default() }.into(), + ]; + + let result = validate_l1_messages(&txs, true); + assert!(matches!(result, Err(ScrollConsensusError::InvalidL1MessageOrder))); + let result = validate_l1_messages(&txs, false); + assert!(matches!(result, Err(ScrollConsensusError::InvalidL1MessageOrder))); + } + + #[test] + fn test_validate_l1_messages_non_sequential_queue_index() { + let txs: Vec = vec![ + TxL1Message { queue_index: 0, ..Default::default() }.into(), + TxL1Message { queue_index: 2, ..Default::default() }.into(), + ]; + + // ok as it's not decreasing. + assert!(validate_l1_messages(&txs, false).is_ok()); + // not ok as it's not sequential. + let result = validate_l1_messages(&txs, true); + assert!(matches!(result, Err(ScrollConsensusError::InvalidL1MessageOrder))); + } + + #[test] + fn test_validate_l1_messages_decreasing_queue_index() { + let txs: Vec = vec![ + TxL1Message { queue_index: 1, ..Default::default() }.into(), + TxL1Message { queue_index: 0, ..Default::default() }.into(), + ]; + + let result = validate_l1_messages(&txs, true); + assert!(matches!(result, Err(ScrollConsensusError::InvalidL1MessageOrder))); + let result = validate_l1_messages(&txs, false); + assert!(matches!(result, Err(ScrollConsensusError::InvalidL1MessageOrder))); + } + + #[test] + fn test_validate_header_base_fee_before_curie() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // pre Curie. + header.number = 500; + header.base_fee_per_gas = Some(1000000000); + + let result = validate_header_base_fee(&header, &chain_spec); + assert!(matches!(result, Err(ScrollConsensusError::UnexpectedBaseFee))); + } + + #[test] + fn test_validate_header_base_fee_after_curie_missing() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // post Curie. + header.number = 7096837; + header.base_fee_per_gas = None; + + let result = validate_header_base_fee(&header, &chain_spec); + assert!(matches!(result, Err(ScrollConsensusError::Eth(ConsensusError::BaseFeeMissing)))); + } + + #[test] + fn test_validate_header_base_fee_after_curie_over_limit() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // post Curie. + header.number = 7096837; + header.base_fee_per_gas = Some(SCROLL_MAXIMUM_BASE_FEE + 1); + + let result = validate_header_base_fee(&header, &chain_spec); + assert!(matches!(result, Err(ScrollConsensusError::BaseFeeOverLimit))); + } + + #[test] + fn test_validate_header_base_fee_after_curie_valid() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // post Curie. + header.number = 7096837; + header.base_fee_per_gas = Some(1000000000); + + let result = validate_header_base_fee(&header, &chain_spec); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_header_fields_pre_euclid_v2() { + let mut header = create_test_header(); + // pre Euclid v2. + header.timestamp = 1745305199; + // valid extra data for pre-euclid v2. + let mut extra_data = vec![0u8; 32]; + extra_data.extend_from_slice(&[0u8; 65]); + header.extra_data = Bytes::from(extra_data); + + assert!(verify_header_fields_pre_euclid_v2(&header, 30000).is_ok()); + } + + #[test] + fn test_validate_header_fields_post_euclid_v2() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // post Euclid v2. + header.timestamp = 1745305201; + + assert!(validate_header_fields(&header, &chain_spec).is_ok()); + } + + #[test] + fn test_validate_header_fields_mix_hash_not_zero() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // invalid mix hash. + header.mix_hash = B256::random(); + + let result = validate_header_fields(&header, &chain_spec); + assert!(matches!(result, Err(ScrollConsensusError::MixHashNotZero(_)))); + } + + #[test] + fn test_validate_header_fields_ommers_hash_not_empty() { + let chain_spec = SCROLL_MAINNET.clone(); + + let mut header = create_test_header(); + // invalid ommer hash. + header.ommers_hash = B256::random(); + + let result = validate_header_fields(&header, &chain_spec); + assert!(matches!( + result, + Err(ScrollConsensusError::Eth(ConsensusError::TheMergeOmmerRootIsNotEmpty)) + )); + } +} diff --git a/crates/scroll/node/src/builder/consensus.rs b/crates/scroll/node/src/builder/consensus.rs index fcb25235ad1..05e6601827d 100644 --- a/crates/scroll/node/src/builder/consensus.rs +++ b/crates/scroll/node/src/builder/consensus.rs @@ -4,6 +4,7 @@ use reth_node_types::NodeTypes; use reth_primitives_traits::NodePrimitives; use reth_scroll_consensus::ScrollBeaconConsensus; use reth_scroll_primitives::ScrollReceipt; +use scroll_alloy_consensus::ScrollTransaction; use scroll_alloy_hardforks::ScrollHardforks; use std::sync::Arc; @@ -16,7 +17,7 @@ where Node: FullNodeTypes< Types: NodeTypes< ChainSpec: EthChainSpec + ScrollHardforks, - Primitives: NodePrimitives, + Primitives: NodePrimitives, >, >, { diff --git a/crates/scroll/node/src/builder/engine.rs b/crates/scroll/node/src/builder/engine.rs index 244017f8e84..09a564b2afa 100644 --- a/crates/scroll/node/src/builder/engine.rs +++ b/crates/scroll/node/src/builder/engine.rs @@ -2,7 +2,6 @@ use crate::addons::ScrollNodeTypes; use std::sync::Arc; use alloy_consensus::BlockHeader; -use alloy_primitives::U256; use alloy_rpc_types_engine::{ExecutionData, PayloadError}; use reth_node_api::{ AddOnsContext, EngineApiMessageVersion, EngineApiValidator, EngineObjectValidationError, @@ -13,16 +12,12 @@ use reth_node_api::{ use reth_node_builder::rpc::PayloadValidatorBuilder; use reth_node_types::NodeTypes; use reth_primitives_traits::{Block, RecoveredBlock}; +use reth_scroll_consensus::{CLIQUE_IN_TURN_DIFFICULTY, CLIQUE_NO_TURN_DIFFICULTY}; use reth_scroll_engine_primitives::try_into_block; use reth_scroll_primitives::ScrollBlock; use scroll_alloy_hardforks::ScrollHardforks; use scroll_alloy_rpc_types_engine::ScrollPayloadAttributes; -/// The block difficulty for in turn signing in the Clique consensus. -const CLIQUE_IN_TURN_DIFFICULTY: U256 = U256::from_limbs([2, 0, 0, 0]); -/// The block difficulty for out of turn signing in the Clique consensus. -const CLIQUE_NO_TURN_DIFFICULTY: U256 = U256::from_limbs([1, 0, 0, 0]); - /// Builder for [`ScrollEngineValidator`]. #[derive(Debug, Default, Clone)] #[non_exhaustive]