Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
485 changes: 227 additions & 258 deletions rust/Cargo.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion rust/alloy-op-evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repository = "https://github.com/ethereum-optimism/optimism"
workspace = true

[dependencies]
alloy-evm = { workspace = true, features = ["op"] }
alloy-evm.workspace = true

alloy-eips = { workspace = true }
alloy-consensus = { workspace = true }
Expand All @@ -30,8 +30,11 @@ thiserror = { workspace = true }

auto_impl = { workspace = true }

reth-evm = { workspace = true, optional = true }

[dev-dependencies]
alloy-hardforks = { workspace = true }
test-case.workspace = true

[features]
default = ["std"]
Expand All @@ -46,4 +49,7 @@ std = [
"thiserror/std"
]
gmp = ["alloy-evm/gmp"]
engine = ["op-alloy/rpc-types-engine"]
reth = ["reth-evm"]
rpc = ["alloy-evm/rpc", "op-alloy/rpc-types"]
asm-keccak = ["alloy-evm/asm-keccak", "alloy-primitives/asm-keccak", "revm/asm-keccak"]
269 changes: 269 additions & 0 deletions rust/alloy-op-evm/src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
//! OP EVM environment configuration.
//!
//! Provides spec ID mapping and `EvmEnv` constructors for Optimism.

use alloy_consensus::BlockHeader;
use alloy_evm::{eth::NextEvmEnvAttributes, EvmEnv};
use alloy_op_hardforks::OpHardforks;
use alloy_primitives::{Address, BlockNumber, BlockTimestamp, ChainId, B256, U256};
use op_revm::OpSpecId;
use revm::{
context::{BlockEnv, CfgEnv},
context_interface::block::BlobExcessGasAndPrice,
primitives::hardfork::SpecId,
};

/// Map the latest active hardfork at the given header to a revm [`OpSpecId`].
pub fn spec(chain_spec: impl OpHardforks, header: impl BlockHeader) -> OpSpecId {
spec_by_timestamp_after_bedrock(chain_spec, header.timestamp())
}

/// Returns the revm [`OpSpecId`] at the given timestamp.
///
/// # Note
///
/// This is only intended to be used after the Bedrock, when hardforks are activated by
/// timestamp.
pub fn spec_by_timestamp_after_bedrock(chain_spec: impl OpHardforks, timestamp: u64) -> OpSpecId {
macro_rules! check_forks {
($($check:ident => $spec:ident),+ $(,)?) => {
$(
if chain_spec.$check(timestamp) {
return OpSpecId::$spec;
}
)+
};
}
check_forks! {
is_interop_active_at_timestamp => INTEROP,
is_jovian_active_at_timestamp => JOVIAN,
is_isthmus_active_at_timestamp => ISTHMUS,
is_holocene_active_at_timestamp => HOLOCENE,
is_granite_active_at_timestamp => GRANITE,
is_fjord_active_at_timestamp => FJORD,
is_ecotone_active_at_timestamp => ECOTONE,
is_canyon_active_at_timestamp => CANYON,
is_regolith_active_at_timestamp => REGOLITH,
}
OpSpecId::BEDROCK
}

/// Internal helper for constructing EVM environment from block header fields.
struct EvmEnvInput {
timestamp: BlockTimestamp,
number: BlockNumber,
beneficiary: Address,
mix_hash: Option<B256>,
difficulty: U256,
gas_limit: u64,
base_fee_per_gas: u64,
}

impl EvmEnvInput {
fn from_block_header(header: impl BlockHeader) -> Self {
Self {
timestamp: header.timestamp(),
number: header.number(),
beneficiary: header.beneficiary(),
mix_hash: header.mix_hash(),
difficulty: header.difficulty(),
gas_limit: header.gas_limit(),
base_fee_per_gas: header.base_fee_per_gas().unwrap_or_default(),
}
}

fn for_next(
parent: impl BlockHeader,
attributes: NextEvmEnvAttributes,
base_fee_per_gas: u64,
) -> Self {
Self {
timestamp: attributes.timestamp,
number: parent.number() + 1,
beneficiary: attributes.suggested_fee_recipient,
mix_hash: Some(attributes.prev_randao),
difficulty: U256::ZERO,
gas_limit: attributes.gas_limit,
base_fee_per_gas,
}
}
}

/// Create a new `EvmEnv` with [`OpSpecId`] from a block `header`, `chain_id` and `chain_spec`.
pub fn evm_env_for_op_block(
header: impl BlockHeader,
chain_spec: impl OpHardforks,
chain_id: ChainId,
) -> EvmEnv<OpSpecId> {
evm_env_for_op(EvmEnvInput::from_block_header(header), chain_spec, chain_id)
}

