diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 000000000..d80506fe4 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,4 @@ +# The model decides when to escalate +approval_policy = "on-request" +model_reasoning_effort = "high" +model_reasoning_summary = "detailed" diff --git a/benchmarks/bench_data.json b/benchmarks/bench_data.json index d22478fbe..acefaa1a7 100644 --- a/benchmarks/bench_data.json +++ b/benchmarks/bench_data.json @@ -13,20 +13,20 @@ "317810": 43147409792 }, "counter": { - "async_call": 851480134, - "sync_call": 678859589 + "async_call": 804844249, + "sync_call": 638234270 }, "cross_program": { - "median": 2436719419 + "median": 2242192684 }, "redirect": { - "median": 3474613683 + "median": 3199953137 }, "message_stack": { - "0": 799115685, - "1": 3769584063, - "5": 15693086906, - "10": 29592077575, - "20": 63989367411 + "0": 701611001, + "1": 3367742913, + "5": 14063393437, + "10": 26273493888, + "20": 51793560821 } } \ No newline at end of file diff --git a/examples/demo/app/src/chaos/mod.rs b/examples/demo/app/src/chaos/mod.rs index d95303c50..bea2c29b0 100644 --- a/examples/demo/app/src/chaos/mod.rs +++ b/examples/demo/app/src/chaos/mod.rs @@ -1,4 +1,4 @@ -use sails_rs::gstd::debug; +use sails_rs::gstd::{Lock, debug}; use sails_rs::{gstd, prelude::*}; static mut REPLY_HOOK_COUNTER: u32 = 0; @@ -10,9 +10,7 @@ impl ChaosService { #[export] pub async fn panic_after_wait(&self) { let source = Syscall::message_source(); - let _ = gstd::msg::send_for_reply::<()>(source, (), 0, 0) - .unwrap() - .await; + let _ = gstd::send_for_reply::<()>(source, (), 0).unwrap().await; debug!("Message received, now panicking!"); panic!("Simulated panic after wait"); } @@ -22,16 +20,19 @@ impl ChaosService { let source = Syscall::message_source(); debug!("before handle_reply"); - let fut = gstd::msg::send_for_reply::<()>(source, (), 0, 10_000_000_000).unwrap(); - let fut = fut - .handle_reply(|| { + let fut = gstd::send_bytes_for_reply( + source, + &[], + 0, + Lock::up_to(1), + None, + Some(10_000_000_000), + Some(Box::new(|| { unsafe { REPLY_HOOK_COUNTER += 1 }; debug!("handle_reply triggered"); - }) - .unwrap() - .up_to(Some(1)) - .unwrap(); - + })), + ) + .unwrap(); let _ = fut.await; debug!("after handle_reply"); } diff --git a/examples/event-routes/app/src/lib.rs b/examples/event-routes/app/src/lib.rs index e0fb59186..21d1e170e 100644 --- a/examples/event-routes/app/src/lib.rs +++ b/examples/event-routes/app/src/lib.rs @@ -29,9 +29,7 @@ impl Service { pub async fn foo(&mut self) { let source = Syscall::message_source(); self.emit_event(Events::Start).unwrap(); - let _res = gstd::msg::send_for_reply(source, self.0, 0, 0) - .unwrap() - .await; + let _res = gstd::send_for_reply(source, self.0, 0).unwrap().await; self.emit_event(Events::End).unwrap(); } } diff --git a/examples/ping-pong-stack/tests/gtest.rs b/examples/ping-pong-stack/tests/gtest.rs index 4213368ad..224b15412 100644 --- a/examples/ping-pong-stack/tests/gtest.rs +++ b/examples/ping-pong-stack/tests/gtest.rs @@ -35,7 +35,7 @@ fn create_env() -> (GtestEnv, CodeId, GasUnit) { let system = System::new(); system.init_logger_with_default_filter( - "gwasm=debug,gtest=info,sails_rs=debug,ping_pong_stack=debug", + "gwasm=debug,gtest=info,sails_rs=debug,ping_pong_stack=debug,gstd=debug", ); system.mint_to(ACTOR_ID, 1_000_000_000_000_000); // Submit program code into the system diff --git a/rs/Cargo.toml b/rs/Cargo.toml index c7f6891a4..62f75c737 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -48,7 +48,7 @@ log = { workspace = true, optional = true } tokio = { workspace = true, features = ["rt", "macros"] } [features] -default = ["gstd"] +default = ["gstd", "async-runtime"] build = ["client-builder", "wasm-builder"] debug = ["gstd?/debug"] ethexe = [ @@ -66,3 +66,4 @@ client-builder = ["std", "idl-gen", "dep:sails-client-gen", "dep:convert_case"] mockall = ["std", "dep:mockall"] std = ["futures/std"] wasm-builder = ["dep:gwasm-builder"] +async-runtime = ["gstd"] \ No newline at end of file diff --git a/rs/src/client/gstd_env.rs b/rs/src/client/gstd_env.rs index e2e4ff5be..2931d9246 100644 --- a/rs/src/client/gstd_env.rs +++ b/rs/src/client/gstd_env.rs @@ -1,27 +1,25 @@ use super::*; -use ::gstd::{ - errors::Error, - msg::{CreateProgramFuture, MessageFuture}, -}; +use crate::gstd::{CreateProgramFuture, Lock, MessageFuture}; +use ::gstd::errors::Error; #[derive(Default)] pub struct GstdParams { + pub value: Option, + pub wait: Option, + pub redirect_on_exit: bool, #[cfg(not(feature = "ethexe"))] pub gas_limit: Option, - pub value: Option, - pub wait_up_to: Option, #[cfg(not(feature = "ethexe"))] pub reply_deposit: Option, #[cfg(not(feature = "ethexe"))] - pub reply_hook: Option>, - pub redirect_on_exit: bool, + pub reply_hook: Option>, } crate::params_for_pending_impl!(GstdEnv, GstdParams { #[cfg(not(feature = "ethexe"))] pub gas_limit: GasUnit, pub value: ValueUnit, - pub wait_up_to: BlockCount, + pub wait: Lock, #[cfg(not(feature = "ethexe"))] pub reply_deposit: GasUnit, }); @@ -76,30 +74,26 @@ impl GearEnv for GstdEnv { } impl GstdEnv { + #[cfg_attr(feature = "ethexe", allow(unused_mut))] pub fn send_one_way( &self, destination: ActorId, payload: impl AsRef<[u8]>, - params: GstdParams, + mut params: GstdParams, ) -> Result { - let value = params.value.unwrap_or_default(); - let payload_bytes = payload.as_ref(); - - #[cfg(not(feature = "ethexe"))] - let waiting_reply_to = if let Some(gas_limit) = params.gas_limit { - ::gcore::msg::send_with_gas(destination, payload_bytes, gas_limit, value)? - } else { - ::gcore::msg::send(destination, payload_bytes, value)? - }; - #[cfg(feature = "ethexe")] - let waiting_reply_to = ::gcore::msg::send(destination, payload_bytes, value)?; - - #[cfg(not(feature = "ethexe"))] - if let Some(reply_deposit) = params.reply_deposit { - ::gcore::exec::reply_deposit(waiting_reply_to, reply_deposit)?; - } - - Ok(waiting_reply_to) + let reply_to = crate::ok!(crate::gstd::send_one_way( + destination, + payload.as_ref(), + params.value.unwrap_or_default(), + #[cfg(not(feature = "ethexe"))] + params.gas_limit, + #[cfg(not(feature = "ethexe"))] + params.reply_deposit, + #[cfg(not(feature = "ethexe"))] + params.reply_hook.take(), + )); + + Ok(reply_to) } } @@ -113,52 +107,7 @@ impl PendingCall { #[cfg(target_arch = "wasm32")] const _: () = { use core::task::ready; - - #[cfg(not(feature = "ethexe"))] - #[inline] - fn send_for_reply_future( - destination: ActorId, - payload: &[u8], - params: &mut GstdParams, - ) -> Result { - let value = params.value.unwrap_or_default(); - let reply_deposit = params.reply_deposit.unwrap_or_default(); - // here can be a redirect target - let mut message_future = if let Some(gas_limit) = params.gas_limit { - ::gstd::msg::send_bytes_with_gas_for_reply( - destination, - payload, - gas_limit, - value, - reply_deposit, - )? - } else { - ::gstd::msg::send_bytes_for_reply(destination, payload, value, reply_deposit)? - }; - - message_future = message_future.up_to(params.wait_up_to)?; - - if let Some(reply_hook) = params.reply_hook.take() { - message_future = message_future.handle_reply(reply_hook)?; - } - Ok(message_future) - } - - #[cfg(feature = "ethexe")] - #[inline] - fn send_for_reply_future( - destination: ActorId, - payload: &[u8], - params: &mut GstdParams, - ) -> Result { - let value = params.value.unwrap_or_default(); - // here can be a redirect target - let mut message_future = ::gstd::msg::send_bytes_for_reply(destination, payload, value)?; - - message_future = message_future.up_to(params.wait_up_to)?; - - Ok(message_future) - } + use futures::future::FusedFuture; #[inline] fn send_for_reply( @@ -167,11 +116,21 @@ const _: () = { params: &mut GstdParams, ) -> Result { // send message - let future = send_for_reply_future(destination, payload.as_ref(), params)?; + // let future = send_for_reply_future(destination, payload.as_ref(), params)?; + let future = crate::ok!(crate::gstd::send_bytes_for_reply( + destination, + payload.as_ref(), + params.value.unwrap_or_default(), + params.wait.unwrap_or_default(), + #[cfg(not(feature = "ethexe"))] + params.gas_limit, + #[cfg(not(feature = "ethexe"))] + params.reply_deposit, + #[cfg(not(feature = "ethexe"))] + params.reply_hook.take(), + )); if params.redirect_on_exit { - let created_block = params.wait_up_to.map(|_| gstd::exec::block_height()); Ok(GstdFuture::MessageWithRedirect { - created_block, future, destination, payload, @@ -181,9 +140,32 @@ const _: () = { } } + fn create_program( + code_id: CodeId, + salt: impl AsRef<[u8]>, + payload: impl AsRef<[u8]>, + params: &mut GstdParams, + ) -> Result<(GstdFuture, ActorId), Error> { + let (future, program_id) = crate::ok!(crate::gstd::create_program_for_reply( + code_id, + salt.as_ref(), + payload.as_ref(), + params.value.unwrap_or_default(), + params.wait.unwrap_or_default(), + #[cfg(not(feature = "ethexe"))] + params.gas_limit, + #[cfg(not(feature = "ethexe"))] + params.reply_deposit, + #[cfg(not(feature = "ethexe"))] + params.reply_hook.take(), + )); + Ok((GstdFuture::CreateProgram { future }, program_id)) + } + impl Future for PendingCall { type Output = Result::Error>; + #[inline(always)] fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if self.state.is_none() { let args = self @@ -223,7 +205,6 @@ const _: () = { let params = this.params.get_or_insert_default(); if let Replace::MessageWithRedirect { destination: _destination, - created_block, payload, .. } = state.as_mut().project_replace(GstdFuture::Dummy) @@ -232,16 +213,6 @@ const _: () = { { gstd::debug!("Redirecting message from {_destination} to {new_target}"); - // Calculate updated `wait_up_to` if provided - // wait_up_to = wait_up_to - (current_block - created_block) - params.wait_up_to = params.wait_up_to.and_then(|wait_up_to| { - created_block.map(|created_block| { - let current_block = gstd::exec::block_height(); - wait_up_to - .saturating_sub(current_block.saturating_sub(created_block)) - }) - }); - // send message to new target let future = send_for_reply(new_target, payload, params)?; // Replace the future with a new one @@ -264,12 +235,26 @@ const _: () = { } } + impl FusedFuture for PendingCall { + fn is_terminated(&self) -> bool { + self.state + .as_ref() + .map(|future| match future { + GstdFuture::CreateProgram { future } => future.is_terminated(), + GstdFuture::Message { future } => future.is_terminated(), + GstdFuture::MessageWithRedirect { future, .. } => future.is_terminated(), + GstdFuture::Dummy => false, + }) + .unwrap_or_default() + } + } + impl Future for PendingCtor { type Output = Result, ::Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { if self.state.is_none() { - let params = self.params.take().unwrap_or_default(); + let mut params = self.params.take().unwrap_or_default(); let value = params.value.unwrap_or_default(); let salt = self.salt.take().unwrap(); @@ -279,45 +264,25 @@ const _: () = { .unwrap_or_else(|| panic!("{PENDING_CALL_INVALID_STATE}")); let payload = T::encode_params(args); // Send message - #[cfg(not(feature = "ethexe"))] - let future = if let Some(gas_limit) = params.gas_limit { - ::gstd::prog::create_program_bytes_with_gas_for_reply( - self.code_id, - salt, - payload, - gas_limit, - value, - params.reply_deposit.unwrap_or_default(), - )? - } else { - ::gstd::prog::create_program_bytes_for_reply( - self.code_id, - salt, - payload, - value, - params.reply_deposit.unwrap_or_default(), - )? - }; - #[cfg(feature = "ethexe")] - let future = ::gstd::prog::create_program_bytes_for_reply( - self.code_id, - salt, - payload, - value, - )?; + let (future, program_id) = + match create_program(self.code_id, salt, payload, &mut params) { + Ok(res) => res, + Err(err) => return Poll::Ready(Err(err)), + }; - // self.program_id = Some(program_future.program_id); - self.state = Some(GstdFuture::CreateProgram { future }); + self.program_id = Some(program_id); + self.state = Some(future); // No need to poll the future return Poll::Pending; } let this = self.as_mut().project(); // SAFETY: checked in the code above. - let mut state = unsafe { this.state.as_pin_mut().unwrap_unchecked() }; + let state = unsafe { this.state.as_pin_mut().unwrap_unchecked() }; if let Projection::CreateProgram { future } = state.project() { // Poll create program future match ready!(future.poll(cx)) { - Ok((program_id, _payload)) => { + Ok(_payload) => { + let program_id = unsafe { this.program_id.unwrap_unchecked() }; // Do not decode payload here Poll::Ready(Ok(Actor::new(this.env.clone(), program_id))) } @@ -340,7 +305,6 @@ pin_project_lite::pin_project! { #[pin] future: MessageFuture, destination: ActorId, - created_block: Option, payload: Vec, // reuse encoded payload when redirecting }, Dummy, diff --git a/rs/src/gstd/async_runtime.rs b/rs/src/gstd/async_runtime.rs new file mode 100644 index 000000000..35b6ed8b8 --- /dev/null +++ b/rs/src/gstd/async_runtime.rs @@ -0,0 +1,796 @@ +use super::*; +use crate::collections::{BinaryHeap, HashMap}; +use core::{ + cmp::Reverse, + pin::Pin, + task::{Context, Poll}, +}; +use futures::future::{FusedFuture, FutureExt as _, LocalBoxFuture}; +use gstd::{BlockNumber, errors::Error}; + +fn tasks() -> &'static mut crate::collections::HashMap { + static mut MAP: Option> = None; + unsafe { &mut *core::ptr::addr_of_mut!(MAP) } + .get_or_insert_with(crate::collections::HashMap::new) +} + +fn signals() -> &'static mut WakeSignals { + static mut MAP: Option = None; + unsafe { &mut *core::ptr::addr_of_mut!(MAP) }.get_or_insert_with(WakeSignals::new) +} + +/// Matches a task to a some message in order to avoid duplicate execution +/// of code that was running before the program was interrupted by `wait`. +/// +/// The [`Task`] lifecycle matches to the single message processing in the `handle()` entry-point +/// and ends when all internal futures are resolved or `handle_signal()` received for this `message_id`. +pub struct Task { + future: LocalBoxFuture<'static, ()>, + locks: BinaryHeap<(Reverse, Option)>, + #[cfg(not(feature = "ethexe"))] + critical_hook: Option>, +} + +impl Task { + fn new(future: F) -> Self + where + F: Future + 'static, + { + Self { + future: future.boxed_local(), + locks: Default::default(), + #[cfg(not(feature = "ethexe"))] + critical_hook: None, + } + } + + /// Stores the lock associated with an outbound reply, keeping it ordered by deadline. + /// + /// - pushes `(lock, Some(reply_to))` into the binary heap so the task can efficiently retrieve the + /// earliest lock when deciding how long to sleep. + /// + /// # Context + /// Called from [`WakeSignals::register_signal`] when the [`message_loop`] schedules a reply wait. + #[inline] + fn insert_lock(&mut self, reply_to: MessageId, lock: Lock) { + self.locks.push((Reverse(lock), Some(reply_to))); + } + + /// Tracks a sleep-specific lock (without a reply identifier) for the inbound message. + /// + /// # Context + /// Used when the task needs to suspend itself via `exec::wait_*` without tying the wait to a + /// particular reply id. + fn insert_sleep(&mut self, lock: Lock) { + self.locks.push((Reverse(lock), None)); + } + + /// Returns the earliest lock still awaiting completion, removing stale or cleared entries. + /// + /// # Context + /// Called from [`message_loop`] whenever the user future remains pending after polling. + #[inline] + fn next_lock(&mut self, now: BlockNumber) -> Option { + let signals_map = signals(); + while let Some((Reverse(lock), reply_to)) = self.locks.peek() { + // 1. skip and remove expired + if now >= lock.deadline() { + self.locks.pop(); + continue; + } + // 2. skip and remove if not waits for reply_to + if let Some(reply_to) = reply_to + && !signals_map.waits_for(reply_to) + { + self.locks.pop(); + continue; + } + // 3. keep lock in `self.locks` for `WakeSignal::Pending` in case of `clear_signals` + return Some(*lock); + } + None + } + + /// Removes all outstanding reply locks from the signal registry without waiting on them. + /// + /// - iterates every stored `(_, reply_to)` pair and asks [`WakeSignals`] to drop the wake entry; + /// - used as part of task teardown to avoid keeping stale replies alive. + /// + /// # Context + /// Called from [`handle_signal`]. + #[cfg(not(feature = "ethexe"))] + #[inline] + fn clear_signals(&self) { + let now = Syscall::block_height(); + let signals_map = signals(); + self.locks.iter().for_each(|(_, reply_to)| { + if let Some(reply_to) = reply_to { + // set the `WakeSignal::Expired` for further processing in `handle_reply` + signals_map.record_timeout(*reply_to, now); + } + }); + } +} + +/// Sets a critical hook. +/// +/// # Context +/// If called in the `handle_reply` or `handle_signal` entrypoints. +/// +/// # SAFETY +/// Ensure that sufficient `gstd::Config::SYSTEM_RESERVE` is set in your +/// program, as this gas is locked during each async call to provide resources +/// for hook execution in case it is triggered. +#[cfg(not(feature = "ethexe"))] +pub fn set_critical_hook(f: F) { + if msg::reply_code().is_ok() { + panic!("`gstd::critical::set_hook()` must not be called in `handle_reply` entrypoint") + } + + if msg::signal_code().is_ok() { + panic!("`gstd::critical::set_hook()` must not be called in `handle_signal` entrypoint") + } + let message_id = Syscall::message_id(); + + tasks() + .get_mut(&message_id) + .expect("A message task must exist") + .critical_hook = Some(Box::new(f)); +} + +/// Drives asynchronous handling for the currently executing inbound message. +/// +/// - locates or creates the `Task` holding the user future for the current message id; +/// - polls the future once and, if it completes, tears down the bookkeeping; +/// - when the future stays pending, arms the shortest wait lock so the runtime suspends until a wake. +/// +/// # Context +/// Called from the contract's `handle` entry point while [`message_loop`] runs single-threaded inside the +/// actor. It must be invoked exactly once per incoming message to advance the async state machine. +/// +/// # Panics +/// Panics propagated from the user future bubble up, and the function will panic if no wait lock is +/// registered when a pending future requests suspension, signalling a contract logic bug. +#[inline] +pub fn message_loop(future: F) +where + F: Future + 'static, +{ + let msg_id = Syscall::message_id(); + let tasks_map = tasks(); + let task = tasks_map.entry(msg_id).or_insert_with(|| { + #[cfg(not(feature = "ethexe"))] + { + Syscall::system_reserve_gas(gstd::Config::system_reserve()) + .expect("Failed to reserve gas for system signal"); + } + Task::new(future) + }); + + let completed = { + let mut cx = Context::from_waker(task::Waker::noop()); + ::gstd::debug!("message_loop: polling future for {msg_id}"); + task.future.as_mut().poll(&mut cx).is_ready() + }; + + if completed { + tasks_map.remove(&msg_id); + } else { + let now = Syscall::block_height(); + task.next_lock(now) + .expect("Cannot find lock to be waited") + .wait(now); + } +} + +pub type Payload = Vec; + +/// The [`WakeSignal`] lifecycle corresponds to waiting for a reply to a sent message +/// and ends when `handle_reply()` is received. +/// +/// May outlive parent [`Task`] in [`WakeSignal::Expired`] state. +/// +/// Can be created in [`WakeSignal::Expired`] state if there is no [`Task`] to await. +enum WakeSignal { + /// Reply is still pending; tracks origin message, deadline, and optional hook to run on completion or timeout. + Pending { + message_id: MessageId, + deadline: BlockNumber, + reply_hook: Option>, + }, + /// Reply handled; captures payload and reply code so the waiting future can resolve. + Ready { + payload: Payload, + reply_code: ReplyCode, + }, + /// Reply missed its deadline; retains timing data and hook so late arrivals can still be acknowledged. + Expired { + expected: BlockNumber, + now: BlockNumber, + reply_hook: Option>, + }, +} + +struct WakeSignals { + signals: HashMap, +} + +impl WakeSignals { + pub fn new() -> Self { + Self { + signals: HashMap::new(), + } + } + + /// Registers a reply wait for `waiting_reply_to` while the current message is being processed. + /// + /// - stores [`WakeSignal::Pending`] together with an optional hook so `poll`/`record_reply` can resolve it later; + /// - records the lock deadline for timeout detection and attaches the lock to the owning [`Task`] for + /// consistent wake bookkeeping. + /// + /// # Context + /// Called from helpers such as `send_bytes_for_reply` / `create_program_for_reply` while the message + /// handler executes inside [`message_loop`] in `handle()` entry point, see [Gear Protocol](https://wiki.vara.network/docs/build/introduction). + /// The current `message_id` is read from the runtime and used to fetch the associated `Task` entry. + /// + /// # Panics + /// Panics if the `Task` for the current `message_id` cannot be found, which indicates the function + /// was invoked outside the [`message_loop`] context (programmer error). + pub fn register_signal( + &mut self, + waiting_reply_to: MessageId, + lock: locks::Lock, + reply_hook: Option>, + ) { + let message_id = Syscall::message_id(); + let deadline = lock.deadline(); + + self.signals.insert( + waiting_reply_to, + WakeSignal::Pending { + message_id, + deadline, + reply_hook, + }, + ); + + // ::gstd::debug!( + // "register_signal: add lock for reply_to {waiting_reply_to} in message {message_id}" + // ); + tasks() + .get_mut(&message_id) + .expect("A message task must exist") + .insert_lock(waiting_reply_to, lock); + } + + /// Registers a reply hook for `waiting_reply_to` without creating a tracked wait. + /// + /// - stores a [`WakeSignal::Expired`] entry so `record_reply` will still execute the hook if a reply + /// arrives later; + /// - intended for one-way sends that want to observe replies from outside [`message_loop`]. + /// + /// # Context + /// Called from [`send_one_way`] and other synchronous helpers; may be invoked outside [`message_loop`]. + #[cfg(not(feature = "ethexe"))] + #[inline] + pub fn register_hook( + &mut self, + waiting_reply_to: MessageId, + reply_hook: Option>, + ) { + if let Some(reply_hook) = reply_hook { + let now = Syscall::block_height(); + self.signals.insert( + waiting_reply_to, + WakeSignal::Expired { + expected: now, + now, + reply_hook: Some(reply_hook), + }, + ); + } + } + + /// Processes an incoming reply for `reply_to` and transitions the stored wake state. + /// + /// - upgrades the [`WakeSignal::Pending`] entry to [`WakeSignal::Ready`], capturing payload and reply code; + /// - executes the optional reply hook once the reply becomes available. + /// - for the [`WakeSignal::Expired`] entry executes the optional reply hook and remove entry; + /// + /// # Context + /// Invoked by [`handle_reply_with_hook`] when a reply arrives during `handle_reply()` execution. The + /// runtime supplies `reply_to`. + /// + /// # Panics + /// Panics if it encounters an already finalised entry [`WakeSignal::Ready`] or the associated task is + /// missing. Both scenarios indicate logic bugs or duplicate delivery. + pub fn record_reply(&mut self, reply_to: &MessageId) { + if let hashbrown::hash_map::EntryRef::Occupied(mut entry) = self.signals.entry_ref(reply_to) + { + match entry.get_mut() { + WakeSignal::Pending { + message_id, + deadline: _, + reply_hook, + } => { + let message_id = *message_id; + let reply_hook = reply_hook.take(); + // replase entry with `WakeSignal::Ready` + _ = entry.insert(WakeSignal::Ready { + payload: Syscall::read_bytes().expect("Failed to read bytes"), + reply_code: Syscall::reply_code() + .expect("Shouldn't be called with incorrect context"), + }); + ::gstd::debug!( + "record_reply: remove lock for reply_to {reply_to} in message {message_id}" + ); + // wake message loop after receiving reply + ::gcore::exec::wake(message_id).expect("Failed to wake the message"); + + // execute reply hook + if let Some(f) = reply_hook { + f() + } + } + WakeSignal::Expired { reply_hook, .. } => { + let reply_hook = reply_hook.take(); + _ = entry.remove(); + // execute reply hook and remove entry + if let Some(f) = reply_hook { + f() + } + } + WakeSignal::Ready { .. } => panic!("A reply has already received"), + }; + } else { + ::gstd::debug!( + "A message has received a reply though it wasn't to receive one, or a processed message has received a reply" + ); + } + } + + /// Marks a pending reply as timed out and preserves context for later handling. + /// + /// - upgrades a [`WakeSignal::Pending`] entry to [`WakeSignal::Expired`], capturing when the reply was expected + /// and when the timeout was detected; + /// - retains the optional reply hook so it can still be executed if a late reply arrives and reuses the + /// stored state when `record_reply` is called afterwards. + /// + /// # Context + /// Triggered from [`Task::clear_signals`]. + #[cfg(not(feature = "ethexe"))] + pub fn record_timeout(&mut self, reply_to: MessageId, now: BlockNumber) { + if let hashbrown::hash_map::Entry::Occupied(mut entry) = self.signals.entry(reply_to) + && let WakeSignal::Pending { + reply_hook, + deadline, + .. + } = entry.get_mut() + { + let expected = *deadline; + // move `reply_hook` to `WakeSignal::Expired` state + let reply_hook = reply_hook.take(); + entry.insert(WakeSignal::Expired { + expected, + now, + reply_hook, + }); + } else { + ::gstd::debug!("A message has timed out after reply"); + } + } + + pub fn waits_for(&self, reply_to: &MessageId) -> bool { + self.signals + .get(reply_to) + .is_some_and(|signal| !matches!(signal, WakeSignal::Expired { .. })) + } + + /// Polls the stored wake signal for `reply_to`, returning the appropriate future state. + /// + /// - inspects the current `WakeSignal` variant, promoting pending entries whose deadline has passed to + /// [`WakeSignal::Expired`]; + /// - returns `Pending`, a `Ready` payload, or propagates a timeout error; when `Ready`, the entry is + /// removed so subsequent polls observe completion. + /// + /// # Context + /// Called by [`MessageFuture::poll`] (and any wrappers) while a consumer awaits a reply produced by + /// [`message_loop`]. It runs on the same execution thread and must be non-blocking. + /// + /// # Panics + /// Panics if the signal was never registered for `reply_to`, which indicates misuse of the async API + /// (polling without having called one of the [`send_bytes_for_reply`]/[`create_program_for_reply`] methods first). + pub fn poll( + &mut self, + reply_to: &MessageId, + _cx: &mut Context<'_>, + ) -> Poll, Error>> { + let hashbrown::hash_map::EntryRef::Occupied(mut entry) = self.signals.entry_ref(reply_to) + else { + panic!("Poll not registered feature") + }; + + match entry.get_mut() { + WakeSignal::Pending { + deadline, + reply_hook, + .. + } => { + let now = Syscall::block_height(); + let expected = *deadline; + if now >= expected { + let reply_hook = reply_hook.take(); + _ = entry.insert(WakeSignal::Expired { + expected, + now, + reply_hook, + }); + Poll::Ready(Err(Error::Timeout(expected, now))) + } else { + Poll::Pending + } + } + WakeSignal::Expired { expected, now, .. } => { + // DO NOT remove entry if `WakeSignal::Expired` + // will be removed in `record_reply` + Poll::Ready(Err(Error::Timeout(*expected, *now))) + } + WakeSignal::Ready { .. } => { + // remove entry if `WakeSignal::Ready` + let WakeSignal::Ready { + payload, + reply_code, + } = entry.remove() + else { + // SAFETY: checked in the code above. + unsafe { hint::unreachable_unchecked() } + }; + match reply_code { + ReplyCode::Success(_) => Poll::Ready(Ok(payload)), + ReplyCode::Error(reason) => { + Poll::Ready(Err(Error::ErrorReply(payload.into(), reason))) + } + ReplyCode::Unsupported => Poll::Ready(Err(Error::UnsupportedReply(payload))), + } + } + } + } +} + +pub struct MessageFuture { + /// A message identifier for an expected reply. + /// + /// This identifier is generated by the corresponding send function (e.g. + /// [`gcore::msg::send`](::gcore::msg::send)). + pub waiting_reply_to: MessageId, +} + +impl Unpin for MessageFuture {} + +impl Future for MessageFuture { + type Output = Result, Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + poll(&self.waiting_reply_to, cx) + } +} + +impl FusedFuture for MessageFuture { + fn is_terminated(&self) -> bool { + is_terminated(&self.waiting_reply_to) + } +} + +#[inline] +pub fn send_for_reply( + destination: ActorId, + payload: E, + value: ValueUnit, +) -> Result { + let size = Encode::encoded_size(&payload); + stack_buffer::with_byte_buffer(size, |buffer: &mut [mem::MaybeUninit]| { + let mut buffer_writer = MaybeUninitBufferWriter::new(buffer); + Encode::encode_to(&payload, &mut buffer_writer); + buffer_writer.with_buffer(|buffer| { + send_bytes_for_reply( + destination, + buffer, + value, + Default::default(), + #[cfg(not(feature = "ethexe"))] + None, + #[cfg(not(feature = "ethexe"))] + None, + #[cfg(not(feature = "ethexe"))] + None, + ) + }) + }) +} + +#[inline] +pub fn send_one_way( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + #[cfg(not(feature = "ethexe"))] gas_limit: Option, + #[cfg(not(feature = "ethexe"))] reply_deposit: Option, + #[cfg(not(feature = "ethexe"))] reply_hook: Option>, +) -> Result { + let waiting_reply_to = crate::ok!(send_bytes( + destination, + payload, + value, + #[cfg(not(feature = "ethexe"))] + gas_limit, + #[cfg(not(feature = "ethexe"))] + reply_deposit + )); + + #[cfg(not(feature = "ethexe"))] + signals().register_hook(waiting_reply_to, reply_hook); + + Ok(waiting_reply_to) +} + +#[cfg(not(feature = "ethexe"))] +#[inline] +pub fn send_bytes_for_reply( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + wait: Lock, + gas_limit: Option, + reply_deposit: Option, + reply_hook: Option>, +) -> Result { + let waiting_reply_to = crate::ok!(send_bytes( + destination, + payload, + value, + gas_limit, + reply_deposit + )); + + signals().register_signal(waiting_reply_to, wait, reply_hook); + + Ok(MessageFuture { waiting_reply_to }) +} + +#[cfg(feature = "ethexe")] +#[inline] +pub fn send_bytes_for_reply( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + wait: Lock, +) -> Result { + let waiting_reply_to = crate::ok!(send_bytes(destination, payload, value)); + + signals().register_signal(waiting_reply_to, wait, None); + + Ok(MessageFuture { waiting_reply_to }) +} + +#[cfg(not(feature = "ethexe"))] +#[allow(clippy::too_many_arguments)] +#[inline] +pub fn create_program_for_reply( + code_id: CodeId, + salt: &[u8], + payload: &[u8], + value: ValueUnit, + wait: Lock, + gas_limit: Option, + reply_deposit: Option, + reply_hook: Option>, +) -> Result<(MessageFuture, ActorId), ::gstd::errors::Error> { + let (waiting_reply_to, program_id) = if let Some(gas_limit) = gas_limit { + crate::ok!(::gcore::prog::create_program_with_gas( + code_id, salt, payload, gas_limit, value + )) + } else { + crate::ok!(::gcore::prog::create_program(code_id, salt, payload, value)) + }; + + if let Some(reply_deposit) = reply_deposit { + _ = ::gcore::exec::reply_deposit(waiting_reply_to, reply_deposit); + } + + signals().register_signal(waiting_reply_to, wait, reply_hook); + + Ok((MessageFuture { waiting_reply_to }, program_id)) +} + +#[cfg(feature = "ethexe")] +#[inline] +pub fn create_program_for_reply( + code_id: CodeId, + salt: &[u8], + payload: &[u8], + value: ValueUnit, + wait: Lock, +) -> Result<(MessageFuture, ActorId), ::gstd::errors::Error> { + let (waiting_reply_to, program_id) = + crate::ok!(::gcore::prog::create_program(code_id, salt, payload, value)); + + signals().register_signal(waiting_reply_to, wait, None); + + Ok((MessageFuture { waiting_reply_to }, program_id)) +} + +/// Default reply handler. +#[inline] +pub fn handle_reply_with_hook() { + let reply_to = Syscall::reply_to().expect("Shouldn't be called with incorrect context"); + + signals().record_reply(&reply_to); +} + +/// Default signal handler. +#[cfg(not(feature = "ethexe"))] +#[inline] +pub fn handle_signal() { + let msg_id = Syscall::signal_from().expect( + "`gstd::async_runtime::handle_signal()` must be called only in `handle_signal` entrypoint", + ); + // Remove Task and all associated signals, execute critical hook + if let Some(mut task) = tasks().remove(&msg_id) { + if let Some(critical_hook) = task.critical_hook.take() { + critical_hook(msg_id); + } + task.clear_signals(); + } +} + +pub fn poll(message_id: &MessageId, cx: &mut Context<'_>) -> Poll, Error>> { + signals().poll(message_id, cx) +} + +pub fn is_terminated(message_id: &MessageId) -> bool { + !signals().waits_for(message_id) +} + +struct MessageSleepFuture { + deadline: BlockNumber, +} + +impl MessageSleepFuture { + fn new(deadline: BlockNumber) -> Self { + Self { deadline } + } +} + +impl Unpin for MessageSleepFuture {} + +impl Future for MessageSleepFuture { + type Output = (); + + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { + let now = Syscall::block_height(); + + if now >= self.deadline { + Poll::Ready(()) + } else { + Poll::Pending + } + } +} + +/// Delays message execution in asynchronous way for the specified number of blocks. +/// +/// It works pretty much like the [`gcore::exec::wait_for`] function, but +/// allows to continue execution after the delay in the same handler. It is +/// worth mentioning that the program state gets persisted inside the call, and +/// the execution resumes with potentially different state. +pub fn sleep_for(block_count: BlockCount) -> impl Future { + let message_id = Syscall::message_id(); + let lock = Lock::exactly(block_count); + tasks() + .get_mut(&message_id) + .expect("A message task must exist") + .insert_sleep(lock); + MessageSleepFuture::new(lock.deadline()) +} + +#[cfg(feature = "std")] +#[cfg(test)] +mod tests { + use super::*; + use crate::gstd::syscalls::Syscall; + use core::{sync::atomic::AtomicU64, task, task::Context}; + + static MSG_ID: AtomicU64 = AtomicU64::new(1); + + fn msg_id() -> MessageId { + MessageId::from(MSG_ID.fetch_add(1, core::sync::atomic::Ordering::SeqCst)) + } + + fn set_context(message_id: MessageId, block_height: u32) { + Syscall::with_message_id(message_id); + Syscall::with_block_height(block_height); + } + + #[test] + fn task_insert_lock_adds_entry() { + set_context(msg_id(), 10); + + let mut task = Task::new(async {}); + let reply_to = msg_id(); + let lock = Lock::up_to(3); + + task.insert_lock(reply_to, lock); + task.insert_lock(msg_id(), Lock::exactly(5)); + + let Some((Reverse(next_lock), next_reply_to)) = task.locks.peek() else { + unreachable!() + }; + + assert_eq!(task.locks.len(), 2); + assert_eq!(Some(&reply_to), next_reply_to.as_ref()); + assert_eq!(next_lock, &lock); + } + + #[test] + fn signals_poll_converts_pending_into_timeout() { + let message_id = msg_id(); + set_context(message_id, 20); + tasks().insert(message_id, Task::new(async {})); + + let reply_to = msg_id(); + let lock = Lock::up_to(5); + let deadline = lock.deadline(); + + signals().register_signal(reply_to, lock, None); + + Syscall::with_block_height(deadline - 1); + let mut cx = Context::from_waker(task::Waker::noop()); + assert!(matches!(signals().poll(&reply_to, &mut cx), Poll::Pending)); + + Syscall::with_block_height(deadline); + let mut cx = Context::from_waker(task::Waker::noop()); + match signals().poll(&reply_to, &mut cx) { + Poll::Ready(Err(Error::Timeout(expected, now))) => { + assert_eq!(expected, deadline); + assert_eq!(now, deadline); + } + other => panic!("expected timeout, got {other:?}"), + } + + signals().signals.remove(&reply_to); + tasks().remove(&message_id); + } + + #[test] + fn task_remove_signal_skip_not_waited_lock() { + let message_id = msg_id(); + set_context(message_id, 30); + let reply_to = msg_id(); + let lock = Lock::up_to(5); + + tasks().insert(message_id, Task::new(async {})); + let task = tasks().get_mut(&message_id).unwrap(); + signals().register_signal(reply_to, lock, None); + + assert_eq!(1, task.locks.len()); + + signals().signals.remove(&reply_to); + + assert_eq!(1, task.locks.len()); + assert_eq!(None, task.next_lock(31)); + tasks().remove(&message_id); + } + + #[test] + fn task_insert_sleep_adds_entry_without_reply() { + let message_id = msg_id(); + set_context(message_id, 40); + + let mut task = Task::new(async {}); + let lock = Lock::exactly(4); + + task.insert_sleep(lock); + assert_eq!(Some(lock), task.next_lock(42)); + assert_eq!(None, task.next_lock(lock.deadline())); + } +} diff --git a/rs/src/gstd/locks.rs b/rs/src/gstd/locks.rs new file mode 100644 index 000000000..56bddd164 --- /dev/null +++ b/rs/src/gstd/locks.rs @@ -0,0 +1,90 @@ +use crate::prelude::*; +use core::cmp::Ordering; +use gstd::{BlockCount, BlockNumber, Config, exec}; + +/// Type of wait locks. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Lock { + deadline: BlockNumber, + ty: WaitType, +} + +/// Wait types. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum WaitType { + Exactly, + #[default] + UpTo, +} + +impl Lock { + /// Wait for + pub fn exactly(b: BlockCount) -> Self { + let current = Syscall::block_height(); + Self { + deadline: current.saturating_add(b), + ty: WaitType::Exactly, + } + } + + /// Wait up to + pub fn up_to(b: BlockCount) -> Self { + let current = Syscall::block_height(); + Self { + deadline: current.saturating_add(b), + ty: WaitType::UpTo, + } + } + + /// Gets the deadline of the current lock. + pub fn deadline(&self) -> BlockNumber { + self.deadline + } + + /// Gets the duration from current [`Syscall::block_height()`]. + pub fn duration(&self) -> Option { + let current = Syscall::block_height(); + self.deadline.checked_sub(current) + } + + pub fn wait_type(&self) -> WaitType { + self.ty + } + + /// Call wait functions by the lock type. + pub fn wait(&self, now: BlockNumber) { + let duration = self + .deadline + .checked_sub(now) + .expect("Checked in `crate::gstd::async_runtime::message_loop`"); + match self.ty { + WaitType::Exactly => exec::wait_for(duration), + WaitType::UpTo => exec::wait_up_to(duration), + } + } +} + +impl PartialOrd for Lock { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Lock { + fn cmp(&self, other: &Self) -> Ordering { + let mut ord = self.deadline().cmp(&other.deadline()); + if ord == Ordering::Equal { + ord = match self.wait_type() { + WaitType::Exactly => Ordering::Greater, + WaitType::UpTo => Ordering::Less, + } + } + ord + } +} + +impl Default for Lock { + fn default() -> Self { + Lock::up_to(Config::wait_up_to()) + } +} diff --git a/rs/src/gstd/mod.rs b/rs/src/gstd/mod.rs index 14c2cc64c..894ca3aef 100644 --- a/rs/src/gstd/mod.rs +++ b/rs/src/gstd/mod.rs @@ -1,15 +1,32 @@ +#[cfg(feature = "async-runtime")] +pub use async_runtime::{ + MessageFuture, create_program_for_reply, handle_reply_with_hook, message_loop, + send_bytes_for_reply, send_for_reply, send_one_way, sleep_for, +}; +#[cfg(feature = "async-runtime")] +pub type CreateProgramFuture = MessageFuture; +#[cfg(feature = "async-runtime")] +#[cfg(not(feature = "ethexe"))] +#[doc(hidden)] +pub use async_runtime::{handle_signal, set_critical_hook}; #[doc(hidden)] #[cfg(feature = "ethexe")] pub use ethexe::{EthEvent, EthEventExpo}; #[doc(hidden)] pub use events::{EventEmitter, SailsEvent}; +#[cfg(not(feature = "async-runtime"))] #[cfg(not(feature = "ethexe"))] #[doc(hidden)] pub use gstd::handle_signal; +#[cfg(not(feature = "async-runtime"))] #[doc(hidden)] -pub use gstd::{async_init, async_main, handle_reply_with_hook, message_loop}; +pub use gstd::msg::{CreateProgramFuture, MessageFuture}; pub use gstd::{debug, exec, msg}; #[doc(hidden)] +#[cfg(not(feature = "async-runtime"))] +pub use gstd::{handle_reply_with_hook, message_loop}; +pub use locks::{Lock, WaitType}; +#[doc(hidden)] pub use sails_macros::{event, export, program, service}; pub use syscalls::Syscall; @@ -20,9 +37,12 @@ use crate::{ }; use gcore::stack_buffer; +#[cfg(feature = "async-runtime")] +mod async_runtime; #[cfg(feature = "ethexe")] mod ethexe; mod events; +mod locks; pub mod services; mod syscalls; @@ -138,3 +158,175 @@ pub trait InvocationIo { TypeId::of::() == TypeId::of::<()>() } } + +#[macro_export] +macro_rules! ok { + ($e:expr) => { + match $e { + Ok(t) => t, + Err(err) => { + return Err(err.into()); + } + } + }; +} + +#[cfg(not(feature = "ethexe"))] +#[inline] +fn send_bytes( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + gas_limit: Option, + reply_deposit: Option, +) -> Result { + let waiting_reply_to = if let Some(gas_limit) = gas_limit { + ::gcore::msg::send_with_gas(destination, payload, gas_limit, value)? + } else { + ::gcore::msg::send(destination, payload, value)? + }; + + if let Some(reply_deposit) = reply_deposit { + _ = ::gcore::exec::reply_deposit(waiting_reply_to, reply_deposit); + } + Ok(waiting_reply_to) +} + +#[cfg(feature = "ethexe")] +#[inline] +fn send_bytes( + destination: ActorId, + payload: &[u8], + value: ValueUnit, +) -> Result { + ::gcore::msg::send(destination, payload, value).map_err(::gstd::errors::Error::Core) +} + +#[cfg(not(feature = "async-runtime"))] +#[inline] +pub fn send_one_way( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + #[cfg(not(feature = "ethexe"))] gas_limit: Option, + #[cfg(not(feature = "ethexe"))] reply_deposit: Option, + #[cfg(not(feature = "ethexe"))] _reply_hook: Option>, +) -> Result { + let waiting_reply_to = crate::ok!(send_bytes( + destination, + payload, + value, + #[cfg(not(feature = "ethexe"))] + gas_limit, + #[cfg(not(feature = "ethexe"))] + reply_deposit + )); + + Ok(waiting_reply_to) +} + +#[cfg(not(feature = "async-runtime"))] +#[cfg(not(feature = "ethexe"))] +#[inline] +pub fn send_bytes_for_reply( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + wait: Lock, + gas_limit: Option, + reply_deposit: Option, + reply_hook: Option>, +) -> Result { + let reply_deposit = reply_deposit.unwrap_or_default(); + // here can be a redirect target + let mut message_future = if let Some(gas_limit) = gas_limit { + ::gstd::msg::send_bytes_with_gas_for_reply( + destination, + payload, + gas_limit, + value, + reply_deposit, + )? + } else { + ::gstd::msg::send_bytes_for_reply(destination, payload, value, reply_deposit)? + }; + + message_future = match wait.wait_type() { + WaitType::Exactly => message_future.exactly(wait.duration())?, + WaitType::UpTo => message_future.up_to(wait.duration())?, + }; + + if let Some(reply_hook) = reply_hook { + message_future = message_future.handle_reply(reply_hook)?; + } + Ok(message_future) +} + +#[cfg(not(feature = "async-runtime"))] +#[cfg(feature = "ethexe")] +#[inline] +pub fn send_bytes_for_reply( + destination: ActorId, + payload: &[u8], + value: ValueUnit, + wait: Lock, +) -> Result { + // here can be a redirect target + let mut message_future = ::gstd::msg::send_bytes_for_reply(destination, payload, value)?; + + message_future = match wait.wait_type() { + WaitType::Exactly => message_future.exactly(wait.duration())?, + WaitType::UpTo => message_future.up_to(wait.duration())?, + }; + + Ok(message_future) +} + +#[cfg(not(feature = "async-runtime"))] +#[allow(clippy::too_many_arguments)] +#[inline] +pub fn create_program_for_reply( + code_id: CodeId, + salt: &[u8], + payload: &[u8], + value: ValueUnit, + wait: Lock, + #[cfg(not(feature = "ethexe"))] gas_limit: Option, + #[cfg(not(feature = "ethexe"))] reply_deposit: Option, + #[cfg(not(feature = "ethexe"))] reply_hook: Option>, +) -> Result<(CreateProgramFuture, ActorId), ::gstd::errors::Error> { + #[cfg(not(feature = "ethexe"))] + let mut future = if let Some(gas_limit) = gas_limit { + ::gstd::prog::create_program_bytes_with_gas_for_reply( + code_id, + salt, + payload, + gas_limit, + value, + reply_deposit.unwrap_or_default(), + )? + } else { + ::gstd::prog::create_program_bytes_for_reply( + code_id, + salt, + payload, + value, + reply_deposit.unwrap_or_default(), + )? + }; + #[cfg(feature = "ethexe")] + let mut future = ::gstd::prog::create_program_bytes_for_reply(code_id, salt, payload, value)?; + let program_id = future.program_id; + + future = match wait.wait_type() { + WaitType::Exactly => future.exactly(wait.duration())?, + WaitType::UpTo => future.up_to(wait.duration())?, + }; + + #[cfg(not(feature = "ethexe"))] + if let Some(reply_hook) = reply_hook { + future = future.handle_reply(reply_hook)?; + } + + Ok((future, program_id)) +} diff --git a/rs/src/gstd/syscalls.rs b/rs/src/gstd/syscalls.rs index 642a8aa14..626ad0dff 100644 --- a/rs/src/gstd/syscalls.rs +++ b/rs/src/gstd/syscalls.rs @@ -9,7 +9,7 @@ use crate::prelude::*; /// These methods are essential for enabling on-chain applications to interact with the Gear runtime /// in a consistent manner. Depending on the target environment, different implementations are provided: /// -/// - For the WASM target, direct calls are made to `gstd::msg` and `gstd::exec` to fetch runtime data. +/// - For the WASM target, direct calls are made to `gcore::msg` and `gcore::exec` to fetch runtime data. /// - In standard (`std`) environments, a mock implementation uses thread-local state for testing purposes. /// - In `no_std` configurations without the `std` feature and not WASM target, the functions are marked as unimplemented. /// @@ -19,72 +19,99 @@ pub struct Syscall; #[cfg(target_arch = "wasm32")] impl Syscall { + #[inline(always)] pub fn message_id() -> MessageId { - gstd::msg::id() + ::gcore::msg::id() } + #[inline(always)] pub fn message_size() -> usize { - gstd::msg::size() + ::gcore::msg::size() } + #[inline(always)] pub fn message_source() -> ActorId { - gstd::msg::source() + ::gcore::msg::source() } + #[inline(always)] pub fn message_value() -> u128 { - gstd::msg::value() + ::gcore::msg::value() } + #[inline(always)] pub fn reply_to() -> Result { - gstd::msg::reply_to() + ::gcore::msg::reply_to() } + #[inline(always)] pub fn reply_code() -> Result { - gstd::msg::reply_code() + ::gcore::msg::reply_code() } #[cfg(not(feature = "ethexe"))] + #[inline(always)] pub fn signal_from() -> Result { - gstd::msg::signal_from() + ::gcore::msg::signal_from() } #[cfg(not(feature = "ethexe"))] + #[inline(always)] pub fn signal_code() -> Result, gcore::errors::Error> { - gstd::msg::signal_code() + ::gcore::msg::signal_code() } + #[inline(always)] pub fn program_id() -> ActorId { - gstd::exec::program_id() + ::gcore::exec::program_id() } + #[inline(always)] pub fn block_height() -> u32 { - gstd::exec::block_height() + ::gcore::exec::block_height() } + #[inline(always)] pub fn block_timestamp() -> u64 { - gstd::exec::block_timestamp() + ::gcore::exec::block_timestamp() } + #[inline(always)] pub fn value_available() -> u128 { - gstd::exec::value_available() + ::gcore::exec::value_available() } - pub fn env_vars() -> gstd::EnvVars { - gstd::exec::env_vars() + #[inline(always)] + pub fn env_vars() -> ::gcore::EnvVars { + ::gcore::exec::env_vars() } + #[inline(always)] pub fn exit(inheritor_id: ActorId) -> ! { - gstd::exec::exit(inheritor_id) + ::gcore::exec::exit(inheritor_id) + } + + #[inline(always)] + pub fn read_bytes() -> Result, ::gcore::errors::Error> { + let mut result = vec![0u8; ::gcore::msg::size()]; + ::gcore::msg::read(result.as_mut())?; + Ok(result) + } + + #[cfg(not(feature = "ethexe"))] + #[inline(always)] + pub fn system_reserve_gas(amount: GasUnit) -> Result<(), ::gcore::errors::Error> { + ::gcore::exec::system_reserve_gas(amount) } } #[cfg(not(target_arch = "wasm32"))] #[cfg(not(feature = "std"))] macro_rules! syscall_unimplemented { - ($($name:ident() -> $type:ty),* $(,)?) => { + ($($name:ident( $( $param:ident : $ty:ty ),* ) -> $type:ty),* $(,)?) => { impl Syscall { $( - pub fn $name() -> $type { + pub fn $name($( $param: $ty ),* ) -> $type { unimplemented!("{ERROR}") } )* @@ -111,17 +138,12 @@ syscall_unimplemented!( block_height() -> u32, block_timestamp() -> u64, value_available() -> u128, - env_vars() -> gstd::EnvVars, + env_vars() -> ::gcore::EnvVars, + exit(_inheritor_id: ActorId) -> !, + read_bytes() -> Result, gcore::errors::Error>, + system_reserve_gas(_amount: GasUnit) -> Result<(), ::gcore::errors::Error>, ); -#[cfg(not(target_arch = "wasm32"))] -#[cfg(not(feature = "std"))] -impl Syscall { - pub fn exit(_inheritor_id: ActorId) -> ! { - unimplemented!("{ERROR}") - } -} - #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "std")] const _: () = { @@ -175,6 +197,7 @@ const _: () = { block_height() -> u32, block_timestamp() -> u64, value_available() -> u128, + read_bytes() -> Result, gcore::errors::Error>, ); impl Default for SyscallState { @@ -194,13 +217,14 @@ const _: () = { block_height: 0, block_timestamp: 0, value_available: 0, + read_bytes: Err(::gcore::errors::Error::SyscallUsage), } } } impl Syscall { - pub fn env_vars() -> gstd::EnvVars { - gstd::EnvVars { + pub fn env_vars() -> ::gcore::EnvVars { + ::gcore::EnvVars { performance_multiplier: gstd::Percent::new(100), existential_deposit: 1_000_000_000_000, mailbox_threshold: 3000, @@ -211,5 +235,10 @@ const _: () = { pub fn exit(inheritor_id: ActorId) -> ! { panic!("Program exited with inheritor id: {}", inheritor_id); } + + #[cfg(not(feature = "ethexe"))] + pub fn system_reserve_gas(_amount: GasUnit) -> Result<(), ::gcore::errors::Error> { + Ok(()) + } } };