diff --git a/bin/client/src/fault/handler/bn128_pair.rs b/bin/client/src/fault/handler/bn128_pair.rs index 091be7e97..2d559c8ca 100644 --- a/bin/client/src/fault/handler/bn128_pair.rs +++ b/bin/client/src/fault/handler/bn128_pair.rs @@ -20,6 +20,9 @@ const PAIR_ELEMENT_LEN: usize = 64 + 128; pub(crate) const FPVM_ECPAIRING: PrecompileWithAddress = PrecompileWithAddress(ECPAIRING_ADDRESS, Precompile::Standard(fpvm_ecpairing)); +pub(crate) const FPVM_ECPAIRING_GRANITE: PrecompileWithAddress = + PrecompileWithAddress(ECPAIRING_ADDRESS, Precompile::Standard(fpvm_ecpairing_granite)); + /// Performs an FPVM-accelerated `ecpairing` precompile call. fn fpvm_ecpairing(input: &Bytes, gas_limit: u64) -> PrecompileResult { let gas_used = @@ -59,3 +62,13 @@ fn fpvm_ecpairing(input: &Bytes, gas_limit: u64) -> PrecompileResult { Ok(PrecompileOutput::new(gas_used, result_data.into())) } + +/// Performs an FPVM-accelerated `ecpairing` precompile call after the Granite hardfork. +fn fpvm_ecpairing_granite(input: &Bytes, gas_limit: u64) -> PrecompileResult { + const BN256_MAX_PAIRING_SIZE_GRANITE: usize = 112_687; + if input.len() > BN256_MAX_PAIRING_SIZE_GRANITE { + return Err(PrecompileError::Bn128PairLength.into()); + } + + fpvm_ecpairing(input, gas_limit) +} diff --git a/bin/client/src/fault/handler/mod.rs b/bin/client/src/fault/handler/mod.rs index af262610d..4df21711e 100644 --- a/bin/client/src/fault/handler/mod.rs +++ b/bin/client/src/fault/handler/mod.rs @@ -37,6 +37,11 @@ pub(crate) fn fpvm_handle_register( kzg_point_eval::FPVM_KZG_POINT_EVAL, ]; ctx_precompiles.extend(override_precompiles); + + if spec_id.is_enabled_in(SpecId::GRANITE) { + ctx_precompiles.extend([bn128_pair::FPVM_ECPAIRING_GRANITE]); + } + ctx_precompiles }); } diff --git a/crates/executor/src/errors.rs b/crates/executor/src/errors.rs index 1450d9d3d..cc7680f0c 100644 --- a/crates/executor/src/errors.rs +++ b/crates/executor/src/errors.rs @@ -12,12 +12,18 @@ pub enum ExecutorError { /// Missing gas limit in the payload attributes. #[error("Gas limit not provided in payload attributes")] MissingGasLimit, + /// Missing transactions in the payload attributes. + #[error("Transactions not provided in payload attributes")] + MissingTransactions, /// Missing EIP-1559 parameters in execution payload post-Holocene. #[error("Missing EIP-1559 parameters in execution payload post-Holocene")] MissingEIP1559Params, /// Missing parent beacon block root in the payload attributes. #[error("Parent beacon block root not provided in payload attributes")] MissingParentBeaconBlockRoot, + /// Invalid `extraData` field in the block header. + #[error("Invalid `extraData` field in the block header")] + InvalidExtraData, /// Block gas limit exceeded. #[error("Block gas limit exceeded")] BlockGasLimitExceeded, diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs index 709d5063d..79144bdfe 100644 --- a/crates/executor/src/lib.rs +++ b/crates/executor/src/lib.rs @@ -15,7 +15,7 @@ use alloy_eips::{ eip1559::BaseFeeParams, eip2718::{Decodable2718, Encodable2718}, }; -use alloy_primitives::{address, keccak256, Address, Bytes, TxKind, B256, B64, U256}; +use alloy_primitives::{address, keccak256, Address, Bytes, TxKind, B256, U256}; use kona_mpt::{ordered_trie_with_encoder, TrieDB, TrieDBError, TrieHinter, TrieProvider}; use op_alloy_consensus::{OpReceiptEnvelope, OpTxEnvelope}; use op_alloy_genesis::RollupConfig; @@ -43,7 +43,10 @@ mod canyon; use canyon::ensure_create2_deployer_canyon; mod util; -use util::{logs_bloom, receipt_envelope_from_parts}; +use util::{ + decode_holocene_eip_1559_params, encode_holocene_eip_1559_params, logs_bloom, + receipt_envelope_from_parts, +}; /// The block executor for the L2 client program. Operates off of a [TrieDB] backed [State], /// allowing for stateless block execution of OP Stack blocks. @@ -108,13 +111,15 @@ where let block_number = initialized_block_env.number.to::(); let base_fee = initialized_block_env.basefee.to::(); let gas_limit = payload.gas_limit.ok_or(ExecutorError::MissingGasLimit)?; + let transactions = + payload.transactions.as_ref().ok_or(ExecutorError::MissingTransactions)?; info!( target: "client_executor", "Executing block # {block_number} | Gas limit: {gas_limit} | Tx count: {tx_len}", block_number = block_number, gas_limit = gas_limit, - tx_len = payload.transactions.as_ref().map(|txs| txs.len()).unwrap_or_default(), + tx_len = transactions.len(), ); let mut state = @@ -138,8 +143,7 @@ where )?; let mut cumulative_gas_used = 0u64; - let mut receipts: Vec = - Vec::with_capacity(payload.transactions.as_ref().map(|t| t.len()).unwrap_or_default()); + let mut receipts: Vec = Vec::with_capacity(transactions.len()); let is_regolith = self.config.is_regolith_active(payload.payload_attributes.timestamp); // Construct the block-scoped EVM with the given configuration. @@ -162,18 +166,15 @@ where }; // Execute the transactions in the payload. - let transactions = if let Some(ref txs) = payload.transactions { - txs.iter() - .map(|raw_tx| { - let tx = OpTxEnvelope::decode_2718(&mut raw_tx.as_ref()) - .map_err(ExecutorError::RLPError)?; - Ok((tx, raw_tx.as_ref())) - }) - .collect::>>()? - } else { - Vec::new() - }; - for (transaction, raw_transaction) in transactions { + let decoded_txs = transactions + .iter() + .map(|raw_tx| { + let tx = OpTxEnvelope::decode_2718(&mut raw_tx.as_ref()) + .map_err(ExecutorError::RLPError)?; + Ok((tx, raw_tx.as_ref())) + }) + .collect::>>()?; + for (transaction, raw_transaction) in decoded_txs { // The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior, // must be no greater than the block’s gasLimit. let block_available_gas = (gas_limit - cumulative_gas_used) as u128; @@ -261,8 +262,7 @@ where // Recompute the header roots. let state_root = state.database.state_root(&bundle)?; - let transactions_root = - Self::compute_transactions_root(payload.transactions.unwrap_or_default().as_slice()); + let transactions_root = Self::compute_transactions_root(transactions.as_slice()); let receipts_root = Self::compute_receipts_root( &receipts, self.config, @@ -303,23 +303,14 @@ where .unwrap_or_default(); // At holocene activation, the base fee parameters from the payload are placed - // into the Header's `nonce` field. Prior to Holocene, the `nonce` field should - // be set to 0. + // into the Header's `extra_data` field. // - // If the payload's `eip_1559_params` are equal to `0`, then the header's `nonce` + // If the payload's `eip_1559_params` are equal to `0`, then the header's `extraData` // field is set to the encoded canyon base fee parameters. let encoded_base_fee_params = self .config .is_holocene_active(payload.payload_attributes.timestamp) - .then(|| { - let payload_params = - payload.eip_1559_params.ok_or(ExecutorError::MissingEIP1559Params)?; - - let params = (payload_params == B64::ZERO) - .then(|| encode_canyon_base_fee_params(self.config)) - .unwrap_or(payload_params); - Ok::<_, ExecutorError>(params) - }) + .then(|| encode_holocene_eip_1559_params(self.config, &payload)) .transpose()? .unwrap_or_default(); @@ -340,13 +331,12 @@ where gas_used: cumulative_gas_used, timestamp: payload.payload_attributes.timestamp, mix_hash: payload.payload_attributes.prev_randao, - nonce: encoded_base_fee_params, + nonce: Default::default(), base_fee_per_gas: base_fee.try_into().ok(), blob_gas_used, excess_blob_gas: excess_blob_gas.and_then(|x| x.try_into().ok()), parent_beacon_block_root: payload.payload_attributes.parent_beacon_block_root, - // Provide no extra data on OP Stack chains - extra_data: Bytes::default(), + extra_data: encoded_base_fee_params, } .seal_slow(); @@ -432,7 +422,9 @@ where /// ## Returns /// The active [SpecId] for the executor. fn revm_spec_id(&self, timestamp: u64) -> SpecId { - if self.config.is_fjord_active(timestamp) { + if self.config.is_holocene_active(timestamp) { + SpecId::HOLOCENE + } else if self.config.is_fjord_active(timestamp) { SpecId::FJORD } else if self.config.is_ecotone_active(timestamp) { SpecId::ECOTONE @@ -558,22 +550,12 @@ where let base_fee_params = if config.is_holocene_active(payload_attrs.payload_attributes.timestamp) { // After Holocene activation, the base fee parameters are stored in the - // `nonce` field of the parent header. If the `nonce` field is not zero, - // then the base fee parameters are extracted from the `nonce` field. - // Otherwise, the canyon base fee parameters are used for the current block. - (parent_header.nonce != B64::ZERO) - .then(|| { - // SAFETY: The `nonce` field is always 8 bytes, and the 4-byte segments are - // guaranteed to be valid `u32` values. - let denominator: u32 = - u32::from_be_bytes(parent_header.nonce[0..4].try_into().unwrap()); - let elasticity: u32 = - u32::from_be_bytes(parent_header.nonce[4..8].try_into().unwrap()); - BaseFeeParams { - max_change_denominator: denominator as u128, - elasticity_multiplier: elasticity as u128, - } - }) + // `extraData` field of the parent header. If Holocene wasn't active in the + // parent block, the default base fee parameters are used. + config + .is_holocene_active(parent_header.timestamp) + .then(|| decode_holocene_eip_1559_params(parent_header)) + .transpose()? .unwrap_or(config.canyon_base_fee_params) } else if config.is_canyon_active(payload_attrs.payload_attributes.timestamp) { // If the payload attribute timestamp is past canyon activation, @@ -705,22 +687,10 @@ where } } -/// Encodes the canyon base fee parameters, per Holocene spec. -/// -/// -fn encode_canyon_base_fee_params(config: &RollupConfig) -> B64 { - let params = config.canyon_base_fee_params; - - let mut buf = B64::ZERO; - buf[0..4].copy_from_slice(&(params.max_change_denominator as u32).to_be_bytes()); - buf[4..8].copy_from_slice(&(params.elasticity_multiplier as u32).to_be_bytes()); - buf -} - #[cfg(test)] mod test { use super::*; - use alloy_primitives::{address, b256, b64, hex}; + use alloy_primitives::{address, b256, hex}; use alloy_rlp::Decodable; use alloy_rpc_types_engine::PayloadAttributes; use anyhow::{anyhow, Result}; @@ -774,18 +744,6 @@ mod test { } } - #[test] - fn test_encode_canyon_1559_params() { - let cfg = RollupConfig { - canyon_base_fee_params: BaseFeeParams { - max_change_denominator: 32, - elasticity_multiplier: 64, - }, - ..Default::default() - }; - assert_eq!(encode_canyon_base_fee_params(&cfg), b64!("0000002000000040")); - } - #[test] fn test_l2_block_executor_small_block() { // Static for the execution of block #120794432 on OP mainnet. diff --git a/crates/executor/src/util.rs b/crates/executor/src/util.rs index c9db5843b..0dfb85e58 100644 --- a/crates/executor/src/util.rs +++ b/crates/executor/src/util.rs @@ -1,11 +1,18 @@ //! Contains utilities for the L2 executor. +use crate::{ExecutorError, ExecutorResult}; use alloc::vec::Vec; -use alloy_consensus::{Eip658Value, Receipt, ReceiptWithBloom}; -use alloy_primitives::{Bloom, Log}; +use alloy_consensus::{Eip658Value, Header, Receipt, ReceiptWithBloom}; +use alloy_eips::eip1559::BaseFeeParams; +use alloy_primitives::{Bloom, Bytes, Log, B64}; use op_alloy_consensus::{ OpDepositReceipt, OpDepositReceiptWithBloom, OpReceiptEnvelope, OpTxType, }; +use op_alloy_genesis::RollupConfig; +use op_alloy_rpc_types_engine::OpPayloadAttributes; + +/// The version byte for the Holocene extra data. +const HOLOCENE_EXTRA_DATA_VERSION: u8 = 0x00; /// Constructs a [OpReceiptEnvelope] from a [Receipt] fields and [OpTxType]. pub(crate) fn receipt_envelope_from_parts<'a>( @@ -55,3 +62,196 @@ pub(crate) fn logs_bloom<'a>(logs: impl IntoIterator) -> Bloom { } bloom } + +/// Parse Holocene [Header] extra data. +/// +/// ## Takes +/// - `extra_data`: The extra data field of the [Header]. +/// +/// ## Returns +/// - `Ok(BaseFeeParams)`: The EIP-1559 parameters. +/// - `Err(ExecutorError::InvalidExtraData)`: If the extra data is invalid. +pub(crate) fn decode_holocene_eip_1559_params(header: &Header) -> ExecutorResult { + // Check the extra data length. + if header.extra_data.len() != 1 + 8 { + return Err(ExecutorError::InvalidExtraData); + } + + // Check the extra data version byte. + if header.extra_data[0] != HOLOCENE_EXTRA_DATA_VERSION { + return Err(ExecutorError::InvalidExtraData); + } + + // Parse the EIP-1559 parameters. + let data = &header.extra_data[1..]; + let denominator = + u32::from_be_bytes(data[..4].try_into().map_err(|_| ExecutorError::InvalidExtraData)?) + as u128; + let elasticity = + u32::from_be_bytes(data[4..].try_into().map_err(|_| ExecutorError::InvalidExtraData)?) + as u128; + + // Check for potential division by zero. + if denominator == 0 { + return Err(ExecutorError::InvalidExtraData); + } + + Ok(BaseFeeParams { elasticity_multiplier: elasticity, max_change_denominator: denominator }) +} + +/// Encode Holocene [Header] extra data. +/// +/// ## Takes +/// - `config`: The [RollupConfig] for the chain. +/// - `attributes`: The [OpPayloadAttributes] for the block. +/// +/// ## Returns +/// - `Ok(data)`: The encoded extra data. +/// - `Err(ExecutorError::MissingEIP1559Params)`: If the EIP-1559 parameters are missing. +pub(crate) fn encode_holocene_eip_1559_params( + config: &RollupConfig, + attributes: &OpPayloadAttributes, +) -> ExecutorResult { + let payload_params = attributes.eip_1559_params.ok_or(ExecutorError::MissingEIP1559Params)?; + let params = if payload_params == B64::ZERO { + encode_canyon_base_fee_params(config) + } else { + payload_params + }; + + let mut data = Vec::with_capacity(1 + 8); + data.push(HOLOCENE_EXTRA_DATA_VERSION); + data.extend_from_slice(params.as_ref()); + Ok(data.into()) +} + +/// Encodes the canyon base fee parameters, per Holocene spec. +/// +/// +pub(crate) fn encode_canyon_base_fee_params(config: &RollupConfig) -> B64 { + let params = config.canyon_base_fee_params; + + let mut buf = B64::ZERO; + buf[..4].copy_from_slice(&(params.max_change_denominator as u32).to_be_bytes()); + buf[4..].copy_from_slice(&(params.elasticity_multiplier as u32).to_be_bytes()); + buf +} + +#[cfg(test)] +mod test { + use super::decode_holocene_eip_1559_params; + use crate::util::{encode_canyon_base_fee_params, encode_holocene_eip_1559_params}; + use alloy_consensus::Header; + use alloy_eips::eip1559::BaseFeeParams; + use alloy_primitives::{b64, hex, B64}; + use alloy_rpc_types_engine::PayloadAttributes; + use op_alloy_genesis::RollupConfig; + use op_alloy_rpc_types_engine::OpPayloadAttributes; + + fn mock_payload(eip_1559_params: Option) -> OpPayloadAttributes { + OpPayloadAttributes { + payload_attributes: PayloadAttributes { + timestamp: 0, + prev_randao: Default::default(), + suggested_fee_recipient: Default::default(), + withdrawals: Default::default(), + parent_beacon_block_root: Default::default(), + }, + transactions: None, + no_tx_pool: None, + gas_limit: None, + eip_1559_params, + } + } + + #[test] + fn test_decode_holocene_eip_1559_params() { + let params = hex!("00BEEFBABE0BADC0DE"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + let params = decode_holocene_eip_1559_params(&mock_header).unwrap(); + + assert_eq!(params.elasticity_multiplier, 0xBADC_0DE); + assert_eq!(params.max_change_denominator, 0xBEEF_BABE); + } + + #[test] + fn test_decode_holocene_eip_1559_params_invalid_version() { + let params = hex!("01BEEFBABE0BADC0DE"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + assert!(decode_holocene_eip_1559_params(&mock_header).is_err()); + } + + #[test] + fn test_decode_holocene_eip_1559_params_invalid_denominator() { + let params = hex!("00000000000BADC0DE"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + assert!(decode_holocene_eip_1559_params(&mock_header).is_err()); + } + + #[test] + fn test_decode_holocene_eip_1559_params_invalid_length() { + let params = hex!("00"); + let mock_header = Header { extra_data: params.to_vec().into(), ..Default::default() }; + assert!(decode_holocene_eip_1559_params(&mock_header).is_err()); + } + + #[test] + fn test_encode_holocene_eip_1559_params_missing() { + let cfg = RollupConfig { + canyon_base_fee_params: BaseFeeParams { + max_change_denominator: 32, + elasticity_multiplier: 64, + }, + ..Default::default() + }; + let attrs = mock_payload(None); + + assert!(encode_holocene_eip_1559_params(&cfg, &attrs).is_err()); + } + + #[test] + fn test_encode_holocene_eip_1559_params_default() { + let cfg = RollupConfig { + canyon_base_fee_params: BaseFeeParams { + max_change_denominator: 32, + elasticity_multiplier: 64, + }, + ..Default::default() + }; + let attrs = mock_payload(Some(B64::ZERO)); + + assert_eq!( + encode_holocene_eip_1559_params(&cfg, &attrs).unwrap(), + hex!("000000002000000040").to_vec() + ); + } + + #[test] + fn test_encode_holocene_eip_1559_params() { + let cfg = RollupConfig { + canyon_base_fee_params: BaseFeeParams { + max_change_denominator: 32, + elasticity_multiplier: 64, + }, + ..Default::default() + }; + let attrs = mock_payload(Some(b64!("0000004000000060"))); + + assert_eq!( + encode_holocene_eip_1559_params(&cfg, &attrs).unwrap(), + hex!("000000004000000060").to_vec() + ); + } + + #[test] + fn test_encode_canyon_1559_params() { + let cfg = RollupConfig { + canyon_base_fee_params: BaseFeeParams { + max_change_denominator: 32, + elasticity_multiplier: 64, + }, + ..Default::default() + }; + assert_eq!(encode_canyon_base_fee_params(&cfg), b64!("0000002000000040")); + } +}