/// Create a new `EvmEnv` with [`OpSpecId`] from a parent block `header`, `chain_id` and
/// `chain_spec`.
pub fn evm_env_for_op_next_block(
header: impl BlockHeader,
attributes: NextEvmEnvAttributes,
base_fee_per_gas: u64,
chain_spec: impl OpHardforks,
chain_id: ChainId,
) -> EvmEnv<OpSpecId> {
evm_env_for_op(
EvmEnvInput::for_next(header, attributes, base_fee_per_gas),
chain_spec,
chain_id,
)
}

fn evm_env_for_op(
input: EvmEnvInput,
chain_spec: impl OpHardforks,
chain_id: ChainId,
) -> EvmEnv<OpSpecId> {
let spec = spec_by_timestamp_after_bedrock(&chain_spec, input.timestamp);
let cfg_env = CfgEnv::new().with_chain_id(chain_id).with_spec_and_mainnet_gas_params(spec);

let blob_excess_gas_and_price = spec
.into_eth_spec()
.is_enabled_in(SpecId::CANCUN)
.then_some(BlobExcessGasAndPrice { excess_blob_gas: 0, blob_gasprice: 1 });

let is_merge_active = spec.into_eth_spec() >= SpecId::MERGE;

let block_env = BlockEnv {
number: U256::from(input.number),
beneficiary: input.beneficiary,
timestamp: U256::from(input.timestamp),
difficulty: if is_merge_active { U256::ZERO } else { input.difficulty },
prevrandao: if is_merge_active { input.mix_hash } else { None },
gas_limit: input.gas_limit,
basefee: input.base_fee_per_gas,
// EIP-4844 excess blob gas of this block, introduced in Cancun
blob_excess_gas_and_price,
};

EvmEnv::new(cfg_env, block_env)
}

#[cfg(feature = "engine")]
mod payload {
use super::*;
use op_alloy::rpc_types_engine::OpExecutionPayload;

/// Create a new `EvmEnv` with [`OpSpecId`] from a `payload`, `chain_id` and `chain_spec`.
pub fn evm_env_for_op_payload(
payload: &OpExecutionPayload,
chain_spec: impl OpHardforks,
chain_id: ChainId,
) -> EvmEnv<OpSpecId> {
let input = EvmEnvInput {
timestamp: payload.timestamp(),
number: payload.block_number(),
beneficiary: payload.as_v1().fee_recipient,
mix_hash: Some(payload.as_v1().prev_randao),
difficulty: payload.as_v1().prev_randao.into(),
gas_limit: payload.as_v1().gas_limit,
base_fee_per_gas: payload.as_v1().base_fee_per_gas.saturating_to(),
};
super::evm_env_for_op(input, chain_spec, chain_id)
}
}

