diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..9d4cb2216 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,67 @@ +name: Continuous Integration + +on: + push: + branches: + - "**" + pull_request: + branches: + - "**" + +jobs: + check: + name: Cargo Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v2 + - name: Check + run: cargo check + + test: + name: Cargo Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v2 + - name: Run tests + run: cargo test --all -- --test-threads=1 + + udeps: + name: udeps + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@cargo-udeps + - name: Run cargo udeps + run: cargo +nightly udeps --workspace --lib --examples --tests --benches --all-features --locked + + clippy: + name: clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - uses: Swatinem/rust-cache@v2 + - name: clippy + run: cargo clippy --workspace --tests --all-features + env: + RUSTFLAGS: "-D warnings" diff --git a/Cargo.lock b/Cargo.lock index 3412aafdf..f0e086904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8720,7 +8720,6 @@ dependencies = [ "derive_more 0.99.20", "eyre", "futures", - "itertools 0.14.0", "jsonrpsee", "jsonrpsee-core", "jsonrpsee-types", diff --git a/Cargo.toml b/Cargo.toml index 48d151f5e..625ea64b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,6 @@ tokio = { version = "1.36", features = ["full"] } tokio-stream = "0.1" tracing = "0.1" rmp-serde = "1.0.0" -itertools = "0.14.0" [target.'cfg(unix)'.dependencies] diff --git a/src/chainspec/hl.rs b/src/chainspec/hl.rs index baa21b2f8..aed1f5d71 100644 --- a/src/chainspec/hl.rs +++ b/src/chainspec/hl.rs @@ -9,7 +9,7 @@ static GENESIS_HASH: B256 = b256!("d8fcc13b6a195b88b7b2da3722ff6cad767b13a8c1e9ffb1c73aa9d216d895f0"); /// The Hyperliqiud Mainnet spec -pub static HL_MAINNET: Lazy> = Lazy::new(|| { +pub fn hl_mainnet() -> ChainSpec { ChainSpec { chain: Chain::from_named(NamedChain::Hyperliquid), genesis: serde_json::from_str(include_str!("genesis.json")) @@ -20,8 +20,7 @@ pub static HL_MAINNET: Lazy> = Lazy::new(|| { prune_delete_limit: 10000, ..Default::default() } - .into() -}); +} /// Empty genesis header for Hyperliquid Mainnet. /// diff --git a/src/chainspec/parser.rs b/src/chainspec/parser.rs index 61386d9ff..66529d966 100644 --- a/src/chainspec/parser.rs +++ b/src/chainspec/parser.rs @@ -1,5 +1,6 @@ -use super::hl::HL_MAINNET; -use reth_chainspec::ChainSpec; +use crate::chainspec::HlChainSpec; + +use super::hl::hl_mainnet; use reth_cli::chainspec::ChainSpecParser; use std::sync::Arc; @@ -11,21 +12,23 @@ pub const SUPPORTED_CHAINS: &[&str] = &["mainnet"]; pub struct HlChainSpecParser; impl ChainSpecParser for HlChainSpecParser { - type ChainSpec = ChainSpec; + type ChainSpec = HlChainSpec; const SUPPORTED_CHAINS: &'static [&'static str] = SUPPORTED_CHAINS; - fn parse(s: &str) -> eyre::Result> { + fn parse(s: &str) -> eyre::Result> { chain_value_parser(s) } } -/// Clap value parser for [`ChainSpec`]s. +/// Clap value parser for [`HlChainSpec`]s. /// /// Currently only mainnet is supported. -pub fn chain_value_parser(s: &str) -> eyre::Result> { +pub fn chain_value_parser(s: &str) -> eyre::Result> { match s { - "mainnet" => Ok(HL_MAINNET.clone().into()), + "mainnet" => Ok(Arc::new(HlChainSpec { + inner: hl_mainnet(), + })), _ => Err(eyre::eyre!("Unsupported chain: {}", s)), } } diff --git a/src/evm/api/builder.rs b/src/evm/api/builder.rs new file mode 100644 index 000000000..14c01eee0 --- /dev/null +++ b/src/evm/api/builder.rs @@ -0,0 +1,39 @@ +use super::HlEvmInner; +use crate::evm::{spec::HlSpecId, transaction::HlTxTr}; +use revm::{ + context::{Cfg, JournalOutput}, + context_interface::{Block, JournalTr}, + handler::instructions::EthInstructions, + interpreter::interpreter::EthInterpreter, + Context, Database, +}; + +/// Trait that allows for hl HlEvm to be built. +pub trait HlBuilder: Sized { + /// Type of the context. + type Context; + + /// Build the hl with an inspector. + fn build_hl_with_inspector( + self, + inspector: INSP, + ) -> HlEvmInner>; +} + +impl HlBuilder for Context +where + BLOCK: Block, + TX: HlTxTr, + CFG: Cfg, + DB: Database, + JOURNAL: JournalTr, +{ + type Context = Self; + + fn build_hl_with_inspector( + self, + inspector: INSP, + ) -> HlEvmInner> { + HlEvmInner::new(self, inspector) + } +} diff --git a/src/evm/api/ctx.rs b/src/evm/api/ctx.rs new file mode 100644 index 000000000..32b53fa1c --- /dev/null +++ b/src/evm/api/ctx.rs @@ -0,0 +1,23 @@ +use crate::evm::{spec::HlSpecId, transaction::HlTxEnv}; +use revm::{ + context::{BlockEnv, CfgEnv, TxEnv}, + database_interface::EmptyDB, + Context, Journal, MainContext, +}; + +/// Type alias for the default context type of the HlEvm. +pub type HlContext = Context, CfgEnv, DB, Journal>; + +/// Trait that allows for a default context to be created. +pub trait DefaultHl { + /// Create a default context. + fn hl() -> HlContext; +} + +impl DefaultHl for HlContext { + fn hl() -> Self { + Context::mainnet() + .with_tx(HlTxEnv::default()) + .with_cfg(CfgEnv::new_with_spec(HlSpecId::default())) + } +} diff --git a/src/evm/api/exec.rs b/src/evm/api/exec.rs new file mode 100644 index 000000000..34eea1063 --- /dev/null +++ b/src/evm/api/exec.rs @@ -0,0 +1,107 @@ +use super::HlEvmInner; +use crate::evm::{handler::HlHandler, spec::HlSpecId, transaction::HlTxTr}; +use revm::{ + context::{ContextSetters, JournalOutput}, + context_interface::{ + result::{EVMError, ExecutionResult, ResultAndState}, + Cfg, ContextTr, Database, JournalTr, + }, + handler::{instructions::EthInstructions, EthFrame, EvmTr, Handler, PrecompileProvider}, + inspector::{InspectCommitEvm, InspectEvm, Inspector, InspectorHandler, JournalExt}, + interpreter::{interpreter::EthInterpreter, InterpreterResult}, + DatabaseCommit, ExecuteCommitEvm, ExecuteEvm, +}; + +// Type alias for HL context +pub trait HlContextTr: + ContextTr, Tx: HlTxTr, Cfg: Cfg> +{ +} + +impl HlContextTr for T where + T: ContextTr< + Journal: JournalTr, + Tx: HlTxTr, + Cfg: Cfg, + > +{ +} + +/// Type alias for the error type of the HlEvm. +type HlError = EVMError<<::Db as Database>::Error>; + +impl ExecuteEvm + for HlEvmInner, PRECOMPILE> +where + CTX: HlContextTr + ContextSetters, + PRECOMPILE: PrecompileProvider, +{ + type Output = Result>; + + type Tx = ::Tx; + + type Block = ::Block; + + fn set_tx(&mut self, tx: Self::Tx) { + self.0.ctx.set_tx(tx); + } + + fn set_block(&mut self, block: Self::Block) { + self.0.ctx.set_block(block); + } + + fn replay(&mut self) -> Self::Output { + let mut h = HlHandler::<_, _, EthFrame<_, _, _>>::new(); + h.run(self) + } +} + +impl ExecuteCommitEvm + for HlEvmInner, PRECOMPILE> +where + CTX: HlContextTr + ContextSetters, + PRECOMPILE: PrecompileProvider, +{ + type CommitOutput = Result>; + + fn replay_commit(&mut self) -> Self::CommitOutput { + self.replay().map(|r| { + self.ctx().db().commit(r.state); + r.result + }) + } +} + +impl InspectEvm + for HlEvmInner, PRECOMPILE> +where + CTX: HlContextTr + ContextSetters, + INSP: Inspector, + PRECOMPILE: PrecompileProvider, +{ + type Inspector = INSP; + + fn set_inspector(&mut self, inspector: Self::Inspector) { + self.0.inspector = inspector; + } + + fn inspect_replay(&mut self) -> Self::Output { + let mut h = HlHandler::<_, _, EthFrame<_, _, _>>::new(); + h.inspect_run(self) + } +} + +impl InspectCommitEvm + for HlEvmInner, PRECOMPILE> +where + CTX: HlContextTr + ContextSetters, + INSP: Inspector, + PRECOMPILE: PrecompileProvider, +{ + fn inspect_replay_commit(&mut self) -> Self::CommitOutput { + self.inspect_replay().map(|r| { + self.ctx().db().commit(r.state); + r.result + }) + } +} diff --git a/src/evm/api/mod.rs b/src/evm/api/mod.rs new file mode 100644 index 000000000..063733872 --- /dev/null +++ b/src/evm/api/mod.rs @@ -0,0 +1,134 @@ +use super::precompiles::HlPrecompiles; +use revm::{ + context::{ContextSetters, Evm as EvmCtx}, + context_interface::ContextTr, + handler::{ + instructions::{EthInstructions, InstructionProvider}, + EvmTr, PrecompileProvider, + }, + inspector::{InspectorEvmTr, JournalExt}, + interpreter::{interpreter::EthInterpreter, Interpreter, InterpreterAction, InterpreterTypes}, + Inspector, +}; + +pub mod builder; +pub mod ctx; +mod exec; + +pub struct HlEvmInner, P = HlPrecompiles>( + pub EvmCtx, +); + +impl + HlEvmInner, HlPrecompiles> +{ + pub fn new(ctx: CTX, inspector: INSP) -> Self { + Self(EvmCtx { + ctx, + inspector, + instruction: EthInstructions::new_mainnet(), + precompiles: HlPrecompiles::default(), + }) + } + + /// Consumes self and returns a new Evm type with given Precompiles. + pub fn with_precompiles( + self, + precompiles: OP, + ) -> HlEvmInner, OP> { + HlEvmInner(self.0.with_precompiles(precompiles)) + } +} + +impl InspectorEvmTr for HlEvmInner +where + CTX: ContextTr + ContextSetters, + I: InstructionProvider< + Context = CTX, + InterpreterTypes: InterpreterTypes, + >, + INSP: Inspector, + P: PrecompileProvider, +{ + type Inspector = INSP; + + fn inspector(&mut self) -> &mut Self::Inspector { + &mut self.0.inspector + } + + fn ctx_inspector(&mut self) -> (&mut Self::Context, &mut Self::Inspector) { + (&mut self.0.ctx, &mut self.0.inspector) + } + + fn run_inspect_interpreter( + &mut self, + interpreter: &mut Interpreter< + ::InterpreterTypes, + >, + ) -> <::InterpreterTypes as InterpreterTypes>::Output + { + self.0.run_inspect_interpreter(interpreter) + } +} + +impl EvmTr for HlEvmInner +where + CTX: ContextTr, + I: InstructionProvider< + Context = CTX, + InterpreterTypes: InterpreterTypes, + >, + P: PrecompileProvider, +{ + type Context = CTX; + type Instructions = I; + type Precompiles = P; + + fn run_interpreter( + &mut self, + interpreter: &mut Interpreter< + ::InterpreterTypes, + >, + ) -> <::InterpreterTypes as InterpreterTypes>::Output + { + let context = &mut self.0.ctx; + let instructions = &mut self.0.instruction; + interpreter.run_plain(instructions.instruction_table(), context) + } + + fn ctx(&mut self) -> &mut Self::Context { + &mut self.0.ctx + } + + fn ctx_ref(&self) -> &Self::Context { + &self.0.ctx + } + + fn ctx_instructions(&mut self) -> (&mut Self::Context, &mut Self::Instructions) { + (&mut self.0.ctx, &mut self.0.instruction) + } + + fn ctx_precompiles(&mut self) -> (&mut Self::Context, &mut Self::Precompiles) { + (&mut self.0.ctx, &mut self.0.precompiles) + } +} + +// #[cfg(test)] +// mod test { +// use super::{builder::HlBuilder, ctx::DefaultHl}; +// use revm::{ +// inspector::{InspectEvm, NoOpInspector}, +// Context, ExecuteEvm, +// }; + +// #[test] +// fn default_run_bsc() { +// let ctx = Context::bsc(); +// let mut evm = ctx.build_bsc_with_inspector(NoOpInspector {}); + +// // execute +// let _ = evm.replay(); +// // inspect +// let _ = evm.inspect_replay(); +// } +// } diff --git a/src/evm/handler.rs b/src/evm/handler.rs new file mode 100644 index 000000000..ec1e08513 --- /dev/null +++ b/src/evm/handler.rs @@ -0,0 +1,148 @@ +//! EVM Handler related to Hl chain + +use super::{spec::HlSpecId, transaction::HlTxTr}; +use revm::{ + context::{ + result::{ExecutionResult, HaltReason}, + Cfg, ContextTr, JournalOutput, LocalContextTr, + }, + context_interface::{result::ResultAndState, JournalTr}, + handler::{handler::EvmTrError, EvmTr, Frame, FrameResult, Handler, MainnetHandler}, + inspector::{Inspector, InspectorEvmTr, InspectorFrame, InspectorHandler}, + interpreter::{interpreter::EthInterpreter, FrameInput, InitialAndFloorGas, SuccessOrHalt}, +}; + +pub struct HlHandler { + pub mainnet: MainnetHandler, +} + +impl HlHandler { + pub fn new() -> Self { + Self { + mainnet: MainnetHandler::default(), + } + } +} + +impl Default for HlHandler { + fn default() -> Self { + Self::new() + } +} + +pub trait HlContextTr: + ContextTr, Tx: HlTxTr, Cfg: Cfg> +{ +} + +impl HlContextTr for T where + T: ContextTr< + Journal: JournalTr, + Tx: HlTxTr, + Cfg: Cfg, + > +{ +} + +impl Handler for HlHandler +where + EVM: EvmTr, + ERROR: EvmTrError, + FRAME: Frame, +{ + type Evm = EVM; + type Error = ERROR; + type Frame = FRAME; + type HaltReason = HaltReason; + + fn validate_initial_tx_gas( + &self, + evm: &Self::Evm, + ) -> Result { + let ctx = evm.ctx_ref(); + let tx = ctx.tx(); + + if tx.is_system_transaction() { + return Ok(InitialAndFloorGas { + initial_gas: 0, + floor_gas: 0, + }); + } + + self.mainnet.validate_initial_tx_gas(evm) + } + + fn reward_beneficiary( + &self, + evm: &mut Self::Evm, + exec_result: &mut ::FrameResult, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn output( + &self, + evm: &mut Self::Evm, + result: ::FrameResult, + ) -> Result, Self::Error> { + let ctx = evm.ctx(); + ctx.error(); + let tx = ctx.tx(); + // used gas with refund calculated. + let gas_refunded = if tx.is_system_transaction() { + 0 + } else { + result.gas().refunded() as u64 + }; + let final_gas_used = result.gas().spent() - gas_refunded; + let output = result.output(); + let instruction_result = result.into_interpreter_result(); + + // Reset journal and return present state. + let JournalOutput { state, logs } = evm.ctx().journal().finalize(); + + let result = match SuccessOrHalt::from(instruction_result.result) { + SuccessOrHalt::Success(reason) => ExecutionResult::Success { + reason, + gas_used: final_gas_used, + gas_refunded, + logs, + output, + }, + SuccessOrHalt::Revert => ExecutionResult::Revert { + gas_used: final_gas_used, + output: output.into_data(), + }, + SuccessOrHalt::Halt(reason) => ExecutionResult::Halt { + reason, + gas_used: final_gas_used, + }, + // Only two internal return flags. + flag @ (SuccessOrHalt::FatalExternalError | SuccessOrHalt::Internal(_)) => { + panic!( + "Encountered unexpected internal return flag: {flag:?} with instruction result: {instruction_result:?}" + ) + } + }; + + // Clear local context + evm.ctx().local().clear(); + // Clear journal + evm.ctx().journal().clear(); + + Ok(ResultAndState { result, state }) + } +} + +impl InspectorHandler for HlHandler +where + EVM: InspectorEvmTr< + Context: HlContextTr, + Inspector: Inspector<<::Evm as EvmTr>::Context, EthInterpreter>, + >, + ERROR: EvmTrError, + FRAME: Frame + + InspectorFrame, +{ + type IT = EthInterpreter; +} diff --git a/src/evm/mod.rs b/src/evm/mod.rs new file mode 100644 index 000000000..909f44385 --- /dev/null +++ b/src/evm/mod.rs @@ -0,0 +1,5 @@ +pub mod api; +mod handler; +pub mod precompiles; +pub mod spec; +pub mod transaction; diff --git a/src/evm/precompiles.rs b/src/evm/precompiles.rs new file mode 100644 index 000000000..4bb2e4638 --- /dev/null +++ b/src/evm/precompiles.rs @@ -0,0 +1,87 @@ +#![allow(unused)] + +use super::spec::HlSpecId; +use cfg_if::cfg_if; +use once_cell::{race::OnceBox, sync::Lazy}; +use revm::{ + context::Cfg, + context_interface::ContextTr, + handler::{EthPrecompiles, PrecompileProvider}, + interpreter::{InputsImpl, InterpreterResult}, + precompile::{bls12_381, kzg_point_evaluation, modexp, secp256r1, Precompiles}, + primitives::{hardfork::SpecId, Address}, +}; +use std::boxed::Box; + +// HL precompile provider +#[derive(Debug, Clone)] +pub struct HlPrecompiles { + /// Inner precompile provider is same as Ethereums. + inner: EthPrecompiles, +} + +impl HlPrecompiles { + /// Create a new precompile provider with the given hl spec. + #[inline] + pub fn new(spec: HlSpecId) -> Self { + let precompiles = cancun(); + + Self { inner: EthPrecompiles { precompiles, spec: spec.into_eth_spec() } } + } + + #[inline] + pub fn precompiles(&self) -> &'static Precompiles { + self.inner.precompiles + } +} + +/// Returns precompiles for Istanbul spec. +pub fn cancun() -> &'static Precompiles { + static INSTANCE: OnceBox = OnceBox::new(); + INSTANCE.get_or_init(|| { + let mut precompiles = Precompiles::cancun().clone(); + // precompiles.extend([tendermint::TENDERMINT_HEADER_VALIDATION, iavl::IAVL_PROOF_VALIDATION]); + Box::new(precompiles) + }) +} + +impl PrecompileProvider for HlPrecompiles +where + CTX: ContextTr>, +{ + type Output = InterpreterResult; + + #[inline] + fn set_spec(&mut self, spec: ::Spec) -> bool { + *self = Self::new(spec); + true + } + + #[inline] + fn run( + &mut self, + context: &mut CTX, + address: &Address, + inputs: &InputsImpl, + is_static: bool, + gas_limit: u64, + ) -> Result, String> { + self.inner.run(context, address, inputs, is_static, gas_limit) + } + + #[inline] + fn warm_addresses(&self) -> Box> { + self.inner.warm_addresses() + } + + #[inline] + fn contains(&self, address: &Address) -> bool { + self.inner.contains(address) + } +} + +impl Default for HlPrecompiles { + fn default() -> Self { + Self::new(HlSpecId::default()) + } +} diff --git a/src/evm/spec.rs b/src/evm/spec.rs new file mode 100644 index 000000000..6d3e06646 --- /dev/null +++ b/src/evm/spec.rs @@ -0,0 +1,55 @@ +use revm::primitives::hardfork::SpecId; +use std::str::FromStr; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[allow(non_camel_case_types)] +#[allow(clippy::upper_case_acronyms)] +pub enum HlSpecId { + #[default] + V1, // V1 +} + +impl HlSpecId { + pub const fn is_enabled_in(self, other: HlSpecId) -> bool { + other as u8 <= self as u8 + } + + /// Converts the [`HlSpecId`] into a [`SpecId`]. + pub const fn into_eth_spec(self) -> SpecId { + match self { + Self::V1 => SpecId::CANCUN, + } + } +} + +impl From for SpecId { + fn from(spec: HlSpecId) -> Self { + spec.into_eth_spec() + } +} + +/// String identifiers for HL hardforks +pub mod name { + pub const V1: &str = "V1"; +} + +impl FromStr for HlSpecId { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + name::V1 => Self::V1, + _ => return Err(format!("Unknown HL spec: {s}")), + }) + } +} + +impl From for &'static str { + fn from(spec_id: HlSpecId) -> Self { + match spec_id { + HlSpecId::V1 => name::V1, + } + } +} diff --git a/src/evm/transaction.rs b/src/evm/transaction.rs new file mode 100644 index 000000000..a0a689dac --- /dev/null +++ b/src/evm/transaction.rs @@ -0,0 +1,203 @@ +use alloy_consensus::Transaction as AlloyTransaction; +use alloy_rpc_types::AccessList; +use auto_impl::auto_impl; +use reth_evm::{FromRecoveredTx, FromTxWithEncoded, IntoTxEnv, TransactionEnv}; +use reth_primitives::TransactionSigned; +use revm::{ + context::TxEnv, + context_interface::transaction::Transaction, + primitives::{Address, Bytes, TxKind, B256, U256}, +}; + +#[auto_impl(&, &mut, Box, Arc)] +pub trait HlTxTr: Transaction { + /// Whether the transaction is a system transaction + fn is_system_transaction(&self) -> bool; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct HlTxEnv { + pub base: T, + pub is_system_transaction: bool, +} + +impl HlTxEnv { + pub fn new(base: T) -> Self { + Self { + base, + is_system_transaction: false, + } + } +} + +impl Default for HlTxEnv { + fn default() -> Self { + Self { + base: TxEnv::default(), + is_system_transaction: false, + } + } +} + +impl Transaction for HlTxEnv { + type AccessListItem<'a> + = T::AccessListItem<'a> + where + T: 'a; + type Authorization<'a> + = T::Authorization<'a> + where + T: 'a; + + fn tx_type(&self) -> u8 { + self.base.tx_type() + } + + fn caller(&self) -> Address { + self.base.caller() + } + + fn gas_limit(&self) -> u64 { + self.base.gas_limit() + } + + fn value(&self) -> U256 { + self.base.value() + } + + fn input(&self) -> &Bytes { + self.base.input() + } + + fn nonce(&self) -> u64 { + self.base.nonce() + } + + fn kind(&self) -> TxKind { + self.base.kind() + } + + fn chain_id(&self) -> Option { + self.base.chain_id() + } + + fn gas_price(&self) -> u128 { + self.base.gas_price() + } + + fn access_list(&self) -> Option>> { + self.base.access_list() + } + + fn blob_versioned_hashes(&self) -> &[B256] { + self.base.blob_versioned_hashes() + } + + fn max_fee_per_blob_gas(&self) -> u128 { + self.base.max_fee_per_blob_gas() + } + + fn authorization_list_len(&self) -> usize { + self.base.authorization_list_len() + } + + fn authorization_list(&self) -> impl Iterator> { + self.base.authorization_list() + } + + fn max_fee_per_gas(&self) -> u128 { + self.base.max_fee_per_gas() + } + + fn max_priority_fee_per_gas(&self) -> Option { + self.base.max_priority_fee_per_gas() + } + + fn effective_gas_price(&self, base_fee: u128) -> u128 { + self.base.effective_gas_price(base_fee) + } +} + +impl HlTxTr for HlTxEnv { + fn is_system_transaction(&self) -> bool { + self.is_system_transaction + } +} + +impl IntoTxEnv for HlTxEnv { + fn into_tx_env(self) -> Self { + self + } +} + +impl FromRecoveredTx for HlTxEnv { + fn from_recovered_tx(tx: &TransactionSigned, sender: Address) -> Self { + Self::new(TxEnv::from_recovered_tx(tx, sender)) + } +} + +impl FromTxWithEncoded for HlTxEnv { + fn from_encoded_tx(tx: &TransactionSigned, sender: Address, _encoded: Bytes) -> Self { + let base = match tx.clone().into_typed_transaction() { + reth_primitives::Transaction::Legacy(tx) => TxEnv::from_recovered_tx(&tx, sender), + reth_primitives::Transaction::Eip2930(tx) => TxEnv::from_recovered_tx(&tx, sender), + reth_primitives::Transaction::Eip1559(tx) => TxEnv::from_recovered_tx(&tx, sender), + reth_primitives::Transaction::Eip4844(tx) => TxEnv::from_recovered_tx(&tx, sender), + reth_primitives::Transaction::Eip7702(tx) => TxEnv::from_recovered_tx(&tx, sender), + }; + + let is_system_transaction = match tx.gas_price() { + Some(x) => x == 0u128, + None => false, + }; + + Self { + base, + is_system_transaction + } + } +} + +impl TransactionEnv for HlTxEnv { + fn set_gas_limit(&mut self, gas_limit: u64) { + self.base.set_gas_limit(gas_limit); + } + + fn nonce(&self) -> u64 { + TransactionEnv::nonce(&self.base) + } + + fn set_nonce(&mut self, nonce: u64) { + self.base.set_nonce(nonce); + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.base.set_access_list(access_list); + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use revm::primitives::Address; + + #[test] + fn test_hl_transaction_fields() { + let hl_tx = HlTxEnv { + base: TxEnv { + tx_type: 0, + gas_limit: 10, + gas_price: 100, + gas_priority_fee: Some(5), + ..Default::default() + }, + is_system_transaction: false, + }; + + assert_eq!(hl_tx.tx_type(), 0); + assert_eq!(hl_tx.gas_limit(), 10); + assert_eq!(hl_tx.kind(), revm::primitives::TxKind::Call(Address::ZERO)); + } +} diff --git a/src/hardforks/hl.rs b/src/hardforks/hl.rs index 034ed72ea..2be0bdcde 100644 --- a/src/hardforks/hl.rs +++ b/src/hardforks/hl.rs @@ -5,7 +5,7 @@ use reth_chainspec::ForkCondition; use reth_ethereum_forks::{hardfork, ChainHardforks, EthereumHardfork, Hardfork}; hardfork!( - /// The name of a bsc hardfork. + /// The name of a hl hardfork. /// /// When building a list of hardforks for a chain, it's still expected to mix with [`EthereumHardfork`]. /// There is no name for these hardforks; just some bugfixes on the evm chain. @@ -62,8 +62,8 @@ impl HlHardfork { ) } - /// Bsc mainnet list of hardforks. - pub fn bsc_mainnet() -> ChainHardforks { + /// Hl mainnet list of hardforks. + pub fn hl_mainnet() -> ChainHardforks { ChainHardforks::new(vec![ (EthereumHardfork::Frontier.boxed(), ForkCondition::Block(0)), (EthereumHardfork::Homestead.boxed(), ForkCondition::Block(0)), @@ -86,14 +86,10 @@ impl HlHardfork { EthereumHardfork::MuirGlacier.boxed(), ForkCondition::Block(0), ), - ( - EthereumHardfork::Berlin.boxed(), - ForkCondition::Block(31302048), - ), - ( - EthereumHardfork::London.boxed(), - ForkCondition::Block(31302048), - ), + (EthereumHardfork::Berlin.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::London.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Shanghai.boxed(), ForkCondition::Block(0)), + (EthereumHardfork::Cancun.boxed(), ForkCondition::Block(0)), (Self::V1.boxed(), ForkCondition::Block(0)), (Self::V2.boxed(), ForkCondition::Block(0)), (Self::V3.boxed(), ForkCondition::Block(0)), diff --git a/src/lib.rs b/src/lib.rs index 26d4b5ca9..53d6494fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ -mod chainspec; -mod consensus; +pub mod chainspec; +pub mod consensus; +mod evm; mod hardforks; -mod node; - -pub use node::primitives; +pub mod node; +pub use node::primitives::{HlBlock, HlBlockBody, HlPrimitives}; diff --git a/src/main.rs b/src/main.rs index aad91a146..29f5cdf1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use clap::{Args, Parser}; use reth::builder::NodeHandle; use reth_hl::{ - chainspec::HlChainSpecParser, + chainspec::parser::HlChainSpecParser, node::{cli::Cli, HlNode}, }; diff --git a/src/node/consensus.rs b/src/node/consensus.rs index 41430cd05..0ccebe734 100644 --- a/src/node/consensus.rs +++ b/src/node/consensus.rs @@ -1,7 +1,7 @@ use crate::{ hardforks::HlHardforks, node::HlNode, - primitives::{HlBlock, HlBlockBody, HlPrimitives}, + {HlBlock, HlBlockBody, HlPrimitives}, }; use reth::{ api::FullNodeTypes, diff --git a/src/node/engine.rs b/src/node/engine.rs new file mode 100644 index 000000000..9f821f9c9 --- /dev/null +++ b/src/node/engine.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; + +use crate::{ + node::{rpc::engine_api::payload::HlPayloadTypes, HlNode}, + HlBlock, HlPrimitives, +}; +use alloy_eips::eip7685::Requests; +use alloy_primitives::U256; +use reth::{ + api::FullNodeTypes, + builder::{components::PayloadServiceBuilder, BuilderContext}, + payload::{PayloadBuilderHandle, PayloadServiceCommand}, + transaction_pool::TransactionPool, +}; +use reth_evm::ConfigureEvm; +use reth_payload_primitives::BuiltPayload; +use reth_primitives::SealedBlock; +use tokio::sync::{broadcast, mpsc}; +use tracing::warn; + +/// Built payload for Hl. This is similar to [`EthBuiltPayload`] but without sidecars as those +/// included into [`HlBlock`]. +#[derive(Debug, Clone)] +pub struct HlBuiltPayload { + /// The built block + pub(crate) block: Arc>, + /// The fees of the block + pub(crate) fees: U256, + /// The requests of the payload + pub(crate) requests: Option, +} + +impl BuiltPayload for HlBuiltPayload { + type Primitives = HlPrimitives; + + fn block(&self) -> &SealedBlock { + self.block.as_ref() + } + + fn fees(&self) -> U256 { + self.fees + } + + fn requests(&self) -> Option { + self.requests.clone() + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub struct HlPayloadServiceBuilder; + +impl PayloadServiceBuilder for HlPayloadServiceBuilder +where + Node: FullNodeTypes, + Pool: TransactionPool, + Evm: ConfigureEvm, +{ + async fn spawn_payload_builder_service( + self, + ctx: &BuilderContext, + _pool: Pool, + _evm_config: Evm, + ) -> eyre::Result> { + let (tx, mut rx) = mpsc::unbounded_channel(); + + ctx.task_executor().spawn_critical("payload builder", async move { + let mut subscriptions = Vec::new(); + + while let Some(message) = rx.recv().await { + match message { + PayloadServiceCommand::Subscribe(tx) => { + let (events_tx, events_rx) = broadcast::channel(100); + // Retain senders to make sure that channels are not getting closed + subscriptions.push(events_tx); + let _ = tx.send(events_rx); + } + message => warn!(?message, "Noop payload service received a message"), + } + } + }); + + Ok(PayloadBuilderHandle::new(tx)) + } +} + +// impl From for HlBuiltPayload { +// fn from(value: EthBuiltPayload) -> Self { +// let EthBuiltPayload { id, block, fees, sidecars, requests } = value; +// HlBuiltPayload { +// id, +// block: block.into(), +// fees, +// requests, +// } +// } +// } + +// pub struct HlPayloadBuilder { +// inner: Inner, +// } + +// impl PayloadBuilder for HlPayloadBuilder +// where +// Inner: PayloadBuilder, +// { +// type Attributes = Inner::Attributes; +// type BuiltPayload = HlBuiltPayload; +// type Error = Inner::Error; + +// fn try_build( +// &self, +// args: BuildArguments, +// ) -> Result, PayloadBuilderError> { +// let outcome = self.inner.try_build(args)?; +// } +// } diff --git a/src/node/evm/assembler.rs b/src/node/evm/assembler.rs new file mode 100644 index 000000000..61864b949 --- /dev/null +++ b/src/node/evm/assembler.rs @@ -0,0 +1,32 @@ +use crate::{ + node::evm::config::{HlBlockExecutorFactory, HlEvmConfig}, + HlBlock, HlBlockBody, +}; +use alloy_consensus::{Block, Header}; +use reth_evm::{ + block::BlockExecutionError, + execute::{BlockAssembler, BlockAssemblerInput}, +}; + +impl BlockAssembler for HlEvmConfig { + type Block = HlBlock; + + fn assemble_block( + &self, + input: BlockAssemblerInput<'_, '_, HlBlockExecutorFactory, Header>, + ) -> Result { + let Block { header, body: inner } = self.block_assembler.assemble_block(input)?; + Ok(HlBlock { + header, + body: HlBlockBody { + inner, + // HACK: we're setting sidecars to `None` here but ideally we should somehow get + // them from the payload builder. + // + // Payload building is out of scope of reth-bsc for now, so this is not critical + sidecars: None, + read_precompile_calls: None, + }, + }) + } +} diff --git a/src/node/evm/config.rs b/src/node/evm/config.rs new file mode 100644 index 000000000..e13f1cbf8 --- /dev/null +++ b/src/node/evm/config.rs @@ -0,0 +1,316 @@ +use super::{executor::HlBlockExecutor, factory::HlEvmFactory}; +use crate::{ + chainspec::HlChainSpec, + evm::{spec::HlSpecId, transaction::HlTxEnv}, + hardforks::HlHardforks, + HlPrimitives, +}; +use alloy_consensus::{BlockHeader, Header, TxReceipt}; +use alloy_primitives::{Log, U256}; +use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; +use reth_ethereum_forks::EthereumHardfork; +use reth_evm::{ + block::{BlockExecutorFactory, BlockExecutorFor}, + eth::{receipt_builder::ReceiptBuilder, EthBlockExecutionCtx}, + ConfigureEvm, EvmEnv, EvmFactory, ExecutionCtxFor, FromRecoveredTx, FromTxWithEncoded, + IntoTxEnv, NextBlockEnvAttributes, +}; +use reth_evm_ethereum::{EthBlockAssembler, RethReceiptBuilder}; +use reth_primitives::{BlockTy, HeaderTy, SealedBlock, SealedHeader, TransactionSigned}; +use reth_revm::State; +use revm::{ + context::{BlockEnv, CfgEnv, TxEnv}, + context_interface::block::BlobExcessGasAndPrice, + primitives::hardfork::SpecId, + Inspector, +}; +use std::{borrow::Cow, convert::Infallible, sync::Arc}; + +/// Ethereum-related EVM configuration. +#[derive(Debug, Clone)] +pub struct HlEvmConfig { + /// Inner [`HlBlockExecutorFactory`]. + pub executor_factory: + HlBlockExecutorFactory, HlEvmFactory>, + /// Ethereum block assembler. + pub block_assembler: EthBlockAssembler, +} + +impl HlEvmConfig { + /// Creates a new Ethereum EVM configuration with the given chain spec. + pub fn new(chain_spec: Arc) -> Self { + Self::hl(chain_spec) + } + + /// Creates a new Ethereum EVM configuration. + pub fn hl(chain_spec: Arc) -> Self { + Self::new_with_evm_factory(chain_spec, HlEvmFactory::default()) + } +} + +impl HlEvmConfig { + /// Creates a new Ethereum EVM configuration with the given chain spec and EVM factory. + pub fn new_with_evm_factory(chain_spec: Arc, evm_factory: HlEvmFactory) -> Self { + Self { + block_assembler: EthBlockAssembler::new(chain_spec.clone()), + executor_factory: HlBlockExecutorFactory::new( + RethReceiptBuilder::default(), + chain_spec, + evm_factory, + ), + } + } + + /// Returns the chain spec associated with this configuration. + pub const fn chain_spec(&self) -> &Arc { + self.executor_factory.spec() + } +} + +/// Ethereum block executor factory. +#[derive(Debug, Clone, Default, Copy)] +pub struct HlBlockExecutorFactory< + R = RethReceiptBuilder, + Spec = Arc, + EvmFactory = HlEvmFactory, +> { + /// Receipt builder. + receipt_builder: R, + /// Chain specification. + spec: Spec, + /// EVM factory. + evm_factory: EvmFactory, +} + +impl HlBlockExecutorFactory { + /// Creates a new [`HlBlockExecutorFactory`] with the given spec, [`EvmFactory`], and + /// [`ReceiptBuilder`]. + pub const fn new(receipt_builder: R, spec: Spec, evm_factory: EvmFactory) -> Self { + Self { + receipt_builder, + spec, + evm_factory, + } + } + + /// Exposes the receipt builder. + pub const fn receipt_builder(&self) -> &R { + &self.receipt_builder + } + + /// Exposes the chain specification. + pub const fn spec(&self) -> &Spec { + &self.spec + } +} + +impl BlockExecutorFactory for HlBlockExecutorFactory +where + R: ReceiptBuilder>, + Spec: EthereumHardforks + HlHardforks + EthChainSpec + Hardforks + Clone, + EvmF: EvmFactory + FromTxWithEncoded>, + R::Transaction: From + Clone, + Self: 'static, + HlTxEnv: IntoTxEnv<::Tx>, +{ + type EvmFactory = EvmF; + type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>; + type Transaction = TransactionSigned; + type Receipt = R::Receipt; + + fn evm_factory(&self) -> &Self::EvmFactory { + &self.evm_factory + } + + fn create_executor<'a, DB, I>( + &'a self, + evm: ::Evm<&'a mut State, I>, + ctx: Self::ExecutionCtx<'a>, + ) -> impl BlockExecutorFor<'a, Self, DB, I> + where + DB: alloy_evm::Database + 'a, + I: Inspector<::Context<&'a mut State>> + 'a, + { + HlBlockExecutor::new(evm, ctx, self.spec().clone(), self.receipt_builder()) + } +} + +const EIP1559_INITIAL_BASE_FEE: u64 = 0; + +impl ConfigureEvm for HlEvmConfig +where + Self: Send + Sync + Unpin + Clone + 'static, +{ + type Primitives = HlPrimitives; + type Error = Infallible; + type NextBlockEnvCtx = NextBlockEnvAttributes; + type BlockExecutorFactory = HlBlockExecutorFactory; + type BlockAssembler = Self; + + fn block_executor_factory(&self) -> &Self::BlockExecutorFactory { + &self.executor_factory + } + + fn block_assembler(&self) -> &Self::BlockAssembler { + self + } + + fn evm_env(&self, header: &Header) -> EvmEnv { + let blob_params = self.chain_spec().blob_params_at_timestamp(header.timestamp); + let spec = revm_spec_by_timestamp_and_block_number( + self.chain_spec().clone(), + header.timestamp(), + header.number(), + ); + + // configure evm env based on parent block + let mut cfg_env = CfgEnv::new() + .with_chain_id(self.chain_spec().chain().id()) + .with_spec(spec); + + if let Some(blob_params) = &blob_params { + cfg_env.set_blob_max_count(blob_params.max_blob_count); + } + + // derive the EIP-4844 blob fees from the header's `excess_blob_gas` and the current + // blobparams + let blob_excess_gas_and_price = + header + .excess_blob_gas + .zip(blob_params) + .map(|(excess_blob_gas, params)| { + let blob_gasprice = params.calc_blob_fee(excess_blob_gas); + BlobExcessGasAndPrice { + excess_blob_gas, + blob_gasprice, + } + }); + + let eth_spec = spec.into_eth_spec(); + + let block_env = BlockEnv { + number: header.number(), + beneficiary: header.beneficiary(), + timestamp: header.timestamp(), + difficulty: if eth_spec >= SpecId::MERGE { + U256::ZERO + } else { + header.difficulty() + }, + prevrandao: if eth_spec >= SpecId::MERGE { + header.mix_hash() + } else { + None + }, + gas_limit: header.gas_limit(), + basefee: header.base_fee_per_gas().unwrap_or_default(), + blob_excess_gas_and_price, + }; + + EvmEnv { cfg_env, block_env } + } + + fn next_evm_env( + &self, + parent: &Header, + attributes: &Self::NextBlockEnvCtx, + ) -> Result, Self::Error> { + // ensure we're not missing any timestamp based hardforks + let spec_id = revm_spec_by_timestamp_and_block_number( + self.chain_spec().clone(), + attributes.timestamp, + parent.number() + 1, + ); + + // configure evm env based on parent block + let cfg_env = CfgEnv::new() + .with_chain_id(self.chain_spec().chain().id()) + .with_spec(spec_id); + + // if the parent block did not have excess blob gas (i.e. it was pre-cancun), but it is + // cancun now, we need to set the excess blob gas to the default value(0) + let blob_excess_gas_and_price = parent + .maybe_next_block_excess_blob_gas( + self.chain_spec() + .blob_params_at_timestamp(attributes.timestamp), + ) + .or_else(|| (spec_id.into_eth_spec().is_enabled_in(SpecId::CANCUN)).then_some(0)) + .map(|gas| BlobExcessGasAndPrice::new(gas, false)); + + let mut basefee = parent.next_block_base_fee( + self.chain_spec() + .base_fee_params_at_timestamp(attributes.timestamp), + ); + + let mut gas_limit = U256::from(parent.gas_limit); + + // If we are on the London fork boundary, we need to multiply the parent's gas limit by the + // elasticity multiplier to get the new gas limit. + if self + .chain_spec() + .inner + .fork(EthereumHardfork::London) + .transitions_at_block(parent.number + 1) + { + let elasticity_multiplier = self + .chain_spec() + .base_fee_params_at_timestamp(attributes.timestamp) + .elasticity_multiplier; + + // multiply the gas limit by the elasticity multiplier + gas_limit *= U256::from(elasticity_multiplier); + + // set the base fee to the initial base fee from the EIP-1559 spec + basefee = Some(EIP1559_INITIAL_BASE_FEE) + } + + let block_env = BlockEnv { + number: parent.number() + 1, + beneficiary: attributes.suggested_fee_recipient, + timestamp: attributes.timestamp, + difficulty: U256::ZERO, + prevrandao: Some(attributes.prev_randao), + gas_limit: attributes.gas_limit, + // calculate basefee based on parent block's gas usage + basefee: basefee.unwrap_or_default(), + // calculate excess gas based on parent block's blob gas usage + blob_excess_gas_and_price, + }; + + Ok(EvmEnv { cfg_env, block_env }) + } + + fn context_for_block<'a>( + &self, + block: &'a SealedBlock>, + ) -> ExecutionCtxFor<'a, Self> { + EthBlockExecutionCtx { + parent_hash: block.header().parent_hash, + parent_beacon_block_root: block.header().parent_beacon_block_root, + ommers: &block.body().ommers, + withdrawals: block.body().withdrawals.as_ref().map(Cow::Borrowed), + } + } + + fn context_for_next_block( + &self, + parent: &SealedHeader>, + attributes: Self::NextBlockEnvCtx, + ) -> ExecutionCtxFor<'_, Self> { + EthBlockExecutionCtx { + parent_hash: parent.hash(), + parent_beacon_block_root: attributes.parent_beacon_block_root, + ommers: &[], + withdrawals: attributes.withdrawals.map(Cow::Owned), + } + } +} + +/// Map the latest active hardfork at the given timestamp or block number to a [`HlSpecId`]. +pub fn revm_spec_by_timestamp_and_block_number( + chain_spec: impl HlHardforks, + timestamp: u64, + block_number: u64, +) -> HlSpecId { + HlSpecId::V1 +} diff --git a/src/node/evm/executor.rs b/src/node/evm/executor.rs new file mode 100644 index 000000000..66acc6a26 --- /dev/null +++ b/src/node/evm/executor.rs @@ -0,0 +1,211 @@ +use super::patch::patch_mainnet_after_tx; +use crate::{evm::transaction::HlTxEnv, hardforks::HlHardforks}; +use alloy_consensus::{Transaction, TxReceipt}; +use alloy_eips::{eip7685::Requests, Encodable2718}; +use alloy_evm::{block::ExecutableTx, eth::receipt_builder::ReceiptBuilderCtx}; +use alloy_primitives::Address; +use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks}; +use reth_evm::{ + block::{BlockValidationError, CommitChanges}, + eth::{receipt_builder::ReceiptBuilder, EthBlockExecutionCtx}, + execute::{BlockExecutionError, BlockExecutor}, + Database, Evm, FromRecoveredTx, FromTxWithEncoded, IntoTxEnv, OnStateHook, RecoveredTx, +}; +use reth_primitives::TransactionSigned; +use reth_provider::BlockExecutionResult; +use reth_revm::State; +use revm::{ + context::{ + result::{ExecutionResult, ResultAndState}, + TxEnv, + }, + state::Bytecode, + Database as _, DatabaseCommit, +}; + +fn is_system_transaction(tx: &TransactionSigned) -> bool { + let Some(gas_price) = tx.gas_price() else { + return false; + }; + gas_price == 0 +} + +pub struct HlBlockExecutor<'a, EVM, Spec, R: ReceiptBuilder> +where + Spec: EthChainSpec, +{ + /// Reference to the specification object. + spec: Spec, + /// Inner EVM. + evm: EVM, + /// Gas used in the block. + gas_used: u64, + /// Receipts of executed transactions. + receipts: Vec, + /// System txs + system_txs: Vec, + /// Receipt builder. + receipt_builder: R, + /// System contracts used to trigger fork specific logic. + // system_contracts: SystemContract, + /// Context for block execution. + _ctx: EthBlockExecutionCtx<'a>, +} + +impl<'a, DB, EVM, Spec, R: ReceiptBuilder> HlBlockExecutor<'a, EVM, Spec, R> +where + DB: Database + 'a, + EVM: Evm< + DB = &'a mut State, + Tx: FromRecoveredTx + + FromRecoveredTx + + FromTxWithEncoded, + >, + Spec: EthereumHardforks + HlHardforks + EthChainSpec + Hardforks + Clone, + R: ReceiptBuilder, + ::Transaction: Unpin + From, + ::Tx: FromTxWithEncoded<::Transaction>, + HlTxEnv: IntoTxEnv<::Tx>, + R::Transaction: Into, +{ + /// Creates a new HlBlockExecutor. + pub fn new( + evm: EVM, + _ctx: EthBlockExecutionCtx<'a>, + spec: Spec, + receipt_builder: R, + // system_contracts: SystemContract, + ) -> Self { + Self { + spec, + evm, + gas_used: 0, + receipts: vec![], + system_txs: vec![], + receipt_builder, + // system_contracts, + _ctx, + } + } + + /// Initializes the genesis contracts + fn deploy_genesis_contracts( + &mut self, + beneficiary: Address, + ) -> Result<(), BlockExecutionError> { + todo!("Deploy WETH, System contract"); + // Ok(()) + } +} + +impl<'a, DB, E, Spec, R> BlockExecutor for HlBlockExecutor<'a, E, Spec, R> +where + DB: Database + 'a, + E: Evm< + DB = &'a mut State, + Tx: FromRecoveredTx + + FromRecoveredTx + + FromTxWithEncoded, + >, + Spec: EthereumHardforks + HlHardforks + EthChainSpec + Hardforks, + R: ReceiptBuilder, + ::Transaction: Unpin + From, + ::Tx: FromTxWithEncoded<::Transaction>, + HlTxEnv: IntoTxEnv<::Tx>, + R::Transaction: Into, +{ + type Transaction = TransactionSigned; + type Receipt = R::Receipt; + type Evm = E; + + fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> { + // If first block deploy genesis contracts + if self.evm.block().number == 1 { + self.deploy_genesis_contracts(self.evm.block().beneficiary)?; + } + + Ok(()) + } + + fn execute_transaction_with_commit_condition( + &mut self, + _tx: impl ExecutableTx, + _f: impl FnOnce(&ExecutionResult<::HaltReason>) -> CommitChanges, + ) -> Result, BlockExecutionError> { + Ok(Some(0)) + } + + fn execute_transaction_with_result_closure( + &mut self, + tx: impl ExecutableTx + + IntoTxEnv<::Tx> + + RecoveredTx, + f: impl for<'b> FnOnce(&'b ExecutionResult<::HaltReason>), + ) -> Result { + // Check if it's a system transaction + // let signer = tx.signer(); + // let is_system_transaction = is_system_transaction(tx.tx()); + + let block_available_gas = self.evm.block().gas_limit - self.gas_used; + if tx.tx().gas_limit() > block_available_gas { + return Err( + BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { + transaction_gas_limit: tx.tx().gas_limit(), + block_available_gas, + } + .into(), + ); + } + let result_and_state = self + .evm + .transact(tx) + .map_err(|err| BlockExecutionError::evm(err, tx.tx().trie_hash()))?; + let ResultAndState { result, mut state } = result_and_state; + f(&result); + let gas_used = result.gas_used(); + self.gas_used += gas_used; + self.receipts + .push(self.receipt_builder.build_receipt(ReceiptBuilderCtx { + tx: tx.tx(), + evm: &self.evm, + result, + state: &state, + cumulative_gas_used: self.gas_used, + })); + + // apply patches after + patch_mainnet_after_tx( + self.evm.block().number, + self.receipts.len() as u64, + is_system_transaction(tx.tx()), + &mut state, + )?; + + self.evm.db_mut().commit(state); + + Ok(gas_used) + } + + fn finish( + mut self, + ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { + Ok(( + self.evm, + BlockExecutionResult { + receipts: self.receipts, + requests: Requests::default(), + gas_used: self.gas_used, + }, + )) + } + + fn set_state_hook(&mut self, _hook: Option>) {} + + fn evm_mut(&mut self) -> &mut Self::Evm { + &mut self.evm + } + + fn evm(&self) -> &Self::Evm { + &self.evm + } +} diff --git a/src/node/evm/factory.rs b/src/node/evm/factory.rs new file mode 100644 index 000000000..8bbbd21b5 --- /dev/null +++ b/src/node/evm/factory.rs @@ -0,0 +1,74 @@ +use super::HlEvm; +use crate::evm::{ + api::{ + builder::HlBuilder, + ctx::{HlContext, DefaultHl}, + }, + precompiles::HlPrecompiles, + spec::HlSpecId, + transaction::HlTxEnv, +}; +use reth_evm::{precompiles::PrecompilesMap, EvmEnv, EvmFactory}; +use reth_revm::{Context, Database}; +use revm::{ + context::{ + result::{EVMError, HaltReason}, + TxEnv, + }, + inspector::NoOpInspector, + Inspector, +}; + +/// Factory producing [`HlEvm`]. +#[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] +pub struct HlEvmFactory; + +impl EvmFactory for HlEvmFactory { + type Evm, I: Inspector>> = + HlEvm; + type Context> = HlContext; + type Tx = HlTxEnv; + type Error = EVMError; + type HaltReason = HaltReason; + type Spec = HlSpecId; + type Precompiles = PrecompilesMap; + + fn create_evm>( + &self, + db: DB, + input: EvmEnv, + ) -> Self::Evm { + let precompiles = HlPrecompiles::new(input.cfg_env.spec).precompiles(); + HlEvm { + inner: Context::hl() + .with_block(input.block_env) + .with_cfg(input.cfg_env) + .with_db(db) + .build_hl_with_inspector(NoOpInspector {}) + .with_precompiles(PrecompilesMap::from_static(precompiles)), + inspect: false, + } + } + + fn create_evm_with_inspector< + DB: Database, + I: Inspector>, + >( + &self, + db: DB, + input: EvmEnv, + inspector: I, + ) -> Self::Evm { + let precompiles = HlPrecompiles::new(input.cfg_env.spec).precompiles(); + HlEvm { + inner: Context::hl() + .with_block(input.block_env) + .with_cfg(input.cfg_env) + .with_db(db) + .build_hl_with_inspector(inspector) + .with_precompiles(PrecompilesMap::from_static(precompiles)), + inspect: true, + } + } +} diff --git a/src/node/evm/mod.rs b/src/node/evm/mod.rs new file mode 100644 index 000000000..9ee32a7ea --- /dev/null +++ b/src/node/evm/mod.rs @@ -0,0 +1,165 @@ +use crate::{ + evm::{ + api::{ctx::HlContext, HlEvmInner}, + precompiles::HlPrecompiles, + spec::HlSpecId, + transaction::{HlTxEnv, HlTxTr}, + }, + node::HlNode, +}; +use alloy_primitives::{Address, Bytes}; +use config::HlEvmConfig; +use reth::{ + api::FullNodeTypes, + builder::{components::ExecutorBuilder, BuilderContext}, +}; +use reth_evm::{Evm, EvmEnv}; +use revm::{ + context::{ + result::{EVMError, HaltReason, ResultAndState}, + BlockEnv, TxEnv, + }, + handler::{instructions::EthInstructions, PrecompileProvider}, + interpreter::{interpreter::EthInterpreter, InterpreterResult}, + Context, Database, ExecuteEvm, InspectEvm, Inspector, +}; +use std::ops::{Deref, DerefMut}; + +mod assembler; +pub mod config; +mod executor; +mod factory; +mod patch; + +/// HL EVM implementation. +/// +/// This is a wrapper type around the `revm` evm with optional [`Inspector`] (tracing) +/// support. [`Inspector`] support is configurable at runtime because it's part of the underlying +#[allow(missing_debug_implementations)] +pub struct HlEvm { + pub inner: HlEvmInner, I, EthInstructions>, P>, + pub inspect: bool, +} + +impl HlEvm { + /// Provides a reference to the EVM context. + pub const fn ctx(&self) -> &HlContext { + &self.inner.0.ctx + } + + /// Provides a mutable reference to the EVM context. + pub fn ctx_mut(&mut self) -> &mut HlContext { + &mut self.inner.0.ctx + } +} + +impl Deref for HlEvm { + type Target = HlContext; + + #[inline] + fn deref(&self) -> &Self::Target { + self.ctx() + } +} + +impl DerefMut for HlEvm { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.ctx_mut() + } +} + +impl Evm for HlEvm +where + DB: Database, + I: Inspector>, + P: PrecompileProvider, Output = InterpreterResult>, + ::Error: std::marker::Send + std::marker::Sync + 'static, +{ + type DB = DB; + type Tx = HlTxEnv; + type Error = EVMError; + type HaltReason = HaltReason; + type Spec = HlSpecId; + type Precompiles = P; + type Inspector = I; + + fn chain_id(&self) -> u64 { + self.cfg.chain_id + } + + fn block(&self) -> &BlockEnv { + &self.block + } + + fn transact_raw( + &mut self, + tx: Self::Tx, + ) -> Result, Self::Error> { + if self.inspect { + self.inner.set_tx(tx); + self.inner.inspect_replay() + } else if tx.is_system_transaction() { + self.inner.set_tx(tx); + self.inner.inspect_replay() + } else { + self.inner.transact(tx) + } + } + + fn transact_system_call( + &mut self, + _caller: Address, + _contract: Address, + _data: Bytes, + ) -> Result, Self::Error> { + unimplemented!() + } + + fn db_mut(&mut self) -> &mut Self::DB { + &mut self.journaled_state.database + } + + fn finish(self) -> (Self::DB, EvmEnv) { + let Context { block: block_env, cfg: cfg_env, journaled_state, .. } = self.inner.0.ctx; + + (journaled_state.database, EvmEnv { block_env, cfg_env }) + } + + fn set_inspector_enabled(&mut self, enabled: bool) { + self.inspect = enabled; + } + + fn precompiles_mut(&mut self) -> &mut Self::Precompiles { + &mut self.inner.0.precompiles + } + + fn inspector_mut(&mut self) -> &mut Self::Inspector { + &mut self.inner.0.inspector + } + + fn precompiles(&self) -> &Self::Precompiles { + &self.inner.0.precompiles + } + + fn inspector(&self) -> &Self::Inspector { + &self.inner.0.inspector + } +} + +/// A regular hl evm and executor builder. +#[derive(Debug, Default, Clone, Copy)] +#[non_exhaustive] +pub struct HlExecutorBuilder; + +impl ExecutorBuilder for HlExecutorBuilder +where + Node: FullNodeTypes, +{ + type EVM = HlEvmConfig; + + async fn build_evm(self, ctx: &BuilderContext) -> eyre::Result { + let evm_config = HlEvmConfig::hl(ctx.chain_spec()); + Ok(evm_config) + } +} diff --git a/src/node/evm/patch.rs b/src/node/evm/patch.rs new file mode 100644 index 000000000..d699d991d --- /dev/null +++ b/src/node/evm/patch.rs @@ -0,0 +1,145 @@ +use alloy_primitives::{address, b256, Address, B256, U256}; +use reth_evm::block::BlockExecutionError; +use reth_primitives_traits::SignedTransaction; +use reth_revm::{db::states::StorageSlot, State}; +use revm::{primitives::HashMap, state::Account, Database}; +use std::{str::FromStr, sync::LazyLock}; +use tracing::trace; + +/// Applies storage patches to the state after a transaction is executed. +/// See https://github.com/hyperliquid-dex/hyper-evm-sync/commit/39047242b6260f7764527a2f5057dd9c3a75aa89 for more details. +static MAINNET_PATCHES_AFTER_TX: &[(u64, u64, bool, Address)] = &[ + ( + 1_467_569, + 0, + false, + address!("0x33f6fe38c55cb100ce27b3138e5d2d041648364f"), + ), + ( + 1_467_631, + 0, + false, + address!("0x33f6fe38c55cb100ce27b3138e5d2d041648364f"), + ), + ( + 1_499_313, + 2, + false, + address!("0xe27bfc0a812b38927ff646f24af9149f45deb550"), + ), + ( + 1_499_406, + 0, + false, + address!("0xe27bfc0a812b38927ff646f24af9149f45deb550"), + ), + ( + 1_499_685, + 0, + false, + address!("0xfee3932b75a87e86930668a6ab3ed43b404c8a30"), + ), + ( + 1_514_843, + 0, + false, + address!("0x723e5fbbeed025772a91240fd0956a866a41a603"), + ), + ( + 1_514_936, + 0, + false, + address!("0x723e5fbbeed025772a91240fd0956a866a41a603"), + ), + ( + 1_530_529, + 2, + false, + address!("0xa694e8fd8f4a177dd23636d838e9f1fb2138d87a"), + ), + ( + 1_530_622, + 2, + false, + address!("0xa694e8fd8f4a177dd23636d838e9f1fb2138d87a"), + ), + ( + 1_530_684, + 3, + false, + address!("0xa694e8fd8f4a177dd23636d838e9f1fb2138d87a"), + ), + ( + 1_530_777, + 3, + false, + address!("0xa694e8fd8f4a177dd23636d838e9f1fb2138d87a"), + ), + ( + 1_530_839, + 2, + false, + address!("0x692a343fc401a7755f8fc2facf61af426adaf061"), + ), + ( + 1_530_901, + 0, + false, + address!("0xfd9716f16596715ce765dabaee11787870e04b8a"), + ), + ( + 1_530_994, + 3, + false, + address!("0xfd9716f16596715ce765dabaee11787870e04b8a"), + ), + ( + 1_531_056, + 4, + false, + address!("0xdc67c2b8349ca20f58760e08371fc9271e82b5a4"), + ), + ( + 1_531_149, + 0, + false, + address!("0xdc67c2b8349ca20f58760e08371fc9271e82b5a4"), + ), + ( + 1_531_211, + 3, + false, + address!("0xdc67c2b8349ca20f58760e08371fc9271e82b5a4"), + ), + ( + 1_531_366, + 1, + false, + address!("0x9a90a517d27a9e60e454c96fefbbe94ff244ed6f"), + ), +]; + +pub(crate) fn patch_mainnet_after_tx( + block_number: u64, + tx_index: u64, + is_system_tx: bool, + changes: &mut HashMap, +) -> Result<(), BlockExecutionError> { + if MAINNET_PATCHES_AFTER_TX.is_empty() { + return Ok(()); + } + + let first = MAINNET_PATCHES_AFTER_TX.first().unwrap().0; + let last = MAINNET_PATCHES_AFTER_TX.last().unwrap().0; + if first > block_number || last < block_number { + return Ok(()); + } + + for (block_num, idx, is_system, address) in MAINNET_PATCHES_AFTER_TX { + if block_number == *block_num && tx_index == *idx && is_system_tx == *is_system { + changes.remove(address); + } + } + + Ok(()) +} diff --git a/src/node/network/block_import/handle.rs b/src/node/network/block_import/handle.rs new file mode 100644 index 000000000..5046efe4a --- /dev/null +++ b/src/node/network/block_import/handle.rs @@ -0,0 +1,46 @@ +use std::task::{Context, Poll}; + +use reth_engine_primitives::EngineTypes; +use reth_network::import::BlockImportError; +use reth_network_api::PeerId; +use reth_payload_primitives::PayloadTypes; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +use super::service::{BlockMsg, ImportEvent, IncomingBlock, Outcome}; + +/// A handle for interacting with the block import service. +/// +/// This handle provides a bidirectional communication channel with the +/// [`super::service::ImportService`]: +/// - Blocks can be sent to the service for import via [`send_block`](ImportHandle::send_block) +/// - Import outcomes can be received via [`poll_outcome`](ImportHandle::poll_outcome)` +#[derive(Debug)] +pub struct ImportHandle { + /// Send the new block to the service + to_import: UnboundedSender, + /// Receive the event(Announcement/Outcome) of the import + import_outcome: UnboundedReceiver, +} + +impl ImportHandle { + /// Create a new handle with the provided channels + pub fn new( + to_import: UnboundedSender, + import_outcome: UnboundedReceiver, + ) -> Self { + Self { to_import, import_outcome } + } + + /// Sends the block to import to the service. + /// Returns a [`BlockImportError`] if the channel to the import service is closed. + pub fn send_block(&self, block: BlockMsg, peer_id: PeerId) -> Result<(), BlockImportError> { + self.to_import + .send((block, peer_id)) + .map_err(|_| BlockImportError::Other("block import service channel closed".into())) + } + + /// Poll for the next import event + pub fn poll_outcome(&mut self, cx: &mut Context<'_>) -> Poll> { + self.import_outcome.poll_recv(cx) + } +} diff --git a/src/node/network/block_import/mod.rs b/src/node/network/block_import/mod.rs new file mode 100644 index 000000000..f75005da5 --- /dev/null +++ b/src/node/network/block_import/mod.rs @@ -0,0 +1,41 @@ +#![allow(unused)] +use handle::ImportHandle; +use reth_engine_primitives::EngineTypes; +use reth_network::import::{BlockImport, BlockImportOutcome, NewBlockEvent}; +use reth_network_peers::PeerId; +use reth_payload_primitives::{BuiltPayload, PayloadTypes}; +use reth_primitives::NodePrimitives; +use service::{BlockMsg, ImportEvent, Outcome}; +use std::{ + fmt, + task::{ready, Context, Poll}, +}; + +use crate::node::network::HlNewBlock; + +pub mod handle; +pub mod service; + +#[derive(Debug)] +pub struct HlBlockImport { + handle: ImportHandle, +} + +impl HlBlockImport { + pub fn new(handle: ImportHandle) -> Self { + Self { handle } + } +} + +impl BlockImport for HlBlockImport { + fn on_new_block(&mut self, peer_id: PeerId, incoming_block: NewBlockEvent) { + unreachable!("reth-hl does not use network, but uses poller for files"); + } + + fn poll(&mut self, cx: &mut Context<'_>) -> Poll { + match ready!(self.handle.poll_outcome(cx)) { + Some(outcome) => Poll::Ready(outcome), + None => Poll::Pending, + } + } +} diff --git a/src/node/network/block_import/service.rs b/src/node/network/block_import/service.rs new file mode 100644 index 000000000..40f5e63ba --- /dev/null +++ b/src/node/network/block_import/service.rs @@ -0,0 +1,418 @@ +use super::handle::ImportHandle; +use crate::{ + consensus::{HlConsensus, HlConsensusErr}, + node::{network::HlNewBlock, rpc::engine_api::payload::HlPayloadTypes}, + HlBlock, HlBlockBody, +}; +use alloy_consensus::{BlockBody, Header}; +use alloy_primitives::U128; +use alloy_rpc_types::engine::{ForkchoiceState, PayloadStatusEnum}; +use futures::{future::Either, stream::FuturesUnordered, StreamExt}; +use reth_engine_primitives::{BeaconConsensusEngineHandle, EngineTypes}; +use reth_network::{ + import::{BlockImportError, BlockImportEvent, BlockImportOutcome, BlockValidation}, + message::NewBlockMessage, +}; +use reth_network_api::PeerId; +use reth_node_ethereum::EthEngineTypes; +use reth_payload_primitives::{BuiltPayload, EngineApiMessageVersion, PayloadTypes}; +use reth_primitives::NodePrimitives; +use reth_primitives_traits::{AlloyBlockHeader, Block}; +use reth_provider::{BlockHashReader, BlockNumReader}; +use std::{ + future::Future, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; + +/// Network message containing a new block +pub(crate) type BlockMsg = NewBlockMessage; + +/// Import outcome for a block +pub(crate) type Outcome = BlockImportOutcome; + +/// Import event for a block +pub(crate) type ImportEvent = BlockImportEvent; + +/// Future that processes a block import and returns its outcome +type ImportFut = Pin> + Send + Sync>>; + +/// Channel message type for incoming blocks +pub(crate) type IncomingBlock = (BlockMsg, PeerId); + +/// A service that handles bidirectional block import communication with the network. +/// It receives new blocks from the network via `from_network` channel and sends back +/// import outcomes via `to_network` channel. +pub struct ImportService +where + Provider: BlockNumReader + Clone, +{ + /// The handle to communicate with the engine service + engine: BeaconConsensusEngineHandle, + /// The consensus implementation + consensus: Arc>, + /// Receive the new block from the network + from_network: UnboundedReceiver, + /// Send the event of the import to the network + to_network: UnboundedSender, + /// Pending block imports. + pending_imports: FuturesUnordered, +} + +impl ImportService +where + Provider: BlockNumReader + Clone + 'static, +{ + /// Create a new block import service + pub fn new( + consensus: Arc>, + engine: BeaconConsensusEngineHandle, + from_network: UnboundedReceiver, + to_network: UnboundedSender, + ) -> Self { + Self { + engine, + consensus, + from_network, + to_network, + pending_imports: FuturesUnordered::new(), + } + } + + /// Process a new payload and return the outcome + fn new_payload(&self, block: BlockMsg, peer_id: PeerId) -> ImportFut { + let engine = self.engine.clone(); + + Box::pin(async move { + let sealed_block = block.block.0.block.clone().seal(); + let payload = HlPayloadTypes::block_to_payload(sealed_block); + + match engine.new_payload(payload).await { + Ok(payload_status) => match payload_status.status { + PayloadStatusEnum::Valid => { + Outcome { peer: peer_id, result: Ok(BlockValidation::ValidBlock { block }) } + .into() + } + PayloadStatusEnum::Invalid { validation_error } => Outcome { + peer: peer_id, + result: Err(BlockImportError::Other(validation_error.into())), + } + .into(), + _ => None, + }, + Err(err) => None, + } + }) + } + + /// Process a forkchoice update and return the outcome + fn update_fork_choice(&self, block: BlockMsg, peer_id: PeerId) -> ImportFut { + let engine = self.engine.clone(); + let consensus = self.consensus.clone(); + let sealed_block = block.block.0.block.clone().seal(); + let hash = sealed_block.hash(); + let number = sealed_block.number(); + + Box::pin(async move { + let (head_block_hash, current_hash) = match consensus.canonical_head(hash, number) { + Ok(hash) => hash, + Err(_) => return None, + }; + + let state = ForkchoiceState { + head_block_hash, + safe_block_hash: head_block_hash, + finalized_block_hash: head_block_hash, + }; + + match engine.fork_choice_updated(state, None, EngineApiMessageVersion::default()).await + { + Ok(response) => match response.payload_status.status { + PayloadStatusEnum::Valid => { + Outcome { peer: peer_id, result: Ok(BlockValidation::ValidBlock { block }) } + .into() + } + PayloadStatusEnum::Invalid { validation_error } => Outcome { + peer: peer_id, + result: Err(BlockImportError::Other(validation_error.into())), + } + .into(), + _ => None, + }, + Err(err) => None, + } + }) + } + + /// Add a new block import task to the pending imports + fn on_new_block(&mut self, block: BlockMsg, peer_id: PeerId) { + let payload_fut = self.new_payload(block.clone(), peer_id); + self.pending_imports.push(payload_fut); + + let fcu_fut = self.update_fork_choice(block, peer_id); + self.pending_imports.push(fcu_fut); + } +} + +impl Future for ImportService +where + Provider: BlockNumReader + BlockHashReader + Clone + 'static + Unpin, +{ + type Output = Result<(), Box>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + // Receive new blocks from network + while let Poll::Ready(Some((block, peer_id))) = this.from_network.poll_recv(cx) { + this.on_new_block(block, peer_id); + } + + // Process completed imports and send events to network + while let Poll::Ready(Some(outcome)) = this.pending_imports.poll_next_unpin(cx) { + if let Some(outcome) = outcome { + if let Err(e) = this.to_network.send(BlockImportEvent::Outcome(outcome)) { + return Poll::Ready(Err(Box::new(e))); + } + } + } + + Poll::Pending + } +} + +#[cfg(test)] +mod tests { + use crate::chainspec::bsc::bsc_mainnet; + + use super::*; + use alloy_primitives::{B256, U128}; + use alloy_rpc_types::engine::PayloadStatus; + use reth_chainspec::ChainInfo; + use reth_engine_primitives::{BeaconEngineMessage, OnForkChoiceUpdated}; + use reth_eth_wire::NewBlock; + use reth_node_ethereum::EthEngineTypes; + use reth_primitives::Block; + use reth_provider::ProviderError; + use std::{ + sync::Arc, + task::{Context, Poll}, + }; + + #[tokio::test] + async fn can_handle_valid_block() { + let mut fixture = TestFixture::new(EngineResponses::both_valid()).await; + fixture + .assert_block_import(|outcome| { + matches!( + outcome, + BlockImportEvent::Outcome(BlockImportOutcome { + peer: _, + result: Ok(BlockValidation::ValidBlock { .. }) + }) + ) + }) + .await; + } + + #[tokio::test] + async fn can_handle_invalid_new_payload() { + let mut fixture = TestFixture::new(EngineResponses::invalid_new_payload()).await; + fixture + .assert_block_import(|outcome| { + matches!( + outcome, + BlockImportEvent::Outcome(BlockImportOutcome { + peer: _, + result: Err(BlockImportError::Other(_)) + }) + ) + }) + .await; + } + + #[tokio::test] + async fn can_handle_invalid_fcu() { + let mut fixture = TestFixture::new(EngineResponses::invalid_fcu()).await; + fixture + .assert_block_import(|outcome| { + matches!( + outcome, + BlockImportEvent::Outcome(BlockImportOutcome { + peer: _, + result: Err(BlockImportError::Other(_)) + }) + ) + }) + .await; + } + + #[derive(Clone)] + struct MockProvider; + + impl BlockNumReader for MockProvider { + fn chain_info(&self) -> Result { + unimplemented!() + } + + fn best_block_number(&self) -> Result { + Ok(0) + } + + fn last_block_number(&self) -> Result { + Ok(0) + } + + fn block_number(&self, _hash: B256) -> Result, ProviderError> { + Ok(None) + } + } + + impl BlockHashReader for MockProvider { + fn block_hash(&self, _number: u64) -> Result, ProviderError> { + Ok(Some(B256::ZERO)) + } + + fn canonical_hashes_range( + &self, + _start: u64, + _end: u64, + ) -> Result, ProviderError> { + Ok(vec![]) + } + } + + /// Response configuration for engine messages + struct EngineResponses { + new_payload: PayloadStatusEnum, + fcu: PayloadStatusEnum, + } + + impl EngineResponses { + fn both_valid() -> Self { + Self { new_payload: PayloadStatusEnum::Valid, fcu: PayloadStatusEnum::Valid } + } + + fn invalid_new_payload() -> Self { + Self { + new_payload: PayloadStatusEnum::Invalid { validation_error: "test error".into() }, + fcu: PayloadStatusEnum::Valid, + } + } + + fn invalid_fcu() -> Self { + Self { + new_payload: PayloadStatusEnum::Valid, + fcu: PayloadStatusEnum::Invalid { validation_error: "fcu error".into() }, + } + } + } + + /// Test fixture for block import tests + struct TestFixture { + handle: ImportHandle, + } + + impl TestFixture { + /// Create a new test fixture with the given engine responses + async fn new(responses: EngineResponses) -> Self { + let consensus = Arc::new(HlConsensus { provider: MockProvider }); + let (to_engine, from_engine) = mpsc::unbounded_channel(); + let engine_handle = BeaconConsensusEngineHandle::new(to_engine); + + handle_engine_msg(from_engine, responses).await; + + let (to_import, from_network) = mpsc::unbounded_channel(); + let (to_network, import_outcome) = mpsc::unbounded_channel(); + + let handle = ImportHandle::new(to_import, import_outcome); + + let service = ImportService::new(consensus, engine_handle, from_network, to_network); + tokio::spawn(Box::pin(async move { + service.await.unwrap(); + })); + + Self { handle } + } + + /// Run a block import test with the given event assertion + async fn assert_block_import(&mut self, assert_fn: F) + where + F: Fn(&BlockImportEvent) -> bool, + { + let block_msg = create_test_block(); + self.handle.send_block(block_msg, PeerId::random()).unwrap(); + + let waker = futures::task::noop_waker(); + let mut cx = Context::from_waker(&waker); + let mut outcomes = Vec::new(); + + // Wait for both NewPayload and FCU outcomes + while outcomes.len() < 2 { + match self.handle.poll_outcome(&mut cx) { + Poll::Ready(Some(outcome)) => { + outcomes.push(outcome); + } + Poll::Ready(None) => break, + Poll::Pending => tokio::task::yield_now().await, + } + } + + // Assert that at least one outcome matches our criteria + assert!( + outcomes.iter().any(assert_fn), + "No outcome matched the expected criteria. Outcomes: {outcomes:?}" + ); + } + } + + /// Creates a test block message + fn create_test_block() -> NewBlockMessage { + let block = HlBlock { + header: Header::default(), + body: HlBlockBody { + inner: BlockBody { + transactions: Vec::new(), + ommers: Vec::new(), + withdrawals: None, + }, + sidecars: None, + }, + }; + let new_block = HlNewBlock(NewBlock { block, td: U128::from(1) }); + let hash = new_block.0.block.header.hash_slow(); + NewBlockMessage { hash, block: Arc::new(new_block) } + } + + /// Helper function to handle engine messages with specified payload statuses + async fn handle_engine_msg( + mut from_engine: mpsc::UnboundedReceiver>, + responses: EngineResponses, + ) { + tokio::spawn(Box::pin(async move { + while let Some(message) = from_engine.recv().await { + match message { + BeaconEngineMessage::NewPayload { payload: _, tx } => { + tx.send(Ok(PayloadStatus::new(responses.new_payload.clone(), None))) + .unwrap(); + } + BeaconEngineMessage::ForkchoiceUpdated { + state: _, + payload_attrs: _, + version: _, + tx, + } => { + tx.send(Ok(OnForkChoiceUpdated::valid(PayloadStatus::new( + responses.fcu.clone(), + None, + )))) + .unwrap(); + } + _ => {} + } + } + })); + } +} diff --git a/src/node/network/mod.rs b/src/node/network/mod.rs new file mode 100644 index 000000000..659ebf5bc --- /dev/null +++ b/src/node/network/mod.rs @@ -0,0 +1,255 @@ +#![allow(clippy::owned_cow)] +use crate::{ + consensus::HlConsensus, + node::{ + network::block_import::{handle::ImportHandle, service::ImportService, HlBlockImport}, + primitives::HlPrimitives, + rpc::engine_api::payload::HlPayloadTypes, + types::ReadPrecompileCalls, + HlNode, + }, + HlBlock, +}; +use alloy_rlp::{Decodable, Encodable}; +// use handshake::HlHandshake; +use reth::{ + api::{FullNodeTypes, TxTy}, + builder::{components::NetworkBuilder, BuilderContext}, + transaction_pool::{PoolTransaction, TransactionPool}, +}; +use reth_discv4::{Discv4Config, NodeRecord}; +use reth_engine_primitives::BeaconConsensusEngineHandle; +use reth_eth_wire::{BasicNetworkPrimitives, NewBlock, NewBlockPayload}; +use reth_ethereum_primitives::PooledTransactionVariant; +use reth_network::{NetworkConfig, NetworkHandle, NetworkManager}; +use reth_network_api::PeersInfo; +use std::{sync::Arc, time::Duration}; +use tokio::sync::{mpsc, oneshot, Mutex}; +use tracing::info; + +pub mod block_import; +// pub mod handshake; +// pub(crate) mod upgrade_status; +/// HL `NewBlock` message value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HlNewBlock(pub NewBlock); + +mod rlp { + use super::*; + use crate::HlBlockBody; + use alloy_consensus::{BlobTransactionSidecar, BlockBody, Header}; + use alloy_primitives::U128; + use alloy_rlp::{RlpDecodable, RlpEncodable}; + use alloy_rpc_types::Withdrawals; + use reth_primitives::TransactionSigned; + use std::borrow::Cow; + + #[derive(RlpEncodable, RlpDecodable)] + #[rlp(trailing)] + struct BlockHelper<'a> { + header: Cow<'a, Header>, + transactions: Cow<'a, Vec>, + ommers: Cow<'a, Vec
>, + withdrawals: Option>, + } + + #[derive(RlpEncodable, RlpDecodable)] + #[rlp(trailing)] + struct HlNewBlockHelper<'a> { + block: BlockHelper<'a>, + td: U128, + sidecars: Option>>, + read_precompile_calls: Option>, + } + + impl<'a> From<&'a HlNewBlock> for HlNewBlockHelper<'a> { + fn from(value: &'a HlNewBlock) -> Self { + let HlNewBlock(NewBlock { + block: + HlBlock { + header, + body: + HlBlockBody { + inner: + BlockBody { + transactions, + ommers, + withdrawals, + }, + sidecars, + read_precompile_calls, + }, + }, + td, + }) = value; + + Self { + block: BlockHelper { + header: Cow::Borrowed(header), + transactions: Cow::Borrowed(transactions), + ommers: Cow::Borrowed(ommers), + withdrawals: withdrawals.as_ref().map(Cow::Borrowed), + }, + td: *td, + sidecars: sidecars.as_ref().map(Cow::Borrowed), + read_precompile_calls: read_precompile_calls.as_ref().map(Cow::Borrowed), + } + } + } + + impl Encodable for HlNewBlock { + fn encode(&self, out: &mut dyn bytes::BufMut) { + HlNewBlockHelper::from(self).encode(out); + } + + fn length(&self) -> usize { + HlNewBlockHelper::from(self).length() + } + } + + impl Decodable for HlNewBlock { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let HlNewBlockHelper { + block: + BlockHelper { + header, + transactions, + ommers, + withdrawals, + }, + td, + sidecars, + read_precompile_calls, + } = HlNewBlockHelper::decode(buf)?; + + Ok(HlNewBlock(NewBlock { + block: HlBlock { + header: header.into_owned(), + body: HlBlockBody { + inner: BlockBody { + transactions: transactions.into_owned(), + ommers: ommers.into_owned(), + withdrawals: withdrawals.map(|w| w.into_owned()), + }, + sidecars: sidecars.map(|s| s.into_owned()), + read_precompile_calls: read_precompile_calls.map(|s| s.into_owned()), + }, + }, + td, + })) + } + } +} + +impl NewBlockPayload for HlNewBlock { + type Block = HlBlock; + + fn block(&self) -> &Self::Block { + &self.0.block + } +} + +/// Network primitives for HL. +pub type HlNetworkPrimitives = + BasicNetworkPrimitives; + +/// A basic hl network builder. +#[derive(Debug)] +pub struct HlNetworkBuilder { + pub(crate) engine_handle_rx: + Arc>>>>, +} + +impl HlNetworkBuilder { + /// Returns the [`NetworkConfig`] that contains the settings to launch the p2p network. + /// + /// This applies the configured [`HlNetworkBuilder`] settings. + pub fn network_config( + self, + ctx: &BuilderContext, + ) -> eyre::Result> + where + Node: FullNodeTypes, + { + let Self { engine_handle_rx } = self; + + let network_builder = ctx.network_config_builder()?; + + let (to_import, from_network) = mpsc::unbounded_channel(); + let (to_network, import_outcome) = mpsc::unbounded_channel(); + + let handle = ImportHandle::new(to_import, import_outcome); + let consensus = Arc::new(HlConsensus { + provider: ctx.provider().clone(), + }); + + ctx.task_executor() + .spawn_critical("block import", async move { + let handle = engine_handle_rx + .lock() + .await + .take() + .expect("node should only be launched once") + .await + .unwrap(); + + ImportService::new(consensus, handle, from_network, to_network) + .await + .unwrap(); + }); + + let network_builder = network_builder + // .boot_nodes(boot_nodes()) + .set_head(ctx.head()) + // .with_pow() + .block_import(Box::new(HlBlockImport::new(handle))); + // .discovery(discv4) + // .eth_rlpx_handshake(Arc::new(HlHandshake::default())); + + let network_config = ctx.build_network_config(network_builder); + + Ok(network_config) + } +} + +impl NetworkBuilder for HlNetworkBuilder +where + Node: FullNodeTypes, + Pool: TransactionPool< + Transaction: PoolTransaction< + Consensus = TxTy, + Pooled = PooledTransactionVariant, + >, + > + Unpin + + 'static, +{ + type Network = NetworkHandle; + + async fn build_network( + self, + ctx: &BuilderContext, + pool: Pool, + ) -> eyre::Result { + let network_config = self.network_config(ctx)?; + let network = NetworkManager::builder(network_config).await?; + let handle = ctx.start_network(network, pool); + info!(target: "reth::cli", enode=%handle.local_node_record(), "P2P networking initialized"); + + Ok(handle) + } +} + +/// HL mainnet bootnodes +static BOOTNODES : [&str; 6] = [ + "enode://433c8bfdf53a3e2268ccb1b829e47f629793291cbddf0c76ae626da802f90532251fc558e2e0d10d6725e759088439bf1cd4714716b03a259a35d4b2e4acfa7f@52.69.102.73:30311", + "enode://571bee8fb902a625942f10a770ccf727ae2ba1bab2a2b64e121594a99c9437317f6166a395670a00b7d93647eacafe598b6bbcef15b40b6d1a10243865a3e80f@35.73.84.120:30311", + "enode://fac42fb0ba082b7d1eebded216db42161163d42e4f52c9e47716946d64468a62da4ba0b1cac0df5e8bf1e5284861d757339751c33d51dfef318be5168803d0b5@18.203.152.54:30311", + "enode://3063d1c9e1b824cfbb7c7b6abafa34faec6bb4e7e06941d218d760acdd7963b274278c5c3e63914bd6d1b58504c59ec5522c56f883baceb8538674b92da48a96@34.250.32.100:30311", + "enode://ad78c64a4ade83692488aa42e4c94084516e555d3f340d9802c2bf106a3df8868bc46eae083d2de4018f40e8d9a9952c32a0943cd68855a9bc9fd07aac982a6d@34.204.214.24:30311", + "enode://5db798deb67df75d073f8e2953dad283148133acb520625ea804c9c4ad09a35f13592a762d8f89056248f3889f6dcc33490c145774ea4ff2966982294909b37a@107.20.191.97:30311", + +]; + +pub fn boot_nodes() -> Vec { + BOOTNODES[..].iter().map(|s| s.parse().unwrap()).collect() +} diff --git a/src/node/rpc/block.rs b/src/node/rpc/block.rs new file mode 100644 index 000000000..861c83258 --- /dev/null +++ b/src/node/rpc/block.rs @@ -0,0 +1,173 @@ +use crate::{ + chainspec::HlChainSpec, + node::rpc::{HlEthApi, HlNodeCore}, + node::{HlBlock, HlPrimitives}, +}; +use alloy_consensus::BlockHeader; +use alloy_primitives::B256; +use reth::{ + api::NodeTypes, + builder::FullNodeComponents, + primitives::{Receipt, SealedHeader, TransactionMeta, TransactionSigned}, + providers::{BlockReaderIdExt, ProviderHeader, ReceiptProvider, TransactionsProvider}, + rpc::{ + eth::EthApiTypes, + server_types::eth::{error::FromEvmError, EthApiError, EthReceiptBuilder, PendingBlock}, + types::{BlockId, TransactionReceipt}, + }, + transaction_pool::{PoolTransaction, TransactionPool}, +}; +use reth_chainspec::{EthChainSpec, EthereumHardforks}; +use reth_evm::{ConfigureEvm, NextBlockEnvAttributes}; +use reth_primitives_traits::BlockBody as _; +use reth_provider::{ + BlockReader, ChainSpecProvider, HeaderProvider, ProviderBlock, ProviderReceipt, ProviderTx, + StateProviderFactory, +}; +use reth_rpc_eth_api::{ + helpers::{EthBlocks, LoadBlock, LoadPendingBlock, LoadReceipt, SpawnBlocking}, + types::RpcTypes, + FromEthApiError, RpcNodeCore, RpcNodeCoreExt, RpcReceipt, +}; + +impl EthBlocks for HlEthApi +where + Self: LoadBlock< + Error = EthApiError, + NetworkTypes: RpcTypes, + Provider: BlockReader, + >, + N: HlNodeCore + HeaderProvider>, +{ + async fn block_receipts( + &self, + block_id: BlockId, + ) -> Result>>, Self::Error> + where + Self: LoadReceipt, + { + if let Some((block, receipts)) = self.load_block_and_receipts(block_id).await? { + let block_number = block.number(); + let base_fee = block.base_fee_per_gas(); + let block_hash = block.hash(); + let excess_blob_gas = block.excess_blob_gas(); + let timestamp = block.timestamp(); + let blob_params = self + .provider() + .chain_spec() + .blob_params_at_timestamp(timestamp); + + return block + .body() + .transactions() + .iter() + .zip(receipts.iter()) + .enumerate() + .map(|(idx, (tx, receipt))| { + let meta = TransactionMeta { + tx_hash: *tx.tx_hash(), + index: idx as u64, + block_hash, + block_number, + base_fee, + excess_blob_gas, + timestamp, + }; + EthReceiptBuilder::new(tx, meta, receipt, &receipts, blob_params) + .map(|builder| builder.build()) + }) + .collect::, Self::Error>>() + .map(Some); + } + + Ok(None) + } +} + +impl LoadBlock for HlEthApi +where + Self: LoadPendingBlock + + SpawnBlocking + + RpcNodeCoreExt< + Pool: TransactionPool< + Transaction: PoolTransaction>, + >, + >, + N: HlNodeCore, +{ +} + +impl LoadPendingBlock for HlEthApi +where + Self: SpawnBlocking + + EthApiTypes< + NetworkTypes: RpcTypes< + Header = alloy_rpc_types_eth::Header>, + >, + Error: FromEvmError, + >, + N: RpcNodeCore< + Provider: BlockReaderIdExt< + Transaction = TransactionSigned, + Block = HlBlock, + Receipt = Receipt, + Header = alloy_consensus::Header, + > + ChainSpecProvider + + StateProviderFactory, + Pool: TransactionPool>>, + Evm: ConfigureEvm, + >, +{ + #[inline] + fn pending_block( + &self, + ) -> &tokio::sync::Mutex< + Option, ProviderReceipt>>, + > { + self.inner.eth_api.pending_block() + } + + fn next_env_attributes( + &self, + parent: &SealedHeader>, + ) -> Result<::NextBlockEnvCtx, Self::Error> { + Ok(NextBlockEnvAttributes { + timestamp: parent.timestamp().saturating_add(12), + suggested_fee_recipient: parent.beneficiary(), + prev_randao: B256::random(), + gas_limit: parent.gas_limit(), + parent_beacon_block_root: parent.parent_beacon_block_root(), + withdrawals: None, + }) + } +} + +impl LoadReceipt for HlEthApi +where + Self: Send + Sync, + N: FullNodeComponents>, + Self::Provider: + TransactionsProvider + ReceiptProvider, +{ + async fn build_transaction_receipt( + &self, + tx: TransactionSigned, + meta: TransactionMeta, + receipt: Receipt, + ) -> Result, Self::Error> { + let hash = meta.block_hash; + // get all receipts for the block + let all_receipts = self + .cache() + .get_receipts(hash) + .await + .map_err(Self::Error::from_eth_err)? + .ok_or(EthApiError::HeaderNotFound(hash.into()))?; + let blob_params = self + .provider() + .chain_spec() + .blob_params_at_timestamp(meta.timestamp); + + Ok(EthReceiptBuilder::new(&tx, meta, &receipt, &all_receipts, blob_params)?.build()) + } +} diff --git a/src/node/rpc/call.rs b/src/node/rpc/call.rs new file mode 100644 index 000000000..40dd39460 --- /dev/null +++ b/src/node/rpc/call.rs @@ -0,0 +1,164 @@ +use super::{HlEthApi, HlNodeCore}; +use crate::evm::transaction::HlTxEnv; +use alloy_consensus::TxType; +use alloy_primitives::{TxKind, U256}; +use alloy_rpc_types::TransactionRequest; +use alloy_signer::Either; +use reth::rpc::server_types::eth::{revm_utils::CallFees, EthApiError, RpcInvalidTransactionError}; +use reth_evm::{block::BlockExecutorFactory, ConfigureEvm, EvmEnv, EvmFactory, SpecFor}; +use reth_primitives::NodePrimitives; +use reth_provider::{ProviderHeader, ProviderTx}; +use reth_rpc_eth_api::{ + helpers::{estimate::EstimateCall, Call, EthCall, LoadBlock, LoadState, SpawnBlocking}, + FromEthApiError, FromEvmError, FullEthApiTypes, IntoEthApiError, +}; +use revm::{ + context::{Block as _, TxEnv}, + Database, +}; + +impl EthCall for HlEthApi +where + Self: EstimateCall + LoadBlock + FullEthApiTypes, + N: HlNodeCore, +{ +} + +impl EstimateCall for HlEthApi +where + Self: Call, + Self::Error: From, + N: HlNodeCore, +{ +} + +impl Call for HlEthApi +where + Self: LoadState< + Evm: ConfigureEvm< + Primitives: NodePrimitives< + BlockHeader = ProviderHeader, + SignedTx = ProviderTx, + >, + BlockExecutorFactory: BlockExecutorFactory< + EvmFactory: EvmFactory>, + >, + >, + Error: FromEvmError, + > + SpawnBlocking, + Self::Error: From, + N: HlNodeCore, +{ + #[inline] + fn call_gas_limit(&self) -> u64 { + self.inner.eth_api.gas_cap() + } + + #[inline] + fn max_simulate_blocks(&self) -> u64 { + self.inner.eth_api.max_simulate_blocks() + } + + fn create_txn_env( + &self, + evm_env: &EvmEnv>, + request: TransactionRequest, + mut db: impl Database>, + ) -> Result, Self::Error> { + // Ensure that if versioned hashes are set, they're not empty + if request.blob_versioned_hashes.as_ref().is_some_and(|hashes| hashes.is_empty()) { + return Err(RpcInvalidTransactionError::BlobTransactionMissingBlobHashes.into_eth_err()) + } + + let tx_type = if request.authorization_list.is_some() { + TxType::Eip7702 + } else if request.sidecar.is_some() || request.max_fee_per_blob_gas.is_some() { + TxType::Eip4844 + } else if request.max_fee_per_gas.is_some() || request.max_priority_fee_per_gas.is_some() { + TxType::Eip1559 + } else if request.access_list.is_some() { + TxType::Eip2930 + } else { + TxType::Legacy + } as u8; + + let TransactionRequest { + from, + to, + gas_price, + max_fee_per_gas, + max_priority_fee_per_gas, + gas, + value, + input, + nonce, + access_list, + chain_id, + blob_versioned_hashes, + max_fee_per_blob_gas, + authorization_list, + transaction_type: _, + sidecar: _, + } = request; + + let CallFees { max_priority_fee_per_gas, gas_price, max_fee_per_blob_gas } = + CallFees::ensure_fees( + gas_price.map(U256::from), + max_fee_per_gas.map(U256::from), + max_priority_fee_per_gas.map(U256::from), + U256::from(evm_env.block_env.basefee), + blob_versioned_hashes.as_deref(), + max_fee_per_blob_gas.map(U256::from), + evm_env.block_env.blob_gasprice().map(U256::from), + )?; + + let gas_limit = gas.unwrap_or( + // Use maximum allowed gas limit. The reason for this + // is that both Erigon and Geth use pre-configured gas cap even if + // it's possible to derive the gas limit from the block: + // + evm_env.block_env.gas_limit, + ); + + let chain_id = chain_id.unwrap_or(evm_env.cfg_env.chain_id); + + let caller = from.unwrap_or_default(); + + let nonce = if let Some(nonce) = nonce { + nonce + } else { + db.basic(caller).map_err(Into::into)?.map(|acc| acc.nonce).unwrap_or_default() + }; + + let env = TxEnv { + tx_type, + gas_limit, + nonce, + caller, + gas_price: gas_price.saturating_to(), + gas_priority_fee: max_priority_fee_per_gas.map(|v| v.saturating_to()), + kind: to.unwrap_or(TxKind::Create), + value: value.unwrap_or_default(), + data: input + .try_into_unique_input() + .map_err(Self::Error::from_eth_err)? + .unwrap_or_default(), + chain_id: Some(chain_id), + access_list: access_list.unwrap_or_default(), + // EIP-4844 fields + blob_hashes: blob_versioned_hashes.unwrap_or_default(), + max_fee_per_blob_gas: max_fee_per_blob_gas + .map(|v| v.saturating_to()) + .unwrap_or_default(), + // EIP-7702 fields + authorization_list: authorization_list + .unwrap_or_default() + .into_iter() + .map(Either::Left) + .collect(), + }; + + Ok(HlTxEnv::new(env)) + } +} diff --git a/src/node/rpc/engine_api/builder.rs b/src/node/rpc/engine_api/builder.rs new file mode 100644 index 000000000..80e198a7c --- /dev/null +++ b/src/node/rpc/engine_api/builder.rs @@ -0,0 +1,20 @@ +use super::HlEngineApi; +use reth::{ + api::{AddOnsContext, FullNodeComponents}, + builder::rpc::EngineApiBuilder, +}; + +/// Builder for mocked [`HlEngineApi`] implementation. +#[derive(Debug, Default)] +pub struct HlEngineApiBuilder; + +impl EngineApiBuilder for HlEngineApiBuilder +where + N: FullNodeComponents, +{ + type EngineApi = HlEngineApi; + + async fn build_engine_api(self, _ctx: &AddOnsContext<'_, N>) -> eyre::Result { + Ok(HlEngineApi::default()) + } +} diff --git a/src/node/rpc/engine_api/mod.rs b/src/node/rpc/engine_api/mod.rs new file mode 100644 index 000000000..8eec0cee3 --- /dev/null +++ b/src/node/rpc/engine_api/mod.rs @@ -0,0 +1,16 @@ +use jsonrpsee_core::server::RpcModule; +use reth::rpc::api::IntoEngineApiRpcModule; + +pub mod builder; +pub mod payload; +pub mod validator; + +impl IntoEngineApiRpcModule for HlEngineApi { + fn into_rpc_module(self) -> RpcModule<()> { + RpcModule::new(()) + } +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct HlEngineApi; diff --git a/src/node/rpc/engine_api/payload.rs b/src/node/rpc/engine_api/payload.rs new file mode 100644 index 000000000..d60a1ea44 --- /dev/null +++ b/src/node/rpc/engine_api/payload.rs @@ -0,0 +1,27 @@ +use crate::node::{engine::HlBuiltPayload, rpc::engine_api::validator::HlExecutionData}; +use reth::{ + payload::EthPayloadBuilderAttributes, + primitives::{NodePrimitives, SealedBlock}, +}; +use reth_node_ethereum::engine::EthPayloadAttributes; +use reth_payload_primitives::{BuiltPayload, PayloadTypes}; + +/// A default payload type for [`HlPayloadTypes`] +#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)] +#[non_exhaustive] +pub struct HlPayloadTypes; + +impl PayloadTypes for HlPayloadTypes { + type BuiltPayload = HlBuiltPayload; + type PayloadAttributes = EthPayloadAttributes; + type PayloadBuilderAttributes = EthPayloadBuilderAttributes; + type ExecutionData = HlExecutionData; + + fn block_to_payload( + block: SealedBlock< + <::Primitives as NodePrimitives>::Block, + >, + ) -> Self::ExecutionData { + HlExecutionData(block.into_block()) + } +} diff --git a/src/node/rpc/engine_api/validator.rs b/src/node/rpc/engine_api/validator.rs new file mode 100644 index 000000000..51f21ba06 --- /dev/null +++ b/src/node/rpc/engine_api/validator.rs @@ -0,0 +1,174 @@ +use crate::{ + chainspec::HlChainSpec, + hardforks::HlHardforks, + node::{HlBlock, HlPrimitives}, +}; +use alloy_consensus::BlockHeader; +use alloy_eips::eip4895::Withdrawal; +use alloy_primitives::B256; +use alloy_rpc_types_engine::{PayloadAttributes, PayloadError}; +use reth::{ + api::{FullNodeComponents, NodeTypes}, + builder::{rpc::EngineValidatorBuilder, AddOnsContext}, + consensus::ConsensusError, +}; +use reth_engine_primitives::{EngineValidator, ExecutionPayload, PayloadValidator}; +use reth_payload_primitives::{ + EngineApiMessageVersion, EngineObjectValidationError, NewPayloadError, PayloadOrAttributes, + PayloadTypes, +}; +use reth_primitives::{RecoveredBlock, SealedBlock}; +use reth_primitives_traits::Block as _; +use reth_trie_common::HashedPostState; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use super::payload::HlPayloadTypes; + +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct HlEngineValidatorBuilder; + +impl EngineValidatorBuilder for HlEngineValidatorBuilder +where + Types: NodeTypes, + Node: FullNodeComponents, +{ + type Validator = HlEngineValidator; + + async fn build(self, ctx: &AddOnsContext<'_, Node>) -> eyre::Result { + Ok(HlEngineValidator::new(Arc::new( + ctx.config.chain.clone().as_ref().clone(), + ))) + } +} + +/// Validator for Optimism engine API. +#[derive(Debug, Clone)] +pub struct HlEngineValidator { + inner: HlExecutionPayloadValidator, +} + +impl HlEngineValidator { + /// Instantiates a new validator. + pub fn new(chain_spec: Arc) -> Self { + Self { + inner: HlExecutionPayloadValidator { inner: chain_spec }, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HlExecutionData(pub HlBlock); + +impl ExecutionPayload for HlExecutionData { + fn parent_hash(&self) -> B256 { + self.0.header.parent_hash() + } + + fn block_hash(&self) -> B256 { + self.0.header.hash_slow() + } + + fn block_number(&self) -> u64 { + self.0.header.number() + } + + fn withdrawals(&self) -> Option<&Vec> { + None + } + + fn parent_beacon_block_root(&self) -> Option { + None + } + + fn timestamp(&self) -> u64 { + self.0.header.timestamp() + } + + fn gas_used(&self) -> u64 { + self.0.header.gas_used() + } +} + +impl PayloadValidator for HlEngineValidator { + type Block = HlBlock; + type ExecutionData = HlExecutionData; + + fn ensure_well_formed_payload( + &self, + payload: Self::ExecutionData, + ) -> Result, NewPayloadError> { + let sealed_block = self + .inner + .ensure_well_formed_payload(payload) + .map_err(NewPayloadError::other)?; + sealed_block + .try_recover() + .map_err(|e| NewPayloadError::Other(e.into())) + } + + fn validate_block_post_execution_with_hashed_state( + &self, + _state_updates: &HashedPostState, + _block: &RecoveredBlock, + ) -> Result<(), ConsensusError> { + Ok(()) + } +} + +impl EngineValidator for HlEngineValidator +where + Types: PayloadTypes, +{ + fn validate_version_specific_fields( + &self, + _version: EngineApiMessageVersion, + _payload_or_attrs: PayloadOrAttributes<'_, Self::ExecutionData, PayloadAttributes>, + ) -> Result<(), EngineObjectValidationError> { + Ok(()) + } + + fn ensure_well_formed_attributes( + &self, + _version: EngineApiMessageVersion, + _attributes: &PayloadAttributes, + ) -> Result<(), EngineObjectValidationError> { + Ok(()) + } +} + +/// Execution payload validator. +#[derive(Clone, Debug)] +pub struct HlExecutionPayloadValidator { + /// Chain spec to validate against. + #[allow(unused)] + inner: Arc, +} + +impl HlExecutionPayloadValidator +where + ChainSpec: HlHardforks, +{ + pub fn ensure_well_formed_payload( + &self, + payload: HlExecutionData, + ) -> Result, PayloadError> { + let block = payload.0; + + let expected_hash = block.header.hash_slow(); + + // First parse the block + let sealed_block = block.seal_slow(); + + // Ensure the hash included in the payload matches the block hash + if expected_hash != sealed_block.hash() { + return Err(PayloadError::BlockHash { + execution: sealed_block.hash(), + consensus: expected_hash, + })?; + } + + Ok(sealed_block) + } +} diff --git a/src/node/rpc/mod.rs b/src/node/rpc/mod.rs new file mode 100644 index 000000000..695407a9e --- /dev/null +++ b/src/node/rpc/mod.rs @@ -0,0 +1,273 @@ +use alloy_network::Ethereum; +use alloy_primitives::U256; +use reth::{ + builder::{ + rpc::{EthApiBuilder, EthApiCtx}, + FullNodeComponents, + }, + chainspec::EthChainSpec, + primitives::EthereumHardforks, + providers::ChainSpecProvider, + rpc::{ + eth::{DevSigner, FullEthApiServer}, + server_types::eth::{EthApiError, EthStateCache, FeeHistoryCache, GasPriceOracle}, + }, + tasks::{ + pool::{BlockingTaskGuard, BlockingTaskPool}, + TaskSpawner, + }, + transaction_pool::TransactionPool, +}; +use reth_evm::ConfigureEvm; +use reth_network::NetworkInfo; +use reth_optimism_rpc::eth::EthApiNodeBackend; +use reth_primitives::NodePrimitives; +use reth_provider::{ + BlockNumReader, BlockReader, BlockReaderIdExt, ProviderBlock, ProviderHeader, ProviderReceipt, + ProviderTx, StageCheckpointReader, StateProviderFactory, +}; +use reth_rpc_eth_api::{ + helpers::{ + AddDevSigners, EthApiSpec, EthFees, EthSigner, EthState, LoadBlock, LoadFee, LoadState, + SpawnBlocking, Trace, + }, + EthApiTypes, FromEvmError, RpcNodeCore, RpcNodeCoreExt, +}; +use std::{fmt, sync::Arc}; + +use crate::HlPrimitives; + +mod block; +mod call; +pub mod engine_api; +mod transaction; + +/// A helper trait with requirements for [`RpcNodeCore`] to be used in [`HlEthApi`]. +pub trait HlNodeCore: RpcNodeCore {} +impl HlNodeCore for T where T: RpcNodeCore {} + +/// Container type `HlEthApi` +#[allow(missing_debug_implementations)] +pub(crate) struct HlEthApiInner { + /// Gateway to node's core components. + pub(crate) eth_api: EthApiNodeBackend, +} + +#[derive(Clone)] +pub struct HlEthApi { + /// Gateway to node's core components. + pub(crate) inner: Arc>, +} + +impl fmt::Debug for HlEthApi { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HlEthApi").finish_non_exhaustive() + } +} + +impl EthApiTypes for HlEthApi +where + Self: Send + Sync, + N: HlNodeCore, +{ + type Error = EthApiError; + type NetworkTypes = Ethereum; + type TransactionCompat = Self; + + fn tx_resp_builder(&self) -> &Self::TransactionCompat { + self + } +} + +impl RpcNodeCore for HlEthApi +where + N: HlNodeCore, +{ + type Primitives = HlPrimitives; + type Provider = N::Provider; + type Pool = N::Pool; + type Evm = ::Evm; + type Network = ::Network; + type PayloadBuilder = (); + + #[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 payload_builder(&self) -> &Self::PayloadBuilder { + &() + } + + #[inline] + fn provider(&self) -> &Self::Provider { + self.inner.eth_api.provider() + } +} + +impl RpcNodeCoreExt for HlEthApi +where + N: HlNodeCore, +{ + #[inline] + fn cache(&self) -> &EthStateCache, ProviderReceipt> { + self.inner.eth_api.cache() + } +} + +impl EthApiSpec for HlEthApi +where + N: HlNodeCore< + Provider: ChainSpecProvider + + BlockNumReader + + StageCheckpointReader, + Network: NetworkInfo, + >, +{ + type Transaction = ProviderTx; + + #[inline] + fn starting_block(&self) -> U256 { + self.inner.eth_api.starting_block() + } + + #[inline] + fn signers(&self) -> &parking_lot::RwLock>>>> { + self.inner.eth_api.signers() + } +} + +impl SpawnBlocking for HlEthApi +where + Self: Send + Sync + Clone + 'static, + N: HlNodeCore, +{ + #[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() + } +} + +impl LoadFee for HlEthApi +where + Self: LoadBlock, + N: HlNodeCore< + Provider: BlockReaderIdExt + + ChainSpecProvider + + StateProviderFactory, + >, +{ + #[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 LoadState for HlEthApi where + N: HlNodeCore< + Provider: StateProviderFactory + ChainSpecProvider, + Pool: TransactionPool, + > +{ +} + +impl EthState for HlEthApi +where + Self: LoadState + SpawnBlocking, + N: HlNodeCore, +{ + #[inline] + fn max_proof_window(&self) -> u64 { + self.inner.eth_api.eth_proof_window() + } +} + +impl EthFees for HlEthApi +where + Self: LoadFee, + N: HlNodeCore, +{ +} + +impl Trace for HlEthApi +where + Self: RpcNodeCore + + LoadState< + Evm: ConfigureEvm< + Primitives: NodePrimitives< + BlockHeader = ProviderHeader, + SignedTx = ProviderTx, + >, + >, + Error: FromEvmError, + >, + N: HlNodeCore, +{ +} + +impl AddDevSigners for HlEthApi +where + N: HlNodeCore, +{ + fn with_dev_accounts(&self) { + *self.inner.eth_api.signers().write() = DevSigner::random_signers(20) + } +} + +/// Builds [`HlEthApi`] for HL. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct HlEthApiBuilder; + +impl EthApiBuilder for HlEthApiBuilder +where + N: FullNodeComponents, + HlEthApi: FullEthApiServer, +{ + type EthApi = HlEthApi; + + async fn build_eth_api(self, ctx: EthApiCtx<'_, N>) -> eyre::Result { + let eth_api = reth::rpc::eth::EthApiBuilder::new( + ctx.components.provider().clone(), + ctx.components.pool().clone(), + ctx.components.network().clone(), + ctx.components.evm_config().clone(), + ) + .eth_cache(ctx.cache) + .task_spawner(ctx.components.task_executor().clone()) + .gas_cap(ctx.config.rpc_gas_cap.into()) + .max_simulate_blocks(ctx.config.rpc_max_simulate_blocks) + .eth_proof_window(ctx.config.eth_proof_window) + .fee_history_cache_config(ctx.config.fee_history_cache) + .proof_permits(ctx.config.proof_permits) + .build_inner(); + + Ok(HlEthApi { inner: Arc::new(HlEthApiInner { eth_api }) }) + } +} diff --git a/src/node/rpc/transaction.rs b/src/node/rpc/transaction.rs new file mode 100644 index 000000000..7a5608db3 --- /dev/null +++ b/src/node/rpc/transaction.rs @@ -0,0 +1,90 @@ +use super::HlNodeCore; +use crate::{node::rpc::HlEthApi, HlPrimitives}; +use alloy_network::{Ethereum, Network}; +use alloy_primitives::{Bytes, Signature, B256}; +use reth::{ + builder::FullNodeComponents, + primitives::{Receipt, Recovered, TransactionSigned}, + providers::ReceiptProvider, + rpc::{ + eth::helpers::types::EthRpcConverter, + server_types::eth::{utils::recover_raw_transaction, EthApiError}, + types::{TransactionInfo, TransactionRequest}, + }, + transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool}, +}; +use reth_provider::{BlockReader, BlockReaderIdExt, ProviderTx, TransactionsProvider}; +use reth_rpc_eth_api::{ + helpers::{EthSigner, EthTransactions, LoadTransaction, SpawnBlocking}, + FromEthApiError, FullEthApiTypes, RpcNodeCore, RpcNodeCoreExt, TransactionCompat, +}; +impl LoadTransaction for HlEthApi +where + Self: SpawnBlocking + FullEthApiTypes + RpcNodeCoreExt, + N: HlNodeCore, + Self::Pool: TransactionPool, +{ +} + +impl TransactionCompat for HlEthApi +where + N: FullNodeComponents>, +{ + type Primitives = HlPrimitives; + type Transaction = ::TransactionResponse; + + type Error = EthApiError; + + fn fill( + &self, + tx: Recovered, + tx_info: TransactionInfo, + ) -> Result { + let builder = EthRpcConverter::default(); + builder.fill(tx, tx_info) + } + + fn build_simulate_v1_transaction( + &self, + request: TransactionRequest, + ) -> Result { + let Ok(tx) = request.build_typed_tx() else { + return Err(EthApiError::TransactionConversionError) + }; + + // Create an empty signature for the transaction. + let signature = Signature::new(Default::default(), Default::default(), false); + Ok(TransactionSigned::new_unhashed(tx.into(), signature)) + } +} + +impl EthTransactions for HlEthApi +where + Self: LoadTransaction, + N: HlNodeCore>>, +{ + fn signers(&self) -> &parking_lot::RwLock>>>> { + self.inner.eth_api.signers() + } + + /// Decodes and recovers the transaction and submits it to the pool. + /// + /// Returns the hash of the transaction. + async fn send_raw_transaction(&self, tx: Bytes) -> Result { + let recovered = recover_raw_transaction(&tx)?; + + // broadcast raw transaction to subscribers if there is any. + self.inner.eth_api.broadcast_raw_transaction(tx); + + let pool_transaction = ::Transaction::from_pooled(recovered); + + // submit the transaction to the pool with a `Local` origin + let hash = self + .pool() + .add_transaction(TransactionOrigin::Local, pool_transaction) + .await + .map_err(Self::Error::from_eth_err)?; + + Ok(hash) + } +} diff --git a/src/node/storage/mod.rs b/src/node/storage/mod.rs index d67084bb0..1762e2c91 100644 --- a/src/node/storage/mod.rs +++ b/src/node/storage/mod.rs @@ -1,6 +1,6 @@ use crate::{ node::types::ReadPrecompileCalls, - primitives::{HlBlock, HlBlockBody, HlPrimitives}, + {HlBlock, HlBlockBody, HlPrimitives}, }; use alloy_consensus::BlockHeader; use alloy_primitives::Bytes;