diff --git a/examples/autoreply/src/lib.rs b/examples/autoreply/src/lib.rs index f36016841ca..6b3077082e8 100644 --- a/examples/autoreply/src/lib.rs +++ b/examples/autoreply/src/lib.rs @@ -35,7 +35,7 @@ mod wasm; mod tests { use alloc::vec::Vec; use gstd::ActorId; - use gtest::{Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Program, System}; #[test] fn auto_reply_received() { @@ -45,7 +45,7 @@ mod tests { let prog1 = Program::current(&system); let prog1_id = ActorId::try_from(prog1.id().as_ref()).unwrap(); - let from = 42; + let from = DEFAULT_USER_ALICE; // Init Program-1 let init_msg1 = prog1.send(from, ActorId::zero()); diff --git a/examples/custom/src/btree.rs b/examples/custom/src/btree.rs index 5711edb49b3..a5bc7437b42 100644 --- a/examples/custom/src/btree.rs +++ b/examples/custom/src/btree.rs @@ -94,7 +94,7 @@ mod tests { use super::{Reply, Request}; use crate::InitMessage; use alloc::vec; - use gtest::{Log, Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Log, Program, System}; #[test] fn program_can_be_initialized() { @@ -103,7 +103,7 @@ mod tests { let program = Program::current(&system); - let from = 42; + let from = DEFAULT_USER_ALICE; program.send(from, InitMessage::BTree); let res = system.run_next_block(); @@ -118,7 +118,7 @@ mod tests { let program = Program::current_opt(&system); - let from = 42; + let from = DEFAULT_USER_ALICE; program.send(from, InitMessage::BTree); IntoIterator::into_iter([ diff --git a/examples/distributor/src/lib.rs b/examples/distributor/src/lib.rs index 6cdb3b12012..6798f87aa46 100644 --- a/examples/distributor/src/lib.rs +++ b/examples/distributor/src/lib.rs @@ -58,7 +58,7 @@ mod wasm; #[cfg(test)] mod tests { use super::{Reply, Request}; - use gtest::{Log, Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Log, Program, System}; #[test] fn program_can_be_initialized() { @@ -67,7 +67,7 @@ mod tests { let program = Program::current(&system); - let from = 42; + let from = DEFAULT_USER_ALICE; program.send_bytes(from, b"init"); let res = system.run_next_block(); @@ -82,7 +82,7 @@ mod tests { let program = Program::current(&system); - let from = 42; + let from = DEFAULT_USER_ALICE; // Init program.send_bytes(from, b"init"); @@ -112,7 +112,7 @@ mod tests { ) -> (Program, Program, Program) { system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program_1 = Program::current_with_id(system, program_1_id); program_1.send_bytes(from, b"init"); @@ -147,7 +147,7 @@ mod tests { let system = System::new(); let (program_1, program_2, _program_3) = multi_program_setup(&system, 1, 2, 3); - let from = 42; + let from = DEFAULT_USER_ALICE; program_1.send(from, Request::Receive(11)); let res = system.run_next_block(); @@ -181,7 +181,7 @@ mod tests { let (program_1, _program_2, _program_3) = multi_program_setup(&system, 1, 2, 3); let program_4_id = 4; - let from = 42; + let from = DEFAULT_USER_ALICE; let program_4 = Program::current_with_id(&system, program_4_id); program_4.send_bytes(from, b"init"); diff --git a/examples/gas-burned/src/lib.rs b/examples/gas-burned/src/lib.rs index 5b6f056cc6b..6c600fbafab 100644 --- a/examples/gas-burned/src/lib.rs +++ b/examples/gas-burned/src/lib.rs @@ -13,14 +13,14 @@ mod wasm; #[cfg(test)] mod tests { - use gtest::{Gas, Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Gas, Program, System}; #[test] fn gas_burned() { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program = Program::current(&system); let init_msg_id = program.send_bytes(from, "init"); diff --git a/examples/new-meta/tests/read_state.rs b/examples/new-meta/tests/read_state.rs index 2caf1b64948..8060bf13022 100644 --- a/examples/new-meta/tests/read_state.rs +++ b/examples/new-meta/tests/read_state.rs @@ -2,7 +2,7 @@ use demo_new_meta::{ MessageInitIn, Person, Wallet, META_EXPORTS_V1, META_EXPORTS_V2, META_WASM_V1, META_WASM_V2, }; use gstd::Encode; -use gtest::{state_args, state_args_encoded, Program, System}; +use gtest::{constants::DEFAULT_USER_ALICE, state_args, state_args_encoded, Program, System}; #[test] fn read_state_bytes_returns_full_state() { @@ -183,10 +183,9 @@ fn read_state_with_two_args_wasm_func_returns_transformed_state() { } fn initialize_current_program(system: &System) -> Program { - const SOME_USER_ID: u64 = 3; let program = Program::current(system); program.send( - SOME_USER_ID, + DEFAULT_USER_ALICE, MessageInitIn { amount: 123, currency: "USD".into(), diff --git a/examples/node/src/lib.rs b/examples/node/src/lib.rs index 3718cbd44fe..a0d4ff32ebe 100644 --- a/examples/node/src/lib.rs +++ b/examples/node/src/lib.rs @@ -61,14 +61,14 @@ mod wasm; #[cfg(test)] mod tests { use super::{Initialization, Operation, Reply, Request}; - use gtest::{Log, Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Log, Program, System}; #[test] fn test_message_send_to_failed_program() { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program = Program::current(&system); let msg_id = program.send(from, Request::IsReady); @@ -81,7 +81,7 @@ mod tests { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program = Program::current(&system); let msg_id = program.send(from, Initialization { status: 5 }); @@ -96,7 +96,7 @@ mod tests { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program = Program::current(&system); program.send(from, Initialization { status: 5 }); @@ -131,7 +131,7 @@ mod tests { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program_1_id = 1; let program_2_id = 2; diff --git a/examples/program-factory/src/lib.rs b/examples/program-factory/src/lib.rs index fa288276848..c140ef14ca3 100644 --- a/examples/program-factory/src/lib.rs +++ b/examples/program-factory/src/lib.rs @@ -55,7 +55,11 @@ mod tests { extern crate std; use super::*; - use gtest::{calculate_program_id, constants::UNITS, Program, System}; + use gtest::{ + calculate_program_id, + constants::{DEFAULT_USER_ALICE, UNITS}, + Program, System, + }; use std::io::Write; // Creates a new factory and initializes it. @@ -67,7 +71,7 @@ mod tests { // Instantiate factory let factory = Program::current_with_id(sys, 100); - let user_id = 10001; + let user_id = DEFAULT_USER_ALICE; sys.mint_to(user_id, 100 * UNITS); // Send init msg to factory @@ -98,7 +102,7 @@ mod tests { let factory = prepare_factory(&sys); // Send handle msg to factory to create a new child - let msg_id = factory.send_bytes(10001, CreateProgram::Default.encode()); + let msg_id = factory.send_bytes(DEFAULT_USER_ALICE, CreateProgram::Default.encode()); let res = sys.run_next_block(); let child_id_expected = calculate_program_id(CHILD_CODE_HASH.into(), &0i32.to_le_bytes(), Some(msg_id)); @@ -116,7 +120,7 @@ mod tests { let payload = CreateProgram::Custom(vec![(CHILD_CODE_HASH, salt.to_vec(), 100_000_000)]); // Send handle msg to factory to create a new child - let msg_id = factory.send_bytes(10001, payload.encode()); + let msg_id = factory.send_bytes(DEFAULT_USER_ALICE, payload.encode()); let res = sys.run_next_block(); let child_id_expected = calculate_program_id(CHILD_CODE_HASH.into(), &salt, Some(msg_id)); @@ -125,7 +129,7 @@ mod tests { assert!(sys.is_active_program(child_id_expected)); // Send handle msg to create a duplicate - let msg_id = factory.send_bytes(10001, payload.encode()); + let msg_id = factory.send_bytes(DEFAULT_USER_ALICE, payload.encode()); let res = sys.run_next_block(); let child_id_expected = calculate_program_id(CHILD_CODE_HASH.into(), &salt, Some(msg_id)); @@ -146,7 +150,7 @@ mod tests { let non_existing_code_hash = [10u8; 32]; let salt = b"some_salt"; let payload = CreateProgram::Custom(vec![(non_existing_code_hash, salt.to_vec(), 100_000)]); - let msg_id = factory.send_bytes(10001, payload.encode()); + let msg_id = factory.send_bytes(DEFAULT_USER_ALICE, payload.encode()); let res = sys.run_next_block(); let fictional_program_id = calculate_program_id(non_existing_code_hash.into(), salt, Some(msg_id)); @@ -171,7 +175,7 @@ mod tests { b"some_salt".to_vec(), 100_000, )]); - factory.send_bytes(10001, payload.encode()); + factory.send_bytes(DEFAULT_USER_ALICE, payload.encode()); let _ = sys.run_next_block(); } } diff --git a/examples/reserve-gas/src/lib.rs b/examples/reserve-gas/src/lib.rs index ee335804869..b8c2a80fc67 100644 --- a/examples/reserve-gas/src/lib.rs +++ b/examples/reserve-gas/src/lib.rs @@ -69,7 +69,7 @@ mod wasm; mod tests { use crate::InitAction; use alloc::vec; - use gtest::{Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Program, System}; #[test] fn program_can_be_initialized() { @@ -79,7 +79,7 @@ mod tests { let program = Program::current(&system); let msg_id = program.send( - 0, + DEFAULT_USER_ALICE, InitAction::Normal(vec![ // orphan reservation; will be removed automatically (50_000, 3), diff --git a/examples/signal-entry/src/lib.rs b/examples/signal-entry/src/lib.rs index 67dfbeb6573..1eec1a3dfc9 100644 --- a/examples/signal-entry/src/lib.rs +++ b/examples/signal-entry/src/lib.rs @@ -66,14 +66,14 @@ mod wasm; #[cfg(test)] mod tests { use crate::HandleAction; - use gtest::{Log, Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Log, Program, System}; #[test] fn signal_can_be_sent() { let system = System::new(); system.init_logger(); - let user_id = 42; + let user_id = DEFAULT_USER_ALICE; let program = Program::current(&system); // Initialize program diff --git a/examples/stack-allocations/src/lib.rs b/examples/stack-allocations/src/lib.rs index 371fc252514..a36fad6b8df 100644 --- a/examples/stack-allocations/src/lib.rs +++ b/examples/stack-allocations/src/lib.rs @@ -64,7 +64,7 @@ mod wasm; #[cfg(test)] mod tests { use super::*; - use gtest::{Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Program, System}; use parity_scale_codec::Decode; use rand::{Rng, SeedableRng}; @@ -84,7 +84,7 @@ mod tests { assert!(MAX_ACTIONS_AMOUNT * HANDLE_DATA_SIZE <= 64 * 1024 * 10); } - let from = 42; + let from = DEFAULT_USER_ALICE; let system = System::new(); system.init_logger(); diff --git a/examples/syscall-error/src/lib.rs b/examples/syscall-error/src/lib.rs index 1ca6d9c2a6f..e88ffecfe89 100644 --- a/examples/syscall-error/src/lib.rs +++ b/examples/syscall-error/src/lib.rs @@ -33,7 +33,7 @@ mod wasm; mod tests { extern crate std; - use gtest::{Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Program, System}; #[test] fn program_can_be_initialized() { @@ -42,7 +42,7 @@ mod tests { let program = Program::current(&system); - let msg_id = program.send_bytes(0, b"dummy"); + let msg_id = program.send_bytes(DEFAULT_USER_ALICE, b"dummy"); let res = system.run_next_block(); assert!(res.succeed.contains(&msg_id)); } diff --git a/examples/wait_wake/src/lib.rs b/examples/wait_wake/src/lib.rs index 25daabcfb6f..29f570e9cce 100644 --- a/examples/wait_wake/src/lib.rs +++ b/examples/wait_wake/src/lib.rs @@ -42,7 +42,7 @@ mod tests { extern crate std; use super::Request; - use gtest::{Log, Program, System}; + use gtest::{constants::DEFAULT_USER_ALICE, Log, Program, System}; #[test] fn program_can_be_initialized() { @@ -51,7 +51,7 @@ mod tests { let program = Program::current(&system); - let from = 42; + let from = DEFAULT_USER_ALICE; program.send_bytes(from, b"init"); let res = system.run_next_block(); @@ -64,7 +64,7 @@ mod tests { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program = Program::current(&system); program.send_bytes(from, b"init"); @@ -102,7 +102,7 @@ mod tests { let system = System::new(); system.init_logger(); - let from = 42; + let from = DEFAULT_USER_ALICE; let program_1 = Program::current(&system); program_1.send_bytes(from, b"init"); diff --git a/examples/waiter/tests/mx_lock_access.rs b/examples/waiter/tests/mx_lock_access.rs index f00269db7fe..498426e713f 100644 --- a/examples/waiter/tests/mx_lock_access.rs +++ b/examples/waiter/tests/mx_lock_access.rs @@ -1,8 +1,8 @@ use demo_waiter::{Command, LockContinuation, LockStaticAccessSubcommand, MxLockContinuation}; use gear_core::ids::MessageId; -use gtest::{Program, System}; +use gtest::{constants::DEFAULT_USER_ALICE, Program, System}; -pub const USER_ID: u64 = 10; +pub const USER_ID: u64 = DEFAULT_USER_ALICE; #[test] fn drop_mx_lock_guard_from_different_msg_fails() { diff --git a/examples/waiter/tests/rw_lock_access.rs b/examples/waiter/tests/rw_lock_access.rs index a17d2f8fd30..ff50d9b810c 100644 --- a/examples/waiter/tests/rw_lock_access.rs +++ b/examples/waiter/tests/rw_lock_access.rs @@ -2,9 +2,9 @@ use demo_waiter::{ Command, LockContinuation, LockStaticAccessSubcommand, RwLockContinuation, RwLockType, }; use gear_core::ids::MessageId; -use gtest::{Program, System}; +use gtest::{constants::DEFAULT_USER_ALICE, Program, System}; -pub const USER_ID: u64 = 10; +pub const USER_ID: u64 = DEFAULT_USER_ALICE; #[test] fn drop_r_lock_guard_from_different_msg_fails() { diff --git a/gtest/src/accounts.rs b/gtest/src/accounts.rs new file mode 100644 index 00000000000..f66e8a767ad --- /dev/null +++ b/gtest/src/accounts.rs @@ -0,0 +1,195 @@ +// This file is part of Gear. + +// Copyright (C) 2024 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 . + +//! Accounts storage. + +use std::{cell::RefCell, collections::HashMap, fmt}; + +use crate::{default_users_list, Value, DEFAULT_USERS_INITIAL_BALANCE, EXISTENTIAL_DEPOSIT}; +use gear_common::ProgramId; + +fn init_default_accounts(storage: &mut HashMap) { + for &id in default_users_list() { + let id = id.into(); + storage.insert(id, Balance::new(DEFAULT_USERS_INITIAL_BALANCE)); + } +} + +thread_local! { + static ACCOUNT_STORAGE: RefCell> = RefCell::new({ + let mut storage = HashMap::new(); + init_default_accounts(&mut storage); + storage + }); +} + +#[derive(Debug)] +struct Balance { + amount: Value, +} + +impl Balance { + #[track_caller] + fn new(amount: Value) -> Self { + if amount < EXISTENTIAL_DEPOSIT { + panic!( + "Failed to create balance: the amount {} cannot be lower than the existential deposit", + amount + ); + } + + Self { amount } + } + + fn balance(&self) -> Value { + self.amount + } + + fn reducible_balance(&self) -> Value { + self.amount - EXISTENTIAL_DEPOSIT + } + + fn decrease(&mut self, amount: Value) { + self.amount -= amount; + } + + fn increase(&mut self, amount: Value) { + self.amount += amount; + } +} + +pub(crate) struct Accounts; + +impl Accounts { + // Checks if account by program id exists. + pub(crate) fn exists(id: ProgramId) -> bool { + Self::balance(id) != 0 + } + + // Returns account balance. + pub(crate) fn balance(id: ProgramId) -> Value { + ACCOUNT_STORAGE.with_borrow(|storage| { + storage + .get(&id) + .map(|balance| balance.balance()) + .unwrap_or_default() + }) + } + + // Returns account reducible balance. + pub(crate) fn reducible_balance(id: ProgramId) -> Value { + ACCOUNT_STORAGE.with_borrow(|storage| { + storage + .get(&id) + .map(|balance| balance.reducible_balance()) + .unwrap_or_default() + }) + } + + // Decreases account balance. + pub(crate) fn decrease(id: ProgramId, amount: Value, keep_alive: bool) { + ACCOUNT_STORAGE.with_borrow_mut(|storage| { + if let Some(balance) = storage.get_mut(&id) { + if keep_alive && balance.reducible_balance() < amount { + panic!( + "Not enough balance to decrease, reducible: {}, value: {amount}", + balance.reducible_balance(), + ); + } + if !keep_alive && balance.balance() < amount { + panic!( + "Not enough balance to decrease, total: {}, value: {amount}", + balance.balance(), + ); + } + + balance.decrease(amount); + if balance.balance() < EXISTENTIAL_DEPOSIT { + log::debug!( + "Removing account {id:?} with balance {} below the existential deposit", + balance.balance() + ); + storage.remove(&id); + } + } else { + panic!("Failed to decrease balance for account {id:?}, balance is zero"); + } + }); + } + + // Increases account balance. + pub(crate) fn increase(id: ProgramId, amount: Value) { + ACCOUNT_STORAGE.with_borrow_mut(|storage| { + let balance = storage.get(&id).map(Balance::balance).unwrap_or_default(); + + if balance + amount < EXISTENTIAL_DEPOSIT { + panic!( + "Failed to increase balance: the sum ({}) of the total balance ({}) \ + and the value ({}) cannot be lower than the existential deposit ({EXISTENTIAL_DEPOSIT})", + balance + amount, + balance, + amount + ); + } + + storage + .entry(id) + .and_modify(|balance| balance.increase(amount)) + .or_insert_with(|| Balance::new(amount)); + }); + } + + // Transfers value between accounts. + pub(crate) fn transfer(from: ProgramId, to: ProgramId, amount: Value, keep_alive: bool) { + Self::decrease(from, amount, keep_alive); + Self::increase(to, amount); + } + + // Overrides account balance. + pub(crate) fn override_balance(id: ProgramId, amount: Value) { + if amount < EXISTENTIAL_DEPOSIT { + panic!( + "Failed to override balance: the amount {} cannot be lower than the existential deposit", + amount + ); + } + + ACCOUNT_STORAGE.with_borrow_mut(|storage| { + storage.insert(id, Balance::new(amount)); + }); + } + + // Checks if value can be deposited to account. + pub(crate) fn can_deposit(id: ProgramId, amount: Value) -> bool { + Accounts::balance(id) + amount >= EXISTENTIAL_DEPOSIT + } + + // Clears accounts storage. + pub(crate) fn clear() { + ACCOUNT_STORAGE.with_borrow_mut(|storage| { + storage.clear(); + init_default_accounts(storage); + }); + } +} + +impl fmt::Debug for Accounts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ACCOUNT_STORAGE.with_borrow(|storage| f.debug_map().entries(storage.iter()).finish()) + } +} diff --git a/gtest/src/actors.rs b/gtest/src/actors.rs new file mode 100644 index 00000000000..39881e4b89a --- /dev/null +++ b/gtest/src/actors.rs @@ -0,0 +1,227 @@ +// This file is part of Gear. + +// Copyright (C) 2024 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 . + +//! Actors storage. + +use std::{cell::RefCell, collections::BTreeMap, fmt}; + +use core_processor::common::ExecutableActorData; +use gear_common::{CodeId, GearPage, MessageId, PageBuf, ProgramId}; +use gear_core::{ + code::InstrumentedCode, + pages::{numerated::tree::IntervalsTree, WasmPage}, + reservation::GasReservationMap, +}; + +use crate::WasmProgram; + +thread_local! { + static ACTORS_STORAGE: RefCell> = RefCell::new(Default::default()); +} + +pub(crate) struct Actors; + +impl Actors { + // Accesses actor by program id. + #[track_caller] + pub(crate) fn access( + program_id: ProgramId, + access: impl FnOnce(Option<&TestActor>) -> R, + ) -> R { + ACTORS_STORAGE.with_borrow(|storage| access(storage.get(&program_id))) + } + + // Modifies actor by program id. + #[track_caller] + pub(crate) fn modify( + program_id: ProgramId, + modify: impl FnOnce(Option<&mut TestActor>) -> R, + ) -> R { + ACTORS_STORAGE.with_borrow_mut(|storage| modify(storage.get_mut(&program_id))) + } + + // Inserts actor by program id. + pub(crate) fn insert(program_id: ProgramId, actor: TestActor) -> Option { + ACTORS_STORAGE.with_borrow_mut(|storage| storage.insert(program_id, actor)) + } + + // Checks if actor by program id exists. + pub(crate) fn contains_key(program_id: ProgramId) -> bool { + ACTORS_STORAGE.with_borrow(|storage| storage.contains_key(&program_id)) + } + + // Checks if actor by program id is a user. + pub(crate) fn is_user(id: ProgramId) -> bool { + // Non-existent program is a user + ACTORS_STORAGE.with_borrow(|storage| storage.get(&id).is_none()) + } + + // Checks if actor by program id is active. + pub(crate) fn is_active_program(id: ProgramId) -> bool { + ACTORS_STORAGE.with_borrow(|storage| { + matches!( + storage.get(&id), + Some(TestActor::Initialized(_) | TestActor::Uninitialized(_, _)) + ) + }) + } + + // Checks if actor by program id is a program. + pub(crate) fn is_program(id: ProgramId) -> bool { + // if it's not a user, then it's a program + !Self::is_user(id) + } + + // Returns all program ids. + pub(crate) fn program_ids() -> Vec { + ACTORS_STORAGE.with_borrow(|storage| storage.keys().copied().collect()) + } + + // Clears actors storage. + pub(crate) fn clear() { + ACTORS_STORAGE.with_borrow_mut(|storage| storage.clear()) + } +} + +impl fmt::Debug for Actors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ACTORS_STORAGE.with_borrow(|storage| f.debug_map().entries(storage.iter()).finish()) + } +} + +#[derive(Debug)] +pub(crate) enum TestActor { + Initialized(Program), + // Contract: program is always `Some`, option is used to take ownership + Uninitialized(Option, Option), + Dormant, +} + +impl TestActor { + // Creates a new uninitialized actor. + pub(crate) fn new(init_message_id: Option, program: Program) -> Self { + TestActor::Uninitialized(init_message_id, Some(program)) + } + + // # Panics + // If actor is initialized or dormant + #[track_caller] + pub(crate) fn set_initialized(&mut self) { + assert!( + self.is_uninitialized(), + "can't transmute actor, which isn't uninitialized" + ); + + if let TestActor::Uninitialized(_, maybe_prog) = self { + *self = TestActor::Initialized( + maybe_prog + .take() + .expect("actor storage contains only `Some` values by contract"), + ); + } + } + + // Checks if actor is dormant. + pub(crate) fn is_dormant(&self) -> bool { + matches!(self, TestActor::Dormant) + } + + // Checks if actor is initialized. + pub(crate) fn is_uninitialized(&self) -> bool { + matches!(self, TestActor::Uninitialized(..)) + } + + // Returns `Some` if actor contains genuine program. + pub(crate) fn genuine_program(&self) -> Option<&GenuineProgram> { + match self { + TestActor::Initialized(Program::Genuine(program)) + | TestActor::Uninitialized(_, Some(Program::Genuine(program))) => Some(program), + _ => None, + } + } + + // Returns `Some` if actor contains genuine program but mutable. + pub(crate) fn genuine_program_mut(&mut self) -> Option<&mut GenuineProgram> { + match self { + TestActor::Initialized(Program::Genuine(program)) + | TestActor::Uninitialized(_, Some(Program::Genuine(program))) => Some(program), + _ => None, + } + } + + // Returns pages data of genuine program. + pub(crate) fn get_pages_data(&self) -> Option<&BTreeMap> { + self.genuine_program().map(|program| &program.pages_data) + } + + // Returns pages data of genuine program but mutable. + pub(crate) fn get_pages_data_mut(&mut self) -> Option<&mut BTreeMap> { + self.genuine_program_mut() + .map(|program| &mut program.pages_data) + } + + // Takes ownership over mock program, putting `None` value instead of it. + pub(crate) fn take_mock(&mut self) -> Option> { + match self { + TestActor::Initialized(Program::Mock(mock)) + | TestActor::Uninitialized(_, Some(Program::Mock(mock))) => mock.take(), + _ => None, + } + } + + // Gets a new executable actor derived from the inner program. + pub(crate) fn get_executable_actor_data( + &self, + ) -> Option<(ExecutableActorData, InstrumentedCode)> { + self.genuine_program().map(|program| { + ( + ExecutableActorData { + allocations: program.allocations.clone(), + code_id: program.code_id, + code_exports: program.code.exports().clone(), + static_pages: program.code.static_pages(), + gas_reservation_map: program.gas_reservation_map.clone(), + memory_infix: Default::default(), + }, + program.code.clone(), + ) + }) + } +} + +#[derive(Debug)] +pub(crate) struct GenuineProgram { + pub code_id: CodeId, + pub code: InstrumentedCode, + pub allocations: IntervalsTree, + pub pages_data: BTreeMap, + pub gas_reservation_map: GasReservationMap, +} + +#[derive(Debug)] +pub(crate) enum Program { + Genuine(GenuineProgram), + // Contract: is always `Some`, option is used to take ownership + Mock(Option>), +} + +impl Program { + pub(crate) fn new_mock(mock: impl WasmProgram + 'static) -> Self { + Program::Mock(Some(Box::new(mock))) + } +} diff --git a/gtest/src/bank.rs b/gtest/src/bank.rs new file mode 100644 index 00000000000..20c555be1e7 --- /dev/null +++ b/gtest/src/bank.rs @@ -0,0 +1,115 @@ +// This file is part of Gear. + +// Copyright (C) 2024 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 . + +//! `gtest` bank + +use std::collections::HashMap; + +use gear_common::{Gas, GasMultiplier, ProgramId}; + +use crate::{accounts::Accounts, constants::Value, GAS_MULTIPLIER}; + +#[derive(Default, Debug)] +struct BankBalance { + gas: Value, + value: Value, +} + +/// `gtest` bank. +#[derive(Default, Debug)] +pub(crate) struct Bank { + accounts: HashMap, +} + +impl Bank { + // Create a new bank. + #[track_caller] + pub(crate) fn deposit_value(&mut self, id: ProgramId, value: Value, keep_alive: bool) { + Accounts::decrease(id, value, keep_alive); + self.accounts + .entry(id) + .or_insert(BankBalance { gas: 0, value: 0 }) + .value += value; + } + + // Deposit gas. + #[track_caller] + pub(crate) fn deposit_gas(&mut self, id: ProgramId, gas: Gas, keep_alive: bool) { + let gas_value = GAS_MULTIPLIER.gas_to_value(gas); + Accounts::decrease(id, gas_value, keep_alive); + self.accounts + .entry(id) + .or_insert(BankBalance { gas: 0, value: 0 }) + .gas += gas_value; + } + + // Withdraw gas. + #[track_caller] + pub(crate) fn spend_gas( + &mut self, + id: ProgramId, + gas: Gas, + multiplier: GasMultiplier, + ) { + let gas_value = multiplier.gas_to_value(gas); + self.accounts + .get_mut(&id) + .unwrap_or_else(|| panic!("Bank::spend_gas: actor id {id:?} not found in bank")) + .gas -= gas_value; + } + + // Withdraw gas. + #[track_caller] + pub(crate) fn withdraw_gas( + &mut self, + id: ProgramId, + gas_left: Gas, + multiplier: GasMultiplier, + ) { + let gas_left_value = multiplier.gas_to_value(gas_left); + self.accounts + .get_mut(&id) + .unwrap_or_else(|| panic!("Bank::withdraw_gas: actor id {id:?} not found in bank")) + .gas -= gas_left_value; + + if !Accounts::can_deposit(id, gas_left_value) { + // Unable to deposit value to account. + // In this case unused value will be lost. + return; + } + + Accounts::increase(id, gas_left_value); + } + + // Transfer value. + #[track_caller] + pub(crate) fn transfer_value(&mut self, from: ProgramId, to: ProgramId, value: Value) { + self.accounts + .get_mut(&from) + .unwrap_or_else(|| panic!("Bank::transfer_value: actor id {from:?} not found in bank")) + .value -= value; + + if !Accounts::can_deposit(to, value) { + // Unable to deposit value to account. + // In this case unused value will be lost. + return; + } + + Accounts::increase(to, value); + } +} diff --git a/gtest/src/gas_tree.rs b/gtest/src/gas_tree.rs index 8e10d5d7cba..ed117087ed3 100644 --- a/gtest/src/gas_tree.rs +++ b/gtest/src/gas_tree.rs @@ -22,12 +22,17 @@ use crate::GAS_MULTIPLIER; use gear_common::{ auxiliary::gas_provider::{AuxiliaryGasProvider, GasTreeError, PlainNodeId}, gas_provider::{ConsumeResultOf, GasNodeId, Provider, ReservableTree, Tree}, - Gas, Origin, + Gas, GasMultiplier, Origin, }; use gear_core::ids::{MessageId, ProgramId, ReservationId}; pub(crate) type PositiveImbalance = ::PositiveImbalance; pub(crate) type NegativeImbalance = ::NegativeImbalance; +pub type OriginNodeDataOf = ( + ::ExternalOrigin, + GasMultiplier<::Funds, ::Balance>, + ::NodeId, +); type GasTree = ::GasTree; /// Gas tree manager which operates under the hood over @@ -179,4 +184,13 @@ impl GasTreeManager { pub(crate) fn system_reserve(&self, key: MessageId, amount: Gas) -> Result<(), GasTreeError> { GasTree::system_reserve(GasNodeId::from(key.cast::()), amount) } + + /// The id of node, external origin and funds multiplier for a key. + /// + /// Error occurs if the tree is invalidated (has "orphan" nodes), and the + /// node identified by the `key` belongs to a subtree originating at + /// such "orphan" node, or in case of inexistent key. + pub(crate) fn get_origin_node(&self, key: MessageId) -> Result { + GasTree::get_origin_node(GasNodeId::from(key.cast::())) + } } diff --git a/gtest/src/lib.rs b/gtest/src/lib.rs index 7903cfa4723..ab50524c030 100644 --- a/gtest/src/lib.rs +++ b/gtest/src/lib.rs @@ -113,6 +113,9 @@ //! mod tests { //! use gtest::{Log, Program, System}; //! +//! // Alternatively, you can use the default users from `gtest::constants`: +//! // `DEFAULT_USER_ALICE`, `DEFAULT_USER_BOB`, `DEFAULT_USER_CHARLIE`, `DEFAULT_USER_EVE`. +//! // The full list of default users can be obtained with `gtest::constants::default_users_list`. //! const USER_ID: u64 = 100001; //! //! #[test] @@ -123,6 +126,9 @@ //! // Initialization of the current program structure. //! let prog = Program::current(&sys); //! +//! // Provide user with some balance. +//! sys.min_to(USER_ID, EXISTENTIAL_DEPOSIT * 1000); +//! //! // Send an init message to the program. //! let init_message_id = prog.send_bytes(USER_ID, b"Doesn't matter"); //! @@ -251,6 +257,30 @@ //! RUST_LOG="target_1=logging_level,target_2=logging_level" cargo test //! ``` //! +//! ## Pre-requisites for sending a message +//! +//! Prior to sending a message, it is necessary to mint sufficient balance for +//! the sender to ensure coverage of the existential deposit and gas costs. +//! +//! ```no_run +//! # use gtest::constants::EXISTENTIAL_DEPOSIT; +//! # let sys = gtest::System::new(); +//! let user_id = 42; +//! sys.mint_to(user_id, EXISTENTIAL_DEPOSIT * 1000); +//! ``` +//! +//! Alternatively, you can use the default users from `gtest::constants`, which +//! have a preallocated balance, as the message sender. +//! +//! ```no_run +//! # use gtest::constants::DEFAULT_USERS_INITIAL_BALANCE; +//! # let sys = gtest::System::new(); +//! assert_eq!( +//! sys.balance_of(gtest::constants::DEFAULT_USER_ALICE), +//! DEFAULT_USERS_INITIAL_BALANCE +//! ); +//! ``` +//! //! ## Sending messages //! //! To send message to the program need to call one of two program's functions: @@ -375,7 +405,25 @@ //! changes the timestamp. If you write time dependent logic, you should spend //! blocks manually. //! -//! ## Balance: +//! ## Balance +//! +//! There are certain invariants in `gtest` regarding user and program balances: +//! +//! * For a user to successfully send a message to the program, they must have +//! sufficient balance to cover the existential deposit and gas costs. +//! * The program charges the existential deposit from the user upon receiving +//! the initial message. +//! +//! As previously mentioned [here](#Pre-requisites-for-Sending-a-Message), +//! a balance for the user must be minted before sending a message. This balance +//! should be sufficient to cover the following: the user's existential deposit, +//! the existential deposit of the initialized program (the first message to the +//! program charges the program's existential deposit from the sender), and the +//! message's gas costs. +//! +//! The [`System::mint_to`] method can be utilized to allocate a balance to the +//! user or the program. The [`System::balance_of`] method may be used to verify +//! the current balance. //! //! ```no_run //! # use gtest::Program; @@ -385,9 +433,9 @@ //! sys.mint_to(user_id, 5000); //! assert_eq!(sys.balance_of(user_id), 5000); //! -//! // To give the balance to the program you should use `mint` method: +//! // To give the balance to the program you should use [`System::transfer`] method: //! let mut prog = Program::current(&sys); -//! prog.mint(1000); +//! sys.transfer(user_id, prog.id(), 1000, true); //! assert_eq!(prog.balance(), 1000); //! ``` //! @@ -444,6 +492,9 @@ #![doc(html_logo_url = "https://docs.gear.rs/logo.svg")] #![doc(html_favicon_url = "https://gear-tech.io/favicons/favicon.ico")] +mod accounts; +mod actors; +mod bank; mod blocks; mod error; mod gas_tree; @@ -464,6 +515,7 @@ pub use program::{ }; pub use system::System; +pub use constants::Value; pub(crate) use constants::*; /// Module containing constants of Gear protocol. @@ -583,4 +635,28 @@ pub mod constants { pub const HOST_FUNC_WRITE_AFTER_READ_COST: Gas = 115129057; /// Loading page data from storage cost. pub const LOAD_PAGE_STORAGE_DATA_COST: Gas = 10630903; + + /* Default users constants with initial balance */ + + /// Default user id for Alice. + pub const DEFAULT_USER_ALICE: u64 = u64::MAX - 1; + /// Default user id for Bob. + pub const DEFAULT_USER_BOB: u64 = u64::MAX - 2; + /// Default user id for Charlie. + pub const DEFAULT_USER_CHARLIE: u64 = u64::MAX - 3; + /// Default user id for Eve. + pub const DEFAULT_USER_EVE: u64 = u64::MAX - 4; + + /// Default list of users. + pub const fn default_users_list() -> &'static [u64] { + &[ + DEFAULT_USER_ALICE, + DEFAULT_USER_BOB, + DEFAULT_USER_CHARLIE, + DEFAULT_USER_EVE, + ] + } + + /// Default initial balance for users. + pub const DEFAULT_USERS_INITIAL_BALANCE: Value = EXISTENTIAL_DEPOSIT * 100_000; } diff --git a/gtest/src/log.rs b/gtest/src/log.rs index 29824cfb8e0..7250cb0794d 100644 --- a/gtest/src/log.rs +++ b/gtest/src/log.rs @@ -16,7 +16,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::program::{Gas, ProgramIdWrapper}; +use crate::{ + program::{Gas, ProgramIdWrapper}, + Value, GAS_MULTIPLIER, +}; use codec::{Codec, Encode}; use core_processor::configs::BlockInfo; use gear_core::{ @@ -447,6 +450,16 @@ impl BlockRunResult { ); } + /// Calculate the total spent value. + pub fn spent_value(&self) -> Value { + let spent_gas = self + .gas_burned + .values() + .fold(Gas::zero(), |acc, &x| acc.saturating_add(x)); + + GAS_MULTIPLIER.gas_to_value(spent_gas.0) + } + /// Trying to get the panic log. fn message_panic_log(&self, message_id: MessageId) -> Option<&CoreLog> { let msg_log = self diff --git a/gtest/src/mailbox/actor.rs b/gtest/src/mailbox/actor.rs index 9b051ce9f61..71de187759e 100644 --- a/gtest/src/mailbox/actor.rs +++ b/gtest/src/mailbox/actor.rs @@ -127,7 +127,7 @@ impl<'a> ActorMailbox<'a> { #[cfg(test)] mod tests { - use crate::{Log, Program, System, EXISTENTIAL_DEPOSIT}; + use crate::{Log, Program, System, DEFAULT_USER_ALICE, EXISTENTIAL_DEPOSIT}; use codec::Encode; use demo_constructor::{Call, Calls, Scheme, WASM_BINARY}; use gear_core::ids::ProgramId; @@ -135,7 +135,7 @@ mod tests { fn prepare_program(system: &System) -> (Program<'_>, ([u8; 32], Vec, Log)) { let program = Program::from_binary_with_id(system, 121, WASM_BINARY); - let sender = ProgramId::from(42).into_bytes(); + let sender = ProgramId::from(DEFAULT_USER_ALICE).into_bytes(); let payload = b"sup!".to_vec(); let log = Log::builder().dest(sender).payload_bytes(payload.clone()); @@ -151,8 +151,7 @@ mod tests { let system = System::new(); let (program, (sender, payload, log)) = prepare_program(&system); - let original_balance = 20 * EXISTENTIAL_DEPOSIT; - system.mint_to(sender, original_balance); + let original_balance = system.balance_of(sender); let value_send = 2 * EXISTENTIAL_DEPOSIT; let handle = Calls::builder().send_value(sender, payload, value_send); @@ -160,12 +159,18 @@ mod tests { let res = system.run_next_block(); assert!(res.succeed.contains(&msg_id)); assert!(res.contains(&log)); - assert_eq!(system.balance_of(sender), original_balance - value_send); + assert_eq!( + system.balance_of(sender), + original_balance - value_send - res.spent_value() + ); let mailbox = system.get_mailbox(sender); assert!(mailbox.contains(&log)); assert!(mailbox.claim_value(log).is_ok()); - assert_eq!(system.balance_of(sender), original_balance); + assert_eq!( + system.balance_of(sender), + original_balance - res.spent_value() + ); } #[test] diff --git a/gtest/src/manager.rs b/gtest/src/manager.rs index 8414cb18edb..6485de30ecd 100644 --- a/gtest/src/manager.rs +++ b/gtest/src/manager.rs @@ -20,22 +20,26 @@ mod journal; mod task; use crate::{ + accounts::Accounts, + actors::{Actors, GenuineProgram, Program, TestActor}, + bank::Bank, blocks::BlocksManager, + constants::Value, gas_tree::GasTreeManager, log::{BlockRunResult, CoreLog}, mailbox::MailboxManager, program::{Gas, WasmProgram}, task_pool::TaskPoolManager, Result, TestError, DISPATCH_HOLD_COST, EPOCH_DURATION_IN_BLOCKS, EXISTENTIAL_DEPOSIT, - GAS_ALLOWANCE, HOST_FUNC_READ_COST, HOST_FUNC_WRITE_AFTER_READ_COST, HOST_FUNC_WRITE_COST, - INITIAL_RANDOM_SEED, LOAD_ALLOCATIONS_PER_INTERVAL, LOAD_PAGE_STORAGE_DATA_COST, - MAILBOX_THRESHOLD, MAX_RESERVATIONS, MODULE_CODE_SECTION_INSTANTIATION_BYTE_COST, - MODULE_DATA_SECTION_INSTANTIATION_BYTE_COST, MODULE_ELEMENT_SECTION_INSTANTIATION_BYTE_COST, - MODULE_GLOBAL_SECTION_INSTANTIATION_BYTE_COST, MODULE_INSTRUMENTATION_BYTE_COST, - MODULE_INSTRUMENTATION_COST, MODULE_TABLE_SECTION_INSTANTIATION_BYTE_COST, - MODULE_TYPE_SECTION_INSTANTIATION_BYTE_COST, READ_COST, READ_PER_BYTE_COST, RESERVATION_COST, - RESERVE_FOR, SIGNAL_READ_COST, SIGNAL_WRITE_AFTER_READ_COST, SIGNAL_WRITE_COST, VALUE_PER_GAS, - WAITLIST_COST, WRITE_COST, + GAS_ALLOWANCE, GAS_MULTIPLIER, HOST_FUNC_READ_COST, HOST_FUNC_WRITE_AFTER_READ_COST, + HOST_FUNC_WRITE_COST, INITIAL_RANDOM_SEED, LOAD_ALLOCATIONS_PER_INTERVAL, + LOAD_PAGE_STORAGE_DATA_COST, MAILBOX_THRESHOLD, MAX_RESERVATIONS, + MODULE_CODE_SECTION_INSTANTIATION_BYTE_COST, MODULE_DATA_SECTION_INSTANTIATION_BYTE_COST, + MODULE_ELEMENT_SECTION_INSTANTIATION_BYTE_COST, MODULE_GLOBAL_SECTION_INSTANTIATION_BYTE_COST, + MODULE_INSTRUMENTATION_BYTE_COST, MODULE_INSTRUMENTATION_COST, + MODULE_TABLE_SECTION_INSTANTIATION_BYTE_COST, MODULE_TYPE_SECTION_INSTANTIATION_BYTE_COST, + READ_COST, READ_PER_BYTE_COST, RESERVATION_COST, RESERVE_FOR, SIGNAL_READ_COST, + SIGNAL_WRITE_AFTER_READ_COST, SIGNAL_WRITE_COST, VALUE_PER_GAS, WAITLIST_COST, WRITE_COST, }; use core_processor::{ common::*, @@ -53,178 +57,21 @@ use gear_core::{ ids::{prelude::*, CodeId, MessageId, ProgramId, ReservationId}, memory::PageBuf, message::{Dispatch, DispatchKind, ReplyMessage, ReplyPacket, StoredDispatch, StoredMessage}, - pages::{numerated::tree::IntervalsTree, GearPage, WasmPage}, - reservation::GasReservationMap, + pages::GearPage, }; use gear_core_errors::{ErrorReplyReason, SimpleExecutionError}; use gear_lazy_pages_common::LazyPagesCosts; use gear_lazy_pages_native_interface::LazyPagesNative; use rand::{rngs::StdRng, RngCore, SeedableRng}; use std::{ - cell::{Ref, RefCell, RefMut}, collections::{BTreeMap, BTreeSet, HashMap, VecDeque}, convert::TryInto, mem, - rc::Rc, }; const OUTGOING_LIMIT: u32 = 1024; const OUTGOING_BYTES_LIMIT: u32 = 64 * 1024 * 1024; -pub(crate) type Balance = u128; - -#[derive(Debug)] -pub(crate) enum TestActor { - Initialized(Program), - // Contract: program is always `Some`, option is used to take ownership - Uninitialized(Option, Option), - Dormant, - User, -} - -impl TestActor { - fn new(init_message_id: Option, program: Program) -> Self { - TestActor::Uninitialized(init_message_id, Some(program)) - } - - // # Panics - // If actor is initialized or dormant - #[track_caller] - fn set_initialized(&mut self) { - assert!( - self.is_uninitialized(), - "can't transmute actor, which isn't uninitialized" - ); - - if let TestActor::Uninitialized(_, maybe_prog) = self { - *self = TestActor::Initialized( - maybe_prog - .take() - .expect("actor storage contains only `Some` values by contract"), - ); - } - } - - fn is_dormant(&self) -> bool { - matches!(self, TestActor::Dormant) - } - - fn is_uninitialized(&self) -> bool { - matches!(self, TestActor::Uninitialized(..)) - } - - pub(crate) fn genuine_program(&self) -> Option<&GenuineProgram> { - match self { - TestActor::Initialized(Program::Genuine(program)) - | TestActor::Uninitialized(_, Some(Program::Genuine(program))) => Some(program), - _ => None, - } - } - - fn genuine_program_mut(&mut self) -> Option<&mut GenuineProgram> { - match self { - TestActor::Initialized(Program::Genuine(program)) - | TestActor::Uninitialized(_, Some(Program::Genuine(program))) => Some(program), - _ => None, - } - } - - pub fn get_pages_data(&self) -> Option<&BTreeMap> { - self.genuine_program().map(|program| &program.pages_data) - } - - fn get_pages_data_mut(&mut self) -> Option<&mut BTreeMap> { - self.genuine_program_mut() - .map(|program| &mut program.pages_data) - } - - // Takes ownership over mock program, putting `None` value instead of it. - fn take_mock(&mut self) -> Option> { - match self { - TestActor::Initialized(Program::Mock(mock)) - | TestActor::Uninitialized(_, Some(Program::Mock(mock))) => mock.take(), - _ => None, - } - } - - // Gets a new executable actor derived from the inner program. - fn get_executable_actor_data(&self) -> Option<(ExecutableActorData, InstrumentedCode)> { - self.genuine_program().map(|program| { - ( - ExecutableActorData { - allocations: program.allocations.clone(), - code_id: program.code_id, - code_exports: program.code.exports().clone(), - static_pages: program.code.static_pages(), - gas_reservation_map: program.gas_reservation_map.clone(), - memory_infix: Default::default(), - }, - program.code.clone(), - ) - }) - } -} - -#[derive(Debug)] -pub(crate) struct GenuineProgram { - pub code_id: CodeId, - pub code: InstrumentedCode, - pub allocations: IntervalsTree, - pub pages_data: BTreeMap, - pub gas_reservation_map: GasReservationMap, -} - -#[derive(Debug)] -pub(crate) enum Program { - Genuine(GenuineProgram), - // Contract: is always `Some`, option is used to take ownership - Mock(Option>), -} - -impl Program { - pub(crate) fn new_mock(mock: impl WasmProgram + 'static) -> Self { - Program::Mock(Some(Box::new(mock))) - } -} - -#[derive(Default, Debug, Clone)] -pub(crate) struct Actors(Rc>>); - -impl Actors { - pub fn borrow(&self) -> Ref<'_, BTreeMap> { - self.0.borrow() - } - - pub fn borrow_mut(&mut self) -> RefMut<'_, BTreeMap> { - self.0.borrow_mut() - } - - fn insert( - &mut self, - program_id: ProgramId, - actor_and_balance: (TestActor, Balance), - ) -> Option<(TestActor, Balance)> { - self.0.borrow_mut().insert(program_id, actor_and_balance) - } - - pub fn contains_key(&self, program_id: &ProgramId) -> bool { - self.0.borrow().contains_key(program_id) - } - - fn remove(&mut self, program_id: &ProgramId) -> Option<(TestActor, Balance)> { - self.0.borrow_mut().remove(program_id) - } -} - -/// Simple boolean for whether an account needs to be kept in existence. -#[derive(PartialEq)] -pub(crate) enum MintMode { - /// Operation must not result in the account going out of existence. - KeepAlive, - /// Operation may result in account going out of existence. - AllowDeath, -} - #[derive(Debug, Default)] pub(crate) struct ExtManager { // State metadata @@ -236,7 +83,7 @@ pub(crate) struct ExtManager { pub(crate) id_nonce: u64, // State - pub(crate) actors: Actors, + pub(crate) bank: Bank, pub(crate) opt_binaries: BTreeMap>, pub(crate) meta_binaries: BTreeMap>, pub(crate) dispatches: VecDeque, @@ -283,12 +130,11 @@ impl ExtManager { program_id: ProgramId, program: Program, init_message_id: Option, - ) -> Option<(TestActor, Balance)> { + ) -> Option { if let Program::Genuine(GenuineProgram { code, .. }) = &program { self.store_new_code(code.code().to_vec()); } - self.actors - .insert(program_id, (TestActor::new(init_message_id, program), 0)) + Actors::insert(program_id, TestActor::new(init_message_id, program)) } pub(crate) fn store_new_code(&mut self, code: Vec) -> CodeId { @@ -308,7 +154,7 @@ impl ExtManager { } pub(crate) fn free_id_nonce(&mut self) -> u64 { - while self.actors.contains_key(&self.id_nonce.into()) { + while Actors::contains_key(self.id_nonce.into()) { self.id_nonce += 1; } self.id_nonce @@ -317,7 +163,7 @@ impl ExtManager { /// Insert message into the delayed queue. fn send_delayed_dispatch(&mut self, dispatch: Dispatch, delay: u32) { let message_id = dispatch.id(); - let task = if self.is_program(&dispatch.destination()) { + let task = if Actors::is_program(dispatch.destination()) { ScheduledTask::SendDispatch(message_id) } else { // TODO #4122, `to_mailbox` must be counted from provided gas @@ -357,19 +203,16 @@ impl ExtManager { program_id: &ProgramId, memory_pages: BTreeMap, ) { - let mut actors = self.actors.borrow_mut(); - let program = &mut actors - .get_mut(program_id) - .unwrap_or_else(|| panic!("Actor {program_id} not found")) - .0; - - let pages_data = program - .get_pages_data_mut() - .expect("No pages data found for program"); - - for (page, buf) in memory_pages { - pages_data.insert(page, buf); - } + Actors::modify(*program_id, |actor| { + let pages_data = actor + .unwrap_or_else(|| panic!("Actor id {program_id:?} not found")) + .get_pages_data_mut() + .expect("No pages data found for program"); + + for (page, buf) in memory_pages { + pages_data.insert(page, buf); + } + }); } pub(crate) fn validate_and_route_dispatch(&mut self, dispatch: Dispatch) -> MessageId { @@ -385,7 +228,7 @@ impl ExtManager { pub(crate) fn route_dispatch(&mut self, dispatch: Dispatch) -> MessageId { let stored_dispatch = dispatch.into_stored(); - if self.is_user(&stored_dispatch.destination()) { + if Actors::is_user(stored_dispatch.destination()) { panic!("Program API only sends message to programs.") } @@ -439,23 +282,33 @@ impl ExtManager { None => break, }; - let mut actors = self.actors.borrow_mut(); - let (actor, balance) = actors - .get_mut(&dispatch.destination()) - .expect("Somehow message queue contains message for user"); - let balance = *balance; - - if actor.is_dormant() { - drop(actors); - self.process_dormant(balance, dispatch); - } else if let Some((data, code)) = actor.get_executable_actor_data() { - drop(actors); - self.process_normal(balance, data, code, dispatch); - } else if let Some(mock) = actor.take_mock() { - drop(actors); - self.process_mock(mock, dispatch); - } else { - unreachable!(); + enum DispatchCase { + Dormant, + Normal(ExecutableActorData, InstrumentedCode), + Mock(Box), + } + + let dispatch_case = Actors::modify(dispatch.destination(), |actor| { + let actor = actor + .unwrap_or_else(|| panic!("Somehow message queue contains message for user")); + if actor.is_dormant() { + DispatchCase::Dormant + } else if let Some((data, code)) = actor.get_executable_actor_data() { + DispatchCase::Normal(data, code) + } else if let Some(mock) = actor.take_mock() { + DispatchCase::Mock(mock) + } else { + unreachable!(); + } + }); + let balance = Accounts::reducible_balance(dispatch.destination()); + + match dispatch_case { + DispatchCase::Dormant => self.process_dormant(balance, dispatch), + DispatchCase::Normal(data, code) => { + self.process_normal(balance, data, code, dispatch) + } + DispatchCase::Mock(mock) => self.process_mock(mock, dispatch), } total_processed += 1; @@ -466,29 +319,53 @@ impl ExtManager { #[track_caller] fn validate_dispatch(&mut self, dispatch: &Dispatch) { - if self.is_program(&dispatch.source()) { + let source = dispatch.source(); + let destination = dispatch.destination(); + + if Actors::is_program(source) { panic!("Sending messages allowed only from users id"); } - let mut actors = self.actors.borrow_mut(); - let (_, balance) = actors - .entry(dispatch.source()) - .or_insert((TestActor::User, 0)); + // User must exist + if !Accounts::exists(source) { + panic!("User's {source} balance is zero; mint value to it first."); + } + + let is_init_msg = dispatch.kind().is_init(); + // We charge ED only for init messages + let maybe_ed = if is_init_msg { EXISTENTIAL_DEPOSIT } else { 0 }; + let balance = Accounts::balance(source); + + let gas_limit = dispatch + .gas_limit() + .unwrap_or_else(|| unreachable!("message from program API always has gas")); + let gas_value = GAS_MULTIPLIER.gas_to_value(gas_limit); - if *balance < dispatch.value() { + // Check sender has enough balance to cover dispatch costs + if balance < { dispatch.value() + gas_value + maybe_ed } { panic!( - "Insufficient value: user ({}) tries to send \ - ({}) value, while his balance ({})", - dispatch.source(), + "Insufficient balance: user ({}) tries to send \ + ({}) value, ({}) gas and ED ({}), while his balance ({:?})", + source, dispatch.value(), - balance + gas_value, + maybe_ed, + balance, ); - } else { - *balance -= dispatch.value(); - if *balance < crate::EXISTENTIAL_DEPOSIT { - *balance = 0; - } } + + // Charge for program ED upon creation + if is_init_msg { + Accounts::transfer(source, destination, EXISTENTIAL_DEPOSIT, false); + } + + if dispatch.value() != 0 { + // Deposit message value + self.bank.deposit_value(source, dispatch.value(), false); + } + + // Deposit gas + self.bank.deposit_gas(source, gas_limit, false); } /// Call non-void meta function from actor stored in manager. @@ -498,14 +375,15 @@ impl ExtManager { payload: Vec, program_id: &ProgramId, ) -> Result> { - let mut actors = self.actors.borrow_mut(); - let (actor, _balance) = actors - .get_mut(program_id) - .ok_or_else(|| TestError::ActorNotFound(*program_id))?; - - if let Some((data, code)) = actor.get_executable_actor_data() { - drop(actors); + let executable_actor_data = Actors::modify(*program_id, |actor| { + if let Some(actor) = actor { + Ok(actor.get_executable_actor_data()) + } else { + Err(TestError::ActorNotFound(*program_id)) + } + })?; + if let Some((data, code)) = executable_actor_data { core_processor::informational::execute_for_reply::, _>( String::from("state"), code, @@ -516,7 +394,9 @@ impl ExtManager { self.blocks_manager.get(), ) .map_err(TestError::ReadStateError) - } else if let Some(mut program_mock) = actor.take_mock() { + } else if let Some(mut program_mock) = Actors::modify(*program_id, |actor| { + actor.expect("Checked before").take_mock() + }) { program_mock .state() .map_err(|err| TestError::ReadStateError(err.into())) @@ -559,49 +439,12 @@ impl ExtManager { .map_err(TestError::ReadStateError) } - pub(crate) fn is_user(&self, id: &ProgramId) -> bool { - matches!( - self.actors.borrow().get(id), - Some((TestActor::User, _)) | None - ) - } - - pub(crate) fn is_active_program(&self, id: &ProgramId) -> bool { - matches!( - self.actors.borrow().get(id), - Some((TestActor::Initialized(_), _)) | Some((TestActor::Uninitialized(_, _), _)) - ) + pub(crate) fn mint_to(&mut self, id: &ProgramId, value: Value) { + Accounts::increase(*id, value); } - pub(crate) fn is_program(&self, id: &ProgramId) -> bool { - matches!( - self.actors.borrow().get(id), - Some((TestActor::Initialized(_), _)) - | Some((TestActor::Uninitialized(_, _), _)) - | Some((TestActor::Dormant, _)) - ) - } - - pub(crate) fn mint_to(&mut self, id: &ProgramId, value: Balance, mint_mode: MintMode) { - if mint_mode == MintMode::KeepAlive && value < crate::EXISTENTIAL_DEPOSIT { - panic!( - "An attempt to mint value ({}) less than existential deposit ({})", - value, - crate::EXISTENTIAL_DEPOSIT - ); - } - - let mut actors = self.actors.borrow_mut(); - let (_, balance) = actors.entry(*id).or_insert((TestActor::User, 0)); - *balance = balance.saturating_add(value); - } - - pub(crate) fn balance_of(&self, id: &ProgramId) -> Balance { - self.actors - .borrow() - .get(id) - .map(|(_, balance)| *balance) - .unwrap_or_default() + pub(crate) fn balance_of(&self, id: &ProgramId) -> Value { + Accounts::balance(*id) } pub(crate) fn claim_value_from_mailbox( @@ -622,58 +465,54 @@ impl ExtManager { } #[track_caller] - pub(crate) fn override_balance(&mut self, id: &ProgramId, balance: Balance) { - if self.is_user(id) && balance < crate::EXISTENTIAL_DEPOSIT { + pub(crate) fn override_balance(&mut self, &id: &ProgramId, balance: Value) { + if Actors::is_user(id) && balance < crate::EXISTENTIAL_DEPOSIT { panic!( "An attempt to override balance with value ({}) less than existential deposit ({})", balance, crate::EXISTENTIAL_DEPOSIT ); } - - let mut actors = self.actors.borrow_mut(); - let (_, actor_balance) = actors.entry(*id).or_insert((TestActor::User, 0)); - *actor_balance = balance; + Accounts::override_balance(id, balance); } #[track_caller] pub(crate) fn read_memory_pages(&self, program_id: &ProgramId) -> BTreeMap { - let actors = self.actors.borrow(); - let program = &actors - .get(program_id) - .unwrap_or_else(|| panic!("Actor {program_id} not found")) - .0; - - let program = match program { - TestActor::Initialized(program) => program, - TestActor::Uninitialized(_, program) => program.as_ref().unwrap(), - TestActor::Dormant | TestActor::User => panic!("Actor {program_id} isn't a program"), - }; + Actors::access(*program_id, |actor| { + let program = match actor.unwrap_or_else(|| panic!("Actor id {program_id:?} not found")) + { + TestActor::Initialized(program) => program, + TestActor::Uninitialized(_, program) => program.as_ref().unwrap(), + TestActor::Dormant => panic!("Actor {program_id} isn't dormant"), + }; - match program { - Program::Genuine(program) => program.pages_data.clone(), - Program::Mock(_) => panic!("Can't read memory of mock program"), - } + match program { + Program::Genuine(program) => program.pages_data.clone(), + Program::Mock(_) => panic!("Can't read memory of mock program"), + } + }) } #[track_caller] fn init_success(&mut self, program_id: ProgramId) { - let mut actors = self.actors.borrow_mut(); - let (actor, _) = actors - .get_mut(&program_id) - .expect("Can't find existing program"); - - actor.set_initialized(); + Actors::modify(program_id, |actor| { + actor + .unwrap_or_else(|| panic!("Actor id {program_id:?} not found")) + .set_initialized() + }); } #[track_caller] - fn init_failure(&mut self, program_id: ProgramId) { - let mut actors = self.actors.borrow_mut(); - let (actor, _) = actors - .get_mut(&program_id) - .expect("Can't find existing program"); - - *actor = TestActor::Dormant; + fn init_failure(&mut self, program_id: ProgramId, origin: ProgramId) { + Actors::modify(program_id, |actor| { + let actor = actor.unwrap_or_else(|| panic!("Actor id {program_id:?} not found")); + *actor = TestActor::Dormant + }); + + let value = Accounts::balance(program_id); + if value != 0 { + Accounts::transfer(program_id, origin, value, false); + } } fn process_mock(&mut self, mut mock: Box, dispatch: StoredDispatch) { @@ -771,14 +610,11 @@ impl ExtManager { // After run either `init_success` is called or `init_failed`. // So only active (init success) program can be modified - self.actors - .borrow_mut() - .entry(program_id) - .and_modify(|(actor, _)| { - if let TestActor::Initialized(old_mock) = actor { - *old_mock = Program::Mock(Some(mock)); - } - }); + Actors::modify(program_id, |actor| { + if let Some(TestActor::Initialized(old_mock)) = actor { + *old_mock = Program::Mock(Some(mock)); + } + }) } fn process_normal( @@ -944,9 +780,8 @@ impl ExtManager { id: ProgramId, op: F, ) -> Option { - let mut actors = self.actors.borrow_mut(); - actors - .get_mut(&id) - .and_then(|(actor, _)| actor.genuine_program_mut().map(op)) + Actors::modify(id, |actor| { + actor.and_then(|actor| actor.genuine_program_mut().map(op)) + }) } } diff --git a/gtest/src/manager/journal.rs b/gtest/src/manager/journal.rs index 85182ac953d..73d11884b5b 100644 --- a/gtest/src/manager/journal.rs +++ b/gtest/src/manager/journal.rs @@ -19,9 +19,11 @@ /// Implementation of the `JournalHandler` trait for the `ExtManager`. use std::collections::BTreeMap; -use super::{Balance, ExtManager, Gas, GenuineProgram, MintMode, Program, TestActor}; +use crate::{accounts::Accounts, actors::Actors, Value, EXISTENTIAL_DEPOSIT}; + +use super::{ExtManager, Gas, GenuineProgram, Program, TestActor}; use core_processor::common::{DispatchOutcome, JournalHandler}; -use gear_common::{scheduler::ScheduledTask, Origin}; +use gear_common::{gas_provider::Imbalance as _, scheduler::ScheduledTask, Origin}; use gear_core::{ code::{Code, CodeAndId, InstrumentedCodeAndId}, ids::{CodeId, MessageId, ProgramId, ReservationId}, @@ -53,8 +55,10 @@ impl JournalHandler for ExtManager { DispatchOutcome::Success | DispatchOutcome::Exit { .. } => { self.succeed.insert(message_id); } - DispatchOutcome::InitFailure { program_id, .. } => { - self.init_failure(program_id); + DispatchOutcome::InitFailure { + program_id, origin, .. + } => { + self.init_failure(program_id, origin); self.failed.insert(message_id); } DispatchOutcome::InitSuccess { program_id, .. } => { @@ -68,7 +72,7 @@ impl JournalHandler for ExtManager { self.gas_allowance = self.gas_allowance.saturating_sub(Gas(amount)); self.gas_tree .spend(message_id, amount) - .unwrap_or_else(|e| unreachable!("GasTree corrupted! {:?}", e)); + .unwrap_or_else(|e| unreachable!("GasTree corrupted! {e:?}")); self.gas_burned .entry(message_id) @@ -76,18 +80,46 @@ impl JournalHandler for ExtManager { *gas += Gas(amount); }) .or_insert(Gas(amount)); + + let (external, multiplier, _) = self + .gas_tree + .get_origin_node(message_id) + .unwrap_or_else(|e| unreachable!("GasTree corrupted! {e:?}")); + + let id: ProgramId = external.into_origin().into(); + self.bank.spend_gas(id, amount, multiplier); } fn exit_dispatch(&mut self, id_exited: ProgramId, value_destination: ProgramId) { - if let Some((_, balance)) = self.actors.remove(&id_exited) { - self.mint_to(&value_destination, balance, MintMode::AllowDeath); + Actors::modify(id_exited, |actor| { + let actor = + actor.unwrap_or_else(|| panic!("Can't find existing program {id_exited:?}")); + *actor = TestActor::Dormant + }); + + let value = Accounts::balance(id_exited); + if value != 0 { + Accounts::transfer(id_exited, value_destination, value, false); } } fn message_consumed(&mut self, message_id: MessageId) { - self.gas_tree + let outcome = self + .gas_tree .consume(message_id) - .unwrap_or_else(|e| unreachable!("GasTree corrupted! {:?}", e)); + .unwrap_or_else(|e| unreachable!("GasTree corrupted! {e:?}")); + + // Retrieve gas + if let Some((imbalance, multiplier, external)) = outcome { + // Peeking numeric value from negative imbalance. + let gas_left = imbalance.peek(); + let id: ProgramId = external.into_origin().into(); + + // Unreserving funds, if left non-zero amount of gas. + if gas_left != 0 { + self.bank.withdraw_gas(id, gas_left, multiplier); + } + } } fn send_dispatch( @@ -107,8 +139,17 @@ impl JournalHandler for ExtManager { log::debug!("[{message_id}] new dispatch#{}", dispatch.id()); let source = dispatch.source(); + let is_program = Actors::is_program(dispatch.destination()); + + let mut deposit_value = || { + if dispatch.value() != 0 { + self.bank.deposit_value(source, dispatch.value(), false); + } + }; + + if is_program { + deposit_value(); - if self.is_program(&dispatch.destination()) { match (dispatch.gas_limit(), reservation) { (Some(gas_limit), None) => self .gas_tree @@ -131,6 +172,8 @@ impl JournalHandler for ExtManager { self.dispatches.push_back(dispatch.into_stored()); } else { + deposit_value(); + let gas_limit = dispatch.gas_limit().unwrap_or_default(); let stored_message = dispatch.into_stored().into_parts().1; @@ -233,31 +276,14 @@ impl JournalHandler for ExtManager { } #[track_caller] - fn send_value(&mut self, from: ProgramId, to: Option, value: Balance) { + fn send_value(&mut self, from: ProgramId, to: Option, value: Value) { if value == 0 { // Nothing to do return; } - if let Some(ref to) = to { - if self.is_program(&from) { - let mut actors = self.actors.borrow_mut(); - let (_, balance) = actors.get_mut(&from).expect("Can't fail"); - - if *balance < value { - unreachable!("Actor {:?} balance is less then sent value", from); - } - - *balance -= value; - if *balance < crate::EXISTENTIAL_DEPOSIT { - *balance = 0; - } - } - - self.mint_to(to, value, MintMode::KeepAlive); - } else { - self.mint_to(&from, value, MintMode::KeepAlive); - } + let to = to.unwrap_or(from); + self.bank.transfer_value(from, to, value); } #[track_caller] @@ -269,7 +295,7 @@ impl JournalHandler for ExtManager { ) { if let Some(code) = self.opt_binaries.get(&code_id).cloned() { for (init_message_id, candidate_id) in candidates { - if !self.actors.contains_key(&candidate_id) { + if !Actors::contains_key(candidate_id) { let schedule = Schedule::default(); let code = Code::try_new( code.clone(), @@ -296,8 +322,9 @@ impl JournalHandler for ExtManager { }), Some(init_message_id), ); + // Transfer the ED from the program-creator to the new program - self.send_value(program_id, Some(candidate_id), crate::EXISTENTIAL_DEPOSIT); + Accounts::transfer(program_id, candidate_id, EXISTENTIAL_DEPOSIT, true); } else { log::debug!("Program with id {candidate_id:?} already exists"); } @@ -305,8 +332,7 @@ impl JournalHandler for ExtManager { } else { log::debug!("No referencing code with code hash {code_id:?} for candidate programs"); for (_, invalid_candidate_id) in candidates { - self.actors - .insert(invalid_candidate_id, (TestActor::Dormant, 0)); + Actors::insert(invalid_candidate_id, TestActor::Dormant); } } } diff --git a/gtest/src/program.rs b/gtest/src/program.rs index 5a346971e47..5a8c3063738 100644 --- a/gtest/src/program.rs +++ b/gtest/src/program.rs @@ -17,9 +17,11 @@ // along with this program. If not, see . use crate::{ - manager::{Balance, ExtManager, GenuineProgram, MintMode, Program as InnerProgram, TestActor}, + actors::{Actors, GenuineProgram, Program as InnerProgram, TestActor}, + default_users_list, + manager::ExtManager, system::System, - Result, GAS_ALLOWANCE, + Result, Value, GAS_ALLOWANCE, }; use codec::{Codec, Decode, Encode}; use gear_core::{ @@ -398,6 +400,10 @@ impl<'a> Program<'a> { ) -> Self { let program_id = id.clone().into().0; + if default_users_list().contains(&(program_id.into_bytes()[0] as u64)) { + panic!("Can't create program with id {id:?}, because it's reserved for default users") + } + if system .0 .borrow_mut() @@ -578,17 +584,16 @@ impl<'a> Program<'a> { None, ); - let mut actors = system.actors.borrow_mut(); - let (actor, _) = actors.get_mut(&self.id).expect("Can't fail"); - - let kind = if let TestActor::Uninitialized(id @ None, _) = actor { - *id = Some(message.id()); - DispatchKind::Init - } else { - DispatchKind::Handle - }; + let kind = Actors::modify(self.id, |actor| { + let actor = actor.expect("Can't fail"); + if let TestActor::Uninitialized(id @ None, _) = actor { + *id = Some(message.id()); + DispatchKind::Init + } else { + DispatchKind::Handle + } + }); - drop(actors); system.validate_and_route_dispatch(Dispatch::new(kind, message)) } @@ -726,15 +731,8 @@ impl<'a> Program<'a> { D::decode(&mut state_bytes.as_ref()).map_err(Into::into) } - /// Mint balance to the account. - pub fn mint(&mut self, value: Balance) { - self.manager - .borrow_mut() - .mint_to(&self.id(), value, MintMode::KeepAlive) - } - /// Returns the balance of the account. - pub fn balance(&self) -> Balance { + pub fn balance(&self) -> Value { self.manager.borrow().balance_of(&self.id()) } @@ -857,7 +855,7 @@ pub mod gbuild { mod tests { use super::Program; - use crate::{Log, ProgramIdWrapper, System}; + use crate::{Log, ProgramIdWrapper, System, Value, DEFAULT_USER_ALICE, EXISTENTIAL_DEPOSIT}; use demo_constructor::{Arg, Scheme}; use gear_common::Origin; @@ -870,7 +868,7 @@ mod tests { let sys = System::new(); sys.init_logger(); - let user_id = 42; + let user_id = DEFAULT_USER_ALICE; let message = "Signal handle"; let panic_message = "Gotcha!"; @@ -906,7 +904,7 @@ mod tests { let sys = System::new(); sys.init_logger(); - let user_id = 100; + let user_id = DEFAULT_USER_ALICE; let prog = Program::from_binary_with_id(&sys, 137, demo_futures_unordered::WASM_BINARY); @@ -935,23 +933,39 @@ mod tests { sys.init_logger(); let user_id = 42; - sys.mint_to(user_id, 10 * crate::EXISTENTIAL_DEPOSIT); - assert_eq!(sys.balance_of(user_id), 10 * crate::EXISTENTIAL_DEPOSIT); + let mut user_spent_balance = 0; + sys.mint_to(user_id, 12 * EXISTENTIAL_DEPOSIT); + assert_eq!(sys.balance_of(user_id), 12 * EXISTENTIAL_DEPOSIT); - let mut prog = Program::from_binary_with_id(&sys, 137, demo_ping::WASM_BINARY); + let program_id = 137; + let prog = Program::from_binary_with_id(&sys, program_id, demo_ping::WASM_BINARY); - prog.mint(2 * crate::EXISTENTIAL_DEPOSIT); - assert_eq!(prog.balance(), 2 * crate::EXISTENTIAL_DEPOSIT); + sys.transfer(user_id, program_id, 2 * EXISTENTIAL_DEPOSIT, true); + assert_eq!(prog.balance(), 2 * EXISTENTIAL_DEPOSIT); - prog.send_with_value(user_id, "init".to_string(), crate::EXISTENTIAL_DEPOSIT); - sys.run_next_block(); - assert_eq!(prog.balance(), 3 * crate::EXISTENTIAL_DEPOSIT); - assert_eq!(sys.balance_of(user_id), 9 * crate::EXISTENTIAL_DEPOSIT); + prog.send_with_value(user_id, "init".to_string(), EXISTENTIAL_DEPOSIT); + // Note: ED is charged upon program creation if its balance is not 0. + user_spent_balance += sys.run_next_block().spent_value() + EXISTENTIAL_DEPOSIT; + assert_eq!( + prog.balance(), + 3 * EXISTENTIAL_DEPOSIT + EXISTENTIAL_DEPOSIT + ); + assert_eq!( + sys.balance_of(user_id), + 9 * EXISTENTIAL_DEPOSIT - user_spent_balance + ); - prog.send_with_value(user_id, "PING".to_string(), 2 * crate::EXISTENTIAL_DEPOSIT); - sys.run_next_block(); - assert_eq!(prog.balance(), 5 * crate::EXISTENTIAL_DEPOSIT); - assert_eq!(sys.balance_of(user_id), 7 * crate::EXISTENTIAL_DEPOSIT); + prog.send_with_value(user_id, "PING".to_string(), 2 * EXISTENTIAL_DEPOSIT); + user_spent_balance += sys.run_next_block().spent_value(); + + assert_eq!( + prog.balance(), + 5 * EXISTENTIAL_DEPOSIT + EXISTENTIAL_DEPOSIT + ); + assert_eq!( + sys.balance_of(user_id), + 7 * EXISTENTIAL_DEPOSIT - user_spent_balance + ); } #[test] @@ -965,33 +979,50 @@ mod tests { let sender2 = 45; // Top-up senders balances - sys.mint_to(sender0, 20 * crate::EXISTENTIAL_DEPOSIT); - sys.mint_to(sender1, 20 * crate::EXISTENTIAL_DEPOSIT); - sys.mint_to(sender2, 20 * crate::EXISTENTIAL_DEPOSIT); + sys.mint_to(sender0, 20 * EXISTENTIAL_DEPOSIT); + sys.mint_to(sender1, 20 * EXISTENTIAL_DEPOSIT); + sys.mint_to(sender2, 20 * EXISTENTIAL_DEPOSIT); + + // Top-up receiver balance + let mut receiver_expected_balance = 10 * EXISTENTIAL_DEPOSIT; + sys.mint_to(receiver, receiver_expected_balance); let prog = Program::from_binary_with_id(&sys, 137, demo_piggy_bank::WASM_BINARY); prog.send_bytes(receiver, b"init"); - sys.run_next_block(); - assert_eq!(prog.balance(), 0); + receiver_expected_balance -= sys.run_next_block().spent_value() + EXISTENTIAL_DEPOSIT; + assert_eq!(prog.balance(), EXISTENTIAL_DEPOSIT); // Send values to the program - prog.send_bytes_with_value(sender0, b"insert", 2 * crate::EXISTENTIAL_DEPOSIT); - sys.run_next_block(); - assert_eq!(sys.balance_of(sender0), 18 * crate::EXISTENTIAL_DEPOSIT); - prog.send_bytes_with_value(sender1, b"insert", 4 * crate::EXISTENTIAL_DEPOSIT); - sys.run_next_block(); - assert_eq!(sys.balance_of(sender1), 16 * crate::EXISTENTIAL_DEPOSIT); - prog.send_bytes_with_value(sender2, b"insert", 6 * crate::EXISTENTIAL_DEPOSIT); - sys.run_next_block(); - assert_eq!(sys.balance_of(sender2), 14 * crate::EXISTENTIAL_DEPOSIT); + prog.send_bytes_with_value(sender0, b"insert", 2 * EXISTENTIAL_DEPOSIT); + let sender0_spent_value = sys.run_next_block().spent_value(); + assert_eq!( + sys.balance_of(sender0), + 18 * EXISTENTIAL_DEPOSIT - sender0_spent_value + ); + prog.send_bytes_with_value(sender1, b"insert", 4 * EXISTENTIAL_DEPOSIT); + let sender1_spent_value = sys.run_next_block().spent_value(); + assert_eq!( + sys.balance_of(sender1), + 16 * EXISTENTIAL_DEPOSIT - sender1_spent_value + ); + prog.send_bytes_with_value(sender2, b"insert", 6 * EXISTENTIAL_DEPOSIT); + let sender2_spent_value = sys.run_next_block().spent_value(); + assert_eq!( + sys.balance_of(sender2), + 14 * EXISTENTIAL_DEPOSIT - sender2_spent_value + ); // Check program's balance - assert_eq!(prog.balance(), (2 + 4 + 6) * crate::EXISTENTIAL_DEPOSIT); + assert_eq!( + prog.balance(), + (2 + 4 + 6) * EXISTENTIAL_DEPOSIT + EXISTENTIAL_DEPOSIT + ); // Request to smash the piggy bank and send the value to the receiver address prog.send_bytes(receiver, b"smash"); let res = sys.run_next_block(); + receiver_expected_balance -= res.spent_value(); let reply_to_id = { let log = res.log(); // 1 auto reply and 1 message from program @@ -1013,36 +1044,37 @@ mod tests { .is_ok()); assert_eq!( sys.balance_of(receiver), - (2 + 4 + 6) * crate::EXISTENTIAL_DEPOSIT + (2 + 4 + 6) * EXISTENTIAL_DEPOSIT + receiver_expected_balance ); - - // Check program's balance is empty - assert_eq!(prog.balance(), 0); + // Program is alive and holds the ED + assert_eq!(prog.balance(), EXISTENTIAL_DEPOSIT); } #[test] #[should_panic( - expected = "An attempt to mint value (1) less than existential deposit (1000000000000)" + expected = "Failed to increase balance: the sum (1) of the total balance (0) and the value (1) \ + cannot be lower than the existential deposit (1000000000000)" )] fn mint_less_than_deposit() { System::new().mint_to(1, 1); } #[test] - #[should_panic(expected = "Insufficient value: user \ - (0x0100000000000000000000000000000000000000000000000000000000000000) tries \ - to send (1000000000001) value, while his balance (1000000000000)")] + #[should_panic( + expected = "Insufficient balance: user (0x0500000000000000000000000000000000000000000000000000000000000000) \ + tries to send (1000000000001) value, (4500000000000) gas and ED (1000000000000), while his balance (1000000000000)" + )] fn fails_on_insufficient_balance() { let sys = System::new(); - let user = 1; - let prog = Program::from_binary_with_id(&sys, 2, demo_piggy_bank::WASM_BINARY); + let user = 5; + let prog = Program::from_binary_with_id(&sys, 6, demo_piggy_bank::WASM_BINARY); assert_eq!(sys.balance_of(user), 0); - sys.mint_to(user, crate::EXISTENTIAL_DEPOSIT); - assert_eq!(sys.balance_of(user), crate::EXISTENTIAL_DEPOSIT); + sys.mint_to(user, EXISTENTIAL_DEPOSIT); + assert_eq!(sys.balance_of(user), EXISTENTIAL_DEPOSIT); - prog.send_bytes_with_value(user, b"init", crate::EXISTENTIAL_DEPOSIT + 1); + prog.send_bytes_with_value(user, b"init", EXISTENTIAL_DEPOSIT + 1); sys.run_next_block(); } @@ -1051,36 +1083,46 @@ mod tests { let sys = System::new(); sys.init_logger(); + const RECEIVER_INITIAL_BALANCE: Value = 10 * EXISTENTIAL_DEPOSIT; + let sender = 42; let receiver = 84; + let mut receiver_expected_balance = RECEIVER_INITIAL_BALANCE; - sys.mint_to(sender, 20 * crate::EXISTENTIAL_DEPOSIT); + sys.mint_to(sender, 20 * EXISTENTIAL_DEPOSIT); + sys.mint_to(receiver, RECEIVER_INITIAL_BALANCE); let prog = Program::from_binary_with_id(&sys, 137, demo_piggy_bank::WASM_BINARY); prog.send_bytes(receiver, b"init"); - sys.run_next_block(); + receiver_expected_balance -= sys.run_next_block().spent_value() + EXISTENTIAL_DEPOSIT; // Get zero value to the receiver's mailbox prog.send_bytes(receiver, b"smash"); - sys.run_next_block(); + receiver_expected_balance -= sys.run_next_block().spent_value(); let receiver_mailbox = sys.get_mailbox(receiver); assert!(receiver_mailbox .claim_value(Log::builder().dest(receiver).payload_bytes(b"send")) .is_ok()); - assert_eq!(sys.balance_of(receiver), 0); + assert_eq!(sys.balance_of(receiver), receiver_expected_balance); // Get the value > ED to the receiver's mailbox - prog.send_bytes_with_value(sender, b"insert", 2 * crate::EXISTENTIAL_DEPOSIT); - prog.send_bytes(receiver, b"smash"); + prog.send_bytes_with_value(sender, b"insert", 2 * EXISTENTIAL_DEPOSIT); sys.run_next_block(); + prog.send_bytes(receiver, b"smash"); + receiver_expected_balance -= sys.run_next_block().spent_value(); // Check receiver's balance assert!(receiver_mailbox .claim_value(Log::builder().dest(receiver).payload_bytes(b"send")) .is_ok()); - assert_eq!(sys.balance_of(receiver), 2 * crate::EXISTENTIAL_DEPOSIT); + assert_eq!( + sys.balance_of(receiver), + 2 * EXISTENTIAL_DEPOSIT + receiver_expected_balance + ); + // Program is alive and holds the ED + assert_eq!(prog.balance(), EXISTENTIAL_DEPOSIT); } struct CleanupFolderOnDrop { @@ -1102,7 +1144,7 @@ mod tests { let mut prog = Program::from_binary_with_id(&sys, 420, WASM_BINARY); - let signer = 42; + let signer = DEFAULT_USER_ALICE; let signer_mailbox = sys.get_mailbox(signer); // Init capacitor with limit = 15 @@ -1156,7 +1198,7 @@ mod tests { let prog = Program::from_binary_with_id(&sys, 420, WASM_BINARY); - let signer = 42; + let signer = DEFAULT_USER_ALICE; // Init simple waiter prog.send(signer, InitMessage::SimpleWaiter); @@ -1188,7 +1230,7 @@ mod tests { let prog = Program::from_binary_with_id(&sys, 420, WASM_BINARY); - let signer = 42; + let signer = DEFAULT_USER_ALICE; // Init reserver prog.send(signer, InitMessage::Reserver); @@ -1214,16 +1256,31 @@ mod tests { let sys = System::new(); sys.init_logger(); - let user_id = [42; 32]; - let prog = Program::from_binary_with_id(&sys, 137, WASM_BINARY); + let user_id = 42; + let mut user_balance = 4 * EXISTENTIAL_DEPOSIT; + sys.mint_to(user_id, user_balance); + + let prog_id = 137; + assert_eq!(sys.balance_of(prog_id), 0); + let prog = Program::from_binary_with_id(&sys, prog_id, WASM_BINARY); - let msg_id = prog.send(user_id, demo_exit_handle::scheme()); + let msg_id = prog.send_with_gas(user_id, demo_exit_handle::scheme(), 1_000_000_000, 0); let result = sys.run_next_block(); + user_balance -= result.spent_value() + EXISTENTIAL_DEPOSIT; + assert!(result.succeed.contains(&msg_id)); + assert_eq!(sys.balance_of(prog_id), EXISTENTIAL_DEPOSIT); + assert_eq!(sys.balance_of(user_id), user_balance); - let msg_id = prog.send_bytes(user_id, []); + let msg_id = prog.send_bytes_with_gas(user_id, [], 1_000_000_000, 0); let result = sys.run_next_block(); + user_balance -= result.spent_value(); + + // ED returned upon program exit + user_balance += EXISTENTIAL_DEPOSIT; assert!(result.succeed.contains(&msg_id)); + assert_eq!(sys.balance_of(prog_id), 0); + assert_eq!(sys.balance_of(user_id), user_balance); } #[test] @@ -1234,6 +1291,7 @@ mod tests { let prog = Program::from_binary_with_id(&sys, 137, demo_ping::WASM_BINARY); let user_id = ActorId::zero(); + sys.mint_to(user_id, EXISTENTIAL_DEPOSIT * 2); // set insufficient gas for execution let msg_id = prog.send_with_gas(user_id, "init".to_string(), 1, 0); @@ -1258,7 +1316,7 @@ mod tests { let sys = System::new(); sys.init_logger(); - let user_id = 42; + let user_id = DEFAULT_USER_ALICE; let prog = Program::from_binary_with_id(&sys, 4242, WASM_BINARY); // Initialize program @@ -1307,7 +1365,7 @@ mod tests { let sys = System::new(); sys.init_logger(); - let user_id = 42; + let user_id = DEFAULT_USER_ALICE; let prog_id = 4242; let prog = Program::from_binary_with_id(&sys, prog_id, WASM_BINARY); @@ -1355,4 +1413,21 @@ mod tests { assert!(res.succeed.contains(&msg_id)); assert!(mailbox.contains(&Log::builder().payload_bytes(payload).source(new_prog_id))); } + + #[test] + fn tests_unused_gas_value_not_transferred() { + let sys = System::new(); + sys.init_verbose_logger(); + + let user = 42; + sys.mint_to(user, 2 * EXISTENTIAL_DEPOSIT); + + let prog = Program::from_binary_with_id(&sys, 69, demo_piggy_bank::WASM_BINARY); + prog.send_bytes_with_gas(user, b"init", 1_000_000_000, 0); + sys.run_next_block(); + + // Unspent gas is not returned to the user's balance when the sum of these is + // lower than ED + assert_eq!(sys.balance_of(user), 0) + } } diff --git a/gtest/src/system.rs b/gtest/src/system.rs index 39abc27cd63..ec3411c1a40 100644 --- a/gtest/src/system.rs +++ b/gtest/src/system.rs @@ -17,11 +17,13 @@ // along with this program. If not, see . use crate::{ + accounts::Accounts, + actors::Actors, log::{BlockRunResult, CoreLog}, mailbox::ActorMailbox, - manager::{Actors, Balance, ExtManager, MintMode}, + manager::ExtManager, program::{Program, ProgramIdWrapper}, - Gas, GAS_ALLOWANCE, + Gas, Value, GAS_ALLOWANCE, }; use codec::{Decode, DecodeAll}; use colored::Colorize; @@ -53,33 +55,36 @@ struct PageKey { } #[derive(Debug)] -struct PagesStorage { - actors: Actors, -} +struct PagesStorage; impl LazyPagesStorage for PagesStorage { fn page_exists(&self, mut key: &[u8]) -> bool { let PageKey { program_id, page, .. } = PageKey::decode_all(&mut key).expect("Invalid key"); - self.actors - .borrow() - .get(&program_id) - .and_then(|(actor, _)| actor.get_pages_data()) - .map(|pages_data| pages_data.contains_key(&page)) - .unwrap_or(false) + + Actors::access(program_id, |actor| { + actor + .and_then(|actor| actor.get_pages_data()) + .map(|pages_data| pages_data.contains_key(&page)) + .unwrap_or(false) + }) } fn load_page(&mut self, mut key: &[u8], buffer: &mut [u8]) -> Option { let PageKey { program_id, page, .. } = PageKey::decode_all(&mut key).expect("Invalid key"); - let actors = self.actors.borrow(); - let (actor, _balance) = actors.get(&program_id)?; - let pages_data = actor.get_pages_data()?; - let page_buf = pages_data.get(&page)?; - buffer.copy_from_slice(page_buf); - Some(page_buf.len() as u32) + + Actors::access(program_id, |actor| { + actor + .and_then(|actor| actor.get_pages_data()) + .and_then(|pages_data| pages_data.get(&page)) + .map(|page_buf| { + buffer.copy_from_slice(page_buf); + page_buf.len() as u32 + }) + }) } } @@ -115,13 +120,10 @@ impl System { } let ext_manager = ExtManager::new(); - - let actors = ext_manager.actors.clone(); - let pages_storage = PagesStorage { actors }; gear_lazy_pages::init( LazyPagesVersion::Version1, LazyPagesInitContext::new(Self::PAGE_STORAGE_PREFIX), - pages_storage, + PagesStorage, ) .expect("Failed to init lazy-pages"); @@ -275,9 +277,7 @@ impl System { /// Returns a [`Program`] by `id`. pub fn get_program>(&self, id: ID) -> Option { let id = id.into().0; - let manager = self.0.borrow(); - - if manager.is_program(&id) { + if Actors::is_program(id) { Some(Program { id, manager: &self.0, @@ -294,12 +294,8 @@ impl System { /// Returns a list of programs. pub fn programs(&self) -> Vec { - let manager = self.0.borrow(); - let actors = manager.actors.borrow(); - actors - .keys() - .copied() - .filter(|id| manager.is_program(id)) + Actors::program_ids() + .into_iter() .map(|id| Program { id, manager: &self.0, @@ -314,7 +310,7 @@ impl System { /// exited or terminated that it can't be called anymore. pub fn is_active_program>(&self, id: ID) -> bool { let program_id = id.into().0; - self.0.borrow().is_active_program(&program_id) + Actors::is_active_program(program_id) } /// Saves code to the storage and returns its code hash @@ -369,22 +365,46 @@ impl System { #[track_caller] pub fn get_mailbox>(&self, id: ID) -> ActorMailbox { let program_id = id.into().0; - if !self.0.borrow().is_user(&program_id) { + if !Actors::is_user(program_id) { panic!("Mailbox available only for users"); } ActorMailbox::new(program_id, &self.0) } /// Mint balance to user with given `id` and `value`. - pub fn mint_to>(&self, id: ID, value: Balance) { - let actor_id = id.into().0; - self.0 - .borrow_mut() - .mint_to(&actor_id, value, MintMode::KeepAlive); + pub fn mint_to>(&self, id: ID, value: Value) { + let id = id.into().0; + + if Actors::is_program(id) { + panic!( + "Attempt to mint value to a program {id:?}, please use `System::transfer` instead" + ); + } + + self.0.borrow_mut().mint_to(&id, value); + } + + /// Transfer balance from user with given `from` id to user with given `to` + /// id. + pub fn transfer( + &self, + from: impl Into, + to: impl Into, + value: Value, + keep_alive: bool, + ) { + let from = from.into().0; + let to = to.into().0; + + if Actors::is_program(from) { + panic!("Attempt to transfer from a program {from:?}"); + } + + Accounts::transfer(from, to, value, keep_alive); } /// Returns balance of user with given `id`. - pub fn balance_of>(&self, id: ID) -> Balance { + pub fn balance_of>(&self, id: ID) -> Value { let actor_id = id.into().0; self.0.borrow().balance_of(&actor_id) } @@ -397,6 +417,10 @@ impl Drop for System { self.0.borrow().gas_tree.reset(); self.0.borrow().mailbox.reset(); self.0.borrow().task_pool.clear(); + + // Clear actors and accounts storages + Actors::clear(); + Accounts::clear(); } } diff --git a/utils/cargo-gbuild/test-program/src/lib.rs b/utils/cargo-gbuild/test-program/src/lib.rs index ac5e4e501c3..6126023cac7 100644 --- a/utils/cargo-gbuild/test-program/src/lib.rs +++ b/utils/cargo-gbuild/test-program/src/lib.rs @@ -40,7 +40,7 @@ extern "C" fn handle() { #[cfg(test)] mod tests { - use gtest::{Program, System}; + use gtest::{Program, System, constants::DEFAULT_USER_ALICE}; #[test] fn test_init() { @@ -51,7 +51,7 @@ mod tests { system.init_logger(); // Get program from artifact - let user = 0; + let user = DEFAULT_USER_ALICE; let program = Program::current(&system); // Init program diff --git a/utils/cargo-gbuild/tests/smoke.rs b/utils/cargo-gbuild/tests/smoke.rs index 988c63a3901..e8ac4612612 100644 --- a/utils/cargo-gbuild/tests/smoke.rs +++ b/utils/cargo-gbuild/tests/smoke.rs @@ -18,12 +18,12 @@ use anyhow::Result; use cargo_gbuild::GBuild; -use gtest::{state_args, Program, System}; +use gtest::{constants::DEFAULT_USER_ALICE, state_args, Program, System}; use std::{fs, path::PathBuf, process::Command}; fn ping(sys: &System, prog: PathBuf) -> Program<'_> { // Get program from artifact - let user = 0; + let user = DEFAULT_USER_ALICE; let program = Program::from_file(sys, prog); // Init program