#[cfg(test)]
mod tests {
use super::*;
use alloy_consensus::Header;
use alloy_hardforks::EthereumHardfork;
use alloy_op_hardforks::{
EthereumHardforks, ForkCondition, OpChainHardforks, OpHardfork,
OP_MAINNET_CANYON_TIMESTAMP, OP_MAINNET_ECOTONE_TIMESTAMP, OP_MAINNET_FJORD_TIMESTAMP,
OP_MAINNET_GRANITE_TIMESTAMP, OP_MAINNET_HOLOCENE_TIMESTAMP, OP_MAINNET_ISTHMUS_TIMESTAMP,
OP_MAINNET_JOVIAN_TIMESTAMP, OP_MAINNET_REGOLITH_TIMESTAMP,
};
use alloy_primitives::BlockTimestamp;

struct FakeHardfork {
fork: OpHardfork,
cond: ForkCondition,
}

macro_rules! fake_hardfork_constructors {
(timestamp: $($ts_name:ident => $ts_fork:ident),+ $(,)?; block: $($blk_name:ident => $blk_fork:ident),+ $(,)?) => {
impl FakeHardfork {
$(
fn $ts_name() -> Self {
Self { fork: OpHardfork::$ts_fork, cond: ForkCondition::Timestamp(0) }
}
)+
$(
fn $blk_name() -> Self {
Self { fork: OpHardfork::$blk_fork, cond: ForkCondition::Block(0) }
}
)+
}
};
}

fake_hardfork_constructors! {
timestamp:
interop => Interop,
jovian => Jovian,
isthmus => Isthmus,
holocene => Holocene,
granite => Granite,
fjord => Fjord,
ecotone => Ecotone,
canyon => Canyon,
regolith => Regolith;
block:
bedrock => Bedrock,
}

impl EthereumHardforks for FakeHardfork {
fn ethereum_fork_activation(&self, _: EthereumHardfork) -> ForkCondition {
unimplemented!()
}
}

impl OpHardforks for FakeHardfork {
fn op_fork_activation(&self, fork: OpHardfork) -> ForkCondition {
if fork == self.fork {
self.cond
} else {
ForkCondition::Never
}
}
}

#[test_case::test_case(FakeHardfork::interop(), OpSpecId::INTEROP; "Interop")]
#[test_case::test_case(FakeHardfork::jovian(), OpSpecId::JOVIAN; "Jovian")]
#[test_case::test_case(FakeHardfork::isthmus(), OpSpecId::ISTHMUS; "Isthmus")]
#[test_case::test_case(FakeHardfork::holocene(), OpSpecId::HOLOCENE; "Holocene")]
#[test_case::test_case(FakeHardfork::granite(), OpSpecId::GRANITE; "Granite")]
#[test_case::test_case(FakeHardfork::fjord(), OpSpecId::FJORD; "Fjord")]
#[test_case::test_case(FakeHardfork::ecotone(), OpSpecId::ECOTONE; "Ecotone")]
#[test_case::test_case(FakeHardfork::canyon(), OpSpecId::CANYON; "Canyon")]
#[test_case::test_case(FakeHardfork::regolith(), OpSpecId::REGOLITH; "Regolith")]
#[test_case::test_case(FakeHardfork::bedrock(), OpSpecId::BEDROCK; "Bedrock")]
fn test_spec_maps_hardfork_successfully(fork: impl OpHardforks, expected_spec: OpSpecId) {
let header = Header::default();
let actual_spec = spec(fork, &header);

assert_eq!(actual_spec, expected_spec);
}

#[test_case::test_case(OP_MAINNET_JOVIAN_TIMESTAMP, OpSpecId::JOVIAN; "Jovian")]
#[test_case::test_case(OP_MAINNET_ISTHMUS_TIMESTAMP, OpSpecId::ISTHMUS; "Isthmus")]
#[test_case::test_case(OP_MAINNET_HOLOCENE_TIMESTAMP, OpSpecId::HOLOCENE; "Holocene")]
#[test_case::test_case(OP_MAINNET_GRANITE_TIMESTAMP, OpSpecId::GRANITE; "Granite")]
#[test_case::test_case(OP_MAINNET_FJORD_TIMESTAMP, OpSpecId::FJORD; "Fjord")]
#[test_case::test_case(OP_MAINNET_ECOTONE_TIMESTAMP, OpSpecId::ECOTONE; "Ecotone")]
#[test_case::test_case(OP_MAINNET_CANYON_TIMESTAMP, OpSpecId::CANYON; "Canyon")]
#[test_case::test_case(OP_MAINNET_REGOLITH_TIMESTAMP, OpSpecId::REGOLITH; "Regolith")]
fn test_op_spec_maps_hardfork_successfully(timestamp: BlockTimestamp, expected_spec: OpSpecId) {
let fork = OpChainHardforks::op_mainnet();
let header = Header { timestamp, ..Default::default() };
let actual_spec = spec(&fork, &header);

assert_eq!(actual_spec, expected_spec);
}
}
41 changes: 41 additions & 0 deletions rust/alloy-op-evm/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Error types for OP EVM execution.

use alloy_evm::InvalidTxError;
use core::fmt;
use op_revm::OpTransactionError;
use revm::context_interface::result::{EVMError, InvalidTransaction};

/// Newtype wrapper around [`OpTransactionError`] that allows implementing foreign traits.
#[derive(Debug)]
pub struct OpTxError(pub OpTransactionError);

impl fmt::Display for OpTxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl core::error::Error for OpTxError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
self.0.source()
}
}

impl InvalidTxError for OpTxError {
fn as_invalid_tx_err(&self) -> Option<&InvalidTransaction> {
match &self.0 {
OpTransactionError::Base(tx) => Some(tx),
_ => None,
}
}
}

/// Maps an [`EVMError<DB, OpTransactionError>`] to [`EVMError<DB, OpTxError>`].
pub fn map_op_err<DB>(err: EVMError<DB, OpTransactionError>) -> EVMError<DB, OpTxError> {
match err {
EVMError::Transaction(e) => EVMError::Transaction(OpTxError(e)),
EVMError::Database(e) => EVMError::Database(e),
EVMError::Header(e) => EVMError::Header(e),
EVMError::Custom(e) => EVMError::Custom(e),
}
}
Loading
Loading