Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
14282b7
wip: Gear Env Client
vobradovich Aug 6, 2025
a2c5015
wip: env impl
vobradovich Aug 7, 2025
c83671d
wip: env impl
vobradovich Aug 8, 2025
e697db3
wip: env
vobradovich Aug 11, 2025
5c63597
wip: envs
vobradovich Aug 12, 2025
62a821a
wip: Client Gen
vobradovich Aug 12, 2025
c190cf6
wip: cleint gen, events, mocks
vobradovich Aug 13, 2025
32f4da0
wip: rmrk example
vobradovich Aug 27, 2025
6816f14
wip: ping pong
vobradovich Aug 28, 2025
1f01c5b
wip: env deploy, DefaultEnv
vobradovich Aug 29, 2025
cb1f409
wip: remove MockEnv
vobradovich Sep 2, 2025
07eb92d
wip: gclient tests
vobradovich Sep 2, 2025
c40c98e
wip: event listener, names
vobradovich Sep 3, 2025
4c98831
Merge branch 'master' into vo/client-v2-draft
vobradovich Sep 3, 2025
1a393e1
wip: fix clippy, cleanup
vobradovich Sep 4, 2025
374260f
wip: benchmarks refactoring
vobradovich Sep 4, 2025
f2803c5
wip: gstd future with redirect
vobradovich Sep 5, 2025
e0fb024
wip: bench redirect
vobradovich Sep 5, 2025
1db71f4
Merge branch 'master' into vo/client-v2-draft
vobradovich Sep 8, 2025
57fbccd
wip: cleanup, fix clippy
vobradovich Sep 8, 2025
501708e
fix: client gen full path
vobradovich Sep 8, 2025
5e50a9b
fix: cli program template
vobradovich Sep 9, 2025
02b778a
chore: optimizations
vobradovich Sep 10, 2025
88ba3d1
wip: opt, code remove
vobradovich Sep 10, 2025
4044c93
wip: redirect opt
vobradovich Sep 10, 2025
4ad6e98
fix: logs
vobradovich Sep 11, 2025
227504e
wip: gtest env query
vobradovich Sep 11, 2025
94ca161
wip: gclient, gtest funcs, remove old code
vobradovich Sep 12, 2025
f74a505
chore: refactor
vobradovich Sep 12, 2025
f1d852f
perf: remove params & payload from GtsdFuture::MessageWithRedirect
vobradovich Sep 16, 2025
223e13a
fix: typo GstdFuture
vobradovich Sep 18, 2025
0c92d8b
perf: return encoded payload to GstdFuture::MessageWithRedirect
vobradovich Sep 19, 2025
a308153
fix: comments, send_one_way
vobradovich Sep 23, 2025
6ecacc2
wip: gstd_env PendingCall::poll opt
vobradovich Sep 29, 2025
0abcc4c
chore: improve readability, but at the cost of gas
vobradovich Sep 29, 2025
ac3c9cb
feat: add ping-pong example w/ message stacking
vobradovich Sep 17, 2025
a8da782
feat: create second Ping program from first one
vobradovich Sep 29, 2025
022543f
wip: asyn runtime pure perf
vobradovich Sep 23, 2025
7d59d61
wip: async runtime timeouts
vobradovich Sep 24, 2025
b9010d7
wip: refactor, opt
vobradovich Sep 24, 2025
1cc1d00
wip: refactor, opt
vobradovich Sep 25, 2025
f1c63fb
wip: locks opt
vobradovich Sep 25, 2025
1c6df3d
wip: use Vec for locks
vobradovich Sep 29, 2025
c12506a
wip: compare locks, prefer WaitUpTo
vobradovich Sep 29, 2025
978250b
wip: replace `?` with `ok!`, remove double hashing in WakeSignals::poll
vobradovich Sep 29, 2025
7326a07
wip: replace retain w/ swap_remove
vobradovich Sep 29, 2025
8793d1f
wip: Lock struct, WaitType enum
vobradovich Sep 29, 2025
e4b80f0
wip: use Lock in GstdParams. remove deadline recalc
vobradovich Oct 1, 2025
9d4bdb8
wip: reply hooks (old)
vobradovich Oct 1, 2025
4215d8c
wip: move reply_hook to WakeSignal, remove HashMap
vobradovich Oct 2, 2025
ee0c1ad
wip: micro perf
vobradovich Oct 3, 2025
68b5d8f
wip: add comments
vobradovich Oct 3, 2025
7968115
wip: add Task::clear(), call in handle_signal()
vobradovich Oct 6, 2025
23fad29
wip: ethexe, docs, tests
vobradovich Oct 7, 2025
7bd9c1b
wip: add critical_hook
vobradovich Oct 8, 2025
8630249
wip: refactor
vobradovich Oct 8, 2025
fdc4a5e
wip: use BinaryHeap for locks, lazy remove locks
vobradovich Oct 8, 2025
673daae
wip: lazy removal lock in `next_lock` call
vobradovich Oct 9, 2025
b806243
wip: add `sleep_for`
vobradovich Oct 9, 2025
69dbe78
wip: move Timeout processing to `poll` fn, store deadline in WakeSign…
vobradovich Oct 10, 2025
24d6127
feat: add feature `async_runtime`
vobradovich Oct 10, 2025
5acecf7
feat: add Syscall::read_bytes, Syscall::system_reserve_gas
vobradovich Oct 13, 2025
1b643d4
Merge branch 'master' into vo/async-runtime
vobradovich Oct 16, 2025
da2dd39
bench: fix message_stack units
vobradovich Oct 17, 2025
4026d4c
Merge branch 'vo/fix-stack-bench-unit' into vo/async-runtime
vobradovich Oct 17, 2025
9052f9b
wip: rename WakeSignal::Timeout to Expired, fix `send_one_way`
vobradovich Oct 17, 2025
c1367c1
Merge branch 'master' into vo/async-runtime
vobradovich Oct 20, 2025
a093c86
fix: clippy
vobradovich Oct 20, 2025
804460a
fix: tests
vobradovich Oct 20, 2025
cf08be8
fix: cfg features
vobradovich Oct 22, 2025
427c8cc
fix: remove entrye before hook exec on WakeSignal::Expired
vobradovich Oct 23, 2025
883ebd8
Merge branch 'master' into vo/async-runtime
vobradovich May 15, 2026
3df3ad4
Merge branch 'master' into vo/async-runtime
vobradovich May 28, 2026
df0bc4f
fix: Memory leak: `WakeSignal::Expired` entries never collected witho…
vobradovich May 28, 2026
78e188b
perf: hasher for `MessageId`, avoids zeroing N bytes in Syscall::read…
vobradovich May 28, 2026
350c14f
perf: noop baseline bench, runtime .expect opt
vobradovich May 29, 2026
2d044d8
wip: refine
vobradovich Jun 1, 2026
63e1c70
Merge branch 'master' into vo/async-runtime
vobradovich Jun 1, 2026
22c7437
wip: rever read_bytes() opt, propagate reply_deposit error
vobradovich Jun 1, 2026
76eb89f
wip: async runtime unit tests, wasm tests
vobradovich Jun 1, 2026
2105663
fmt
vobradovich Jun 1, 2026
872a255
clippy
vobradovich Jun 1, 2026
2401de0
fix: reentrancy/aliasing UB in the gstd async runtime
vobradovich Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions benchmarks/bench_data.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
{
"compute": {
"median": 465525077223
"median": 465525097695
},
"alloc": {
"0": 599843943,
"12": 603255248,
"143": 763104169,
"986": 865787429,
"10945": 2216328240,
"46367": 6545787850,
"121392": 17172248387,
"317810": 43909564540
"0": 599863562,
"12": 603274867,
"143": 763123788,
"986": 865807048,
"10945": 2216347859,
"46367": 6545807469,
"121392": 17172268006,
"317810": 43909584159
},
"counter": {
"async_call": 865267022,
"sync_call": 695201799
"async_call": 817171163,
"sync_call": 653756647
},
"cross_program": {
"median": 2391505600
"median": 2198136314
},
"redirect": {
"median": 3464158516
"median": 3170024744
},
"message_stack": {
"0": 792932822,
"1": 3752847200,
"5": 15633706718,
"10": 29340040097,
"20": 65942391269
"0": 696547455,
"1": 3357401971,
"5": 14035799870,
"10": 26225131218,
"20": 51911611286
},
"noop_baseline": {
"median": 363446797
Expand Down
4 changes: 3 additions & 1 deletion docs/syscall-mapping-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ See `rs/src/types.rs`.
| `Syscall::exit(inheritor_id)` | `!` | `gcore::exec::exit(inheritor_id)` | |
| `Syscall::panic(data)` | `!` | `gcore::ext::panic(data)` | |
| `Syscall::read_bytes()` | `Result<Vec<u8>, gcore::errors::Error>` | allocate `vec![0u8; gcore::msg::size()]`, then `gcore::msg::read(result.as_mut())` | |
| `Syscall::wake(message_id)` | `Result<(), gcore::errors::Error>` | `gcore::exec::wake(message_id)` | |
| `Syscall::system_reserve_gas(amount)` | `Result<(), gcore::errors::Error>` | `gcore::exec::system_reserve_gas(amount)` | only when `ethexe` is disabled |

## Behavioral Notes
Expand All @@ -58,7 +59,7 @@ The `message_*`, `reply_*`, and `signal_*` methods are wrappers over `gcore::msg

### Execution Context

The `program_id`, `block_height`, `block_timestamp`, `value_available`, `gas_available`, `env_vars`, `exit`, and `system_reserve_gas` methods are wrappers over `gcore::exec::*` accessors and control flow.
The `program_id`, `block_height`, `block_timestamp`, `value_available`, `gas_available`, `env_vars`, `exit`, `wake`, and `system_reserve_gas` methods are wrappers over `gcore::exec::*` accessors and control flow.

### Panic Surface

Expand All @@ -75,6 +76,7 @@ On non-`wasm32` targets with feature `std`:
- `env_vars()` returns a constructed `gcore::EnvVars` value rather than delegating to `gcore::exec::env_vars()`.
- `exit()` and `panic()` call Rust `panic!` with diagnostic text instead of delegating to runtime syscalls.
- `read_bytes()` reads from thread-local mock state.
- `wake()` is a no-op that returns `Ok(())` (it takes a `MessageId` argument, so it is a manual mock rather than a generated getter).
- `system_reserve_gas()` returns `Ok(())` when present, that is, when `ethexe` is disabled.

These behaviors are intentionally test-oriented and are not part of the `gcore` mapping defined above.
Expand Down
78 changes: 66 additions & 12 deletions examples/demo/app/src/chaos/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use sails_rs::gstd::debug;
use sails_rs::gstd::{Lock, debug};
use sails_rs::{gstd, prelude::*};

static mut REPLY_HOOK_COUNTER: u32 = 0;
static mut CRITICAL_HOOK_COUNTER: u32 = 0;

pub struct ChaosService;

Expand All @@ -10,9 +11,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");
}
Expand All @@ -22,16 +21,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");
}
Expand All @@ -40,4 +42,56 @@ impl ChaosService {
pub fn reply_hook_counter(&self) -> u32 {
unsafe { REPLY_HOOK_COUNTER }
}

/// Suspends the message for `blocks` blocks via `sleep_for`, then returns
/// the actual block delta. Used to verify the `MessageSleepFuture` lifecycle:
/// register sleep lock -> `exec::wait_for` -> resume at deadline -> complete.
#[export]
pub async fn sleep_then_return(&self, blocks: u32) -> u32 {
let start = Syscall::block_height();
gstd::sleep_for(blocks).await;
Syscall::block_height().saturating_sub(start)
}

/// Sends two messages for reply concurrently and resolves with the first to
/// reply via `futures::select`. Exercises `next_lock` picking the earliest
/// of several armed locks and `forget_future` reclaiming the losing branch
/// (its `MessageFuture` is dropped unresolved). Returns `0` if the first
/// send (`b"A"`) wins, `1` if the second (`b"B"`) does.
#[export]
pub async fn select_first_reply(&self) -> u8 {
use sails_rs::futures::future::{Either, select};

let source = Syscall::message_source();
let a = gstd::send_bytes_for_reply(source, b"A", 0, Lock::up_to(100), None, None, None)
.unwrap();
let b = gstd::send_bytes_for_reply(source, b"B", 0, Lock::up_to(100), None, None, None)
.unwrap();

match select(a, b).await {
Either::Left(_) => 0,
Either::Right(_) => 1,
}
}

/// Registers a critical hook, awaits a reply, then panics after resuming.
/// The trap on a message that reserved system gas makes the runtime invoke
/// `handle_signal`, which runs the stored critical hook. Used to verify the
/// `set_critical_hook` -> `handle_signal` path (distinct from the userspace
/// error reply). Observe the effect via [`Self::critical_hook_counter`].
#[export]
pub async fn critical_hook_on_signal(&self) {
gstd::set_critical_hook(|_msg_id| {
unsafe { CRITICAL_HOOK_COUNTER += 1 };
debug!("critical hook fired in handle_signal");
});
let source = Syscall::message_source();
let _ = gstd::send_for_reply::<()>(source, (), 0).unwrap().await;
panic!("panic after wait to trigger a signal");
}

#[export]
pub fn critical_hook_counter(&self) -> u32 {
unsafe { CRITICAL_HOOK_COUNTER }
}
}
141 changes: 141 additions & 0 deletions examples/demo/app/tests/gtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,147 @@ fn chaos_service_timeout_wait() {
);
}

#[test]
fn chaos_service_sleep_for_works() {
use demo_client::{chaos::io::SleepThenReturn, io::Default};
use sails_rs::gtest::{Program, System};

let system = System::new();
system.init_logger_with_default_filter("gwasm=debug,gtest=info,sails_rs=debug");
system.mint_to(ACTOR_ID, 1_000_000_000_000_000);
let program = Program::from_file(&system, DEMO_WASM_PATH);
program.send_bytes(ACTOR_ID, Default::encode_call(0));
system.run_next_block();

const SLEEP_BLOCKS: u32 = 3;
let msg_id = program.send_bytes(
ACTOR_ID,
SleepThenReturn::encode_call(DemoClientProgram::ROUTE_ID_CHAOS, SLEEP_BLOCKS),
);

// First block sends the message and suspends via sleep_for(SLEEP_BLOCKS),
// then the runtime must wake the task at the deadline. Scan a few blocks
// beyond the deadline; the reply lands in whichever block the wake occurs.
let mut reply_payload: Option<Vec<u8>> = None;
for _ in 0..(SLEEP_BLOCKS + 3) {
let run = system.run_next_block();
if let Some(payload) = run
.log()
.iter()
.find(|log| log.reply_to() == Some(msg_id))
.map(|log| log.payload().to_vec())
{
reply_payload = Some(payload);
break;
}
}
let payload = reply_payload.expect("sleep_then_return reply not produced");
let elapsed =
SleepThenReturn::decode_reply(DemoClientProgram::ROUTE_ID_CHAOS, payload).unwrap();
assert!(
elapsed >= SLEEP_BLOCKS,
"sleep_for should suspend at least {SLEEP_BLOCKS} blocks, got {elapsed}"
);
}

#[test]
fn chaos_service_select_first_reply() {
use demo_client::{chaos::io::SelectFirstReply, io::Default};
use sails_rs::gtest::{Log, Program, System};

let system = System::new();
system.init_logger_with_default_filter("gwasm=debug,gtest=info,sails_rs=debug");
system.mint_to(ACTOR_ID, 1_000_000_000_000_000);
let program = Program::from_file(&system, DEMO_WASM_PATH);
program.send_bytes(ACTOR_ID, Default::encode_call(0));
system.run_next_block();

let msg_id = program.send_bytes(
ACTOR_ID,
SelectFirstReply::encode_call(DemoClientProgram::ROUTE_ID_CHAOS),
);
// First block: the method sends b"A" and b"B" concurrently, then suspends
// with two armed locks.
system.run_next_block();

// Reply only to the first send (b"A"); the second future is dropped
// unresolved when `select` resolves Left, exercising `forget_future`.
let log = Log::builder().source(program.id()).dest(ACTOR_ID);
system
.get_mailbox(ACTOR_ID)
.reply_bytes(log.payload_bytes(b"A"), vec![], 0)
.unwrap();
let run = system.run_next_block();

let payload = run
.log()
.iter()
.find(|log| log.reply_to() == Some(msg_id))
.map(|log| log.payload().to_vec())
.expect("select_first_reply did not produce a reply");
let winner =
SelectFirstReply::decode_reply(DemoClientProgram::ROUTE_ID_CHAOS, payload).unwrap();
assert_eq!(winner, 0, "the first send (b\"A\") must win the select");
}

#[test]
fn chaos_service_critical_hook_on_signal() {
use demo_client::{
chaos::io::{CriticalHookCounter, CriticalHookOnSignal},
io::Default,
};
use sails_rs::gtest::{Log, Program, System};

let system = System::new();
system.init_logger_with_default_filter("gwasm=debug,gtest=info,sails_rs=debug");
system.mint_to(ACTOR_ID, 1_000_000_000_000_000);
let program = Program::from_file(&system, DEMO_WASM_PATH);
program.send_bytes(ACTOR_ID, Default::encode_call(0));
system.run_next_block();

let read_counter = |system: &System, program: &Program| {
let msg_id = program.send_bytes(
ACTOR_ID,
CriticalHookCounter::encode_call(DemoClientProgram::ROUTE_ID_CHAOS),
);
let run = system.run_next_block();
let payload = run
.log()
.iter()
.find(|log| log.reply_to() == Some(msg_id))
.map(|log| log.payload().to_vec())
.expect("counter reply not found");
CriticalHookCounter::decode_reply(DemoClientProgram::ROUTE_ID_CHAOS, payload).unwrap()
};

assert_eq!(read_counter(&system, &program), 0, "hook must not fire yet");

program.send_bytes(
ACTOR_ID,
CriticalHookOnSignal::encode_call(DemoClientProgram::ROUTE_ID_CHAOS),
);
// Sends the inner message (reserving system gas) and suspends.
system.run_next_block();

// Reply so the method resumes and panics; the trap on a message that
// reserved system gas drives the runtime into `handle_signal`, which runs
// the stored critical hook.
let log = Log::builder().source(program.id()).dest(ACTOR_ID);
system
.get_mailbox(ACTOR_ID)
.reply_bytes(log.payload_bytes(().encode()), vec![], 0)
.unwrap();
system.run_next_block();
// Signal processing may land in the following block.
system.run_next_block();

assert_eq!(
read_counter(&system, &program),
1,
"critical hook must fire exactly once via handle_signal"
);
}

#[tokio::test]
async fn chaos_panic_does_not_affect_other_services() {
use demo_client::chaos::Chaos as _;
Expand Down
25 changes: 23 additions & 2 deletions examples/demo/client/demo_client.idl
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,33 @@ service Validator@0x4e78bafffdb4fb1c {
}
}

service Chaos@0xf0c8c80dfabf72d5 {
service Chaos@0x6412b1f7e47f892d {
functions {
@query
CriticalHookCounter() -> u32;
/// Registers a critical hook, awaits a reply, then panics after resuming.
/// The trap on a message that reserved system gas makes the runtime invoke
/// `handle_signal`, which runs the stored critical hook. Used to verify the
/// `set_critical_hook` -> `handle_signal` path (distinct from the userspace
/// error reply). Observe the effect via [`Self::critical_hook_counter`].
@query
CriticalHookOnSignal();
@query
PanicAfterWait();
@query
ReplyHookCounter() -> u32;
/// Sends two messages for reply concurrently and resolves with the first to
/// reply via `futures::select`. Exercises `next_lock` picking the earliest
/// of several armed locks and `forget_future` reclaiming the losing branch
/// (its `MessageFuture` is dropped unresolved). Returns `0` if the first
/// send (`b"A"`) wins, `1` if the second (`b"B"`) does.
@query
SelectFirstReply() -> u8;
/// Suspends the message for `blocks` blocks via `sleep_for`, then returns
/// the actual block delta. Used to verify the `MessageSleepFuture` lifecycle:
/// register sleep lock -> `exec::wait_for` -> resume at deadline -> complete.
@query
SleepThenReturn(blocks: u32) -> u32;
@query
TimeoutWait();
}
Expand Down Expand Up @@ -198,7 +219,7 @@ program DemoClient {
ThisThat@0x381e13fdd02d675f,
ValueFee@0x61261a86528bf9d5,
Validator@0x4e78bafffdb4fb1c,
Chaos@0xf0c8c80dfabf72d5,
Chaos@0x6412b1f7e47f892d,
Chain@0x01fcbe183e2199b0,
OverrideGenerics@0xa33febc87c18925b,
}
Expand Down
Loading