From 0e33a2e7963a5c22c56d17b803fb3ad74bb6c4ec Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 19 Mar 2026 15:13:01 +0700 Subject: [PATCH 1/6] initial --- Cargo.lock | 1 + core/processor/src/ext.rs | 4 +- core/processor/src/lib.rs | 4 +- ethexe/runtime/common/Cargo.toml | 1 + ethexe/runtime/common/src/ext.rs | 199 +++++++++++++++++++++++++++ ethexe/runtime/common/src/journal.rs | 21 ++- ethexe/runtime/common/src/lib.rs | 11 +- 7 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 ethexe/runtime/common/src/ext.rs diff --git a/Cargo.lock b/Cargo.lock index 4c2304fb48a..e785cacd0d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5399,6 +5399,7 @@ version = "1.10.0" dependencies = [ "anyhow", "auto_impl", + "delegate", "derive_more 2.1.1", "ethexe-common", "gear-core", diff --git a/core/processor/src/ext.rs b/core/processor/src/ext.rs index 67a896d516d..2fd53089c9e 100644 --- a/core/processor/src/ext.rs +++ b/core/processor/src/ext.rs @@ -1225,7 +1225,7 @@ impl Externalities for Ext { if let Some(reimbursement) = reimburse { let current_gas_amount = self.gas_amount(); - // Basically amount of the reseravtion and the cost for the hold duration. + // Basically amount of the reservation and the cost for the hold duration. let reimbursement_amount = self.cost_for_reservation(amount, reimbursement.duration()); self.context .gas_counter @@ -2324,7 +2324,7 @@ mod tests { .build(), ); - // Check all the reseravations are in "existing" state. + // Check all the reservations are in "existing" state. assert!( ext.context .gas_reserver diff --git a/core/processor/src/lib.rs b/core/processor/src/lib.rs index d90fc12191a..ab713ab2d44 100644 --- a/core/processor/src/lib.rs +++ b/core/processor/src/lib.rs @@ -37,9 +37,11 @@ mod processing; pub use context::{ProcessExecutionContext, SystemReservationContext}; pub use ext::{ - AllocExtError, Ext, FallibleExtError, ProcessorContext, ProcessorExternalities, + AllocExtError, Ext, ExtInfo, FallibleExtError, ProcessorContext, ProcessorExternalities, UnrecoverableExtError, }; +pub use gear_core::{env::Externalities, gas::CountersOwner}; +pub use gear_core_backend::BackendExternalities; pub use handler::handle_journal; pub use precharge::*; pub use processing::{ diff --git a/ethexe/runtime/common/Cargo.toml b/ethexe/runtime/common/Cargo.toml index 2288f8146e5..236a8053c75 100644 --- a/ethexe/runtime/common/Cargo.toml +++ b/ethexe/runtime/common/Cargo.toml @@ -25,6 +25,7 @@ derive_more.workspace = true auto_impl.workspace = true serde = { workspace = true, features = ["derive"], optional = true } gear-workspace-hack.workspace = true +delegate.workspace = true [features] default = ["std"] diff --git a/ethexe/runtime/common/src/ext.rs b/ethexe/runtime/common/src/ext.rs new file mode 100644 index 00000000000..e247123bce3 --- /dev/null +++ b/ethexe/runtime/common/src/ext.rs @@ -0,0 +1,199 @@ +use crate::RuntimeInterface; +use alloc::collections::btree_set::BTreeSet; +use core_processor::{ + BackendExternalities, CountersOwner, Ext as CoreExt, ExtInfo, Externalities, FallibleExtError, + ProcessorContext, ProcessorExternalities, configs::SyscallName, +}; +use gear_core::{ + buffer::PayloadSlice, + costs::{CostToken, LazyPagesCosts}, + env_vars::EnvVars, + gas::{ChargeError, CounterType, GasAmount, GasLeft}, + memory::{Memory, MemoryError, MemoryInterval}, + message::{HandlePacket, InitPacket, MessageContext, ReplyPacket}, + pages::WasmPage, + program::MemoryInfix, +}; +use gear_core_errors::ExtError; +use gear_lazy_pages_common::{GlobalsAccessConfig, ProcessAccessError}; +use gprimitives::{ActorId, MessageId, ReservationId}; + +pub struct Ext { + core: CoreExt, +} + +impl ProcessorExternalities for Ext { + fn new(context: ProcessorContext) -> Self { + Self { + core: CoreExt::::new(context), + } + } + + fn lazy_pages_init_for_program( + ctx: &mut Context, + mem: &mut impl Memory, + prog_id: ActorId, + memory_infix: MemoryInfix, + stack_end: Option, + globals_config: GlobalsAccessConfig, + lazy_pages_costs: LazyPagesCosts, + ) { + CoreExt::::lazy_pages_init_for_program( + ctx, + mem, + prog_id, + memory_infix, + stack_end, + globals_config, + lazy_pages_costs, + ) + } + + fn lazy_pages_post_execution_actions( + ctx: &mut Context, + mem: &mut impl Memory, + ) { + CoreExt::::lazy_pages_post_execution_actions(ctx, mem) + } + + fn lazy_pages_status() -> gear_lazy_pages_common::Status { + CoreExt::::lazy_pages_status() + } + + delegate::delegate! { + to self.core { + fn into_ext_info( + self, + ctx: &mut Context, + memory: &impl Memory, + ) -> Result; + + } + } +} + +impl Externalities for Ext { + type UnrecoverableError = as Externalities>::UnrecoverableError; + type FallibleError = as Externalities>::FallibleError; + type AllocError = as Externalities>::AllocError; + + delegate::delegate! { + to self.core { + fn alloc(&mut self, ctx: &mut Context, mem: &mut impl Memory, pages_num: u32) -> Result; + fn free(&mut self, page: WasmPage) -> Result<(), Self::AllocError>; + fn free_range(&mut self, start: WasmPage, end: WasmPage) -> Result<(), Self::AllocError>; + fn env_vars(&self, version: u32) -> Result; + fn block_height(&self) -> Result; + fn block_timestamp(&self) -> Result; + fn send_init(&mut self) -> Result; + fn send_push(&mut self, handle: u32, buffer: &[u8]) -> Result<(), Self::FallibleError>; + fn send_commit(&mut self, handle: u32, msg: HandlePacket, delay: u32) -> Result; + fn send_push_input(&mut self, handle: u32, offset: u32, len: u32) -> Result<(), Self::FallibleError>; + fn reply_push(&mut self, buffer: &[u8]) -> Result<(), Self::FallibleError>; + fn reply_commit(&mut self, msg: ReplyPacket) -> Result; + fn reply_to(&self) -> Result; + fn reply_push_input(&mut self, offset: u32, len: u32) -> Result<(), Self::FallibleError>; + fn source(&self) -> Result; + fn reply_code(&self) -> Result; + fn message_id(&self) -> Result; + fn program_id(&self) -> Result; + fn debug(&self, data: &str) -> Result<(), Self::UnrecoverableError>; + fn payload_slice(&mut self, at: u32, len: u32) -> Result; + fn size(&self) -> Result; + fn gas_available(&self) -> Result; + fn value(&self) -> Result; + fn value_available(&self) -> Result; + fn wait_for(&mut self, duration: u32) -> Result<(), Self::UnrecoverableError>; + fn wait_up_to(&mut self, duration: u32) -> Result; + fn forbidden_funcs(&self) -> &BTreeSet; + fn msg_ctx(&self) -> &MessageContext; + } + } + + fn wake(&mut self, waker_id: MessageId, delay: u32) -> Result<(), Self::FallibleError> { + if delay != 0 { + Err(FallibleExtError::Core(ExtError::Unsupported)) + } else { + self.core.wake(waker_id, delay) + } + } + + fn reservation_send_commit( + &mut self, + _: ReservationId, + _: u32, + _: HandlePacket, + _: u32, + ) -> Result { + unreachable!("reservation_send_commit syscall must be forbidden in ethexe runtime") + } + + fn reservation_reply_commit( + &mut self, + _: ReservationId, + _: ReplyPacket, + ) -> Result { + unreachable!("reservation_reply_commit syscall must be forbidden in ethexe runtime") + } + + fn signal_from(&self) -> Result { + unreachable!("signal_from syscall must be forbidden in ethexe runtime") + } + + fn signal_code(&self) -> Result { + unreachable!("signal_code syscall must be forbidden in ethexe runtime") + } + + fn wait(&mut self) -> Result<(), Self::UnrecoverableError> { + unreachable!("wait syscall must be forbidden in ethexe runtime") + } + + fn random(&self) -> Result<(&[u8], u32), Self::UnrecoverableError> { + unreachable!("random syscall must be forbidden in ethexe runtime +_+_+") + } + + fn create_program( + &mut self, + _: InitPacket, + _: u32, + ) -> Result<(MessageId, ActorId), Self::FallibleError> { + unreachable!("create_program syscall must be forbidden in ethexe runtime +_+_+") + } + + fn reply_deposit(&mut self, _: MessageId, _: u64) -> Result<(), Self::FallibleError> { + unreachable!("reply_deposit syscall must be forbidden in ethexe runtime") + } + fn reserve_gas(&mut self, _: u64, _: u32) -> Result { + unreachable!("reserve_gas syscall must be forbidden in ethexe runtime") + } + + fn unreserve_gas(&mut self, _: ReservationId) -> Result { + unreachable!("unreserve_gas syscall must be forbidden in ethexe runtime") + } + + fn system_reserve_gas(&mut self, _: u64) -> Result<(), Self::FallibleError> { + unreachable!("system_reserve_gas syscall must be forbidden in ethexe runtime") + } +} + +impl CountersOwner for Ext { + delegate::delegate! { + to self.core { + fn charge_gas_for_token(&mut self, token: CostToken) -> Result<(), ChargeError>; + fn charge_gas_if_enough(&mut self, amount: u64) -> Result<(), ChargeError>; + fn gas_left(&self) -> GasLeft; + fn current_counter_type(&self) -> CounterType; + fn decrease_current_counter_to(&mut self, amount: u64); + fn define_current_counter(&mut self) -> u64; + } + } +} + +impl BackendExternalities for Ext { + delegate::delegate! { + to self.core { + fn gas_amount(&self) -> GasAmount; + fn pre_process_memory_accesses(&mut self, reads: &[MemoryInterval], writes: &[MemoryInterval], gas_counter: &mut u64) -> Result<(), ProcessAccessError>; + } + } +} diff --git a/ethexe/runtime/common/src/journal.rs b/ethexe/runtime/common/src/journal.rs index 69aa104beb8..f4c135eab66 100644 --- a/ethexe/runtime/common/src/journal.rs +++ b/ethexe/runtime/common/src/journal.rs @@ -24,6 +24,10 @@ use gear_core_errors::SignalCode; use gprimitives::{ActorId, CodeId, H256, MessageId, ReservationId}; use gsys::GasMultiplier; +/// Maximum duration for gr_wait_up_to in blocks, +/// when not enough gas was provided for the requested duration. +const WAIT_UP_TO_SAFE_DURATION: u32 = 64; + // Handles unprocessed journal notes during chunk processing. pub struct NativeJournalHandler<'a, S: Storage + ?Sized> { pub program_id: ActorId, @@ -284,12 +288,21 @@ impl JournalHandler for NativeJournalHandler<'_, S> { &mut self, dispatch: StoredDispatch, duration: Option, - _waited_type: MessageWaitedType, + waited_type: MessageWaitedType, ) { - let Some(duration) = duration else { - todo!("Wait dispatch without specified duration"); + let Some(mut duration) = duration else { + unreachable!("Wait dispatch without specified duration is forbidden in ethexe runtime"); }; + match waited_type { + MessageWaitedType::Wait => unreachable!("gr_wait is forbidden in ethexe runtime"), + MessageWaitedType::WaitUpTo => { + // If not gas was not enough for duration, we use safe duration as max + duration = duration.min(WAIT_UP_TO_SAFE_DURATION); + } + MessageWaitedType::WaitFor | MessageWaitedType::WaitUpToFull => {} + } + let in_blocks = NonZero::::try_from(duration).expect("must be checked on backend side"); @@ -335,7 +348,7 @@ impl JournalHandler for NativeJournalHandler<'_, S> { delay: u32, ) { if delay != 0 { - todo!("Delayed wake message"); + unreachable!("delayed wake is forbidden in ethexe runtime"); } log::trace!("Dispatch {message_id} tries to wake {awakening_id}"); diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 79fa1e596c5..48d8dbc4dc9 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -25,7 +25,7 @@ extern crate alloc; use crate::journal::{Limiter, LimitsStatus}; use alloc::vec::Vec; use core_processor::{ - ContextCharged, Ext, ProcessExecutionContext, + ContextCharged, ProcessExecutionContext, common::{ExecutableActorData, JournalNote}, configs::{BlockConfig, SyscallName}, }; @@ -34,6 +34,7 @@ use ethexe_common::{ gear::{CHUNK_PROCESSING_GAS_LIMIT, MessageType}, injected::Promise, }; +use ext::Ext; use gear_core::{ code::{CodeMetadata, InstrumentedCode, InstrumentedCodeAndMetadata, MAX_WASM_PAGES_AMOUNT}, gas::GasAllowanceCounter, @@ -56,6 +57,7 @@ pub use transitions::{FinalizedBlockTransitions, InBlockTransitions, NonFinalTra pub mod state; +mod ext; mod journal; mod schedule; mod transitions; @@ -150,7 +152,7 @@ impl TransitionController<'_, S> { pub fn process_queue(mut ctx: ProcessQueueContext, ri: &RI) -> (ProgramJournals, u64) where - RI: RuntimeInterface, + RI: RuntimeInterface + 'static, RI::LazyPages: Send, { let mut program_state = ri.program_state(ctx.state_root).unwrap(); @@ -197,6 +199,7 @@ where SyscallName::SendWGas, SyscallName::SystemReserveGas, SyscallName::UnreserveGas, + SyscallName::Wait, // TBD about deprecation SyscallName::SignalCode, SyscallName::SignalFrom, @@ -339,7 +342,7 @@ fn process_dispatch( ri: &RI, ) -> Vec where - RI: RuntimeInterface, + RI: RuntimeInterface + 'static, RI::LazyPages: Send, { let Dispatch { @@ -456,7 +459,7 @@ where let random_data = ri.random_data(); - core_processor::process::>(block_config, execution_context, random_data) + core_processor::process::>(block_config, execution_context, random_data) .unwrap_or_else(|err| unreachable!("{err}")) } From c4fe44116a7cf26bd8c0c494cb5161706cf1fdcd Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 19 Mar 2026 16:15:57 +0700 Subject: [PATCH 2/6] append tests --- core/processor/src/ext.rs | 12 ++ ethexe/processor/src/tests.rs | 240 ++++++++++++++++++++++++++- ethexe/runtime/common/src/journal.rs | 2 +- ethexe/runtime/common/src/lib.rs | 2 +- 4 files changed, 252 insertions(+), 4 deletions(-) diff --git a/core/processor/src/ext.rs b/core/processor/src/ext.rs index 2fd53089c9e..a786158eb66 100644 --- a/core/processor/src/ext.rs +++ b/core/processor/src/ext.rs @@ -151,18 +151,30 @@ impl ProcessorContext { } } +/// Message execution result info #[derive(Debug)] pub struct ExtInfo { + /// Gas amount left after execution. pub gas_amount: GasAmount, + /// Gas reserver with updated reservations after execution. pub gas_reserver: GasReserver, + /// System reservation context with current and previous reservations. pub system_reservation_context: SystemReservationContext, + /// Whether allocations were changed during execution and final state of allocations if they were changed. pub allocations: Option>, + /// Data of accessed pages during execution. pub pages_data: BTreeMap, + /// List of generated dispatches with their delay and optional reservation id. pub generated_dispatches: Vec<(Dispatch, u32, Option)>, + /// List of wakened messages with their id and delay until awakening. pub awakening: Vec<(MessageId, u32)>, + /// List of reply deposits with message id and amount. pub reply_deposits: Vec<(MessageId, u64)>, + /// Programs to create data. pub program_candidates_data: BTreeMap>, + /// Executed message context store after execution. pub context_store: ContextStore, + /// Whether reply was sent during execution. pub reply_sent: bool, } diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 63ae59e6efb..2b8ff75c650 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -20,7 +20,7 @@ use crate::*; use anyhow::{Result, anyhow}; use ethexe_common::{ DEFAULT_BLOCK_GAS_LIMIT, OUTGOING_MESSAGES_SOFT_LIMIT, PROGRAM_MODIFICATIONS_SOFT_LIMIT, - SimpleBlockData, + ScheduledTask, SimpleBlockData, db::*, events::{ BlockRequestEvent, MirrorRequestEvent, RouterRequestEvent, @@ -29,7 +29,7 @@ use ethexe_common::{ }, mock::*, }; -use ethexe_runtime_common::{RUNTIME_ID, state::MessageQueue}; +use ethexe_runtime_common::{RUNTIME_ID, WAIT_UP_TO_SAFE_DURATION, state::MessageQueue}; use gear_core::{ ids::prelude::CodeIdExt, message::{ErrorReplyReason, ReplyCode, SuccessReplyReason}, @@ -133,6 +133,47 @@ mod utils { salt: H256::random().0.to_vec().try_into().unwrap(), } } + + pub async fn simple_init_test(code: impl AsRef<[u8]>) -> InBlockTransitions { + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_ref()]); + let block1 = chain.blocks[1].to_simple(); + + let mut handler = setup_handler(processor.db.clone(), block1); + let actor_id = ActorId::from(0x10000); + handler + .handle_router_event(RouterRequestEvent::ProgramCreated(ProgramCreatedEvent { + actor_id, + code_id, + })) + .expect("failed to create new program"); + handler + .handle_mirror_event( + actor_id, + MirrorRequestEvent::ExecutableBalanceTopUpRequested( + ExecutableBalanceTopUpRequestedEvent { + value: 350_000_000_000, + }, + ), + ) + .expect("failed to top up balance"); + handler + .handle_mirror_event( + actor_id, + MirrorRequestEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { + id: MessageId::from(1), + source: ActorId::from(10), + payload: vec![], + value: 0, + call_reply: false, + }), + ) + .expect("failed to queue message"); + + processor + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) + .await + .unwrap() + } } #[tokio::test(flavor = "multi_thread")] @@ -1382,3 +1423,198 @@ async fn insufficient_executable_balance_still_charged() { let exec_balance_after = handler.program_state(actor_id).executable_balance; assert!(exec_balance_after < INSUFFICIENT_EXECUTABLE_BALANCE); } + +#[tokio::test(flavor = "multi_thread")] +async fn call_gr_wait_is_forbidden() { + init_logger(); + + let wat = format!( + r#" + (module + (import "env" "memory" (memory 0)) + (import "env" "gr_wait" (func $wait)) + (export "init" (func $init)) + (func $init call $wait) + ) + "# + ); + + let (_, code) = wat_to_wasm(wat.as_str()); + + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()]); + let block1 = chain.blocks[1].to_simple(); + + let mut handler = setup_handler(processor.db.clone(), block1); + let actor_id = ActorId::from(0x10000); + handler + .handle_router_event(RouterRequestEvent::ProgramCreated(ProgramCreatedEvent { + actor_id, + code_id, + })) + .expect("failed to create new program"); + handler + .handle_mirror_event( + actor_id, + MirrorRequestEvent::ExecutableBalanceTopUpRequested( + ExecutableBalanceTopUpRequestedEvent { + value: 350_000_000_000, + }, + ), + ) + .expect("failed to top up balance"); + handler + .handle_mirror_event( + actor_id, + MirrorRequestEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { + id: MessageId::from(1), + source: ActorId::from(10), + payload: vec![], + value: 0, + call_reply: false, + }), + ) + .expect("failed to queue message"); + + let transitions = processor + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) + .await + .unwrap(); + + let reply_code = transitions.current_messages()[0] + .1 + .reply_details + .expect("must be reply") + .to_reply_code(); + assert_eq!( + reply_code, + ReplyCode::Error(ErrorReplyReason::Execution( + SimpleExecutionError::BackendError + )), + "Forbidden syscall should return backend error" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn call_wake_with_delay_is_unsupported() { + init_logger(); + + let wat = format!( + r#" + (module + (import "env" "memory" (memory 1)) + (import "env" "gr_wake" (func $wake (param i32 i32 i32))) + (export "init" (func $init)) + (func $init + (call $wake (i32.const 0) (i32.const 20) (i32.const 0)) + (if (i32.eqz (i32.load (i32.const 0x0))) + (then nop) + (else unreachable) + ) + ) + ) + "# + ); + + let (_, code) = wat_to_wasm(wat.as_str()); + + let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()]); + let block1 = chain.blocks[1].to_simple(); + + let mut handler = setup_handler(processor.db.clone(), block1); + let actor_id = ActorId::from(0x10000); + handler + .handle_router_event(RouterRequestEvent::ProgramCreated(ProgramCreatedEvent { + actor_id, + code_id, + })) + .expect("failed to create new program"); + handler + .handle_mirror_event( + actor_id, + MirrorRequestEvent::ExecutableBalanceTopUpRequested( + ExecutableBalanceTopUpRequestedEvent { + value: 350_000_000_000, + }, + ), + ) + .expect("failed to top up balance"); + handler + .handle_mirror_event( + actor_id, + MirrorRequestEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { + id: MessageId::from(1), + source: ActorId::from(10), + payload: vec![], + value: 0, + call_reply: false, + }), + ) + .expect("failed to queue message"); + + let transitions = processor + .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) + .await + .unwrap(); + + let reply_code = transitions.current_messages()[0] + .1 + .reply_details + .expect("must be reply") + .to_reply_code(); + assert_eq!( + reply_code, + ReplyCode::Error(ErrorReplyReason::Execution( + SimpleExecutionError::UnreachableInstruction + )), + "Calling gr_wake with non-zero delay should lead to unreachable instruction" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn call_wait_up_to_with_huge_delay() { + init_logger(); + + let get_wat = |duration: u32| { + format!( + r#" + (module + (import "env" "memory" (memory 1)) + (import "env" "gr_wait_up_to" (func $wait_up_to (param i32))) + (export "init" (func $init)) + (func $init + (call $wait_up_to (i32.const {duration})) + ) + ) + "# + ) + }; + + // Huge duration + let wat = get_wat(0xFFFFFFFF); + let transitions = simple_init_test(wat_to_wasm(&wat).1).await; + let block_height = transitions.block_height(); + let FinalizedBlockTransitions { schedule, .. } = transitions.finalize(); + let (block, tasks) = schedule.into_iter().next().unwrap(); + assert_eq!( + block, + block_height + WAIT_UP_TO_SAFE_DURATION, + "Delay should be capped to WAIT_UP_TO_SAFE_DURATION" + ); + let task = tasks.into_iter().next().unwrap(); + assert!(matches!(task, ScheduledTask::WakeMessage(_, _))); + + // Normal duration + let duration = WAIT_UP_TO_SAFE_DURATION + 20; + let wat = get_wat(duration); + let transitions = simple_init_test(wat_to_wasm(&wat).1).await; + let block_height = transitions.block_height(); + let FinalizedBlockTransitions { schedule, .. } = transitions.finalize(); + let (block, tasks) = schedule.into_iter().next().unwrap(); + assert_eq!( + block, + block_height + duration, + "Delay should not be capped if it's less than WAIT_UP_TO_SAFE_DURATION" + ); + let task = tasks.into_iter().next().unwrap(); + assert!(matches!(task, ScheduledTask::WakeMessage(_, _))); +} diff --git a/ethexe/runtime/common/src/journal.rs b/ethexe/runtime/common/src/journal.rs index f4c135eab66..7b5a65500c4 100644 --- a/ethexe/runtime/common/src/journal.rs +++ b/ethexe/runtime/common/src/journal.rs @@ -26,7 +26,7 @@ use gsys::GasMultiplier; /// Maximum duration for gr_wait_up_to in blocks, /// when not enough gas was provided for the requested duration. -const WAIT_UP_TO_SAFE_DURATION: u32 = 64; +pub const WAIT_UP_TO_SAFE_DURATION: u32 = 64; // Handles unprocessed journal notes during chunk processing. pub struct NativeJournalHandler<'a, S: Storage + ?Sized> { diff --git a/ethexe/runtime/common/src/lib.rs b/ethexe/runtime/common/src/lib.rs index 48d8dbc4dc9..98aedbbdfba 100644 --- a/ethexe/runtime/common/src/lib.rs +++ b/ethexe/runtime/common/src/lib.rs @@ -51,7 +51,7 @@ use parity_scale_codec::{Decode, Encode}; use state::{Dispatch, ProgramState, Storage}; pub use core_processor::configs::BlockInfo; -pub use journal::NativeJournalHandler as JournalHandler; +pub use journal::{NativeJournalHandler as JournalHandler, WAIT_UP_TO_SAFE_DURATION}; pub use schedule::{Handler as ScheduleHandler, Restorer as ScheduleRestorer}; pub use transitions::{FinalizedBlockTransitions, InBlockTransitions, NonFinalTransition}; From bf66c36288410730ae18605c5d5abb2227afed69 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 19 Mar 2026 16:22:12 +0700 Subject: [PATCH 3/6] refactor tests --- ethexe/processor/src/tests.rs | 124 ++++++++-------------------------- 1 file changed, 29 insertions(+), 95 deletions(-) diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 2b8ff75c650..4738d786a6a 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -1439,47 +1439,7 @@ async fn call_gr_wait_is_forbidden() { "# ); - let (_, code) = wat_to_wasm(wat.as_str()); - - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()]); - let block1 = chain.blocks[1].to_simple(); - - let mut handler = setup_handler(processor.db.clone(), block1); - let actor_id = ActorId::from(0x10000); - handler - .handle_router_event(RouterRequestEvent::ProgramCreated(ProgramCreatedEvent { - actor_id, - code_id, - })) - .expect("failed to create new program"); - handler - .handle_mirror_event( - actor_id, - MirrorRequestEvent::ExecutableBalanceTopUpRequested( - ExecutableBalanceTopUpRequestedEvent { - value: 350_000_000_000, - }, - ), - ) - .expect("failed to top up balance"); - handler - .handle_mirror_event( - actor_id, - MirrorRequestEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { - id: MessageId::from(1), - source: ActorId::from(10), - payload: vec![], - value: 0, - call_reply: false, - }), - ) - .expect("failed to queue message"); - - let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) - .await - .unwrap(); - + let transitions = simple_init_test(wat_to_wasm(&wat).1).await; let reply_code = transitions.current_messages()[0] .1 .reply_details @@ -1498,64 +1458,28 @@ async fn call_gr_wait_is_forbidden() { async fn call_wake_with_delay_is_unsupported() { init_logger(); - let wat = format!( - r#" - (module - (import "env" "memory" (memory 1)) - (import "env" "gr_wake" (func $wake (param i32 i32 i32))) - (export "init" (func $init)) - (func $init - (call $wake (i32.const 0) (i32.const 20) (i32.const 0)) - (if (i32.eqz (i32.load (i32.const 0x0))) - (then nop) - (else unreachable) + let get_wat = |delay: u32| { + format!( + r#" + (module + (import "env" "memory" (memory 1)) + (import "env" "gr_wake" (func $wake (param i32 i32 i32))) + (export "init" (func $init)) + (func $init + (call $wake (i32.const 0) (i32.const {delay}) (i32.const 0)) + (if (i32.eqz (i32.load (i32.const 0x0))) + (then nop) + (else unreachable) + ) ) ) + "# ) - "# - ); - - let (_, code) = wat_to_wasm(wat.as_str()); - - let (mut processor, chain, [code_id]) = setup_test_env_and_load_codes([code.as_slice()]); - let block1 = chain.blocks[1].to_simple(); - - let mut handler = setup_handler(processor.db.clone(), block1); - let actor_id = ActorId::from(0x10000); - handler - .handle_router_event(RouterRequestEvent::ProgramCreated(ProgramCreatedEvent { - actor_id, - code_id, - })) - .expect("failed to create new program"); - handler - .handle_mirror_event( - actor_id, - MirrorRequestEvent::ExecutableBalanceTopUpRequested( - ExecutableBalanceTopUpRequestedEvent { - value: 350_000_000_000, - }, - ), - ) - .expect("failed to top up balance"); - handler - .handle_mirror_event( - actor_id, - MirrorRequestEvent::MessageQueueingRequested(MessageQueueingRequestedEvent { - id: MessageId::from(1), - source: ActorId::from(10), - payload: vec![], - value: 0, - call_reply: false, - }), - ) - .expect("failed to queue message"); - - let transitions = processor - .process_queues(handler.transitions, block1, DEFAULT_BLOCK_GAS_LIMIT, None) - .await - .unwrap(); + }; + // with delay != 0 + let wat = get_wat(10); + let transitions = simple_init_test(wat_to_wasm(&wat).1).await; let reply_code = transitions.current_messages()[0] .1 .reply_details @@ -1568,6 +1492,16 @@ async fn call_wake_with_delay_is_unsupported() { )), "Calling gr_wake with non-zero delay should lead to unreachable instruction" ); + + // with delay == 0 + let wat = get_wat(0); + let transitions = simple_init_test(wat_to_wasm(&wat).1).await; + let reply_code = transitions.current_messages()[0] + .1 + .reply_details + .expect("must be reply") + .to_reply_code(); + assert_eq!(reply_code, ReplyCode::Success(SuccessReplyReason::Auto)); } #[tokio::test(flavor = "multi_thread")] From 19e1a134ac3e9ddb838b14656a543066ee548b80 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 19 Mar 2026 20:37:58 +0700 Subject: [PATCH 4/6] prepare for rreview --- ethexe/runtime/common/src/ext.rs | 48 +++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/ethexe/runtime/common/src/ext.rs b/ethexe/runtime/common/src/ext.rs index e247123bce3..058b1c78b4e 100644 --- a/ethexe/runtime/common/src/ext.rs +++ b/ethexe/runtime/common/src/ext.rs @@ -1,3 +1,23 @@ +// This file is part of Gear. +// +// Copyright (C) 2026 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Externalities implementation for ethexe runtime. + use crate::RuntimeInterface; use alloc::collections::btree_set::BTreeSet; use core_processor::{ @@ -14,7 +34,7 @@ use gear_core::{ pages::WasmPage, program::MemoryInfix, }; -use gear_core_errors::ExtError; +use gear_core_errors::{ExtError, ReplyCode}; use gear_lazy_pages_common::{GlobalsAccessConfig, ProcessAccessError}; use gprimitives::{ActorId, MessageId, ReservationId}; @@ -94,7 +114,7 @@ impl Externalities for Ext { fn reply_to(&self) -> Result; fn reply_push_input(&mut self, offset: u32, len: u32) -> Result<(), Self::FallibleError>; fn source(&self) -> Result; - fn reply_code(&self) -> Result; + fn reply_code(&self) -> Result; fn message_id(&self) -> Result; fn program_id(&self) -> Result; fn debug(&self, data: &str) -> Result<(), Self::UnrecoverableError>; @@ -125,7 +145,7 @@ impl Externalities for Ext { _: HandlePacket, _: u32, ) -> Result { - unreachable!("reservation_send_commit syscall must be forbidden in ethexe runtime") + unreachable!("reservation_send_commit syscall is forbidden in ethexe runtime") } fn reservation_reply_commit( @@ -133,23 +153,24 @@ impl Externalities for Ext { _: ReservationId, _: ReplyPacket, ) -> Result { - unreachable!("reservation_reply_commit syscall must be forbidden in ethexe runtime") + unreachable!("reservation_reply_commit syscall is forbidden in ethexe runtime") } fn signal_from(&self) -> Result { - unreachable!("signal_from syscall must be forbidden in ethexe runtime") + unreachable!("signal_from syscall is forbidden in ethexe runtime") } fn signal_code(&self) -> Result { - unreachable!("signal_code syscall must be forbidden in ethexe runtime") + unreachable!("signal_code syscall is forbidden in ethexe runtime") } fn wait(&mut self) -> Result<(), Self::UnrecoverableError> { - unreachable!("wait syscall must be forbidden in ethexe runtime") + unreachable!("wait syscall is forbidden in ethexe runtime") } fn random(&self) -> Result<(&[u8], u32), Self::UnrecoverableError> { - unreachable!("random syscall must be forbidden in ethexe runtime +_+_+") + // TODO: #5238 implement random data generation in ethexe runtime + unreachable!("random syscall is forbidden in ethexe runtime") } fn create_program( @@ -157,22 +178,23 @@ impl Externalities for Ext { _: InitPacket, _: u32, ) -> Result<(MessageId, ActorId), Self::FallibleError> { - unreachable!("create_program syscall must be forbidden in ethexe runtime +_+_+") + // TODO: #5239 implement program creation in ethexe runtime + unreachable!("create_program syscall is forbidden in ethexe runtime") } fn reply_deposit(&mut self, _: MessageId, _: u64) -> Result<(), Self::FallibleError> { - unreachable!("reply_deposit syscall must be forbidden in ethexe runtime") + unreachable!("reply_deposit syscall is forbidden in ethexe runtime") } fn reserve_gas(&mut self, _: u64, _: u32) -> Result { - unreachable!("reserve_gas syscall must be forbidden in ethexe runtime") + unreachable!("reserve_gas syscall is forbidden in ethexe runtime") } fn unreserve_gas(&mut self, _: ReservationId) -> Result { - unreachable!("unreserve_gas syscall must be forbidden in ethexe runtime") + unreachable!("unreserve_gas syscall is forbidden in ethexe runtime") } fn system_reserve_gas(&mut self, _: u64) -> Result<(), Self::FallibleError> { - unreachable!("system_reserve_gas syscall must be forbidden in ethexe runtime") + unreachable!("system_reserve_gas syscall is forbidden in ethexe runtime") } } From f4eaa6f08bb1f65543769eb77c9c5310080c3b36 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 19 Mar 2026 21:01:15 +0700 Subject: [PATCH 5/6] fixes --- Cargo.lock | 1 + core/processor/src/lib.rs | 2 -- ethexe/processor/src/tests.rs | 16 +++++++--------- ethexe/runtime/common/Cargo.toml | 1 + ethexe/runtime/common/src/ext.rs | 8 +++++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e785cacd0d9..10d793331b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5403,6 +5403,7 @@ dependencies = [ "derive_more 2.1.1", "ethexe-common", "gear-core", + "gear-core-backend", "gear-core-errors", "gear-core-processor", "gear-lazy-pages-common", diff --git a/core/processor/src/lib.rs b/core/processor/src/lib.rs index ab713ab2d44..67254194fa7 100644 --- a/core/processor/src/lib.rs +++ b/core/processor/src/lib.rs @@ -40,8 +40,6 @@ pub use ext::{ AllocExtError, Ext, ExtInfo, FallibleExtError, ProcessorContext, ProcessorExternalities, UnrecoverableExtError, }; -pub use gear_core::{env::Externalities, gas::CountersOwner}; -pub use gear_core_backend::BackendExternalities; pub use handler::handle_journal; pub use precharge::*; pub use processing::{ diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 4738d786a6a..59d82c664de 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -1428,16 +1428,14 @@ async fn insufficient_executable_balance_still_charged() { async fn call_gr_wait_is_forbidden() { init_logger(); - let wat = format!( - r#" + let wat = r#" (module (import "env" "memory" (memory 0)) (import "env" "gr_wait" (func $wait)) (export "init" (func $init)) (func $init call $wait) ) - "# - ); + "#; let transitions = simple_init_test(wat_to_wasm(&wat).1).await; let reply_code = transitions.current_messages()[0] @@ -1466,7 +1464,7 @@ async fn call_wake_with_delay_is_unsupported() { (import "env" "gr_wake" (func $wake (param i32 i32 i32))) (export "init" (func $init)) (func $init - (call $wake (i32.const 0) (i32.const {delay}) (i32.const 0)) + (call $wake (i32.const 0x0) (i32.const {delay}) (i32.const 0x0)) (if (i32.eqz (i32.load (i32.const 0x0))) (then nop) (else unreachable) @@ -1505,14 +1503,14 @@ async fn call_wake_with_delay_is_unsupported() { } #[tokio::test(flavor = "multi_thread")] -async fn call_wait_up_to_with_huge_delay() { +async fn call_wait_up_to_with_huge_duration() { init_logger(); let get_wat = |duration: u32| { format!( r#" (module - (import "env" "memory" (memory 1)) + (import "env" "memory" (memory 0)) (import "env" "gr_wait_up_to" (func $wait_up_to (param i32))) (export "init" (func $init)) (func $init @@ -1532,7 +1530,7 @@ async fn call_wait_up_to_with_huge_delay() { assert_eq!( block, block_height + WAIT_UP_TO_SAFE_DURATION, - "Delay should be capped to WAIT_UP_TO_SAFE_DURATION" + "Duration should be capped to WAIT_UP_TO_SAFE_DURATION" ); let task = tasks.into_iter().next().unwrap(); assert!(matches!(task, ScheduledTask::WakeMessage(_, _))); @@ -1547,7 +1545,7 @@ async fn call_wait_up_to_with_huge_delay() { assert_eq!( block, block_height + duration, - "Delay should not be capped if it's less than WAIT_UP_TO_SAFE_DURATION" + "Duration should not be capped if msg has enough gas to cover it" ); let task = tasks.into_iter().next().unwrap(); assert!(matches!(task, ScheduledTask::WakeMessage(_, _))); diff --git a/ethexe/runtime/common/Cargo.toml b/ethexe/runtime/common/Cargo.toml index 236a8053c75..3ed8140ecf3 100644 --- a/ethexe/runtime/common/Cargo.toml +++ b/ethexe/runtime/common/Cargo.toml @@ -17,6 +17,7 @@ gear-core.workspace = true gprimitives.workspace = true gsys.workspace = true gear-core-errors.workspace = true +gear-core-backend.workspace = true anyhow.workspace = true parity-scale-codec = { workspace = true, features = ["derive"] } diff --git a/ethexe/runtime/common/src/ext.rs b/ethexe/runtime/common/src/ext.rs index 058b1c78b4e..6194fac197e 100644 --- a/ethexe/runtime/common/src/ext.rs +++ b/ethexe/runtime/common/src/ext.rs @@ -21,19 +21,21 @@ use crate::RuntimeInterface; use alloc::collections::btree_set::BTreeSet; use core_processor::{ - BackendExternalities, CountersOwner, Ext as CoreExt, ExtInfo, Externalities, FallibleExtError, - ProcessorContext, ProcessorExternalities, configs::SyscallName, + Ext as CoreExt, ExtInfo, FallibleExtError, ProcessorContext, ProcessorExternalities, + configs::SyscallName, }; use gear_core::{ buffer::PayloadSlice, costs::{CostToken, LazyPagesCosts}, + env::Externalities, env_vars::EnvVars, - gas::{ChargeError, CounterType, GasAmount, GasLeft}, + gas::{ChargeError, CounterType, CountersOwner, GasAmount, GasLeft}, memory::{Memory, MemoryError, MemoryInterval}, message::{HandlePacket, InitPacket, MessageContext, ReplyPacket}, pages::WasmPage, program::MemoryInfix, }; +use gear_core_backend::BackendExternalities; use gear_core_errors::{ExtError, ReplyCode}; use gear_lazy_pages_common::{GlobalsAccessConfig, ProcessAccessError}; use gprimitives::{ActorId, MessageId, ReservationId}; From 388a35f8ecb01aa684e42aba80c3d953c6f1ba48 Mon Sep 17 00:00:00 2001 From: Gregory Sobol Date: Thu, 19 Mar 2026 21:12:53 +0700 Subject: [PATCH 6/6] fix clippy --- ethexe/processor/src/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 59d82c664de..6bb7c7fb246 100644 --- a/ethexe/processor/src/tests.rs +++ b/ethexe/processor/src/tests.rs @@ -1437,7 +1437,7 @@ async fn call_gr_wait_is_forbidden() { ) "#; - let transitions = simple_init_test(wat_to_wasm(&wat).1).await; + let transitions = simple_init_test(wat_to_wasm(wat).1).await; let reply_code = transitions.current_messages()[0] .1 .reply_details