diff --git a/Cargo.lock b/Cargo.lock index 4c2304fb48a..10d793331b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5399,9 +5399,11 @@ version = "1.10.0" dependencies = [ "anyhow", "auto_impl", + "delegate", "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/ext.rs b/core/processor/src/ext.rs index 67a896d516d..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, } @@ -1225,7 +1237,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 +2336,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..67254194fa7 100644 --- a/core/processor/src/lib.rs +++ b/core/processor/src/lib.rs @@ -37,7 +37,7 @@ mod processing; pub use context::{ProcessExecutionContext, SystemReservationContext}; pub use ext::{ - AllocExtError, Ext, FallibleExtError, ProcessorContext, ProcessorExternalities, + AllocExtError, Ext, ExtInfo, FallibleExtError, ProcessorContext, ProcessorExternalities, UnrecoverableExtError, }; pub use handler::handle_journal; diff --git a/ethexe/processor/src/tests.rs b/ethexe/processor/src/tests.rs index 63ae59e6efb..6bb7c7fb246 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,130 @@ 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 = 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] + .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 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 0x0) (i32.const {delay}) (i32.const 0x0)) + (if (i32.eqz (i32.load (i32.const 0x0))) + (then nop) + (else unreachable) + ) + ) + ) + "# + ) + }; + + // 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 + .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" + ); + + // 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")] +async fn call_wait_up_to_with_huge_duration() { + init_logger(); + + let get_wat = |duration: u32| { + format!( + r#" + (module + (import "env" "memory" (memory 0)) + (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, + "Duration 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, + "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 2288f8146e5..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"] } @@ -25,6 +26,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..6194fac197e --- /dev/null +++ b/ethexe/runtime/common/src/ext.rs @@ -0,0 +1,223 @@ +// 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::{ + 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, 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}; + +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 is forbidden in ethexe runtime") + } + + fn reservation_reply_commit( + &mut self, + _: ReservationId, + _: ReplyPacket, + ) -> Result { + unreachable!("reservation_reply_commit syscall is forbidden in ethexe runtime") + } + + fn signal_from(&self) -> Result { + unreachable!("signal_from syscall is forbidden in ethexe runtime") + } + + fn signal_code(&self) -> Result { + unreachable!("signal_code syscall is forbidden in ethexe runtime") + } + + fn wait(&mut self) -> Result<(), Self::UnrecoverableError> { + unreachable!("wait syscall is forbidden in ethexe runtime") + } + + fn random(&self) -> Result<(&[u8], u32), Self::UnrecoverableError> { + // TODO: #5238 implement random data generation in ethexe runtime + unreachable!("random syscall is forbidden in ethexe runtime") + } + + fn create_program( + &mut self, + _: InitPacket, + _: u32, + ) -> Result<(MessageId, ActorId), Self::FallibleError> { + // 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 is forbidden in ethexe runtime") + } + fn reserve_gas(&mut self, _: u64, _: u32) -> Result { + unreachable!("reserve_gas syscall is forbidden in ethexe runtime") + } + + fn unreserve_gas(&mut self, _: ReservationId) -> Result { + unreachable!("unreserve_gas syscall is forbidden in ethexe runtime") + } + + fn system_reserve_gas(&mut self, _: u64) -> Result<(), Self::FallibleError> { + unreachable!("system_reserve_gas syscall is 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..7b5a65500c4 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. +pub 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..98aedbbdfba 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, @@ -50,12 +51,13 @@ 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}; 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}")) }