From caf771520fc5f2ea82db69bf54979c03116c6627 Mon Sep 17 00:00:00 2001 From: panos Date: Fri, 23 Jan 2026 17:46:16 +0800 Subject: [PATCH] feat: add morph l2 engine api - Add MorphL2EngineApi trait for block building and validation - Add MorphL2EngineRpcServer for JSON-RPC implementation - Add MorphValidationContext for state root validation - Refactor hardfork code into separate modules (block/curie.rs, block/receipt.rs) - Fix state root validation logic for MPT fork --- .gitignore | 3 +- Cargo.lock | 311 +++++++++++++++++++++++++++++ Cargo.toml | 2 + crates/engine-api/Cargo.toml | 34 ++++ crates/engine-api/src/api.rs | 95 +++++++++ crates/engine-api/src/error.rs | 116 +++++++++++ crates/engine-api/src/lib.rs | 32 +++ crates/engine-api/src/rpc.rs | 147 ++++++++++++++ crates/engine-api/src/validator.rs | 118 +++++++++++ crates/payload/types/Cargo.toml | 2 +- 10 files changed, 858 insertions(+), 2 deletions(-) create mode 100644 crates/engine-api/Cargo.toml create mode 100644 crates/engine-api/src/api.rs create mode 100644 crates/engine-api/src/error.rs create mode 100644 crates/engine-api/src/lib.rs create mode 100644 crates/engine-api/src/rpc.rs create mode 100644 crates/engine-api/src/validator.rs diff --git a/.gitignore b/.gitignore index cac0b30..bfe5531 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target CLAUDE.md -.DS_Store \ No newline at end of file +.DS_Store +/docs \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index bf7bf37..316adbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1429,6 +1429,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1531,6 +1537,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concat-kdf" version = "0.1.0" @@ -2452,6 +2468,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper", +] + [[package]] name = "futures-util" version = "0.3.31" @@ -2543,6 +2569,52 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gmp-mpfr-sys" version = "1.6.8" @@ -2821,6 +2893,7 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -3173,6 +3246,28 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -3199,12 +3294,41 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" dependencies = [ + "jsonrpsee-client-transport", "jsonrpsee-core", + "jsonrpsee-http-client", "jsonrpsee-proc-macros", "jsonrpsee-server", "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" +dependencies = [ + "base64", + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "pin-project", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 2.0.17", "tokio", + "tokio-rustls", + "tokio-util", "tracing", + "url", ] [[package]] @@ -3215,6 +3339,7 @@ checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" dependencies = [ "async-trait", "bytes", + "futures-timer", "futures-util", "http", "http-body", @@ -3228,8 +3353,33 @@ dependencies = [ "serde_json", "thiserror 2.0.17", "tokio", + "tokio-stream", "tower", "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" +dependencies = [ + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower", + "url", ] [[package]] @@ -3284,6 +3434,32 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7902885de4779f711a95d82c8da2d7e5f9f3a7c7cfa44d51c067fd1c29d72a3c" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower", + "url", +] + [[package]] name = "jsonwebtoken" version = "9.3.1" @@ -3678,6 +3854,23 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "morph-engine-api" +version = "0.7.5" +dependencies = [ + "alloy-genesis", + "alloy-primitives", + "async-trait", + "auto_impl", + "jsonrpsee", + "morph-chainspec", + "morph-payload-types", + "morph-primitives", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + [[package]] name = "morph-evm" version = "0.7.5" @@ -6711,6 +6904,7 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -6741,6 +6935,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.8" @@ -6942,6 +7163,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "serde" version = "1.0.228" @@ -8165,6 +8392,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.5", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "widestring" version = "1.2.1" @@ -8314,6 +8559,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -8359,6 +8613,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8407,6 +8676,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8425,6 +8700,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8443,6 +8724,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8473,6 +8760,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8491,6 +8784,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8509,6 +8808,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8527,6 +8832,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index e54fe00..1e52c69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ resolver = "3" members = [ "crates/chainspec", "crates/consensus", + "crates/engine-api", "crates/evm", "crates/payload/builder", "crates/payload/types", @@ -39,6 +40,7 @@ all = "warn" [workspace.dependencies] 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-payload-builder = { path = "crates/payload/builder", default-features = false } morph-payload-types = { path = "crates/payload/types", default-features = false } diff --git a/crates/engine-api/Cargo.toml b/crates/engine-api/Cargo.toml new file mode 100644 index 0000000..31f74ad --- /dev/null +++ b/crates/engine-api/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "morph-engine-api" +description = "Morph L2 Engine API implementation" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +# morph +morph-chainspec.workspace = true +morph-payload-types.workspace = true +morph-primitives.workspace = true + +# alloy +alloy-primitives.workspace = true + +# misc +async-trait.workspace = true +auto_impl.workspace = true +jsonrpsee = { workspace = true, features = ["server", "macros"] } +thiserror.workspace = true +tracing.workspace = true + +[dev-dependencies] +alloy-genesis.workspace = true +serde_json.workspace = true + +[features] +default = [] diff --git a/crates/engine-api/src/api.rs b/crates/engine-api/src/api.rs new file mode 100644 index 0000000..cc97073 --- /dev/null +++ b/crates/engine-api/src/api.rs @@ -0,0 +1,95 @@ +//! Morph L2 Engine API trait definition. +//! +//! This module defines the L2 Engine API trait that provides methods for +//! building, validating, and importing L2 blocks. These methods are called +//! by the sequencer to produce new blocks. + +use crate::EngineApiResult; +use alloy_primitives::B256; +use morph_payload_types::{AssembleL2BlockParams, ExecutableL2Data, GenericResponse, SafeL2Data}; +use morph_primitives::MorphHeader; + +/// Morph L2 Engine API trait. +/// +/// This trait defines the interface for the L2 Engine API, which is used by +/// the sequencer to interact with the execution layer for block production. +/// +/// The API is designed to be compatible with the go-ethereum implementation +/// and provides the following methods: +/// +/// - `assemble_l2_block`: Build a new L2 block with the given transactions +/// - `validate_l2_block`: Validate an L2 block without importing it +/// - `new_l2_block`: Import and finalize a new L2 block +/// - `new_safe_l2_block`: Import a safe L2 block from derivation +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc, &, Box)] +pub trait MorphL2EngineApi: Send + Sync { + /// Build a new L2 block with the given transactions. + /// + /// This method is called by the sequencer to assemble a new block containing + /// the provided transactions. The transactions should include L1 messages + /// at the beginning, followed by L2 transactions. + /// + /// # Arguments + /// + /// * `params` - The parameters for assembling the block, including: + /// - `number`: The expected block number (must be `current_head + 1`) + /// - `transactions`: RLP-encoded transactions to include in the block + /// + /// # Returns + /// + /// Returns the execution result including state root, receipts root, etc. + async fn assemble_l2_block( + &self, + params: AssembleL2BlockParams, + ) -> EngineApiResult; + + /// Validate an L2 block without importing it. + /// + /// This method validates a block by re-executing it and comparing the results. + /// If the block has been previously validated (cached), it returns immediately. + /// + /// # Arguments + /// + /// * `data` - The block data to validate, including execution results + /// + /// # Returns + /// + /// Returns a `GenericResponse` indicating whether validation succeeded. + async fn validate_l2_block(&self, data: ExecutableL2Data) -> EngineApiResult; + + /// Import and finalize a new L2 block. + /// + /// This method imports a validated block into the chain and updates the + /// canonical head. The block must have been previously validated. + /// + /// # Arguments + /// + /// * `data` - The block data to import + /// * `batch_hash` - Optional batch hash if this block is a batch point + /// + /// # Returns + /// + /// Returns `Ok(())` on success. + async fn new_l2_block( + &self, + data: ExecutableL2Data, + batch_hash: Option, + ) -> EngineApiResult<()>; + + /// Import a safe L2 block from derivation. + /// + /// This method is used by the derivation pipeline to import blocks that + /// have been confirmed on L1. Unlike `new_l2_block`, this method accepts + /// only the inputs needed to re-execute the block and computes the + /// execution results. + /// + /// # Arguments + /// + /// * `data` - The safe block data containing only input fields + /// + /// # Returns + /// + /// Returns the header of the imported block. + async fn new_safe_l2_block(&self, data: SafeL2Data) -> EngineApiResult; +} diff --git a/crates/engine-api/src/error.rs b/crates/engine-api/src/error.rs new file mode 100644 index 0000000..7d8b77a --- /dev/null +++ b/crates/engine-api/src/error.rs @@ -0,0 +1,116 @@ +//! Morph Engine API error types. + +use alloy_primitives::B256; +use jsonrpsee::types::{ErrorObject, ErrorObjectOwned}; +use thiserror::Error; + +/// Morph Engine API errors. +#[derive(Debug, Error)] +pub enum MorphEngineApiError { + /// Block number is not continuous with the current chain head. + #[error("discontinuous block number {actual}, expected {expected}")] + DiscontinuousBlockNumber { + /// Expected block number. + expected: u64, + /// Actual block number. + actual: u64, + }, + + /// Wrong parent hash. + #[error("wrong parent hash {actual}, expected {expected}")] + WrongParentHash { + /// Expected parent hash. + expected: B256, + /// Actual parent hash. + actual: B256, + }, + + /// Invalid transaction. + #[error("transaction at index {index} is invalid: {message}")] + InvalidTransaction { + /// Transaction index. + index: usize, + /// Error message. + message: String, + }, + + /// Block build error. + #[error("failed to build block: {0}")] + BlockBuildError(String), + + /// Block validation failed. + #[error("block validation failed: {0}")] + ValidationFailed(String), + + /// Block execution error. + #[error("block execution failed: {0}")] + ExecutionFailed(String), + + /// Withdraw trie root mismatch. + #[error("withdraw trie root mismatch: expected {expected}, got {actual}")] + WithdrawTrieRootMismatch { + /// Expected withdraw trie root. + expected: B256, + /// Actual withdraw trie root. + actual: B256, + }, + + /// Database error. + #[error("database error: {0}")] + Database(String), + + /// Internal error. + #[error("internal error: {0}")] + Internal(String), +} + +impl MorphEngineApiError { + /// Converts this error into a JSON-RPC error object. + pub fn into_rpc_error(self) -> ErrorObjectOwned { + // Use custom error codes + // -32000 to -32099: Server error (reserved for implementation-defined server-errors) + let code = match &self { + Self::DiscontinuousBlockNumber { .. } | Self::WrongParentHash { .. } => -32001, + Self::InvalidTransaction { .. } => -32002, + Self::BlockBuildError(_) => -32003, + Self::ValidationFailed(_) => -32004, + Self::ExecutionFailed(_) => -32005, + Self::WithdrawTrieRootMismatch { .. } => -32006, + Self::Database(_) => -32010, + Self::Internal(_) => -32099, + }; + + ErrorObject::owned(code, self.to_string(), None::<()>) + } +} + +impl From for ErrorObjectOwned { + fn from(err: MorphEngineApiError) -> Self { + err.into_rpc_error() + } +} + +/// A type alias for the result of Engine API methods. +pub type EngineApiResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_codes() { + let err = MorphEngineApiError::DiscontinuousBlockNumber { + expected: 100, + actual: 102, + }; + let rpc_err = err.into_rpc_error(); + assert_eq!(rpc_err.code(), -32001); + + let err = MorphEngineApiError::InvalidTransaction { + index: 0, + message: "invalid signature".to_string(), + }; + let rpc_err = err.into_rpc_error(); + assert_eq!(rpc_err.code(), -32002); + } +} diff --git a/crates/engine-api/src/lib.rs b/crates/engine-api/src/lib.rs new file mode 100644 index 0000000..a41cd69 --- /dev/null +++ b/crates/engine-api/src/lib.rs @@ -0,0 +1,32 @@ +//! Morph L2 Engine API implementation. +//! +//! This crate provides the **custom L2 Engine API** for Morph, including: +//! +//! - [`MorphL2EngineApi`]: The L2 Engine API trait for block building and validation +//! - [`MorphL2EngineRpcServer`]: The JSON-RPC server implementation +//! - [`MorphValidationContext`]: Validation context with Emerald hardfork support +//! +//! # L2 Engine API +//! +//! The L2 Engine API provides methods for the sequencer to interact with the +//! execution layer. These are **Morph-specific** methods, different from the +//! standard Ethereum Engine API: +//! +//! - `engine_assembleL2Block`: Build a new block with given transactions +//! - `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))] + +mod api; +mod error; +mod rpc; +mod validator; + +pub use api::MorphL2EngineApi; +pub use error::{EngineApiResult, MorphEngineApiError}; +pub use rpc::{MorphL2EngineRpcHandler, MorphL2EngineRpcServer, into_rpc_result}; +pub use validator::{MorphValidationContext, should_validate_state_root}; diff --git a/crates/engine-api/src/rpc.rs b/crates/engine-api/src/rpc.rs new file mode 100644 index 0000000..93dacd6 --- /dev/null +++ b/crates/engine-api/src/rpc.rs @@ -0,0 +1,147 @@ +//! Morph L2 Engine API JSON-RPC handler. +//! +//! This module implements the JSON-RPC interface for the L2 Engine API, +//! allowing the sequencer to interact with the execution layer via RPC. + +use crate::{EngineApiResult, api::MorphL2EngineApi}; +use alloy_primitives::B256; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; +use morph_payload_types::{AssembleL2BlockParams, ExecutableL2Data, GenericResponse, SafeL2Data}; +use morph_primitives::MorphHeader; +use std::sync::Arc; + +/// Morph L2 Engine RPC API trait. +/// +/// This trait defines the JSON-RPC interface for the L2 Engine API. +/// It uses the `jsonrpsee` proc macro to generate the RPC server implementation. +#[rpc(server, namespace = "engine")] +pub trait MorphL2EngineRpc { + /// Build a new L2 block with the given transactions. + /// + /// # JSON-RPC Method + /// + /// `engine_assembleL2Block` + #[method(name = "assembleL2Block")] + async fn assemble_l2_block(&self, params: AssembleL2BlockParams) + -> RpcResult; + + /// Validate an L2 block without importing it. + /// + /// # JSON-RPC Method + /// + /// `engine_validateL2Block` + #[method(name = "validateL2Block")] + async fn validate_l2_block(&self, data: ExecutableL2Data) -> RpcResult; + + /// Import and finalize a new L2 block. + /// + /// # JSON-RPC Method + /// + /// `engine_newL2Block` + #[method(name = "newL2Block")] + async fn new_l2_block(&self, data: ExecutableL2Data, batch_hash: Option) + -> RpcResult<()>; + + /// Import a safe L2 block from derivation. + /// + /// # JSON-RPC Method + /// + /// `engine_newSafeL2Block` + #[method(name = "newSafeL2Block")] + async fn new_safe_l2_block(&self, data: SafeL2Data) -> RpcResult; +} + +/// Implementation of the L2 Engine RPC API. +/// +/// This struct wraps an implementation of `MorphL2EngineApi` and provides +/// the JSON-RPC interface. +#[derive(Debug, Clone)] +pub struct MorphL2EngineRpcHandler { + inner: Arc, +} + +impl MorphL2EngineRpcHandler { + /// Creates a new `MorphL2EngineRpcHandler`. + pub fn new(api: Api) -> Self { + Self { + inner: Arc::new(api), + } + } + + /// Creates a new `MorphL2EngineRpcHandler` from an `Arc`. + pub fn from_arc(api: Arc) -> Self { + Self { inner: api } + } +} + +#[async_trait::async_trait] +impl MorphL2EngineRpcServer for MorphL2EngineRpcHandler +where + Api: MorphL2EngineApi + 'static, +{ + async fn assemble_l2_block( + &self, + params: AssembleL2BlockParams, + ) -> RpcResult { + tracing::debug!(target: "morph::engine", block_number = params.number, "assembling L2 block"); + + self.inner.assemble_l2_block(params).await.map_err(|e| { + tracing::error!(target: "morph::engine", error = %e, "failed to assemble L2 block"); + e.into() + }) + } + + async fn validate_l2_block(&self, data: ExecutableL2Data) -> RpcResult { + tracing::debug!( + target: "morph::engine", + block_number = data.number, + block_hash = %data.hash, + "validating L2 block" + ); + + self.inner.validate_l2_block(data).await.map_err(|e| { + tracing::error!(target: "morph::engine", error = %e, "failed to validate L2 block"); + e.into() + }) + } + + async fn new_l2_block( + &self, + data: ExecutableL2Data, + batch_hash: Option, + ) -> RpcResult<()> { + tracing::info!( + target: "morph::engine", + block_number = data.number, + block_hash = %data.hash, + ?batch_hash, + "importing new L2 block" + ); + + self.inner + .new_l2_block(data, batch_hash) + .await + .map_err(|e| { + tracing::error!(target: "morph::engine", error = %e, "failed to import L2 block"); + e.into() + }) + } + + async fn new_safe_l2_block(&self, data: SafeL2Data) -> RpcResult { + tracing::info!( + target: "morph::engine", + block_number = data.number, + "importing safe L2 block" + ); + + self.inner.new_safe_l2_block(data).await.map_err(|e| { + tracing::error!(target: "morph::engine", error = %e, "failed to import safe L2 block"); + e.into() + }) + } +} + +/// Converts an `EngineApiResult` into a `RpcResult`. +pub fn into_rpc_result(result: EngineApiResult) -> RpcResult { + result.map_err(|e| e.into_rpc_error()) +} diff --git a/crates/engine-api/src/validator.rs b/crates/engine-api/src/validator.rs new file mode 100644 index 0000000..be23b65 --- /dev/null +++ b/crates/engine-api/src/validator.rs @@ -0,0 +1,118 @@ +//! Morph Engine Validator utilities. +//! +//! This module provides utilities for state root validation according to +//! the MPT fork 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). + +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 +/// ZK proof instead. +/// +/// # Arguments +/// +/// * `chain_spec` - The chain specification +/// * `timestamp` - The block timestamp to check +/// +/// # Returns +/// +/// Returns `true` if state root validation should be performed (MPT fork is active), +/// `false` if MPT fork is not active (validation skipped, using ZK-trie). +pub fn should_validate_state_root(chain_spec: &MorphChainSpec, timestamp: u64) -> bool { + chain_spec.is_mpt_fork_active_at_timestamp(timestamp) +} + +/// Helper struct to hold chain spec for validation decisions. +#[derive(Debug, Clone)] +pub struct MorphValidationContext { + chain_spec: Arc, +} + +impl MorphValidationContext { + /// Creates a new validation context. + pub const fn new(chain_spec: Arc) -> Self { + Self { chain_spec } + } + + /// Returns whether state root validation should be performed at the given timestamp. + pub fn should_validate_state_root(&self, timestamp: u64) -> bool { + should_validate_state_root(&self.chain_spec, timestamp) + } + + /// Returns the chain spec. + pub fn chain_spec(&self) -> &MorphChainSpec { + &self.chain_spec + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_genesis::Genesis; + use serde_json::json; + + fn create_test_chainspec(mpt_fork_time: Option) -> Arc { + let mut genesis_json = json!({ + "config": { + "chainId": 1337, + "bernoulliBlock": 0, + "curieBlock": 0, + "morph": {} + }, + "alloc": {} + }); + + if let Some(time) = mpt_fork_time { + genesis_json["config"]["mptForkTime"] = json!(time); + } + + let genesis: Genesis = serde_json::from_value(genesis_json).unwrap(); + Arc::new(MorphChainSpec::from(genesis)) + } + + #[test] + fn test_should_validate_state_root_before_mpt_fork() { + let chain_spec = create_test_chainspec(Some(1000)); + + // Before MPT fork: should skip validation (return false, using ZK-trie) + assert!(!should_validate_state_root(&chain_spec, 0)); + assert!(!should_validate_state_root(&chain_spec, 500)); + assert!(!should_validate_state_root(&chain_spec, 999)); + } + + #[test] + fn test_should_validate_state_root_after_mpt_fork() { + let chain_spec = create_test_chainspec(Some(1000)); + + // After MPT fork: should validate (return true, using MPT) + assert!(should_validate_state_root(&chain_spec, 1000)); + assert!(should_validate_state_root(&chain_spec, 2000)); + } + + #[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) + let chain_spec = create_test_chainspec(None); + + assert!(!should_validate_state_root(&chain_spec, 0)); + assert!(!should_validate_state_root(&chain_spec, 1000)); + } + + #[test] + fn test_validation_context() { + let chain_spec = create_test_chainspec(Some(1000)); + let ctx = MorphValidationContext::new(chain_spec); + + // Before MPT fork: should skip validation (using ZK-trie) + assert!(!ctx.should_validate_state_root(500)); + // After MPT fork: should validate (using MPT) + assert!(ctx.should_validate_state_root(1000)); + } +} diff --git a/crates/payload/types/Cargo.toml b/crates/payload/types/Cargo.toml index f6df09e..7ef640a 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"] } +morph-primitives = { workspace = true, features = ["serde", "reth-codec"] } # Reth reth-payload-builder.workspace = true