From af247d39c42b59e62c8d68d717ff0c8d4c0fccee Mon Sep 17 00:00:00 2001 From: panos Date: Fri, 23 Jan 2026 11:47:42 +0800 Subject: [PATCH 01/19] feat: add morph l2 tx pool --- Cargo.lock | 189 +++++++++++++++ Cargo.toml | 2 + crates/engine-api/src/lib.rs | 3 +- crates/engine-api/src/validator.rs | 14 +- crates/txpool/Cargo.toml | 39 +++ crates/txpool/src/lib.rs | 62 +++++ crates/txpool/src/transaction.rs | 212 ++++++++++++++++ crates/txpool/src/validator.rs | 377 +++++++++++++++++++++++++++++ 8 files changed, 889 insertions(+), 9 deletions(-) create mode 100644 crates/txpool/Cargo.toml create mode 100644 crates/txpool/src/lib.rs create mode 100644 crates/txpool/src/transaction.rs create mode 100644 crates/txpool/src/validator.rs diff --git a/Cargo.lock b/Cargo.lock index d13e828..349e319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,7 @@ dependencies = [ "alloy-serde", "alloy-trie", "alloy-tx-macros", + "arbitrary", "auto_impl", "borsh", "c-kzg", @@ -116,6 +117,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-serde", + "arbitrary", "serde", ] @@ -144,7 +146,9 @@ checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" dependencies = [ "alloy-primitives", "alloy-rlp", + "arbitrary", "crc", + "rand 0.8.5", "serde", "thiserror 2.0.17", ] @@ -157,7 +161,9 @@ checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "arbitrary", "borsh", + "rand 0.8.5", "serde", ] @@ -169,8 +175,10 @@ checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "arbitrary", "borsh", "k256", + "rand 0.8.5", "serde", "thiserror 2.0.17", ] @@ -187,6 +195,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-serde", + "arbitrary", "auto_impl", "borsh", "c-kzg", @@ -323,6 +332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7db950a29746be9e2f2c6288c8bd7a6202a81f999ce109a2933d2379970ec0fa" dependencies = [ "alloy-rlp", + "arbitrary", "bytes", "cfg-if", "const-hex", @@ -336,6 +346,7 @@ dependencies = [ "keccak-asm", "paste", "proptest", + "proptest-derive 0.6.0", "rand 0.9.2", "rapidhash", "ruint", @@ -496,6 +507,7 @@ dependencies = [ "alloy-rlp", "alloy-serde", "alloy-sol-types", + "arbitrary", "itertools 0.14.0", "serde", "serde_json", @@ -539,6 +551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", + "arbitrary", "serde", "serde_json", ] @@ -558,6 +571,22 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "alloy-signer-local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror 2.0.17", +] + [[package]] name = "alloy-sol-macro" version = "1.5.1" @@ -674,9 +703,13 @@ checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" dependencies = [ "alloy-primitives", "alloy-rlp", + "arbitrary", "arrayvec", + "derive_arbitrary", "derive_more", "nybbles", + "proptest", + "proptest-derive 0.5.1", "serde", "smallvec", "tracing", @@ -773,6 +806,15 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -1377,6 +1419,7 @@ version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" dependencies = [ + "arbitrary", "blst", "cc", "glob", @@ -1927,6 +1970,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -2655,6 +2709,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + [[package]] name = "hashbrown" version = "0.12.3" @@ -3143,6 +3203,7 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ + "arbitrary", "equivalent", "hashbrown 0.16.1", "serde", @@ -3986,6 +4047,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "morph-txpool" +version = "0.7.5" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "c-kzg", + "derive_more", + "morph-chainspec", + "morph-primitives", + "morph-revm", + "parking_lot", + "reth-chainspec", + "reth-primitives-traits", + "reth-provider", + "reth-revm", + "reth-storage-api", + "reth-transaction-pool", + "tracing", +] + [[package]] name = "multiaddr" version = "0.18.2" @@ -4207,6 +4290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" dependencies = [ "alloy-rlp", + "arbitrary", "cfg-if", "proptest", "ruint", @@ -4256,6 +4340,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", + "arbitrary", "derive_more", "serde", "thiserror 2.0.17", @@ -4470,6 +4555,7 @@ version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ + "arbitrary", "arrayvec", "bitvec", "byte-slice-cast", @@ -4639,6 +4725,15 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain_hasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e19e6491bdde87c2c43d70f4c194bc8a758f2eb732df00f61e43f7362e3b4cc" +dependencies = [ + "crunchy", +] + [[package]] name = "polyval" version = "0.6.2" @@ -4760,6 +4855,38 @@ dependencies = [ "unarray", ] +[[package]] +name = "proptest-arbitrary-interop" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1981e49bd2432249da8b0e11e5557099a8e74690d6b94e721f7dc0bb7f3555f" +dependencies = [ + "arbitrary", + "proptest", +] + +[[package]] +name = "proptest-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "proptest-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "prost" version = "0.14.1" @@ -5133,6 +5260,8 @@ dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", + "alloy-signer", + "alloy-signer-local", "derive_more", "metrics", "parking_lot", @@ -5225,12 +5354,14 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "alloy-trie", + "arbitrary", "bytes", "modular-bitfield", "op-alloy-consensus", "reth-codecs-derive", "reth-zstd-compressors", "serde", + "visibility", ] [[package]] @@ -5294,6 +5425,7 @@ dependencies = [ "eyre", "metrics", "page_size", + "parking_lot", "reth-db-api", "reth-fs-util", "reth-libmdbx", @@ -5305,6 +5437,7 @@ dependencies = [ "rustc-hash", "strum", "sysinfo", + "tempfile", "thiserror 2.0.17", ] @@ -5316,14 +5449,17 @@ dependencies = [ "alloy-consensus", "alloy-genesis", "alloy-primitives", + "arbitrary", "bytes", "derive_more", "metrics", "modular-bitfield", "parity-scale-codec", + "proptest", "reth-codecs", "reth-db-models", "reth-ethereum-primitives", + "reth-optimism-primitives", "reth-primitives-traits", "reth-prune-types", "reth-stages-types", @@ -5340,6 +5476,7 @@ source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab6077 dependencies = [ "alloy-eips", "alloy-primitives", + "arbitrary", "bytes", "modular-bitfield", "reth-codecs", @@ -5599,6 +5736,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-serde", + "arbitrary", "modular-bitfield", "reth-codecs", "reth-primitives-traits", @@ -5990,6 +6128,21 @@ dependencies = [ "reth-primitives-traits", ] +[[package]] +name = "reth-optimism-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "op-alloy-consensus", + "reth-primitives-traits", + "serde", + "serde_with", +] + [[package]] name = "reth-payload-builder" version = "1.9.3" @@ -6068,6 +6221,7 @@ dependencies = [ "alloy-rlp", "alloy-rpc-types-eth", "alloy-trie", + "arbitrary", "auto_impl", "byteorder", "bytes", @@ -6075,6 +6229,8 @@ dependencies = [ "modular-bitfield", "once_cell", "op-alloy-consensus", + "proptest", + "proptest-arbitrary-interop", "rayon", "reth-codecs", "revm-bytecode", @@ -6108,6 +6264,7 @@ dependencies = [ "reth-db", "reth-db-api", "reth-errors", + "reth-ethereum-engine-primitives", "reth-ethereum-primitives", "reth-execution-types", "reth-fs-util", @@ -6123,7 +6280,9 @@ dependencies = [ "reth-trie", "reth-trie-db", "revm-database", + "revm-state", "strum", + "tokio", "tracing", ] @@ -6133,6 +6292,7 @@ version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ "alloy-primitives", + "arbitrary", "derive_more", "modular-bitfield", "reth-codecs", @@ -6287,6 +6447,7 @@ version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ "alloy-primitives", + "arbitrary", "bytes", "modular-bitfield", "reth-codecs", @@ -6468,6 +6629,7 @@ dependencies = [ "reth-trie-sparse", "revm-database", "tracing", + "triehash", ] [[package]] @@ -6481,11 +6643,14 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-serde", "alloy-trie", + "arbitrary", "arrayvec", "bytes", "derive_more", + "hash-db", "itertools 0.14.0", "nybbles", + "plain_hasher", "rayon", "reth-codecs", "reth-primitives-traits", @@ -6516,6 +6681,7 @@ dependencies = [ "alloy-rlp", "alloy-trie", "auto_impl", + "rayon", "reth-execution-errors", "reth-primitives-traits", "reth-trie-common", @@ -6825,6 +6991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "alloy-rlp", + "arbitrary", "ark-ff 0.3.0", "ark-ff 0.4.2", "ark-ff 0.5.0", @@ -7390,6 +7557,7 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ + "arbitrary", "serde", ] @@ -8075,6 +8243,16 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "triehash" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" +dependencies = [ + "hash-db", + "rlp", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -8257,6 +8435,17 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 1e52c69..1cff19e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/payload/types", "crates/primitives", "crates/revm", + "crates/txpool", ] [workspace.lints] @@ -49,6 +50,7 @@ morph-primitives = { path = "crates/primitives", default-features = false, featu "reth", ] } morph-revm = { path = "crates/revm", default-features = false } +morph-txpool = { path = "crates/txpool", default-features = false } reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-chain-state = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } diff --git a/crates/engine-api/src/lib.rs b/crates/engine-api/src/lib.rs index a41cd69..068fc8c 100644 --- a/crates/engine-api/src/lib.rs +++ b/crates/engine-api/src/lib.rs @@ -4,7 +4,7 @@ //! //! - [`MorphL2EngineApi`]: The L2 Engine API trait for block building and validation //! - [`MorphL2EngineRpcServer`]: The JSON-RPC server implementation -//! - [`MorphValidationContext`]: Validation context with Emerald hardfork support +//! - [`MorphValidationContext`]: Validation context with MPTFork hardfork support //! //! # L2 Engine API //! @@ -16,7 +16,6 @@ //! - `engine_validateL2Block`: Validate a block without importing //! - `engine_newL2Block`: Import and finalize a block //! - `engine_newSafeL2Block`: Import a safe block from derivation -//! #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] diff --git a/crates/engine-api/src/validator.rs b/crates/engine-api/src/validator.rs index be23b65..1ee25c0 100644 --- a/crates/engine-api/src/validator.rs +++ b/crates/engine-api/src/validator.rs @@ -1,19 +1,19 @@ //! Morph Engine Validator utilities. //! //! This module provides utilities for state root validation according to -//! the MPT fork rules. +//! the MPTFork hardfork rules. //! -//! **Important**: Morph skips state root validation before the MPT fork, -//! because state root verification happens in the ZK proof instead (using ZK-trie). -//! After MPT fork, state root validation is performed in the node (using MPT). +//! **Important**: Morph skips state root validation before the MPTFork hardfork, +//! similar to how Scroll skips it before Euclid. Before MPTFork, Morph uses +//! ZK-trie, and state root verification happens in the ZK proof instead. use morph_chainspec::{MorphChainSpec, MorphHardforks}; use std::sync::Arc; /// Determines if state root validation should be performed for a given timestamp. /// -/// Before the MPT fork, state root validation is skipped because Morph -/// uses ZK-trie before MPT fork, and the state root verification happens in the +/// Before the MPTFork hardfork, state root validation is skipped because Morph +/// uses ZK-trie before MPTFork, and the state root verification happens in the /// ZK proof instead. /// /// # Arguments @@ -98,7 +98,7 @@ mod tests { #[test] fn test_should_validate_state_root_no_mpt_fork() { - // If MPT fork is not set, should always skip validation (return false, using ZK-trie) + // If MPTFork is not set, should always return false let chain_spec = create_test_chainspec(None); assert!(!should_validate_state_root(&chain_spec, 0)); diff --git a/crates/txpool/Cargo.toml b/crates/txpool/Cargo.toml new file mode 100644 index 0000000..4d7ca10 --- /dev/null +++ b/crates/txpool/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "morph-txpool" +description = "Morph L2 transaction pool" + +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# Morph +morph-chainspec.workspace = true +morph-primitives = { workspace = true, features = ["reth-codec"] } +morph-revm.workspace = true + +# Reth +reth-chainspec.workspace = true +reth-primitives-traits.workspace = true +reth-revm.workspace = true +reth-storage-api.workspace = true +reth-transaction-pool.workspace = true + +# Alloy +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-primitives.workspace = true + +# Misc +c-kzg = "2.1.5" +derive_more.workspace = true +parking_lot.workspace = true +tracing.workspace = true + +[dev-dependencies] +reth-provider = { workspace = true, features = ["test-utils"] } diff --git a/crates/txpool/src/lib.rs b/crates/txpool/src/lib.rs new file mode 100644 index 0000000..997ef1f --- /dev/null +++ b/crates/txpool/src/lib.rs @@ -0,0 +1,62 @@ +//! Transaction pool for Morph L2 node. +//! +//! This crate provides transaction pool validation and ordering for Morph L2. +//! +//! # Key Components +//! +//! - [`MorphPooledTransaction`]: Pool transaction wrapper with L2-specific caching +//! - [`MorphTransactionValidator`]: Transaction validator with L1 fee validation +//! - [`MorphL1BlockInfo`]: L1 block info tracker for fee calculation +//! - [`MorphTransactionPool`]: Type alias for the default Morph transaction pool +//! +//! # Transaction Validation +//! +//! The validator performs the following Morph-specific checks: +//! - Rejects EIP-4844 blob transactions (not supported on L2) +//! - Rejects L1 message transactions (only included by sequencer) +//! - Validates L1 data fee affordability (optional) +//! +//! # Usage +//! +//! ``` +//! use morph_txpool::{MorphTransactionValidator, MorphPooledTransaction}; +//! use reth_transaction_pool::{EthTransactionValidator, Pool, TransactionValidationTaskExecutor}; +//! +//! // Create the inner Ethereum validator +//! let eth_validator = EthTransactionValidatorBuilder::new(client) +//! .no_cancun() +//! .build(blob_store); +//! +//! // Wrap with Morph-specific validation +//! let validator = MorphTransactionValidator::new(eth_validator); +//! +//! // Create the pool +//! let pool = Pool::new( +//! TransactionValidationTaskExecutor::eth_builder(validator), +//! ordering, +//! blob_store, +//! ); +//! ``` + +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg), allow(unexpected_cfgs))] + +mod transaction; +pub use transaction::MorphPooledTransaction; + +mod validator; +pub use validator::{MorphL1BlockInfo, MorphTransactionValidator}; + +use reth_transaction_pool::{CoinbaseTipOrdering, Pool, TransactionValidationTaskExecutor}; + +/// Type alias for default Morph transaction pool. +/// +/// This pool uses: +/// - [`MorphTransactionValidator`] for transaction validation +/// - [`CoinbaseTipOrdering`] for transaction ordering (by effective gas tip) +/// - [`MorphPooledTransaction`] as the pooled transaction type +pub type MorphTransactionPool = Pool< + TransactionValidationTaskExecutor>, + CoinbaseTipOrdering, + S, +>; diff --git a/crates/txpool/src/transaction.rs b/crates/txpool/src/transaction.rs new file mode 100644 index 0000000..329f134 --- /dev/null +++ b/crates/txpool/src/transaction.rs @@ -0,0 +1,212 @@ +//! Pool transaction type for Morph L2. + +use alloy_consensus::{ + BlobTransactionValidationError, Typed2718, transaction::Recovered, transaction::TxHashRef, +}; +use alloy_eips::{ + eip2930::AccessList, eip7594::BlobTransactionSidecarVariant, eip7702::SignedAuthorization, +}; +use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U256}; +use c_kzg::KzgSettings; +use core::fmt::Debug; +use morph_primitives::{MorphTxEnvelope, MorphTxType}; +use reth_primitives_traits::InMemorySize; +use reth_transaction_pool::{ + EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction, +}; +use std::sync::{Arc, OnceLock}; + +/// Pool transaction for Morph L2. +/// +/// This type wraps the actual transaction and caches values that are frequently used by the pool. +/// It provides efficient access to encoded transaction bytes for L1 fee calculation. +#[derive(Debug, Clone, derive_more::Deref)] +pub struct MorphPooledTransaction { + #[deref] + inner: EthPooledTransaction, + + /// Cached EIP-2718 encoded bytes of the transaction, lazily computed. + encoded_2718: OnceLock, +} + +impl MorphPooledTransaction { + /// Create a new instance of [`MorphPooledTransaction`]. + pub fn new(transaction: Recovered, encoded_length: usize) -> Self { + Self { + inner: EthPooledTransaction::new(transaction, encoded_length), + encoded_2718: Default::default(), + } + } + + /// Returns lazily computed EIP-2718 encoded bytes of the transaction. + pub fn encoded_2718(&self) -> &Bytes { + self.encoded_2718 + .get_or_init(|| self.inner.transaction().rlp()) + } + + /// Returns true if this is an L1 message transaction. + pub fn is_l1_message(&self) -> bool { + self.inner.transaction().is_l1_msg() + } + + /// Returns the queue index for L1 message transactions. + pub fn queue_index(&self) -> Option { + self.inner.transaction().queue_index() + } + + /// Returns true if this is an AltFee transaction. + pub fn is_alt_fee(&self) -> bool { + self.inner.transaction().tx_type() == MorphTxType::AltFee + } +} + +impl PoolTransaction for MorphPooledTransaction { + type TryFromConsensusError = >::Error; + type Consensus = MorphTxEnvelope; + type Pooled = MorphTxEnvelope; + + fn clone_into_consensus(&self) -> Recovered { + self.inner.transaction().clone() + } + + fn into_consensus(self) -> Recovered { + self.inner.transaction + } + + fn from_pooled(tx: Recovered) -> Self { + let encoded_len = alloy_eips::eip2718::Encodable2718::encode_2718_len(&tx); + Self::new(tx, encoded_len) + } + + fn hash(&self) -> &TxHash { + self.inner.transaction.tx_hash() + } + + fn sender(&self) -> Address { + self.inner.transaction.signer() + } + + fn sender_ref(&self) -> &Address { + self.inner.transaction.signer_ref() + } + + fn cost(&self) -> &U256 { + &self.inner.cost + } + + fn encoded_length(&self) -> usize { + self.inner.encoded_length + } +} + +impl Typed2718 for MorphPooledTransaction { + fn ty(&self) -> u8 { + self.inner.ty() + } +} + +impl InMemorySize for MorphPooledTransaction { + fn size(&self) -> usize { + self.inner.size() + } +} + +impl alloy_consensus::Transaction for MorphPooledTransaction { + fn chain_id(&self) -> Option { + self.inner.chain_id() + } + + fn nonce(&self) -> u64 { + self.inner.nonce() + } + + fn gas_limit(&self) -> u64 { + self.inner.gas_limit() + } + + fn gas_price(&self) -> Option { + self.inner.gas_price() + } + + fn max_fee_per_gas(&self) -> u128 { + self.inner.max_fee_per_gas() + } + + fn max_priority_fee_per_gas(&self) -> Option { + self.inner.max_priority_fee_per_gas() + } + + fn max_fee_per_blob_gas(&self) -> Option { + self.inner.max_fee_per_blob_gas() + } + + fn priority_fee_or_price(&self) -> u128 { + self.inner.priority_fee_or_price() + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + self.inner.effective_gas_price(base_fee) + } + + fn is_dynamic_fee(&self) -> bool { + self.inner.is_dynamic_fee() + } + + fn kind(&self) -> TxKind { + self.inner.kind() + } + + fn is_create(&self) -> bool { + self.inner.is_create() + } + + fn value(&self) -> U256 { + self.inner.value() + } + + fn input(&self) -> &Bytes { + self.inner.input() + } + + fn access_list(&self) -> Option<&AccessList> { + self.inner.access_list() + } + + fn blob_versioned_hashes(&self) -> Option<&[B256]> { + self.inner.blob_versioned_hashes() + } + + fn authorization_list(&self) -> Option<&[SignedAuthorization]> { + self.inner.authorization_list() + } +} + +impl EthPoolTransaction for MorphPooledTransaction { + fn take_blob(&mut self) -> EthBlobTransactionSidecar { + EthBlobTransactionSidecar::None + } + + fn try_into_pooled_eip4844( + self, + _sidecar: Arc, + ) -> Option> { + None + } + + fn try_from_eip4844( + _tx: Recovered, + _sidecar: BlobTransactionSidecarVariant, + ) -> Option { + None + } + + fn validate_blob( + &self, + _sidecar: &BlobTransactionSidecarVariant, + _settings: &KzgSettings, + ) -> Result<(), BlobTransactionValidationError> { + Err(BlobTransactionValidationError::NotBlobTransaction( + self.ty(), + )) + } +} diff --git a/crates/txpool/src/validator.rs b/crates/txpool/src/validator.rs new file mode 100644 index 0000000..fdd424f --- /dev/null +++ b/crates/txpool/src/validator.rs @@ -0,0 +1,377 @@ +//! Transaction validator for Morph L2. +//! +//! This module provides Morph-specific transaction validation that extends the standard +//! Ethereum transaction validation with L2 checks: +//! - Rejection of EIP-4844 blob transactions +//! - Rejection of L1 message transactions from the pool +//! - L1 data fee validation + +use alloy_consensus::BlockHeader; +use alloy_eips::Encodable2718; +use morph_chainspec::hardfork::MorphHardforks; +use morph_primitives::MorphTxEnvelope; +use morph_revm::L1BlockInfo; +use parking_lot::RwLock; +use reth_chainspec::ChainSpecProvider; +use reth_primitives_traits::{ + Block, GotExpected, SealedBlock, transaction::error::InvalidTransactionError, +}; +use reth_revm::database::StateProviderDatabase; +use reth_storage_api::{BlockReaderIdExt, StateProviderFactory}; +use reth_transaction_pool::{ + EthPoolTransaction, EthTransactionValidator, TransactionOrigin, TransactionValidationOutcome, + TransactionValidator, +}; +use std::sync::{ + Arc, + atomic::{AtomicU64, Ordering}, +}; + +/// Tracks L1 block info for the current chain head. +/// +/// This is used to cache L1 fee parameters and update them when the chain head changes. +#[derive(Debug, Default)] +pub struct MorphL1BlockInfo { + /// The current L1 block info. + l1_block_info: RwLock, + /// Current block timestamp. + timestamp: AtomicU64, + /// Current block number. + number: AtomicU64, +} + +impl MorphL1BlockInfo { + /// Creates a new instance with default values. + pub fn new() -> Self { + Self::default() + } + + /// Returns the current L1 block info. + pub fn l1_block_info(&self) -> L1BlockInfo { + self.l1_block_info.read().clone() + } + + /// Updates the L1 block info. + pub fn update(&self, info: L1BlockInfo, timestamp: u64, number: u64) { + *self.l1_block_info.write() = info; + self.timestamp.store(timestamp, Ordering::Relaxed); + self.number.store(number, Ordering::Relaxed); + } + + /// Returns the current block timestamp. + pub fn timestamp(&self) -> u64 { + self.timestamp.load(Ordering::Relaxed) + } + + /// Returns the current block number. + pub fn number(&self) -> u64 { + self.number.load(Ordering::Relaxed) + } +} + +/// Validator for Morph L2 transactions. +/// +/// This validator extends [`EthTransactionValidator`] with Morph-specific checks: +/// - Rejects EIP-4844 blob transactions (not supported on L2) +/// - Rejects L1 message transactions (only included by sequencer) +/// - Optionally validates L1 data fee affordability +#[derive(Debug)] +pub struct MorphTransactionValidator { + /// The type that performs the actual validation. + inner: EthTransactionValidator, + /// Additional block info required for validation. + block_info: Arc, + /// If true, ensure that the transaction's sender has enough balance to cover the L1 gas fee + /// derived from the tracked L1 block info. + require_l1_data_gas_fee: bool, +} + +impl MorphTransactionValidator { + /// Returns the configured chain spec. + pub fn chain_spec(&self) -> Arc + where + Client: ChainSpecProvider, + { + self.inner.chain_spec() + } + + /// Returns the configured client. + pub const fn client(&self) -> &Client { + self.inner.client() + } + + /// Returns the current block timestamp. + fn block_timestamp(&self) -> u64 { + self.block_info.timestamp() + } + + /// Returns the current block number. + fn block_number(&self) -> u64 { + self.block_info.number() + } + + /// Whether to ensure that the transaction's sender has enough balance to also cover the L1 gas + /// fee. + pub fn require_l1_data_gas_fee(self, require_l1_data_gas_fee: bool) -> Self { + Self { + require_l1_data_gas_fee, + ..self + } + } + + /// Returns whether this validator also requires the transaction's sender to have enough balance + /// to cover the L1 gas fee. + pub const fn requires_l1_data_gas_fee(&self) -> bool { + self.require_l1_data_gas_fee + } + + /// Returns a reference to the block info tracker. + pub fn block_info(&self) -> &Arc { + &self.block_info + } +} + +impl MorphTransactionValidator +where + Client: ChainSpecProvider + StateProviderFactory + BlockReaderIdExt, + Tx: EthPoolTransaction, +{ + /// Create a new [`MorphTransactionValidator`]. + pub fn new(inner: EthTransactionValidator) -> Self { + let this = Self::with_block_info(inner, MorphL1BlockInfo::default()); + if let Ok(Some(block)) = this + .inner + .client() + .block_by_number_or_tag(alloy_eips::BlockNumberOrTag::Latest) + { + this.block_info + .timestamp + .store(block.header().timestamp(), Ordering::Relaxed); + this.block_info + .number + .store(block.header().number(), Ordering::Relaxed); + this.update_l1_block_info(block.header()); + } + + this + } + + /// Create a new [`MorphTransactionValidator`] with the given [`MorphL1BlockInfo`]. + pub fn with_block_info( + inner: EthTransactionValidator, + block_info: MorphL1BlockInfo, + ) -> Self { + Self { + inner, + block_info: Arc::new(block_info), + require_l1_data_gas_fee: true, + } + } + + /// Update the L1 block info for the given header. + pub fn update_l1_block_info(&self, header: &H) + where + H: BlockHeader, + { + self.block_info + .timestamp + .store(header.timestamp(), Ordering::Relaxed); + self.block_info + .number + .store(header.number(), Ordering::Relaxed); + + let provider = match self + .client() + .state_by_block_number_or_tag(header.number().into()) + { + Ok(provider) => provider, + Err(err) => { + tracing::warn!(target: "morph_txpool", %err, "Failed to get state provider for L1 block info update"); + return; + } + }; + + let mut db = StateProviderDatabase::new(provider); + let hardfork = self + .chain_spec() + .morph_hardfork_at(header.number(), header.timestamp()); + + match L1BlockInfo::try_fetch(&mut db, hardfork) { + Ok(l1_block_info) => { + *self.block_info.l1_block_info.write() = l1_block_info; + } + Err(err) => { + tracing::warn!(target: "morph_txpool", ?err, "Failed to fetch L1 block info"); + } + } + } + + /// Validates a single transaction. + /// + /// See also [`TransactionValidator::validate_transaction`] + /// + /// This behaves the same as [`EthTransactionValidator::validate_one`], but in addition: + /// - Rejects EIP-4844 blob transactions + /// - Rejects L1 message transactions + /// - Ensures that the account has enough balance to cover the L1 gas cost (if enabled) + pub fn validate_one( + &self, + origin: TransactionOrigin, + transaction: Tx, + ) -> TransactionValidationOutcome { + // Reject EIP-4844 blob transactions - not supported on L2 + if transaction.is_eip4844() { + return TransactionValidationOutcome::Invalid( + transaction, + InvalidTransactionError::Eip4844Disabled.into(), + ); + } + + // Reject L1 message transactions - only included by sequencer + if is_l1_message(&transaction) { + return TransactionValidationOutcome::Invalid( + transaction, + InvalidTransactionError::TxTypeNotSupported.into(), + ); + } + + let outcome = self.inner.validate_one(origin, transaction); + if outcome.is_invalid() || outcome.is_error() { + tracing::trace!(target: "morph_txpool", ?outcome, "tx pool validation failed"); + return outcome; + } + + if !self.requires_l1_data_gas_fee() { + // No need to check L1 gas fee + return outcome; + } + + // Ensure that the account has enough balance to cover the L1 gas cost + if let TransactionValidationOutcome::Valid { + balance, + state_nonce, + transaction: valid_tx, + propagate, + bytecode_hash, + authorities, + } = outcome + { + let l1_block_info = self.block_info.l1_block_info.read().clone(); + let hardfork = self + .chain_spec() + .morph_hardfork_at(self.block_number(), self.block_timestamp()); + + // Encode the transaction for L1 fee calculation + let mut encoded = Vec::with_capacity(valid_tx.transaction().encoded_length()); + let tx = valid_tx.transaction().clone_into_consensus(); + tx.encode_2718(&mut encoded); + + // Calculate L1 data cost + let cost_addition = l1_block_info.calculate_tx_l1_cost(&encoded, hardfork); + + let cost = valid_tx.transaction().cost().saturating_add(cost_addition); + + // Check if the user has enough balance to cover L2 gas + L1 data fee + if cost > balance { + return TransactionValidationOutcome::Invalid( + valid_tx.into_transaction(), + InvalidTransactionError::InsufficientFunds( + GotExpected { + got: balance, + expected: cost, + } + .into(), + ) + .into(), + ); + } + + return TransactionValidationOutcome::Valid { + balance, + state_nonce, + bytecode_hash, + transaction: valid_tx, + propagate, + authorities, + }; + } + + outcome + } + + /// Validates all given transactions. + /// + /// Returns all outcomes for the given transactions in the same order. + /// + /// See also [`Self::validate_one`] + pub fn validate_all( + &self, + transactions: Vec<(TransactionOrigin, Tx)>, + ) -> Vec> { + transactions + .into_iter() + .map(|(origin, tx)| self.validate_one(origin, tx)) + .collect() + } +} + +impl TransactionValidator for MorphTransactionValidator +where + Client: ChainSpecProvider + StateProviderFactory + BlockReaderIdExt, + Tx: EthPoolTransaction, +{ + type Transaction = Tx; + + async fn validate_transaction( + &self, + origin: TransactionOrigin, + transaction: Self::Transaction, + ) -> TransactionValidationOutcome { + self.validate_one(origin, transaction) + } + + async fn validate_transactions( + &self, + transactions: Vec<(TransactionOrigin, Self::Transaction)>, + ) -> Vec> { + self.validate_all(transactions) + } + + fn on_new_head_block(&self, new_tip_block: &SealedBlock) + where + B: Block, + { + self.inner.on_new_head_block(new_tip_block); + self.update_l1_block_info(new_tip_block.header()); + } +} + +/// Helper function to check if a transaction is an L1 message. +fn is_l1_message(tx: &Tx) -> bool +where + Tx: EthPoolTransaction, +{ + tx.clone_into_consensus().is_l1_msg() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_morph_l1_block_info_default() { + let info = MorphL1BlockInfo::new(); + assert_eq!(info.timestamp(), 0); + assert_eq!(info.number(), 0); + } + + #[test] + fn test_morph_l1_block_info_update() { + let info = MorphL1BlockInfo::new(); + let l1_info = L1BlockInfo::default(); + info.update(l1_info, 1234, 100); + + assert_eq!(info.timestamp(), 1234); + assert_eq!(info.number(), 100); + } +} From 37428815be74c4f75791b4e6b892c5f8781e91d6 Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 28 Jan 2026 11:22:18 +0800 Subject: [PATCH 02/19] feat: add morph tx validation --- Cargo.lock | 2 + crates/chainspec/src/hardfork.rs | 19 ++ crates/chainspec/src/spec.rs | 100 ++++++ crates/primitives/src/transaction/envelope.rs | 21 ++ crates/txpool/Cargo.toml | 3 + crates/txpool/src/error.rs | 201 ++++++++++++ crates/txpool/src/lib.rs | 40 +-- crates/txpool/src/maintain.rs | 190 +++++++++++ crates/txpool/src/morph_tx_validation.rs | 186 +++++++++++ crates/txpool/src/transaction.rs | 6 +- crates/txpool/src/validator.rs | 302 ++++++++++++++++-- 11 files changed, 1018 insertions(+), 52 deletions(-) create mode 100644 crates/txpool/src/error.rs create mode 100644 crates/txpool/src/maintain.rs create mode 100644 crates/txpool/src/morph_tx_validation.rs diff --git a/Cargo.lock b/Cargo.lock index 349e319..6868ccb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4053,9 +4053,11 @@ version = "0.7.5" dependencies = [ "alloy-consensus", "alloy-eips", + "alloy-evm", "alloy-primitives", "c-kzg", "derive_more", + "futures", "morph-chainspec", "morph-primitives", "morph-revm", diff --git a/crates/chainspec/src/hardfork.rs b/crates/chainspec/src/hardfork.rs index 0b882a9..b3da1e6 100644 --- a/crates/chainspec/src/hardfork.rs +++ b/crates/chainspec/src/hardfork.rs @@ -162,6 +162,25 @@ pub trait MorphHardforks: EthereumHardforks { } } +// Blanket implementations for Arc and reference types +impl MorphHardforks for std::sync::Arc +where + T: MorphHardforks, +{ + fn morph_fork_activation(&self, fork: MorphHardfork) -> ForkCondition { + T::morph_fork_activation(self, fork) + } +} + +impl MorphHardforks for &T +where + T: MorphHardforks, +{ + fn morph_fork_activation(&self, fork: MorphHardfork) -> ForkCondition { + T::morph_fork_activation(*self, fork) + } +} + impl From for SpecId { fn from(value: MorphHardfork) -> Self { match value { diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 3b70589..44f0632 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -159,6 +159,17 @@ impl From for MorphChainSpec { base_spec.hardforks.extend(morph_forks); + // Add EthereumHardfork::Prague at Emerald activation time. + // This enables EIP-7702 support in the transaction pool validator, + // which checks `is_prague_active_at_timestamp()`. + // Note: genesis.json doesn't need pragueTime - we derive it from emeraldTime. + if let Some(emerald_time) = hardfork_info.emerald_time { + base_spec.hardforks.insert( + EthereumHardfork::Prague, + ForkCondition::Timestamp(emerald_time), + ); + } + Self { inner: base_spec.map_header(MorphHeader::from), info: chain_info, @@ -566,6 +577,95 @@ mod tests { assert!(config.is_fee_vault_enabled()); } + #[test] + fn test_prague_activated_with_emerald() { + // Prague should be activated at the same time as Emerald + // This enables EIP-7702 in the transaction pool validator + 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, + "bernoulliBlock": 0, + "curieBlock": 0, + "morph203Time": 1000, + "viridianTime": 2000, + "emeraldTime": 3000, + "morph": {} + }, + "alloc": {} + }); + + let genesis: Genesis = + serde_json::from_value(genesis_json).expect("genesis should be valid"); + let chainspec = MorphChainSpec::from(genesis); + + // Prague should not be active before Emerald + assert!(!chainspec.is_prague_active_at_timestamp(2999)); + + // Prague should be active at Emerald time + assert!(chainspec.is_prague_active_at_timestamp(3000)); + + // Prague should remain active after Emerald + assert!(chainspec.is_prague_active_at_timestamp(5000)); + + // Verify the fork condition is set correctly + let prague_activation = chainspec.ethereum_fork_activation(EthereumHardfork::Prague); + assert_eq!(prague_activation, ForkCondition::Timestamp(3000)); + } + + #[test] + fn test_prague_not_activated_without_emerald() { + // If Emerald is not configured, Prague should not be activated + 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, + "bernoulliBlock": 0, + "curieBlock": 0, + "morph203Time": 1000, + "viridianTime": 2000, + "morph": {} + }, + "alloc": {} + }); + + let genesis: Genesis = + serde_json::from_value(genesis_json).expect("genesis should be valid"); + let chainspec = MorphChainSpec::from(genesis); + + // Prague should not be active since Emerald is not configured + assert!(!chainspec.is_prague_active_at_timestamp(0)); + assert!(!chainspec.is_prague_active_at_timestamp(5000)); + } + #[test] fn test_chain_config_with_fee_vault() { let genesis_json = json!({ diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index efcec41..759e5ad 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -62,6 +62,27 @@ impl MorphTxEnvelope { self.tx_type() == MorphTxType::L1Msg } + /// Returns `true` if this is a MorphTx (0x7F) transaction. + pub fn is_morph_tx(&self) -> bool { + self.tx_type() == MorphTxType::Morph + } + + /// Returns the fee token ID for MorphTx, or `None` for other transaction types. + pub fn fee_token_id(&self) -> Option { + match self { + Self::Morph(tx) => Some(tx.tx().fee_token_id), + _ => None, + } + } + + /// Returns the fee limit for MorphTx, or `None` for other transaction types. + pub fn fee_limit(&self) -> Option { + match self { + Self::Morph(tx) => Some(tx.tx().fee_limit), + _ => None, + } + } + pub fn queue_index(&self) -> Option { match self { Self::Legacy(_) diff --git a/crates/txpool/Cargo.toml b/crates/txpool/Cargo.toml index 4d7ca10..77e6e13 100644 --- a/crates/txpool/Cargo.toml +++ b/crates/txpool/Cargo.toml @@ -20,6 +20,7 @@ morph-revm.workspace = true # Reth reth-chainspec.workspace = true reth-primitives-traits.workspace = true +reth-provider.workspace = true reth-revm.workspace = true reth-storage-api.workspace = true reth-transaction-pool.workspace = true @@ -27,11 +28,13 @@ reth-transaction-pool.workspace = true # Alloy alloy-consensus.workspace = true alloy-eips.workspace = true +alloy-evm.workspace = true alloy-primitives.workspace = true # Misc c-kzg = "2.1.5" derive_more.workspace = true +futures.workspace = true parking_lot.workspace = true tracing.workspace = true diff --git a/crates/txpool/src/error.rs b/crates/txpool/src/error.rs new file mode 100644 index 0000000..169c0ec --- /dev/null +++ b/crates/txpool/src/error.rs @@ -0,0 +1,201 @@ +//! Error types for Morph transaction pool validation. +//! +//! This module defines error types specific to MorphTx (0x7F) validation, +//! which allows users to pay gas fees using ERC20 tokens. + +use alloy_primitives::{Address, U256}; +use reth_transaction_pool::error::{InvalidPoolTransactionError, PoolTransactionError}; +use std::fmt; + +/// Errors that can occur during MorphTx validation. +/// +/// These errors are specific to transactions that use ERC20 tokens for gas payment. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MorphTxError { + /// Token ID 0 is not allowed (reserved for ETH). + InvalidTokenId, + + /// Token is not registered in the L2TokenRegistry. + TokenNotFound { + /// The requested token ID. + token_id: u16, + }, + + /// Token is registered but not active for gas payment. + TokenNotActive { + /// The token ID that is not active. + token_id: u16, + }, + + /// Token price ratio is zero or invalid. + InvalidPriceRatio { + /// The token ID with invalid price. + token_id: u16, + }, + + /// The fee_limit is lower than the required token amount. + FeeLimitTooLow { + /// The fee_limit specified in the transaction. + fee_limit: U256, + /// The required token amount for the transaction. + required: U256, + }, + + /// Insufficient ERC20 token balance to pay for gas. + InsufficientTokenBalance { + /// The token ID. + token_id: u16, + /// The token address. + token_address: Address, + /// The actual token balance. + balance: U256, + /// The required token amount. + required: U256, + }, + + /// Insufficient ETH balance to pay for transaction value. + /// MorphTx still requires ETH for the `value` field. + InsufficientEthForValue { + /// The ETH balance. + balance: U256, + /// The transaction value. + value: U256, + }, + + /// Failed to fetch token information from state. + TokenInfoFetchFailed { + /// The token ID. + token_id: u16, + /// Error message. + message: String, + }, +} + +impl fmt::Display for MorphTxError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidTokenId => { + write!(f, "invalid token ID: token ID 0 is reserved for ETH") + } + Self::TokenNotFound { token_id } => { + write!( + f, + "token ID {token_id} is not registered in L2TokenRegistry" + ) + } + Self::TokenNotActive { token_id } => { + write!(f, "token ID {token_id} is not active for gas payment") + } + Self::InvalidPriceRatio { token_id } => { + write!(f, "token ID {token_id} has invalid price ratio (zero)") + } + Self::FeeLimitTooLow { + fee_limit, + required, + } => { + write!( + f, + "fee_limit ({fee_limit}) is lower than required token amount ({required})" + ) + } + Self::InsufficientTokenBalance { + token_id, + token_address, + balance, + required, + } => { + write!( + f, + "insufficient token balance for token ID {token_id} ({token_address}): \ + balance {balance}, required {required}" + ) + } + Self::InsufficientEthForValue { balance, value } => { + write!( + f, + "insufficient ETH balance for transaction value: balance {balance}, value {value}" + ) + } + Self::TokenInfoFetchFailed { token_id, message } => { + write!(f, "failed to fetch token info for ID {token_id}: {message}") + } + } + } +} + +impl std::error::Error for MorphTxError {} + +impl PoolTransactionError for MorphTxError { + fn is_bad_transaction(&self) -> bool { + // MorphTx validation errors are not necessarily "bad" transactions that warrant + // peer penalization. They are often just insufficient balance or inactive tokens. + match self { + // Invalid token ID is a bad transaction (should never be submitted) + Self::InvalidTokenId => true, + // Token not found or not active - could be due to temporary state, not penalizable + Self::TokenNotFound { .. } | Self::TokenNotActive { .. } => false, + // Invalid price ratio - configuration issue, not penalizable + Self::InvalidPriceRatio { .. } => false, + // Insufficient balance or fee limit - normal validation failure + Self::FeeLimitTooLow { .. } + | Self::InsufficientTokenBalance { .. } + | Self::InsufficientEthForValue { .. } => false, + // Fetch failures - infrastructure issue, not penalizable + Self::TokenInfoFetchFailed { .. } => false, + } + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl From for InvalidPoolTransactionError { + fn from(err: MorphTxError) -> Self { + Self::Other(Box::new(err)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + + #[test] + fn test_error_display() { + let err = MorphTxError::InvalidTokenId; + assert!(err.to_string().contains("token ID 0")); + + let err = MorphTxError::TokenNotFound { token_id: 1 }; + assert!(err.to_string().contains("token ID 1")); + assert!(err.to_string().contains("not registered")); + + let err = MorphTxError::TokenNotActive { token_id: 2 }; + assert!(err.to_string().contains("token ID 2")); + assert!(err.to_string().contains("not active")); + + let err = MorphTxError::FeeLimitTooLow { + fee_limit: U256::from(100), + required: U256::from(200), + }; + assert!(err.to_string().contains("100")); + assert!(err.to_string().contains("200")); + + let err = MorphTxError::InsufficientTokenBalance { + token_id: 1, + token_address: address!("1234567890123456789012345678901234567890"), + balance: U256::from(50), + required: U256::from(100), + }; + assert!(err.to_string().contains("token ID 1")); + assert!(err.to_string().contains("50")); + assert!(err.to_string().contains("100")); + } + + #[test] + fn test_error_conversion() { + let err = MorphTxError::InvalidTokenId; + let pool_err: InvalidPoolTransactionError = err.into(); + assert!(matches!(pool_err, InvalidPoolTransactionError::Other(_))); + } +} diff --git a/crates/txpool/src/lib.rs b/crates/txpool/src/lib.rs index 997ef1f..2754164 100644 --- a/crates/txpool/src/lib.rs +++ b/crates/txpool/src/lib.rs @@ -8,6 +8,7 @@ //! - [`MorphTransactionValidator`]: Transaction validator with L1 fee validation //! - [`MorphL1BlockInfo`]: L1 block info tracker for fee calculation //! - [`MorphTransactionPool`]: Type alias for the default Morph transaction pool +//! - [`MorphTxError`]: Error types for MorphTx (0x7F) validation //! //! # Transaction Validation //! @@ -15,38 +16,37 @@ //! - Rejects EIP-4844 blob transactions (not supported on L2) //! - Rejects L1 message transactions (only included by sequencer) //! - Validates L1 data fee affordability (optional) +//! - Validates MorphTx (0x7F) ERC20 token balance and fee_limit //! -//! # Usage +//! # MorphTx (0x7F) Validation //! -//! ``` -//! use morph_txpool::{MorphTransactionValidator, MorphPooledTransaction}; -//! use reth_transaction_pool::{EthTransactionValidator, Pool, TransactionValidationTaskExecutor}; -//! -//! // Create the inner Ethereum validator -//! let eth_validator = EthTransactionValidatorBuilder::new(client) -//! .no_cancun() -//! .build(blob_store); -//! -//! // Wrap with Morph-specific validation -//! let validator = MorphTransactionValidator::new(eth_validator); -//! -//! // Create the pool -//! let pool = Pool::new( -//! TransactionValidationTaskExecutor::eth_builder(validator), -//! ordering, -//! blob_store, -//! ); -//! ``` +//! MorphTx allows users to pay gas fees using ERC20 tokens. The validator: +//! 1. Checks the token is registered and active in L2TokenRegistry +//! 2. Calculates required token amount: `eth_to_token(gas_fee + l1_data_fee)` +//! 3. Verifies `fee_limit >= required_token_amount` +//! 4. Verifies `token_balance >= min(fee_limit, required_token_amount)` +//! 5. Verifies `eth_balance >= value` (transaction value is still in ETH) #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg), allow(unexpected_cfgs))] +mod error; +pub use error::MorphTxError; + mod transaction; pub use transaction::MorphPooledTransaction; mod validator; pub use validator::{MorphL1BlockInfo, MorphTransactionValidator}; +mod maintain; +pub use maintain::maintain_morph_pool; + +mod morph_tx_validation; +pub use morph_tx_validation::{ + MorphTxValidationInput, MorphTxValidationResult, extract_morph_tx_fields, validate_morph_tx, +}; + use reth_transaction_pool::{CoinbaseTipOrdering, Pool, TransactionValidationTaskExecutor}; /// Type alias for default Morph transaction pool. diff --git a/crates/txpool/src/maintain.rs b/crates/txpool/src/maintain.rs new file mode 100644 index 0000000..693089e --- /dev/null +++ b/crates/txpool/src/maintain.rs @@ -0,0 +1,190 @@ +//! Transaction pool maintenance tasks for Morph L2. +//! +//! This module provides maintenance tasks for the Morph transaction pool, +//! specifically for revalidating MorphTx (0x7F) transactions when the chain +//! state changes. +//! +//! # Background +//! +//! MorphTx allows users to pay gas fees using ERC20 tokens. Since reth's txpool +//! only tracks ETH balance changes (via `SenderInfo`), it cannot automatically +//! demote MorphTx transactions when the token balance decreases. +//! +//! This maintenance task solves this by: +//! 1. Listening to canonical state changes (new blocks) +//! 2. Re-validating all MorphTx transactions in the pool +//! 3. Removing transactions that no longer have sufficient token balance +//! +//! # Reference +//! +//! This is similar to how go-ethereum handles MorphTx in `promoteExecutables` +//! and `demoteUnexecutables` (tx_pool.go), but implemented as a separate +//! maintenance task since we cannot modify reth's internal pool logic. + +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{TxHash, U256}; +use futures::StreamExt; +use morph_chainspec::hardfork::MorphHardforks; +use morph_primitives::MorphTxEnvelope; +use morph_revm::L1BlockInfo; +use reth_chainspec::ChainSpecProvider; +use reth_primitives_traits::AlloyBlockHeader; +use reth_provider::CanonStateSubscriptions; +use reth_revm::Database; +use reth_revm::database::StateProviderDatabase; +use reth_storage_api::StateProviderFactory; +use reth_transaction_pool::{EthPoolTransaction, PoolTransaction, TransactionPool}; +use std::collections::HashSet; + +/// Maintains the Morph transaction pool by revalidating MorphTx transactions. +/// +/// This task runs continuously and: +/// - Listens for new canonical blocks +/// - Re-validates MorphTx (0x7F) transactions in the pool +/// - Removes transactions that no longer have sufficient token balance +/// +pub async fn maintain_morph_pool(pool: Pool, client: Client) +where + Pool: TransactionPool + Clone, + Pool::Transaction: EthPoolTransaction, + Client: ChainSpecProvider + + StateProviderFactory + + CanonStateSubscriptions + + Clone + + 'static, +{ + let mut chain_events = client.canonical_state_stream(); + + tracing::info!(target: "morph_txpool::maintain", "Starting MorphTx maintenance task"); + + loop { + // Wait for the next canonical state change + let Some(event) = chain_events.next().await else { + tracing::debug!(target: "morph_txpool::maintain", "Chain event stream ended"); + break; + }; + + let new_tip = event.tip(); + let block_number = new_tip.number(); + let block_timestamp = new_tip.timestamp(); + + tracing::trace!( + target: "morph_txpool::maintain", + block_number, + "Processing new block for MorphTx validation" + ); + + // Get the hardfork at this block + let hardfork = client + .chain_spec() + .morph_hardfork_at(block_number, block_timestamp); + + // Collect all MorphTx transactions from the pool + let all_txs = pool.all_transactions(); + let morph_txs: Vec<_> = all_txs + .pending + .iter() + .chain(all_txs.queued.iter()) + .filter(|tx| tx.transaction.clone_into_consensus().is_morph_tx()) + .collect(); + + if morph_txs.is_empty() { + continue; + } + + // Get state provider for the new tip + let state_provider = match client.state_by_block_hash(new_tip.hash()) { + Ok(provider) => provider, + Err(err) => { + tracing::warn!( + target: "morph_txpool::maintain", + %err, + "Failed to get state provider for MorphTx revalidation" + ); + continue; + } + }; + + let mut db = StateProviderDatabase::new(state_provider); + + // Fetch L1 block info for fee calculation + let l1_block_info = match L1BlockInfo::try_fetch(&mut db, hardfork) { + Ok(info) => info, + Err(err) => { + tracing::warn!( + target: "morph_txpool::maintain", + ?err, + "Failed to fetch L1 block info for MorphTx revalidation" + ); + continue; + } + }; + + tracing::trace!( + target: "morph_txpool::maintain", + count = morph_txs.len(), + "Revalidating MorphTx transactions" + ); + + // Revalidate each MorphTx and collect invalid ones + let mut to_remove: HashSet = HashSet::new(); + + for pooled_tx in morph_txs { + let tx = &pooled_tx.transaction; + let consensus_tx = tx.clone_into_consensus(); + let sender = tx.sender(); + + // Get ETH balance for tx.value() check + let eth_balance = match db.basic(sender) { + Ok(Some(account)) => account.balance, + Ok(None) => U256::ZERO, + Err(err) => { + tracing::warn!( + target: "morph_txpool::maintain", + tx_hash = ?tx.hash(), + ?err, + "Failed to get account balance" + ); + continue; + } + }; + + // Calculate L1 data fee + let mut encoded = Vec::with_capacity(consensus_tx.encode_2718_len()); + consensus_tx.encode_2718(&mut encoded); + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(&encoded, hardfork); + + // Use shared validation logic (includes ETH balance >= tx.value() check) + let input = crate::MorphTxValidationInput { + consensus_tx: &consensus_tx, + sender, + eth_balance, + l1_data_fee, + hardfork, + }; + + if let Err(err) = crate::validate_morph_tx(&mut db, &input) { + tracing::debug!( + target: "morph_txpool::maintain", + tx_hash = ?tx.hash(), + ?err, + "Removing MorphTx: validation failed" + ); + to_remove.insert(*tx.hash()); + } + } + + // Remove invalid transactions + if !to_remove.is_empty() { + let count = to_remove.len(); + let hashes: Vec<_> = to_remove.into_iter().collect(); + pool.remove_transactions(hashes); + tracing::info!( + target: "morph_txpool::maintain", + count, + block_number, + "Removed invalid MorphTx transactions" + ); + } + } +} diff --git a/crates/txpool/src/morph_tx_validation.rs b/crates/txpool/src/morph_tx_validation.rs new file mode 100644 index 0000000..10fa069 --- /dev/null +++ b/crates/txpool/src/morph_tx_validation.rs @@ -0,0 +1,186 @@ +//! Shared MorphTx validation logic. +//! +//! This module provides common validation logic for MorphTx (0x7F) transactions +//! that is used by both the validator (for new transactions) and the maintenance +//! task (for revalidating existing transactions). + +use alloy_consensus::Transaction; +use alloy_evm::Database; +use alloy_primitives::{Address, U256}; +use morph_chainspec::hardfork::MorphHardfork; +use morph_primitives::MorphTxEnvelope; +use morph_revm::TokenFeeInfo; + +use crate::MorphTxError; + +/// High-level input for MorphTx validation. +/// +/// This encapsulates all the context needed to validate a MorphTx transaction. +#[derive(Debug, Clone)] +pub struct MorphTxValidationInput<'a> { + /// The consensus transaction + pub consensus_tx: &'a MorphTxEnvelope, + /// The sender's address + pub sender: Address, + /// The sender's ETH balance (for tx.value() check) + pub eth_balance: U256, + /// L1 data fee (pre-calculated) + pub l1_data_fee: U256, + /// Current hardfork + pub hardfork: MorphHardfork, +} + +/// Result of MorphTx validation. +#[derive(Debug)] +pub struct MorphTxValidationResult { + /// The token info fetched during validation + pub token_info: TokenFeeInfo, + /// The required token amount + pub required_token_amount: U256, + /// The amount that will be paid (min of fee_limit and required) + pub amount_to_pay: U256, +} + +/// Validates a MorphTx transaction's token-related fields. +/// +/// This is the main entry point for MorphTx validation. It: +/// 1. Validates ETH balance >= tx.value() (value is still paid in ETH) +/// 2. Validates token balance and fee_limit +/// +pub fn validate_morph_tx( + db: &mut DB, + input: &MorphTxValidationInput<'_>, +) -> Result { + // Check ETH balance >= tx.value() (value is still paid in ETH) + // Reference: geth tx_pool.go:1649 - costLimit.Cmp(tx.Value()) < 0 + let tx_value = input.consensus_tx.value(); + if tx_value > input.eth_balance { + return Err(MorphTxError::InsufficientEthForValue { + balance: input.eth_balance, + value: tx_value, + }); + } + + // Extract MorphTx fields + let (fee_token_id, fee_limit) = + extract_morph_tx_fields(input.consensus_tx).ok_or(MorphTxError::InvalidTokenId)?; + + // Token ID 0 is reserved for ETH + if fee_token_id == 0 { + return Err(MorphTxError::InvalidTokenId); + } + + // Fetch token info from L2TokenRegistry + let token_info = TokenFeeInfo::try_fetch(db, fee_token_id, input.sender, input.hardfork) + .map_err(|err| MorphTxError::TokenInfoFetchFailed { + token_id: fee_token_id, + message: format!("{err:?}"), + })? + .ok_or(MorphTxError::TokenNotFound { + token_id: fee_token_id, + })?; + + // Check token is active + if !token_info.is_active { + return Err(MorphTxError::TokenNotActive { + token_id: fee_token_id, + }); + } + + // Check price ratio is valid + if token_info.price_ratio.is_zero() { + return Err(MorphTxError::InvalidPriceRatio { + token_id: fee_token_id, + }); + } + + // Calculate gas fee in ETH + let gas_limit = U256::from(input.consensus_tx.gas_limit()); + let max_fee_per_gas = U256::from(input.consensus_tx.max_fee_per_gas()); + let gas_fee = gas_limit.saturating_mul(max_fee_per_gas); + + // Total ETH fee = gas_fee + l1_data_fee + let total_eth_fee = gas_fee.saturating_add(input.l1_data_fee); + + // Convert ETH fee to token amount + let required_token_amount = token_info.calculate_token_amount(total_eth_fee); + + // Check fee_limit >= required_token_amount + if fee_limit < required_token_amount { + return Err(MorphTxError::FeeLimitTooLow { + fee_limit, + required: required_token_amount, + }); + } + + // Check token balance >= required amount (use min of fee_limit and required) + let amount_to_pay = fee_limit.min(required_token_amount); + if token_info.balance < amount_to_pay { + return Err(MorphTxError::InsufficientTokenBalance { + token_id: fee_token_id, + token_address: token_info.token_address, + balance: token_info.balance, + required: amount_to_pay, + }); + } + + Ok(MorphTxValidationResult { + token_info, + required_token_amount, + amount_to_pay, + }) +} + +/// Helper function to extract MorphTx fields from a consensus transaction. +/// +/// Returns `None` if this is not a MorphTx or if required fields are missing. +pub fn extract_morph_tx_fields(tx: &MorphTxEnvelope) -> Option<(u16, U256)> { + let fee_token_id = tx.fee_token_id()?; + let fee_limit = tx.fee_limit()?; + Some((fee_token_id, fee_limit)) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + + #[test] + fn test_morph_tx_validation_input_construction() { + use alloy_consensus::{Signed, TxEip1559}; + use alloy_primitives::{B256, Signature, TxKind}; + + let sender = address!("1000000000000000000000000000000000000001"); + + // Create a dummy EIP-1559 transaction for testing + let tx = TxEip1559 { + chain_id: 2818, + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::ZERO, + input: Default::default(), + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + access_list: Default::default(), + }; + let envelope = MorphTxEnvelope::Eip1559(Signed::new_unchecked( + tx, + Signature::test_signature(), + B256::ZERO, + )); + + let input = MorphTxValidationInput { + consensus_tx: &envelope, + sender, + eth_balance: U256::from(1_000_000_000_000_000_000u128), // 1 ETH + l1_data_fee: U256::from(100_000), + hardfork: MorphHardfork::Viridian, + }; + + assert_eq!(input.sender, sender); + assert_eq!(input.hardfork, MorphHardfork::Viridian); + assert_eq!(input.eth_balance, U256::from(1_000_000_000_000_000_000u128)); + assert_eq!(input.l1_data_fee, U256::from(100_000)); + } +} diff --git a/crates/txpool/src/transaction.rs b/crates/txpool/src/transaction.rs index 329f134..435a019 100644 --- a/crates/txpool/src/transaction.rs +++ b/crates/txpool/src/transaction.rs @@ -54,9 +54,9 @@ impl MorphPooledTransaction { self.inner.transaction().queue_index() } - /// Returns true if this is an AltFee transaction. - pub fn is_alt_fee(&self) -> bool { - self.inner.transaction().tx_type() == MorphTxType::AltFee + /// Returns true if this is a Morph transaction. + pub fn is_morph_tx(&self) -> bool { + self.inner.transaction().tx_type() == MorphTxType::Morph } } diff --git a/crates/txpool/src/validator.rs b/crates/txpool/src/validator.rs index fdd424f..e51d499 100644 --- a/crates/txpool/src/validator.rs +++ b/crates/txpool/src/validator.rs @@ -5,9 +5,12 @@ //! - Rejection of EIP-4844 blob transactions //! - Rejection of L1 message transactions from the pool //! - L1 data fee validation +//! - MorphTx (0x7F) ERC20 token balance validation -use alloy_consensus::BlockHeader; +use crate::MorphTxError; +use alloy_consensus::{BlockHeader, Transaction}; use alloy_eips::Encodable2718; +use alloy_primitives::{Address, U256}; use morph_chainspec::hardfork::MorphHardforks; use morph_primitives::MorphTxEnvelope; use morph_revm::L1BlockInfo; @@ -75,6 +78,21 @@ impl MorphL1BlockInfo { /// - Rejects EIP-4844 blob transactions (not supported on L2) /// - Rejects L1 message transactions (only included by sequencer) /// - Optionally validates L1 data fee affordability +/// - Validates MorphTx (0x7F) ERC20 token balance and fee_limit +/// +/// # MorphTx Validation +/// +/// For MorphTx (type 0x7F), this validator performs additional checks: +/// 1. Token must be registered and active in L2TokenRegistry +/// 2. Fee limit must be sufficient for the calculated token cost +/// 3. Token balance must cover the fee +/// 4. ETH balance must cover the transaction value (value is still in ETH) +/// +/// # Balance Check Configuration +/// +/// When using MorphTx, the inner `EthTransactionValidator` should have balance +/// checking disabled via `disable_balance_check()`, since MorphTx users may have +/// zero ETH balance but sufficient ERC20 tokens for gas payment. #[derive(Debug)] pub struct MorphTransactionValidator { /// The type that performs the actual validation. @@ -213,6 +231,7 @@ where /// This behaves the same as [`EthTransactionValidator::validate_one`], but in addition: /// - Rejects EIP-4844 blob transactions /// - Rejects L1 message transactions + /// - Validates MorphTx (0x7F) ERC20 token balance and fee_limit /// - Ensures that the account has enough balance to cover the L1 gas cost (if enabled) pub fn validate_one( &self, @@ -235,18 +254,16 @@ where ); } + // Check if this is a MorphTx (0x7F) - need special handling for ERC20 gas payment + let is_morph_tx = is_morph_tx(&transaction); + let outcome = self.inner.validate_one(origin, transaction); if outcome.is_invalid() || outcome.is_error() { tracing::trace!(target: "morph_txpool", ?outcome, "tx pool validation failed"); return outcome; } - if !self.requires_l1_data_gas_fee() { - // No need to check L1 gas fee - return outcome; - } - - // Ensure that the account has enough balance to cover the L1 gas cost + // Ensure that the account has enough balance to cover fees if let TransactionValidationOutcome::Valid { balance, state_nonce, @@ -261,29 +278,43 @@ where .chain_spec() .morph_hardfork_at(self.block_number(), self.block_timestamp()); - // Encode the transaction for L1 fee calculation - let mut encoded = Vec::with_capacity(valid_tx.transaction().encoded_length()); - let tx = valid_tx.transaction().clone_into_consensus(); - tx.encode_2718(&mut encoded); - - // Calculate L1 data cost - let cost_addition = l1_block_info.calculate_tx_l1_cost(&encoded, hardfork); - - let cost = valid_tx.transaction().cost().saturating_add(cost_addition); - - // Check if the user has enough balance to cover L2 gas + L1 data fee - if cost > balance { - return TransactionValidationOutcome::Invalid( - valid_tx.into_transaction(), - InvalidTransactionError::InsufficientFunds( - GotExpected { - got: balance, - expected: cost, - } + // Calculate L1 data fee (always calculated for all transactions) + let consensus_tx = valid_tx.transaction().clone_into_consensus(); + let mut encoded = Vec::with_capacity(consensus_tx.encode_2718_len()); + consensus_tx.encode_2718(&mut encoded); + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(&encoded, hardfork); + + if is_morph_tx { + // MorphTx: validate ERC20 token balance + let sender = valid_tx.transaction().sender(); + if let Err(err) = self.validate_morph_tx_balance( + valid_tx.transaction(), + sender, + balance, + l1_data_fee, + hardfork, + ) { + return TransactionValidationOutcome::Invalid( + valid_tx.into_transaction(), + err.into(), + ); + } + } else if self.requires_l1_data_gas_fee() { + // Regular transaction: validate ETH balance covers cost + L1 fee + let cost = valid_tx.transaction().cost().saturating_add(l1_data_fee); + if cost > balance { + return TransactionValidationOutcome::Invalid( + valid_tx.into_transaction(), + InvalidTransactionError::InsufficientFunds( + GotExpected { + got: balance, + expected: cost, + } + .into(), + ) .into(), - ) - .into(), - ); + ); + } } return TransactionValidationOutcome::Valid { @@ -299,6 +330,63 @@ where outcome } + /// Validates MorphTx (0x7F) ERC20 token balance and fee_limit. + /// + /// This method performs the following checks (reference: go-ethereum tx_pool.go:727-791): + /// 1. Token ID must be non-zero (0 is reserved for ETH) + /// 2. Token must be registered in L2TokenRegistry + /// 3. Token must be active for gas payment + /// 4. Token price ratio must be valid (non-zero) + /// 5. fee_limit must be >= required token amount + /// 6. Token balance must be >= required token amount + /// 7. ETH balance must be >= transaction value (value is still in ETH) + fn validate_morph_tx_balance( + &self, + tx: &Tx, + sender: Address, + eth_balance: U256, + l1_data_fee: U256, + hardfork: morph_chainspec::hardfork::MorphHardfork, + ) -> Result<(), MorphTxError> { + let consensus_tx = tx.clone_into_consensus(); + + // Get state provider for token info lookup + let provider = self + .client() + .state_by_block_number_or_tag(self.block_number().into()) + .map_err(|err| MorphTxError::TokenInfoFetchFailed { + token_id: 0, // token_id not yet extracted + message: err.to_string(), + })?; + + let mut db = StateProviderDatabase::new(provider); + + // Use shared validation logic with unified API (includes ETH balance check) + let input = crate::MorphTxValidationInput { + consensus_tx: &consensus_tx, + sender, + eth_balance, + l1_data_fee, + hardfork, + }; + + let result = crate::validate_morph_tx(&mut db, &input)?; + + tracing::trace!( + target: "morph_txpool", + fee_token_id = ?consensus_tx.fee_token_id(), + fee_limit = ?consensus_tx.fee_limit(), + required_token_amount = ?result.required_token_amount, + token_balance = ?result.token_info.balance, + l1_data_fee = ?l1_data_fee, + eth_balance = ?eth_balance, + tx_value = ?consensus_tx.value(), + "MorphTx validation passed" + ); + + Ok(()) + } + /// Validates all given transactions. /// /// Returns all outcomes for the given transactions in the same order. @@ -354,9 +442,27 @@ where tx.clone_into_consensus().is_l1_msg() } +/// Helper function to check if a transaction is a MorphTx (0x7F). +fn is_morph_tx(tx: &Tx) -> bool +where + Tx: EthPoolTransaction, +{ + tx.clone_into_consensus().is_morph_tx() +} + #[cfg(test)] mod tests { use super::*; + use alloy_consensus::{Signed, TxEip1559, TxLegacy}; + use alloy_eips::eip2718::Encodable2718; + use alloy_primitives::{B256, Signature, TxKind, address}; + use morph_chainspec::MORPH_MAINNET; + use morph_primitives::TxL1Msg; + use reth_primitives_traits::Recovered; + use reth_provider::test_utils::MockEthProvider; + use reth_transaction_pool::{ + blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, + }; #[test] fn test_morph_l1_block_info_default() { @@ -374,4 +480,142 @@ mod tests { assert_eq!(info.timestamp(), 1234); assert_eq!(info.number(), 100); } + + #[test] + fn validate_l1_message_rejected() { + // Create validator with mock provider + let client = MockEthProvider::default().with_chain_spec(MORPH_MAINNET.clone()); + let eth_validator = EthTransactionValidatorBuilder::new(client) + .no_shanghai() + .no_cancun() + .build(InMemoryBlobStore::default()); + let validator = MorphTransactionValidator::new(eth_validator); + + let origin = TransactionOrigin::External; + let signer = address!("0000000000000000000000000000000000000001"); + + // Create L1 message transaction (type 0x7E) + let l1_msg_tx = TxL1Msg { + queue_index: 0, + gas_limit: 21_000, + to: address!("0000000000000000000000000000000000000002"), + value: U256::ZERO, + input: Default::default(), + sender: signer, + }; + let envelope = MorphTxEnvelope::L1Msg(alloy_consensus::Sealed::new_unchecked( + l1_msg_tx, + B256::ZERO, + )); + let recovered = Recovered::new_unchecked(envelope, signer); + let len = recovered.encode_2718_len(); + let pooled_tx = crate::MorphPooledTransaction::new(recovered, len); + + // Validate and check rejection + let outcome = validator.validate_one(origin, pooled_tx); + + let err = match outcome { + TransactionValidationOutcome::Invalid(_, err) => err, + _ => panic!("Expected invalid transaction for L1 message"), + }; + assert_eq!(err.to_string(), "transaction type not supported"); + } + + #[test] + fn validate_valid_eip1559_transaction() { + // Create validator with mock provider and disable balance check for simplicity + let client = MockEthProvider::default().with_chain_spec(MORPH_MAINNET.clone()); + let eth_validator = EthTransactionValidatorBuilder::new(client) + .no_shanghai() + .no_cancun() + .disable_balance_check() + .build(InMemoryBlobStore::default()); + let validator = + MorphTransactionValidator::new(eth_validator).require_l1_data_gas_fee(false); // Disable L1 fee check for simplicity + + let origin = TransactionOrigin::External; + let signer = address!("0000000000000000000000000000000000000001"); + + // Create valid EIP-1559 transaction + let tx = TxEip1559 { + chain_id: 2818, + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::ZERO, + input: Default::default(), + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + access_list: Default::default(), + }; + let signature = Signature::test_signature(); + let signed_tx = Signed::new_unchecked(tx, signature, B256::ZERO); + let envelope = MorphTxEnvelope::Eip1559(signed_tx); + let recovered = Recovered::new_unchecked(envelope, signer); + let len = recovered.encode_2718_len(); + let pooled_tx = crate::MorphPooledTransaction::new(recovered, len); + + // Validate and check acceptance + let outcome = validator.validate_one(origin, pooled_tx); + + match outcome { + TransactionValidationOutcome::Valid { .. } => { + // Success - transaction was accepted + } + TransactionValidationOutcome::Invalid(_, err) => { + panic!("Expected valid transaction, got invalid: {err}"); + } + TransactionValidationOutcome::Error(_, err) => { + panic!("Expected valid transaction, got error: {err:?}"); + } + } + } + + #[test] + fn validate_valid_legacy_transaction() { + // Create validator with mock provider and disable balance check for simplicity + let client = MockEthProvider::default().with_chain_spec(MORPH_MAINNET.clone()); + let eth_validator = EthTransactionValidatorBuilder::new(client) + .no_shanghai() + .no_cancun() + .disable_balance_check() + .build(InMemoryBlobStore::default()); + let validator = + MorphTransactionValidator::new(eth_validator).require_l1_data_gas_fee(false); // Disable L1 fee check for simplicity + + let origin = TransactionOrigin::External; + let signer = address!("0000000000000000000000000000000000000001"); + + // Create valid Legacy transaction + let tx = TxLegacy { + chain_id: Some(2818), + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::ZERO, + input: Default::default(), + gas_price: 2_000_000_000, + }; + let signature = Signature::test_signature(); + let signed_tx = Signed::new_unchecked(tx, signature, B256::ZERO); + let envelope = MorphTxEnvelope::Legacy(signed_tx); + let recovered = Recovered::new_unchecked(envelope, signer); + let len = recovered.encode_2718_len(); + let pooled_tx = crate::MorphPooledTransaction::new(recovered, len); + + // Validate and check acceptance + let outcome = validator.validate_one(origin, pooled_tx); + + match outcome { + TransactionValidationOutcome::Valid { .. } => { + // Success - transaction was accepted + } + TransactionValidationOutcome::Invalid(_, err) => { + panic!("Expected valid transaction, got invalid: {err}"); + } + TransactionValidationOutcome::Error(_, err) => { + panic!("Expected valid transaction, got error: {err:?}"); + } + } + } } From c001bbfaf0f2a787e3070f926aca303f56d6ba62 Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 28 Jan 2026 13:39:13 +0800 Subject: [PATCH 03/19] refactor: use auto impl --- crates/chainspec/src/hardfork.rs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/crates/chainspec/src/hardfork.rs b/crates/chainspec/src/hardfork.rs index b3da1e6..6eb1e34 100644 --- a/crates/chainspec/src/hardfork.rs +++ b/crates/chainspec/src/hardfork.rs @@ -98,6 +98,7 @@ impl MorphHardfork { } /// Trait for querying Morph-specific hardfork activations. +#[auto_impl::auto_impl(&, Arc)] pub trait MorphHardforks: EthereumHardforks { /// Retrieves activation condition for a Morph-specific hardfork fn morph_fork_activation(&self, fork: MorphHardfork) -> ForkCondition; @@ -162,25 +163,6 @@ pub trait MorphHardforks: EthereumHardforks { } } -// Blanket implementations for Arc and reference types -impl MorphHardforks for std::sync::Arc -where - T: MorphHardforks, -{ - fn morph_fork_activation(&self, fork: MorphHardfork) -> ForkCondition { - T::morph_fork_activation(self, fork) - } -} - -impl MorphHardforks for &T -where - T: MorphHardforks, -{ - fn morph_fork_activation(&self, fork: MorphHardfork) -> ForkCondition { - T::morph_fork_activation(*self, fork) - } -} - impl From for SpecId { fn from(value: MorphHardfork) -> Self { match value { From c0f0698ad9339211a3d63b0f7ef41f1539f16a65 Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 28 Jan 2026 13:41:04 +0800 Subject: [PATCH 04/19] docs: change validator comments --- crates/engine-api/src/validator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/engine-api/src/validator.rs b/crates/engine-api/src/validator.rs index 1ee25c0..df759ef 100644 --- a/crates/engine-api/src/validator.rs +++ b/crates/engine-api/src/validator.rs @@ -4,8 +4,8 @@ //! the MPTFork hardfork rules. //! //! **Important**: Morph skips state root validation before the MPTFork hardfork, -//! similar to how Scroll skips it before Euclid. Before MPTFork, Morph uses -//! ZK-trie, and state root verification happens in the ZK proof instead. +//! before MPTFork, Morph uses ZK-trie, and state root verification happens in the +//! ZK proof instead. use morph_chainspec::{MorphChainSpec, MorphHardforks}; use std::sync::Arc; From 3aefb05a1aa7cb75f3be64332e78cfb653120c3f Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 29 Jan 2026 10:23:27 +0800 Subject: [PATCH 05/19] refactor: Minimize the default dependencies of the primitives crate --- Cargo.lock | 36 +++- Cargo.toml | 7 +- crates/chainspec/Cargo.toml | 16 +- crates/consensus/Cargo.toml | 2 +- crates/evm/Cargo.toml | 2 +- crates/node/Cargo.toml | 40 ++++ crates/node/src/lib.rs | 7 + crates/node/src/rpc/error.rs | 93 +++++++++ crates/node/src/rpc/mod.rs | 12 ++ crates/node/src/rpc/morph_api.rs | 171 +++++++++++++++++ crates/node/src/rpc/types.rs | 177 ++++++++++++++++++ crates/primitives/Cargo.toml | 59 +++--- crates/primitives/src/header.rs | 6 +- crates/primitives/src/lib.rs | 14 +- crates/primitives/src/receipt/mod.rs | 3 + crates/primitives/src/transaction/envelope.rs | 3 + crates/txpool/Cargo.toml | 2 +- 17 files changed, 603 insertions(+), 47 deletions(-) create mode 100644 crates/node/Cargo.toml create mode 100644 crates/node/src/lib.rs create mode 100644 crates/node/src/rpc/error.rs create mode 100644 crates/node/src/rpc/mod.rs create mode 100644 crates/node/src/rpc/morph_api.rs create mode 100644 crates/node/src/rpc/types.rs diff --git a/Cargo.lock b/Cargo.lock index 6868ccb..96c4fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "k256", "rand 0.8.5", "serde", + "serde_with", "thiserror 2.0.17", ] @@ -772,7 +773,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -783,7 +784,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2084,7 +2085,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2300,7 +2301,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3956,6 +3957,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "morph-node" +version = "0.7.5" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "async-trait", + "jsonrpsee", + "morph-chainspec", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "serde", + "serde_json", + "thiserror 2.0.17", +] + [[package]] name = "morph-payload-builder" version = "0.7.5" @@ -4161,7 +4180,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4345,6 +4364,7 @@ dependencies = [ "arbitrary", "derive_more", "serde", + "serde_with", "thiserror 2.0.17", ] @@ -7064,7 +7084,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7749,7 +7769,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8629,7 +8649,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1cff19e..0337c7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/consensus", "crates/engine-api", "crates/evm", + "crates/node", "crates/payload/builder", "crates/payload/types", "crates/primitives", @@ -43,12 +44,10 @@ morph-chainspec = { path = "crates/chainspec", default-features = false } morph-consensus = { path = "crates/consensus", default-features = false } morph-engine-api = { path = "crates/engine-api", default-features = false } morph-evm = { path = "crates/evm", default-features = false } +morph-node = { path = "crates/node"} morph-payload-builder = { path = "crates/payload/builder", default-features = false } morph-payload-types = { path = "crates/payload/types", default-features = false } -morph-primitives = { path = "crates/primitives", default-features = false, features = [ - "serde", - "reth", -] } +morph-primitives = { path = "crates/primitives", default-features = false } morph-revm = { path = "crates/revm", default-features = false } morph-txpool = { path = "crates/txpool", default-features = false } diff --git a/crates/chainspec/Cargo.toml b/crates/chainspec/Cargo.toml index a7fccd9..c247aca 100644 --- a/crates/chainspec/Cargo.toml +++ b/crates/chainspec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morph-chainspec" -description = "TODO:" +description = "Morph L2 chainspec implementation" version.workspace = true edition.workspace = true @@ -33,11 +33,23 @@ serde.workspace = true serde_json.workspace = true [features] -default = ["serde"] +default = ["std"] +std = [ + "alloy-chains/std", + "alloy-consensus/std", + "alloy-eips/std", + "alloy-evm/std", + "alloy-genesis/std", + "alloy-primitives/std", + "morph-primitives/std", + "reth-chainspec/std", + "serde/std", +] serde = [ "alloy-consensus/serde", "alloy-eips/serde", "alloy-hardforks/serde", "alloy-primitives/serde", + "morph-primitives/serde", ] cli = ["dep:reth-cli", "dep:eyre"] diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index b913e5f..e108efa 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # Morph morph-chainspec.workspace = true -morph-primitives = { workspace = true, features = ["reth-codec"] } +morph-primitives = { workspace = true, features = ["reth-codec", "serde-bincode-compat"] } # Reth reth-consensus.workspace = true diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index e2754da..e2e2e55 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] morph-chainspec.workspace = true -morph-primitives.workspace = true +morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } morph-revm.workspace = true reth-chainspec = { workspace = true, optional = true } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml new file mode 100644 index 0000000..f6c0c11 --- /dev/null +++ b/crates/node/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "morph-node" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# Morph crates +morph-chainspec.workspace = true + +# Reth RPC +reth-rpc-eth-api.workspace = true +reth-rpc-eth-types.workspace = true + +# Alloy +alloy-primitives.workspace = true +alloy-consensus.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-serde.workspace = true + +# Async +async-trait.workspace = true + +# JSON-RPC +jsonrpsee.workspace = true + +# Serialization +serde.workspace = true +serde_json.workspace = true + +# Error handling +thiserror.workspace = true + +[features] +default = [] diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs new file mode 100644 index 0000000..394c3b8 --- /dev/null +++ b/crates/node/src/lib.rs @@ -0,0 +1,7 @@ +//! Morph node implementation +//! +//! This crate provides the Morph node types and RPC implementation. + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod rpc; diff --git a/crates/node/src/rpc/error.rs b/crates/node/src/rpc/error.rs new file mode 100644 index 0000000..aa2b3b3 --- /dev/null +++ b/crates/node/src/rpc/error.rs @@ -0,0 +1,93 @@ +//! Error types for Morph RPC + +use alloy_primitives::B256; +use reth_rpc_eth_types::EthApiError; +use thiserror::Error; + +/// Morph Eth API errors +#[derive(Debug, Error)] +pub enum MorphEthApiError { + /// Inner eth API error + #[error(transparent)] + Eth(#[from] EthApiError), + + /// Block not found + #[error("block not found")] + BlockNotFound, + + /// Transaction not found + #[error("transaction {0} not found")] + TransactionNotFound(B256), + + /// Skipped transaction not found + #[error("skipped transaction {0} not found")] + SkippedTransactionNotFound(B256), + + /// Invalid block number or hash + #[error("invalid block number or hash")] + InvalidBlockNumberOrHash, + + /// State not available for block + #[error("state not available for block")] + StateNotAvailable, + + /// Internal error + #[error("internal error: {0}")] + Internal(String), + + /// Database error + #[error("database error: {0}")] + Database(String), + + /// Provider error + #[error("provider error: {0}")] + Provider(String), +} + +impl From for jsonrpsee::types::ErrorObject<'static> { + fn from(err: MorphEthApiError) -> Self { + match err { + MorphEthApiError::Eth(e) => e.into(), + MorphEthApiError::BlockNotFound => { + jsonrpsee::types::ErrorObject::owned(-32001, "Block not found", None::<()>) + } + MorphEthApiError::TransactionNotFound(hash) => jsonrpsee::types::ErrorObject::owned( + -32002, + format!("Transaction {hash} not found"), + None::<()>, + ), + MorphEthApiError::SkippedTransactionNotFound(hash) => { + jsonrpsee::types::ErrorObject::owned( + -32003, + format!("Skipped transaction {hash} not found"), + None::<()>, + ) + } + MorphEthApiError::InvalidBlockNumberOrHash => jsonrpsee::types::ErrorObject::owned( + -32004, + "Invalid block number or hash", + None::<()>, + ), + MorphEthApiError::StateNotAvailable => jsonrpsee::types::ErrorObject::owned( + -32005, + "State not available for block", + None::<()>, + ), + MorphEthApiError::Internal(msg) => jsonrpsee::types::ErrorObject::owned( + -32603, + format!("Internal error: {msg}"), + None::<()>, + ), + MorphEthApiError::Database(msg) => jsonrpsee::types::ErrorObject::owned( + -32006, + format!("Database error: {msg}"), + None::<()>, + ), + MorphEthApiError::Provider(msg) => jsonrpsee::types::ErrorObject::owned( + -32007, + format!("Provider error: {msg}"), + None::<()>, + ), + } + } +} diff --git a/crates/node/src/rpc/mod.rs b/crates/node/src/rpc/mod.rs new file mode 100644 index 0000000..8f831f6 --- /dev/null +++ b/crates/node/src/rpc/mod.rs @@ -0,0 +1,12 @@ +//! Morph RPC implementation +//! +//! This module provides the Morph-specific RPC types and API implementations. +//! Following Tempo's pattern, RPC is implemented as a submodule of the node crate. + +pub mod error; +pub mod morph_api; +pub mod types; + +pub use error::MorphEthApiError; +pub use morph_api::{MorphApiServer, MorphRpc}; +pub use types::*; diff --git a/crates/node/src/rpc/morph_api.rs b/crates/node/src/rpc/morph_api.rs new file mode 100644 index 0000000..83f8399 --- /dev/null +++ b/crates/node/src/rpc/morph_api.rs @@ -0,0 +1,171 @@ +//! Morph RPC API implementation +//! +//! This module provides the `morph_` namespace JSON-RPC methods. + +use crate::rpc::{DiskAndHeaderRoot, SyncStatus}; +use alloy_primitives::{B256, BlockHash, U64}; +use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionRequest}; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +/// Morph-specific RPC methods under the `morph_` namespace +#[rpc(server, namespace = "morph")] +pub trait MorphApi { + /// Returns the block by hash with additional Morph-specific fields. + /// + /// This is similar to `eth_getBlockByHash` but includes: + /// - `withdrawTrieRoot`: The withdraw trie root from the L2 message queue + /// - `batchHash`: The batch hash if this is a batch point + /// - `nextL1MsgIndex`: The next L1 message index to be processed + #[method(name = "getBlockByHash")] + async fn get_block_by_hash( + &self, + hash: BlockHash, + full_transactions: bool, + ) -> RpcResult>; + + /// Returns the block by number with additional Morph-specific fields. + #[method(name = "getBlockByNumber")] + async fn get_block_by_number( + &self, + number: BlockNumberOrTag, + full_transactions: bool, + ) -> RpcResult>; + + /// Estimates the L1 data fee for a transaction. + /// + /// The L1 data fee is the cost of posting transaction data to L1. + /// This is separate from the L2 execution gas fee. + #[method(name = "estimateL1DataFee")] + async fn estimate_l1_data_fee( + &self, + tx: TransactionRequest, + block_number: Option, + ) -> RpcResult; + + /// Returns the total number of skipped transactions. + #[method(name = "getNumSkippedTransactions")] + async fn get_num_skipped_transactions(&self) -> RpcResult; + + /// Returns the hashes of skipped transactions in a range. + #[method(name = "getSkippedTransactionHashes")] + async fn get_skipped_transaction_hashes(&self, from: U64, to: U64) -> RpcResult>; + + /// Returns the current L1 sync height. + #[method(name = "getL1SyncHeight")] + async fn get_l1_sync_height(&self) -> RpcResult; + + /// Returns the latest relayed L1 message queue index. + #[method(name = "getLatestRelayedQueueIndex")] + async fn get_latest_relayed_queue_index(&self) -> RpcResult; + + /// Returns the current sync status. + #[method(name = "syncStatus")] + async fn sync_status(&self) -> RpcResult; + + /// Returns both the disk state root and header root for a block. + #[method(name = "diskRoot")] + async fn disk_root( + &self, + block_number: Option, + ) -> RpcResult; +} + +/// Implementation of the `morph_` namespace RPC methods. +/// +/// This is a placeholder implementation. The actual implementation will be +/// completed once the full node components are available. +#[derive(Debug, Clone, Default)] +pub struct MorphRpc { + // Provider and other dependencies will be added here +} + +impl MorphRpc { + /// Creates a new `MorphRpc` instance. + pub fn new() -> Self { + Self::default() + } +} + +#[async_trait::async_trait] +impl MorphApiServer for MorphRpc { + async fn get_block_by_hash( + &self, + _hash: BlockHash, + _full_transactions: bool, + ) -> RpcResult> { + // TODO: Implement with Morph-specific fields (withdrawTrieRoot, batchHash, nextL1MsgIndex) + Err(jsonrpsee::types::ErrorObject::owned( + -32601, + "method not implemented", + None::<()>, + )) + } + + async fn get_block_by_number( + &self, + _number: BlockNumberOrTag, + _full_transactions: bool, + ) -> RpcResult> { + // TODO: Implement with Morph-specific fields + Err(jsonrpsee::types::ErrorObject::owned( + -32601, + "method not implemented", + None::<()>, + )) + } + + async fn estimate_l1_data_fee( + &self, + _tx: TransactionRequest, + _block_number: Option, + ) -> RpcResult { + // TODO: Implement L1 data fee estimation using morph-revm + Err(jsonrpsee::types::ErrorObject::owned( + -32601, + "method not implemented", + None::<()>, + )) + } + + async fn get_num_skipped_transactions(&self) -> RpcResult { + // TODO: Query from database + Ok(U64::ZERO) + } + + async fn get_skipped_transaction_hashes(&self, _from: U64, _to: U64) -> RpcResult> { + // TODO: Query from database + Ok(vec![]) + } + + async fn get_l1_sync_height(&self) -> RpcResult { + // TODO: Query from sync state + Ok(U64::ZERO) + } + + async fn get_latest_relayed_queue_index(&self) -> RpcResult { + // TODO: Query from database + Ok(U64::ZERO) + } + + async fn sync_status(&self) -> RpcResult { + // TODO: Query actual sync status + Ok(SyncStatus { + current_l2_block: U64::ZERO, + highest_l2_block: U64::ZERO, + l1_sync_height: U64::ZERO, + latest_relayed_queue_index: U64::ZERO, + }) + } + + async fn disk_root( + &self, + _block_number: Option, + ) -> RpcResult { + // TODO: Implement disk root lookup + Err(jsonrpsee::types::ErrorObject::owned( + -32601, + "method not implemented", + None::<()>, + )) + } +} diff --git a/crates/node/src/rpc/types.rs b/crates/node/src/rpc/types.rs new file mode 100644 index 0000000..d948eee --- /dev/null +++ b/crates/node/src/rpc/types.rs @@ -0,0 +1,177 @@ +//! Morph-specific RPC types +//! +//! This module provides RPC representations for Morph transactions, receipts, and blocks. + +use alloy_primitives::{Address, B256, BlockHash, Bloom, Bytes, U64, U128, U256}; +use alloy_rpc_types_eth::{AccessList, Log}; +use alloy_serde::OtherFields; +use serde::{Deserialize, Serialize}; + +/// Morph RPC transaction representation +/// +/// Extends standard Ethereum transaction with: +/// - L1Message fields (sender, queueIndex) +/// - AltFee/MorphTx fields (feeTokenID, feeLimit) +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MorphRpcTransaction { + /// Transaction hash + pub hash: B256, + /// Transaction nonce + pub nonce: U64, + /// Block hash where this transaction was included + #[serde(skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + /// Block number where this transaction was included + #[serde(skip_serializing_if = "Option::is_none")] + pub block_number: Option, + /// Transaction index in the block + #[serde(skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, + /// Sender address + pub from: Address, + /// Recipient address (None for contract creation) + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option
, + /// Transferred value + pub value: U256, + /// Gas limit + pub gas: U256, + /// Gas price (for legacy and access list transactions) + #[serde(skip_serializing_if = "Option::is_none")] + pub gas_price: Option, + /// Maximum fee per gas (EIP-1559) + #[serde(skip_serializing_if = "Option::is_none")] + pub max_fee_per_gas: Option, + /// Maximum priority fee per gas (EIP-1559) + #[serde(skip_serializing_if = "Option::is_none")] + pub max_priority_fee_per_gas: Option, + /// Transaction input data + pub input: Bytes, + /// Transaction type (0x00-0x7f) + #[serde(rename = "type")] + pub tx_type: U64, + /// Access list (EIP-2930) + #[serde(skip_serializing_if = "Option::is_none")] + pub access_list: Option, + /// Chain ID + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + /// ECDSA signature v + pub v: U256, + /// ECDSA signature r + pub r: U256, + /// ECDSA signature s + pub s: U256, + /// EIP-2718 y parity + #[serde(skip_serializing_if = "Option::is_none")] + pub y_parity: Option, + + // Morph-specific fields for L1Message transactions (0x7E) + /// L1 message sender (only for L1Message type 0x7E) + #[serde(skip_serializing_if = "Option::is_none")] + pub sender: Option
, + /// L1 message queue index (only for L1Message type 0x7E) + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_index: Option, + + // Morph-specific fields for AltFee/MorphTx transactions (0x7F) + /// Token ID for fee payment (only for MorphTx type 0x7F) + #[serde(rename = "feeTokenID", skip_serializing_if = "Option::is_none")] + pub fee_token_id: Option, + /// Maximum token amount willing to pay for fees (only for MorphTx type 0x7F) + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, + + /// Additional fields for future extensibility + #[serde(flatten)] + pub other: OtherFields, +} + +/// Morph RPC transaction receipt +/// +/// Extends standard Ethereum receipt with L1 fee and token fee fields +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MorphRpcReceipt { + /// Transaction hash + pub transaction_hash: B256, + /// Transaction index in the block + pub transaction_index: U64, + /// Block hash + pub block_hash: BlockHash, + /// Block number + pub block_number: U256, + /// Sender address + pub from: Address, + /// Recipient address (None for contract creation) + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option
, + /// Cumulative gas used in the block + pub cumulative_gas_used: U256, + /// Gas used by this transaction + pub gas_used: U256, + /// Effective gas price paid + pub effective_gas_price: U128, + /// Contract address created (for contract creation) + #[serde(skip_serializing_if = "Option::is_none")] + pub contract_address: Option
, + /// Logs emitted + pub logs: Vec, + /// Logs bloom filter + pub logs_bloom: Bloom, + /// Transaction type + #[serde(rename = "type")] + pub tx_type: U64, + /// Status code (1 for success, 0 for failure) + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// State root (pre-Byzantium) + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + + // Morph-specific fields + /// L1 data fee paid (in wei) + #[serde(rename = "l1Fee")] + pub l1_fee: U256, + /// Fee rate used for token fee calculation + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_rate: Option, + /// Token scale factor + #[serde(skip_serializing_if = "Option::is_none")] + pub token_scale: Option, + /// Fee limit specified in the transaction + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, + /// Token ID used for fee payment + #[serde(rename = "feeTokenID", skip_serializing_if = "Option::is_none")] + pub fee_token_id: Option, + + /// Additional fields for future extensibility + #[serde(flatten)] + pub other: OtherFields, +} + +/// Sync status information +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SyncStatus { + /// Current L2 block number + pub current_l2_block: U64, + /// Highest known L2 block + pub highest_l2_block: U64, + /// Current L1 sync height + pub l1_sync_height: U64, + /// Latest relayed L1 message queue index + pub latest_relayed_queue_index: U64, +} + +/// Disk and header root response +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiskAndHeaderRoot { + /// The state root stored on disk + pub disk_root: B256, + /// The state root in the block header + pub header_root: B256, +} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 2180863..5ea7662 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -12,50 +12,65 @@ workspace = true [dependencies] # Reth -reth-db-api = { workspace = true, optional = true } -reth-ethereum-primitives = { workspace = true, optional = true } reth-primitives-traits.workspace = true reth-codecs = { workspace = true, optional = true } +reth-db-api = { workspace = true, optional = true } reth-zstd-compressors = { workspace = true, optional = true } +reth-ethereum-primitives = { workspace = true, optional = true } # Alloy alloy-consensus.workspace = true alloy-eips.workspace = true alloy-primitives.workspace = true alloy-rlp.workspace = true -alloy-serde = { workspace = true, optional = true } -# Utils -bytes.workspace = true -serde = { workspace = true, features = ["derive"], optional = true } +# Codec +bytes = { workspace = true, optional = true } modular-bitfield = { version = "0.11.2", optional = true } - +serde = { workspace = true, features = ["derive"], optional = true } +alloy-serde = { workspace = true, optional = true } [dev-dependencies] serde_json.workspace = true [features] -default = ["serde", "reth"] +default = ["std"] +std = [ + "serde?/std", + "alloy-consensus/std", + "alloy-eips/std", + "alloy-primitives/std", + "alloy-rlp/std", + "bytes?/std", + "reth-primitives-traits/std", + "reth-zstd-compressors?/std", + "reth-codecs?/std", +] serde = [ "dep:serde", "dep:alloy-serde", - "alloy-primitives/serde", - "alloy-eips/serde", "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-primitives/serde", "reth-primitives-traits/serde", - "reth-ethereum-primitives?/serde", + "reth-codecs?/serde", ] -reth = [ - "dep:reth-ethereum-primitives", +serde-bincode-compat = [ + "serde", + "alloy-consensus/serde-bincode-compat", + "alloy-eips/serde-bincode-compat", + "reth-primitives-traits/serde-bincode-compat", + "dep:reth-ethereum-primitives", + "reth-ethereum-primitives/serde", + "reth-ethereum-primitives/serde-bincode-compat", ] reth-codec = [ - "reth", - "serde", - "dep:reth-codecs", - "dep:reth-db-api", - "dep:modular-bitfield", - "dep:reth-zstd-compressors", - "reth-ethereum-primitives/reth-codec", - "reth-codecs/alloy", - "reth-primitives-traits/reth-codec", + "std", + "serde", + "dep:bytes", + "dep:reth-codecs", + "dep:reth-db-api", + "dep:modular-bitfield", + "dep:reth-zstd-compressors", + "reth-codecs/alloy", ] diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs index bb6af1e..51d2742 100644 --- a/crates/primitives/src/header.rs +++ b/crates/primitives/src/header.rs @@ -168,7 +168,9 @@ impl Sealable for MorphHeader { } } -#[cfg(feature = "reth")] +#[cfg(feature = "serde-bincode-compat")] +impl reth_primitives_traits::serde_bincode_compat::RlpBincode for MorphHeader {} + impl reth_primitives_traits::InMemorySize for MorphHeader { fn size(&self) -> usize { reth_primitives_traits::InMemorySize::size(&self.inner) @@ -177,10 +179,8 @@ impl reth_primitives_traits::InMemorySize for MorphHeader { } } -#[cfg(feature = "reth")] impl reth_primitives_traits::BlockHeader for MorphHeader {} -#[cfg(feature = "reth")] impl reth_primitives_traits::header::HeaderMut for MorphHeader { fn set_parent_hash(&mut self, hash: B256) { self.inner.set_parent_hash(hash); diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d40f384..ad3648a 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -26,22 +26,22 @@ //! //! [`MorphPrimitives`] implements reth's `NodePrimitives` trait, providing //! all the type bindings needed for a Morph node. -//! -//! Note: `NodePrimitives` implementation requires the `reth-codec` feature. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg), allow(unexpected_cfgs))] +#![cfg_attr(not(feature = "std"), no_std)] // Suppress unused_crate_dependencies warnings for dependencies used in submodules use alloy_consensus as _; use alloy_eips as _; use alloy_primitives as _; use alloy_rlp as _; +#[cfg(feature = "reth-codec")] use bytes as _; -#[cfg(feature = "reth")] -use reth_ethereum_primitives as _; #[cfg(feature = "reth-codec")] use reth_zstd_compressors as _; +#[cfg(feature = "serde-bincode-compat")] +use reth_ethereum_primitives as _; pub mod header; pub mod receipt; @@ -65,11 +65,15 @@ pub use transaction::{ }; /// A [`NodePrimitives`] implementation for Morph. +/// +/// This implementation is only available when the `serde-bincode-compat` feature is enabled. +/// For zkprover/sp1 or other use cases that only need basic primitive types without +/// NodePrimitives, use `default-features = false`. #[derive(Debug, Clone, Default, Eq, PartialEq)] #[non_exhaustive] pub struct MorphPrimitives; -#[cfg(feature = "reth-codec")] +#[cfg(feature = "serde-bincode-compat")] impl reth_primitives_traits::NodePrimitives for MorphPrimitives { type Block = Block; type BlockHeader = MorphHeader; diff --git a/crates/primitives/src/receipt/mod.rs b/crates/primitives/src/receipt/mod.rs index bad6ebf..fd9db53 100644 --- a/crates/primitives/src/receipt/mod.rs +++ b/crates/primitives/src/receipt/mod.rs @@ -412,6 +412,9 @@ impl InMemorySize for MorphReceipt { } } +#[cfg(feature = "serde-bincode-compat")] +impl reth_primitives_traits::serde_bincode_compat::RlpBincode for MorphReceipt {} + /// Calculates the receipt root for a header. /// /// This function computes the Merkle root of receipts using the standard encoding diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 759e5ad..d6d0aa2 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -204,6 +204,9 @@ impl alloy_consensus::transaction::SignerRecoverable for MorphTxEnvelope { impl reth_primitives_traits::SignedTransaction for MorphTxEnvelope {} +#[cfg(feature = "serde-bincode-compat")] +impl reth_primitives_traits::serde_bincode_compat::RlpBincode for MorphTxEnvelope {} + #[cfg(feature = "reth-codec")] mod codec { use crate::L1_TX_TYPE_ID; diff --git a/crates/txpool/Cargo.toml b/crates/txpool/Cargo.toml index 77e6e13..6467d9b 100644 --- a/crates/txpool/Cargo.toml +++ b/crates/txpool/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # Morph morph-chainspec.workspace = true -morph-primitives = { workspace = true, features = ["reth-codec"] } +morph-primitives = { workspace = true, features = ["reth-codec", "serde-bincode-compat"] } morph-revm.workspace = true # Reth From 08679020f251e96709b64f25fc0e4f6e70e8eb09 Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 29 Jan 2026 11:57:57 +0800 Subject: [PATCH 06/19] refactor: change the dependencies of the primitives crate --- crates/consensus/Cargo.toml | 2 +- crates/evm/Cargo.toml | 6 ++-- crates/evm/src/lib.rs | 4 +-- crates/payload/builder/Cargo.toml | 4 +-- crates/payload/types/Cargo.toml | 14 +++++++-- crates/payload/types/src/attributes.rs | 11 ++++--- .../payload/types/src/executable_l2_data.rs | 29 ++++++++++-------- crates/payload/types/src/lib.rs | 4 +-- crates/payload/types/src/params.rs | 13 ++++---- crates/payload/types/src/safe_l2_data.rs | 30 +++++++++++-------- crates/primitives/src/lib.rs | 12 +------- .../src/transaction/l1_transaction.rs | 5 ++++ .../src/transaction/morph_transaction.rs | 5 ++++ crates/txpool/Cargo.toml | 2 +- 14 files changed, 81 insertions(+), 60 deletions(-) diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index e108efa..880abd2 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # Morph morph-chainspec.workspace = true -morph-primitives = { workspace = true, features = ["reth-codec", "serde-bincode-compat"] } +morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } # Reth reth-consensus.workspace = true diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index e2e2e55..dfc9f0e 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morph-evm" -description = "TODO:" +description = "Morph EVM implementation" version.workspace = true edition.workspace = true @@ -38,8 +38,8 @@ alloy-genesis.workspace = true [features] default = [] -reth-codec = [ - "morph-primitives/reth-codec", +serde-bincode-compat = [ + "morph-primitives/serde-bincode-compat", "dep:reth-chainspec", ] rpc = ["dep:reth-rpc-eth-api", "morph-revm/rpc"] diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index f554657..bcca9ab 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -31,13 +31,13 @@ //! //! # Features //! -//! - `reth-codec`: Enable `ConfigureEvm` implementation for reth integration +//! - `serde-bincode-compat`: Enable `ConfigureEvm` implementation for reth integration //! - `engine`: Enable engine API types #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(feature = "reth-codec")] +#[cfg(feature = "serde-bincode-compat")] mod config; mod assemble; diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index 44d9e25..c4fb922 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -14,9 +14,9 @@ workspace = true [dependencies] # Morph morph-chainspec.workspace = true -morph-evm = { workspace = true, features = ["reth-codec"] } +morph-evm = { workspace = true, features = ["serde-bincode-compat"] } morph-payload-types.workspace = true -morph-primitives.workspace = true +morph-primitives = { workspace = true, features = ["serde-bincode-compat", "reth-codec"] } # Reth reth-basic-payload-builder.workspace = true diff --git a/crates/payload/types/Cargo.toml b/crates/payload/types/Cargo.toml index 7ef640a..d05a577 100644 --- a/crates/payload/types/Cargo.toml +++ b/crates/payload/types/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] # Morph -morph-primitives = { workspace = true, features = ["serde", "reth-codec"] } +morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } # Reth reth-payload-builder.workspace = true @@ -29,7 +29,7 @@ alloy-rpc-types-engine = { workspace = true, features = ["serde"] } alloy-serde.workspace = true # Utils -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"], optional = true } sha2.workspace = true [dev-dependencies] @@ -38,4 +38,12 @@ rand.workspace = true alloy-primitives = { workspace = true, features = ["rand"] } [features] -default = [] +default = ["serde"] +serde = [ + "morph-primitives/serde", + "alloy-consensus/serde", + "alloy-eips/serde", + "alloy-primitives/serde", + "alloy-rpc-types-engine/serde", + "dep:serde", +] diff --git a/crates/payload/types/src/attributes.rs b/crates/payload/types/src/attributes.rs index 2871227..674438c 100644 --- a/crates/payload/types/src/attributes.rs +++ b/crates/payload/types/src/attributes.rs @@ -8,15 +8,15 @@ use morph_primitives::MorphTxEnvelope; use reth_payload_builder::EthPayloadBuilderAttributes; use reth_payload_primitives::PayloadBuilderAttributes; use reth_primitives_traits::{Recovered, SignerRecoverable, WithEncoded}; -use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; /// Morph-specific payload attributes for Engine API. /// /// This extends the standard Ethereum [`PayloadAttributes`] with L2-specific fields /// for forced transaction inclusion (L1 messages). -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct MorphPayloadAttributes { /// Standard Ethereum payload attributes. #[serde(flatten)] @@ -26,7 +26,10 @@ pub struct MorphPayloadAttributes { /// /// This includes L1 messages that must be processed in order. /// These transactions are not in the mempool and must be explicitly provided. - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] pub transactions: Option>, } diff --git a/crates/payload/types/src/executable_l2_data.rs b/crates/payload/types/src/executable_l2_data.rs index 9530df1..4df7cce 100644 --- a/crates/payload/types/src/executable_l2_data.rs +++ b/crates/payload/types/src/executable_l2_data.rs @@ -1,7 +1,6 @@ //! ExecutableL2Data type definition. use alloy_primitives::{Address, B256, Bytes}; -use serde::{Deserialize, Serialize}; /// L2 block data used for AssembleL2Block/ValidateL2Block/NewL2Block. /// @@ -13,8 +12,9 @@ use serde::{Deserialize, Serialize}; /// 1. **BLS message fields**: Fields that affect state calculation and need BLS signing /// 2. **Execution results**: Fields computed after block execution /// 3. **Metadata**: Additional L2-specific fields -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct ExecutableL2Data { // === BLS message fields (need to be signed) === /// Parent block hash. @@ -24,27 +24,30 @@ pub struct ExecutableL2Data { pub miner: Address, /// Block number. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub number: u64, /// Gas limit for this block. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub gas_limit: u64, /// Base fee per gas (EIP-1559). - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "alloy_serde::quantity::opt" + #[cfg_attr( + feature = "serde", + serde( + default, + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + ) )] pub base_fee_per_gas: Option, /// Block timestamp. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub timestamp: u64, /// RLP-encoded transactions. - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub transactions: Vec, // === Execution results === @@ -52,7 +55,7 @@ pub struct ExecutableL2Data { pub state_root: B256, /// Gas used by all transactions. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub gas_used: u64, /// Receipts root. @@ -66,7 +69,7 @@ pub struct ExecutableL2Data { // === Metadata === /// Next L1 message queue index after this block. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub next_l1_message_index: u64, /// Cached block hash. diff --git a/crates/payload/types/src/lib.rs b/crates/payload/types/src/lib.rs index bcbaa0a..347349d 100644 --- a/crates/payload/types/src/lib.rs +++ b/crates/payload/types/src/lib.rs @@ -24,7 +24,6 @@ use alloy_primitives::{B256, Bytes}; use morph_primitives::Block; use reth_payload_primitives::{BuiltPayload, ExecutionPayload, PayloadTypes}; use reth_primitives_traits::{NodePrimitives, SealedBlock}; -use serde::{Deserialize, Serialize}; use std::sync::Arc; // Re-export main types @@ -47,7 +46,8 @@ pub use safe_l2_data::SafeL2Data; pub struct MorphPayloadTypes; /// Execution data for Morph node. Simply wraps a sealed block. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MorphExecutionData { /// The built block. pub block: Arc>, diff --git a/crates/payload/types/src/params.rs b/crates/payload/types/src/params.rs index 470c1f8..0dab0a3 100644 --- a/crates/payload/types/src/params.rs +++ b/crates/payload/types/src/params.rs @@ -1,21 +1,21 @@ //! Request/response types for L2 Engine API methods. use alloy_primitives::Bytes; -use serde::{Deserialize, Serialize}; /// Parameters for engine_assembleL2Block. /// /// This struct contains the input parameters for building a new L2 block. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct AssembleL2BlockParams { /// Block number to build. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub number: u64, /// Transactions to include in the block. /// These are RLP-encoded transaction bytes. - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub transactions: Vec, } @@ -41,7 +41,8 @@ impl AssembleL2BlockParams { /// /// This is used by methods like engine_validateL2Block that return /// a simple success/failure status. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GenericResponse { /// Whether the operation was successful. pub success: bool, diff --git a/crates/payload/types/src/safe_l2_data.rs b/crates/payload/types/src/safe_l2_data.rs index 24e66da..1c8df49 100644 --- a/crates/payload/types/src/safe_l2_data.rs +++ b/crates/payload/types/src/safe_l2_data.rs @@ -3,7 +3,6 @@ //! This type is used for NewSafeL2Block in the derivation pipeline. use alloy_primitives::{B256, Bytes}; -use serde::{Deserialize, Serialize}; /// Safe L2 block data, used for NewSafeL2Block (derivation). /// @@ -13,35 +12,42 @@ use serde::{Deserialize, Serialize}; /// during execution rather than provided upfront. /// /// [`ExecutableL2Data`]: super::ExecutableL2Data -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct SafeL2Data { /// Block number. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub number: u64, /// Gas limit. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub gas_limit: u64, /// Base fee per gas (EIP-1559). - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "alloy_serde::quantity::opt" + #[cfg_attr( + feature = "serde", + serde( + default, + skip_serializing_if = "Option::is_none", + with = "alloy_serde::quantity::opt" + ) )] pub base_fee_per_gas: Option, /// Block timestamp. - #[serde(with = "alloy_serde::quantity")] + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub timestamp: u64, /// RLP-encoded transactions. - #[serde(default)] + #[cfg_attr(feature = "serde", serde(default))] pub transactions: Vec, /// Optional batch hash for batch association. - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Option::is_none") + )] pub batch_hash: Option, } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index ad3648a..3236857 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -31,15 +31,7 @@ #![cfg_attr(docsrs, feature(doc_cfg), allow(unexpected_cfgs))] #![cfg_attr(not(feature = "std"), no_std)] -// Suppress unused_crate_dependencies warnings for dependencies used in submodules -use alloy_consensus as _; -use alloy_eips as _; -use alloy_primitives as _; -use alloy_rlp as _; -#[cfg(feature = "reth-codec")] -use bytes as _; -#[cfg(feature = "reth-codec")] -use reth_zstd_compressors as _; + #[cfg(feature = "serde-bincode-compat")] use reth_ethereum_primitives as _; @@ -67,8 +59,6 @@ pub use transaction::{ /// A [`NodePrimitives`] implementation for Morph. /// /// This implementation is only available when the `serde-bincode-compat` feature is enabled. -/// For zkprover/sp1 or other use cases that only need basic primitive types without -/// NodePrimitives, use `default-features = false`. #[derive(Debug, Clone, Default, Eq, PartialEq)] #[non_exhaustive] pub struct MorphPrimitives; diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index 4df1f90..0205813 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -17,6 +17,11 @@ use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, k use alloy_rlp::{BufMut, Decodable, Encodable, Header}; use core::mem; +#[cfg(not(feature = "std"))] +extern crate alloc; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + /// L1 Message Transaction type ID (0x7E). pub const L1_TX_TYPE_ID: u8 = 0x7E; diff --git a/crates/primitives/src/transaction/morph_transaction.rs b/crates/primitives/src/transaction/morph_transaction.rs index 227c861..5298c43 100644 --- a/crates/primitives/src/transaction/morph_transaction.rs +++ b/crates/primitives/src/transaction/morph_transaction.rs @@ -16,6 +16,11 @@ use alloy_primitives::{B256, Bytes, ChainId, Signature, TxKind, U256, keccak256} use alloy_rlp::{BufMut, Decodable, Encodable, Header}; use core::mem; +#[cfg(not(feature = "std"))] +extern crate alloc; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + /// Morph Transaction type ID (0x7F). pub const MORPH_TX_TYPE_ID: u8 = 0x7F; diff --git a/crates/txpool/Cargo.toml b/crates/txpool/Cargo.toml index 6467d9b..0ebfd7a 100644 --- a/crates/txpool/Cargo.toml +++ b/crates/txpool/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] # Morph morph-chainspec.workspace = true -morph-primitives = { workspace = true, features = ["reth-codec", "serde-bincode-compat"] } +morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } morph-revm.workspace = true # Reth From 7a7d95a2e89b275dfb5b11d1e02417ee6f13cb59 Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 29 Jan 2026 13:34:57 +0800 Subject: [PATCH 07/19] refactor: change dep --- crates/evm/Cargo.toml | 3 +-- crates/evm/src/lib.rs | 1 - crates/payload/builder/Cargo.toml | 2 +- crates/payload/types/Cargo.toml | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index dfc9f0e..ce6353f 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -16,7 +16,7 @@ morph-chainspec.workspace = true morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } morph-revm.workspace = true -reth-chainspec = { workspace = true, optional = true } +reth-chainspec.workspace = true reth-evm.workspace = true reth-evm-ethereum.workspace = true reth-revm.workspace = true @@ -40,6 +40,5 @@ alloy-genesis.workspace = true default = [] serde-bincode-compat = [ "morph-primitives/serde-bincode-compat", - "dep:reth-chainspec", ] rpc = ["dep:reth-rpc-eth-api", "morph-revm/rpc"] diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index bcca9ab..ae73572 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -37,7 +37,6 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(feature = "serde-bincode-compat")] mod config; mod assemble; diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index c4fb922..8249bb5 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -15,7 +15,7 @@ workspace = true # Morph morph-chainspec.workspace = true morph-evm = { workspace = true, features = ["serde-bincode-compat"] } -morph-payload-types.workspace = true +morph-payload-types = { workspace = true, features = ["serde"] } morph-primitives = { workspace = true, features = ["serde-bincode-compat", "reth-codec"] } # Reth diff --git a/crates/payload/types/Cargo.toml b/crates/payload/types/Cargo.toml index d05a577..9a0de4d 100644 --- a/crates/payload/types/Cargo.toml +++ b/crates/payload/types/Cargo.toml @@ -38,7 +38,7 @@ rand.workspace = true alloy-primitives = { workspace = true, features = ["rand"] } [features] -default = ["serde"] +default = [] serde = [ "morph-primitives/serde", "alloy-consensus/serde", From 4f8ef722b566f5c539287c4473f92c0add4bd84c Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 29 Jan 2026 14:03:45 +0800 Subject: [PATCH 08/19] refactor: change dep --- crates/chainspec/Cargo.toml | 14 +------------ crates/payload/builder/Cargo.toml | 2 +- crates/payload/types/Cargo.toml | 10 +-------- crates/primitives/Cargo.toml | 21 ++++++------------- crates/primitives/src/lib.rs | 2 -- .../src/transaction/l1_transaction.rs | 5 ----- .../src/transaction/morph_transaction.rs | 5 ----- 7 files changed, 9 insertions(+), 50 deletions(-) diff --git a/crates/chainspec/Cargo.toml b/crates/chainspec/Cargo.toml index c247aca..460858c 100644 --- a/crates/chainspec/Cargo.toml +++ b/crates/chainspec/Cargo.toml @@ -33,23 +33,11 @@ serde.workspace = true serde_json.workspace = true [features] -default = ["std"] -std = [ - "alloy-chains/std", - "alloy-consensus/std", - "alloy-eips/std", - "alloy-evm/std", - "alloy-genesis/std", - "alloy-primitives/std", - "morph-primitives/std", - "reth-chainspec/std", - "serde/std", -] +default = ["serde"] serde = [ "alloy-consensus/serde", "alloy-eips/serde", "alloy-hardforks/serde", "alloy-primitives/serde", - "morph-primitives/serde", ] cli = ["dep:reth-cli", "dep:eyre"] diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index 8249bb5..c4fb922 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -15,7 +15,7 @@ workspace = true # Morph morph-chainspec.workspace = true morph-evm = { workspace = true, features = ["serde-bincode-compat"] } -morph-payload-types = { workspace = true, features = ["serde"] } +morph-payload-types.workspace = true morph-primitives = { workspace = true, features = ["serde-bincode-compat", "reth-codec"] } # Reth diff --git a/crates/payload/types/Cargo.toml b/crates/payload/types/Cargo.toml index 9a0de4d..5ce3355 100644 --- a/crates/payload/types/Cargo.toml +++ b/crates/payload/types/Cargo.toml @@ -38,12 +38,4 @@ rand.workspace = true alloy-primitives = { workspace = true, features = ["rand"] } [features] -default = [] -serde = [ - "morph-primitives/serde", - "alloy-consensus/serde", - "alloy-eips/serde", - "alloy-primitives/serde", - "alloy-rpc-types-engine/serde", - "dep:serde", -] +default = ["serde"] diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 5ea7662..9aa0b74 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -34,18 +34,7 @@ alloy-serde = { workspace = true, optional = true } serde_json.workspace = true [features] -default = ["std"] -std = [ - "serde?/std", - "alloy-consensus/std", - "alloy-eips/std", - "alloy-primitives/std", - "alloy-rlp/std", - "bytes?/std", - "reth-primitives-traits/std", - "reth-zstd-compressors?/std", - "reth-codecs?/std", -] +default = ["serde"] serde = [ "dep:serde", "dep:alloy-serde", @@ -53,8 +42,9 @@ serde = [ "alloy-eips/serde", "alloy-primitives/serde", "reth-primitives-traits/serde", - "reth-codecs?/serde", + "reth-ethereum-primitives?/serde", ] + serde-bincode-compat = [ "serde", "alloy-consensus/serde-bincode-compat", @@ -65,12 +55,13 @@ serde-bincode-compat = [ "reth-ethereum-primitives/serde-bincode-compat", ] reth-codec = [ - "std", "serde", - "dep:bytes", "dep:reth-codecs", "dep:reth-db-api", "dep:modular-bitfield", "dep:reth-zstd-compressors", + "reth-ethereum-primitives/reth-codec", "reth-codecs/alloy", + "dep:bytes", + "reth-primitives-traits/reth-codec", ] diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 3236857..0161bc8 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -29,8 +29,6 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg), allow(unexpected_cfgs))] -#![cfg_attr(not(feature = "std"), no_std)] - #[cfg(feature = "serde-bincode-compat")] use reth_ethereum_primitives as _; diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index 0205813..4df1f90 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -17,11 +17,6 @@ use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, k use alloy_rlp::{BufMut, Decodable, Encodable, Header}; use core::mem; -#[cfg(not(feature = "std"))] -extern crate alloc; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; - /// L1 Message Transaction type ID (0x7E). pub const L1_TX_TYPE_ID: u8 = 0x7E; diff --git a/crates/primitives/src/transaction/morph_transaction.rs b/crates/primitives/src/transaction/morph_transaction.rs index 5298c43..227c861 100644 --- a/crates/primitives/src/transaction/morph_transaction.rs +++ b/crates/primitives/src/transaction/morph_transaction.rs @@ -16,11 +16,6 @@ use alloy_primitives::{B256, Bytes, ChainId, Signature, TxKind, U256, keccak256} use alloy_rlp::{BufMut, Decodable, Encodable, Header}; use core::mem; -#[cfg(not(feature = "std"))] -extern crate alloc; -#[cfg(not(feature = "std"))] -use alloc::vec::Vec; - /// Morph Transaction type ID (0x7F). pub const MORPH_TX_TYPE_ID: u8 = 0x7F; From bfbb3d0da7ae6f689a7f0b9e0aff23aaedd3fa43 Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 3 Feb 2026 09:53:44 +0800 Subject: [PATCH 09/19] refactor: morph rpc refactor --- Cargo.lock | 2125 +++++++++++++++++++-- Cargo.toml | 3 + crates/node/Cargo.toml | 26 +- crates/node/src/lib.rs | 2 +- crates/node/src/rpc/mod.rs | 12 - crates/node/src/rpc/morph_api.rs | 171 -- crates/node/src/rpc/types.rs | 177 -- crates/primitives/Cargo.toml | 1 + crates/primitives/src/lib.rs | 2 +- crates/primitives/src/receipt/envelope.rs | 302 +++ crates/primitives/src/receipt/mod.rs | 16 +- crates/primitives/src/receipt/receipt.rs | 22 +- crates/rpc/Cargo.toml | 58 + crates/{node/src/rpc => rpc/src}/error.rs | 61 +- crates/rpc/src/eth/api.rs | 308 +++ crates/rpc/src/eth/call.rs | 232 +++ crates/rpc/src/eth/mod.rs | 94 + crates/rpc/src/eth/receipt.rs | 135 ++ crates/rpc/src/eth/transaction.rs | 258 +++ crates/rpc/src/lib.rs | 11 + crates/rpc/src/types/mod.rs | 9 + crates/rpc/src/types/receipt.rs | 98 + crates/rpc/src/types/request.rs | 66 + crates/rpc/src/types/transaction.rs | 154 ++ 24 files changed, 3799 insertions(+), 544 deletions(-) delete mode 100644 crates/node/src/rpc/mod.rs delete mode 100644 crates/node/src/rpc/morph_api.rs delete mode 100644 crates/node/src/rpc/types.rs create mode 100644 crates/primitives/src/receipt/envelope.rs create mode 100644 crates/rpc/Cargo.toml rename crates/{node/src/rpc => rpc/src}/error.rs (64%) create mode 100644 crates/rpc/src/eth/api.rs create mode 100644 crates/rpc/src/eth/call.rs create mode 100644 crates/rpc/src/eth/mod.rs create mode 100644 crates/rpc/src/eth/receipt.rs create mode 100644 crates/rpc/src/eth/transaction.rs create mode 100644 crates/rpc/src/lib.rs create mode 100644 crates/rpc/src/types/mod.rs create mode 100644 crates/rpc/src/types/receipt.rs create mode 100644 crates/rpc/src/types/request.rs create mode 100644 crates/rpc/src/types/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 96c4fef..eb56fa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -59,6 +65,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -184,6 +205,18 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "alloy-eip7928" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926b2c0d34e641cf8b17bf54ce50fda16715b9f68ad878fa6128bae410c6f890" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + [[package]] name = "alloy-eips" version = "1.1.3" @@ -370,26 +403,56 @@ dependencies = [ "alloy-network", "alloy-network-primitives", "alloy-primitives", + "alloy-pubsub", "alloy-rpc-client", + "alloy-rpc-types-debug", "alloy-rpc-types-eth", + "alloy-rpc-types-trace", "alloy-signer", "alloy-sol-types", "alloy-transport", + "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "async-stream", "async-trait", "auto_impl", - "dashmap", + "dashmap 6.1.0", "either", "futures", "futures-utils-wasm", "lru 0.13.0", "parking_lot", "pin-project", + "reqwest", "serde", "serde_json", "thiserror 2.0.17", "tokio", "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-pubsub" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd4c64eb250a18101d22ae622357c6b505e158e9165d4c7974d59082a600c5e" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "auto_impl", + "bimap", + "futures", + "parking_lot", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", "wasmtimer", ] @@ -423,8 +486,11 @@ checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" dependencies = [ "alloy-json-rpc", "alloy-primitives", + "alloy-pubsub", "alloy-transport", "alloy-transport-http", + "alloy-transport-ipc", + "alloy-transport-ws", "futures", "pin-project", "reqwest", @@ -463,6 +529,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alloy-rpc-types-anvil" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ce4c24e416bd0f17fceeb2f26cd8668df08fe19e1dc02f9d41c3b8ed1e93e0" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-any" version = "1.1.3" @@ -474,6 +552,38 @@ dependencies = [ "alloy-serde", ] +[[package]] +name = "alloy-rpc-types-beacon" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16633087e23d8d75161c3a59aa183203637b817a5a8d2f662f612ccb6d129af0" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "derive_more", + "ethereum_ssz", + "ethereum_ssz_derive", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", + "tree_hash", + "tree_hash_derive", +] + +[[package]] +name = "alloy-rpc-types-debug" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925ff0f48c2169c050f0ae7a82769bdf3f45723d6742ebb6a5efb4ed2f491b26" +dependencies = [ + "alloy-primitives", + "derive_more", + "serde", + "serde_with", +] + [[package]] name = "alloy-rpc-types-engine" version = "1.1.3" @@ -545,6 +655,18 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "alloy-rpc-types-txpool" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb5a795264a02222f9534435b8f40dcbd88de8e9d586647884aae24f389ebf2" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-serde" version = "1.1.3" @@ -583,9 +705,12 @@ dependencies = [ "alloy-primitives", "alloy-signer", "async-trait", + "coins-bip32", + "coins-bip39", "k256", "rand 0.8.5", "thiserror 2.0.17", + "zeroize", ] [[package]] @@ -666,7 +791,7 @@ checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" dependencies = [ "alloy-json-rpc", "auto_impl", - "base64", + "base64 0.22.1", "derive_more", "futures", "futures-utils-wasm", @@ -696,6 +821,44 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-transport-ipc" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8db249779ebc20dc265920c7e706ed0d31dbde8627818d1cbde60919b875bb0" +dependencies = [ + "alloy-json-rpc", + "alloy-pubsub", + "alloy-transport", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad2344a12398d7105e3722c9b7a7044ea837128e11d453604dec6e3731a86e2" +dependencies = [ + "alloy-pubsub", + "alloy-transport", + "futures", + "http", + "rustls", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + [[package]] name = "alloy-trie" version = "0.9.1" @@ -1127,6 +1290,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" +[[package]] +name = "async-compression" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1160,6 +1335,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version 0.4.1", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1221,6 +1407,12 @@ dependencies = [ "match-lookup", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -1233,6 +1425,18 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bincode" version = "1.3.3" @@ -1260,6 +1464,24 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.111", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1372,12 +1594,43 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "boyer-moore-magiclen" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7441b4796eb8a7107d4cd99d829810be75f5573e1081c37faa0e8094169ea0d6" +dependencies = [ + "debug-helper", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ + "sha2", "tinyvec", ] @@ -1393,6 +1646,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.24.0" @@ -1448,6 +1707,19 @@ dependencies = [ "serde", ] +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.27", + "serde", + "serde_json", +] + [[package]] name = "cargo_metadata" version = "0.19.2" @@ -1575,6 +1847,57 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "coins-bip32" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2073678591747aed4000dd468b97b14d7007f7936851d3f2f01846899f5ebf08" +dependencies = [ + "bs58", + "coins-core", + "digest 0.10.7", + "hmac", + "k256", + "serde", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "coins-bip39" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b169b26623ff17e9db37a539fe4f15342080df39f129ef7631df7683d6d9d4" +dependencies = [ + "bitvec", + "coins-bip32", + "hmac", + "once_cell", + "pbkdf2", + "rand 0.8.5", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "coins-core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" +dependencies = [ + "base64 0.21.7", + "bech32", + "bs58", + "const-hex", + "digest 0.10.7", + "generic-array", + "ripemd", + "serde", + "sha2", + "sha3", + "thiserror 1.0.69", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -1591,6 +1914,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "concat-kdf" version = "0.1.0" @@ -1702,6 +2045,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1880,12 +2232,11 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "crossbeam-utils", "hashbrown 0.14.5", "lock_api", "once_cell", @@ -1893,8 +2244,22 @@ dependencies = [ ] [[package]] -name = "data-encoding" -version = "2.9.0" +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" @@ -1918,6 +2283,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + [[package]] name = "delay_map" version = "0.4.1" @@ -2036,6 +2407,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -2143,6 +2520,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "dunce" version = "1.0.5" @@ -2243,7 +2626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" dependencies = [ "alloy-rlp", - "base64", + "base64 0.22.1", "bytes", "ed25519-dalek", "hex", @@ -2304,6 +2687,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "ethereum_hashing" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c853bd72c9e5787f8aafc3df2907c2ed03cff3150c3acd94e2e53a98ab70a8ab" +dependencies = [ + "cpufeatures", + "ring", + "sha2", +] + [[package]] name = "ethereum_serde_utils" version = "0.8.0" @@ -2382,6 +2785,16 @@ dependencies = [ "bytes", ] +[[package]] +name = "fdlimit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182f7dbc2ef73d9ef67351c5fbbea084729c48362d3ce9dd44c28e32e277fe5" +dependencies = [ + "libc", + "thiserror 1.0.69", +] + [[package]] name = "ff" version = "0.13.1" @@ -2410,6 +2823,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2530,7 +2953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ "gloo-timers", - "send_wrapper", + "send_wrapper 0.4.0", ] [[package]] @@ -2768,6 +3191,16 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" @@ -2894,6 +3327,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -2961,6 +3400,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 1.0.5", ] [[package]] @@ -2982,7 +3422,7 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -3241,6 +3681,21 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -3374,7 +3829,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" dependencies = [ - "base64", + "base64 0.22.1", "futures-channel", "futures-util", "gloo-net", @@ -3427,7 +3882,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" dependencies = [ - "base64", + "base64 0.22.1", "http-body", "hyper", "hyper-rustls", @@ -3528,7 +3983,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -3650,6 +4105,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "libproc" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" +dependencies = [ + "bindgen 0.72.1", + "errno", + "libc", +] + [[package]] name = "libredox" version = "0.1.11" @@ -3688,6 +4154,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -3746,6 +4218,15 @@ version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -3814,12 +4295,99 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.1", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", +] + +[[package]] +name = "metrics-process" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f615e08e049bd14a44c4425415782efb9bcd479fc1e19ddeb971509074c060d0" +dependencies = [ + "libc", + "libproc", + "mach2", + "metrics", + "once_cell", + "procfs 0.18.0", + "rlimit", + "windows 0.62.2", +] + +[[package]] +name = "metrics-util" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "metrics", + "quanta", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -3961,18 +4529,7 @@ dependencies = [ name = "morph-node" version = "0.7.5" dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "async-trait", - "jsonrpsee", - "morph-chainspec", - "reth-rpc-eth-api", - "reth-rpc-eth-types", - "serde", - "serde_json", - "thiserror 2.0.17", + "morph-rpc", ] [[package]] @@ -4066,6 +4623,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "morph-rpc" +version = "0.7.5" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-network", + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "derive_more", + "eyre", + "jsonrpsee", + "morph-chainspec", + "morph-evm", + "morph-primitives", + "morph-revm", + "reth-chainspec", + "reth-errors", + "reth-evm", + "reth-node-api", + "reth-node-builder", + "reth-primitives-traits", + "reth-provider", + "reth-revm", + "reth-rpc", + "reth-rpc-convert", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "reth-tasks", + "reth-transaction-pool", + "revm", + "serde", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "morph-txpool" version = "0.7.5" @@ -4630,13 +5224,23 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "pem" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -4656,6 +5260,16 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version 0.4.1", +] + [[package]] name = "phf" version = "0.13.1" @@ -4798,6 +5412,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -4858,6 +5482,52 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags 2.10.0", + "chrono", + "flate2", + "hex", + "procfs-core 0.17.0", + "rustix 0.38.44", +] + +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags 2.10.0", + "procfs-core 0.18.0", + "rustix 1.1.2", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags 2.10.0", + "chrono", + "hex", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.10.0", + "hex", +] + [[package]] name = "proptest" version = "1.9.0" @@ -4933,25 +5603,51 @@ dependencies = [ ] [[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quick-protobuf" -version = "0.8.1" +name = "pulldown-cmark" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "byteorder", + "bitflags 2.10.0", + "memchr", + "unicase", ] [[package]] -name = "quinn" -version = "0.11.9" +name = "quanta" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -5094,6 +5790,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + [[package]] name = "rapidhash" version = "4.2.0" @@ -5104,6 +5809,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "rayon" version = "1.11.0" @@ -5124,6 +5838,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -5210,7 +5930,7 @@ version = "0.12.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -5235,13 +5955,16 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots 1.0.5", ] [[package]] @@ -5437,6 +6160,32 @@ dependencies = [ "reth-primitives-traits", ] +[[package]] +name = "reth-consensus-debug-client" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-engine", + "alloy-transport", + "auto_impl", + "derive_more", + "eyre", + "futures", + "reqwest", + "reth-node-api", + "reth-primitives-traits", + "reth-tracing", + "ringbuffer", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "reth-db" version = "1.9.3" @@ -5491,6 +6240,36 @@ dependencies = [ "serde", ] +[[package]] +name = "reth-db-common" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-genesis", + "alloy-primitives", + "boyer-moore-magiclen", + "eyre", + "reth-chainspec", + "reth-codecs", + "reth-config", + "reth-db-api", + "reth-etl", + "reth-execution-errors", + "reth-fs-util", + "reth-node-types", + "reth-primitives-traits", + "reth-provider", + "reth-stages-types", + "reth-static-file-types", + "reth-trie", + "reth-trie-db", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + [[package]] name = "reth-db-models" version = "1.9.3" @@ -5579,6 +6358,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "reth-downloaders" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "futures", + "futures-util", + "metrics", + "pin-project", + "rayon", + "reth-config", + "reth-consensus", + "reth-metrics", + "reth-network-p2p", + "reth-network-peers", + "reth-primitives-traits", + "reth-storage-api", + "reth-tasks", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + [[package]] name = "reth-ecies" version = "1.9.3" @@ -5657,130 +6464,296 @@ dependencies = [ ] [[package]] -name = "reth-errors" +name = "reth-engine-service" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ + "futures", + "pin-project", + "reth-chainspec", "reth-consensus", - "reth-execution-errors", - "reth-storage-errors", - "thiserror 2.0.17", + "reth-engine-primitives", + "reth-engine-tree", + "reth-ethereum-primitives", + "reth-evm", + "reth-network-p2p", + "reth-node-types", + "reth-payload-builder", + "reth-provider", + "reth-prune", + "reth-stages-api", + "reth-tasks", ] [[package]] -name = "reth-eth-wire" +name = "reth-engine-tree" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ - "alloy-chains", + "alloy-consensus", + "alloy-eip7928", + "alloy-eips", + "alloy-evm", "alloy-primitives", "alloy-rlp", - "bytes", + "alloy-rpc-types-engine", + "crossbeam-channel", + "dashmap 6.1.0", "derive_more", "futures", - "pin-project", - "reth-codecs", - "reth-ecies", - "reth-eth-wire-types", - "reth-ethereum-forks", + "metrics", + "mini-moka", + "parking_lot", + "rayon", + "reth-chain-state", + "reth-consensus", + "reth-db", + "reth-engine-primitives", + "reth-errors", + "reth-ethereum-primitives", + "reth-evm", + "reth-execution-types", "reth-metrics", - "reth-network-peers", + "reth-network-p2p", + "reth-payload-builder", + "reth-payload-primitives", "reth-primitives-traits", - "serde", - "snap", + "reth-provider", + "reth-prune", + "reth-revm", + "reth-stages-api", + "reth-tasks", + "reth-trie", + "reth-trie-parallel", + "reth-trie-sparse", + "reth-trie-sparse-parallel", + "revm", + "revm-primitives", + "schnellru", + "smallvec", "thiserror 2.0.17", "tokio", - "tokio-stream", - "tokio-util", "tracing", ] [[package]] -name = "reth-eth-wire-types" +name = "reth-engine-util" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ - "alloy-chains", "alloy-consensus", - "alloy-eips", - "alloy-hardforks", - "alloy-primitives", - "alloy-rlp", - "bytes", - "derive_more", + "alloy-rpc-types-engine", + "eyre", + "futures", + "itertools 0.14.0", + "pin-project", "reth-chainspec", - "reth-codecs-derive", - "reth-ethereum-primitives", + "reth-engine-primitives", + "reth-engine-tree", + "reth-errors", + "reth-evm", + "reth-fs-util", + "reth-payload-primitives", "reth-primitives-traits", + "reth-revm", + "reth-storage-api", "serde", - "thiserror 2.0.17", + "serde_json", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "reth-ethereum-engine-primitives" +name = "reth-era" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ + "alloy-consensus", "alloy-eips", "alloy-primitives", "alloy-rlp", - "alloy-rpc-types-engine", - "reth-engine-primitives", - "reth-ethereum-primitives", - "reth-payload-primitives", - "reth-primitives-traits", - "serde", - "sha2", + "ethereum_ssz", + "ethereum_ssz_derive", + "snap", "thiserror 2.0.17", ] [[package]] -name = "reth-ethereum-forks" +name = "reth-era-downloader" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ - "alloy-eip2124", - "alloy-hardforks", "alloy-primitives", - "auto_impl", - "once_cell", - "rustc-hash", + "bytes", + "eyre", + "futures-util", + "reqwest", + "reth-era", + "reth-fs-util", + "sha2", + "tokio", ] [[package]] -name = "reth-ethereum-primitives" +name = "reth-era-utils" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ "alloy-consensus", - "alloy-eips", "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "arbitrary", - "modular-bitfield", - "reth-codecs", + "eyre", + "futures-util", + "reth-db-api", + "reth-era", + "reth-era-downloader", + "reth-etl", + "reth-fs-util", "reth-primitives-traits", - "reth-zstd-compressors", - "serde", - "serde_with", + "reth-provider", + "reth-stages-types", + "reth-storage-api", + "tokio", + "tracing", ] [[package]] -name = "reth-evm" +name = "reth-errors" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ - "alloy-consensus", - "alloy-eips", + "reth-consensus", + "reth-execution-errors", + "reth-storage-errors", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-eth-wire" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-chains", + "alloy-primitives", + "alloy-rlp", + "bytes", + "derive_more", + "futures", + "pin-project", + "reth-codecs", + "reth-ecies", + "reth-eth-wire-types", + "reth-ethereum-forks", + "reth-metrics", + "reth-network-peers", + "reth-primitives-traits", + "serde", + "snap", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "reth-eth-wire-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-primitives", + "alloy-rlp", + "bytes", + "derive_more", + "reth-chainspec", + "reth-codecs-derive", + "reth-ethereum-primitives", + "reth-primitives-traits", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-ethereum-engine-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-engine", + "reth-engine-primitives", + "reth-ethereum-primitives", + "reth-payload-primitives", + "reth-primitives-traits", + "serde", + "sha2", + "thiserror 2.0.17", +] + +[[package]] +name = "reth-ethereum-forks" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-eip2124", + "alloy-hardforks", + "alloy-primitives", + "auto_impl", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "reth-ethereum-primitives" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-serde", + "arbitrary", + "modular-bitfield", + "reth-codecs", + "reth-primitives-traits", + "reth-zstd-compressors", + "serde", + "serde_with", +] + +[[package]] +name = "reth-etl" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "rayon", + "reth-db-api", + "tempfile", +] + +[[package]] +name = "reth-evm" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", "alloy-evm", "alloy-primitives", "auto_impl", "derive_more", "futures-util", + "metrics", "reth-execution-errors", "reth-execution-types", + "reth-metrics", "reth-primitives-traits", "reth-storage-api", "reth-storage-errors", @@ -5840,6 +6813,58 @@ dependencies = [ "serde_with", ] +[[package]] +name = "reth-exex" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "eyre", + "futures", + "itertools 0.14.0", + "metrics", + "parking_lot", + "reth-chain-state", + "reth-chainspec", + "reth-config", + "reth-ethereum-primitives", + "reth-evm", + "reth-exex-types", + "reth-fs-util", + "reth-metrics", + "reth-node-api", + "reth-node-core", + "reth-payload-builder", + "reth-primitives-traits", + "reth-provider", + "reth-prune-types", + "reth-revm", + "reth-stages-api", + "reth-tasks", + "reth-tracing", + "rmp-serde", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "reth-exex-types" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "reth-chain-state", + "reth-execution-types", + "reth-primitives-traits", + "serde", + "serde_with", +] + [[package]] name = "reth-fs-util" version = "1.9.3" @@ -5850,6 +6875,54 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "reth-invalid-block-hooks" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-debug", + "eyre", + "futures", + "jsonrpsee", + "pretty_assertions", + "reth-engine-primitives", + "reth-evm", + "reth-primitives-traits", + "reth-provider", + "reth-revm", + "reth-rpc-api", + "reth-tracing", + "reth-trie", + "revm", + "revm-bytecode", + "revm-database", + "serde", + "serde_json", +] + +[[package]] +name = "reth-ipc" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "bytes", + "futures", + "futures-util", + "interprocess", + "jsonrpsee", + "pin-project", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + [[package]] name = "reth-libmdbx" version = "1.9.3" @@ -5857,7 +6930,7 @@ source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab6077 dependencies = [ "bitflags 2.10.0", "byteorder", - "dashmap", + "dashmap 6.1.0", "derive_more", "parking_lot", "reth-mdbx-sys", @@ -5871,7 +6944,7 @@ name = "reth-mdbx-sys" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ - "bindgen", + "bindgen 0.71.1", "cc", ] @@ -6083,61 +7156,197 @@ dependencies = [ ] [[package]] -name = "reth-node-core" +name = "reth-node-builder" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-primitives", + "alloy-provider", + "alloy-rpc-types", "alloy-rpc-types-engine", - "clap", - "derive_more", - "dirs-next", + "aquamarine", "eyre", + "fdlimit", "futures", - "humantime", - "ipnet", - "rand 0.9.2", + "jsonrpsee", + "rayon", + "reth-basic-payload-builder", + "reth-chain-state", "reth-chainspec", - "reth-cli-util", "reth-config", "reth-consensus", + "reth-consensus-debug-client", "reth-db", - "reth-discv4", - "reth-discv5", + "reth-db-api", + "reth-db-common", + "reth-downloaders", "reth-engine-local", "reth-engine-primitives", - "reth-ethereum-forks", - "reth-net-banlist", - "reth-net-nat", + "reth-engine-service", + "reth-engine-tree", + "reth-engine-util", + "reth-evm", + "reth-exex", + "reth-fs-util", + "reth-invalid-block-hooks", "reth-network", + "reth-network-api", "reth-network-p2p", - "reth-network-peers", + "reth-node-api", + "reth-node-core", + "reth-node-ethstats", + "reth-node-events", + "reth-node-metrics", + "reth-payload-builder", "reth-primitives-traits", "reth-provider", - "reth-prune-types", - "reth-rpc-convert", + "reth-prune", + "reth-rpc", + "reth-rpc-api", + "reth-rpc-builder", + "reth-rpc-engine-api", "reth-rpc-eth-types", - "reth-rpc-server-types", - "reth-stages-types", - "reth-storage-api", - "reth-storage-errors", + "reth-rpc-layer", + "reth-stages", + "reth-static-file", + "reth-tasks", + "reth-tokio-util", "reth-tracing", - "reth-tracing-otlp", "reth-transaction-pool", "secp256k1 0.30.0", - "serde", - "shellexpand", - "strum", - "thiserror 2.0.17", - "toml", - "tracing", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "reth-node-core" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "clap", + "derive_more", + "dirs-next", + "eyre", + "futures", + "humantime", + "ipnet", + "rand 0.9.2", + "reth-chainspec", + "reth-cli-util", + "reth-config", + "reth-consensus", + "reth-db", + "reth-discv4", + "reth-discv5", + "reth-engine-local", + "reth-engine-primitives", + "reth-ethereum-forks", + "reth-net-banlist", + "reth-net-nat", + "reth-network", + "reth-network-p2p", + "reth-network-peers", + "reth-primitives-traits", + "reth-provider", + "reth-prune-types", + "reth-rpc-convert", + "reth-rpc-eth-types", + "reth-rpc-server-types", + "reth-stages-types", + "reth-storage-api", + "reth-storage-errors", + "reth-tracing", + "reth-tracing-otlp", + "reth-transaction-pool", + "secp256k1 0.30.0", + "serde", + "shellexpand", + "strum", + "thiserror 2.0.17", + "toml", + "tracing", "url", "vergen", "vergen-git2", ] +[[package]] +name = "reth-node-ethstats" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "chrono", + "futures-util", + "reth-chain-state", + "reth-network-api", + "reth-primitives-traits", + "reth-storage-api", + "reth-transaction-pool", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "url", +] + +[[package]] +name = "reth-node-events" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "derive_more", + "futures", + "humantime", + "pin-project", + "reth-engine-primitives", + "reth-network-api", + "reth-primitives-traits", + "reth-prune-types", + "reth-stages", + "reth-static-file-types", + "reth-storage-api", + "tokio", + "tracing", +] + +[[package]] +name = "reth-node-metrics" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "eyre", + "http", + "jsonrpsee-server", + "metrics", + "metrics-exporter-prometheus", + "metrics-process", + "metrics-util", + "procfs 0.17.0", + "reqwest", + "reth-metrics", + "reth-tasks", + "tokio", + "tower", + "tracing", +] + [[package]] name = "reth-node-types" version = "1.9.3" @@ -6273,7 +7482,7 @@ dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rpc-types-engine", - "dashmap", + "dashmap 6.1.0", "eyre", "itertools 0.14.0", "metrics", @@ -6308,6 +7517,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "reth-prune" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "itertools 0.14.0", + "metrics", + "rayon", + "reth-config", + "reth-db-api", + "reth-errors", + "reth-exex-types", + "reth-metrics", + "reth-primitives-traits", + "reth-provider", + "reth-prune-types", + "reth-stages-types", + "reth-static-file-types", + "reth-tokio-util", + "rustc-hash", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "reth-prune-types" version = "1.9.3" @@ -6332,9 +7569,162 @@ dependencies = [ "reth-primitives-traits", "reth-storage-api", "reth-storage-errors", + "reth-trie", "revm", ] +[[package]] +name = "reth-rpc" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-dyn-abi", + "alloy-eips", + "alloy-evm", + "alloy-genesis", + "alloy-network", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-rpc-types-admin", + "alloy-rpc-types-beacon", + "alloy-rpc-types-debug", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-rpc-types-mev", + "alloy-rpc-types-trace", + "alloy-rpc-types-txpool", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "async-trait", + "derive_more", + "dyn-clone", + "futures", + "http", + "http-body", + "hyper", + "itertools 0.14.0", + "jsonrpsee", + "jsonrpsee-types", + "jsonwebtoken", + "parking_lot", + "pin-project", + "reth-chain-state", + "reth-chainspec", + "reth-consensus", + "reth-consensus-common", + "reth-engine-primitives", + "reth-errors", + "reth-ethereum-engine-primitives", + "reth-ethereum-primitives", + "reth-evm", + "reth-evm-ethereum", + "reth-execution-types", + "reth-metrics", + "reth-network-api", + "reth-network-peers", + "reth-network-types", + "reth-node-api", + "reth-primitives-traits", + "reth-revm", + "reth-rpc-api", + "reth-rpc-convert", + "reth-rpc-engine-api", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "reth-rpc-server-types", + "reth-storage-api", + "reth-tasks", + "reth-transaction-pool", + "reth-trie-common", + "revm", + "revm-inspectors", + "revm-primitives", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tower", + "tracing", + "tracing-futures", +] + +[[package]] +name = "reth-rpc-api" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-eips", + "alloy-genesis", + "alloy-json-rpc", + "alloy-primitives", + "alloy-rpc-types", + "alloy-rpc-types-admin", + "alloy-rpc-types-anvil", + "alloy-rpc-types-beacon", + "alloy-rpc-types-debug", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-rpc-types-mev", + "alloy-rpc-types-trace", + "alloy-rpc-types-txpool", + "alloy-serde", + "jsonrpsee", + "reth-chain-state", + "reth-engine-primitives", + "reth-network-peers", + "reth-rpc-eth-api", + "reth-trie-common", + "serde", + "serde_json", +] + +[[package]] +name = "reth-rpc-builder" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-network", + "alloy-provider", + "dyn-clone", + "http", + "jsonrpsee", + "metrics", + "pin-project", + "reth-chain-state", + "reth-chainspec", + "reth-consensus", + "reth-engine-primitives", + "reth-evm", + "reth-ipc", + "reth-metrics", + "reth-network-api", + "reth-node-core", + "reth-primitives-traits", + "reth-rpc", + "reth-rpc-api", + "reth-rpc-eth-api", + "reth-rpc-eth-types", + "reth-rpc-layer", + "reth-rpc-server-types", + "reth-storage-api", + "reth-tasks", + "reth-tokio-util", + "reth-transaction-pool", + "serde", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", +] + [[package]] name = "reth-rpc-convert" version = "1.9.3" @@ -6356,6 +7746,35 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "reth-rpc-engine-api" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "async-trait", + "jsonrpsee-core", + "jsonrpsee-types", + "metrics", + "reth-chainspec", + "reth-engine-primitives", + "reth-metrics", + "reth-payload-builder", + "reth-payload-builder-primitives", + "reth-payload-primitives", + "reth-primitives-traits", + "reth-rpc-api", + "reth-storage-api", + "reth-tasks", + "reth-transaction-pool", + "serde", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "reth-rpc-eth-api" version = "1.9.3" @@ -6447,6 +7866,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "reth-rpc-layer" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-rpc-types-engine", + "http", + "jsonrpsee-http-client", + "pin-project", + "tower", + "tower-http", + "tracing", +] + [[package]] name = "reth-rpc-server-types" version = "1.9.3" @@ -6463,18 +7896,109 @@ dependencies = [ "strum", ] +[[package]] +name = "reth-stages" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "bincode", + "eyre", + "futures-util", + "itertools 0.14.0", + "num-traits", + "rayon", + "reqwest", + "reth-codecs", + "reth-config", + "reth-consensus", + "reth-db", + "reth-db-api", + "reth-era", + "reth-era-downloader", + "reth-era-utils", + "reth-etl", + "reth-evm", + "reth-execution-types", + "reth-exex", + "reth-fs-util", + "reth-network-p2p", + "reth-primitives-traits", + "reth-provider", + "reth-prune", + "reth-prune-types", + "reth-revm", + "reth-stages-api", + "reth-static-file-types", + "reth-storage-errors", + "reth-trie", + "reth-trie-db", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "reth-stages-api" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "aquamarine", + "auto_impl", + "futures-util", + "metrics", + "reth-consensus", + "reth-errors", + "reth-metrics", + "reth-network-p2p", + "reth-primitives-traits", + "reth-provider", + "reth-prune", + "reth-stages-types", + "reth-static-file", + "reth-static-file-types", + "reth-tokio-util", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "reth-stages-types" version = "1.9.3" source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" dependencies = [ "alloy-primitives", - "arbitrary", - "bytes", - "modular-bitfield", + "arbitrary", + "bytes", + "modular-bitfield", + "reth-codecs", + "reth-trie-common", + "serde", +] + +[[package]] +name = "reth-static-file" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-primitives", + "parking_lot", + "rayon", "reth-codecs", - "reth-trie-common", - "serde", + "reth-db-api", + "reth-primitives-traits", + "reth-provider", + "reth-prune-types", + "reth-stages-types", + "reth-static-file-types", + "reth-storage-errors", + "reth-tokio-util", + "tracing", ] [[package]] @@ -6694,6 +8218,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "reth-trie-parallel" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crossbeam-channel", + "dashmap 6.1.0", + "derive_more", + "itertools 0.14.0", + "metrics", + "rayon", + "reth-execution-errors", + "reth-metrics", + "reth-provider", + "reth-storage-errors", + "reth-trie", + "reth-trie-common", + "reth-trie-sparse", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "reth-trie-sparse" version = "1.9.3" @@ -6703,14 +8252,34 @@ dependencies = [ "alloy-rlp", "alloy-trie", "auto_impl", + "metrics", "rayon", "reth-execution-errors", + "reth-metrics", "reth-primitives-traits", "reth-trie-common", "smallvec", "tracing", ] +[[package]] +name = "reth-trie-sparse-parallel" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "metrics", + "rayon", + "reth-execution-errors", + "reth-metrics", + "reth-trie-common", + "reth-trie-sparse", + "smallvec", + "tracing", +] + [[package]] name = "reth-zstd-compressors" version = "1.9.3" @@ -6950,6 +8519,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ringbuffer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" + [[package]] name = "ripemd" version = "0.1.3" @@ -6959,6 +8534,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + [[package]] name = "rlp" version = "0.5.2" @@ -6969,6 +8553,25 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "roaring" version = "0.10.12" @@ -7074,6 +8677,19 @@ dependencies = [ "semver 1.0.27", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.2" @@ -7083,7 +8699,7 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -7358,6 +8974,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.228" @@ -7429,7 +9051,7 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -7549,6 +9171,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -7567,6 +9195,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata 0.14.2", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + [[package]] name = "slab" version = "0.4.11" @@ -7615,7 +9264,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures", "http", @@ -7744,7 +9393,7 @@ dependencies = [ "libc", "memchr", "ntapi", - "windows", + "windows 0.57.0", ] [[package]] @@ -7768,7 +9417,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.2", "windows-sys 0.61.2", ] @@ -7947,6 +9596,23 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -8040,7 +9706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "bytes", "http", "http-body", @@ -8078,6 +9744,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "hdrhistogram", "indexmap 2.12.1", "pin-project-lite", "slab", @@ -8095,16 +9762,29 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", + "base64 0.22.1", "bitflags 2.10.0", "bytes", + "futures-core", "futures-util", "http", "http-body", + "http-body-util", + "http-range-header", + "httpdate", "iri-string", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", + "tracing", + "uuid", ] [[package]] @@ -8265,6 +9945,31 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tree_hash" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee44f4cef85f88b4dea21c0b1f58320bdf35715cf56d840969487cff00613321" +dependencies = [ + "alloy-primitives", + "ethereum_hashing", + "ethereum_ssz", + "smallvec", + "typenum", +] + +[[package]] +name = "tree_hash_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bee2ea1551f90040ab0e34b6fb7f2fa3bad8acc925837ac654f2c78a13e3089" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "triehash" version = "0.8.4" @@ -8275,12 +9980,37 @@ dependencies = [ "rlp", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" @@ -8323,6 +10053,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -8375,6 +10111,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -8417,7 +10159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" dependencies = [ "anyhow", - "cargo_metadata", + "cargo_metadata 0.19.2", "derive_builder", "regex", "rustversion", @@ -8569,6 +10311,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -8621,6 +10376,24 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "widestring" version = "1.2.1" @@ -8668,6 +10441,27 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -8693,6 +10487,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -8743,6 +10548,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -8887,6 +10702,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -9098,6 +10922,25 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version 0.4.1", + "send_wrapper 0.6.0", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" @@ -9107,6 +10950,12 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 0337c7d..0caeaaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/engine-api", "crates/evm", "crates/node", + "crates/rpc", "crates/payload/builder", "crates/payload/types", "crates/primitives", @@ -48,6 +49,7 @@ morph-node = { path = "crates/node"} morph-payload-builder = { path = "crates/payload/builder", default-features = false } morph-payload-types = { path = "crates/payload/types", default-features = false } morph-primitives = { path = "crates/primitives", default-features = false } +morph-rpc = { path = "crates/rpc" } morph-revm = { path = "crates/revm", default-features = false } morph-txpool = { path = "crates/txpool", default-features = false } @@ -96,6 +98,7 @@ reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3 reth-rpc-eth-types = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } +reth-tasks = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-tracing = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3", default-features = false } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index f6c0c11..a854578 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -10,31 +10,7 @@ publish.workspace = true workspace = true [dependencies] -# Morph crates -morph-chainspec.workspace = true - -# Reth RPC -reth-rpc-eth-api.workspace = true -reth-rpc-eth-types.workspace = true - -# Alloy -alloy-primitives.workspace = true -alloy-consensus.workspace = true -alloy-rpc-types-eth.workspace = true -alloy-serde.workspace = true - -# Async -async-trait.workspace = true - -# JSON-RPC -jsonrpsee.workspace = true - -# Serialization -serde.workspace = true -serde_json.workspace = true - -# Error handling -thiserror.workspace = true +morph-rpc.workspace = true [features] default = [] diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 394c3b8..f75e0a9 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -4,4 +4,4 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -pub mod rpc; +pub use morph_rpc as rpc; diff --git a/crates/node/src/rpc/mod.rs b/crates/node/src/rpc/mod.rs deleted file mode 100644 index 8f831f6..0000000 --- a/crates/node/src/rpc/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Morph RPC implementation -//! -//! This module provides the Morph-specific RPC types and API implementations. -//! Following Tempo's pattern, RPC is implemented as a submodule of the node crate. - -pub mod error; -pub mod morph_api; -pub mod types; - -pub use error::MorphEthApiError; -pub use morph_api::{MorphApiServer, MorphRpc}; -pub use types::*; diff --git a/crates/node/src/rpc/morph_api.rs b/crates/node/src/rpc/morph_api.rs deleted file mode 100644 index 83f8399..0000000 --- a/crates/node/src/rpc/morph_api.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Morph RPC API implementation -//! -//! This module provides the `morph_` namespace JSON-RPC methods. - -use crate::rpc::{DiskAndHeaderRoot, SyncStatus}; -use alloy_primitives::{B256, BlockHash, U64}; -use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionRequest}; -use jsonrpsee::{core::RpcResult, proc_macros::rpc}; - -/// Morph-specific RPC methods under the `morph_` namespace -#[rpc(server, namespace = "morph")] -pub trait MorphApi { - /// Returns the block by hash with additional Morph-specific fields. - /// - /// This is similar to `eth_getBlockByHash` but includes: - /// - `withdrawTrieRoot`: The withdraw trie root from the L2 message queue - /// - `batchHash`: The batch hash if this is a batch point - /// - `nextL1MsgIndex`: The next L1 message index to be processed - #[method(name = "getBlockByHash")] - async fn get_block_by_hash( - &self, - hash: BlockHash, - full_transactions: bool, - ) -> RpcResult>; - - /// Returns the block by number with additional Morph-specific fields. - #[method(name = "getBlockByNumber")] - async fn get_block_by_number( - &self, - number: BlockNumberOrTag, - full_transactions: bool, - ) -> RpcResult>; - - /// Estimates the L1 data fee for a transaction. - /// - /// The L1 data fee is the cost of posting transaction data to L1. - /// This is separate from the L2 execution gas fee. - #[method(name = "estimateL1DataFee")] - async fn estimate_l1_data_fee( - &self, - tx: TransactionRequest, - block_number: Option, - ) -> RpcResult; - - /// Returns the total number of skipped transactions. - #[method(name = "getNumSkippedTransactions")] - async fn get_num_skipped_transactions(&self) -> RpcResult; - - /// Returns the hashes of skipped transactions in a range. - #[method(name = "getSkippedTransactionHashes")] - async fn get_skipped_transaction_hashes(&self, from: U64, to: U64) -> RpcResult>; - - /// Returns the current L1 sync height. - #[method(name = "getL1SyncHeight")] - async fn get_l1_sync_height(&self) -> RpcResult; - - /// Returns the latest relayed L1 message queue index. - #[method(name = "getLatestRelayedQueueIndex")] - async fn get_latest_relayed_queue_index(&self) -> RpcResult; - - /// Returns the current sync status. - #[method(name = "syncStatus")] - async fn sync_status(&self) -> RpcResult; - - /// Returns both the disk state root and header root for a block. - #[method(name = "diskRoot")] - async fn disk_root( - &self, - block_number: Option, - ) -> RpcResult; -} - -/// Implementation of the `morph_` namespace RPC methods. -/// -/// This is a placeholder implementation. The actual implementation will be -/// completed once the full node components are available. -#[derive(Debug, Clone, Default)] -pub struct MorphRpc { - // Provider and other dependencies will be added here -} - -impl MorphRpc { - /// Creates a new `MorphRpc` instance. - pub fn new() -> Self { - Self::default() - } -} - -#[async_trait::async_trait] -impl MorphApiServer for MorphRpc { - async fn get_block_by_hash( - &self, - _hash: BlockHash, - _full_transactions: bool, - ) -> RpcResult> { - // TODO: Implement with Morph-specific fields (withdrawTrieRoot, batchHash, nextL1MsgIndex) - Err(jsonrpsee::types::ErrorObject::owned( - -32601, - "method not implemented", - None::<()>, - )) - } - - async fn get_block_by_number( - &self, - _number: BlockNumberOrTag, - _full_transactions: bool, - ) -> RpcResult> { - // TODO: Implement with Morph-specific fields - Err(jsonrpsee::types::ErrorObject::owned( - -32601, - "method not implemented", - None::<()>, - )) - } - - async fn estimate_l1_data_fee( - &self, - _tx: TransactionRequest, - _block_number: Option, - ) -> RpcResult { - // TODO: Implement L1 data fee estimation using morph-revm - Err(jsonrpsee::types::ErrorObject::owned( - -32601, - "method not implemented", - None::<()>, - )) - } - - async fn get_num_skipped_transactions(&self) -> RpcResult { - // TODO: Query from database - Ok(U64::ZERO) - } - - async fn get_skipped_transaction_hashes(&self, _from: U64, _to: U64) -> RpcResult> { - // TODO: Query from database - Ok(vec![]) - } - - async fn get_l1_sync_height(&self) -> RpcResult { - // TODO: Query from sync state - Ok(U64::ZERO) - } - - async fn get_latest_relayed_queue_index(&self) -> RpcResult { - // TODO: Query from database - Ok(U64::ZERO) - } - - async fn sync_status(&self) -> RpcResult { - // TODO: Query actual sync status - Ok(SyncStatus { - current_l2_block: U64::ZERO, - highest_l2_block: U64::ZERO, - l1_sync_height: U64::ZERO, - latest_relayed_queue_index: U64::ZERO, - }) - } - - async fn disk_root( - &self, - _block_number: Option, - ) -> RpcResult { - // TODO: Implement disk root lookup - Err(jsonrpsee::types::ErrorObject::owned( - -32601, - "method not implemented", - None::<()>, - )) - } -} diff --git a/crates/node/src/rpc/types.rs b/crates/node/src/rpc/types.rs deleted file mode 100644 index d948eee..0000000 --- a/crates/node/src/rpc/types.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Morph-specific RPC types -//! -//! This module provides RPC representations for Morph transactions, receipts, and blocks. - -use alloy_primitives::{Address, B256, BlockHash, Bloom, Bytes, U64, U128, U256}; -use alloy_rpc_types_eth::{AccessList, Log}; -use alloy_serde::OtherFields; -use serde::{Deserialize, Serialize}; - -/// Morph RPC transaction representation -/// -/// Extends standard Ethereum transaction with: -/// - L1Message fields (sender, queueIndex) -/// - AltFee/MorphTx fields (feeTokenID, feeLimit) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MorphRpcTransaction { - /// Transaction hash - pub hash: B256, - /// Transaction nonce - pub nonce: U64, - /// Block hash where this transaction was included - #[serde(skip_serializing_if = "Option::is_none")] - pub block_hash: Option, - /// Block number where this transaction was included - #[serde(skip_serializing_if = "Option::is_none")] - pub block_number: Option, - /// Transaction index in the block - #[serde(skip_serializing_if = "Option::is_none")] - pub transaction_index: Option, - /// Sender address - pub from: Address, - /// Recipient address (None for contract creation) - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option
, - /// Transferred value - pub value: U256, - /// Gas limit - pub gas: U256, - /// Gas price (for legacy and access list transactions) - #[serde(skip_serializing_if = "Option::is_none")] - pub gas_price: Option, - /// Maximum fee per gas (EIP-1559) - #[serde(skip_serializing_if = "Option::is_none")] - pub max_fee_per_gas: Option, - /// Maximum priority fee per gas (EIP-1559) - #[serde(skip_serializing_if = "Option::is_none")] - pub max_priority_fee_per_gas: Option, - /// Transaction input data - pub input: Bytes, - /// Transaction type (0x00-0x7f) - #[serde(rename = "type")] - pub tx_type: U64, - /// Access list (EIP-2930) - #[serde(skip_serializing_if = "Option::is_none")] - pub access_list: Option, - /// Chain ID - #[serde(skip_serializing_if = "Option::is_none")] - pub chain_id: Option, - /// ECDSA signature v - pub v: U256, - /// ECDSA signature r - pub r: U256, - /// ECDSA signature s - pub s: U256, - /// EIP-2718 y parity - #[serde(skip_serializing_if = "Option::is_none")] - pub y_parity: Option, - - // Morph-specific fields for L1Message transactions (0x7E) - /// L1 message sender (only for L1Message type 0x7E) - #[serde(skip_serializing_if = "Option::is_none")] - pub sender: Option
, - /// L1 message queue index (only for L1Message type 0x7E) - #[serde(skip_serializing_if = "Option::is_none")] - pub queue_index: Option, - - // Morph-specific fields for AltFee/MorphTx transactions (0x7F) - /// Token ID for fee payment (only for MorphTx type 0x7F) - #[serde(rename = "feeTokenID", skip_serializing_if = "Option::is_none")] - pub fee_token_id: Option, - /// Maximum token amount willing to pay for fees (only for MorphTx type 0x7F) - #[serde(skip_serializing_if = "Option::is_none")] - pub fee_limit: Option, - - /// Additional fields for future extensibility - #[serde(flatten)] - pub other: OtherFields, -} - -/// Morph RPC transaction receipt -/// -/// Extends standard Ethereum receipt with L1 fee and token fee fields -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MorphRpcReceipt { - /// Transaction hash - pub transaction_hash: B256, - /// Transaction index in the block - pub transaction_index: U64, - /// Block hash - pub block_hash: BlockHash, - /// Block number - pub block_number: U256, - /// Sender address - pub from: Address, - /// Recipient address (None for contract creation) - #[serde(skip_serializing_if = "Option::is_none")] - pub to: Option
, - /// Cumulative gas used in the block - pub cumulative_gas_used: U256, - /// Gas used by this transaction - pub gas_used: U256, - /// Effective gas price paid - pub effective_gas_price: U128, - /// Contract address created (for contract creation) - #[serde(skip_serializing_if = "Option::is_none")] - pub contract_address: Option
, - /// Logs emitted - pub logs: Vec, - /// Logs bloom filter - pub logs_bloom: Bloom, - /// Transaction type - #[serde(rename = "type")] - pub tx_type: U64, - /// Status code (1 for success, 0 for failure) - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - /// State root (pre-Byzantium) - #[serde(skip_serializing_if = "Option::is_none")] - pub root: Option, - - // Morph-specific fields - /// L1 data fee paid (in wei) - #[serde(rename = "l1Fee")] - pub l1_fee: U256, - /// Fee rate used for token fee calculation - #[serde(skip_serializing_if = "Option::is_none")] - pub fee_rate: Option, - /// Token scale factor - #[serde(skip_serializing_if = "Option::is_none")] - pub token_scale: Option, - /// Fee limit specified in the transaction - #[serde(skip_serializing_if = "Option::is_none")] - pub fee_limit: Option, - /// Token ID used for fee payment - #[serde(rename = "feeTokenID", skip_serializing_if = "Option::is_none")] - pub fee_token_id: Option, - - /// Additional fields for future extensibility - #[serde(flatten)] - pub other: OtherFields, -} - -/// Sync status information -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SyncStatus { - /// Current L2 block number - pub current_l2_block: U64, - /// Highest known L2 block - pub highest_l2_block: U64, - /// Current L1 sync height - pub l1_sync_height: U64, - /// Latest relayed L1 message queue index - pub latest_relayed_queue_index: U64, -} - -/// Disk and header root response -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DiskAndHeaderRoot { - /// The state root stored on disk - pub disk_root: B256, - /// The state root in the block header - pub header_root: B256, -} diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 9aa0b74..62d2232 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -60,6 +60,7 @@ reth-codec = [ "dep:reth-db-api", "dep:modular-bitfield", "dep:reth-zstd-compressors", + "dep:reth-ethereum-primitives", "reth-ethereum-primitives/reth-codec", "reth-codecs/alloy", "dep:bytes", diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 0161bc8..9b2e357 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -47,7 +47,7 @@ pub type Block = alloy_consensus::Block; pub type BlockBody = alloy_consensus::BlockBody; // Re-export receipt types -pub use receipt::{MorphReceipt, MorphReceiptWithBloom, MorphTransactionReceipt}; +pub use receipt::{MorphReceipt, MorphReceiptEnvelope, MorphReceiptWithBloom, MorphTransactionReceipt}; // Re-export transaction types pub use transaction::{ diff --git a/crates/primitives/src/receipt/envelope.rs b/crates/primitives/src/receipt/envelope.rs new file mode 100644 index 0000000..343fbf5 --- /dev/null +++ b/crates/primitives/src/receipt/envelope.rs @@ -0,0 +1,302 @@ +//! Receipt envelope types for Morph. + +use crate::transaction::envelope::MorphTxType; +use std::vec::Vec; + +use alloy_consensus::{Eip658Value, Receipt, ReceiptWithBloom, TxReceipt}; +use alloy_eips::{ + eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, + Typed2718, +}; +use alloy_primitives::{logs_bloom, Bloom, Log}; +use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; + +/// Receipt envelope, as defined in [EIP-2718], modified for Morph chains. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[non_exhaustive] +pub enum MorphReceiptEnvelope { + /// Receipt envelope with no type flag. + #[cfg_attr(feature = "serde", serde(rename = "0x0", alias = "0x00"))] + Legacy(ReceiptWithBloom>), + /// Receipt envelope with type flag 1, containing a [EIP-2930] receipt. + #[cfg_attr(feature = "serde", serde(rename = "0x1", alias = "0x01"))] + Eip2930(ReceiptWithBloom>), + /// Receipt envelope with type flag 2, containing a [EIP-1559] receipt. + #[cfg_attr(feature = "serde", serde(rename = "0x2", alias = "0x02"))] + Eip1559(ReceiptWithBloom>), + /// Receipt envelope with type flag 4, containing a [EIP-7702] receipt. + #[cfg_attr(feature = "serde", serde(rename = "0x4", alias = "0x04"))] + Eip7702(ReceiptWithBloom>), + /// Receipt envelope with type flag 126, containing a Morph L1 message receipt. + #[cfg_attr(feature = "serde", serde(rename = "0x7e", alias = "0x7E"))] + L1Message(ReceiptWithBloom>), + /// Receipt envelope with type flag 127, containing a Morph transaction receipt. + #[cfg_attr(feature = "serde", serde(rename = "0x7f", alias = "0x7F"))] + Morph(ReceiptWithBloom>), +} + +impl MorphReceiptEnvelope { + /// Creates a new [`MorphReceiptEnvelope`] from the given parts. + pub fn from_parts<'a>( + status: bool, + cumulative_gas_used: u64, + logs: impl IntoIterator, + tx_type: MorphTxType, + ) -> Self { + let logs = logs.into_iter().cloned().collect::>(); + let logs_bloom = logs_bloom(&logs); + let inner_receipt = + Receipt { status: Eip658Value::Eip658(status), cumulative_gas_used, logs }; + let with_bloom = ReceiptWithBloom { receipt: inner_receipt, logs_bloom }; + match tx_type { + MorphTxType::Legacy => Self::Legacy(with_bloom), + MorphTxType::Eip2930 => Self::Eip2930(with_bloom), + MorphTxType::Eip1559 => Self::Eip1559(with_bloom), + MorphTxType::Eip7702 => Self::Eip7702(with_bloom), + MorphTxType::L1Msg => Self::L1Message(with_bloom), + MorphTxType::Morph => Self::Morph(with_bloom), + } + } +} + +impl MorphReceiptEnvelope { + /// Return the [`MorphTxType`] of the inner receipt. + pub const fn tx_type(&self) -> MorphTxType { + match self { + Self::Legacy(_) => MorphTxType::Legacy, + Self::Eip2930(_) => MorphTxType::Eip2930, + Self::Eip1559(_) => MorphTxType::Eip1559, + Self::Eip7702(_) => MorphTxType::Eip7702, + Self::L1Message(_) => MorphTxType::L1Msg, + Self::Morph(_) => MorphTxType::Morph, + } + } + + /// Returns the success status of the receipt's transaction. + pub const fn status(&self) -> bool { + self.as_receipt().unwrap().status.coerce_status() + } + + /// Return true if the transaction was successful. + pub const fn is_success(&self) -> bool { + self.status() + } + + /// Returns the cumulative gas used at this receipt. + pub const fn cumulative_gas_used(&self) -> u64 { + self.as_receipt().unwrap().cumulative_gas_used + } + + /// Return the receipt logs. + pub fn logs(&self) -> &[T] { + &self.as_receipt().unwrap().logs + } + + /// Return the receipt's bloom. + pub const fn logs_bloom(&self) -> &Bloom { + match self { + Self::Legacy(t) + | Self::Eip2930(t) + | Self::Eip1559(t) + | Self::Eip7702(t) + | Self::L1Message(t) + | Self::Morph(t) => &t.logs_bloom, + } + } + + /// Returns the L1 message receipt if it is a deposit receipt. + pub const fn as_l1_message_receipt_with_bloom( + &self, + ) -> Option<&ReceiptWithBloom>> { + match self { + Self::L1Message(t) => Some(t), + _ => None, + } + } + + /// Returns the L1 message receipt if it is a deposit receipt. + pub const fn as_l1_message_receipt(&self) -> Option<&Receipt> { + match self { + Self::L1Message(t) => Some(&t.receipt), + _ => None, + } + } + + /// Return the inner receipt. Currently this is infallible, however, future + /// receipt types may be added. + pub const fn as_receipt(&self) -> Option<&Receipt> { + match self { + Self::Legacy(t) + | Self::Eip2930(t) + | Self::Eip1559(t) + | Self::Eip7702(t) + | Self::L1Message(t) + | Self::Morph(t) => Some(&t.receipt), + } + } +} + +impl MorphReceiptEnvelope { + /// Get the length of the inner receipt in the 2718 encoding. + pub fn inner_length(&self) -> usize { + match self { + Self::Legacy(t) + | Self::Eip2930(t) + | Self::Eip1559(t) + | Self::Eip7702(t) + | Self::L1Message(t) + | Self::Morph(t) => t.length(), + } + } + + /// Calculate the length of the rlp payload of the network encoded receipt. + pub fn rlp_payload_length(&self) -> usize { + let length = self.inner_length(); + match self { + Self::Legacy(_) => length, + _ => length + 1, + } + } +} + +impl TxReceipt for MorphReceiptEnvelope +where + T: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync, +{ + type Log = T; + + fn status_or_post_state(&self) -> Eip658Value { + self.as_receipt().unwrap().status + } + + fn status(&self) -> bool { + self.as_receipt().unwrap().status.coerce_status() + } + + fn bloom(&self) -> Bloom { + *self.logs_bloom() + } + + fn bloom_cheap(&self) -> Option { + Some(self.bloom()) + } + + fn cumulative_gas_used(&self) -> u64 { + self.as_receipt().unwrap().cumulative_gas_used + } + + fn logs(&self) -> &[T] { + &self.as_receipt().unwrap().logs + } +} + +impl Encodable for MorphReceiptEnvelope { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + self.network_encode(out) + } + + fn length(&self) -> usize { + let mut payload_length = self.rlp_payload_length(); + if !self.is_legacy() { + payload_length += length_of_length(payload_length); + } + payload_length + } +} + +impl Decodable for MorphReceiptEnvelope { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::network_decode(buf) + .map_or_else(|_| Err(alloy_rlp::Error::Custom("Unexpected type")), Ok) + } +} + +impl Encodable2718 for MorphReceiptEnvelope { + fn type_flag(&self) -> Option { + match self { + Self::Legacy(_) => None, + Self::Eip2930(_) => Some(MorphTxType::Eip2930 as u8), + Self::Eip1559(_) => Some(MorphTxType::Eip1559 as u8), + Self::Eip7702(_) => Some(MorphTxType::Eip7702 as u8), + Self::L1Message(_) => Some(MorphTxType::L1Msg as u8), + Self::Morph(_) => Some(MorphTxType::Morph as u8), + } + } + + fn encode_2718_len(&self) -> usize { + self.inner_length() + !self.is_legacy() as usize + } + + fn encode_2718(&self, out: &mut dyn BufMut) { + if let Some(ty) = self.type_flag() { + out.put_u8(ty); + } + match self { + Self::Legacy(t) + | Self::Eip2930(t) + | Self::Eip1559(t) + | Self::Eip7702(t) + | Self::L1Message(t) + | Self::Morph(t) => t.encode(out), + } + } +} + +impl Typed2718 for MorphReceiptEnvelope { + fn ty(&self) -> u8 { + let ty = match self { + Self::Legacy(_) => MorphTxType::Legacy, + Self::Eip2930(_) => MorphTxType::Eip2930, + Self::Eip1559(_) => MorphTxType::Eip1559, + Self::Eip7702(_) => MorphTxType::Eip7702, + Self::L1Message(_) => MorphTxType::L1Msg, + Self::Morph(_) => MorphTxType::Morph, + }; + ty as u8 + } +} + +impl Decodable2718 for MorphReceiptEnvelope { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? { + MorphTxType::Legacy => Err( + alloy_rlp::Error::Custom("type-0 eip2718 receipts are not supported").into(), + ), + MorphTxType::Eip2930 => Ok(Self::Eip2930(Decodable::decode(buf)?)), + MorphTxType::Eip1559 => Ok(Self::Eip1559(Decodable::decode(buf)?)), + MorphTxType::Eip7702 => Ok(Self::Eip7702(Decodable::decode(buf)?)), + MorphTxType::L1Msg => Ok(Self::L1Message(Decodable::decode(buf)?)), + MorphTxType::Morph => Ok(Self::Morph(Decodable::decode(buf)?)), + } + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + Ok(Self::Legacy(Decodable::decode(buf)?)) + } +} + +impl From for MorphReceiptEnvelope { + fn from(value: crate::receipt::MorphReceipt) -> Self { + let (tx_type, inner) = match value { + crate::receipt::MorphReceipt::Legacy(receipt) => (MorphTxType::Legacy, receipt.inner), + crate::receipt::MorphReceipt::Eip2930(receipt) => (MorphTxType::Eip2930, receipt.inner), + crate::receipt::MorphReceipt::Eip1559(receipt) => (MorphTxType::Eip1559, receipt.inner), + crate::receipt::MorphReceipt::Eip7702(receipt) => (MorphTxType::Eip7702, receipt.inner), + crate::receipt::MorphReceipt::Morph(receipt) => (MorphTxType::Morph, receipt.inner), + crate::receipt::MorphReceipt::L1Msg(receipt) => (MorphTxType::L1Msg, receipt), + }; + + let logs_bloom = logs_bloom(&inner.logs); + let with_bloom = ReceiptWithBloom { receipt: inner, logs_bloom }; + match tx_type { + MorphTxType::Legacy => Self::Legacy(with_bloom), + MorphTxType::Eip2930 => Self::Eip2930(with_bloom), + MorphTxType::Eip1559 => Self::Eip1559(with_bloom), + MorphTxType::Eip7702 => Self::Eip7702(with_bloom), + MorphTxType::L1Msg => Self::L1Message(with_bloom), + MorphTxType::Morph => Self::Morph(with_bloom), + } + } +} diff --git a/crates/primitives/src/receipt/mod.rs b/crates/primitives/src/receipt/mod.rs index fd9db53..ceae6ec 100644 --- a/crates/primitives/src/receipt/mod.rs +++ b/crates/primitives/src/receipt/mod.rs @@ -6,6 +6,8 @@ #[allow(clippy::module_inception)] mod receipt; +mod envelope; +pub use envelope::MorphReceiptEnvelope; pub use receipt::{MorphReceiptWithBloom, MorphTransactionReceipt}; use crate::transaction::envelope::MorphTxType; @@ -71,15 +73,15 @@ impl MorphReceipt { } } - /// Returns the L1 fee if present. - pub fn l1_fee(&self) -> Option { + /// Returns the L1 fee for the receipt. + pub fn l1_fee(&self) -> alloy_primitives::U256 { match self { Self::Legacy(r) | Self::Eip2930(r) | Self::Eip1559(r) | Self::Eip7702(r) | Self::Morph(r) => r.l1_fee, - Self::L1Msg(_) => None, + Self::L1Msg(_) => alloy_primitives::U256::ZERO, } } @@ -480,7 +482,7 @@ mod compact { | MorphReceipt::Eip1559(r) | MorphReceipt::Eip7702(r) | MorphReceipt::Morph(r) => ( - r.l1_fee, + (r.l1_fee != U256::ZERO).then_some(r.l1_fee), r.fee_token_id.map(u64::from), r.fee_rate, r.token_scale, @@ -530,7 +532,7 @@ mod compact { let morph_receipt = MorphTransactionReceipt { inner, - l1_fee, + l1_fee: l1_fee.unwrap_or_default(), fee_token_id: fee_token_id.map(|id| id as u16), fee_rate, token_scale, @@ -651,10 +653,10 @@ mod tests { #[test] fn test_receipt_l1_fee() { let receipt = create_test_receipt(); - assert_eq!(receipt.l1_fee(), Some(U256::from(1000))); + assert_eq!(receipt.l1_fee(), U256::from(1000)); let l1_msg = MorphReceipt::L1Msg(Receipt::default()); - assert_eq!(l1_msg.l1_fee(), None); + assert_eq!(l1_msg.l1_fee(), U256::ZERO); } #[test] diff --git a/crates/primitives/src/receipt/receipt.rs b/crates/primitives/src/receipt/receipt.rs index fd5751e..1e4bc54 100644 --- a/crates/primitives/src/receipt/receipt.rs +++ b/crates/primitives/src/receipt/receipt.rs @@ -28,9 +28,9 @@ pub struct MorphTransactionReceipt { /// This is the cost of posting the transaction data to L1. #[cfg_attr( feature = "serde", - serde(default, skip_serializing_if = "Option::is_none") + serde(default, skip_serializing_if = "U256::is_zero") )] - pub l1_fee: Option, + pub l1_fee: U256, /// The ERC20 token ID used for fee payment (TxMorph feature). /// Only present for TxMorph. @@ -70,7 +70,7 @@ impl MorphTransactionReceipt { pub const fn new(inner: Receipt) -> Self { Self { inner, - l1_fee: None, + l1_fee: U256::ZERO, fee_token_id: None, fee_rate: None, token_scale: None, @@ -82,7 +82,7 @@ impl MorphTransactionReceipt { pub const fn with_l1_fee(inner: Receipt, l1_fee: U256) -> Self { Self { inner, - l1_fee: Some(l1_fee), + l1_fee, fee_token_id: None, fee_rate: None, token_scale: None, @@ -101,7 +101,7 @@ impl MorphTransactionReceipt { ) -> Self { Self { inner, - l1_fee: Some(l1_fee), + l1_fee, fee_token_id: Some(fee_token_id), fee_rate: Some(fee_rate), token_scale: Some(token_scale), @@ -114,9 +114,9 @@ impl MorphTransactionReceipt { self.fee_token_id.is_some() } - /// Returns the L1 fee, defaulting to zero if not set. - pub fn l1_fee_or_zero(&self) -> U256 { - self.l1_fee.unwrap_or(U256::ZERO) + /// Returns the L1 fee. + pub const fn l1_fee(&self) -> U256 { + self.l1_fee } } @@ -249,7 +249,7 @@ mod tests { assert!(receipt.status()); assert_eq!(receipt.cumulative_gas_used(), 21000); - assert!(receipt.l1_fee.is_none()); + assert_eq!(receipt.l1_fee, U256::ZERO); assert!(receipt.fee_token_id.is_none()); assert!(!receipt.is_morph_tx()); } @@ -264,8 +264,8 @@ mod tests { let l1_fee = U256::from(1000000); let receipt = MorphTransactionReceipt::with_l1_fee(inner, l1_fee); - assert_eq!(receipt.l1_fee, Some(l1_fee)); - assert_eq!(receipt.l1_fee_or_zero(), l1_fee); + assert_eq!(receipt.l1_fee, l1_fee); + assert_eq!(receipt.l1_fee(), l1_fee); assert!(!receipt.is_morph_tx()); } diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml new file mode 100644 index 0000000..087a2be --- /dev/null +++ b/crates/rpc/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "morph-rpc" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# Morph crates +morph-primitives = { workspace = true, features = ["serde", "reth-codec"] } +morph-chainspec.workspace = true +morph-revm = { workspace = true, features = ["rpc"] } +morph-evm = { workspace = true, features = ["rpc"] } + +# Reth +reth-chainspec.workspace = true +reth-node-api.workspace = true +reth-node-builder.workspace = true +reth-primitives-traits.workspace = true +reth-rpc.workspace = true +reth-rpc-convert.workspace = true +reth-rpc-eth-api.workspace = true +reth-rpc-eth-types.workspace = true +reth-evm.workspace = true +reth-provider.workspace = true +reth-revm.workspace = true +reth-tasks.workspace = true +reth-errors.workspace = true +reth-transaction-pool.workspace = true + +# Alloy +alloy-consensus.workspace = true +alloy-primitives.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-serde.workspace = true +alloy-network.workspace = true +alloy-eips.workspace = true + +# JSON-RPC +jsonrpsee.workspace = true + +# Serialization +serde.workspace = true +derive_more.workspace = true +tokio.workspace = true +revm.workspace = true + +# Error handling +thiserror.workspace = true +eyre.workspace = true + + +[features] +default = [] diff --git a/crates/node/src/rpc/error.rs b/crates/rpc/src/error.rs similarity index 64% rename from crates/node/src/rpc/error.rs rename to crates/rpc/src/error.rs index aa2b3b3..f6ce93f 100644 --- a/crates/node/src/rpc/error.rs +++ b/crates/rpc/src/error.rs @@ -1,7 +1,16 @@ //! Error types for Morph RPC use alloy_primitives::B256; -use reth_rpc_eth_types::EthApiError; +use morph_evm::MorphEvmConfig; +use reth_errors::ProviderError; +use reth_evm::revm::context::result::EVMError; +use reth_evm::{HaltReasonFor, InvalidTxError}; +use reth_rpc_convert::TransactionConversionError; +use reth_rpc_eth_types::{ + error::{api::FromEvmHalt, api::FromRevert, AsEthApiError}, + EthApiError, +}; +use std::convert::Infallible; use thiserror::Error; /// Morph Eth API errors @@ -91,3 +100,53 @@ impl From for jsonrpsee::types::ErrorObject<'static> { } } } + +impl AsEthApiError for MorphEthApiError { + fn as_err(&self) -> Option<&EthApiError> { + match self { + MorphEthApiError::Eth(err) => Some(err), + _ => None, + } + } +} + +impl FromEvmHalt> for MorphEthApiError { + fn from_evm_halt(halt: HaltReasonFor, gas_limit: u64) -> Self { + MorphEthApiError::Eth(EthApiError::from_evm_halt(halt, gas_limit)) + } +} + +impl FromRevert for MorphEthApiError { + fn from_revert(output: alloy_primitives::Bytes) -> Self { + MorphEthApiError::Eth(EthApiError::from_revert(output)) + } +} + +impl From for MorphEthApiError { + fn from(err: ProviderError) -> Self { + MorphEthApiError::Eth(err.into()) + } +} + +impl From> for MorphEthApiError +where + T: Into, + TxError: InvalidTxError, +{ + fn from(err: EVMError) -> Self { + MorphEthApiError::Eth(err.into()) + } +} + +impl From for MorphEthApiError { + fn from(err: TransactionConversionError) -> Self { + MorphEthApiError::Eth(err.into()) + } +} + +impl From for MorphEthApiError { + fn from(err: Infallible) -> Self { + match err {} + } +} + diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs new file mode 100644 index 0000000..705c483 --- /dev/null +++ b/crates/rpc/src/eth/api.rs @@ -0,0 +1,308 @@ +//! Morph `eth_` API wrapper. + +use crate::MorphEthApiError; +use morph_chainspec::MorphChainSpec; +use morph_evm::MorphEvmConfig; +use morph_primitives::MorphPrimitives; +use reth_provider::ChainSpecProvider; +use reth_rpc::EthApi; +use reth_rpc_convert::RpcConvert; +use reth_rpc_eth_api::{ + helpers::{ + pending_block::PendingEnvBuilder, EthApiSpec, EthBlocks, EthFees, EthState, + EthTransactions, LoadBlock, LoadFee, LoadPendingBlock, LoadReceipt, LoadState, + LoadTransaction, SpawnBlocking, Trace, + }, + EthApiTypes, RpcNodeCore, RpcNodeCoreExt, +}; +use reth_rpc_eth_types::{EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle}; +use reth_tasks::{ + pool::{BlockingTaskGuard, BlockingTaskPool}, + TaskSpawner, +}; +use std::{fmt, sync::Arc, time::Duration}; + +/// Adapter for [`EthApi`], which holds all the data required to serve core `eth_` API. +pub type EthApiNodeBackend = EthApi; + +/// A helper trait with requirements for [`RpcNodeCore`] to be used in [`MorphEthApi`]. +pub trait MorphNodeCore: RpcNodeCore {} +impl MorphNodeCore for T where T: RpcNodeCore {} + +/// Morph `Eth` API implementation. +/// +/// This wraps a default `Eth` implementation, and provides additional functionality +/// where the Morph spec deviates from the default (ethereum) spec, e.g. L1 fee +/// and ERC20 fee token support in `eth_estimateGas`. +pub struct MorphEthApi { + /// Gateway to node's core components. + inner: Arc>, +} + +impl Clone for MorphEthApi { + fn clone(&self) -> Self { + Self { inner: self.inner.clone() } + } +} + +impl MorphEthApi { + /// Creates a new [`MorphEthApi`]. + pub fn new(eth_api: EthApiNodeBackend) -> Self { + let inner = Arc::new(MorphEthApiInner { eth_api }); + Self { inner } + } + + /// Returns a reference to the [`EthApiNodeBackend`]. + pub fn eth_api(&self) -> &EthApiNodeBackend { + self.inner.eth_api() + } +} + +impl EthApiTypes for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + type Error = MorphEthApiError; + type NetworkTypes = Rpc::Network; + type RpcConvert = Rpc; + + fn converter(&self) -> &Self::RpcConvert { + self.inner.eth_api.converter() + } +} + +impl RpcNodeCore for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + type Primitives = N::Primitives; + type Provider = N::Provider; + type Pool = N::Pool; + type Evm = ::Evm; + type Network = ::Network; + + #[inline] + fn pool(&self) -> &Self::Pool { + self.inner.eth_api.pool() + } + + #[inline] + fn evm_config(&self) -> &Self::Evm { + self.inner.eth_api.evm_config() + } + + #[inline] + fn network(&self) -> &Self::Network { + self.inner.eth_api.network() + } + + #[inline] + fn provider(&self) -> &Self::Provider { + self.inner.eth_api.provider() + } +} + +impl RpcNodeCoreExt for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + #[inline] + fn cache(&self) -> &EthStateCache { + self.inner.eth_api.cache() + } +} + +impl EthApiSpec for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + #[inline] + fn starting_block(&self) -> alloy_primitives::U256 { + self.inner.eth_api.starting_block() + } +} + +impl SpawnBlocking for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + #[inline] + fn io_task_spawner(&self) -> impl TaskSpawner { + self.inner.eth_api.task_spawner() + } + + #[inline] + fn tracing_task_pool(&self) -> &BlockingTaskPool { + self.inner.eth_api.blocking_task_pool() + } + + #[inline] + fn tracing_task_guard(&self) -> &BlockingTaskGuard { + self.inner.eth_api.blocking_task_guard() + } + + #[inline] + fn blocking_io_task_guard(&self) -> &std::sync::Arc { + self.inner.eth_api.blocking_io_request_semaphore() + } +} + +impl LoadFee for MorphEthApi +where + N: MorphNodeCore, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, + Rpc: RpcConvert, +{ + #[inline] + fn gas_oracle(&self) -> &GasPriceOracle { + self.inner.eth_api.gas_oracle() + } + + #[inline] + fn fee_history_cache(&self) -> &FeeHistoryCache> { + self.inner.eth_api.fee_history_cache() + } +} + +impl LoadPendingBlock for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ + fn pending_block(&self) -> &tokio::sync::Mutex>> { + self.inner.eth_api.pending_block() + } + + fn pending_env_builder(&self) -> &dyn PendingEnvBuilder { + self.inner.eth_api.pending_env_builder() + } + + fn pending_block_kind(&self) -> reth_rpc_eth_types::builder::config::PendingBlockKind { + self.inner.eth_api.pending_block_kind() + } +} + +impl LoadBlock for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl LoadTransaction for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ +} + +impl LoadReceipt for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ +} + +impl EthBlocks for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl EthTransactions for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ + #[inline] + fn signers( + &self, + ) -> &reth_rpc_eth_api::helpers::spec::SignersForRpc { + self.inner.eth_api.signers() + } + + #[inline] + fn send_raw_transaction_sync_timeout(&self) -> Duration { + self.inner.eth_api.send_raw_transaction_sync_timeout() + } + + async fn send_transaction( + &self, + tx: reth_primitives_traits::WithEncoded< + reth_primitives_traits::Recovered< + reth_transaction_pool::PoolPooledTx, + >, + >, + ) -> Result { + reth_rpc_eth_api::helpers::EthTransactions::send_transaction(&self.inner.eth_api, tx) + .await + .map_err(Into::into) + } +} + +impl EthFees for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl LoadState for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + Self: LoadPendingBlock, +{ +} + +impl EthState for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + Self: LoadPendingBlock, +{ + #[inline] + fn max_proof_window(&self) -> u64 { + self.inner.eth_api.eth_proof_window() + } +} + +impl Trace for MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, + Rpc: RpcConvert, +{ +} + +impl fmt::Debug for MorphEthApi { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MorphEthApi").finish_non_exhaustive() + } +} + +/// Container type `MorphEthApi` +#[allow(missing_debug_implementations)] +pub struct MorphEthApiInner { + /// Gateway to node's core components. + pub eth_api: EthApiNodeBackend, +} + +impl MorphEthApiInner { + /// Returns a reference to the [`EthApiNodeBackend`]. + const fn eth_api(&self) -> &EthApiNodeBackend { + &self.eth_api + } +} diff --git a/crates/rpc/src/eth/call.rs b/crates/rpc/src/eth/call.rs new file mode 100644 index 0000000..5e12116 --- /dev/null +++ b/crates/rpc/src/eth/call.rs @@ -0,0 +1,232 @@ +//! Morph `eth_call` and `eth_estimateGas` overrides. + +use crate::eth::api::{MorphEthApi, MorphNodeCore}; +use crate::MorphEthApiError; +use alloy_primitives::U256; +use morph_chainspec::{MorphChainSpec, MorphHardforks}; +use morph_revm::{L1BlockInfo, MorphTxExt, TokenFeeInfo}; +use reth_evm::{EvmEnvFor, TxEnvFor}; +use reth_provider::ChainSpecProvider; +use reth_rpc_eth_api::{ + helpers::{estimate::EstimateCall, Call, EthCall}, + EthApiTypes, RpcNodeCore, +}; +use reth_rpc_eth_types::{error::FromEthApiError, EthApiError}; +use revm::{context::Transaction as RevmTransaction, Database}; + +const ERR_INSUFFICIENT_FUNDS_FOR_L1_FEE: &str = "insufficient funds for l1 fee"; +const ERR_INSUFFICIENT_FUNDS_FOR_TRANSFER: &str = "insufficient funds for transfer"; +const ERR_INVALID_TOKEN: &str = "invalid token"; + +impl EthCall for MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + Rpc: reth_rpc_convert::RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl Call for MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + Rpc: reth_rpc_convert::RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ + fn call_gas_limit(&self) -> u64 { + self.eth_api().gas_cap() + } + + fn max_simulate_blocks(&self) -> u64 { + self.eth_api().max_simulate_blocks() + } + + fn evm_memory_limit(&self) -> u64 { + self.eth_api().evm_memory_limit() + } + + fn caller_gas_allowance( + &self, + mut db: impl Database>, + evm_env: &EvmEnvFor< as RpcNodeCore>::Evm>, + tx_env: &TxEnvFor< as RpcNodeCore>::Evm>, + ) -> Result::Error> { + let caller = tx_env.caller(); + let balance = db + .basic(caller) + .map_err(Into::::into) + .map_err(::from_eth_err)? + .map(|acc| acc.balance) + .unwrap_or_default(); + + let value = tx_env.value(); + if value > balance { + return Err(::from_eth_err( + EthApiError::InvalidParams(ERR_INSUFFICIENT_FUNDS_FOR_TRANSFER.to_string()), + )); + } + + let (hardfork, l1_fee) = self + .estimate_l1_fee(&mut db, evm_env, tx_env) + .map_err(::from_eth_err)?; + + if let Some(fee_token_id) = tx_env.fee_token_id.filter(|id| *id > 0) { + return self.caller_gas_allowance_with_token( + &mut db, + caller, + balance, + value, + hardfork, + l1_fee, + fee_token_id, + tx_env.fee_limit, + tx_env.gas_price(), + ); + } + + caller_gas_allowance_with_eth(balance, value, l1_fee, tx_env.gas_price()) + .map_err(::from_eth_err) + } +} + +impl MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + Rpc: reth_rpc_convert::RpcConvert, +{ + fn estimate_l1_fee( + &self, + db: &mut DB, + evm_env: &EvmEnvFor< as RpcNodeCore>::Evm>, + tx_env: &TxEnvFor< as RpcNodeCore>::Evm>, + ) -> Result<(morph_chainspec::MorphHardfork, U256), EthApiError> + where + DB: Database, + DB::Error: Into, + { + let block_number = u64::try_from(evm_env.block_env.number) + .map_err(|_| EthApiError::InvalidParams("invalid block number".to_string()))?; + let timestamp = u64::try_from(evm_env.block_env.timestamp) + .map_err(|_| EthApiError::InvalidParams("invalid block timestamp".to_string()))?; + let chain_spec = self.provider().chain_spec(); + let hardfork = chain_spec.morph_hardfork_at(block_number, timestamp); + + if tx_env.is_l1_msg() { + return Ok((hardfork, U256::ZERO)); + } + + let rlp_bytes = tx_env.rlp_bytes.as_ref().ok_or_else(|| { + EthApiError::InvalidParams("missing rlp bytes for l1 fee".to_string()) + })?; + + let l1_info = L1BlockInfo::try_fetch(db, hardfork).map_err(|err| { + EthApiError::InvalidParams(format!("failed to estimate L1 data fee: {err}")) + })?; + + Ok((hardfork, l1_info.calculate_tx_l1_cost(rlp_bytes, hardfork))) + } + + fn caller_gas_allowance_with_token( + &self, + db: &mut DB, + caller: alloy_primitives::Address, + balance: U256, + value: U256, + hardfork: morph_chainspec::MorphHardfork, + l1_fee: U256, + fee_token_id: u16, + fee_limit: Option, + gas_price: u128, + ) -> Result + where + DB: Database, + DB::Error: Into, + { + let token_fee_info = TokenFeeInfo::try_fetch(db, fee_token_id, caller, hardfork) + .map_err(|_| EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string())) + .map_err(::from_eth_err)? + .ok_or_else(|| EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string())) + .map_err(::from_eth_err)?; + + if !token_fee_info.is_active + || token_fee_info.price_ratio.is_zero() + || token_fee_info.scale.is_zero() + { + return Err(::from_eth_err( + EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string()), + )); + } + + let limit = match fee_limit { + Some(limit) if !limit.is_zero() => token_fee_info.balance.min(limit), + _ => token_fee_info.balance, + }; + + let l1_fee_in_token = token_fee_info.calculate_token_amount(l1_fee); + if l1_fee_in_token >= limit { + return Err(::from_eth_err( + EthApiError::InvalidParams(ERR_INSUFFICIENT_FUNDS_FOR_L1_FEE.to_string()), + )); + } + + let available_token = limit - l1_fee_in_token; + let available_eth = + token_amount_to_eth(available_token, &token_fee_info).ok_or_else(|| { + EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string()) + })?; + + caller_gas_allowance_with_eth(balance, value, U256::ZERO, gas_price) + .and_then(|allowance| { + let allowance_eth = gas_allowance_from_balance(available_eth, gas_price); + Ok(allowance.min(allowance_eth)) + }) + .map_err(::from_eth_err) + } +} + +impl EstimateCall for MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + Rpc: reth_rpc_convert::RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +fn caller_gas_allowance_with_eth( + balance: U256, + value: U256, + l1_fee: U256, + gas_price: u128, +) -> Result { + let mut available = balance.saturating_sub(value); + if l1_fee >= available { + return Err(EthApiError::InvalidParams( + ERR_INSUFFICIENT_FUNDS_FOR_L1_FEE.to_string(), + )); + } + available -= l1_fee; + Ok(gas_allowance_from_balance(available, gas_price)) +} + +fn gas_allowance_from_balance(balance: U256, gas_price: u128) -> u64 { + if gas_price == 0 { + return u64::MAX; + } + let gas_price = U256::from(gas_price); + let allowance = balance / gas_price; + if allowance > U256::from(u64::MAX) { + u64::MAX + } else { + allowance.to::() + } +} + +fn token_amount_to_eth(token_amount: U256, info: &TokenFeeInfo) -> Option { + if info.price_ratio.is_zero() || info.scale.is_zero() { + return None; + } + Some(token_amount.saturating_mul(info.price_ratio) / info.scale) +} diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs new file mode 100644 index 0000000..414b582 --- /dev/null +++ b/crates/rpc/src/eth/mod.rs @@ -0,0 +1,94 @@ +//! Morph `eth_` RPC wiring and conversions. + +use crate::eth::receipt::MorphReceiptConverter; +use crate::types::{MorphRpcReceipt, MorphRpcTransaction, MorphTransactionRequest}; +use alloy_rpc_types_eth::Header as RpcHeader; +use eyre::Result; +use morph_chainspec::MorphChainSpec; +use morph_evm::MorphEvmConfig; +use morph_primitives::{MorphHeader, MorphPrimitives}; +use reth_evm::ConfigureEvm; +use reth_node_api::{FullNodeComponents, FullNodeTypes, HeaderTy, NodeTypes}; +use reth_node_builder::rpc::{EthApiBuilder, EthApiCtx}; +use reth_provider::ChainSpecProvider; +use reth_rpc_convert::{RpcConverter, RpcTypes}; +use reth_rpc_eth_api::helpers::pending_block::BuildPendingEnv; +use std::marker::PhantomData; + +pub mod api; +pub mod call; +pub mod receipt; +pub mod transaction; + +pub use api::MorphEthApi; + +/// Morph RPC response type definitions. +#[derive(Clone, Debug, Default)] +pub struct MorphRpcTypes; + +impl RpcTypes for MorphRpcTypes { + type Header = RpcHeader; + type Receipt = MorphRpcReceipt; + type TransactionResponse = MorphRpcTransaction; + type TransactionRequest = MorphTransactionRequest; +} + +/// Morph RPC converter with custom receipt and header conversion. +pub type MorphRpcConverter = + RpcConverter::Evm, MorphReceiptConverter>; + +/// Builder for Morph `eth_` RPC that installs Morph RPC conversions. +#[derive(Debug)] +pub struct MorphEthApiBuilder { + _nt: PhantomData, +} + +impl Default for MorphEthApiBuilder { + fn default() -> Self { + Self { _nt: PhantomData } + } +} + +impl MorphEthApiBuilder { + /// Creates a new builder. + pub fn new() -> Self { + Self::default() + } +} + +impl EthApiBuilder for MorphEthApiBuilder +where + N: FullNodeComponents< + Evm = MorphEvmConfig, + Evm: ConfigureEvm>>, + Types: NodeTypes, + Provider: ChainSpecProvider, + > + crate::eth::api::MorphNodeCore + + reth_rpc_eth_api::RpcNodeCore< + Provider = ::Provider, + Pool = ::Pool, + >, + NetworkT: RpcTypes< + Header = RpcHeader, + Receipt = MorphRpcReceipt, + TransactionResponse = MorphRpcTransaction, + TransactionRequest = MorphTransactionRequest, + >, + MorphRpcConverter: reth_rpc_convert::RpcConvert< + Network = NetworkT, + Primitives = ::Primitives, + Error = reth_rpc_eth_types::EthApiError, + Evm = ::Evm, + >, +{ + type EthApi = MorphEthApi>; + + async fn build_eth_api(self, ctx: EthApiCtx<'_, N>) -> Result { + let rpc_converter = RpcConverter::new(MorphReceiptConverter::default()); + Ok(MorphEthApi::new( + ctx.eth_api_builder() + .with_rpc_converter(rpc_converter) + .build(), + )) + } +} diff --git a/crates/rpc/src/eth/receipt.rs b/crates/rpc/src/eth/receipt.rs new file mode 100644 index 0000000..4bfb7bf --- /dev/null +++ b/crates/rpc/src/eth/receipt.rs @@ -0,0 +1,135 @@ +//! Morph receipt conversion for `eth_` RPC responses. + +use crate::types::receipt::MorphRpcReceipt; +use alloy_consensus::{Receipt, Transaction, TxReceipt}; +use alloy_primitives::{Address, TxKind, U256, U64}; +use alloy_rpc_types_eth::{Log, TransactionReceipt}; +use morph_primitives::{MorphReceipt, MorphReceiptEnvelope}; +use reth_primitives_traits::NodePrimitives; +use reth_rpc_convert::transaction::{ConvertReceiptInput, ReceiptConverter}; +use std::fmt::Debug; + +/// Converter for Morph receipts. +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct MorphReceiptConverter; + +impl ReceiptConverter for MorphReceiptConverter +where + N: NodePrimitives, +{ + type RpcReceipt = MorphRpcReceipt; + type Error = reth_rpc_eth_types::EthApiError; + + fn convert_receipts( + &self, + inputs: Vec>, + ) -> Result, Self::Error> { + let mut receipts = Vec::with_capacity(inputs.len()); + for input in inputs { + receipts.push(MorphReceiptBuilder::new(input).build()); + } + Ok(receipts) + } +} + +/// Builds a [`MorphRpcReceipt`]. +#[derive(Debug)] +struct MorphReceiptBuilder { + receipt: MorphRpcReceipt, +} + +impl MorphReceiptBuilder { + fn new(input: ConvertReceiptInput<'_, N>) -> Self + where + N: NodePrimitives, + { + let ConvertReceiptInput { tx, meta, receipt, gas_used, next_log_index } = input; + + let from = tx.signer(); + let (contract_address, to) = match tx.kind() { + TxKind::Create => (Some(from.create(tx.nonce())), None), + TxKind::Call(addr) => (None, Some(Address(*addr))), + }; + + let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = + morph_fee_fields(&receipt); + + let map_logs = |receipt: Receipt| { + let Receipt { status, cumulative_gas_used, logs } = receipt; + let logs = Log::collect_for_receipt(next_log_index, meta, logs); + Receipt { status, cumulative_gas_used, logs } + }; + + let receipt_envelope = match receipt { + MorphReceipt::Legacy(receipt) => { + MorphReceiptEnvelope::Legacy(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::Eip2930(receipt) => { + MorphReceiptEnvelope::Eip2930(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::Eip1559(receipt) => { + MorphReceiptEnvelope::Eip1559(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::Eip7702(receipt) => { + MorphReceiptEnvelope::Eip7702(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::L1Msg(receipt) => { + MorphReceiptEnvelope::L1Message(map_logs(receipt).into_with_bloom()) + } + MorphReceipt::Morph(receipt) => { + MorphReceiptEnvelope::Morph(map_logs(receipt.inner).into_with_bloom()) + } + }; + + let inner = TransactionReceipt { + inner: receipt_envelope, + transaction_hash: meta.tx_hash, + transaction_index: Some(meta.index), + block_hash: Some(meta.block_hash), + block_number: Some(meta.block_number), + gas_used, + effective_gas_price: tx.effective_gas_price(meta.base_fee), + blob_gas_used: None, + blob_gas_price: None, + from, + to, + contract_address, + }; + + let receipt = MorphRpcReceipt { + inner, + l1_fee, + fee_rate, + token_scale, + fee_limit, + fee_token_id: fee_token_id.map(U64::from), + }; + + Self { receipt } + } + + fn build(self) -> MorphRpcReceipt { + self.receipt + } +} + +fn morph_fee_fields( + receipt: &MorphReceipt, +) -> (U256, Option, Option, Option, Option) { + match receipt { + MorphReceipt::Legacy(r) + | MorphReceipt::Eip2930(r) + | MorphReceipt::Eip1559(r) + | MorphReceipt::Eip7702(r) + | MorphReceipt::Morph(r) => ( + r.l1_fee, + r.fee_token_id, + r.fee_rate, + r.token_scale, + r.fee_limit, + ), + MorphReceipt::L1Msg(_) => (U256::ZERO, None, None, None, None), + } +} + diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs new file mode 100644 index 0000000..bdfaf93 --- /dev/null +++ b/crates/rpc/src/eth/transaction.rs @@ -0,0 +1,258 @@ +//! Morph transaction conversion for `eth_` RPC responses. + +use crate::types::transaction::MorphRpcTransaction; +use crate::MorphTransactionRequest; +use alloy_consensus::{ + transaction::Recovered, + EthereumTxEnvelope, SignableTransaction, Transaction, TxEip4844, +}; +use alloy_eips::eip2718::Encodable2718; +use alloy_network::TxSigner; +use alloy_primitives::{Address, Bytes, Signature, TxKind, U256, U64}; +use alloy_rpc_types_eth::{AccessList, Transaction as RpcTransaction, TransactionInfo}; +use reth_rpc_convert::{ + transaction::FromConsensusTx, SignTxRequestError, SignableTxRequest, TryIntoSimTx, + TryIntoTxEnv, +}; +use reth_rpc_eth_types::EthApiError; +use revm::context::Transaction as RevmTransaction; +use std::convert::Infallible; + +use morph_primitives::{MorphTxEnvelope, TxMorph}; +use morph_revm::{MorphBlockEnv, MorphTxEnv}; +use reth_evm::EvmEnv; + +impl FromConsensusTx for MorphRpcTransaction { + type TxInfo = TransactionInfo; + type Err = Infallible; + + fn from_consensus_tx( + tx: MorphTxEnvelope, + signer: Address, + tx_info: Self::TxInfo, + ) -> Result { + let (sender, queue_index) = match &tx { + MorphTxEnvelope::L1Msg(msg) => { + (Some(msg.sender), Some(U64::from(msg.queue_index))) + } + _ => (None, None), + }; + let fee_token_id = tx.fee_token_id().map(U64::from); + let fee_limit = tx.fee_limit(); + + let effective_gas_price = tx_info.base_fee.map(|base_fee| { + tx.effective_tip_per_gas(base_fee) + .unwrap_or_default() + .saturating_add(base_fee as u128) + }); + + let inner = RpcTransaction { + inner: Recovered::new_unchecked(tx, signer), + block_hash: tx_info.block_hash, + block_number: tx_info.block_number, + transaction_index: tx_info.index, + effective_gas_price, + }; + + Ok(MorphRpcTransaction { + inner, + sender, + queue_index, + fee_token_id, + fee_limit, + }) + } +} + +impl TryIntoSimTx for MorphTransactionRequest { + fn try_into_sim_tx( + self, + ) -> Result> { + let tx_req = self.clone(); + if let Some(fee_token_id) = tx_req.fee_token_id.filter(|id| id.to::() > 0) { + let morph_tx = build_morph_tx_from_request( + &tx_req.inner, + fee_token_id, + tx_req.fee_limit.unwrap_or_default(), + ) + .map_err(|err| alloy_consensus::error::ValueError::new(tx_req, err))?; + let signature = Signature::from_bytes_and_parity(&[0u8; 64], false); + return Ok(MorphTxEnvelope::Morph(morph_tx.into_signed(signature))); + } + + let inner = tx_req.inner.clone(); + let envelope = inner.build_typed_simulate_transaction().map_err(|err| { + err.map(|inner| Self { + inner, + fee_token_id: tx_req.fee_token_id, + fee_limit: tx_req.fee_limit, + }) + })?; + morph_envelope_from_ethereum(envelope).map_err(|err| { + alloy_consensus::error::ValueError::new(tx_req, err) + }) + } +} + +impl SignableTxRequest for MorphTransactionRequest { + async fn try_build_and_sign( + self, + signer: impl TxSigner + Send, + ) -> Result { + if let Some(fee_token_id) = self.fee_token_id.filter(|id| id.to::() > 0) { + let mut morph_tx = build_morph_tx_from_request( + &self.inner, + fee_token_id, + self.fee_limit.unwrap_or_default(), + ) + .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?; + let signature = signer.sign_transaction(&mut morph_tx).await?; + return Ok(MorphTxEnvelope::Morph(morph_tx.into_signed(signature))); + } + + let mut tx = self + .inner + .build_typed_tx() + .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?; + let signature = signer.sign_transaction(&mut tx).await?; + let signed_envelope: EthereumTxEnvelope = + EthereumTxEnvelope::new_unhashed(tx, signature).into(); + morph_envelope_from_ethereum(signed_envelope) + .map_err(|_| SignTxRequestError::InvalidTransactionRequest) + } +} + +impl TryIntoTxEnv for MorphTransactionRequest { + type Err = EthApiError; + + fn try_into_tx_env( + self, + evm_env: &EvmEnv, + ) -> Result { + let fee_token_id = self.fee_token_id; + let fee_limit = self.fee_limit; + let access_list = self.inner.access_list.clone().unwrap_or_default(); + + let inner_tx_env = self + .inner + .clone() + .try_into_tx_env(evm_env) + .map_err(EthApiError::from)?; + + let mut tx_env = MorphTxEnv::new(inner_tx_env); + tx_env.fee_token_id = match fee_token_id { + Some(id) => Some( + u16::try_from(id.to::()) + .map_err(|_| EthApiError::InvalidParams("invalid token".to_string()))?, + ), + None => None, + }; + tx_env.fee_limit = fee_limit; + if tx_env.fee_token_id.unwrap_or_default() > 0 { + tx_env.inner.tx_type = morph_primitives::MORPH_TX_TYPE_ID; + } + + let rlp_bytes = if tx_env.fee_token_id.unwrap_or_default() > 0 || tx_env.fee_limit.is_some() + { + let fee_token_id = U64::from(tx_env.fee_token_id.unwrap_or_default()); + let fee_limit = tx_env.fee_limit.unwrap_or_default(); + let morph_tx = build_morph_tx_from_env( + &tx_env, + fee_token_id, + fee_limit, + access_list, + evm_env, + )?; + encode_2718(morph_tx) + } else { + let envelope = self + .inner + .build_typed_simulate_transaction() + .map_err(|err| EthApiError::InvalidParams(err.to_string()))?; + encode_2718(envelope) + }; + + tx_env.rlp_bytes = Some(rlp_bytes); + Ok(tx_env) + } +} + +fn morph_envelope_from_ethereum( + env: EthereumTxEnvelope, +) -> Result { + match env { + EthereumTxEnvelope::Legacy(tx) => Ok(MorphTxEnvelope::Legacy(tx)), + EthereumTxEnvelope::Eip2930(tx) => Ok(MorphTxEnvelope::Eip2930(tx)), + EthereumTxEnvelope::Eip1559(tx) => Ok(MorphTxEnvelope::Eip1559(tx)), + EthereumTxEnvelope::Eip7702(tx) => Ok(MorphTxEnvelope::Eip7702(tx)), + EthereumTxEnvelope::Eip4844(_) => Err("EIP-4844 transactions are not supported on Morph"), + } +} + + +fn build_morph_tx_from_request( + req: &alloy_rpc_types_eth::TransactionRequest, + fee_token_id: U64, + fee_limit: U256, +) -> Result { + let chain_id = req.chain_id.ok_or("missing chain_id for morph transaction")?; + let fee_token_id = + u16::try_from(fee_token_id.to::()).map_err(|_| "invalid token")?; + let gas_limit = req.gas.unwrap_or_default() as u128; + let nonce = req.nonce.unwrap_or_default(); + let max_fee_per_gas = req.max_fee_per_gas.or(req.gas_price).unwrap_or_default(); + let max_priority_fee_per_gas = req.max_priority_fee_per_gas.unwrap_or_default(); + let access_list: AccessList = req.access_list.clone().unwrap_or_default(); + let input = req.input.clone().into_input().unwrap_or_default(); + let to = req.to.unwrap_or(TxKind::Create); + + Ok(TxMorph { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value: req.value.unwrap_or_default(), + access_list, + input, + fee_token_id, + fee_limit, + }) +} + +fn build_morph_tx_from_env( + tx_env: &MorphTxEnv, + fee_token_id: U64, + fee_limit: U256, + access_list: AccessList, + evm_env: &EvmEnv, +) -> Result { + let fee_token_id = u16::try_from(fee_token_id.to::()) + .map_err(|_| EthApiError::InvalidParams("invalid token".to_string()))?; + let chain_id = tx_env.chain_id().unwrap_or_else(|| evm_env.cfg_env.chain_id); + let input = tx_env.input().clone(); + let to = tx_env.kind(); + let max_fee_per_gas = tx_env.max_fee_per_gas(); + let max_priority_fee_per_gas = tx_env.max_priority_fee_per_gas().unwrap_or_default(); + + Ok(TxMorph { + chain_id, + nonce: tx_env.nonce(), + gas_limit: tx_env.gas_limit() as u128, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value: tx_env.value(), + access_list, + input, + fee_token_id, + fee_limit, + }) +} + +fn encode_2718(tx: T) -> Bytes { + let mut out = Vec::with_capacity(tx.encode_2718_len()); + tx.encode_2718(&mut out); + Bytes::from(out) +} diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs new file mode 100644 index 0000000..eb9cb3a --- /dev/null +++ b/crates/rpc/src/lib.rs @@ -0,0 +1,11 @@ +//! Morph RPC implementation and type conversions. + +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub mod error; +pub mod eth; +pub mod types; + +pub use error::MorphEthApiError; +pub use eth::{MorphEthApi, MorphEthApiBuilder, MorphRpcConverter, MorphRpcTypes}; +pub use types::*; diff --git a/crates/rpc/src/types/mod.rs b/crates/rpc/src/types/mod.rs new file mode 100644 index 0000000..e34391b --- /dev/null +++ b/crates/rpc/src/types/mod.rs @@ -0,0 +1,9 @@ +//! Morph-specific RPC types. + +pub mod receipt; +pub mod request; +pub mod transaction; + +pub use receipt::MorphRpcReceipt; +pub use request::MorphTransactionRequest; +pub use transaction::MorphRpcTransaction; diff --git a/crates/rpc/src/types/receipt.rs b/crates/rpc/src/types/receipt.rs new file mode 100644 index 0000000..58c0fe4 --- /dev/null +++ b/crates/rpc/src/types/receipt.rs @@ -0,0 +1,98 @@ +//! Morph RPC receipt type. + +use alloy_network::ReceiptResponse; +use alloy_primitives::{Address, BlockHash, U64, U256}; +use alloy_rpc_types_eth::{Log, TransactionReceipt}; +use morph_primitives::MorphReceiptEnvelope; +use serde::{Deserialize, Serialize}; + +/// Morph RPC transaction receipt representation. +/// +/// Wraps the standard RPC transaction receipt and adds Morph-specific fields: +/// - L1 fee and fee token metadata +/// - Custom tx type for L1 message / Morph tx receipts +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MorphRpcReceipt { + /// Standard RPC receipt fields. + #[serde(flatten)] + pub inner: TransactionReceipt>, + + /// L1 data fee paid (in wei). + #[serde(rename = "l1Fee")] + pub l1_fee: U256, + + /// Fee rate used for token fee calculation. + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_rate: Option, + + /// Token scale factor. + #[serde(skip_serializing_if = "Option::is_none")] + pub token_scale: Option, + + /// Fee limit specified in the transaction. + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, + + /// Token ID used for fee payment. + #[serde(rename = "feeTokenID", skip_serializing_if = "Option::is_none")] + pub fee_token_id: Option, +} + +impl ReceiptResponse for MorphRpcReceipt { + fn contract_address(&self) -> Option
{ + self.inner.contract_address + } + + fn status(&self) -> bool { + self.inner.inner.status() + } + + fn block_hash(&self) -> Option { + self.inner.block_hash + } + + fn block_number(&self) -> Option { + self.inner.block_number + } + + fn transaction_hash(&self) -> alloy_primitives::B256 { + self.inner.transaction_hash + } + + fn transaction_index(&self) -> Option { + self.inner.transaction_index() + } + + fn gas_used(&self) -> u64 { + self.inner.gas_used() + } + + fn effective_gas_price(&self) -> u128 { + self.inner.effective_gas_price() + } + + fn blob_gas_used(&self) -> Option { + self.inner.blob_gas_used() + } + + fn blob_gas_price(&self) -> Option { + self.inner.blob_gas_price() + } + + fn from(&self) -> Address { + self.inner.from() + } + + fn to(&self) -> Option
{ + self.inner.to() + } + + fn cumulative_gas_used(&self) -> u64 { + self.inner.cumulative_gas_used() + } + + fn state_root(&self) -> Option { + self.inner.state_root() + } +} diff --git a/crates/rpc/src/types/request.rs b/crates/rpc/src/types/request.rs new file mode 100644 index 0000000..9321c07 --- /dev/null +++ b/crates/rpc/src/types/request.rs @@ -0,0 +1,66 @@ +//! Morph RPC transaction request type. + +use alloy_primitives::{U64, U256}; +use alloy_rpc_types_eth::TransactionRequest; +use serde::{Deserialize, Serialize}; + +/// Morph RPC transaction request representation. +/// +/// Extends standard Ethereum transaction request with: +/// - `feeTokenID`: Token ID for ERC20 gas payment +/// - `feeLimit`: Maximum token amount willing to pay for fees +#[derive( + Debug, + Clone, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + derive_more::Deref, + derive_more::DerefMut, +)] +#[serde(rename_all = "camelCase")] +pub struct MorphTransactionRequest { + /// Inner [`TransactionRequest`]. + #[serde(flatten)] + #[deref] + #[deref_mut] + pub inner: TransactionRequest, + + /// Token ID for fee payment (only for MorphTx type 0x7F). + #[serde(rename = "feeTokenID", default, skip_serializing_if = "Option::is_none")] + pub fee_token_id: Option, + + /// Maximum token amount willing to pay for fees (only for MorphTx type 0x7F). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, +} + +impl AsRef for MorphTransactionRequest { + fn as_ref(&self) -> &TransactionRequest { + &self.inner + } +} + +impl AsMut for MorphTransactionRequest { + fn as_mut(&mut self) -> &mut TransactionRequest { + &mut self.inner + } +} + +impl From for MorphTransactionRequest { + fn from(value: TransactionRequest) -> Self { + Self { + inner: value, + fee_token_id: None, + fee_limit: None, + } + } +} + +impl From for TransactionRequest { + fn from(value: MorphTransactionRequest) -> Self { + value.inner + } +} diff --git a/crates/rpc/src/types/transaction.rs b/crates/rpc/src/types/transaction.rs new file mode 100644 index 0000000..9767b40 --- /dev/null +++ b/crates/rpc/src/types/transaction.rs @@ -0,0 +1,154 @@ +//! Morph RPC transaction type. + +use alloy_consensus::Transaction as ConsensusTransaction; +use alloy_consensus::Transaction as TransactionTrait; +use alloy_eips::Typed2718; +use alloy_network::TransactionResponse; +use alloy_primitives::{Address, BlockHash, TxKind, U64, U256}; +use alloy_rpc_types_eth::Transaction as RpcTransaction; +use morph_primitives::MorphTxEnvelope; +use serde::{Deserialize, Serialize}; + +/// Morph RPC transaction representation. +/// +/// Wraps the standard RPC transaction and adds Morph-specific fields: +/// - L1 message sender/queue index +/// - Morph fee token fields +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Serialize, + Deserialize, + derive_more::Deref, + derive_more::DerefMut, +)] +#[serde(rename_all = "camelCase")] +pub struct MorphRpcTransaction { + /// Standard RPC transaction fields. + #[serde(flatten)] + #[deref] + #[deref_mut] + pub inner: RpcTransaction, + + /// L1 message sender (only for L1Message type 0x7E). + #[serde(skip_serializing_if = "Option::is_none")] + pub sender: Option
, + + /// L1 message queue index (only for L1Message type 0x7E). + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_index: Option, + + /// Token ID for fee payment (only for MorphTx type 0x7F). + #[serde(rename = "feeTokenID", skip_serializing_if = "Option::is_none")] + pub fee_token_id: Option, + + /// Maximum token amount willing to pay for fees (only for MorphTx type 0x7F). + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, +} + +impl Typed2718 for MorphRpcTransaction { + fn ty(&self) -> u8 { + self.inner.ty() + } +} + +impl ConsensusTransaction for MorphRpcTransaction { + fn chain_id(&self) -> Option { + self.inner.chain_id() + } + + fn nonce(&self) -> u64 { + self.inner.nonce() + } + + fn gas_limit(&self) -> u64 { + self.inner.gas_limit() + } + + fn gas_price(&self) -> Option { + TransactionTrait::gas_price(&self.inner) + } + + fn max_fee_per_gas(&self) -> u128 { + TransactionTrait::max_fee_per_gas(&self.inner) + } + + fn max_priority_fee_per_gas(&self) -> Option { + self.inner.max_priority_fee_per_gas() + } + + fn max_fee_per_blob_gas(&self) -> Option { + self.inner.max_fee_per_blob_gas() + } + + fn priority_fee_or_price(&self) -> u128 { + self.inner.priority_fee_or_price() + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + self.inner.effective_gas_price(base_fee) + } + + fn is_dynamic_fee(&self) -> bool { + self.inner.is_dynamic_fee() + } + + fn kind(&self) -> TxKind { + self.inner.kind() + } + + fn is_create(&self) -> bool { + self.inner.is_create() + } + + fn to(&self) -> Option
{ + self.inner.to() + } + + fn value(&self) -> U256 { + self.inner.value() + } + + fn input(&self) -> &alloy_primitives::Bytes { + self.inner.input() + } + + fn access_list(&self) -> Option<&alloy_eips::eip2930::AccessList> { + self.inner.access_list() + } + + fn blob_versioned_hashes(&self) -> Option<&[alloy_primitives::B256]> { + self.inner.blob_versioned_hashes() + } + + fn authorization_list( + &self, + ) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> { + self.inner.authorization_list() + } +} + +impl TransactionResponse for MorphRpcTransaction { + fn tx_hash(&self) -> alloy_primitives::B256 { + self.inner.tx_hash() + } + + fn block_hash(&self) -> Option { + self.inner.block_hash() + } + + fn block_number(&self) -> Option { + self.inner.block_number() + } + + fn transaction_index(&self) -> Option { + self.inner.transaction_index() + } + + fn from(&self) -> Address { + self.inner.from() + } +} From 10971195e10a601a95b534d42030d8cf360e0202 Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 3 Feb 2026 11:20:37 +0800 Subject: [PATCH 10/19] refactor: merge main --- Cargo.lock | 139 --------------------------------------- crates/txpool/Cargo.toml | 42 ++++++++++++ 2 files changed, 42 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62cfab3..eb56fa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,6 @@ dependencies = [ "alloy-trie", "alloy-tx-macros", "arbitrary", - "arbitrary", "auto_impl", "borsh", "c-kzg", @@ -140,7 +139,6 @@ dependencies = [ "alloy-rlp", "alloy-serde", "arbitrary", - "arbitrary", "serde", ] @@ -170,10 +168,8 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arbitrary", - "arbitrary", "crc", "rand 0.8.5", - "rand 0.8.5", "serde", "thiserror 2.0.17", ] @@ -187,10 +183,8 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arbitrary", - "arbitrary", "borsh", "rand 0.8.5", - "rand 0.8.5", "serde", ] @@ -203,11 +197,9 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arbitrary", - "arbitrary", "borsh", "k256", "rand 0.8.5", - "rand 0.8.5", "serde", "serde_with", "thiserror 2.0.17", @@ -238,7 +230,6 @@ dependencies = [ "alloy-rlp", "alloy-serde", "arbitrary", - "arbitrary", "auto_impl", "borsh", "c-kzg", @@ -376,7 +367,6 @@ checksum = "7db950a29746be9e2f2c6288c8bd7a6202a81f999ce109a2933d2379970ec0fa" dependencies = [ "alloy-rlp", "arbitrary", - "arbitrary", "bytes", "cfg-if", "const-hex", @@ -391,7 +381,6 @@ dependencies = [ "paste", "proptest", "proptest-derive 0.6.0", - "proptest-derive 0.6.0", "rand 0.9.2", "rapidhash", "ruint", @@ -630,7 +619,6 @@ dependencies = [ "alloy-serde", "alloy-sol-types", "arbitrary", - "arbitrary", "itertools 0.14.0", "serde", "serde_json", @@ -687,7 +675,6 @@ checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", "arbitrary", - "arbitrary", "serde", "serde_json", ] @@ -881,16 +868,12 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arbitrary", - "arbitrary", "arrayvec", "derive_arbitrary", - "derive_arbitrary", "derive_more", "nybbles", "proptest", "proptest-derive 0.5.1", - "proptest", - "proptest-derive 0.5.1", "serde", "smallvec", "tracing", @@ -996,15 +979,6 @@ dependencies = [ "derive_arbitrary", ] -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -1705,7 +1679,6 @@ version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" dependencies = [ - "arbitrary", "arbitrary", "blst", "cc", @@ -2380,17 +2353,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -3177,12 +3139,6 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" -[[package]] -name = "hash-db" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" - [[package]] name = "hashbrown" version = "0.12.3" @@ -3688,7 +3644,6 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ - "arbitrary", "arbitrary", "equivalent", "hashbrown 0.16.1", @@ -4951,7 +4906,6 @@ checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" dependencies = [ "alloy-rlp", "arbitrary", - "arbitrary", "cfg-if", "proptest", "ruint", @@ -5002,7 +4956,6 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-serde", "arbitrary", - "arbitrary", "derive_more", "serde", "serde_with", @@ -5218,7 +5171,6 @@ version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ - "arbitrary", "arbitrary", "arrayvec", "bitvec", @@ -5418,15 +5370,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "plain_hasher" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e19e6491bdde87c2c43d70f4c194bc8a758f2eb732df00f61e43f7362e3b4cc" -dependencies = [ - "crunchy", -] - [[package]] name = "polyval" version = "0.6.2" @@ -5636,38 +5579,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "proptest-arbitrary-interop" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1981e49bd2432249da8b0e11e5557099a8e74690d6b94e721f7dc0bb7f3555f" -dependencies = [ - "arbitrary", - "proptest", -] - -[[package]] -name = "proptest-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "proptest-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "prost" version = "0.14.1" @@ -6096,8 +6007,6 @@ dependencies = [ "alloy-primitives", "alloy-signer", "alloy-signer-local", - "alloy-signer", - "alloy-signer-local", "derive_more", "metrics", "parking_lot", @@ -6191,7 +6100,6 @@ dependencies = [ "alloy-primitives", "alloy-trie", "arbitrary", - "arbitrary", "bytes", "modular-bitfield", "op-alloy-consensus", @@ -6199,7 +6107,6 @@ dependencies = [ "reth-zstd-compressors", "serde", "visibility", - "visibility", ] [[package]] @@ -6290,7 +6197,6 @@ dependencies = [ "metrics", "page_size", "parking_lot", - "parking_lot", "reth-db-api", "reth-fs-util", "reth-libmdbx", @@ -6303,7 +6209,6 @@ dependencies = [ "strum", "sysinfo", "tempfile", - "tempfile", "thiserror 2.0.17", ] @@ -6316,19 +6221,16 @@ dependencies = [ "alloy-genesis", "alloy-primitives", "arbitrary", - "arbitrary", "bytes", "derive_more", "metrics", "modular-bitfield", "parity-scale-codec", "proptest", - "proptest", "reth-codecs", "reth-db-models", "reth-ethereum-primitives", "reth-optimism-primitives", - "reth-optimism-primitives", "reth-primitives-traits", "reth-prune-types", "reth-stages-types", @@ -6376,7 +6278,6 @@ dependencies = [ "alloy-eips", "alloy-primitives", "arbitrary", - "arbitrary", "bytes", "modular-bitfield", "reth-codecs", @@ -6819,7 +6720,6 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-serde", "arbitrary", - "arbitrary", "modular-bitfield", "reth-codecs", "reth-primitives-traits", @@ -7474,21 +7374,6 @@ dependencies = [ "serde_with", ] -[[package]] -name = "reth-optimism-primitives" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "op-alloy-consensus", - "reth-primitives-traits", - "serde", - "serde_with", -] - [[package]] name = "reth-payload-builder" version = "1.9.3" @@ -7568,7 +7453,6 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-trie", "arbitrary", - "arbitrary", "auto_impl", "byteorder", "bytes", @@ -7578,8 +7462,6 @@ dependencies = [ "op-alloy-consensus", "proptest", "proptest-arbitrary-interop", - "proptest", - "proptest-arbitrary-interop", "rayon", "reth-codecs", "revm-bytecode", @@ -7614,7 +7496,6 @@ dependencies = [ "reth-db-api", "reth-errors", "reth-ethereum-engine-primitives", - "reth-ethereum-engine-primitives", "reth-ethereum-primitives", "reth-execution-types", "reth-fs-util", @@ -7631,7 +7512,6 @@ dependencies = [ "reth-trie-db", "revm-database", "revm-state", - "revm-state", "strum", "tokio", "tracing", @@ -7672,7 +7552,6 @@ source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab6077 dependencies = [ "alloy-primitives", "arbitrary", - "arbitrary", "derive_more", "modular-bitfield", "reth-codecs", @@ -8095,7 +7974,6 @@ source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab6077 dependencies = [ "alloy-primitives", "arbitrary", - "arbitrary", "bytes", "modular-bitfield", "reth-codecs", @@ -8298,7 +8176,6 @@ dependencies = [ "revm-database", "tracing", "triehash", - "triehash", ] [[package]] @@ -8313,16 +8190,13 @@ dependencies = [ "alloy-serde", "alloy-trie", "arbitrary", - "arbitrary", "arrayvec", "bytes", "derive_more", "hash-db", - "hash-db", "itertools 0.14.0", "nybbles", "plain_hasher", - "plain_hasher", "rayon", "reth-codecs", "reth-primitives-traits", @@ -8743,7 +8617,6 @@ checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "alloy-rlp", "arbitrary", - "arbitrary", "ark-ff 0.3.0", "ark-ff 0.4.2", "ark-ff 0.5.0", @@ -9355,7 +9228,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ - "arbitrary", "arbitrary", "serde", ] @@ -10338,17 +10210,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "visibility" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/crates/txpool/Cargo.toml b/crates/txpool/Cargo.toml index e69de29..0ebfd7a 100644 --- a/crates/txpool/Cargo.toml +++ b/crates/txpool/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "morph-txpool" +description = "Morph L2 transaction pool" + +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# Morph +morph-chainspec.workspace = true +morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } +morph-revm.workspace = true + +# Reth +reth-chainspec.workspace = true +reth-primitives-traits.workspace = true +reth-provider.workspace = true +reth-revm.workspace = true +reth-storage-api.workspace = true +reth-transaction-pool.workspace = true + +# Alloy +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-evm.workspace = true +alloy-primitives.workspace = true + +# Misc +c-kzg = "2.1.5" +derive_more.workspace = true +futures.workspace = true +parking_lot.workspace = true +tracing.workspace = true + +[dev-dependencies] +reth-provider = { workspace = true, features = ["test-utils"] } From 566e3615ac809cfe395a5898e69f333568f54f20 Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 3 Feb 2026 17:15:39 +0800 Subject: [PATCH 11/19] refactor: change tokenfee info fetch --- Cargo.lock | 1 + crates/revm/src/handler.rs | 10 +- crates/revm/src/lib.rs | 5 +- crates/revm/src/token_fee.rs | 357 +++++++++++++---------- crates/rpc/Cargo.toml | 3 + crates/rpc/src/error.rs | 52 ++++ crates/rpc/src/eth/api.rs | 8 +- crates/rpc/src/eth/call.rs | 86 +++--- crates/txpool/src/morph_tx_validation.rs | 2 +- 9 files changed, 313 insertions(+), 211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb56fa9..f6897d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4658,6 +4658,7 @@ dependencies = [ "serde", "thiserror 2.0.17", "tokio", + "tracing", ] [[package]] diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 1fa80ae..b649af0 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -18,7 +18,7 @@ use crate::{ error::MorphHaltReason, evm::MorphContext, l1block::L1BlockInfo, - token_fee::{TokenFeeInfo, get_mapping_account_slot}, + token_fee::{TokenFeeInfo, mapping_slot_for}, tx::MorphTxExt, }; @@ -330,7 +330,7 @@ where // Fetch token fee info from Token Registry let spec = evm.ctx_ref().cfg().spec(); let token_fee_info = - TokenFeeInfo::try_fetch(evm.ctx_mut().db_mut(), token_id, caller, spec)? + TokenFeeInfo::fetch(evm.ctx_mut().db_mut(), token_id, caller, spec)? .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; // Check if token is active @@ -391,7 +391,7 @@ where // Fetch token fee info from Token Registry let token_fee_info = - TokenFeeInfo::try_fetch(journal.db_mut(), token_id, caller_addr, hardfork)? + TokenFeeInfo::fetch(journal.db_mut(), token_id, caller_addr, hardfork)? .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; // Check if token is active @@ -526,7 +526,7 @@ where DB: alloy_evm::Database, { // Sub amount - let from_storage_slot = get_mapping_account_slot(token_balance_slot, from); + let from_storage_slot = mapping_slot_for(token_balance_slot, from); let balance = journal.sload(token, from_storage_slot)?; journal.sstore( token, @@ -535,7 +535,7 @@ where )?; // Add amount - let to_storage_slot = get_mapping_account_slot(token_balance_slot, to); + let to_storage_slot = mapping_slot_for(token_balance_slot, to); let balance = journal.sload(token, to_storage_slot)?; journal.sstore(token, to_storage_slot, balance.saturating_add(token_amount))?; Ok((from_storage_slot, to_storage_slot)) diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 222dcdb..112e7e4 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -77,5 +77,8 @@ pub use l1block::{ L1BlockInfo, }; pub use precompiles::MorphPrecompiles; -pub use token_fee::{L2_TOKEN_REGISTRY_ADDRESS, TokenFeeInfo, get_erc20_balance_with_evm}; +pub use token_fee::{ + L2_TOKEN_REGISTRY_ADDRESS, TokenFeeInfo, encode_balance_of, erc20_balance_of, mapping_slot, + mapping_slot_for, +}; pub use tx::{MorphTxEnv, MorphTxExt}; diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index e6d9122..f77f30a 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -9,7 +9,7 @@ use alloy_evm::Database; use alloy_primitives::{Address, Bytes, U256, address, keccak256}; use morph_chainspec::hardfork::MorphHardfork; use revm::SystemCallEvm; -use revm::{Inspector, context_interface::result::EVMError, inspector::NoOpInspector}; +use revm::{Database as RevmDatabase, Inspector, context_interface::result::EVMError, inspector::NoOpInspector}; use crate::evm::MorphContext; use crate::{MorphEvm, MorphInvalidTransaction}; @@ -47,87 +47,74 @@ pub struct TokenFeeInfo { pub balance_slot: Option, } +#[derive(Clone, Debug)] +struct TokenRegistryEntry { + token_address: Address, + is_active: bool, + decimals: u8, + price_ratio: U256, + scale: U256, + balance_slot: Option, +} + impl TokenFeeInfo { - /// Try to fetch the token fee information from the database. + /// Fetch token fee information with EVM call fallback. /// - /// This reads the token parameters from the L2 Token Registry contract storage. + /// Reads token parameters from L2 Token Registry storage. If the token's + /// balance slot is unknown, falls back to an EVM `balanceOf` call. /// Returns `None` if the token is not registered. - pub fn try_fetch( + pub fn fetch( db: &mut DB, token_id: u16, caller: Address, - spec: MorphHardfork, + hardfork: MorphHardfork, ) -> 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()); - let token_registry_base = get_mapping_slot(TOKEN_REGISTRY_SLOT, token_id_bytes.to_vec()); - - // TokenInfo struct layout in storage (following Solidity storage packing rules): - // slot + 0: tokenAddress (address, 20 bytes) + 12 bytes padding - // slot + 1: balanceSlot (bytes32, 32 bytes) - // slot + 2: isActive (bool, 1 byte) + decimals (uint8, 1 byte) + 30 bytes padding - // slot + 3: scale (uint256, 32 bytes) - - // Read tokenAddress from slot + 0 - let slot_0 = db.storage(L2_TOKEN_REGISTRY_ADDRESS, token_registry_base)?; - let token_address = Address::from_word(slot_0.into()); - if token_address == Address::default() { - return Ok(None); - } - - // Read balanceSlot from slot + 1 - let balance_slot_value = db.storage( - L2_TOKEN_REGISTRY_ADDRESS, - token_registry_base + U256::from(1), - )?; - let token_balance_slot = if !balance_slot_value.is_zero() { - Some(balance_slot_value.saturating_sub(U256::from(1u64))) - } else { - None + let entry = match load_registry_entry(db, token_id)? { + Some(e) => e, + None => return Ok(None), }; - // Read isActive and decimals from slot + 2 - // In big-endian representation, rightmost byte is the lowest position - // isActive is at the rightmost (byte 31), decimals is to its left (byte 30) - let slot_2 = db.storage( - L2_TOKEN_REGISTRY_ADDRESS, - token_registry_base + U256::from(2), - )?; - let slot_2_bytes = slot_2.to_be_bytes::<32>(); - let is_active = slot_2_bytes[31] != 0; - let decimals = slot_2_bytes[30]; - - // Read scale from slot + 3 - let scale = db.storage( - L2_TOKEN_REGISTRY_ADDRESS, - token_registry_base + U256::from(3), - )?; + let balance = + read_erc20_balance(db, entry.token_address, caller, entry.balance_slot, hardfork)?; - // Get price ratio from priceRatio mapping - let price_ratio = load_mapping_value( - db, - L2_TOKEN_REGISTRY_ADDRESS, - PRICE_RATIO_SLOT, - token_id_bytes.to_vec(), - )?; - - // Get caller's token balance - let caller_token_balance = - get_erc20_balance(db, token_address, caller, token_balance_slot, spec)?; - - let token_fee = Self { - token_address, - is_active, - decimals, - price_ratio, - scale, + Ok(Some(Self { + token_address: entry.token_address, + is_active: entry.is_active, + decimals: entry.decimals, + price_ratio: entry.price_ratio, + scale: entry.scale, caller, - balance: caller_token_balance, - balance_slot: token_balance_slot, + balance, + balance_slot: entry.balance_slot, + })) + } + + /// Fetch token fee information using storage-only reads. + /// + /// Does not execute EVM calls. If the balance slot is unknown, + /// the returned `balance` will be zero. + pub fn fetch_storage_only( + db: &mut DB, + token_id: u16, + caller: Address, + ) -> Result, DB::Error> { + let entry = match load_registry_entry(db, token_id)? { + Some(e) => e, + None => return Ok(None), }; - Ok(Some(token_fee)) + let balance = read_balance_slot(db, entry.token_address, caller, entry.balance_slot)?; + + Ok(Some(Self { + token_address: entry.token_address, + is_active: entry.is_active, + decimals: entry.decimals, + price_ratio: entry.price_ratio, + scale: entry.scale, + caller, + balance, + balance_slot: entry.balance_slot, + })) } /// Calculate the token amount required for a given ETH amount. @@ -157,84 +144,135 @@ impl TokenFeeInfo { } } -/// Calculate the storage slot for a mapping value. -/// -/// For a mapping `mapping(keyType => valueType)` at storage slot `slot_index`, -/// the value for `key` is stored at `keccak256(key || slot_index)`. -pub fn get_mapping_slot(slot_index: U256, mut key: Vec) -> U256 { - let mut pre_image = slot_index.to_be_bytes_vec(); - key.append(&mut pre_image); - let storage_key = keccak256(key); - U256::from_be_bytes(storage_key.0) +fn load_registry_entry( + db: &mut DB, + token_id: u16, +) -> 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()); + let base = mapping_slot(TOKEN_REGISTRY_SLOT, token_id_bytes.to_vec()); + + // TokenInfo struct layout in storage (Solidity packing): + // base + 0: tokenAddress (20 bytes) + padding + // base + 1: balanceSlot (32 bytes) + // base + 2: isActive (1 byte) + decimals (1 byte) + padding + // base + 3: scale (32 bytes) + + let slot_0 = db.storage(L2_TOKEN_REGISTRY_ADDRESS, base)?; + let token_address = Address::from_word(slot_0.into()); + if token_address == Address::default() { + return Ok(None); + } + + let balance_slot_raw = db.storage(L2_TOKEN_REGISTRY_ADDRESS, base + U256::from(1))?; + let balance_slot = if !balance_slot_raw.is_zero() { + Some(balance_slot_raw.saturating_sub(U256::from(1))) + } else { + None + }; + + // isActive at byte 31, decimals at byte 30 (big-endian) + let slot_2 = db.storage(L2_TOKEN_REGISTRY_ADDRESS, base + U256::from(2))?; + let bytes = slot_2.to_be_bytes::<32>(); + let is_active = bytes[31] != 0; + let decimals = bytes[30]; + + let scale = db.storage(L2_TOKEN_REGISTRY_ADDRESS, base + U256::from(3))?; + + // Get price ratio from priceRatio mapping + let price_ratio = load_mapping_value( + db, + L2_TOKEN_REGISTRY_ADDRESS, + PRICE_RATIO_SLOT, + token_id_bytes.to_vec(), + )?; + + Ok(Some(TokenRegistryEntry { + token_address, + is_active, + decimals, + price_ratio, + scale, + balance_slot, + })) } -/// Calculate the account's storage slot for a mapping value. +/// Calculate the storage slot for a Solidity mapping value. /// -/// For address-keyed mappings, the address is left-padded to 32 bytes. +/// For `mapping(keyType => valueType)` at slot `base_slot`, +/// the value for `key` is at `keccak256(key ++ base_slot)`. +pub fn mapping_slot(base_slot: U256, mut key: Vec) -> U256 { + let mut preimage = base_slot.to_be_bytes_vec(); + key.append(&mut preimage); + U256::from_be_bytes(keccak256(key).0) +} + +/// Calculate mapping slot for an address key (left-padded to 32 bytes). #[inline] -pub fn get_mapping_account_slot(slot_index: U256, account: Address) -> U256 { +pub fn mapping_slot_for(base_slot: U256, account: Address) -> U256 { let mut key = [0u8; 32]; key[12..32].copy_from_slice(account.as_slice()); - get_mapping_slot(slot_index, key.to_vec()) + mapping_slot(base_slot, key.to_vec()) } /// Load a value from a mapping in contract storage. -fn load_mapping_value( +fn load_mapping_value( db: &mut DB, - account: Address, - slot_index: U256, + contract: Address, + base_slot: U256, key: Vec, ) -> Result { - let storage_slot = get_mapping_slot(slot_index, key); - let storage_value = db.storage(account, storage_slot)?; - Ok(storage_value) + db.storage(contract, mapping_slot(base_slot, key)) } -/// Get ERC20 token balance for an account (storage-only version). +/// Read ERC20 balance with EVM call fallback. /// -/// First tries to read directly from storage if the balance slot is known. -/// If the balance slot is not available, returns zero. +/// If `balance_slot` is known, reads directly from storage. +/// Otherwise, constructs a temporary EVM to call `balanceOf(address)`. +fn read_erc20_balance( + db: &mut DB, + token: Address, + account: Address, + balance_slot: Option, + hardfork: MorphHardfork, +) -> Result { + if balance_slot.is_some() { + return read_balance_slot(db, token, account, balance_slot); + } + + // EVM fallback: construct temporary MorphEvm for balanceOf call + let db: &mut dyn Database = db; + let mut evm = MorphEvm::new(MorphContext::new(db, hardfork), NoOpInspector {}); + + match call_balance_of(&mut evm, token, account) { + Ok(balance) => Ok(balance), + Err(EVMError::Database(e)) => Err(e), + Err(_) => Ok(U256::ZERO), // Non-DB errors → zero (safe fallback) + } +} + +/// Read ERC20 balance directly from storage slot. /// -/// Use [`get_erc20_balance_with_evm`] if you have access to a `MorphEvm` instance -/// and need the EVM call fallback. -pub fn get_erc20_balance( +/// Returns zero if `balance_slot` is `None`. +fn read_balance_slot( db: &mut DB, token: Address, account: Address, - token_balance_slot: Option, - spec: MorphHardfork, + balance_slot: Option, ) -> Result { - // If balance slot is provided, read directly from storage - if let Some(slot) = token_balance_slot { - let mut data = [0u8; 32]; - data[12..32].copy_from_slice(account.as_slice()); - load_mapping_value(db, token, slot, data.to_vec()) - } else { - // For the EVM fallback we construct a temporary MorphEvm instance. - // - // Notes: - // - `MorphContext::new` requires a hardfork/spec parameter. - // - We pass `&mut DB` as the context database type (so we don't move `db`). - // - `NoOpInspector` satisfies the `Inspector` bound without adding side effects. - let db: &mut dyn Database = db; - - let mut evm = MorphEvm::new(MorphContext::new(db, spec), NoOpInspector {}); - - match get_erc20_balance_with_evm(&mut evm, token, account) { - Ok(balance) => Ok(balance), - Err(EVMError::Database(db_err)) => Err(db_err), - // For non-database EVM errors, fall back to zero (matches original behavior). - Err(_) => Ok(U256::ZERO), + match balance_slot { + Some(slot) => { + let mut key = [0u8; 32]; + key[12..32].copy_from_slice(account.as_slice()); + load_mapping_value(db, token, slot, key.to_vec()) } + None => Ok(U256::ZERO), } } -/// Get ERC20 token balance for an account with EVM call fallback. -/// -/// First tries to read directly from storage if the balance slot is known. -/// If the balance slot is not available, falls back to executing an EVM call -/// to `balanceOf(address)` using the provided `MorphEvm` instance. -pub fn get_erc20_balance_with_evm( +/// Execute EVM `balanceOf(address)` call. +fn call_balance_of( evm: &mut MorphEvm, token: Address, account: Address, @@ -243,37 +281,46 @@ where DB: Database, I: Inspector>, { - // Fallback: Execute EVM call to balanceOf(address) - let calldata = build_balance_of_calldata(account); + let calldata = encode_balance_of(account); match evm.system_call_one(token, calldata) { - Ok(result) => { - if result.is_success() { - // Parse the returned balance (32 bytes) - if let Some(output) = result.output() - && output.len() >= 32 - { - return Ok(U256::from_be_slice(&output[..32])); - } + Ok(result) if result.is_success() => { + if let Some(output) = result.output() + && output.len() >= 32 + { + return Ok(U256::from_be_slice(&output[..32])); } Ok(U256::ZERO) } - Err(_) => { - // On error, return zero (matches original behavior) - Ok(U256::ZERO) - } + Ok(_) => Ok(U256::ZERO), + Err(_) => Ok(U256::ZERO), } } -/// Build the calldata for ERC20 balanceOf(address) call. +/// Query ERC20 balance via EVM call. +/// +/// Use this when you have a `MorphEvm` instance and need to call `balanceOf`. +pub fn erc20_balance_of( + evm: &mut MorphEvm, + token: Address, + account: Address, +) -> Result> +where + DB: Database, + I: Inspector>, +{ + call_balance_of(evm, token, account) +} + +/// Encode ERC20 `balanceOf(address)` calldata. /// -/// Method signature: `balanceOf(address) -> 0x70a08231` -pub fn build_balance_of_calldata(account: Address) -> Bytes { - let method_id = [0x70u8, 0xa0, 0x82, 0x31]; - let mut calldata = Vec::with_capacity(36); - calldata.extend_from_slice(&method_id); - calldata.extend_from_slice(&[0u8; 12]); // Pad address to 32 bytes - calldata.extend_from_slice(account.as_slice()); - Bytes::from(calldata) +/// Function selector: `0x70a08231` +pub fn encode_balance_of(account: Address) -> Bytes { + const SELECTOR: [u8; 4] = [0x70, 0xa0, 0x82, 0x31]; + let mut data = Vec::with_capacity(36); + data.extend_from_slice(&SELECTOR); + data.extend_from_slice(&[0u8; 12]); // Left-pad address + data.extend_from_slice(account.as_slice()); + Bytes::from(data) } #[cfg(test)] @@ -293,20 +340,20 @@ mod tests { } #[test] - fn test_get_mapping_slot() { + fn test_mapping_slot() { // Test that mapping slot calculation produces deterministic results let slot = U256::from(151); let key = vec![0u8; 32]; - let result1 = get_mapping_slot(slot, key.clone()); - let result2 = get_mapping_slot(slot, key); + let result1 = mapping_slot(slot, key.clone()); + let result2 = mapping_slot(slot, key); assert_eq!(result1, result2); } #[test] - fn test_get_mapping_account_slot() { + fn test_mapping_slot_for() { let slot = U256::from(1); let account = address!("1234567890123456789012345678901234567890"); - let result = get_mapping_account_slot(slot, account); + let result = mapping_slot_for(slot, account); // Result should be non-zero assert!(!result.is_zero()); } @@ -356,13 +403,13 @@ mod tests { } #[test] - fn test_build_balance_of_calldata() { + fn test_encode_balance_of() { let account = address!("1234567890123456789012345678901234567890"); - let calldata = build_balance_of_calldata(account); + let calldata = encode_balance_of(account); - // Should be 4 bytes method id + 32 bytes address = 36 bytes + // Should be 4 bytes selector + 32 bytes address = 36 bytes assert_eq!(calldata.len(), 36); - // First 4 bytes should be the method id + // First 4 bytes should be the selector assert_eq!(&calldata[0..4], &[0x70, 0xa0, 0x82, 0x31]); // Last 20 bytes should be the address assert_eq!(&calldata[16..36], account.as_slice()); diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 087a2be..91f32cd 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -53,6 +53,9 @@ revm.workspace = true thiserror.workspace = true eyre.workspace = true +# Logging +tracing.workspace = true + [features] default = [] diff --git a/crates/rpc/src/error.rs b/crates/rpc/src/error.rs index f6ce93f..67382f4 100644 --- a/crates/rpc/src/error.rs +++ b/crates/rpc/src/error.rs @@ -13,6 +13,29 @@ use reth_rpc_eth_types::{ use std::convert::Infallible; use thiserror::Error; +/// Extension trait for converting `Result` where `E: Into` to `Result`. +/// +/// This simplifies the common pattern of: +/// ```ignore +/// result +/// .map_err(Into::::into) +/// .map_err(::from_eth_err) +/// ``` +/// to just: +/// ```ignore +/// result.to_morph_err() +/// ``` +pub trait ToMorphErr { + /// Convert the error to `MorphEthApiError`. + fn to_morph_err(self) -> Result; +} + +impl> ToMorphErr for Result { + fn to_morph_err(self) -> Result { + self.map_err(|e| MorphEthApiError::Eth(e.into())) + } +} + /// Morph Eth API errors #[derive(Debug, Error)] pub enum MorphEthApiError { @@ -51,6 +74,19 @@ pub enum MorphEthApiError { /// Provider error #[error("provider error: {0}")] Provider(String), + + // ========== Gas estimation errors ========== + /// Insufficient funds for L1 data fee + #[error("insufficient funds for l1 fee")] + InsufficientFundsForL1Fee, + + /// Insufficient funds for value transfer + #[error("insufficient funds for transfer")] + InsufficientFundsForTransfer, + + /// Invalid fee token (not registered, inactive, or invalid configuration) + #[error("invalid token")] + InvalidFeeToken, } impl From for jsonrpsee::types::ErrorObject<'static> { @@ -97,6 +133,19 @@ impl From for jsonrpsee::types::ErrorObject<'static> { format!("Provider error: {msg}"), None::<()>, ), + MorphEthApiError::InsufficientFundsForL1Fee => jsonrpsee::types::ErrorObject::owned( + -32008, + "insufficient funds for l1 fee", + None::<()>, + ), + MorphEthApiError::InsufficientFundsForTransfer => jsonrpsee::types::ErrorObject::owned( + -32009, + "insufficient funds for transfer", + None::<()>, + ), + MorphEthApiError::InvalidFeeToken => { + jsonrpsee::types::ErrorObject::owned(-32010, "invalid token", None::<()>) + } } } } @@ -110,6 +159,9 @@ impl AsEthApiError for MorphEthApiError { } } +// Note: `FromEthApiError` is auto-implemented via blanket impl for any `T: From`. +// We get it for free since we have `#[from] EthApiError` above. + impl FromEvmHalt> for MorphEthApiError { fn from_evm_halt(halt: HaltReasonFor, gas_limit: u64) -> Self { MorphEthApiError::Eth(EthApiError::from_evm_halt(halt, gas_limit)) diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 705c483..edc9bbf 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -196,25 +196,25 @@ where { } -impl LoadTransaction for MorphEthApi +impl EthBlocks for MorphEthApi where N: MorphNodeCore, Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, { } -impl LoadReceipt for MorphEthApi +impl LoadTransaction for MorphEthApi where N: MorphNodeCore, Rpc: RpcConvert, { } -impl EthBlocks for MorphEthApi +impl LoadReceipt for MorphEthApi where N: MorphNodeCore, Rpc: RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, { } diff --git a/crates/rpc/src/eth/call.rs b/crates/rpc/src/eth/call.rs index 5e12116..148779f 100644 --- a/crates/rpc/src/eth/call.rs +++ b/crates/rpc/src/eth/call.rs @@ -1,5 +1,6 @@ //! Morph `eth_call` and `eth_estimateGas` overrides. +use crate::error::ToMorphErr; use crate::eth::api::{MorphEthApi, MorphNodeCore}; use crate::MorphEthApiError; use alloy_primitives::U256; @@ -11,13 +12,9 @@ use reth_rpc_eth_api::{ helpers::{estimate::EstimateCall, Call, EthCall}, EthApiTypes, RpcNodeCore, }; -use reth_rpc_eth_types::{error::FromEthApiError, EthApiError}; +use reth_rpc_eth_types::EthApiError; use revm::{context::Transaction as RevmTransaction, Database}; -const ERR_INSUFFICIENT_FUNDS_FOR_L1_FEE: &str = "insufficient funds for l1 fee"; -const ERR_INSUFFICIENT_FUNDS_FOR_TRANSFER: &str = "insufficient funds for transfer"; -const ERR_INVALID_TOKEN: &str = "invalid token"; - impl EthCall for MorphEthApi where N: MorphNodeCore, @@ -55,21 +52,16 @@ where let caller = tx_env.caller(); let balance = db .basic(caller) - .map_err(Into::::into) - .map_err(::from_eth_err)? + .to_morph_err()? .map(|acc| acc.balance) .unwrap_or_default(); let value = tx_env.value(); if value > balance { - return Err(::from_eth_err( - EthApiError::InvalidParams(ERR_INSUFFICIENT_FUNDS_FOR_TRANSFER.to_string()), - )); + return Err(MorphEthApiError::InsufficientFundsForTransfer); } - let (hardfork, l1_fee) = self - .estimate_l1_fee(&mut db, evm_env, tx_env) - .map_err(::from_eth_err)?; + let l1_fee = self.estimate_l1_fee(&mut db, evm_env, tx_env)?; if let Some(fee_token_id) = tx_env.fee_token_id.filter(|id| *id > 0) { return self.caller_gas_allowance_with_token( @@ -77,7 +69,6 @@ where caller, balance, value, - hardfork, l1_fee, fee_token_id, tx_env.fee_limit, @@ -86,7 +77,6 @@ where } caller_gas_allowance_with_eth(balance, value, l1_fee, tx_env.gas_price()) - .map_err(::from_eth_err) } } @@ -101,7 +91,7 @@ where db: &mut DB, evm_env: &EvmEnvFor< as RpcNodeCore>::Evm>, tx_env: &TxEnvFor< as RpcNodeCore>::Evm>, - ) -> Result<(morph_chainspec::MorphHardfork, U256), EthApiError> + ) -> Result where DB: Database, DB::Error: Into, @@ -114,7 +104,7 @@ where let hardfork = chain_spec.morph_hardfork_at(block_number, timestamp); if tx_env.is_l1_msg() { - return Ok((hardfork, U256::ZERO)); + return Ok(U256::ZERO); } let rlp_bytes = tx_env.rlp_bytes.as_ref().ok_or_else(|| { @@ -125,16 +115,19 @@ where EthApiError::InvalidParams(format!("failed to estimate L1 data fee: {err}")) })?; - Ok((hardfork, l1_info.calculate_tx_l1_cost(rlp_bytes, hardfork))) + Ok(l1_info.calculate_tx_l1_cost(rlp_bytes, hardfork)) } + /// Calculate caller's gas allowance when paying with ERC20 tokens. + /// + /// Uses storage-only reads. For tokens without a known `balance_slot`, + /// skips the token balance limit (EVM handler will verify during execution). fn caller_gas_allowance_with_token( &self, db: &mut DB, caller: alloy_primitives::Address, balance: U256, value: U256, - hardfork: morph_chainspec::MorphHardfork, l1_fee: U256, fee_token_id: u16, fee_limit: Option, @@ -144,21 +137,34 @@ where DB: Database, DB::Error: Into, { - let token_fee_info = TokenFeeInfo::try_fetch(db, fee_token_id, caller, hardfork) - .map_err(|_| EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string())) - .map_err(::from_eth_err)? - .ok_or_else(|| EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string())) - .map_err(::from_eth_err)?; + let token_fee_info = TokenFeeInfo::fetch_storage_only(db, fee_token_id, caller) + .map_err(|_| MorphEthApiError::InvalidFeeToken)? + .ok_or(MorphEthApiError::InvalidFeeToken)?; + // Validate token is registered and active if !token_fee_info.is_active || token_fee_info.price_ratio.is_zero() || token_fee_info.scale.is_zero() { - return Err(::from_eth_err( - EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string()), - )); + return Err(MorphEthApiError::InvalidFeeToken); } + // Calculate base ETH allowance (for tx.value transfer) + let eth_allowance = caller_gas_allowance_with_eth(balance, value, U256::ZERO, gas_price)?; + + // If balance_slot is unknown, we cannot accurately read the token balance + // via storage. Skip the token balance limit and let the EVM handler + // (`validate_and_deduct_token_fee`) verify the actual balance. + if token_fee_info.balance_slot.is_none() { + tracing::debug!( + target: "morph::rpc", + token_id = fee_token_id, + "Token balance_slot unknown, skipping token balance limit in caller_gas_allowance" + ); + return Ok(eth_allowance); + } + + // Calculate token-based gas allowance let limit = match fee_limit { Some(limit) if !limit.is_zero() => token_fee_info.balance.min(limit), _ => token_fee_info.balance, @@ -166,23 +172,15 @@ where let l1_fee_in_token = token_fee_info.calculate_token_amount(l1_fee); if l1_fee_in_token >= limit { - return Err(::from_eth_err( - EthApiError::InvalidParams(ERR_INSUFFICIENT_FUNDS_FOR_L1_FEE.to_string()), - )); + return Err(MorphEthApiError::InsufficientFundsForL1Fee); } let available_token = limit - l1_fee_in_token; - let available_eth = - token_amount_to_eth(available_token, &token_fee_info).ok_or_else(|| { - EthApiError::InvalidParams(ERR_INVALID_TOKEN.to_string()) - })?; - - caller_gas_allowance_with_eth(balance, value, U256::ZERO, gas_price) - .and_then(|allowance| { - let allowance_eth = gas_allowance_from_balance(available_eth, gas_price); - Ok(allowance.min(allowance_eth)) - }) - .map_err(::from_eth_err) + let available_eth = token_amount_to_eth(available_token, &token_fee_info) + .ok_or(MorphEthApiError::InvalidFeeToken)?; + + let token_allowance = gas_allowance_from_balance(available_eth, gas_price); + Ok(eth_allowance.min(token_allowance)) } } @@ -200,12 +198,10 @@ fn caller_gas_allowance_with_eth( value: U256, l1_fee: U256, gas_price: u128, -) -> Result { +) -> Result { let mut available = balance.saturating_sub(value); if l1_fee >= available { - return Err(EthApiError::InvalidParams( - ERR_INSUFFICIENT_FUNDS_FOR_L1_FEE.to_string(), - )); + return Err(MorphEthApiError::InsufficientFundsForL1Fee); } available -= l1_fee; Ok(gas_allowance_from_balance(available, gas_price)) diff --git a/crates/txpool/src/morph_tx_validation.rs b/crates/txpool/src/morph_tx_validation.rs index 10fa069..9cd3359 100644 --- a/crates/txpool/src/morph_tx_validation.rs +++ b/crates/txpool/src/morph_tx_validation.rs @@ -71,7 +71,7 @@ pub fn validate_morph_tx( } // Fetch token info from L2TokenRegistry - let token_info = TokenFeeInfo::try_fetch(db, fee_token_id, input.sender, input.hardfork) + let token_info = TokenFeeInfo::fetch(db, fee_token_id, input.sender, input.hardfork) .map_err(|err| MorphTxError::TokenInfoFetchFailed { token_id: fee_token_id, message: format!("{err:?}"), From 6cb8cca20e727fb0db08b9f39bfdd46257be62ce Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 3 Feb 2026 17:51:00 +0800 Subject: [PATCH 12/19] style: fmt and lint --- crates/primitives/src/lib.rs | 4 +- crates/primitives/src/receipt/envelope.rs | 38 ++- crates/primitives/src/receipt/mod.rs | 2 +- crates/revm/src/handler.rs | 5 +- crates/revm/src/token_fee.rs | 14 +- crates/rpc/src/error.rs | 15 +- crates/rpc/src/eth/api.rs | 308 ------------------- crates/rpc/src/eth/call.rs | 41 +-- crates/rpc/src/eth/mod.rs | 345 ++++++++++++++++++++-- crates/rpc/src/eth/receipt.rs | 26 +- crates/rpc/src/eth/transaction.rs | 61 ++-- crates/rpc/src/types/request.rs | 6 +- crates/rpc/src/types/transaction.rs | 13 +- 13 files changed, 446 insertions(+), 432 deletions(-) delete mode 100644 crates/rpc/src/eth/api.rs diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 9b2e357..7b0b95c 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -47,7 +47,9 @@ pub type Block = alloy_consensus::Block; pub type BlockBody = alloy_consensus::BlockBody; // Re-export receipt types -pub use receipt::{MorphReceipt, MorphReceiptEnvelope, MorphReceiptWithBloom, MorphTransactionReceipt}; +pub use receipt::{ + MorphReceipt, MorphReceiptEnvelope, MorphReceiptWithBloom, MorphTransactionReceipt, +}; // Re-export transaction types pub use transaction::{ diff --git a/crates/primitives/src/receipt/envelope.rs b/crates/primitives/src/receipt/envelope.rs index 343fbf5..a1bd41d 100644 --- a/crates/primitives/src/receipt/envelope.rs +++ b/crates/primitives/src/receipt/envelope.rs @@ -5,11 +5,11 @@ use std::vec::Vec; use alloy_consensus::{Eip658Value, Receipt, ReceiptWithBloom, TxReceipt}; use alloy_eips::{ - eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, Typed2718, + eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, }; -use alloy_primitives::{logs_bloom, Bloom, Log}; -use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; +use alloy_primitives::{Bloom, Log, logs_bloom}; +use alloy_rlp::{BufMut, Decodable, Encodable, length_of_length}; /// Receipt envelope, as defined in [EIP-2718], modified for Morph chains. #[derive(Debug, Clone, PartialEq, Eq)] @@ -47,9 +47,15 @@ impl MorphReceiptEnvelope { ) -> Self { let logs = logs.into_iter().cloned().collect::>(); let logs_bloom = logs_bloom(&logs); - let inner_receipt = - Receipt { status: Eip658Value::Eip658(status), cumulative_gas_used, logs }; - let with_bloom = ReceiptWithBloom { receipt: inner_receipt, logs_bloom }; + let inner_receipt = Receipt { + status: Eip658Value::Eip658(status), + cumulative_gas_used, + logs, + }; + let with_bloom = ReceiptWithBloom { + receipt: inner_receipt, + logs_bloom, + }; match tx_type { MorphTxType::Legacy => Self::Legacy(with_bloom), MorphTxType::Eip2930 => Self::Eip2930(with_bloom), @@ -107,9 +113,7 @@ impl MorphReceiptEnvelope { } /// Returns the L1 message receipt if it is a deposit receipt. - pub const fn as_l1_message_receipt_with_bloom( - &self, - ) -> Option<&ReceiptWithBloom>> { + pub const fn as_l1_message_receipt_with_bloom(&self) -> Option<&ReceiptWithBloom>> { match self { Self::L1Message(t) => Some(t), _ => None, @@ -260,10 +264,13 @@ impl Typed2718 for MorphReceiptEnvelope { impl Decodable2718 for MorphReceiptEnvelope { fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { - match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? { - MorphTxType::Legacy => Err( - alloy_rlp::Error::Custom("type-0 eip2718 receipts are not supported").into(), - ), + match ty + .try_into() + .map_err(|_| Eip2718Error::UnexpectedType(ty))? + { + MorphTxType::Legacy => { + Err(alloy_rlp::Error::Custom("type-0 eip2718 receipts are not supported").into()) + } MorphTxType::Eip2930 => Ok(Self::Eip2930(Decodable::decode(buf)?)), MorphTxType::Eip1559 => Ok(Self::Eip1559(Decodable::decode(buf)?)), MorphTxType::Eip7702 => Ok(Self::Eip7702(Decodable::decode(buf)?)), @@ -289,7 +296,10 @@ impl From for MorphReceiptEnvelope { }; let logs_bloom = logs_bloom(&inner.logs); - let with_bloom = ReceiptWithBloom { receipt: inner, logs_bloom }; + let with_bloom = ReceiptWithBloom { + receipt: inner, + logs_bloom, + }; match tx_type { MorphTxType::Legacy => Self::Legacy(with_bloom), MorphTxType::Eip2930 => Self::Eip2930(with_bloom), diff --git a/crates/primitives/src/receipt/mod.rs b/crates/primitives/src/receipt/mod.rs index ceae6ec..ea37841 100644 --- a/crates/primitives/src/receipt/mod.rs +++ b/crates/primitives/src/receipt/mod.rs @@ -4,9 +4,9 @@ //! - [`MorphTransactionReceipt`]: Receipt with L1 fee and Morph transaction fields //! - [`MorphReceipt`]: Typed receipt enum for different transaction types +mod envelope; #[allow(clippy::module_inception)] mod receipt; -mod envelope; pub use envelope::MorphReceiptEnvelope; pub use receipt::{MorphReceiptWithBloom, MorphTransactionReceipt}; diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index b649af0..e85d403 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -329,9 +329,8 @@ where // Fetch token fee info from Token Registry let spec = evm.ctx_ref().cfg().spec(); - let token_fee_info = - TokenFeeInfo::fetch(evm.ctx_mut().db_mut(), token_id, caller, spec)? - .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; + let token_fee_info = TokenFeeInfo::fetch(evm.ctx_mut().db_mut(), token_id, caller, spec)? + .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; // Check if token is active if !token_fee_info.is_active { diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index f77f30a..714f8b3 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -9,7 +9,10 @@ use alloy_evm::Database; use alloy_primitives::{Address, Bytes, U256, address, keccak256}; use morph_chainspec::hardfork::MorphHardfork; use revm::SystemCallEvm; -use revm::{Database as RevmDatabase, Inspector, context_interface::result::EVMError, inspector::NoOpInspector}; +use revm::{ + Database as RevmDatabase, Inspector, context_interface::result::EVMError, + inspector::NoOpInspector, +}; use crate::evm::MorphContext; use crate::{MorphEvm, MorphInvalidTransaction}; @@ -74,8 +77,13 @@ impl TokenFeeInfo { None => return Ok(None), }; - let balance = - read_erc20_balance(db, entry.token_address, caller, entry.balance_slot, hardfork)?; + let balance = read_erc20_balance( + db, + entry.token_address, + caller, + entry.balance_slot, + hardfork, + )?; Ok(Some(Self { token_address: entry.token_address, diff --git a/crates/rpc/src/error.rs b/crates/rpc/src/error.rs index 67382f4..d0b2dd3 100644 --- a/crates/rpc/src/error.rs +++ b/crates/rpc/src/error.rs @@ -7,8 +7,8 @@ use reth_evm::revm::context::result::EVMError; use reth_evm::{HaltReasonFor, InvalidTxError}; use reth_rpc_convert::TransactionConversionError; use reth_rpc_eth_types::{ - error::{api::FromEvmHalt, api::FromRevert, AsEthApiError}, EthApiError, + error::{AsEthApiError, api::FromEvmHalt, api::FromRevert}, }; use std::convert::Infallible; use thiserror::Error; @@ -153,7 +153,7 @@ impl From for jsonrpsee::types::ErrorObject<'static> { impl AsEthApiError for MorphEthApiError { fn as_err(&self) -> Option<&EthApiError> { match self { - MorphEthApiError::Eth(err) => Some(err), + Self::Eth(err) => Some(err), _ => None, } } @@ -164,19 +164,19 @@ impl AsEthApiError for MorphEthApiError { impl FromEvmHalt> for MorphEthApiError { fn from_evm_halt(halt: HaltReasonFor, gas_limit: u64) -> Self { - MorphEthApiError::Eth(EthApiError::from_evm_halt(halt, gas_limit)) + Self::Eth(EthApiError::from_evm_halt(halt, gas_limit)) } } impl FromRevert for MorphEthApiError { fn from_revert(output: alloy_primitives::Bytes) -> Self { - MorphEthApiError::Eth(EthApiError::from_revert(output)) + Self::Eth(EthApiError::from_revert(output)) } } impl From for MorphEthApiError { fn from(err: ProviderError) -> Self { - MorphEthApiError::Eth(err.into()) + Self::Eth(err.into()) } } @@ -186,13 +186,13 @@ where TxError: InvalidTxError, { fn from(err: EVMError) -> Self { - MorphEthApiError::Eth(err.into()) + Self::Eth(err.into()) } } impl From for MorphEthApiError { fn from(err: TransactionConversionError) -> Self { - MorphEthApiError::Eth(err.into()) + Self::Eth(err.into()) } } @@ -201,4 +201,3 @@ impl From for MorphEthApiError { match err {} } } - diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs deleted file mode 100644 index edc9bbf..0000000 --- a/crates/rpc/src/eth/api.rs +++ /dev/null @@ -1,308 +0,0 @@ -//! Morph `eth_` API wrapper. - -use crate::MorphEthApiError; -use morph_chainspec::MorphChainSpec; -use morph_evm::MorphEvmConfig; -use morph_primitives::MorphPrimitives; -use reth_provider::ChainSpecProvider; -use reth_rpc::EthApi; -use reth_rpc_convert::RpcConvert; -use reth_rpc_eth_api::{ - helpers::{ - pending_block::PendingEnvBuilder, EthApiSpec, EthBlocks, EthFees, EthState, - EthTransactions, LoadBlock, LoadFee, LoadPendingBlock, LoadReceipt, LoadState, - LoadTransaction, SpawnBlocking, Trace, - }, - EthApiTypes, RpcNodeCore, RpcNodeCoreExt, -}; -use reth_rpc_eth_types::{EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle}; -use reth_tasks::{ - pool::{BlockingTaskGuard, BlockingTaskPool}, - TaskSpawner, -}; -use std::{fmt, sync::Arc, time::Duration}; - -/// Adapter for [`EthApi`], which holds all the data required to serve core `eth_` API. -pub type EthApiNodeBackend = EthApi; - -/// A helper trait with requirements for [`RpcNodeCore`] to be used in [`MorphEthApi`]. -pub trait MorphNodeCore: RpcNodeCore {} -impl MorphNodeCore for T where T: RpcNodeCore {} - -/// Morph `Eth` API implementation. -/// -/// This wraps a default `Eth` implementation, and provides additional functionality -/// where the Morph spec deviates from the default (ethereum) spec, e.g. L1 fee -/// and ERC20 fee token support in `eth_estimateGas`. -pub struct MorphEthApi { - /// Gateway to node's core components. - inner: Arc>, -} - -impl Clone for MorphEthApi { - fn clone(&self) -> Self { - Self { inner: self.inner.clone() } - } -} - -impl MorphEthApi { - /// Creates a new [`MorphEthApi`]. - pub fn new(eth_api: EthApiNodeBackend) -> Self { - let inner = Arc::new(MorphEthApiInner { eth_api }); - Self { inner } - } - - /// Returns a reference to the [`EthApiNodeBackend`]. - pub fn eth_api(&self) -> &EthApiNodeBackend { - self.inner.eth_api() - } -} - -impl EthApiTypes for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ - type Error = MorphEthApiError; - type NetworkTypes = Rpc::Network; - type RpcConvert = Rpc; - - fn converter(&self) -> &Self::RpcConvert { - self.inner.eth_api.converter() - } -} - -impl RpcNodeCore for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ - type Primitives = N::Primitives; - type Provider = N::Provider; - type Pool = N::Pool; - type Evm = ::Evm; - type Network = ::Network; - - #[inline] - fn pool(&self) -> &Self::Pool { - self.inner.eth_api.pool() - } - - #[inline] - fn evm_config(&self) -> &Self::Evm { - self.inner.eth_api.evm_config() - } - - #[inline] - fn network(&self) -> &Self::Network { - self.inner.eth_api.network() - } - - #[inline] - fn provider(&self) -> &Self::Provider { - self.inner.eth_api.provider() - } -} - -impl RpcNodeCoreExt for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ - #[inline] - fn cache(&self) -> &EthStateCache { - self.inner.eth_api.cache() - } -} - -impl EthApiSpec for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ - #[inline] - fn starting_block(&self) -> alloy_primitives::U256 { - self.inner.eth_api.starting_block() - } -} - -impl SpawnBlocking for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ - #[inline] - fn io_task_spawner(&self) -> impl TaskSpawner { - self.inner.eth_api.task_spawner() - } - - #[inline] - fn tracing_task_pool(&self) -> &BlockingTaskPool { - self.inner.eth_api.blocking_task_pool() - } - - #[inline] - fn tracing_task_guard(&self) -> &BlockingTaskGuard { - self.inner.eth_api.blocking_task_guard() - } - - #[inline] - fn blocking_io_task_guard(&self) -> &std::sync::Arc { - self.inner.eth_api.blocking_io_request_semaphore() - } -} - -impl LoadFee for MorphEthApi -where - N: MorphNodeCore, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, - Rpc: RpcConvert, -{ - #[inline] - fn gas_oracle(&self) -> &GasPriceOracle { - self.inner.eth_api.gas_oracle() - } - - #[inline] - fn fee_history_cache(&self) -> &FeeHistoryCache> { - self.inner.eth_api.fee_history_cache() - } -} - -impl LoadPendingBlock for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, -{ - fn pending_block(&self) -> &tokio::sync::Mutex>> { - self.inner.eth_api.pending_block() - } - - fn pending_env_builder(&self) -> &dyn PendingEnvBuilder { - self.inner.eth_api.pending_env_builder() - } - - fn pending_block_kind(&self) -> reth_rpc_eth_types::builder::config::PendingBlockKind { - self.inner.eth_api.pending_block_kind() - } -} - -impl LoadBlock for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, -{ -} - -impl EthBlocks for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, -{ -} - -impl LoadTransaction for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ -} - -impl LoadReceipt for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ -} - -impl EthTransactions for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, -{ - #[inline] - fn signers( - &self, - ) -> &reth_rpc_eth_api::helpers::spec::SignersForRpc { - self.inner.eth_api.signers() - } - - #[inline] - fn send_raw_transaction_sync_timeout(&self) -> Duration { - self.inner.eth_api.send_raw_transaction_sync_timeout() - } - - async fn send_transaction( - &self, - tx: reth_primitives_traits::WithEncoded< - reth_primitives_traits::Recovered< - reth_transaction_pool::PoolPooledTx, - >, - >, - ) -> Result { - reth_rpc_eth_api::helpers::EthTransactions::send_transaction(&self.inner.eth_api, tx) - .await - .map_err(Into::into) - } -} - -impl EthFees for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, -{ -} - -impl LoadState for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - Self: LoadPendingBlock, -{ -} - -impl EthState for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, - Self: LoadPendingBlock, -{ - #[inline] - fn max_proof_window(&self) -> u64 { - self.inner.eth_api.eth_proof_window() - } -} - -impl Trace for MorphEthApi -where - N: MorphNodeCore, - N::Provider: ChainSpecProvider, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, - Rpc: RpcConvert, -{ -} - -impl fmt::Debug for MorphEthApi { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MorphEthApi").finish_non_exhaustive() - } -} - -/// Container type `MorphEthApi` -#[allow(missing_debug_implementations)] -pub struct MorphEthApiInner { - /// Gateway to node's core components. - pub eth_api: EthApiNodeBackend, -} - -impl MorphEthApiInner { - /// Returns a reference to the [`EthApiNodeBackend`]. - const fn eth_api(&self) -> &EthApiNodeBackend { - &self.eth_api - } -} diff --git a/crates/rpc/src/eth/call.rs b/crates/rpc/src/eth/call.rs index 148779f..325eb5d 100644 --- a/crates/rpc/src/eth/call.rs +++ b/crates/rpc/src/eth/call.rs @@ -1,21 +1,31 @@ //! Morph `eth_call` and `eth_estimateGas` overrides. -use crate::error::ToMorphErr; -use crate::eth::api::{MorphEthApi, MorphNodeCore}; use crate::MorphEthApiError; +use crate::error::ToMorphErr; +use crate::eth::{MorphEthApi, MorphNodeCore}; use alloy_primitives::U256; use morph_chainspec::{MorphChainSpec, MorphHardforks}; use morph_revm::{L1BlockInfo, MorphTxExt, TokenFeeInfo}; use reth_evm::{EvmEnvFor, TxEnvFor}; use reth_provider::ChainSpecProvider; use reth_rpc_eth_api::{ - helpers::{estimate::EstimateCall, Call, EthCall}, EthApiTypes, RpcNodeCore, + helpers::{Call, EthCall, estimate::EstimateCall}, }; use reth_rpc_eth_types::EthApiError; -use revm::{context::Transaction as RevmTransaction, Database}; +use revm::{Database, context::Transaction as RevmTransaction}; impl EthCall for MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + Rpc: + reth_rpc_convert::RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl EstimateCall for MorphEthApi where N: MorphNodeCore, N::Provider: ChainSpecProvider, @@ -28,7 +38,8 @@ impl Call for MorphEthApi where N: MorphNodeCore, N::Provider: ChainSpecProvider, - Rpc: reth_rpc_convert::RpcConvert, + Rpc: + reth_rpc_convert::RpcConvert, MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, { fn call_gas_limit(&self) -> u64 { @@ -46,8 +57,8 @@ where fn caller_gas_allowance( &self, mut db: impl Database>, - evm_env: &EvmEnvFor< as RpcNodeCore>::Evm>, - tx_env: &TxEnvFor< as RpcNodeCore>::Evm>, + evm_env: &EvmEnvFor<::Evm>, + tx_env: &TxEnvFor<::Evm>, ) -> Result::Error> { let caller = tx_env.caller(); let balance = db @@ -84,13 +95,14 @@ impl MorphEthApi where N: MorphNodeCore, N::Provider: ChainSpecProvider, - Rpc: reth_rpc_convert::RpcConvert, + Rpc: + reth_rpc_convert::RpcConvert, { fn estimate_l1_fee( &self, db: &mut DB, - evm_env: &EvmEnvFor< as RpcNodeCore>::Evm>, - tx_env: &TxEnvFor< as RpcNodeCore>::Evm>, + evm_env: &EvmEnvFor<::Evm>, + tx_env: &TxEnvFor<::Evm>, ) -> Result where DB: Database, @@ -122,6 +134,7 @@ where /// /// Uses storage-only reads. For tokens without a known `balance_slot`, /// skips the token balance limit (EVM handler will verify during execution). + #[allow(clippy::too_many_arguments)] fn caller_gas_allowance_with_token( &self, db: &mut DB, @@ -184,14 +197,6 @@ where } } -impl EstimateCall for MorphEthApi -where - N: MorphNodeCore, - N::Provider: ChainSpecProvider, - Rpc: reth_rpc_convert::RpcConvert, - MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, -{ -} fn caller_gas_allowance_with_eth( balance: U256, diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 414b582..81f542e 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -1,6 +1,7 @@ //! Morph `eth_` RPC wiring and conversions. use crate::eth::receipt::MorphReceiptConverter; +use crate::MorphEthApiError; use crate::types::{MorphRpcReceipt, MorphRpcTransaction, MorphTransactionRequest}; use alloy_rpc_types_eth::Header as RpcHeader; use eyre::Result; @@ -11,17 +12,28 @@ use reth_evm::ConfigureEvm; use reth_node_api::{FullNodeComponents, FullNodeTypes, HeaderTy, NodeTypes}; use reth_node_builder::rpc::{EthApiBuilder, EthApiCtx}; use reth_provider::ChainSpecProvider; -use reth_rpc_convert::{RpcConverter, RpcTypes}; -use reth_rpc_eth_api::helpers::pending_block::BuildPendingEnv; -use std::marker::PhantomData; +use reth_rpc::EthApi; +use reth_rpc_convert::{RpcConvert, RpcConverter, RpcTypes}; +use reth_rpc_eth_api::{ + EthApiTypes, RpcNodeCore, RpcNodeCoreExt, + helpers::{ + EthApiSpec, EthBlocks, EthFees, EthState, EthTransactions, LoadBlock, LoadFee, + LoadPendingBlock, LoadReceipt, LoadState, LoadTransaction, SpawnBlocking, Trace, + pending_block::{BuildPendingEnv, PendingEnvBuilder}, + }, +}; +use reth_rpc_eth_types::{EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle}; +use reth_tasks::{ + TaskSpawner, + pool::{BlockingTaskGuard, BlockingTaskPool}, +}; +use std::{fmt, marker::PhantomData, sync::Arc, time::Duration}; -pub mod api; pub mod call; pub mod receipt; pub mod transaction; -pub use api::MorphEthApi; - +// ===== RPC type wiring ===== /// Morph RPC response type definitions. #[derive(Clone, Debug, Default)] pub struct MorphRpcTypes; @@ -37,6 +49,7 @@ impl RpcTypes for MorphRpcTypes { pub type MorphRpcConverter = RpcConverter::Evm, MorphReceiptConverter>; +// ===== Builder entrypoint ===== /// Builder for Morph `eth_` RPC that installs Morph RPC conversions. #[derive(Debug)] pub struct MorphEthApiBuilder { @@ -59,27 +72,27 @@ impl MorphEthApiBuilder { impl EthApiBuilder for MorphEthApiBuilder where N: FullNodeComponents< - Evm = MorphEvmConfig, - Evm: ConfigureEvm>>, - Types: NodeTypes, - Provider: ChainSpecProvider, - > + crate::eth::api::MorphNodeCore + Evm = MorphEvmConfig, + Evm: ConfigureEvm>>, + Types: NodeTypes, + Provider: ChainSpecProvider, + > + crate::eth::MorphNodeCore + reth_rpc_eth_api::RpcNodeCore< Provider = ::Provider, Pool = ::Pool, >, NetworkT: RpcTypes< - Header = RpcHeader, - Receipt = MorphRpcReceipt, - TransactionResponse = MorphRpcTransaction, - TransactionRequest = MorphTransactionRequest, - >, + Header = RpcHeader, + Receipt = MorphRpcReceipt, + TransactionResponse = MorphRpcTransaction, + TransactionRequest = MorphTransactionRequest, + >, MorphRpcConverter: reth_rpc_convert::RpcConvert< - Network = NetworkT, - Primitives = ::Primitives, - Error = reth_rpc_eth_types::EthApiError, - Evm = ::Evm, - >, + Network = NetworkT, + Primitives = ::Primitives, + Error = reth_rpc_eth_types::EthApiError, + Evm = ::Evm, + >, { type EthApi = MorphEthApi>; @@ -92,3 +105,293 @@ where )) } } + +// ===== Core API wrapper ===== +/// Adapter for [`EthApi`], which holds all the data required to serve core `eth_` API. +pub type EthApiNodeBackend = EthApi; + +/// A helper trait with requirements for [`RpcNodeCore`] to be used in [`MorphEthApi`]. +pub trait MorphNodeCore: RpcNodeCore {} +impl MorphNodeCore for T where T: RpcNodeCore {} + +/// Morph `Eth` API implementation. +/// +/// This wraps a default `Eth` implementation, and provides additional functionality +/// where the Morph spec deviates from the default (ethereum) spec, e.g. L1 fee +/// and ERC20 fee token support in `eth_estimateGas`. +pub struct MorphEthApi { + /// Gateway to node's core components. + inner: Arc>, +} + +impl Clone for MorphEthApi { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl MorphEthApi { + /// Creates a new [`MorphEthApi`]. + pub fn new(eth_api: EthApiNodeBackend) -> Self { + let inner = Arc::new(MorphEthApiInner { eth_api }); + Self { inner } + } + + /// Returns a reference to the [`EthApiNodeBackend`]. + pub fn eth_api(&self) -> &EthApiNodeBackend { + self.inner.eth_api() + } +} + +// ===== Trait implementations ===== +impl EthApiTypes for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + type Error = MorphEthApiError; + type NetworkTypes = Rpc::Network; + type RpcConvert = Rpc; + + fn converter(&self) -> &Self::RpcConvert { + self.inner.eth_api.converter() + } +} + +impl RpcNodeCore for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + type Primitives = N::Primitives; + type Provider = N::Provider; + type Pool = N::Pool; + type Evm = ::Evm; + type Network = ::Network; + + #[inline] + fn pool(&self) -> &Self::Pool { + self.inner.eth_api.pool() + } + + #[inline] + fn evm_config(&self) -> &Self::Evm { + self.inner.eth_api.evm_config() + } + + #[inline] + fn network(&self) -> &Self::Network { + self.inner.eth_api.network() + } + + #[inline] + fn provider(&self) -> &Self::Provider { + self.inner.eth_api.provider() + } +} + +impl RpcNodeCoreExt for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + #[inline] + fn cache(&self) -> &EthStateCache { + self.inner.eth_api.cache() + } +} + +impl EthApiSpec for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + #[inline] + fn starting_block(&self) -> alloy_primitives::U256 { + self.inner.eth_api.starting_block() + } +} + +impl SpawnBlocking for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ + #[inline] + fn io_task_spawner(&self) -> impl TaskSpawner { + self.inner.eth_api.task_spawner() + } + + #[inline] + fn tracing_task_pool(&self) -> &BlockingTaskPool { + self.inner.eth_api.blocking_task_pool() + } + + #[inline] + fn tracing_task_guard(&self) -> &BlockingTaskGuard { + self.inner.eth_api.blocking_task_guard() + } + + #[inline] + fn blocking_io_task_guard(&self) -> &std::sync::Arc { + self.inner.eth_api.blocking_io_request_semaphore() + } +} + +impl LoadFee for MorphEthApi +where + N: MorphNodeCore, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, + Rpc: RpcConvert, +{ + #[inline] + fn gas_oracle(&self) -> &GasPriceOracle { + self.inner.eth_api.gas_oracle() + } + + #[inline] + fn fee_history_cache(&self) -> &FeeHistoryCache> { + self.inner.eth_api.fee_history_cache() + } +} + +impl LoadPendingBlock for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ + fn pending_block( + &self, + ) -> &tokio::sync::Mutex>> { + self.inner.eth_api.pending_block() + } + + fn pending_env_builder(&self) -> &dyn PendingEnvBuilder { + self.inner.eth_api.pending_env_builder() + } + + fn pending_block_kind(&self) -> reth_rpc_eth_types::builder::config::PendingBlockKind { + self.inner.eth_api.pending_block_kind() + } +} + +impl LoadBlock for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl EthBlocks for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl LoadTransaction for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ +} + +impl LoadReceipt for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ +} + +impl EthTransactions for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ + #[inline] + fn signers( + &self, + ) -> &reth_rpc_eth_api::helpers::spec::SignersForRpc { + self.inner.eth_api.signers() + } + + #[inline] + fn send_raw_transaction_sync_timeout(&self) -> Duration { + self.inner.eth_api.send_raw_transaction_sync_timeout() + } + + async fn send_transaction( + &self, + tx: reth_primitives_traits::WithEncoded< + reth_primitives_traits::Recovered>, + >, + ) -> Result { + reth_rpc_eth_api::helpers::EthTransactions::send_transaction(&self.inner.eth_api, tx) + .await + .map_err(Into::into) + } +} + +impl EthFees for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, +{ +} + +impl LoadState for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + Self: LoadPendingBlock, +{ +} + +impl EthState for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, + Self: LoadPendingBlock, +{ + #[inline] + fn max_proof_window(&self) -> u64 { + self.inner.eth_api.eth_proof_window() + } +} + +impl Trace for MorphEthApi +where + N: MorphNodeCore, + N::Provider: ChainSpecProvider, + MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, + Rpc: RpcConvert, +{ +} + +// ===== Internal container ===== +impl fmt::Debug for MorphEthApi { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MorphEthApi").finish_non_exhaustive() + } +} + +/// Container type `MorphEthApi` +#[allow(missing_debug_implementations)] +pub struct MorphEthApiInner { + /// Gateway to node's core components. + pub eth_api: EthApiNodeBackend, +} + +impl MorphEthApiInner { + /// Returns a reference to the [`EthApiNodeBackend`]. + const fn eth_api(&self) -> &EthApiNodeBackend { + &self.eth_api + } +} diff --git a/crates/rpc/src/eth/receipt.rs b/crates/rpc/src/eth/receipt.rs index 4bfb7bf..d2aaae7 100644 --- a/crates/rpc/src/eth/receipt.rs +++ b/crates/rpc/src/eth/receipt.rs @@ -2,7 +2,7 @@ use crate::types::receipt::MorphRpcReceipt; use alloy_consensus::{Receipt, Transaction, TxReceipt}; -use alloy_primitives::{Address, TxKind, U256, U64}; +use alloy_primitives::{Address, TxKind, U64, U256}; use alloy_rpc_types_eth::{Log, TransactionReceipt}; use morph_primitives::{MorphReceipt, MorphReceiptEnvelope}; use reth_primitives_traits::NodePrimitives; @@ -44,7 +44,13 @@ impl MorphReceiptBuilder { where N: NodePrimitives, { - let ConvertReceiptInput { tx, meta, receipt, gas_used, next_log_index } = input; + let ConvertReceiptInput { + tx, + meta, + receipt, + gas_used, + next_log_index, + } = input; let from = tx.signer(); let (contract_address, to) = match tx.kind() { @@ -52,13 +58,20 @@ impl MorphReceiptBuilder { TxKind::Call(addr) => (None, Some(Address(*addr))), }; - let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = - morph_fee_fields(&receipt); + let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = morph_fee_fields(&receipt); let map_logs = |receipt: Receipt| { - let Receipt { status, cumulative_gas_used, logs } = receipt; + let Receipt { + status, + cumulative_gas_used, + logs, + } = receipt; let logs = Log::collect_for_receipt(next_log_index, meta, logs); - Receipt { status, cumulative_gas_used, logs } + Receipt { + status, + cumulative_gas_used, + logs, + } }; let receipt_envelope = match receipt { @@ -132,4 +145,3 @@ fn morph_fee_fields( MorphReceipt::L1Msg(_) => (U256::ZERO, None, None, None, None), } } - diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs index bdfaf93..5cf136c 100644 --- a/crates/rpc/src/eth/transaction.rs +++ b/crates/rpc/src/eth/transaction.rs @@ -1,18 +1,16 @@ //! Morph transaction conversion for `eth_` RPC responses. -use crate::types::transaction::MorphRpcTransaction; use crate::MorphTransactionRequest; +use crate::types::transaction::MorphRpcTransaction; use alloy_consensus::{ - transaction::Recovered, - EthereumTxEnvelope, SignableTransaction, Transaction, TxEip4844, + EthereumTxEnvelope, SignableTransaction, Transaction, TxEip4844, transaction::Recovered, }; use alloy_eips::eip2718::Encodable2718; use alloy_network::TxSigner; -use alloy_primitives::{Address, Bytes, Signature, TxKind, U256, U64}; +use alloy_primitives::{Address, Bytes, Signature, TxKind, U64, U256}; use alloy_rpc_types_eth::{AccessList, Transaction as RpcTransaction, TransactionInfo}; use reth_rpc_convert::{ - transaction::FromConsensusTx, SignTxRequestError, SignableTxRequest, TryIntoSimTx, - TryIntoTxEnv, + SignTxRequestError, SignableTxRequest, TryIntoSimTx, TryIntoTxEnv, transaction::FromConsensusTx, }; use reth_rpc_eth_types::EthApiError; use revm::context::Transaction as RevmTransaction; @@ -32,9 +30,7 @@ impl FromConsensusTx for MorphRpcTransaction { tx_info: Self::TxInfo, ) -> Result { let (sender, queue_index) = match &tx { - MorphTxEnvelope::L1Msg(msg) => { - (Some(msg.sender), Some(U64::from(msg.queue_index))) - } + MorphTxEnvelope::L1Msg(msg) => (Some(msg.sender), Some(U64::from(msg.queue_index))), _ => (None, None), }; let fee_token_id = tx.fee_token_id().map(U64::from); @@ -54,7 +50,7 @@ impl FromConsensusTx for MorphRpcTransaction { effective_gas_price, }; - Ok(MorphRpcTransaction { + Ok(Self { inner, sender, queue_index, @@ -65,32 +61,28 @@ impl FromConsensusTx for MorphRpcTransaction { } impl TryIntoSimTx for MorphTransactionRequest { - fn try_into_sim_tx( - self, - ) -> Result> { - let tx_req = self.clone(); - if let Some(fee_token_id) = tx_req.fee_token_id.filter(|id| id.to::() > 0) { + fn try_into_sim_tx(self) -> Result> { + if let Some(fee_token_id) = self.fee_token_id.filter(|id| id.to::() > 0) { let morph_tx = build_morph_tx_from_request( - &tx_req.inner, + &self.inner, fee_token_id, - tx_req.fee_limit.unwrap_or_default(), + self.fee_limit.unwrap_or_default(), ) - .map_err(|err| alloy_consensus::error::ValueError::new(tx_req, err))?; + .map_err(|err| alloy_consensus::error::ValueError::new(self, err))?; let signature = Signature::from_bytes_and_parity(&[0u8; 64], false); return Ok(MorphTxEnvelope::Morph(morph_tx.into_signed(signature))); } - let inner = tx_req.inner.clone(); + let inner = self.inner.clone(); let envelope = inner.build_typed_simulate_transaction().map_err(|err| { err.map(|inner| Self { inner, - fee_token_id: tx_req.fee_token_id, - fee_limit: tx_req.fee_limit, + fee_token_id: self.fee_token_id, + fee_limit: self.fee_limit, }) })?; - morph_envelope_from_ethereum(envelope).map_err(|err| { - alloy_consensus::error::ValueError::new(tx_req, err) - }) + morph_envelope_from_ethereum(envelope) + .map_err(|err| alloy_consensus::error::ValueError::new(self, err)) } } @@ -156,13 +148,8 @@ impl TryIntoTxEnv for MorphTransactionRequest { { let fee_token_id = U64::from(tx_env.fee_token_id.unwrap_or_default()); let fee_limit = tx_env.fee_limit.unwrap_or_default(); - let morph_tx = build_morph_tx_from_env( - &tx_env, - fee_token_id, - fee_limit, - access_list, - evm_env, - )?; + let morph_tx = + build_morph_tx_from_env(&tx_env, fee_token_id, fee_limit, access_list, evm_env)?; encode_2718(morph_tx) } else { let envelope = self @@ -189,15 +176,15 @@ fn morph_envelope_from_ethereum( } } - fn build_morph_tx_from_request( req: &alloy_rpc_types_eth::TransactionRequest, fee_token_id: U64, fee_limit: U256, ) -> Result { - let chain_id = req.chain_id.ok_or("missing chain_id for morph transaction")?; - let fee_token_id = - u16::try_from(fee_token_id.to::()).map_err(|_| "invalid token")?; + let chain_id = req + .chain_id + .ok_or("missing chain_id for morph transaction")?; + let fee_token_id = u16::try_from(fee_token_id.to::()).map_err(|_| "invalid token")?; let gas_limit = req.gas.unwrap_or_default() as u128; let nonce = req.nonce.unwrap_or_default(); let max_fee_per_gas = req.max_fee_per_gas.or(req.gas_price).unwrap_or_default(); @@ -230,7 +217,9 @@ fn build_morph_tx_from_env( ) -> Result { let fee_token_id = u16::try_from(fee_token_id.to::()) .map_err(|_| EthApiError::InvalidParams("invalid token".to_string()))?; - let chain_id = tx_env.chain_id().unwrap_or_else(|| evm_env.cfg_env.chain_id); + let chain_id = tx_env + .chain_id() + .unwrap_or(evm_env.cfg_env.chain_id); let input = tx_env.input().clone(); let to = tx_env.kind(); let max_fee_per_gas = tx_env.max_fee_per_gas(); diff --git a/crates/rpc/src/types/request.rs b/crates/rpc/src/types/request.rs index 9321c07..eccf62b 100644 --- a/crates/rpc/src/types/request.rs +++ b/crates/rpc/src/types/request.rs @@ -29,7 +29,11 @@ pub struct MorphTransactionRequest { pub inner: TransactionRequest, /// Token ID for fee payment (only for MorphTx type 0x7F). - #[serde(rename = "feeTokenID", default, skip_serializing_if = "Option::is_none")] + #[serde( + rename = "feeTokenID", + default, + skip_serializing_if = "Option::is_none" + )] pub fee_token_id: Option, /// Maximum token amount willing to pay for fees (only for MorphTx type 0x7F). diff --git a/crates/rpc/src/types/transaction.rs b/crates/rpc/src/types/transaction.rs index 9767b40..b89811e 100644 --- a/crates/rpc/src/types/transaction.rs +++ b/crates/rpc/src/types/transaction.rs @@ -15,14 +15,7 @@ use serde::{Deserialize, Serialize}; /// - L1 message sender/queue index /// - Morph fee token fields #[derive( - Clone, - Debug, - PartialEq, - Eq, - Serialize, - Deserialize, - derive_more::Deref, - derive_more::DerefMut, + Clone, Debug, PartialEq, Eq, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut, )] #[serde(rename_all = "camelCase")] pub struct MorphRpcTransaction { @@ -124,9 +117,7 @@ impl ConsensusTransaction for MorphRpcTransaction { self.inner.blob_versioned_hashes() } - fn authorization_list( - &self, - ) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> { + fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> { self.inner.authorization_list() } } From 1551498877b57b2ef762739cfaf823a472a6e01b Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 3 Feb 2026 19:50:08 +0800 Subject: [PATCH 13/19] refactor: change rpc transaction request --- crates/rpc/src/error.rs | 11 -------- crates/rpc/src/eth/transaction.rs | 43 ++++++++++++++++++------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/crates/rpc/src/error.rs b/crates/rpc/src/error.rs index d0b2dd3..13fe2e3 100644 --- a/crates/rpc/src/error.rs +++ b/crates/rpc/src/error.rs @@ -14,17 +14,6 @@ use std::convert::Infallible; use thiserror::Error; /// Extension trait for converting `Result` where `E: Into` to `Result`. -/// -/// This simplifies the common pattern of: -/// ```ignore -/// result -/// .map_err(Into::::into) -/// .map_err(::from_eth_err) -/// ``` -/// to just: -/// ```ignore -/// result.to_morph_err() -/// ``` pub trait ToMorphErr { /// Convert the error to `MorphEthApiError`. fn to_morph_err(self) -> Result; diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs index 5cf136c..2ecc818 100644 --- a/crates/rpc/src/eth/transaction.rs +++ b/crates/rpc/src/eth/transaction.rs @@ -69,7 +69,7 @@ impl TryIntoSimTx for MorphTransactionRequest { self.fee_limit.unwrap_or_default(), ) .map_err(|err| alloy_consensus::error::ValueError::new(self, err))?; - let signature = Signature::from_bytes_and_parity(&[0u8; 64], false); + let signature = Signature::new(Default::default(), Default::default(), false); return Ok(MorphTxEnvelope::Morph(morph_tx.into_signed(signature))); } @@ -123,10 +123,10 @@ impl TryIntoTxEnv for MorphTransactionRequest { ) -> Result { let fee_token_id = self.fee_token_id; let fee_limit = self.fee_limit; - let access_list = self.inner.access_list.clone().unwrap_or_default(); + let inner = self.inner; + let access_list = inner.access_list.clone().unwrap_or_default(); - let inner_tx_env = self - .inner + let inner_tx_env = inner .clone() .try_into_tx_env(evm_env) .map_err(EthApiError::from)?; @@ -144,20 +144,7 @@ impl TryIntoTxEnv for MorphTransactionRequest { tx_env.inner.tx_type = morph_primitives::MORPH_TX_TYPE_ID; } - let rlp_bytes = if tx_env.fee_token_id.unwrap_or_default() > 0 || tx_env.fee_limit.is_some() - { - let fee_token_id = U64::from(tx_env.fee_token_id.unwrap_or_default()); - let fee_limit = tx_env.fee_limit.unwrap_or_default(); - let morph_tx = - build_morph_tx_from_env(&tx_env, fee_token_id, fee_limit, access_list, evm_env)?; - encode_2718(morph_tx) - } else { - let envelope = self - .inner - .build_typed_simulate_transaction() - .map_err(|err| EthApiError::InvalidParams(err.to_string()))?; - encode_2718(envelope) - }; + let rlp_bytes = encode_tx_for_l1_fee(&tx_env, access_list, evm_env, inner)?; tx_env.rlp_bytes = Some(rlp_bytes); Ok(tx_env) @@ -240,6 +227,26 @@ fn build_morph_tx_from_env( }) } +fn encode_tx_for_l1_fee( + tx_env: &MorphTxEnv, + access_list: AccessList, + evm_env: &EvmEnv, + inner: alloy_rpc_types_eth::TransactionRequest, +) -> Result { + if tx_env.fee_token_id.unwrap_or_default() > 0 || tx_env.fee_limit.is_some() { + let fee_token_id = U64::from(tx_env.fee_token_id.unwrap_or_default()); + let fee_limit = tx_env.fee_limit.unwrap_or_default(); + let morph_tx = + build_morph_tx_from_env(tx_env, fee_token_id, fee_limit, access_list, evm_env)?; + Ok(encode_2718(morph_tx)) + } else { + let envelope = inner + .build_typed_simulate_transaction() + .map_err(|err| EthApiError::InvalidParams(err.to_string()))?; + Ok(encode_2718(envelope)) + } +} + fn encode_2718(tx: T) -> Bytes { let mut out = Vec::with_capacity(tx.encode_2718_len()); tx.encode_2718(&mut out); From cf26b27f5de2d84c311f1487106dbe488f9823d5 Mon Sep 17 00:00:00 2001 From: panos Date: Tue, 3 Feb 2026 23:51:57 +0800 Subject: [PATCH 14/19] refactor: change rpc receipt convert --- crates/rpc/src/eth/mod.rs | 9 +-- crates/rpc/src/eth/receipt.rs | 117 +++++++++++++++------------------- 2 files changed, 51 insertions(+), 75 deletions(-) diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 81f542e..13a91d9 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -18,7 +18,7 @@ use reth_rpc_eth_api::{ EthApiTypes, RpcNodeCore, RpcNodeCoreExt, helpers::{ EthApiSpec, EthBlocks, EthFees, EthState, EthTransactions, LoadBlock, LoadFee, - LoadPendingBlock, LoadReceipt, LoadState, LoadTransaction, SpawnBlocking, Trace, + LoadPendingBlock, LoadState, LoadTransaction, SpawnBlocking, Trace, pending_block::{BuildPendingEnv, PendingEnvBuilder}, }, }; @@ -301,13 +301,6 @@ where { } -impl LoadReceipt for MorphEthApi -where - N: MorphNodeCore, - Rpc: RpcConvert, -{ -} - impl EthTransactions for MorphEthApi where N: MorphNodeCore, diff --git a/crates/rpc/src/eth/receipt.rs b/crates/rpc/src/eth/receipt.rs index d2aaae7..8ee484b 100644 --- a/crates/rpc/src/eth/receipt.rs +++ b/crates/rpc/src/eth/receipt.rs @@ -1,12 +1,15 @@ //! Morph receipt conversion for `eth_` RPC responses. +use crate::eth::{MorphEthApi, MorphNodeCore}; use crate::types::receipt::MorphRpcReceipt; -use alloy_consensus::{Receipt, Transaction, TxReceipt}; -use alloy_primitives::{Address, TxKind, U64, U256}; -use alloy_rpc_types_eth::{Log, TransactionReceipt}; +use alloy_consensus::{Receipt, TxReceipt}; +use alloy_primitives::{U64, U256}; +use alloy_rpc_types_eth::Log; use morph_primitives::{MorphReceipt, MorphReceiptEnvelope}; use reth_primitives_traits::NodePrimitives; -use reth_rpc_convert::transaction::{ConvertReceiptInput, ReceiptConverter}; +use reth_rpc_convert::{RpcConvert, transaction::{ConvertReceiptInput, ReceiptConverter}}; +use reth_rpc_eth_api::helpers::LoadReceipt; +use reth_rpc_eth_types::{EthApiError, receipt::build_receipt}; use std::fmt::Debug; /// Converter for Morph receipts. @@ -44,74 +47,47 @@ impl MorphReceiptBuilder { where N: NodePrimitives, { - let ConvertReceiptInput { - tx, - meta, - receipt, - gas_used, - next_log_index, - } = input; + let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = morph_fee_fields(&input.receipt); - let from = tx.signer(); - let (contract_address, to) = match tx.kind() { - TxKind::Create => (Some(from.create(tx.nonce())), None), - TxKind::Call(addr) => (None, Some(Address(*addr))), - }; - - let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = morph_fee_fields(&receipt); + let core_receipt = build_receipt(input, None, |receipt, next_log_index, meta| { + let map_logs = |receipt: Receipt| { + let Receipt { + status, + cumulative_gas_used, + logs, + } = receipt; + let logs = Log::collect_for_receipt(next_log_index, meta, logs); + Receipt { + status, + cumulative_gas_used, + logs, + } + }; - let map_logs = |receipt: Receipt| { - let Receipt { - status, - cumulative_gas_used, - logs, - } = receipt; - let logs = Log::collect_for_receipt(next_log_index, meta, logs); - Receipt { - status, - cumulative_gas_used, - logs, + match receipt { + MorphReceipt::Legacy(receipt) => { + MorphReceiptEnvelope::Legacy(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::Eip2930(receipt) => { + MorphReceiptEnvelope::Eip2930(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::Eip1559(receipt) => { + MorphReceiptEnvelope::Eip1559(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::Eip7702(receipt) => { + MorphReceiptEnvelope::Eip7702(map_logs(receipt.inner).into_with_bloom()) + } + MorphReceipt::L1Msg(receipt) => { + MorphReceiptEnvelope::L1Message(map_logs(receipt).into_with_bloom()) + } + MorphReceipt::Morph(receipt) => { + MorphReceiptEnvelope::Morph(map_logs(receipt.inner).into_with_bloom()) + } } - }; - - let receipt_envelope = match receipt { - MorphReceipt::Legacy(receipt) => { - MorphReceiptEnvelope::Legacy(map_logs(receipt.inner).into_with_bloom()) - } - MorphReceipt::Eip2930(receipt) => { - MorphReceiptEnvelope::Eip2930(map_logs(receipt.inner).into_with_bloom()) - } - MorphReceipt::Eip1559(receipt) => { - MorphReceiptEnvelope::Eip1559(map_logs(receipt.inner).into_with_bloom()) - } - MorphReceipt::Eip7702(receipt) => { - MorphReceiptEnvelope::Eip7702(map_logs(receipt.inner).into_with_bloom()) - } - MorphReceipt::L1Msg(receipt) => { - MorphReceiptEnvelope::L1Message(map_logs(receipt).into_with_bloom()) - } - MorphReceipt::Morph(receipt) => { - MorphReceiptEnvelope::Morph(map_logs(receipt.inner).into_with_bloom()) - } - }; - - let inner = TransactionReceipt { - inner: receipt_envelope, - transaction_hash: meta.tx_hash, - transaction_index: Some(meta.index), - block_hash: Some(meta.block_hash), - block_number: Some(meta.block_number), - gas_used, - effective_gas_price: tx.effective_gas_price(meta.base_fee), - blob_gas_used: None, - blob_gas_price: None, - from, - to, - contract_address, - }; + }); let receipt = MorphRpcReceipt { - inner, + inner: core_receipt, l1_fee, fee_rate, token_scale, @@ -127,6 +103,13 @@ impl MorphReceiptBuilder { } } +impl LoadReceipt for MorphEthApi +where + N: MorphNodeCore, + Rpc: RpcConvert, +{ +} + fn morph_fee_fields( receipt: &MorphReceipt, ) -> (U256, Option, Option, Option, Option) { From f361f2875505efc0524c9d7611a379f3c24c69c7 Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 4 Feb 2026 00:35:41 +0800 Subject: [PATCH 15/19] fix: fixed token amount to eth --- crates/revm/src/handler.rs | 4 ++-- crates/revm/src/token_fee.rs | 14 +++++++------- crates/rpc/src/eth/call.rs | 11 +++++++++-- crates/txpool/src/morph_tx_validation.rs | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index e85d403..f03f89a 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -338,7 +338,7 @@ where } // Calculate token amount required for total fee - let token_amount_required = token_fee_info.calculate_token_amount(reimburse_eth); + let token_amount_required = token_fee_info.eth_to_token_amount(reimburse_eth); // Get mutable access to journal components let journal = evm.ctx().journal_mut(); @@ -421,7 +421,7 @@ where let total_eth_fee = l2_gas_fee.saturating_add(l1_data_fee); // Calculate token amount required for total fee - let token_amount_required = token_fee_info.calculate_token_amount(total_eth_fee); + let token_amount_required = token_fee_info.eth_to_token_amount(total_eth_fee); // Determine fee limit let mut fee_limit = tx.fee_limit.unwrap_or_default(); diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs index 714f8b3..70e8840 100644 --- a/crates/revm/src/token_fee.rs +++ b/crates/revm/src/token_fee.rs @@ -128,8 +128,8 @@ impl TokenFeeInfo { /// Calculate the token amount required for a given ETH amount. /// /// Uses the price ratio and scale to convert ETH value to token amount. - pub fn calculate_token_amount(&self, eth_amount: U256) -> U256 { - if self.price_ratio.is_zero() { + pub fn eth_to_token_amount(&self, eth_amount: U256) -> U256 { + if self.price_ratio.is_zero() || self.scale.is_zero() { return U256::ZERO; } @@ -147,7 +147,7 @@ impl TokenFeeInfo { /// Check if the caller has sufficient token balance for the given ETH amount. pub fn has_sufficient_balance(&self, eth_amount: U256) -> bool { - let required = self.calculate_token_amount(eth_amount); + let required = self.eth_to_token_amount(eth_amount); self.balance >= required } } @@ -367,7 +367,7 @@ mod tests { } #[test] - fn test_calculate_token_amount() { + fn test_eth_to_token_amount() { let info = TokenFeeInfo { price_ratio: U256::from(2_000_000_000_000_000_000u128), // 2 ETH per token scale: U256::from(1_000_000_000_000_000_000u128), // 1e18 @@ -376,12 +376,12 @@ mod tests { // 1 ETH should give 0.5 tokens let eth_amount = U256::from(1_000_000_000_000_000_000u128); // 1 ETH - let token_amount = info.calculate_token_amount(eth_amount); + let token_amount = info.eth_to_token_amount(eth_amount); assert_eq!(token_amount, U256::from(500_000_000_000_000_000u128)); // 0.5 tokens } #[test] - fn test_calculate_token_amount_zero_ratio() { + fn test_eth_to_token_amount_zero_ratio() { let info = TokenFeeInfo { price_ratio: U256::ZERO, scale: U256::from(1_000_000_000_000_000_000u128), @@ -389,7 +389,7 @@ mod tests { }; let eth_amount = U256::from(1_000_000_000_000_000_000u128); - let token_amount = info.calculate_token_amount(eth_amount); + let token_amount = info.eth_to_token_amount(eth_amount); assert_eq!(token_amount, U256::ZERO); } diff --git a/crates/rpc/src/eth/call.rs b/crates/rpc/src/eth/call.rs index 325eb5d..2bc0c47 100644 --- a/crates/rpc/src/eth/call.rs +++ b/crates/rpc/src/eth/call.rs @@ -183,7 +183,7 @@ where _ => token_fee_info.balance, }; - let l1_fee_in_token = token_fee_info.calculate_token_amount(l1_fee); + let l1_fee_in_token = token_fee_info.eth_to_token_amount(l1_fee); if l1_fee_in_token >= limit { return Err(MorphEthApiError::InsufficientFundsForL1Fee); } @@ -229,5 +229,12 @@ fn token_amount_to_eth(token_amount: U256, info: &TokenFeeInfo) -> Option if info.price_ratio.is_zero() || info.scale.is_zero() { return None; } - Some(token_amount.saturating_mul(info.price_ratio) / info.scale) + let (eth_amount, remainder) = token_amount + .saturating_mul(info.price_ratio) + .div_rem(info.scale); + if remainder.is_zero() { + Some(eth_amount) + } else { + Some(eth_amount.saturating_add(U256::from(1))) + } } diff --git a/crates/txpool/src/morph_tx_validation.rs b/crates/txpool/src/morph_tx_validation.rs index 9cd3359..673bb07 100644 --- a/crates/txpool/src/morph_tx_validation.rs +++ b/crates/txpool/src/morph_tx_validation.rs @@ -103,7 +103,7 @@ pub fn validate_morph_tx( let total_eth_fee = gas_fee.saturating_add(input.l1_data_fee); // Convert ETH fee to token amount - let required_token_amount = token_info.calculate_token_amount(total_eth_fee); + let required_token_amount = token_info.eth_to_token_amount(total_eth_fee); // Check fee_limit >= required_token_amount if fee_limit < required_token_amount { From 85bf954b6c0b05aec7b29b79841b9751e664b4c1 Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 4 Feb 2026 00:37:00 +0800 Subject: [PATCH 16/19] style: fmt code --- crates/rpc/src/eth/call.rs | 4 ++-- crates/rpc/src/eth/mod.rs | 2 +- crates/rpc/src/eth/receipt.rs | 8 ++++++-- crates/rpc/src/eth/transaction.rs | 4 +--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/rpc/src/eth/call.rs b/crates/rpc/src/eth/call.rs index 2bc0c47..a4dd6a9 100644 --- a/crates/rpc/src/eth/call.rs +++ b/crates/rpc/src/eth/call.rs @@ -29,7 +29,8 @@ impl EstimateCall for MorphEthApi where N: MorphNodeCore, N::Provider: ChainSpecProvider, - Rpc: reth_rpc_convert::RpcConvert, + Rpc: + reth_rpc_convert::RpcConvert, MorphEthApiError: reth_rpc_eth_types::error::FromEvmError, { } @@ -197,7 +198,6 @@ where } } - fn caller_gas_allowance_with_eth( balance: U256, value: U256, diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 13a91d9..4aa1f30 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -1,7 +1,7 @@ //! Morph `eth_` RPC wiring and conversions. -use crate::eth::receipt::MorphReceiptConverter; use crate::MorphEthApiError; +use crate::eth::receipt::MorphReceiptConverter; use crate::types::{MorphRpcReceipt, MorphRpcTransaction, MorphTransactionRequest}; use alloy_rpc_types_eth::Header as RpcHeader; use eyre::Result; diff --git a/crates/rpc/src/eth/receipt.rs b/crates/rpc/src/eth/receipt.rs index 8ee484b..52f01cc 100644 --- a/crates/rpc/src/eth/receipt.rs +++ b/crates/rpc/src/eth/receipt.rs @@ -7,7 +7,10 @@ use alloy_primitives::{U64, U256}; use alloy_rpc_types_eth::Log; use morph_primitives::{MorphReceipt, MorphReceiptEnvelope}; use reth_primitives_traits::NodePrimitives; -use reth_rpc_convert::{RpcConvert, transaction::{ConvertReceiptInput, ReceiptConverter}}; +use reth_rpc_convert::{ + RpcConvert, + transaction::{ConvertReceiptInput, ReceiptConverter}, +}; use reth_rpc_eth_api::helpers::LoadReceipt; use reth_rpc_eth_types::{EthApiError, receipt::build_receipt}; use std::fmt::Debug; @@ -47,7 +50,8 @@ impl MorphReceiptBuilder { where N: NodePrimitives, { - let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = morph_fee_fields(&input.receipt); + let (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit) = + morph_fee_fields(&input.receipt); let core_receipt = build_receipt(input, None, |receipt, next_log_index, meta| { let map_logs = |receipt: Receipt| { diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs index 2ecc818..877e809 100644 --- a/crates/rpc/src/eth/transaction.rs +++ b/crates/rpc/src/eth/transaction.rs @@ -204,9 +204,7 @@ fn build_morph_tx_from_env( ) -> Result { let fee_token_id = u16::try_from(fee_token_id.to::()) .map_err(|_| EthApiError::InvalidParams("invalid token".to_string()))?; - let chain_id = tx_env - .chain_id() - .unwrap_or(evm_env.cfg_env.chain_id); + let chain_id = tx_env.chain_id().unwrap_or(evm_env.cfg_env.chain_id); let input = tx_env.input().clone(); let to = tx_env.kind(); let max_fee_per_gas = tx_env.max_fee_per_gas(); From 57add8b244f55e3d72ebc0bcca649dc700557793 Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 5 Feb 2026 11:35:22 +0800 Subject: [PATCH 17/19] refactor: make serde-bincode-compat feature optional in evm --- crates/evm/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index ce6353f..97d6884 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] morph-chainspec.workspace = true -morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } +morph-primitives.workspace = true morph-revm.workspace = true reth-chainspec.workspace = true From 03cfe14f45880cf96afcaca6c8acc0c5d587ef29 Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 5 Feb 2026 14:36:00 +0800 Subject: [PATCH 18/19] docs: add some docs comments --- crates/rpc/Cargo.toml | 2 +- crates/rpc/src/eth/call.rs | 14 ++++++++++++++ crates/rpc/src/eth/receipt.rs | 4 ++++ crates/rpc/src/eth/transaction.rs | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 91f32cd..889991a 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -11,7 +11,7 @@ workspace = true [dependencies] # Morph crates -morph-primitives = { workspace = true, features = ["serde", "reth-codec"] } +morph-primitives = { workspace = true, features = ["serde-bincode-compat", "reth-codec"] } morph-chainspec.workspace = true morph-revm = { workspace = true, features = ["rpc"] } morph-evm = { workspace = true, features = ["rpc"] } diff --git a/crates/rpc/src/eth/call.rs b/crates/rpc/src/eth/call.rs index a4dd6a9..b368ada 100644 --- a/crates/rpc/src/eth/call.rs +++ b/crates/rpc/src/eth/call.rs @@ -99,6 +99,9 @@ where Rpc: reth_rpc_convert::RpcConvert, { + /// Estimates the L1 data fee for the given transaction. + /// + /// Returns zero for L1 message transactions since they don't pay L1 fees. fn estimate_l1_fee( &self, db: &mut DB, @@ -198,6 +201,10 @@ where } } +/// Calculates the caller's gas allowance when paying with ETH. +/// +/// Subtracts the transaction value and L1 fee from the balance, +/// then converts the remaining balance to gas units. fn caller_gas_allowance_with_eth( balance: U256, value: U256, @@ -212,6 +219,9 @@ fn caller_gas_allowance_with_eth( Ok(gas_allowance_from_balance(available, gas_price)) } +/// Converts a balance to gas units based on the gas price. +/// +/// Returns `u64::MAX` if gas price is zero (unlimited gas). fn gas_allowance_from_balance(balance: U256, gas_price: u128) -> u64 { if gas_price == 0 { return u64::MAX; @@ -225,6 +235,10 @@ fn gas_allowance_from_balance(balance: U256, gas_price: u128) -> u64 { } } +/// Converts a token amount to ETH equivalent using the fee token info. +/// +/// Uses the formula: `eth = token_amount * price_ratio / scale`. +/// Returns `None` if price_ratio or scale is zero. fn token_amount_to_eth(token_amount: U256, info: &TokenFeeInfo) -> Option { if info.price_ratio.is_zero() || info.scale.is_zero() { return None; diff --git a/crates/rpc/src/eth/receipt.rs b/crates/rpc/src/eth/receipt.rs index 52f01cc..0f602fc 100644 --- a/crates/rpc/src/eth/receipt.rs +++ b/crates/rpc/src/eth/receipt.rs @@ -114,6 +114,10 @@ where { } +/// Extracts Morph-specific fee fields from a receipt. +/// +/// Returns a tuple of (l1_fee, fee_token_id, fee_rate, token_scale, fee_limit). +/// L1 message receipts return zero/None for all fee fields. fn morph_fee_fields( receipt: &MorphReceipt, ) -> (U256, Option, Option, Option, Option) { diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs index 877e809..c0eda93 100644 --- a/crates/rpc/src/eth/transaction.rs +++ b/crates/rpc/src/eth/transaction.rs @@ -151,6 +151,9 @@ impl TryIntoTxEnv for MorphTransactionRequest { } } +/// Converts an Ethereum transaction envelope to a Morph envelope. +/// +/// EIP-4844 blob transactions are not supported on Morph. fn morph_envelope_from_ethereum( env: EthereumTxEnvelope, ) -> Result { @@ -163,6 +166,10 @@ fn morph_envelope_from_ethereum( } } +/// Builds a [`TxMorph`] from an RPC transaction request. +/// +/// Extracts fields from the request and constructs a Morph transaction +/// with the specified fee token ID and fee limit. fn build_morph_tx_from_request( req: &alloy_rpc_types_eth::TransactionRequest, fee_token_id: U64, @@ -195,6 +202,9 @@ fn build_morph_tx_from_request( }) } +/// Builds a [`TxMorph`] from an existing transaction environment. +/// +/// Used for encoding transactions for L1 fee calculation. fn build_morph_tx_from_env( tx_env: &MorphTxEnv, fee_token_id: U64, @@ -225,6 +235,9 @@ fn build_morph_tx_from_env( }) } +/// Encodes a transaction for L1 fee calculation. +/// +/// Returns the RLP-encoded bytes used to calculate the L1 data fee. fn encode_tx_for_l1_fee( tx_env: &MorphTxEnv, access_list: AccessList, @@ -245,6 +258,7 @@ fn encode_tx_for_l1_fee( } } +/// Encodes a transaction using EIP-2718 typed transaction encoding. fn encode_2718(tx: T) -> Bytes { let mut out = Vec::with_capacity(tx.encode_2718_len()); tx.encode_2718(&mut out); From 87e1beafdd8ffca0b28f9160c4fc5d850824ad9e Mon Sep 17 00:00:00 2001 From: panos Date: Thu, 5 Feb 2026 14:53:25 +0800 Subject: [PATCH 19/19] docs: add more docs --- crates/rpc/src/error.rs | 8 ++++++++ crates/rpc/src/eth/receipt.rs | 2 ++ crates/rpc/src/eth/transaction.rs | 12 +++++++++++- crates/rpc/src/types/receipt.rs | 3 +++ crates/rpc/src/types/request.rs | 6 ++++++ crates/rpc/src/types/transaction.rs | 7 +++++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/rpc/src/error.rs b/crates/rpc/src/error.rs index 13fe2e3..a0f2b57 100644 --- a/crates/rpc/src/error.rs +++ b/crates/rpc/src/error.rs @@ -78,6 +78,7 @@ pub enum MorphEthApiError { InvalidFeeToken, } +/// Converts [`MorphEthApiError`] to a JSON-RPC error object. impl From for jsonrpsee::types::ErrorObject<'static> { fn from(err: MorphEthApiError) -> Self { match err { @@ -139,6 +140,7 @@ impl From for jsonrpsee::types::ErrorObject<'static> { } } +/// Extracts the inner [`EthApiError`] if present. impl AsEthApiError for MorphEthApiError { fn as_err(&self) -> Option<&EthApiError> { match self { @@ -151,24 +153,28 @@ impl AsEthApiError for MorphEthApiError { // Note: `FromEthApiError` is auto-implemented via blanket impl for any `T: From`. // We get it for free since we have `#[from] EthApiError` above. +/// Converts EVM halt reasons to [`MorphEthApiError`]. impl FromEvmHalt> for MorphEthApiError { fn from_evm_halt(halt: HaltReasonFor, gas_limit: u64) -> Self { Self::Eth(EthApiError::from_evm_halt(halt, gas_limit)) } } +/// Converts EVM revert output to [`MorphEthApiError`]. impl FromRevert for MorphEthApiError { fn from_revert(output: alloy_primitives::Bytes) -> Self { Self::Eth(EthApiError::from_revert(output)) } } +/// Converts [`ProviderError`] to [`MorphEthApiError`]. impl From for MorphEthApiError { fn from(err: ProviderError) -> Self { Self::Eth(err.into()) } } +/// Converts [`EVMError`] to [`MorphEthApiError`]. impl From> for MorphEthApiError where T: Into, @@ -179,12 +185,14 @@ where } } +/// Converts [`TransactionConversionError`] to [`MorphEthApiError`]. impl From for MorphEthApiError { fn from(err: TransactionConversionError) -> Self { Self::Eth(err.into()) } } +/// Infallible conversion (never fails). impl From for MorphEthApiError { fn from(err: Infallible) -> Self { match err {} diff --git a/crates/rpc/src/eth/receipt.rs b/crates/rpc/src/eth/receipt.rs index 0f602fc..3f378b3 100644 --- a/crates/rpc/src/eth/receipt.rs +++ b/crates/rpc/src/eth/receipt.rs @@ -46,6 +46,7 @@ struct MorphReceiptBuilder { } impl MorphReceiptBuilder { + /// Creates a new builder from a receipt conversion input. fn new(input: ConvertReceiptInput<'_, N>) -> Self where N: NodePrimitives, @@ -102,6 +103,7 @@ impl MorphReceiptBuilder { Self { receipt } } + /// Consumes the builder and returns the built receipt. fn build(self) -> MorphRpcReceipt { self.receipt } diff --git a/crates/rpc/src/eth/transaction.rs b/crates/rpc/src/eth/transaction.rs index c0eda93..11b0318 100644 --- a/crates/rpc/src/eth/transaction.rs +++ b/crates/rpc/src/eth/transaction.rs @@ -20,6 +20,7 @@ use morph_primitives::{MorphTxEnvelope, TxMorph}; use morph_revm::{MorphBlockEnv, MorphTxEnv}; use reth_evm::EvmEnv; +/// Converts a consensus [`MorphTxEnvelope`] to an RPC [`MorphRpcTransaction`]. impl FromConsensusTx for MorphRpcTransaction { type TxInfo = TransactionInfo; type Err = Infallible; @@ -60,6 +61,9 @@ impl FromConsensusTx for MorphRpcTransaction { } } +/// Converts a [`MorphTransactionRequest`] into a simulated transaction envelope. +/// +/// Handles both standard Ethereum transactions and Morph-specific fee token transactions. impl TryIntoSimTx for MorphTransactionRequest { fn try_into_sim_tx(self) -> Result> { if let Some(fee_token_id) = self.fee_token_id.filter(|id| id.to::() > 0) { @@ -86,6 +90,9 @@ impl TryIntoSimTx for MorphTransactionRequest { } } +/// Builds and signs a transaction from an RPC request. +/// +/// Supports both standard Ethereum transactions and Morph fee token transactions. impl SignableTxRequest for MorphTransactionRequest { async fn try_build_and_sign( self, @@ -114,6 +121,9 @@ impl SignableTxRequest for MorphTransactionRequest { } } +/// Converts a transaction request into a transaction environment for EVM execution. +/// +/// Also encodes the transaction for L1 fee calculation. impl TryIntoTxEnv for MorphTransactionRequest { type Err = EthApiError; @@ -244,7 +254,7 @@ fn encode_tx_for_l1_fee( evm_env: &EvmEnv, inner: alloy_rpc_types_eth::TransactionRequest, ) -> Result { - if tx_env.fee_token_id.unwrap_or_default() > 0 || tx_env.fee_limit.is_some() { + if tx_env.fee_token_id.unwrap_or_default() > 0 { let fee_token_id = U64::from(tx_env.fee_token_id.unwrap_or_default()); let fee_limit = tx_env.fee_limit.unwrap_or_default(); let morph_tx = diff --git a/crates/rpc/src/types/receipt.rs b/crates/rpc/src/types/receipt.rs index 58c0fe4..86f7d32 100644 --- a/crates/rpc/src/types/receipt.rs +++ b/crates/rpc/src/types/receipt.rs @@ -39,6 +39,9 @@ pub struct MorphRpcReceipt { pub fee_token_id: Option, } +/// Implementation of [`ReceiptResponse`] for Morph receipts. +/// +/// Delegates all methods to the inner receipt. impl ReceiptResponse for MorphRpcReceipt { fn contract_address(&self) -> Option
{ self.inner.contract_address diff --git a/crates/rpc/src/types/request.rs b/crates/rpc/src/types/request.rs index eccf62b..354926a 100644 --- a/crates/rpc/src/types/request.rs +++ b/crates/rpc/src/types/request.rs @@ -41,18 +41,23 @@ pub struct MorphTransactionRequest { pub fee_limit: Option, } +/// Returns a reference to the inner [`TransactionRequest`]. impl AsRef for MorphTransactionRequest { fn as_ref(&self) -> &TransactionRequest { &self.inner } } +/// Returns a mutable reference to the inner [`TransactionRequest`]. impl AsMut for MorphTransactionRequest { fn as_mut(&mut self) -> &mut TransactionRequest { &mut self.inner } } +/// Creates a [`MorphTransactionRequest`] from a standard [`TransactionRequest`]. +/// +/// Sets `fee_token_id` and `fee_limit` to `None`. impl From for MorphTransactionRequest { fn from(value: TransactionRequest) -> Self { Self { @@ -63,6 +68,7 @@ impl From for MorphTransactionRequest { } } +/// Extracts the inner [`TransactionRequest`] from a [`MorphTransactionRequest`]. impl From for TransactionRequest { fn from(value: MorphTransactionRequest) -> Self { value.inner diff --git a/crates/rpc/src/types/transaction.rs b/crates/rpc/src/types/transaction.rs index b89811e..d12e860 100644 --- a/crates/rpc/src/types/transaction.rs +++ b/crates/rpc/src/types/transaction.rs @@ -42,12 +42,16 @@ pub struct MorphRpcTransaction { pub fee_limit: Option, } +/// Implementation of [`Typed2718`] for Morph RPC transactions. impl Typed2718 for MorphRpcTransaction { fn ty(&self) -> u8 { self.inner.ty() } } +/// Implementation of [`ConsensusTransaction`] for Morph RPC transactions. +/// +/// Delegates all consensus transaction methods to the inner transaction. impl ConsensusTransaction for MorphRpcTransaction { fn chain_id(&self) -> Option { self.inner.chain_id() @@ -122,6 +126,9 @@ impl ConsensusTransaction for MorphRpcTransaction { } } +/// Implementation of [`TransactionResponse`] for Morph RPC transactions. +/// +/// Provides RPC-specific transaction metadata like block hash and index. impl TransactionResponse for MorphRpcTransaction { fn tx_hash(&self) -> alloy_primitives::B256 { self.inner.tx_hash()