From 3a9edfe7c70e7b546e2787e9d3f5ee99f910d2e6 Mon Sep 17 00:00:00 2001 From: AshinGau Date: Wed, 5 Aug 2026 15:40:50 +0800 Subject: [PATCH] feat(evm): restrict parallel precompile state access --- .config/zepter.yaml | 1 + Cargo.lock | 3 +- Cargo.toml | 3 +- clippy.toml | 3 + crates/ethereum/evm/src/parallel_execute.rs | 71 +++++- crates/evm/evm/Cargo.toml | 5 + crates/evm/evm/src/either.rs | 2 +- crates/evm/evm/src/lib.rs | 7 +- crates/evm/evm/src/noop.rs | 9 +- crates/evm/evm/src/parallel_execute.rs | 27 ++- .../gravity-precompiles/src/bls_pop_verify.rs | 45 +++- .../src/randomness_by_height.rs | 89 +++++-- .../execute/src/custom_precompiles.rs | 228 ++++++++++++++++++ .../pipe-exec-layer-ext-v2/execute/src/lib.rs | 18 +- .../gravity_system_tx_bls_replay_test.rs | 4 +- 15 files changed, 463 insertions(+), 52 deletions(-) create mode 100644 crates/pipe-exec-layer-ext-v2/execute/src/custom_precompiles.rs diff --git a/.config/zepter.yaml b/.config/zepter.yaml index 7392c217a9..5fae21aec5 100644 --- a/.config/zepter.yaml +++ b/.config/zepter.yaml @@ -19,6 +19,7 @@ workflows: "--left-side-outside-workspace=ignore", # Auxiliary flags: + "--ignore-missing-propagate=reth-evm/test-utils:grevm/test-utils", "--ignore-missing-propagate=reth-evm-ethereum/test-utils:grevm/test-utils", "--offline", "--locked", diff --git a/Cargo.lock b/Cargo.lock index 7ea13ea62a..a031abd7d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4480,7 +4480,7 @@ dependencies = [ [[package]] name = "grevm" version = "2.2.2" -source = "git+https://github.com/Galxe/grevm?rev=dcd1460b20d9f3d793c8309c28b0b7401be91523#dcd1460b20d9f3d793c8309c28b0b7401be91523" +source = "git+https://github.com/Galxe/grevm?rev=25b71edcac1b94be7fc5ee0e4f20e5fcce6908ad#25b71edcac1b94be7fc5ee0e4f20e5fcce6908ad" dependencies = [ "ahash", "alloy-evm", @@ -8886,6 +8886,7 @@ dependencies = [ "auto_impl", "derive_more", "futures-util", + "grevm", "metrics", "rayon", "reth-ethereum-primitives", diff --git a/Cargo.toml b/Cargo.toml index 05414cc91d..1438edcf7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -221,6 +221,7 @@ rustdoc.all = "warn" # rust.unnameable-types = "warn" [workspace.lints.clippy] +disallowed_methods = "deny" # These are some of clippy's nursery (i.e., experimental) lints that we like. # By default, nursery lints are allowed. Some of the lints below have made good # suggestions which we fixed. The others didn't have any findings, so we can @@ -451,7 +452,7 @@ reth-provider = { path = "crates/storage/provider" } reth-prune = { path = "crates/prune/prune" } reth-prune-types = { path = "crates/prune/types", default-features = false } reth-revm = { path = "crates/revm", default-features = false } -grevm = { package = "grevm", git = "https://github.com/Galxe/grevm", rev = "dcd1460b20d9f3d793c8309c28b0b7401be91523" } +grevm = { package = "grevm", git = "https://github.com/Galxe/grevm", rev = "25b71edcac1b94be7fc5ee0e4f20e5fcce6908ad" } reth-rpc = { path = "crates/rpc/rpc" } reth-rpc-api = { path = "crates/rpc/rpc-api" } reth-rpc-api-testing-util = { path = "crates/rpc/rpc-testing-util" } diff --git a/clippy.toml b/clippy.toml index 9ddf101480..d5c1c8b734 100644 --- a/clippy.toml +++ b/clippy.toml @@ -15,3 +15,6 @@ doc-valid-idents = [ "MessagePack", ] allow-dbg-in-tests = true +disallowed-methods = [ + { path = "alloy_evm::EvmInternals::db_mut", reason = "grevm custom precompiles must access EVM state through the journal-aware capability facade" }, +] diff --git a/crates/ethereum/evm/src/parallel_execute.rs b/crates/ethereum/evm/src/parallel_execute.rs index b343ae138c..4a777bbcd0 100644 --- a/crates/ethereum/evm/src/parallel_execute.rs +++ b/crates/ethereum/evm/src/parallel_execute.rs @@ -12,7 +12,10 @@ use alloy_evm::{ }; use alloy_primitives::{map::HashMap, Address}; use gravity_primitives::get_gravity_config; -use grevm::{GrevmConfig, ParallelBundleState, ParallelState, Scheduler, TxExecutionOutcome}; +use grevm::{ + DynParallelPrecompile, GrevmConfig, ParallelBundleState, ParallelState, Scheduler, + TxExecutionOutcome, +}; use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks, Hardforks}; use reth_ethereum_primitives::{Block, EthPrimitives, Receipt}; use reth_evm::{ @@ -53,8 +56,8 @@ pub struct GrevmExecutor { state: Option>, /// System caller for executing system calls. system_caller: SystemCaller>, - /// Custom precompiled contracts to inject into the EVM. - custom_precompiles: Option>>, + /// Capability-restricted custom precompiles available to user transactions. + custom_precompiles: Option>>, /// Block-scoped grevm execution policy. grevm_config: GrevmConfig, } @@ -387,7 +390,10 @@ where .map_err(|e| BlockExecutionError::msg(alloc::format!("basic {address}: {e:?}"))) } - fn apply_custom_precompiles(&mut self, custom_precompiles: Arc>) { + fn apply_custom_precompiles( + &mut self, + custom_precompiles: Arc>, + ) { self.custom_precompiles = Some(custom_precompiles); } } @@ -493,6 +499,7 @@ mod tests { eip7685::EMPTY_REQUESTS_HASH, }; use alloy_primitives::{keccak256, Bytes, B256, U256}; + use core::sync::atomic::{AtomicUsize, Ordering}; use reth_chainspec::{ ChainHardforks, ChainSpec, ChainSpecBuilder, ForkCondition, GravityHardfork, MAINNET, }; @@ -503,6 +510,7 @@ mod tests { bytecode::Bytecode, context::TxEnv, database::{CacheDB, EmptyDB}, + precompile::{PrecompileId, PrecompileOutput}, primitives::TxKind, state::{Account, AccountInfo, AccountStatus}, }; @@ -875,6 +883,61 @@ mod tests { assert_eq!(info.nonce, 2, "SYSTEM_CALLER nonce must reflect both system txs"); } + /// Executor-level custom precompiles are scoped to Grevm's user-transaction scheduler. + /// System transactions only see the Alloy precompiles explicitly supplied for that call. + #[test] + fn executor_custom_precompile_does_not_leak_into_system_transactions() { + let chain_spec = alpha_active_chainspec(1); + let chain_id = chain_spec.chain().id(); + let evm_config = EthEvmConfig::new(chain_spec.clone()); + let evm_env = evm_config + .evm_env(&alpha_block_header(1)) + .expect("system transaction EVM environment must build"); + let precompile_address = Address::with_last_byte(0xfe); + + let calls = Arc::new(AtomicUsize::new(0)); + let calls_in_precompile = calls.clone(); + let custom_precompile = DynParallelPrecompile::new( + PrecompileId::custom("system-precompile-isolation-sentinel"), + move |input| { + calls_in_precompile.fetch_add(1, Ordering::SeqCst); + Ok(PrecompileOutput::new(0, Bytes::new(), input.reservoir())) + }, + ); + + let mut executor = GrevmExecutor::new(chain_spec, &evm_config, seeded_db(U256::ZERO, 0)); + executor.apply_custom_precompiles(Arc::new(vec![( + precompile_address, + custom_precompile.clone(), + )])); + + let mut implicit_tx = system_tx_env(0, chain_id); + implicit_tx.kind = TxKind::Call(precompile_address); + executor + .transact_system_txn(evm_env.clone(), Vec::new(), implicit_tx) + .expect("system transaction without explicit precompile must succeed"); + assert_eq!( + calls.load(Ordering::SeqCst), + 0, + "executor-level custom precompile must not be visible to system transactions" + ); + + let mut explicit_tx = system_tx_env(1, chain_id); + explicit_tx.kind = TxKind::Call(precompile_address); + executor + .transact_system_txn( + evm_env, + vec![(precompile_address, custom_precompile.to_alloy())], + explicit_tx, + ) + .expect("system transaction with explicit precompile must succeed"); + assert_eq!( + calls.load(Ordering::SeqCst), + 1, + "explicit system precompile must execute exactly once" + ); + } + // --- U-7: fee归零 + 余额不动 + coinbase 不收 tip (matrix §1.2) --- /// `u7`: under Alpha gate, SYSTEM_CALLER balance is preserved bit-for-bit diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index ab367006f8..5fe9bc74bf 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true homepage.workspace = true repository.workspace = true +[package.metadata.zepter.propagate-feature] +ignore = ["grevm"] + [lints] workspace = true @@ -19,6 +22,7 @@ reth-primitives-traits.workspace = true reth-storage-api.workspace = true reth-storage-errors.workspace = true reth-trie-common.workspace = true +grevm = { workspace = true, optional = true } revm.workspace = true @@ -42,6 +46,7 @@ reth-ethereum-primitives.workspace = true default = ["std"] std = [ "dep:rayon", + "dep:grevm", "reth-primitives-traits/std", "alloy-eips/std", "alloy-primitives/std", diff --git a/crates/evm/evm/src/either.rs b/crates/evm/evm/src/either.rs index cddd1258e7..832b608cdf 100644 --- a/crates/evm/evm/src/either.rs +++ b/crates/evm/evm/src/either.rs @@ -2,7 +2,7 @@ use crate::{execute::Executor, Database, OnStateHook}; -use alloc::sync::Arc; +use alloc::{sync::Arc, vec::Vec}; use alloy_evm::{precompiles::DynPrecompile, EvmEnv}; use alloy_primitives::Address; pub use futures_util::future::Either; diff --git a/crates/evm/evm/src/lib.rs b/crates/evm/evm/src/lib.rs index 8ffbbafb0a..20d7ab254f 100644 --- a/crates/evm/evm/src/lib.rs +++ b/crates/evm/evm/src/lib.rs @@ -18,7 +18,9 @@ extern crate alloc; use crate::execute::{BasicBlockBuilder, Executor}; -use alloc::{boxed::Box, vec::Vec}; +#[cfg(feature = "std")] +use alloc::boxed::Box; +use alloc::vec::Vec; use alloy_eips::eip4895::Withdrawals; use alloy_evm::{ block::{BlockExecutorFactory, BlockExecutorFor}, @@ -41,7 +43,9 @@ use revm::{ pub mod either; /// EVM environment configuration. pub mod execute; +#[cfg(feature = "std")] pub mod parallel_execute; +#[cfg(feature = "std")] use parallel_execute::ParallelExecutor; mod aliases; @@ -494,6 +498,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin { } /// Returns a new [`ParallelExecutor`]. + #[cfg(feature = "std")] fn parallel_executor<'a, DB: ParallelDatabase + 'a>( &self, db: DB, diff --git a/crates/evm/evm/src/noop.rs b/crates/evm/evm/src/noop.rs index 76191d62ab..9f721fc3bb 100644 --- a/crates/evm/evm/src/noop.rs +++ b/crates/evm/evm/src/noop.rs @@ -1,9 +1,9 @@ //! Helpers for testing. -use crate::{ - parallel_execute::ParallelExecutor, BlockExecutionError, ConfigureEvm, EvmEnvFor, - ParallelDatabase, -}; +#[cfg(feature = "std")] +use crate::{parallel_execute::ParallelExecutor, BlockExecutionError, ParallelDatabase}; +use crate::{ConfigureEvm, EvmEnvFor}; +#[cfg(feature = "std")] use alloc::boxed::Box; use reth_primitives_traits::{BlockTy, HeaderTy, SealedBlock, SealedHeader}; @@ -74,6 +74,7 @@ where self.inner().context_for_next_block(parent, attributes) } + #[cfg(feature = "std")] fn parallel_executor<'a, DB: ParallelDatabase + 'a>( &self, db: DB, diff --git a/crates/evm/evm/src/parallel_execute.rs b/crates/evm/evm/src/parallel_execute.rs index d7789bb012..976f277a10 100644 --- a/crates/evm/evm/src/parallel_execute.rs +++ b/crates/evm/evm/src/parallel_execute.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use crate::execute::Executor; use alloy_evm::{precompiles::DynPrecompile, Database, EvmEnv}; use alloy_primitives::Address; +use grevm::DynParallelPrecompile; use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult}; use reth_primitives_traits::{NodePrimitives, RecoveredBlock}; use revm::{ @@ -61,7 +62,8 @@ pub trait ParallelExecutor { /// external bridging. /// /// `precompiles` allows callers to inject custom precompiles (e.g. mint, BLS) for this - /// specific transaction, in addition to any executor-level custom precompiles. + /// specific transaction. Executor-level user precompiles are intentionally not installed on + /// this system-transaction path. fn transact_system_txn( &mut self, evm_env: EvmEnv, @@ -88,11 +90,15 @@ pub trait ParallelExecutor { /// [`apply_state_change`](Self::apply_state_change). fn basic(&mut self, address: Address) -> Result, Self::Error>; - /// Applies custom precompiled contracts to the executor. + /// Applies capability-restricted custom precompiles to user transaction execution. /// - /// These precompiles will be available during transaction execution alongside - /// the standard Ethereum precompiles. - fn apply_custom_precompiles(&mut self, custom_precompiles: Arc>); + /// Grevm-backed executors must only accept [`DynParallelPrecompile`] here. Adapting an + /// unrestricted Alloy `DynPrecompile` would expose raw EVM internals and allow speculative + /// execution to bypass journal-aware state tracking. + fn apply_custom_precompiles( + &mut self, + custom_precompiles: Arc>, + ); } /// Wraps a [`Executor`] to provide a [`ParallelExecutor`] implementation. @@ -150,7 +156,14 @@ impl> ParallelExecutor for WrapExecutor { } #[inline] - fn apply_custom_precompiles(&mut self, custom_precompiles: Arc>) { - self.0.apply_custom_precompiles(custom_precompiles); + fn apply_custom_precompiles( + &mut self, + custom_precompiles: Arc>, + ) { + let custom_precompiles = custom_precompiles + .iter() + .map(|(address, precompile)| (*address, precompile.to_alloy())) + .collect(); + self.0.apply_custom_precompiles(Arc::new(custom_precompiles)); } } diff --git a/crates/gravity-precompiles/src/bls_pop_verify.rs b/crates/gravity-precompiles/src/bls_pop_verify.rs index 2bd6c47bf2..1da8ba2bce 100644 --- a/crates/gravity-precompiles/src/bls_pop_verify.rs +++ b/crates/gravity-precompiles/src/bls_pop_verify.rs @@ -36,7 +36,8 @@ const EXPECTED_INPUT_LEN: usize = BLS_PUBKEY_LEN + BLS_POP_LEN; /// /// At ~0.05 gas/ns (mid-range for pairing ops): 2,160,000 ns × 0.05 ≈ 108,000 gas. /// Rounded to 110,000 to align with EIP-2537 BLS pairing (2 pairs) pricing. -const POP_VERIFY_GAS: u64 = 110_000; +/// Flat gas charged by the BLS proof-of-possession precompile. +pub const POP_VERIFY_GAS: u64 = 110_000; /// Domain separation tag for BLS `PoP` verification /// Matches the IETF standard for BLS12-381 `PoP` @@ -61,13 +62,17 @@ pub fn create_bls_pop_verify_precompile() -> DynPrecompile { let precompile_id = PrecompileId::custom("bls_pop_verify"); (precompile_id, move |input: PrecompileInput<'_>| -> PrecompileResult { - bls_pop_verify_handler(input) + bls_pop_verify_handler(input.data, input.gas) }) .into() } -/// BLS `PoP` verification handler -fn bls_pop_verify_handler(input: PrecompileInput<'_>) -> PrecompileResult { +/// Executes BLS `PoP` verification with the precompile's complete gas semantics. +/// +/// This is the shared entry point for EVM adapters. It checks the forwarded gas before running the +/// comparatively expensive verification and delegates input processing to +/// [`bls_pop_verify_handler_raw`]. +pub fn bls_pop_verify_handler(data: &[u8], gas: u64) -> PrecompileResult { // Charge-gas check before running the verification logic. This precompile // charges a flat `POP_VERIFY_GAS`, and the EVM dispatcher executes // `assert!(record_cost(gas_used))` after receiving `Ok(gas_used)`: if the @@ -76,10 +81,10 @@ fn bls_pop_verify_handler(input: PrecompileInput<'_>) -> PrecompileResult { // network-wide halt triggerable by any user transaction). Return OutOfGas // early so the dispatcher takes the normal PrecompileOOG branch instead of // panicking. - if POP_VERIFY_GAS > input.gas { + if POP_VERIFY_GAS > gas { return Ok(PrecompileOutput::halt(PrecompileHalt::OutOfGas, 0)); } - bls_pop_verify_handler_raw(input.data) + bls_pop_verify_handler_raw(data) } /// Core BLS `PoP` verification logic operating on raw input bytes. @@ -241,4 +246,32 @@ mod tests { let result = bls_pop_verify_handler_raw(&input_data).unwrap(); assert_eq!(result.bytes[31], 0, "Invalid PoP should return false"); } + + #[test] + fn test_precompile_gas_aware_handler_boundary() { + let (pubkey, pop) = generate_test_keypair(); + let mut input_data = Vec::with_capacity(EXPECTED_INPUT_LEN); + input_data.extend_from_slice(&pubkey); + input_data.extend_from_slice(&pop); + + let out_of_gas = bls_pop_verify_handler(&input_data, POP_VERIFY_GAS - 1).unwrap(); + assert!(matches!( + out_of_gas.status, + revm::precompile::PrecompileStatus::Halt(PrecompileHalt::OutOfGas) + )); + assert_eq!(out_of_gas.gas_used, 0); + + let exact_gas = bls_pop_verify_handler(&input_data, POP_VERIFY_GAS).unwrap(); + assert!(exact_gas.is_success()); + assert_eq!(exact_gas.gas_used, POP_VERIFY_GAS); + assert_eq!(exact_gas.bytes[31], 1); + + // At the exact gas threshold, input validation runs and preserves its existing halt. + let invalid_input = bls_pop_verify_handler(&[], POP_VERIFY_GAS).unwrap(); + assert!(matches!( + invalid_input.status, + revm::precompile::PrecompileStatus::Halt(PrecompileHalt::Other(_)) + )); + assert_eq!(invalid_input.gas_used, 0); + } } diff --git a/crates/gravity-precompiles/src/randomness_by_height.rs b/crates/gravity-precompiles/src/randomness_by_height.rs index 4702a9049a..88dda79bf8 100644 --- a/crates/gravity-precompiles/src/randomness_by_height.rs +++ b/crates/gravity-precompiles/src/randomness_by_height.rs @@ -129,21 +129,37 @@ where let precompile_id = PrecompileId::custom("randomness_by_height"); (precompile_id, move |input: PrecompileInput<'_>| -> PrecompileResult { - // Gas guard (mirrors `bls_precompile`): explicitly enforce the precompile's gas and - // return OutOfGas when the call forwarded less than `gas_used`, instead of letting the - // precompile dispatcher underflow-panic on `record_cost`. Without this, any user - // transaction that calls this precompile with insufficient forwarded gas panics every - // node — a network-wide consensus halt. The lookup itself is cheap, so charging after - // computing the (data-dependent) gas tier is fine. - let output = randomness_by_height_handler_raw(input.data, provider.as_ref())?; - if output.gas_used > input.gas { - return Ok(PrecompileOutput::halt(PrecompileHalt::OutOfGas, 0)); - } - Ok(output) + randomness_by_height_handler(input.data, input.gas, provider.as_ref()) }) .into() } +/// Executes a randomness lookup with the precompile's complete gas semantics. +/// +/// The provider selects the gas tier together with the lookup result, so the lookup must run before +/// the forwarded-gas check. This preserves the behavior of the Alloy precompile while allowing +/// other EVM adapters to share the same data-dependent gas handling. +pub fn randomness_by_height_handler( + data: &[u8], + gas: u64, + provider: &Provider, +) -> PrecompileResult +where + Provider: RandomnessByHeightProvider + ?Sized, +{ + // Gas guard (mirrors `bls_precompile`): explicitly enforce the precompile's gas and + // return OutOfGas when the call forwarded less than `gas_used`, instead of letting the + // precompile dispatcher underflow-panic on `record_cost`. Without this, any user + // transaction that calls this precompile with insufficient forwarded gas panics every + // node — a network-wide consensus halt. The lookup itself is cheap, so charging after + // computing the (data-dependent) gas tier is fine. + let output = randomness_by_height_handler_raw(data, provider)?; + if output.gas_used > gas { + return Ok(PrecompileOutput::halt(PrecompileHalt::OutOfGas, 0)); + } + Ok(output) +} + /// Core lookup logic separated from `PrecompileInput` for unit tests and RPC reuse. pub fn randomness_by_height_handler_raw( data: &[u8], @@ -206,11 +222,12 @@ pub fn encode_randomness_by_height_result(found: bool, randomness: B256) -> Byte #[cfg(test)] mod tests { use super::{ - randomness_by_height_handler_raw, RandomnessByHeightGasPolicy, RandomnessByHeightLookup, - RandomnessByHeightProvider, RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, - RANDOMNESS_BY_HEIGHT_RECENT_GAS, + randomness_by_height_handler, randomness_by_height_handler_raw, + RandomnessByHeightGasPolicy, RandomnessByHeightLookup, RandomnessByHeightProvider, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, RANDOMNESS_BY_HEIGHT_RECENT_GAS, }; use alloy_primitives::{B256, U256}; + use revm::precompile::{PrecompileHalt, PrecompileStatus}; use std::{collections::BTreeMap, convert::Infallible}; #[derive(Default)] @@ -310,6 +327,8 @@ mod tests { .expect("lookup succeeds"); assert!(result.is_success()); + // Oversized heights never reach the provider and retain the historical-lookup price. + assert_eq!(result.gas_used, RANDOMNESS_BY_HEIGHT_LOOKUP_GAS); assert_eq!(result.bytes[31], 0); assert_eq!(&result.bytes[32..64], B256::ZERO.as_slice()); } @@ -321,4 +340,46 @@ mod tests { randomness_by_height_handler_raw(&[1, 2, 3], &provider).expect("halt is not fatal"); assert!(output.is_halt()); } + + #[test] + fn gas_aware_handler_enforces_provider_selected_tier() { + let height = encode_height(U256::from(10)); + let lookup_provider = MockRandomnessProvider::default(); + let lookup_oog = randomness_by_height_handler( + &height, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS - 1, + &lookup_provider, + ) + .unwrap(); + assert!(matches!(lookup_oog.status, PrecompileStatus::Halt(PrecompileHalt::OutOfGas))); + assert_eq!(lookup_oog.gas_used, 0); + + let lookup_success = randomness_by_height_handler( + &height, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, + &lookup_provider, + ) + .unwrap(); + assert!(lookup_success.is_success()); + assert_eq!(lookup_success.gas_used, RANDOMNESS_BY_HEIGHT_LOOKUP_GAS); + + let recent_provider = RecentMockRandomnessProvider::default(); + let recent_success = randomness_by_height_handler( + &height, + RANDOMNESS_BY_HEIGHT_RECENT_GAS, + &recent_provider, + ) + .unwrap(); + assert!(recent_success.is_success()); + assert_eq!(recent_success.gas_used, RANDOMNESS_BY_HEIGHT_RECENT_GAS); + + let recent_oog = randomness_by_height_handler( + &height, + RANDOMNESS_BY_HEIGHT_RECENT_GAS - 1, + &recent_provider, + ) + .unwrap(); + assert!(matches!(recent_oog.status, PrecompileStatus::Halt(PrecompileHalt::OutOfGas))); + assert_eq!(recent_oog.gas_used, 0); + } } diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/custom_precompiles.rs b/crates/pipe-exec-layer-ext-v2/execute/src/custom_precompiles.rs new file mode 100644 index 0000000000..2937269712 --- /dev/null +++ b/crates/pipe-exec-layer-ext-v2/execute/src/custom_precompiles.rs @@ -0,0 +1,228 @@ +//! Capability-restricted adapters for precompiles used by grevm user transactions. + +use gravity_precompiles::{ + bls_pop_verify::bls_pop_verify_handler, + randomness_by_height::{randomness_by_height_handler, RandomnessByHeightProvider}, +}; +use grevm::DynParallelPrecompile; +use revm::precompile::PrecompileId; +use std::sync::Arc; + +/// Creates the capability-restricted BLS proof-of-possession precompile. +pub(crate) fn create_bls_pop_verify_precompile() -> DynParallelPrecompile { + DynParallelPrecompile::new(PrecompileId::custom("bls_pop_verify"), |input| { + bls_pop_verify_handler(input.data(), input.gas()).map_err(Into::into) + }) +} + +/// Creates a capability-restricted historical-randomness precompile. +/// +/// `provider` must be immutable for the lifetime of the executing block and return deterministic +/// results: grevm may invoke this closure concurrently and may retry the same transaction after a +/// speculative conflict. +/// [`ExecutionRandomnessProvider`](super::randomness_precompile::ExecutionRandomnessProvider) +/// satisfies this by capturing the current/parent block values and using a read-only canonical +/// storage fallback. +pub(crate) fn create_randomness_by_height_precompile( + provider: Arc, +) -> DynParallelPrecompile +where + Provider: RandomnessByHeightProvider + Send + Sync + 'static, +{ + DynParallelPrecompile::new(PrecompileId::custom("randomness_by_height"), move |input| { + randomness_by_height_handler(input.data(), input.gas(), provider.as_ref()) + .map_err(Into::into) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Address, B256, U256}; + use gravity_precompiles::{ + bls_pop_verify::POP_VERIFY_GAS, + randomness_by_height::{ + RandomnessByHeightLookup, RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, + RANDOMNESS_BY_HEIGHT_RECENT_GAS, + }, + }; + use hex_literal::hex; + use reth_evm::{ + eth::EthEvmContext, + precompiles::{Precompile, PrecompileInput}, + EvmInternals, + }; + use revm::{ + database::EmptyDB, + precompile::{PrecompileHalt, PrecompileOutput, PrecompileResult, PrecompileStatus}, + }; + + const PRECOMPILE_ADDRESS: Address = Address::with_last_byte(0xfe); + + // Deterministically generated from secret-key material `[42; 32]` using the protocol's PoP + // domain separation tag. Keeping this as a fixture avoids adding `blst` to this crate solely + // for adapter tests. + const VALID_BLS_POP_INPUT: [u8; 144] = hex!( + "8ae7e5822ba97ab07877ea318e747499da648b27302414f9d0b9bb7e3646d248" + "be90c9fdaddfdb93485a6e9334f01093" + "b16db5b947dda6c513b24b8724b659996826bfb69a8914f1b295e39572f40923" + "e08150a0bdd12d0ee920e9a1e33acf81192230e9f074e350555315a427264246" + "ab03b99601738c4179746e73913388b68285a854e85be32b1539ec925dd3d7fe" + ); + + #[derive(Clone, Copy)] + struct FixedRandomnessProvider { + lookup: Result, + } + + impl RandomnessByHeightProvider for FixedRandomnessProvider { + type Error = &'static str; + + fn randomness_by_height( + &self, + _height: u64, + ) -> Result { + self.lookup + } + } + + fn call_adapter(precompile: &DynParallelPrecompile, data: &[u8], gas: u64) -> PrecompileResult { + let mut ctx = EthEvmContext::new(EmptyDB::default(), Default::default()); + precompile.to_alloy().call(PrecompileInput { + data, + gas, + reservoir: 0, + caller: Address::ZERO, + value: U256::ZERO, + target_address: PRECOMPILE_ADDRESS, + bytecode_address: PRECOMPILE_ADDRESS, + is_static: false, + internals: EvmInternals::from_context(&mut ctx), + }) + } + + fn assert_adapter_matches_handler( + precompile: &DynParallelPrecompile, + data: &[u8], + gas: u64, + expected: PrecompileResult, + ) -> PrecompileOutput { + let actual = call_adapter(precompile, data, gas); + assert_eq!(actual, expected); + actual.expect("tested handler result must be non-fatal") + } + + #[test] + fn bls_adapter_preserves_low_gas_and_exact_gas_success() { + let precompile = create_bls_pop_verify_precompile(); + + let out_of_gas = assert_adapter_matches_handler( + &precompile, + &VALID_BLS_POP_INPUT, + POP_VERIFY_GAS - 1, + bls_pop_verify_handler(&VALID_BLS_POP_INPUT, POP_VERIFY_GAS - 1), + ); + assert_eq!(out_of_gas.status, PrecompileStatus::Halt(PrecompileHalt::OutOfGas)); + assert_eq!(out_of_gas.gas_used, 0); + + let success = assert_adapter_matches_handler( + &precompile, + &VALID_BLS_POP_INPUT, + POP_VERIFY_GAS, + bls_pop_verify_handler(&VALID_BLS_POP_INPUT, POP_VERIFY_GAS), + ); + assert!(success.is_success()); + assert_eq!(success.gas_used, POP_VERIFY_GAS); + assert_eq!(success.bytes.len(), 32); + assert_eq!(success.bytes[31], 1); + } + + #[test] + fn randomness_adapter_preserves_historical_gas_boundaries_and_output() { + let randomness = B256::repeat_byte(0xa5); + let provider = Arc::new(FixedRandomnessProvider { + lookup: Ok(RandomnessByHeightLookup::storage(Some(randomness))), + }); + let precompile = create_randomness_by_height_precompile(provider.clone()); + let input = U256::from(7).to_be_bytes::<32>(); + + let success = assert_adapter_matches_handler( + &precompile, + &input, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, + randomness_by_height_handler( + &input, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, + provider.as_ref(), + ), + ); + assert!(success.is_success()); + assert_eq!(success.gas_used, RANDOMNESS_BY_HEIGHT_LOOKUP_GAS); + assert_eq!(success.bytes[31], 1); + assert_eq!(&success.bytes[32..], randomness.as_slice()); + + let out_of_gas = assert_adapter_matches_handler( + &precompile, + &input, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS - 1, + randomness_by_height_handler( + &input, + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS - 1, + provider.as_ref(), + ), + ); + assert_eq!(out_of_gas.status, PrecompileStatus::Halt(PrecompileHalt::OutOfGas)); + assert_eq!(out_of_gas.gas_used, 0); + } + + #[test] + fn randomness_adapter_preserves_recent_gas_boundaries_and_invalid_input() { + let randomness = B256::repeat_byte(0x5a); + let provider = Arc::new(FixedRandomnessProvider { + lookup: Ok(RandomnessByHeightLookup::recent(Some(randomness))), + }); + let precompile = create_randomness_by_height_precompile(provider.clone()); + let input = U256::from(8).to_be_bytes::<32>(); + + let success = assert_adapter_matches_handler( + &precompile, + &input, + RANDOMNESS_BY_HEIGHT_RECENT_GAS, + randomness_by_height_handler( + &input, + RANDOMNESS_BY_HEIGHT_RECENT_GAS, + provider.as_ref(), + ), + ); + assert!(success.is_success()); + assert_eq!(success.gas_used, RANDOMNESS_BY_HEIGHT_RECENT_GAS); + assert_eq!(success.bytes[31], 1); + assert_eq!(&success.bytes[32..], randomness.as_slice()); + + let out_of_gas = assert_adapter_matches_handler( + &precompile, + &input, + RANDOMNESS_BY_HEIGHT_RECENT_GAS - 1, + randomness_by_height_handler( + &input, + RANDOMNESS_BY_HEIGHT_RECENT_GAS - 1, + provider.as_ref(), + ), + ); + assert_eq!(out_of_gas.status, PrecompileStatus::Halt(PrecompileHalt::OutOfGas)); + assert_eq!(out_of_gas.gas_used, 0); + + let invalid = assert_adapter_matches_handler( + &precompile, + &[0xff], + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, + randomness_by_height_handler( + &[0xff], + RANDOMNESS_BY_HEIGHT_LOOKUP_GAS, + provider.as_ref(), + ), + ); + assert!(matches!(invalid.status, PrecompileStatus::Halt(PrecompileHalt::Other(_)))); + assert_eq!(invalid.gas_used, 0); + } +} diff --git a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs index 1b56e1c108..847227579c 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/src/lib.rs @@ -1,6 +1,7 @@ //! Pipeline execution layer extension #[macro_use] mod channel; +mod custom_precompiles; mod eip_2935; mod metrics; pub mod mint_precompile; @@ -29,7 +30,7 @@ use gravity_precompiles::{ randomness_by_height::randomness_by_height_gas_policy_at_block, }; use gravity_primitives::PIPE_BLOCK_GAS_LIMIT; -use grevm::DelegatedSafetyConfig; +use grevm::{DelegatedSafetyConfig, DynParallelPrecompile}; use reth_chain_state::{ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates}; use reth_chainspec::{ChainSpec, EthChainSpec, EthereumHardforks, GravityHardfork}; use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned}; @@ -81,10 +82,7 @@ use crate::{ SystemTxnResult, DKG_ADDR, NATIVE_MINT_PRECOMPILE_ADDR, NATIVE_ORACLE_ADDR, RANDOMNESS_BY_HEIGHT_PRECOMPILE_ADDR, SYSTEM_CALLER, }, - randomness_precompile::{ - create_randomness_by_height_precompile, ExecutionRandomnessProvider, - GravityStorageRandomnessProvider, - }, + randomness_precompile::{ExecutionRandomnessProvider, GravityStorageRandomnessProvider}, }; fn extract_gravity_events_from_system_receipts( @@ -438,8 +436,8 @@ struct Core { storage: Arc, evm_config: EthEvmConfig, chain_spec: Arc, - bls_pop_verify_precompile: DynPrecompile, - pre_alpha_precompiles: Arc>, + bls_pop_verify_precompile: DynParallelPrecompile, + pre_alpha_precompiles: Arc>, event_tx: std::sync::mpsc::Sender>, execute_block_barrier: Channel<(u64, u64) /* epoch, block number */, ExecuteBlockContext>, merklize_barrier: Channel, @@ -575,7 +573,7 @@ impl Core { &self, ordered_block: &OrderedBlock, parent_header: &Header, - ) -> Arc> { + ) -> Arc> { let block_number = ordered_block.number; let block_timestamp = ordered_block.timestamp_us / 1_000_000; if !self @@ -602,7 +600,7 @@ impl Core { (BLS_PRECOMPILE_ADDR, self.bls_pop_verify_precompile.clone()), ( RANDOMNESS_BY_HEIGHT_PRECOMPILE_ADDR, - create_randomness_by_height_precompile(execution_provider), + custom_precompiles::create_randomness_by_height_precompile(execution_provider), ), ]) } @@ -1703,7 +1701,7 @@ where "new pipe exec layer api" ); - let bls_pop_verify_precompile = create_bls_pop_verify_precompile(); + let bls_pop_verify_precompile = custom_precompiles::create_bls_pop_verify_precompile(); let pre_alpha_precompiles = Arc::new(vec![(BLS_PRECOMPILE_ADDR, bls_pop_verify_precompile.clone())]); let start_time = Instant::now(); diff --git a/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_system_tx_bls_replay_test.rs b/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_system_tx_bls_replay_test.rs index 6900323f51..70b2edfe9b 100644 --- a/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_system_tx_bls_replay_test.rs +++ b/crates/pipe-exec-layer-ext-v2/execute/tests/gravity_system_tx_bls_replay_test.rs @@ -65,6 +65,7 @@ use gravity_api_types::{ config_storage::{BlockNumber, ConfigStorage, OnChainConfig}, events::contract_event::GravityEvent, }; +use gravity_precompiles::bls_pop_verify::POP_VERIFY_GAS; use gravity_storage::{block_view_storage::BlockViewStorage, GravityStorage}; use reth_chainspec::ChainSpec; use reth_cli_commands::{launcher::FnLauncher, NodeCommand}; @@ -106,9 +107,6 @@ const BLS_PRECOMPILE_ADDR: Address = address!("000000000000000000000000000000016 /// byte-equal canonical assertion to be meaningful — see file-level docs). const BLS_INPUT_LEN: usize = 144; -/// Flat gas charge of the BLS precompile (`POP_VERIFY_GAS`). -const POP_VERIFY_GAS: u64 = 110_000; - /// gas_limit for the BLS user tx: intrinsic (~21k base + ~580 for 144 zero /// calldata bytes) + `POP_VERIFY_GAS` headroom. 200_000 is well above and not /// dropped by `filter_invalid_txs`.