From 5cfb148777d6f407ab1eda4c674cf767d514e9ec Mon Sep 17 00:00:00 2001 From: Sabaun Taraki Date: Tue, 16 Jul 2024 16:16:14 +0300 Subject: [PATCH] refactoring(gtest): Introduce `gear_common`s mailbox to `gtest` (#4010) --- common/src/auxiliary/mailbox.rs | 41 +- common/src/auxiliary/mod.rs | 38 +- common/src/gas_provider/property_tests/mod.rs | 7 +- examples/piggy-bank/src/wasm.rs | 2 +- gtest/src/blocks.rs | 93 +++- gtest/src/gas_tree.rs | 2 +- gtest/src/lib.rs | 1 + gtest/src/log.rs | 36 +- gtest/src/mailbox.rs | 406 +----------------- gtest/src/mailbox/actor.rs | 246 +++++++++++ gtest/src/mailbox/manager.rs | 89 ++++ gtest/src/manager.rs | 74 ++-- gtest/src/program.rs | 37 +- gtest/src/system.rs | 14 +- 14 files changed, 600 insertions(+), 486 deletions(-) create mode 100644 gtest/src/mailbox/actor.rs create mode 100644 gtest/src/mailbox/manager.rs diff --git a/common/src/auxiliary/mailbox.rs b/common/src/auxiliary/mailbox.rs index 728e63ca779..0eb3820e5a1 100644 --- a/common/src/auxiliary/mailbox.rs +++ b/common/src/auxiliary/mailbox.rs @@ -20,8 +20,12 @@ use crate::{ auxiliary::DoubleBTreeMap, - storage::{DoubleMapStorage, Interval, MailboxError, MailboxImpl, MailboxKeyGen}, + storage::{ + CountedByKey, DoubleMapStorage, GetSecondPos, Interval, IterableByKeyMap, IteratorWrap, + MailboxError, MailboxImpl, MailboxKeyGen, + }, }; +use alloc::collections::btree_map::IntoIter; use core::cell::RefCell; use gear_core::{ ids::{MessageId, ProgramId}, @@ -97,6 +101,41 @@ impl DoubleMapStorage for MailboxStorageWrap { } } +impl CountedByKey for MailboxStorageWrap { + type Key = ProgramId; + type Length = usize; + + fn len(key: &Self::Key) -> Self::Length { + MAILBOX_STORAGE.with_borrow(|map| map.count_key(key)) + } +} + +impl IterableByKeyMap<(MailboxedMessage, Interval)> for MailboxStorageWrap { + type Key = ProgramId; + + type DrainIter = IteratorWrap< + IntoIter)>, + (MailboxedMessage, Interval), + GetSecondPos, + >; + + type Iter = IteratorWrap< + IntoIter)>, + (MailboxedMessage, Interval), + GetSecondPos, + >; + + fn drain_key(key: Self::Key) -> Self::DrainIter { + MAILBOX_STORAGE + .with_borrow_mut(|map| map.drain_key(&key)) + .into() + } + + fn iter_key(key: Self::Key) -> Self::Iter { + MAILBOX_STORAGE.with_borrow(|map| map.iter_key(&key)).into() + } +} + /// An implementor of the error returned from calling `Mailbox` trait functions. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MailboxErrorImpl { diff --git a/common/src/auxiliary/mod.rs b/common/src/auxiliary/mod.rs index 6999839c096..c3a3866b009 100644 --- a/common/src/auxiliary/mod.rs +++ b/common/src/auxiliary/mod.rs @@ -23,7 +23,7 @@ pub mod gas_provider; pub mod mailbox; -use alloc::collections::btree_map::{BTreeMap, Entry}; +use alloc::collections::btree_map::{BTreeMap, Entry, IntoIter}; /// Double key `BTreeMap`. /// @@ -52,6 +52,16 @@ impl DoubleBTreeMap { .unwrap_or_default() } + pub fn count_key(&self, key1: &K1) -> usize + where + K1: Ord, + { + self.inner + .get(key1) + .map(|key2_map| key2_map.len()) + .unwrap_or_default() + } + /// Returns a reference to the value corresponding to the keys. pub fn get(&self, key1: &K1, key2: &K2) -> Option<&V> where @@ -95,6 +105,32 @@ impl DoubleBTreeMap { } } +// Iterator related impl +impl DoubleBTreeMap { + pub fn iter_key(&self, key1: &K1) -> IntoIter + where + K1: Ord, + K2: Clone, + V: Clone, + { + self.inner + .get(key1) + .cloned() + .map(|key2_map| key2_map.into_iter()) + .unwrap_or_default() + } + + pub fn drain_key(&mut self, key1: &K1) -> IntoIter + where + K1: Ord, + { + self.inner + .remove(key1) + .map(|key2_map| key2_map.into_iter()) + .unwrap_or_default() + } +} + impl Default for DoubleBTreeMap { fn default() -> Self { Self::new() diff --git a/common/src/gas_provider/property_tests/mod.rs b/common/src/gas_provider/property_tests/mod.rs index be2bfd9cf83..de2f5ff63f8 100644 --- a/common/src/gas_provider/property_tests/mod.rs +++ b/common/src/gas_provider/property_tests/mod.rs @@ -75,16 +75,15 @@ //! //! 14. Value catch can be performed only on consumed nodes (not tested). -use super::{auxiliary::gas_provider::*, *}; -use crate::storage::MapStorage; -use alloc::collections::BTreeSet; +use super::*; +use crate::{auxiliary::gas_provider::*, storage::MapStorage}; use core::iter::FromIterator; use enum_iterator::all; use frame_support::{assert_err, assert_ok}; use gear_utils::{NonEmpty, RingGet}; use primitive_types::H256; use proptest::prelude::*; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use strategies::GasTreeAction; mod assertions; diff --git a/examples/piggy-bank/src/wasm.rs b/examples/piggy-bank/src/wasm.rs index 6f3e89f77e3..44093917c53 100644 --- a/examples/piggy-bank/src/wasm.rs +++ b/examples/piggy-bank/src/wasm.rs @@ -27,7 +27,7 @@ extern "C" fn handle() { if msg.expect("Failed to load payload bytes") == b"smash" { debug!("smashing, total: {available_value}"); - msg::reply_bytes(b"send", available_value).unwrap(); + msg::send(msg::source(), b"send", available_value).unwrap(); } }); } diff --git a/gtest/src/blocks.rs b/gtest/src/blocks.rs index 0e3e2fc77bf..1e4b593e792 100644 --- a/gtest/src/blocks.rs +++ b/gtest/src/blocks.rs @@ -21,42 +21,53 @@ use core_processor::configs::BlockInfo; use std::{ cell::RefCell, + rc::Rc, time::{SystemTime, UNIX_EPOCH}, }; use crate::BLOCK_DURATION_IN_MSECS; +type BlockInfoStorageInner = Rc>>; + thread_local! { /// Definition of the storage value storing block info (timestamp and height). - static BLOCK_INFO_STORAGE: RefCell> = const { RefCell::new(None) }; + static BLOCK_INFO_STORAGE: BlockInfoStorageInner = Rc::new(RefCell::new(None)); } -/// Block info storage manager. -#[derive(Debug, Default)] -pub(crate) struct BlocksManager(()); +#[derive(Debug)] +pub(crate) struct BlocksManager { + _unused: BlockInfoStorageInner, +} impl BlocksManager { /// Create block info storage manager with a further initialization of the /// storage. pub(crate) fn new() -> Self { - BLOCK_INFO_STORAGE.with_borrow_mut(|block_info| { - let info = BlockInfo { - height: 0, - timestamp: now(), - }; - - block_info.replace(info); + let unused = BLOCK_INFO_STORAGE.with(|bi_rc| { + let mut ref_mut = bi_rc.borrow_mut(); + if ref_mut.is_none() { + let info = BlockInfo { + height: 0, + timestamp: now(), + }; + + *ref_mut = Some(info); + } + + Rc::clone(bi_rc) }); - Self(()) + Self { _unused: unused } } /// Get current block info. pub(crate) fn get(&self) -> BlockInfo { - BLOCK_INFO_STORAGE.with_borrow(|cell| { - cell.as_ref() + BLOCK_INFO_STORAGE.with(|bi_rc| { + bi_rc + .borrow() + .as_ref() .copied() - .expect("must be initialized in a `BlocksManager::new`") + .expect("instance always initialized") }) } @@ -67,9 +78,10 @@ impl BlocksManager { /// Adjusts blocks info by moving blocks by `amount`. pub(crate) fn move_blocks_by(&self, amount: u32) -> BlockInfo { - BLOCK_INFO_STORAGE.with_borrow_mut(|block_info| { - let Some(block_info) = block_info.as_mut() else { - panic!("must initialized in a `BlocksManager::new`"); + BLOCK_INFO_STORAGE.with(|bi_rc| { + let mut bi_ref_mut = bi_rc.borrow_mut(); + let Some(block_info) = bi_ref_mut.as_mut() else { + panic!("instance always initialized"); }; block_info.height += amount; let duration = BLOCK_DURATION_IN_MSECS.saturating_mul(amount as u64); @@ -80,9 +92,54 @@ impl BlocksManager { } } +impl Default for BlocksManager { + fn default() -> Self { + Self::new() + } +} + +impl Drop for BlocksManager { + fn drop(&mut self) { + BLOCK_INFO_STORAGE.with(|bi_rc| { + if Rc::strong_count(bi_rc) == 2 { + *bi_rc.borrow_mut() = None; + } + }); + } +} + fn now() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_millis() as u64 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_nullified_on_drop() { + let first_instance = BlocksManager::new(); + let second_instance = BlocksManager::new(); + + first_instance.next_block(); + first_instance.next_block(); + + // Assert all instance use same data; + assert_eq!(second_instance.get().height, 2); + BLOCK_INFO_STORAGE.with(|bi_rc| bi_rc.borrow().is_some()); + + // Drop first instance and check whether data is removed. + drop(first_instance); + assert_eq!(second_instance.get().height, 2); + + second_instance.next_block(); + assert_eq!(second_instance.get().height, 3); + BLOCK_INFO_STORAGE.with(|bi_rc| bi_rc.borrow().is_some()); + + drop(second_instance); + BLOCK_INFO_STORAGE.with(|bi_rc| bi_rc.borrow().is_none()); + } +} diff --git a/gtest/src/gas_tree.rs b/gtest/src/gas_tree.rs index 0bfe93a7690..f2e97f0c201 100644 --- a/gtest/src/gas_tree.rs +++ b/gtest/src/gas_tree.rs @@ -30,7 +30,7 @@ pub(crate) type PositiveImbalance = ::PositiveImbalance; pub(crate) type NegativeImbalance = ::NegativeImbalance; type GasTree = ::GasTree; -/// Gas tree manager which uses operates under the hood over +/// Gas tree manager which operates under the hood over /// [`gear_common::AuxiliaryGasProvider`]. /// /// Manager is needed mainly to adapt arguments of the gas tree methods to the diff --git a/gtest/src/lib.rs b/gtest/src/lib.rs index 396ac673ba3..4c42b9b5312 100644 --- a/gtest/src/lib.rs +++ b/gtest/src/lib.rs @@ -432,6 +432,7 @@ mod system; pub use crate::log::{CoreLog, Log, RunResult}; pub use codec; pub use error::{Result, TestError}; +pub use mailbox::ActorMailbox; pub use program::{ calculate_program_id, gbuild::ensure_gbuild, Gas, Program, ProgramBuilder, ProgramIdWrapper, WasmProgram, diff --git a/gtest/src/log.rs b/gtest/src/log.rs index eba28d932ef..2e45c0981f7 100644 --- a/gtest/src/log.rs +++ b/gtest/src/log.rs @@ -20,7 +20,7 @@ use crate::program::{Gas, ProgramIdWrapper}; use codec::{Codec, Encode}; use gear_core::{ ids::{MessageId, ProgramId}, - message::{Payload, StoredMessage}, + message::{Payload, StoredMessage, UserStoredMessage}, }; use gear_core_errors::{ErrorReplyReason, ReplyCode, SimpleExecutionError, SuccessReplyReason}; use std::{collections::BTreeMap, convert::TryInto, fmt::Debug}; @@ -151,11 +151,11 @@ impl DecodedCoreLog { /// ``` #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Log { - source: Option, - destination: Option, - payload: Option, - reply_code: Option, - reply_to: Option, + pub(crate) source: Option, + pub(crate) destination: Option, + pub(crate) payload: Option, + pub(crate) reply_code: Option, + pub(crate) reply_to: Option, } impl From<(ID, T)> for Log @@ -283,22 +283,32 @@ impl Log { } } -impl PartialEq for Log { - fn eq(&self, other: &StoredMessage) -> bool { - if matches!(other.reply_details(), Some(reply) if Some(reply.to_reply_code()) != self.reply_code) - { - return false; - } +impl PartialEq for Log { + fn eq(&self, other: &UserStoredMessage) -> bool { + // Any log field is set. + let has_any = self.source.is_some() + || self.destination.is_some() + || self.payload.is_some() + || self.reply_to.is_some(); + + // If any of log field doesn't match, then there's no equality. if matches!(self.source, Some(source) if source != other.source()) { return false; } + if matches!(self.destination, Some(dest) if dest != other.destination()) { return false; } + if matches!(&self.payload, Some(payload) if payload.inner() != other.payload_bytes()) { return false; } - true + + if matches!(self.reply_to, Some(reply_to) if reply_to != other.id()) { + return false; + } + + has_any } } diff --git a/gtest/src/mailbox.rs b/gtest/src/mailbox.rs index 708ab45cc16..66123645e84 100644 --- a/gtest/src/mailbox.rs +++ b/gtest/src/mailbox.rs @@ -1,6 +1,6 @@ // This file is part of Gear. -// Copyright (C) 2021-2024 Gear Technologies Inc. +// 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 @@ -16,404 +16,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::{manager::ExtManager, CoreLog, Log, RunResult}; -use codec::Encode; -use core_processor::common::JournalHandler; -use gear_core::{ - ids::{MessageId, ProgramId}, - message::{Dispatch, DispatchKind, Message, ReplyDetails, StoredMessage}, -}; -use gear_core_errors::{ReplyCode, SuccessReplyReason}; -use std::{cell::RefCell, convert::TryInto}; +mod actor; +mod manager; -pub struct Mailbox<'a> { - manager: &'a RefCell, - user_id: ProgramId, -} - -impl<'a> Mailbox<'a> { - pub(crate) fn new(user_id: ProgramId, manager: &'a RefCell) -> Mailbox<'a> { - Mailbox { user_id, manager } - } - - pub fn contains + Clone>(&self, log: &T) -> bool { - let log: Log = log.clone().into(); - if let Some(mailbox) = self.manager.borrow().mailbox.get(&self.user_id) { - return mailbox.iter().any(|message| log.eq(message)); - } - self.manager - .borrow_mut() - .mailbox - .insert(self.user_id, Vec::default()); - false - } - - pub fn take_message>(&self, log: T) -> MessageReplier { - MessageReplier::new(self.remove_message(log), self.manager) - } - - pub fn reply(&self, log: Log, payload: impl Encode, value: u128) -> RunResult { - self.reply_bytes(log, payload.encode(), value) - } - - pub fn reply_bytes(&self, log: Log, raw_payload: impl AsRef<[u8]>, value: u128) -> RunResult { - self.take_message(log).reply_bytes(raw_payload, value) - } - - pub fn claim_value>(&self, log: T) { - let message = self.remove_message(log); - self.manager.borrow_mut().send_value( - message.source(), - Some(message.destination()), - message.value(), - ); - } - - #[track_caller] - fn remove_message>(&self, log: T) -> StoredMessage { - let log = log.into(); - let mut manager = self.manager.borrow_mut(); - let messages = manager - .mailbox - .get_mut(&self.user_id) - .expect("Infallible. No mailbox associated with this user id"); - let index = messages - .iter() - .position(|message| log.eq(message)) - .expect("No message that satisfies log"); - messages.remove(index) - } -} - -pub struct MessageReplier<'a> { - log: CoreLog, - manager: &'a RefCell, -} - -impl<'a> MessageReplier<'a> { - pub(crate) fn new( - message: StoredMessage, - manager: &'a RefCell, - ) -> MessageReplier<'a> { - MessageReplier { - log: message.into(), - manager, - } - } - - pub fn reply(&self, payload: impl Encode, value: u128) -> RunResult { - self.reply_bytes(payload.encode(), value) - } - - pub fn reply_bytes(&self, raw_payload: impl AsRef<[u8]>, value: u128) -> RunResult { - let message = Message::new( - MessageId::from(self.manager.borrow_mut().fetch_inc_message_nonce()), - self.log.destination(), - self.log.source(), - raw_payload.as_ref().to_vec().try_into().unwrap(), - None, - value, - Some( - ReplyDetails::new( - self.log.id(), - ReplyCode::Success(SuccessReplyReason::Manual), - ) - .into(), - ), - ); - - self.manager - .borrow_mut() - .validate_and_run_dispatch(Dispatch::new(DispatchKind::Reply, message)) - } -} - -#[cfg(test)] -mod tests { - use std::convert::TryInto; - - use crate::{program::ProgramIdWrapper, Log, Program, System}; - use codec::Encode; - use gear_core::{ - ids::MessageId, - message::{Dispatch, DispatchKind, Message, Payload}, - }; - - #[test] - fn mailbox_walk_through_test() { - //Arranging data for future messages - let system = System::new(); - let message_id: MessageId = Default::default(); - let source_user_id = ProgramIdWrapper::from(100).0; - let destination_user_id = ProgramIdWrapper::from(200).0; - let message_payload: Payload = vec![1, 2, 3].try_into().unwrap(); - let encoded_message_payload: Payload = message_payload.encode().try_into().unwrap(); - let reply_payload: Payload = vec![3, 2, 1].try_into().unwrap(); - let encoded_reply_payload: Payload = reply_payload.encode().try_into().unwrap(); - let log = Log::builder().payload(message_payload); - - //Building message based on arranged data - let message = Message::new( - message_id, - source_user_id, - destination_user_id, - encoded_message_payload.clone(), - Default::default(), - 0, - None, - ); - - //Sending created message and extracting its log - let message_result = - system.send_dispatch(Dispatch::new(DispatchKind::Handle, message.clone())); - let message_log = message_result - .log - .last() - .expect("No message log in run result"); - - //Getting mailbox of destination user and extracting message - let destination_user_mailbox = system.get_mailbox(destination_user_id); - let message_replier = destination_user_mailbox.take_message(log); - - //Replying on sended message and extracting log - let reply_log = message_replier.reply(reply_payload, 0).log; - let last_reply_log = reply_log.last().expect("No message log in run result"); - - //Sending one more message to be sure that no critical move semantic didn't - // occur - let second_message_result = - system.send_dispatch(Dispatch::new(DispatchKind::Handle, message)); - let second_message_log = message_result - .log - .last() - .expect("No message log in run result"); - - //Asserting results - assert!(!message_result.main_failed); - assert!(!message_result.others_failed); - assert!(!second_message_result.main_failed); - assert!(!second_message_result.others_failed); - assert_eq!(reply_log.len(), 1); - assert_eq!(last_reply_log.payload(), encoded_reply_payload.inner()); - assert_eq!(message_log.payload(), encoded_message_payload.inner()); - assert_eq!( - second_message_log.payload(), - encoded_message_payload.inner() - ); - } - - #[test] - fn mailbox_deletes_message_after_reply() { - //Arranging data for future messages - let system = System::new(); - let message_id: MessageId = Default::default(); - let source_user_id = ProgramIdWrapper::from(100).0; - let destination_user_id = ProgramIdWrapper::from(200).0; - let message_payload: Payload = vec![1, 2, 3].try_into().unwrap(); - let reply_payload: Payload = vec![3, 2, 1].try_into().unwrap(); - let message_log = Log::builder().payload(message_payload.clone()); - - //Building message based on arranged data - let message = Message::new( - message_id, - source_user_id, - destination_user_id, - message_payload.encode().try_into().unwrap(), - Default::default(), - 0, - None, - ); - - //Sending created message - system.send_dispatch(Dispatch::new(DispatchKind::Handle, message)); - - //Getting mailbox of destination user and replying on it - let mut destination_user_mailbox = system.get_mailbox(destination_user_id); - destination_user_mailbox.reply(message_log.clone(), reply_payload, 0); - - //Making sure that original message deletes after reply - destination_user_mailbox = system.get_mailbox(destination_user_id); - assert!(!destination_user_mailbox.contains(&message_log)) - } - - #[test] - fn mailbox_reply_bytes_test() { - //Arranging data for future messages - let system = System::new(); - let message_id: MessageId = Default::default(); - let source_user_id = ProgramIdWrapper::from(100).0; - let destination_user_id = ProgramIdWrapper::from(200).0; - let message_payload: Payload = vec![1, 2, 3].try_into().unwrap(); - let reply_payload_array: [u8; 3] = [3, 2, 1]; - let reply_payload: Payload = reply_payload_array.to_vec().try_into().unwrap(); - let log = Log::builder().payload(message_payload.clone()); - - //Building message based on arranged data - let message = Message::new( - message_id, - source_user_id, - destination_user_id, - message_payload.encode().try_into().unwrap(), - Default::default(), - 0, - None, - ); - - //Sending created message - system.send_dispatch(Dispatch::new(DispatchKind::Handle, message)); - - //Getting mailbox of destination user and extracting message - let destination_user_mailbox = system.get_mailbox(destination_user_id); - let message_replier = destination_user_mailbox.take_message(log); - - //Replying by bytes and extracting result log - let result = message_replier.reply_bytes(reply_payload_array, 0); - let result_log = result.log; - let last_result_log = result_log.last().expect("No message log in run result"); - - assert_eq!(last_result_log.payload(), reply_payload.inner()); - } - - #[test] - fn mailbox_deletes_message_after_taking() { - //Arranging data for future messages - let system = System::new(); - let message_id: MessageId = Default::default(); - let source_user_id = ProgramIdWrapper::from(100).0; - let destination_user_id = ProgramIdWrapper::from(200).0; - let message_payload: Payload = vec![1, 2, 3].try_into().unwrap(); - let log = Log::builder().payload(message_payload.clone()); - - //Building message based on arranged data - let message = Message::new( - message_id, - source_user_id, - destination_user_id, - message_payload.encode().try_into().unwrap(), - Default::default(), - 0, - None, - ); - - //Sending created message - system.send_dispatch(Dispatch::new(DispatchKind::Handle, message)); - - //Getting mailbox of destination user and extracting message - let destination_user_mailbox = system.get_mailbox(destination_user_id); - destination_user_mailbox.take_message(log.clone()); - - //Making sure that taken message is deleted - assert!(!destination_user_mailbox.contains(&log)) - } - - #[test] - #[should_panic(expected = "No message that satisfies log")] - fn take_unknown_log_message() { - // Arranging data for future messages - let system = System::new(); - let source_user_id = 100; - let destination_user_id = 200; - let log = Log::builder().source(source_user_id); - - // Taking mailbox and message that doesn't exists - let mailbox = system.get_mailbox(destination_user_id); - mailbox.take_message(log); - } - - #[test] - #[should_panic(expected = "Mailbox available only for users")] - fn take_programs_mailbox() { - // Setting up variables for test - let system = System::new(); - let restricted_user_id = 42; - - Program::from_binary_with_id( - &system, - restricted_user_id, - demo_futures_unordered::WASM_BINARY, - ); - - // Getting user id that is already registered as a program - system.get_mailbox(restricted_user_id); - } - - #[test] - fn claim_value_from_mailbox() { - let system = System::new(); - let message_id: MessageId = Default::default(); - let sender_id = 1; - let receiver_id = 42; - let payload = b"hello".to_vec(); - - let log = Log::builder() - .source(sender_id) - .dest(receiver_id) - .payload(payload.clone()); - - let message = Message::new( - message_id, - sender_id.into(), - receiver_id.into(), - payload.encode().try_into().unwrap(), - Default::default(), - 2 * crate::EXISTENTIAL_DEPOSIT, - None, - ); - - system.mint_to(sender_id, 2 * crate::EXISTENTIAL_DEPOSIT); - system.send_dispatch(Dispatch::new(DispatchKind::Handle, message)); - - let receiver_mailbox = system.get_mailbox(receiver_id); - receiver_mailbox.claim_value(log); - - assert_eq!( - system.balance_of(receiver_id), - 2 * crate::EXISTENTIAL_DEPOSIT - ); - } - - #[test] - fn delayed_dispatches_works() { - // Arranging data for future messages. - let system = System::new(); - let message_id: MessageId = Default::default(); - let source_user_id = ProgramIdWrapper::from(100).0; - let destination_user_id = ProgramIdWrapper::from(200).0; - let message_payload: Payload = vec![1, 2, 3].try_into().unwrap(); - let log = Log::builder().payload(message_payload.clone()); - - // Building message based on arranged data. - let message = Message::new( - message_id, - source_user_id, - destination_user_id, - message_payload.encode().try_into().unwrap(), - Default::default(), - 0, - None, - ); - - let bn_before_schedule = 5; - let scheduled_delay = 10; - system.0.borrow_mut().send_delayed_dispatch( - Dispatch::new(DispatchKind::Handle, message), - scheduled_delay, - ); - - let mailbox = system.get_mailbox(destination_user_id); - assert!(!mailbox.contains(&log)); - - // Run to block number before scheduled delay - assert_eq!(system.spend_blocks(bn_before_schedule).len(), 0); - assert!(!mailbox.contains(&log)); - - // Run to block number at scheduled delay - assert_eq!( - system - .spend_blocks(scheduled_delay - bn_before_schedule) - .len(), - 1 - ); - assert!(mailbox.contains(&log)); - } -} +pub use actor::ActorMailbox; +pub(crate) use manager::MailboxManager; diff --git a/gtest/src/mailbox/actor.rs b/gtest/src/mailbox/actor.rs new file mode 100644 index 00000000000..940b8c42e66 --- /dev/null +++ b/gtest/src/mailbox/actor.rs @@ -0,0 +1,246 @@ +// 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 . + +use crate::{manager::ExtManager, Log, RunResult, GAS_ALLOWANCE}; +use codec::Encode; +use gear_common::{auxiliary::mailbox::*, storage::Interval}; +use gear_core::{ + ids::{prelude::MessageIdExt, MessageId, ProgramId}, + message::{ReplyMessage, ReplyPacket}, +}; +use std::{cell::RefCell, convert::TryInto}; + +/// Interface to a particular user mailbox. +/// +/// Gives a simplified interface to perform some operations +/// over a particular user mailbox. +pub struct ActorMailbox<'a> { + manager: &'a RefCell, + user_id: ProgramId, +} + +impl<'a> ActorMailbox<'a> { + pub(crate) fn new(user_id: ProgramId, manager: &'a RefCell) -> ActorMailbox<'a> { + ActorMailbox { user_id, manager } + } + + /// Checks whether message with some traits (defined in `log`) is + /// in mailbox. + pub fn contains + Clone>(&self, log: &T) -> bool { + self.find_message_by_log(&log.clone().into()).is_some() + } + + /// Sends user reply message. + /// + /// Same as [`Self::reply_bytes`], but payload is encoded + /// in a *partiy-scale-codec* format. + pub fn reply( + &self, + log: Log, + payload: impl Encode, + value: u128, + ) -> Result { + self.reply_bytes(log, payload.encode(), value) + } + + /// Sends user reply message to a mailboxed message + /// finding it in the mailbox by traits of `log`. + pub fn reply_bytes( + &self, + log: Log, + raw_payload: impl AsRef<[u8]>, + value: u128, + ) -> Result { + let mailboxed_msg = self + .find_message_by_log(&log) + .ok_or(MailboxErrorImpl::ElementNotFound)?; + self.manager + .borrow() + .mailbox + .remove(self.user_id, mailboxed_msg.id())?; + + let dispatch = { + let packet = ReplyPacket::new_with_gas( + raw_payload + .as_ref() + .to_vec() + .try_into() + .unwrap_or_else(|err| panic!("Can't send reply with such payload: {err:?}")), + GAS_ALLOWANCE, + value, + ); + let reply_message = + ReplyMessage::from_packet(MessageId::generate_reply(mailboxed_msg.id()), packet); + + reply_message.into_dispatch(self.user_id, mailboxed_msg.source(), mailboxed_msg.id()) + }; + + Ok(self + .manager + .borrow_mut() + .validate_and_run_dispatch(dispatch)) + } + + /// Claims value from a message in mailbox. + /// + /// If message with traits defined in `log` is not found, an error is + /// returned. + pub fn claim_value>(&self, log: T) -> Result<(), MailboxErrorImpl> { + let mailboxed_msg = self + .find_message_by_log(&log.into()) + .ok_or(MailboxErrorImpl::ElementNotFound)?; + self.manager + .borrow_mut() + .claim_value_from_mailbox(self.user_id, mailboxed_msg.id()) + .unwrap_or_else(|e| unreachable!("Unexpected mailbox error: {e:?}")); + + Ok(()) + } + + fn find_message_by_log(&self, log: &Log) -> Option { + self.get_user_mailbox() + .find_map(|(msg, _)| log.eq(&msg).then_some(msg)) + } + + fn get_user_mailbox(&self) -> impl Iterator)> { + self.manager.borrow().mailbox.iter_key(self.user_id) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + program::ProgramIdWrapper, Log, Program, System, EXISTENTIAL_DEPOSIT, GAS_ALLOWANCE, + }; + use codec::Encode; + use demo_constructor::{Call, Calls, Scheme, WASM_BINARY}; + use gear_common::auxiliary::mailbox::MailboxErrorImpl; + use gear_core::{ + ids::{prelude::MessageIdExt, MessageId, ProgramId}, + message::{Dispatch, DispatchKind, HandleMessage, HandlePacket, Payload}, + }; + use std::convert::TryInto; + + 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 payload = b"sup!".to_vec(); + let log = Log::builder().dest(sender).payload_bytes(payload.clone()); + + let res = program.send(sender, Scheme::empty()); + assert!(!res.main_failed()); + + (program, (sender, payload, log)) + } + + #[test] + fn user2user_doesnt_reach_mailbox() { + let system = System::new(); + + let source = ProgramIdWrapper::from(100).0; + let message_id: MessageId = MessageId::generate_from_user(0, source, 0); + let destination = ProgramIdWrapper::from(200).0; + let payload: Payload = vec![1, 2, 3].try_into().expect("len exceed"); + let log = Log::builder() + .dest(destination) + .payload_bytes(payload.inner()); + + let dispatch = Dispatch::new( + DispatchKind::Handle, + HandleMessage::from_packet( + message_id, + HandlePacket::new_with_gas(destination, payload, GAS_ALLOWANCE, 0), + ) + .into_message(source), + ); + + // Log exists + let res = system.send_dispatch(dispatch); + assert!(res.contains(&log)); + + // But message doesn't exist in mailbox + let mailbox = system.get_mailbox(destination); + let res = mailbox.reply(log, b"", 0); + assert!(res.is_err()); + assert_eq!(res.unwrap_err(), MailboxErrorImpl::ElementNotFound); + } + + #[test] + fn claim_value_from_mailbox() { + 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 value_send = 2 * EXISTENTIAL_DEPOSIT; + let handle = Calls::builder().send_value(sender, payload, value_send); + let res = program.send_bytes_with_value(sender, handle.encode(), value_send); + assert!(!res.main_failed()); + assert!(res.contains(&log)); + assert_eq!(system.balance_of(sender), original_balance - value_send); + + 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); + } + + #[test] + fn reply_to_mailbox_message() { + let system = System::new(); + let (program, (sender, payload, log)) = prepare_program(&system); + + let handle = Calls::builder().send(sender, payload); + let res = program.send(sender, handle); + assert!(!res.main_failed()); + assert!(res.contains(&log)); + + let mailbox = system.get_mailbox(sender); + assert!(mailbox.contains(&log)); + let res = mailbox + .reply(log, Calls::default(), 0) + .expect("sending reply failed: didn't find message in mailbox"); + assert!(!res.main_failed()); + } + + #[test] + fn delayed_mailbox_message() { + let system = System::new(); + let (program, (sender, payload, log)) = prepare_program(&system); + + let delay = 5; + let handle = Calls::builder().add_call(Call::Send( + sender.into(), + payload.into(), + None, + 0.into(), + delay.into(), + )); + let res = program.send(sender, handle); + assert!(!res.main_failed()); + + let results = system.spend_blocks(delay); + let delayed_dispatch_res = results.last().expect("internal error: no blocks spent"); + + assert!(delayed_dispatch_res.contains(&log)); + let mailbox = system.get_mailbox(sender); + assert!(mailbox.contains(&log)); + } +} diff --git a/gtest/src/mailbox/manager.rs b/gtest/src/mailbox/manager.rs new file mode 100644 index 00000000000..5df3d041d50 --- /dev/null +++ b/gtest/src/mailbox/manager.rs @@ -0,0 +1,89 @@ +// 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 . + +//! Mailbox manager. + +use crate::blocks::BlocksManager; +use gear_common::{ + auxiliary::mailbox::*, + storage::{GetCallback, Interval, IterableByKeyMap, Mailbox, MailboxCallbacks}, +}; +use gear_core::ids::{MessageId, ProgramId}; + +/// Mailbox manager which operates under the hood over +/// [`gear_common::AuxiliaryMailbox`]. +#[derive(Debug, Default)] +pub(crate) struct MailboxManager; + +impl MailboxManager { + /// Insert user message into mailbox. + pub(crate) fn insert(&self, message: MailboxedMessage) -> Result<(), MailboxErrorImpl> { + as Mailbox>::insert(message, u32::MAX) + } + + /// Remove user message from mailbox. + pub(crate) fn remove( + &self, + user: ProgramId, + reply_to: MessageId, + ) -> Result<(MailboxedMessage, Interval), MailboxErrorImpl> { + as Mailbox>::remove(user, reply_to) + } + + /// Returns an iterator over all `to` user messages and their deadlines + /// inside mailbox. + pub(crate) fn iter_key( + &self, + to: ProgramId, + ) -> impl Iterator)> { + as IterableByKeyMap<_>>::iter_key(to) + } + + /// Fully reset mailbox. + /// + /// # Note: + /// Must be called by `MailboxManager` owner to reset mailbox + /// when the owner is dropped. + pub(crate) fn reset(&self) { + as Mailbox>::clear(); + } +} + +/// Mailbox callbacks implementor. +pub(crate) struct MailboxCallbacksImpl; + +impl MailboxCallbacks for MailboxCallbacksImpl { + type Value = MailboxedMessage; + type BlockNumber = BlockNumber; + + type GetBlockNumber = GetBlockNumberImpl; + + type OnInsert = (); + type OnRemove = (); +} + +/// Block number getter. +/// +/// Used to get block number to insert message into mailbox. +pub(crate) struct GetBlockNumberImpl; + +impl GetCallback for GetBlockNumberImpl { + fn call() -> BlockNumber { + BlocksManager::new().get().height + } +} diff --git a/gtest/src/manager.rs b/gtest/src/manager.rs index 5271989c05d..85e3174d981 100644 --- a/gtest/src/manager.rs +++ b/gtest/src/manager.rs @@ -20,6 +20,7 @@ use crate::{ blocks::BlocksManager, gas_tree::GasTreeManager, log::{CoreLog, RunResult}, + mailbox::MailboxManager, program::{Gas, WasmProgram}, Result, TestError, DISPATCH_HOLD_COST, EPOCH_DURATION_IN_BLOCKS, EXISTENTIAL_DEPOSIT, GAS_ALLOWANCE, INITIAL_RANDOM_SEED, MAILBOX_THRESHOLD, MAX_RESERVATIONS, @@ -37,6 +38,7 @@ use core_processor::{ }, ContextChargedForCode, ContextChargedForInstrumentation, Ext, }; +use gear_common::auxiliary::mailbox::MailboxErrorImpl; use gear_core::{ code::{Code, CodeAndId, InstrumentedCode, InstrumentedCodeAndId, TryNewCodeConfig}, ids::{prelude::*, CodeId, MessageId, ProgramId, ReservationId}, @@ -235,7 +237,7 @@ pub(crate) struct ExtManager { pub(crate) opt_binaries: BTreeMap>, pub(crate) meta_binaries: BTreeMap>, pub(crate) dispatches: VecDeque, - pub(crate) mailbox: HashMap>, + pub(crate) mailbox: MailboxManager, pub(crate) wait_list: BTreeMap<(ProgramId, MessageId), StoredDispatch>, pub(crate) wait_list_schedules: BTreeMap>, pub(crate) gas_tree: GasTreeManager, @@ -303,9 +305,7 @@ impl ExtManager { } pub(crate) fn free_id_nonce(&mut self) -> u64 { - while self.actors.contains_key(&self.id_nonce.into()) - || self.mailbox.contains_key(&self.id_nonce.into()) - { + while self.actors.contains_key(&self.id_nonce.into()) { self.id_nonce += 1; } self.id_nonce @@ -432,7 +432,7 @@ impl ExtManager { #[track_caller] pub(crate) fn run_dispatch(&mut self, dispatch: Dispatch, from_task_pool: bool) -> RunResult { - self.prepare_for(&dispatch); + self.prepare_for(&dispatch, !from_task_pool); if self.is_program(&dispatch.destination()) { if !from_task_pool { @@ -448,17 +448,20 @@ impl ExtManager { .unwrap_or_else(|| unreachable!("message from program API has always gas")); self.gas_tree .create(dispatch.source(), dispatch.id(), gas_limit) - .unwrap_or_else(|e| unreachable!("GasTree corrupter! {:?}", e)); + .unwrap_or_else(|e| unreachable!("GasTree corrupted! {:?}", e)); } self.dispatches.push_back(dispatch.into_stored()); } else { let message = dispatch.into_parts().1.into_stored(); - - self.mailbox - .entry(message.destination()) - .or_default() - .push(message.clone()); + if let (Ok(mailbox_msg), true) = ( + message.clone().try_into(), + self.is_program(&message.source()), + ) { + self.mailbox + .insert(mailbox_msg) + .unwrap_or_else(|e| unreachable!("Mailbox corrupted! {:?}", e)); + } self.log.push(message) } @@ -615,18 +618,21 @@ impl ExtManager { .unwrap_or_default() } - pub(crate) fn claim_value_from_mailbox(&mut self, id: &ProgramId) { - let messages = self.mailbox.remove(id); - if let Some(messages) = messages { - messages.into_iter().for_each(|message| { - self.send_value( - message.source(), - Some(message.destination()), - message.value(), - ); - self.message_consumed(message.id()); - }); - } + pub(crate) fn claim_value_from_mailbox( + &mut self, + to: ProgramId, + from_mid: MessageId, + ) -> Result<(), MailboxErrorImpl> { + let (message, _) = self.mailbox.remove(to, from_mid)?; + + self.send_value( + message.source(), + Some(message.destination()), + message.value(), + ); + self.message_consumed(message.id()); + + Ok(()) } #[track_caller] @@ -665,7 +671,7 @@ impl ExtManager { } #[track_caller] - fn prepare_for(&mut self, dispatch: &Dispatch) { + fn prepare_for(&mut self, dispatch: &Dispatch, update_block: bool) { self.msg_id = dispatch.id(); self.origin = dispatch.source(); self.log.clear(); @@ -680,6 +686,9 @@ impl ExtManager { m }; self.gas_allowance = Gas(GAS_ALLOWANCE); + if update_block { + let _ = self.blocks_manager.next_block(); + } } fn mark_failed(&mut self, msg_id: MessageId) { @@ -1020,14 +1029,17 @@ impl JournalHandler for ExtManager { let gas_limit = dispatch.gas_limit().unwrap_or_default(); let stored_message = dispatch.into_stored().into_parts().1; - self.gas_tree - .cut(message_id, stored_message.id(), gas_limit) - .unwrap_or_else(|e| unreachable!("GasTree corrupted! {:?}", e)); + if let Ok(mailbox_msg) = stored_message.clone().try_into() { + self.gas_tree + .cut(message_id, stored_message.id(), gas_limit) + .unwrap_or_else(|e| unreachable!("GasTree corrupted! {:?}", e)); - self.mailbox - .entry(stored_message.destination()) - .or_default() - .push(stored_message.clone()); + self.mailbox + .insert(mailbox_msg) + .unwrap_or_else(|e| unreachable!("Mailbox corrupted! {:?}", e)); + } else { + log::debug!("A reply message is sent to user: {stored_message:?}"); + }; self.log.push(stored_message); } diff --git a/gtest/src/program.rs b/gtest/src/program.rs index 4e27ea399d8..00ae9123a62 100644 --- a/gtest/src/program.rs +++ b/gtest/src/program.rs @@ -963,8 +963,26 @@ mod tests { assert_eq!(prog.balance(), (2 + 4 + 6) * crate::EXISTENTIAL_DEPOSIT); // Request to smash the piggy bank and send the value to the receiver address - prog.send_bytes(receiver, b"smash"); - sys.claim_value_from_mailbox(receiver); + let res = prog.send_bytes(receiver, b"smash"); + let reply_to_id = { + let log = res.log(); + // 1 auto reply and 1 message from program + assert_eq!(log.len(), 2); + + let core_log = log + .iter() + .find(|&core_log| { + core_log.eq(&Log::builder().dest(receiver).payload_bytes(b"send")) + }) + .expect("message not found"); + + core_log.id() + }; + + assert!(sys + .get_mailbox(receiver) + .claim_value(Log::builder().reply_to(reply_to_id)) + .is_ok()); assert_eq!( sys.balance_of(receiver), (2 + 4 + 6) * crate::EXISTENTIAL_DEPOSIT @@ -1016,12 +1034,20 @@ mod tests { // Get zero value to the receiver's mailbox prog.send_bytes(receiver, b"smash"); + 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); + // 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"); // Check receiver's balance - sys.claim_value_from_mailbox(receiver); + 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); } @@ -1045,6 +1071,7 @@ mod tests { let mut prog = Program::from_binary_with_id(&sys, 420, WASM_BINARY); let signer = 42; + let signer_mailbox = sys.get_mailbox(signer); // Init capacitor with limit = 15 prog.send(signer, InitMessage::Capacitor("15".to_string())); @@ -1070,7 +1097,7 @@ mod tests { .payload_bytes("Discharged: 20"); // dbg!(log.clone()); assert!(response.contains(&log)); - sys.claim_value_from_mailbox(signer); + assert!(signer_mailbox.claim_value(log).is_ok()); prog.load_memory_dump("./296c6962726/demo_custom.dump"); drop(cleanup); @@ -1082,7 +1109,7 @@ mod tests { .dest(signer) .payload_bytes("Discharged: 20"); assert!(response.contains(&log)); - sys.claim_value_from_mailbox(signer); + assert!(signer_mailbox.claim_value(log).is_ok()); } #[test] diff --git a/gtest/src/system.rs b/gtest/src/system.rs index d42b6e7d842..9b81f2c2a5d 100644 --- a/gtest/src/system.rs +++ b/gtest/src/system.rs @@ -18,7 +18,7 @@ use crate::{ log::RunResult, - mailbox::Mailbox, + mailbox::ActorMailbox, manager::{Actors, Balance, ExtManager, MintMode}, program::{Program, ProgramIdWrapper}, }; @@ -303,13 +303,12 @@ impl System { /// The mailbox contains messages from the program that are waiting /// for user action. #[track_caller] - pub fn get_mailbox>(&self, id: ID) -> Mailbox { + pub fn get_mailbox>(&self, id: ID) -> ActorMailbox { let program_id = id.into().0; if !self.0.borrow().is_user(&program_id) { panic!("Mailbox available only for users"); } - self.0.borrow_mut().mailbox.entry(program_id).or_default(); - Mailbox::new(program_id, &self.0) + ActorMailbox::new(program_id, &self.0) } /// Mint balance to user with given `id` and `value`. @@ -325,12 +324,6 @@ impl System { let actor_id = id.into().0; self.0.borrow().balance_of(&actor_id) } - - /// Claim the user's value from the mailbox with given `id`. - pub fn claim_value_from_mailbox>(&self, id: ID) { - let actor_id = id.into().0; - self.0.borrow_mut().claim_value_from_mailbox(&actor_id); - } } impl Drop for System { @@ -338,6 +331,7 @@ impl Drop for System { // Uninitialize SYSTEM_INITIALIZED.with_borrow_mut(|initialized| *initialized = false); self.0.borrow().gas_tree.reset(); + self.0.borrow().mailbox.reset(); } }