diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c4b078a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + + - name: Build debug + run: cargo build --all --verbose + + - name: Build release + run: cargo build --all --release --verbose diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..fe7e1b5 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,53 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-clippy- + + - name: Run Clippy + run: cargo clippy --all --all-targets -- -D warnings + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..66c22f0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-test- + + - name: Run tests + run: cargo test --all --verbose + + doc-test: + name: Doc Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-doc-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-doc- + + - name: Run doc tests + run: cargo test --doc --all --verbose + diff --git a/crates/chainspec/src/lib.rs b/crates/chainspec/src/lib.rs index 6a1d926..a398fed 100644 --- a/crates/chainspec/src/lib.rs +++ b/crates/chainspec/src/lib.rs @@ -3,6 +3,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +// Used only in tests, but declared here to silence unused_crate_dependencies warning +use serde_json as _; + pub mod hardfork; pub mod spec; pub use spec::MorphChainSpec; diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 1a4c0b3..4e5acc8 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -67,9 +67,7 @@ pub const SUPPORTED_CHAINS: &[&str] = &["testnet"]; /// to a json file, or a json formatted string in-memory. The json needs to be a Genesis struct. #[cfg(feature = "cli")] pub fn chain_value_parser(s: &str) -> eyre::Result> { - Ok(match s { - _ => MorphChainSpec::from_genesis(reth_cli::chainspec::parse_genesis(s)?).into(), - }) + Ok(MorphChainSpec::from_genesis(reth_cli::chainspec::parse_genesis(s)?).into()) } #[cfg(feature = "cli")] @@ -238,25 +236,41 @@ impl MorphHardforks for MorphChainSpec { mod tests { use crate::hardfork::{MorphHardfork, MorphHardforks}; use reth_chainspec::{EthereumHardfork, ForkCondition, Hardforks}; - use reth_cli::chainspec::ChainSpecParser as _; use serde_json::json; - #[test] - fn can_load_testnet() { - let _ = super::MorphChainSpecParser::parse("testnet") - .expect("the testnet chainspec must always be well formed"); - } - - #[test] - fn can_load_dev() { - let _ = super::MorphChainSpecParser::parse("dev") - .expect("the dev chainspec must always be well formed"); + /// Helper function to create a test genesis with Morph hardforks at timestamp 0 + fn create_test_genesis() -> alloy_genesis::Genesis { + let genesis_json = json!({ + "config": { + "chainId": 1337, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "shanghaiTime": 0, + "cancunTime": 0, + "bernoulliTime": 0, + "curieTime": 0, + "morph203Time": 0, + "viridianTime": 0 + }, + "alloc": {} + }); + serde_json::from_value(genesis_json).expect("genesis should be valid") } #[test] fn test_morph_chainspec_has_morph_hardforks() { - let chainspec = super::MorphChainSpecParser::parse("testnet") - .expect("the testnet chainspec must always be well formed"); + let chainspec = super::MorphChainSpec::from_genesis(create_test_genesis()); // Bernoulli should be active at genesis (timestamp 0) assert!(chainspec.is_bernoulli_active_at_timestamp(0)); @@ -264,8 +278,7 @@ mod tests { #[test] fn test_morph_chainspec_implements_morph_hardforks_trait() { - let chainspec = super::MorphChainSpecParser::parse("testnet") - .expect("the testnet chainspec must always be well formed"); + let chainspec = super::MorphChainSpec::from_genesis(create_test_genesis()); // Should be able to query Morph hardfork activation through trait let activation = chainspec.morph_fork_activation(MorphHardfork::Bernoulli); @@ -278,8 +291,7 @@ mod tests { #[test] fn test_morph_hardforks_in_inner_hardforks() { - let chainspec = super::MorphChainSpecParser::parse("testnet") - .expect("the testnet chainspec must always be well formed"); + let chainspec = super::MorphChainSpec::from_genesis(create_test_genesis()); // Morph hardforks should be queryable from inner.hardforks via Hardforks trait let activation = chainspec.fork(MorphHardfork::Bernoulli); diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index a17afcd..345186e 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -32,6 +32,8 @@ thiserror.workspace = true [dev-dependencies] revm.workspace = true +serde_json.workspace = true +alloy-genesis.workspace = true [features] default = ["rpc"] diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs index 8896880..0e4b52b 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -207,15 +207,30 @@ where #[cfg(test)] mod tests { + use alloy_primitives::U256; use reth_revm::context::BlockEnv; - use revm::{context::TxEnv, database::EmptyDB}; + use revm::{ + context::TxEnv, + database::{CacheDB, EmptyDB}, + state::AccountInfo, + }; use super::*; #[test] fn can_execute_tx() { + // Create a database with the caller having sufficient balance + let mut db = CacheDB::new(EmptyDB::default()); + db.insert_account_info( + Address::ZERO, + AccountInfo { + balance: U256::from(1_000_000), + ..Default::default() + }, + ); + let mut evm = MorphEvm::new( - EmptyDB::default(), + db, EvmEnv { block_env: MorphBlockEnv { inner: BlockEnv { @@ -230,7 +245,7 @@ mod tests { .transact(MorphTxEnv { inner: TxEnv { caller: Address::ZERO, - gas_price: 0, + gas_price: 1, // Must be >= basefee gas_limit: 21000, ..Default::default() }, diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index b59cff6..e23a693 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -3,9 +3,9 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +mod assemble; #[cfg(feature = "engine")] mod engine; -mod assemble; use alloy_consensus::BlockHeader as _; pub use assemble::MorphBlockAssembler; mod block; @@ -193,12 +193,43 @@ impl ConfigureEvm for MorphEvmConfig { mod tests { use super::*; use morph_chainspec::hardfork::{MorphHardfork, MorphHardforks}; + use serde_json::json; + + /// Helper function to create a test genesis with Morph hardforks at timestamp 0 + fn create_test_genesis() -> alloy_genesis::Genesis { + let genesis_json = json!({ + "config": { + "chainId": 1337, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "mergeNetsplitBlock": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "shanghaiTime": 0, + "cancunTime": 0, + "bernoulliTime": 0, + "curieTime": 0, + "morph203Time": 0, + "viridianTime": 0 + }, + "alloc": {} + }); + serde_json::from_value(genesis_json).expect("genesis should be valid") + } #[test] fn test_evm_config_can_query_morph_hardforks() { // Create a test chainspec with Bernoulli at genesis let chainspec = Arc::new(morph_chainspec::MorphChainSpec::from_genesis( - Default::default(), + create_test_genesis(), )); let evm_config = MorphEvmConfig::new_with_default_factory(chainspec); diff --git a/crates/primitives/src/transaction/alt_fee.rs b/crates/primitives/src/transaction/alt_fee.rs index bba7c26..9a3cc94 100644 --- a/crates/primitives/src/transaction/alt_fee.rs +++ b/crates/primitives/src/transaction/alt_fee.rs @@ -74,7 +74,6 @@ pub struct TxAltFee { /// accessing outside the list. pub access_list: AccessList, - /// Maximum amount of tokens the sender is willing to pay as fee. pub fee_limit: U256, @@ -309,7 +308,7 @@ impl RlpEcdsaEncodableTx for TxAltFee { } impl RlpEcdsaDecodableTx for TxAltFee { - const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 }; + const DEFAULT_TX_TYPE: u8 = { Self::tx_type() }; /// Decodes the inner [TxEip1559] fields from RLP bytes. fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { @@ -323,7 +322,7 @@ impl SignableTransaction for TxAltFee { } fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - out.put_u8(Self::tx_type() as u8); + out.put_u8(Self::tx_type()); self.encode(out) } diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 826000e..2e367f0 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -6,7 +6,6 @@ use crate::{TxAltFee, TxL1Msg}; #[derive(Debug, Clone, TransactionEnvelope)] #[envelope(tx_type_name = MorphTxType)] -#[expect(clippy::large_enum_variant)] pub enum MorphTxEnvelope { /// Legacy transaction (type 0x00) #[envelope(ty = 0)] diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index 79b215a..b2a0009 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -241,7 +241,7 @@ impl RlpEcdsaEncodableTx for TxL1Msg { } impl RlpEcdsaDecodableTx for TxL1Msg { - const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 }; + const DEFAULT_TX_TYPE: u8 = { Self::tx_type() }; /// Decodes the inner [TxEip1559] fields from RLP bytes. fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { @@ -253,7 +253,7 @@ impl SignableTransaction for TxL1Msg { fn set_chain_id(&mut self, _chain_id: ChainId) {} fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - out.put_u8(Self::tx_type() as u8); + out.put_u8(Self::tx_type()); self.encode(out) } @@ -382,8 +382,9 @@ mod tests { }; // Test Transaction trait methods + // Note: L1 transactions always return nonce 0 from Transaction trait assert_eq!(tx.chain_id(), None); - assert_eq!(Transaction::nonce(&tx), 42); + assert_eq!(Transaction::nonce(&tx), 0); assert_eq!(Transaction::gas_limit(&tx), 21_000); assert_eq!(tx.gas_price(), Some(0)); assert_eq!(tx.max_fee_per_gas(), 0); diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 1083117..17b9665 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -437,13 +437,11 @@ fn calculate_caller_fee_with_l1_cost( // Total spending = L2 fees + L1 data fee let total_spending = effective_balance_spending.saturating_add(l1_data_fee); // Check if caller has enough balance for total spending - if !is_balance_check_disabled { - if balance < total_spending { - return Err(InvalidTransaction::LackOfFundForMaxFee { - fee: Box::new(total_spending), - balance: Box::new(balance), - }); - } + if !is_balance_check_disabled && balance < total_spending { + return Err(InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(total_spending), + balance: Box::new(balance), + }); } // Calculate gas balance spending (excluding value transfer) diff --git a/crates/revm/src/l1block.rs b/crates/revm/src/l1block.rs index c438860..64f7244 100644 --- a/crates/revm/src/l1block.rs +++ b/crates/revm/src/l1block.rs @@ -64,13 +64,13 @@ impl L1BlockInfo { pub fn try_fetch( db: &mut DB, hardfork: MorphHardfork, - ) -> Result { + ) -> Result { let l1_base_fee = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_BASE_FEE_SLOT)?; let l1_fee_overhead = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_OVERHEAD_SLOT)?; let l1_base_fee_scalar = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_SCALAR_SLOT)?; if !hardfork.is_curie() { - Ok(L1BlockInfo { + Ok(Self { l1_base_fee, l1_fee_overhead, l1_base_fee_scalar, @@ -86,7 +86,7 @@ impl L1BlockInfo { // calldata component of commit fees (calldata gas + execution) let calldata_gas = l1_commit_scalar.saturating_mul(l1_base_fee); - Ok(L1BlockInfo { + Ok(Self { l1_base_fee, l1_fee_overhead, l1_base_fee_scalar, diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index 6efa461..94f1ae5 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -60,7 +60,7 @@ impl TokenFeeInfo { db: &mut DB, token_id: u16, caller: Address, - ) -> Result, DB::Error> { + ) -> Result, DB::Error> { // Get the base slot for this token_id in tokenRegistry mapping let mut token_id_bytes = [0u8; 32]; token_id_bytes[30..32].copy_from_slice(&token_id.to_be_bytes()); @@ -119,7 +119,7 @@ impl TokenFeeInfo { let caller_token_balance = get_erc20_balance(db, token_address, caller, token_balance_slot)?; - let token_fee = TokenFeeInfo { + let token_fee = Self { token_address, is_active, decimals, @@ -246,14 +246,16 @@ where let calldata = build_balance_of_calldata(account); // Create a minimal transaction environment for the call - let mut tx = TxEnv::default(); - tx.caller = Address::ZERO; - tx.gas_limit = BALANCE_OF_GAS_LIMIT; - tx.kind = TxKind::Call(token); - tx.value = U256::ZERO; - tx.data = calldata; - tx.nonce = 0; - tx.tx_type = L1_TX_TYPE_ID; // Mark as L1 message to skip gas validation + let tx = TxEnv { + caller: Address::ZERO, + gas_limit: BALANCE_OF_GAS_LIMIT, + kind: TxKind::Call(token), + value: U256::ZERO, + data: calldata, + nonce: 0, + tx_type: L1_TX_TYPE_ID, // Mark as L1 message to skip gas validation + ..Default::default() + }; // Convert to MorphTxEnv let morph_tx = crate::MorphTxEnv::new(tx); @@ -263,10 +265,10 @@ where Ok(result) => { if result.is_success() { // Parse the returned balance (32 bytes) - if let Some(output) = result.output() { - if output.len() >= 32 { - return Ok(U256::from_be_slice(&output[..32])); - } + if let Some(output) = result.output() + && output.len() >= 32 + { + return Ok(U256::from_be_slice(&output[..32])); } } Ok(U256::ZERO) diff --git a/crates/revm/src/tx.rs b/crates/revm/src/tx.rs index 93be407..ad72b11 100644 --- a/crates/revm/src/tx.rs +++ b/crates/revm/src/tx.rs @@ -24,7 +24,7 @@ use alloy_consensus::transaction::Either; /// - L1 message detection (tx_type 0x7E) /// - Morph transaction with token-based gas payment (tx_type 0x7F) /// - RLP encoded transaction bytes for L1 data fee calculation -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct MorphTxEnv { /// Inner transaction environment. pub inner: TxEnv, @@ -38,17 +38,6 @@ pub struct MorphTxEnv { pub fee_token_id: Option, } -impl Default for MorphTxEnv { - fn default() -> Self { - Self { - inner: TxEnv::default(), - rlp_bytes: None, - fee_limit: None, - fee_token_id: None, - } - } -} - impl MorphTxEnv { /// Create a new Morph transaction environment from a standard TxEnv. pub fn new(inner: TxEnv) -> Self { @@ -107,7 +96,7 @@ impl MorphTxEnv { let inner = TxEnv { tx_type, caller: signer, - gas_limit: AlloyTransaction::gas_limit(tx) as u64, + gas_limit: AlloyTransaction::gas_limit(tx), gas_price: tx.effective_gas_price(None), kind: AlloyTransaction::kind(tx), value: AlloyTransaction::value(tx), @@ -174,8 +163,8 @@ impl From for TxEnv { } // Implement ToTxEnv for MorphTxEnv (identity conversion) -impl ToTxEnv for MorphTxEnv { - fn to_tx_env(&self) -> MorphTxEnv { +impl ToTxEnv for MorphTxEnv { + fn to_tx_env(&self) -> Self { self.clone() } } @@ -183,7 +172,7 @@ impl ToTxEnv for MorphTxEnv { // Implement FromRecoveredTx for MorphTxEnv impl FromRecoveredTx for MorphTxEnv { fn from_recovered_tx(tx: &MorphTxEnvelope, sender: Address) -> Self { - MorphTxEnv::from_recovered_tx(tx, sender) + Self::from_recovered_tx(tx, sender) } } @@ -206,7 +195,7 @@ impl FromTxWithEncoded> for MorphTxEnv { // Implement FromTxWithEncoded for MorphTxEnv impl FromTxWithEncoded for MorphTxEnv { fn from_encoded_tx(tx: &MorphTxEnvelope, sender: Address, encoded: Bytes) -> Self { - MorphTxEnv::from_tx_with_rlp_bytes(tx, sender, encoded) + Self::from_tx_with_rlp_bytes(tx, sender, encoded) } } @@ -380,8 +369,10 @@ mod tests { #[test] fn test_txenv_morph_tx_detection() { - let mut tx = TxEnv::default(); - tx.tx_type = ALT_FEE_TX_TYPE_ID; + let tx = TxEnv { + tx_type: ALT_FEE_TX_TYPE_ID, + ..Default::default() + }; assert!(tx.is_alt_fee_tx()); let regular_tx = TxEnv::default(); @@ -390,8 +381,13 @@ mod tests { #[test] fn test_deref() { - let mut tx = MorphTxEnv::default(); - tx.gas_limit = 21000; + let tx = MorphTxEnv { + inner: TxEnv { + gas_limit: 21000, + ..Default::default() + }, + ..Default::default() + }; assert_eq!(tx.gas_limit, 21000); assert_eq!(tx.inner.gas_limit, 21000); }