use crate::{
GAS_ALLOWANCE, Gas, Value,
error::usage_panic,
log::{BlockRunResult, CoreLog},
manager::ExtManager,
program::{Program, ProgramIdWrapper},
state::{
accounts::Accounts, bridge::BridgeBuiltinStorage, mailbox::ActorMailbox,
programs::ProgramsStorageManager,
},
};
use core_processor::common::JournalNote;
use gear_common::MessageId;
use gear_core::{
ids::{
ActorId, CodeId,
prelude::{CodeIdExt, MessageIdExt},
},
message::{Dispatch, DispatchKind, Message, ReplyDetails},
pages::GearPage,
program::Program as PrimaryProgram,
rpc::ReplyInfo,
};
use gear_lazy_pages::{LazyPagesStorage, LazyPagesVersion};
use gear_lazy_pages_common::LazyPagesInitContext;
use parity_scale_codec::{Decode, DecodeAll};
use path_clean::PathClean;
use std::{borrow::Cow, cell::RefCell, env, fs, mem, panic, path::Path};
use tracing_subscriber::EnvFilter;
thread_local! {
static SYSTEM_INITIALIZED: RefCell<bool> = const { RefCell::new(false) };
}
#[derive(Decode)]
struct PageKey {
_page_storage_prefix: [u8; 32],
program_id: ActorId,
_memory_infix: u32,
page: GearPage,
}
#[derive(Debug)]
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");
ProgramsStorageManager::program_page(program_id, page).is_some()
}
fn load_page(&mut self, mut key: &[u8], buffer: &mut [u8]) -> Option<u32> {
let PageKey {
program_id, page, ..
} = PageKey::decode_all(&mut key).expect("Invalid key");
ProgramsStorageManager::program_page(program_id, page).map(|page_buf| {
buffer.copy_from_slice(page_buf.as_ref());
page_buf.len() as u32
})
}
}
pub struct System(pub(crate) RefCell<ExtManager>);
impl System {
pub(crate) const PAGE_STORAGE_PREFIX: [u8; 32] = *b"gtestgtestgtestgtestgtestgtest00";
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
SYSTEM_INITIALIZED.with_borrow_mut(|initialized| {
if *initialized {
panic!("Impossible to have multiple instances of the `System`.");
}
let ext_manager = ExtManager::new();
gear_lazy_pages::init(
LazyPagesVersion::Version1,
LazyPagesInitContext::new(Self::PAGE_STORAGE_PREFIX),
PagesStorage,
)
.expect("Failed to init lazy-pages");
*initialized = true;
Self(RefCell::new(ext_manager))
})
}
pub fn init_logger(&self) {
self.init_logger_with_default_filter("gwasm=debug");
}
pub fn init_verbose_logger(&self) {
self.init_logger_with_default_filter("gwasm=debug,gtest=debug");
}
pub fn init_logger_with_default_filter<'a>(&self, default_filter: impl Into<Cow<'a, str>>) {
let filter = if env::var(EnvFilter::DEFAULT_ENV).is_ok() {
EnvFilter::from_default_env()
} else {
EnvFilter::new(default_filter.into())
};
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.without_time()
.with_thread_names(true)
.try_init();
}
pub fn queue_len(&self) -> usize {
self.0.borrow().dispatches.len()
}
pub fn run_next_block(&self) -> BlockRunResult {
self.run_next_block_with_allowance(GAS_ALLOWANCE)
}
pub fn run_next_block_with_allowance(&self, allowance: Gas) -> BlockRunResult {
if allowance > GAS_ALLOWANCE {
usage_panic!(
"Provided allowance more than allowed limit of {GAS_ALLOWANCE}. \
Please, provide an allowance less than or equal to the limit."
);
}
self.0.borrow_mut().run_new_block(allowance)
}
pub fn run_to_block(&self, bn: u32) -> Vec<BlockRunResult> {
let mut manager = self.0.borrow_mut();
let mut current_block = manager.block_height();
if current_block > bn {
usage_panic!("Can't run blocks until bn {bn}, as current bn is {current_block}");
}
let mut ret = Vec::with_capacity((bn - current_block) as usize);
while current_block != bn {
let res = manager.run_new_block(GAS_ALLOWANCE);
ret.push(res);
current_block = manager.block_height();
}
ret
}
pub fn run_scheduled_tasks(&self, amount: u32) -> Vec<BlockRunResult> {
let mut manager = self.0.borrow_mut();
let block_height = manager.block_height();
(block_height..block_height + amount)
.map(|_| {
let block_info = manager.blocks_manager.next_block();
let next_block_number = block_info.height;
manager.process_tasks(next_block_number);
let log = mem::take(&mut manager.log)
.into_iter()
.map(CoreLog::from)
.collect();
BlockRunResult {
block_info,
gas_allowance_spent: GAS_ALLOWANCE - manager.gas_allowance,
log,
..Default::default()
}
})
.collect()
}
pub fn block_height(&self) -> u32 {
self.0.borrow().block_height()
}
pub fn block_timestamp(&self) -> u64 {
self.0.borrow().blocks_manager.get().timestamp
}
pub fn get_program<ID: Into<ProgramIdWrapper>>(&self, id: ID) -> Option<Program<'_>> {
let id = id.into().0;
if ProgramsStorageManager::is_program(id) {
Some(Program {
id,
manager: &self.0,
})
} else {
None
}
}
pub fn last_program(&self) -> Option<Program<'_>> {
self.programs().into_iter().next_back()
}
pub fn programs(&self) -> Vec<Program<'_>> {
ProgramsStorageManager::program_ids()
.into_iter()
.map(|id| Program {
id,
manager: &self.0,
})
.collect()
}
pub fn is_active_program<ID: Into<ProgramIdWrapper>>(&self, id: ID) -> bool {
let program_id = id.into().0;
ProgramsStorageManager::is_active_program(program_id)
}
pub fn inheritor_of<ID: Into<ProgramIdWrapper>>(&self, id: ID) -> Option<ActorId> {
let program_id = id.into().0;
ProgramsStorageManager::access_primary_program(program_id, |program| {
program.and_then(|program| {
if let PrimaryProgram::Exited(inheritor_id) = program {
Some(*inheritor_id)
} else {
None
}
})
})
}
pub fn submit_local_code_file<P: AsRef<Path>>(&self, code_path: P) -> CodeId {
let path = env::current_dir()
.expect("Unable to get root directory of the project")
.join(code_path)
.clean();
self.submit_code_file(path)
}
pub fn submit_code_file<P: AsRef<Path>>(&self, code_path: P) -> CodeId {
let code = fs::read(&code_path).unwrap_or_else(|_| {
usage_panic!(
"Failed to read file {}",
code_path.as_ref().to_string_lossy()
)
});
self.submit_code(code)
}
pub fn submit_code(&self, binary: impl Into<Vec<u8>>) -> CodeId {
let code = binary.into();
let code_id = CodeId::generate(code.as_ref());
self.0.borrow_mut().store_code(code_id, code);
code_id
}
pub fn submitted_code(&self, code_id: CodeId) -> Option<Vec<u8>> {
self.0
.borrow()
.original_code(code_id)
.map(|code| code.to_vec())
}
pub fn get_mailbox<ID: Into<ProgramIdWrapper>>(&self, id: ID) -> ActorMailbox<'_> {
let program_id = id.into().0;
if !ProgramsStorageManager::is_user(program_id) {
usage_panic!("Mailbox available only for users. Please, provide a user id.");
}
ActorMailbox::new(program_id, &self.0)
}
pub fn mint_to<ID: Into<ProgramIdWrapper>>(&self, id: ID, value: Value) {
let id = id.into().0;
if ProgramsStorageManager::is_program(id) {
usage_panic!(
"Attempt to mint value to a program {id:?}. Please, use `System::transfer` instead"
);
}
self.0.borrow_mut().mint_to(id, value);
}
pub fn transfer(
&self,
from: impl Into<ProgramIdWrapper>,
to: impl Into<ProgramIdWrapper>,
value: Value,
keep_alive: bool,
) {
let from = from.into().0;
let to = to.into().0;
if ProgramsStorageManager::is_program(from) {
usage_panic!(
"Attempt to transfer from a program {from:?}. Please, provide `from` user id."
);
}
Accounts::transfer(from, to, value, keep_alive);
}
pub fn balance_of<ID: Into<ProgramIdWrapper>>(&self, id: ID) -> Value {
let actor_id = id.into().0;
self.0.borrow().balance_of(actor_id)
}
pub fn calculate_reply_for_handle(
&self,
origin: impl Into<ProgramIdWrapper>,
destination: impl Into<ProgramIdWrapper>,
payload: impl Into<Vec<u8>>,
gas_limit: u64,
value: Value,
) -> Result<ReplyInfo, String> {
let mut manager_mut = self.0.borrow_mut();
manager_mut.enable_overlay();
manager_mut.dispatches.clear();
let origin = origin.into().0;
let destination = destination.into().0;
let payload = payload
.into()
.try_into()
.expect("failed to convert payload to limited payload");
let block_number = manager_mut.block_height() + 1;
let message = Message::new(
MessageId::generate_from_user(
block_number,
origin,
manager_mut.fetch_inc_message_nonce() as u128,
),
origin,
destination,
payload,
Some(gas_limit),
value,
None,
);
if !manager_mut.is_builtin(destination)
&& !ProgramsStorageManager::is_active_program(destination)
{
usage_panic!("Actor with {destination} id is not executable");
}
let dispatch = Dispatch::new(DispatchKind::Handle, message);
let message_id = manager_mut.validate_and_route_dispatch(dispatch);
let block_config = manager_mut.block_config();
while let Some(dispatch) = manager_mut.dispatches.pop_front() {
manager_mut.gas_allowance = GAS_ALLOWANCE;
manager_mut.messages_processing_enabled = true;
let journal = manager_mut.process_dispatch(&block_config, dispatch);
for note in &journal {
let JournalNote::SendDispatch { dispatch, .. } = note else {
continue;
};
if let Some(code) = dispatch
.reply_details()
.map(ReplyDetails::into_parts)
.and_then(|(replied_to, code)| replied_to.eq(&message_id).then_some(code))
{
manager_mut.disable_overlay();
return Ok(ReplyInfo {
payload: dispatch.payload_bytes().to_vec(),
value: dispatch.value(),
code,
});
}
}
core_processor::handle_journal(journal, &mut *manager_mut);
}
manager_mut.disable_overlay();
Err(String::from("Queue is empty, but reply wasn't found"))
}
}
impl Drop for System {
fn drop(&mut self) {
SYSTEM_INITIALIZED.with_borrow_mut(|initialized| *initialized = false);
let manager = self.0.borrow();
manager.gas_tree.clear();
manager.mailbox.clear();
manager.task_pool.clear();
manager.waitlist.clear();
manager.blocks_manager.reset();
manager.bank.clear();
manager.nonce_manager.reset();
manager.dispatches.clear();
manager.dispatches_stash.clear();
ProgramsStorageManager::clear();
Accounts::clear();
BridgeBuiltinStorage::clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DEFAULT_USER_ALICE, EXISTENTIAL_DEPOSIT, Log, MAX_USER_GAS_LIMIT};
use gear_core_errors::{ReplyCode, SuccessReplyReason};
#[test]
#[should_panic(expected = "Impossible to have multiple instances of the `System`.")]
fn test_system_being_singleton() {
let _first_instance = System::new();
let _second_instance = System::new();
}
#[test]
fn test_multithread_copy_singleton() {
let first_instance = System::new();
first_instance.run_scheduled_tasks(5);
assert_eq!(first_instance.block_height(), 5);
let h = std::thread::spawn(|| {
let second_instance = System::new();
second_instance.run_scheduled_tasks(10);
assert_eq!(second_instance.block_height(), 10);
});
h.join().expect("internal error failed joining thread");
assert_eq!(first_instance.block_height(), 5);
}
#[test]
fn test_bn_adjustments() {
let sys = System::new();
assert_eq!(sys.block_height(), 0);
let res = sys.run_next_block();
let block_info = res.block_info;
assert_eq!(block_info.height, sys.block_height());
assert_eq!(block_info.height, 1);
let current_height = block_info.height;
let until_height = 5;
let results = sys.run_to_block(until_height);
assert_eq!(results.len(), (until_height - current_height) as usize);
let first_run = results.first().expect("checked above");
assert_eq!(first_run.block_info.height, current_height + 1);
let last_run = results.last().expect("checked above");
assert_eq!(last_run.block_info.height, until_height);
assert_eq!(last_run.block_info.height, sys.block_height());
let current_height = last_run.block_info.height;
let amount_of_blocks = 10;
let results = sys.run_scheduled_tasks(amount_of_blocks);
assert_eq!(results.len(), amount_of_blocks as usize);
let first_run = results.first().expect("checked above");
assert_eq!(first_run.block_info.height, current_height + 1);
let last_run = results.last().expect("checked above");
assert_eq!(
last_run.block_info.height,
current_height + amount_of_blocks
);
assert_eq!(last_run.block_info.height, 15);
}
#[test]
#[should_panic(expected = "Got message sent to incomplete user program")]
fn panic_calculate_reply_no_actor() {
let sys = System::new();
let origin = DEFAULT_USER_ALICE;
let pid = 42;
let ping_program = Program::from_binary_with_id(&sys, pid, demo_ping::WASM_BINARY);
let destination = ping_program.id();
let _ = sys.calculate_reply_for_handle(
origin,
destination,
b"PING".to_vec(),
MAX_USER_GAS_LIMIT,
0,
);
}
#[test]
fn test_calculate_reply_for_handle() {
use demo_piggy_bank::WASM_BINARY;
let sys = System::new();
let program = Program::from_binary_with_id(&sys, 42, WASM_BINARY);
let pid = program.id();
let init_mid = program.send_bytes(DEFAULT_USER_ALICE, b"");
let block_result = sys.run_next_block();
assert!(block_result.succeed.contains(&init_mid));
let program_balance_before_overlay = sys.balance_of(pid);
let alice_balance_before_overlay = sys.balance_of(DEFAULT_USER_ALICE);
let reply_info = sys
.calculate_reply_for_handle(
DEFAULT_USER_ALICE,
pid,
b"",
MAX_USER_GAS_LIMIT,
EXISTENTIAL_DEPOSIT * 10,
)
.expect("Failed to calculate reply for handle");
assert_eq!(
reply_info.code,
ReplyCode::Success(SuccessReplyReason::Auto)
);
assert_eq!(sys.balance_of(pid), program_balance_before_overlay);
assert_eq!(
sys.balance_of(DEFAULT_USER_ALICE),
alice_balance_before_overlay
);
let storing_value = EXISTENTIAL_DEPOSIT * 10;
let handle_mid1 = program.send_bytes_with_value(DEFAULT_USER_ALICE, b"", storing_value);
let block_result = sys.run_next_block();
assert!(block_result.succeed.contains(&handle_mid1));
assert!(
block_result.contains(
&Log::builder()
.dest(DEFAULT_USER_ALICE)
.reply_code(reply_info.code)
)
);
let alice_expected_balance_after_msg1 =
alice_balance_before_overlay - storing_value - block_result.spent_value();
assert_eq!(
sys.balance_of(pid),
program_balance_before_overlay + storing_value
);
assert_eq!(
sys.balance_of(DEFAULT_USER_ALICE),
alice_expected_balance_after_msg1
);
let reply_info = sys
.calculate_reply_for_handle(DEFAULT_USER_ALICE, pid, b"smash", MAX_USER_GAS_LIMIT, 0)
.expect("Failed to calculate reply for handle");
assert_eq!(
reply_info.code,
ReplyCode::Success(SuccessReplyReason::Auto)
);
assert_eq!(
sys.balance_of(pid),
program_balance_before_overlay + storing_value
);
assert_eq!(
sys.balance_of(DEFAULT_USER_ALICE),
alice_expected_balance_after_msg1
);
let mailbox = sys.get_mailbox(DEFAULT_USER_ALICE);
let log = Log::builder()
.dest(DEFAULT_USER_ALICE)
.payload_bytes(b"send");
assert!(!mailbox.contains(&log));
let handle_mid = program.send_bytes(DEFAULT_USER_ALICE, b"smash");
let block_result = sys.run_next_block();
assert!(block_result.succeed.contains(&handle_mid));
assert_eq!(sys.balance_of(pid), EXISTENTIAL_DEPOSIT);
let mailbox = sys.get_mailbox(DEFAULT_USER_ALICE);
let log = Log::builder()
.dest(DEFAULT_USER_ALICE)
.payload_bytes(b"send");
assert!(mailbox.contains(&log));
mailbox.claim_value(log).expect("Failed to claim value");
let alice_expected_balance_after_msg2 =
alice_expected_balance_after_msg1 - block_result.spent_value() + storing_value;
assert_eq!(
sys.balance_of(DEFAULT_USER_ALICE),
alice_expected_balance_after_msg2
);
}
}