use anyhow::{Context, Result};
use tycho_types::cell::Lazy;
use tycho_types::dict;
use tycho_types::error::Error;
use tycho_types::models::{
Account, AccountState, AccountStatus, BlockId, CurrencyCollection, HashUpdate, IntAddr,
LibDescr, Message, OwnedMessage, ShardAccount, SimpleLib, StdAddr, StorageInfo, StorageUsed,
TickTock, Transaction, TxInfo,
};
use tycho_types::num::{Tokens, Uint15};
use tycho_types::prelude::*;
pub use self::config::ParsedConfig;
pub use self::error::{TxError, TxResult};
use self::util::new_varuint56_truncate;
pub use self::util::{ExtStorageStat, OwnedExtStorageStat, StorageStatLimits};
mod config;
mod error;
mod util;
pub mod phase {
pub use self::action::{ActionPhaseContext, ActionPhaseFull};
pub use self::bounce::BouncePhaseContext;
pub use self::compute::{
ComputePhaseContext, ComputePhaseFull, ComputePhaseSmcInfo, TransactionInput,
};
pub use self::receive::{MsgStateInit, ReceivedMessage};
pub use self::storage::StoragePhaseContext;
mod action;
mod bounce;
mod compute;
mod credit;
mod receive;
mod storage;
}
mod tx {
mod ordinary;
mod ticktock;
}
#[derive(Debug)]
pub struct Executor<'a> {
params: &'a ExecutorParams,
config: &'a ParsedConfig,
min_lt: u64,
override_special: Option<bool>,
}
impl<'a> Executor<'a> {
pub fn new(params: &'a ExecutorParams, config: &'a ParsedConfig) -> Self {
Self {
params,
config,
min_lt: 0,
override_special: None,
}
}
pub fn with_min_lt(mut self, min_lt: u64) -> Self {
self.set_min_lt(min_lt);
self
}
pub fn set_min_lt(&mut self, min_lt: u64) {
self.min_lt = min_lt;
}
pub fn override_special(mut self, is_special: bool) -> Self {
self.override_special = Some(is_special);
self
}
#[inline]
pub fn begin_ordinary<'s, M>(
&self,
address: &StdAddr,
is_external: bool,
msg: M,
state: &'s ShardAccount,
) -> TxResult<UncommittedTransaction<'a, 's>>
where
M: LoadMessage,
{
self.begin_ordinary_ext(address, is_external, msg, state, None)
}
pub fn begin_ordinary_ext<'s, M>(
&self,
address: &StdAddr,
is_external: bool,
msg: M,
state: &'s ShardAccount,
inspector: Option<&mut ExecutorInspector<'_>>,
) -> TxResult<UncommittedTransaction<'a, 's>>
where
M: LoadMessage,
{
let msg_root = msg.load_message_root()?;
let account = state.load_account()?;
let mut exec = self.begin(address, account)?;
let info = exec.run_ordinary_transaction(is_external, msg_root.clone(), inspector)?;
UncommittedTransaction::with_info(exec, state, Some(msg_root), info).map_err(TxError::Fatal)
}
#[inline]
pub fn check_ordinary<M>(&self, address: &StdAddr, msg: M, state: &ShardAccount) -> TxResult<()>
where
M: LoadMessage,
{
self.check_ordinary_ext(address, msg, state, None)
}
pub fn check_ordinary_ext<M>(
&self,
address: &StdAddr,
msg: M,
state: &ShardAccount,
inspector: Option<&mut ExecutorInspector<'_>>,
) -> TxResult<()>
where
M: LoadMessage,
{
let msg_root = msg.load_message_root()?;
let account = state.load_account()?;
let mut exec = self.begin(address, account)?;
exec.check_ordinary_transaction(msg_root, inspector)
}
#[inline]
pub fn begin_tick_tock<'s>(
&self,
address: &StdAddr,
kind: TickTock,
state: &'s ShardAccount,
) -> TxResult<UncommittedTransaction<'a, 's>> {
self.begin_tick_tock_ext(address, kind, state, None)
}
pub fn begin_tick_tock_ext<'s>(
&self,
address: &StdAddr,
kind: TickTock,
state: &'s ShardAccount,
inspector: Option<&mut ExecutorInspector<'_>>,
) -> TxResult<UncommittedTransaction<'a, 's>> {
let account = state.load_account()?;
let mut exec = self.begin(address, account)?;
let info = exec.run_tick_tock_transaction(kind, inspector)?;
UncommittedTransaction::with_info(exec, state, None, info).map_err(TxError::Fatal)
}
pub fn begin(&self, address: &StdAddr, account: Option<Account>) -> Result<ExecutorState<'a>> {
let is_special = self
.override_special
.unwrap_or_else(|| self.config.is_special(address));
let acc_address;
let acc_storage_stat;
let acc_balance;
let acc_state;
let orig_status;
let end_status;
let start_lt;
match account {
Some(acc) => {
acc_address = 'addr: {
if let IntAddr::Std(acc_addr) = acc.address
&& acc_addr == *address
{
break 'addr acc_addr;
}
anyhow::bail!("account address mismatch");
};
acc_storage_stat = acc.storage_stat;
acc_balance = acc.balance;
acc_state = acc.state;
orig_status = acc_state.status();
end_status = orig_status;
start_lt = std::cmp::max(self.min_lt, acc.last_trans_lt);
}
None => {
acc_address = address.clone();
acc_storage_stat = StorageInfo {
used: StorageUsed::ZERO,
storage_extra: Default::default(),
last_paid: 0,
due_payment: None,
};
acc_balance = CurrencyCollection::ZERO;
acc_state = AccountState::Uninit;
orig_status = AccountStatus::NotExists;
end_status = AccountStatus::Uninit;
start_lt = self.min_lt;
}
};
let mut is_marks_authority = false;
let mut is_suspended_by_marks = false;
if self.params.authority_marks_enabled
&& let Some(marks) = &self.config.authority_marks
{
is_marks_authority = marks.is_authority(address);
is_suspended_by_marks =
!is_special && !is_marks_authority && marks.is_suspended(&acc_balance)?;
}
Ok(ExecutorState {
params: self.params,
config: self.config,
is_special,
is_marks_authority,
is_suspended_by_marks,
address: acc_address,
storage_stat: acc_storage_stat,
balance: acc_balance,
state: acc_state,
orig_status,
end_status,
start_lt,
end_lt: start_lt + 1,
out_msgs: Vec::new(),
total_fees: Tokens::ZERO,
burned: Tokens::ZERO,
cached_storage_stat: None,
})
}
}
#[derive(Default)]
pub struct ExecutorInspector<'e> {
pub actions: Option<Cell>,
pub public_libs_diff: ahash::HashMap<HashBytes, PublicLibraryChange>,
pub exit_code: Option<i32>,
pub missing_library: Option<HashBytes>,
pub total_gas_used: u64,
pub debug: Option<&'e mut dyn std::fmt::Write>,
pub modify_smc_info: Option<&'e mut ModifySmcInfoFn>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PublicLibraryChange {
Add(Cell),
Remove,
}
pub type ModifySmcInfoFn = dyn FnMut(&mut phase::ComputePhaseSmcInfo) -> Result<()>;
#[derive(Debug)]
pub struct ExecutorState<'a> {
pub params: &'a ExecutorParams,
pub config: &'a ParsedConfig,
pub is_special: bool,
pub is_marks_authority: bool,
pub is_suspended_by_marks: bool,
pub address: StdAddr,
pub storage_stat: StorageInfo,
pub balance: CurrencyCollection,
pub state: AccountState,
pub orig_status: AccountStatus,
pub end_status: AccountStatus,
pub start_lt: u64,
pub end_lt: u64,
pub out_msgs: Vec<Lazy<OwnedMessage>>,
pub total_fees: Tokens,
pub burned: Tokens,
pub cached_storage_stat: Option<OwnedExtStorageStat>,
}
#[cfg(test)]
impl<'a> ExecutorState<'a> {
pub(crate) fn new_non_existent(
params: &'a ExecutorParams,
config: &'a impl AsRef<ParsedConfig>,
address: &StdAddr,
) -> Self {
Self {
params,
config: config.as_ref(),
is_special: false,
is_marks_authority: false,
is_suspended_by_marks: false,
address: address.clone(),
storage_stat: Default::default(),
balance: CurrencyCollection::ZERO,
state: AccountState::Uninit,
orig_status: AccountStatus::NotExists,
end_status: AccountStatus::Uninit,
start_lt: 0,
end_lt: 1,
out_msgs: Vec::new(),
total_fees: Tokens::ZERO,
burned: Tokens::ZERO,
cached_storage_stat: None,
}
}
pub(crate) fn new_uninit(
params: &'a ExecutorParams,
config: &'a impl AsRef<ParsedConfig>,
address: &StdAddr,
balance: impl Into<CurrencyCollection>,
) -> Self {
let mut res = Self::new_non_existent(params, config, address);
res.balance = balance.into();
res.orig_status = AccountStatus::Uninit;
if params.authority_marks_enabled
&& let Some(marks) = &config.as_ref().authority_marks
{
res.is_marks_authority = marks.is_authority(address);
res.is_suspended_by_marks = !res.is_special
&& !res.is_marks_authority
&& marks.is_suspended(&res.balance).unwrap();
}
res
}
pub(crate) fn new_frozen(
params: &'a ExecutorParams,
config: &'a impl AsRef<ParsedConfig>,
address: &StdAddr,
balance: impl Into<CurrencyCollection>,
state_hash: HashBytes,
) -> Self {
let mut res = Self::new_uninit(params, config, address, balance);
res.state = AccountState::Frozen(state_hash);
res.orig_status = AccountStatus::Frozen;
res.end_status = AccountStatus::Frozen;
res
}
pub(crate) fn new_active(
params: &'a ExecutorParams,
config: &'a impl AsRef<ParsedConfig>,
address: &StdAddr,
balance: impl Into<CurrencyCollection>,
data: Cell,
code_boc: impl AsRef<[u8]>,
) -> Self {
use tycho_types::models::StateInit;
let mut res = Self::new_uninit(params, config, address, balance);
res.state = AccountState::Active(StateInit {
split_depth: None,
special: None,
code: Some(Boc::decode(code_boc).unwrap()),
data: Some(data),
libraries: Dict::new(),
});
res.orig_status = AccountStatus::Active;
res.end_status = AccountStatus::Active;
res
}
}
#[derive(Debug, Default, Clone)]
pub struct ExecutorParams {
pub libraries: Dict<HashBytes, LibDescr>,
pub rand_seed: HashBytes,
pub block_unixtime: u32,
pub block_lt: u64,
pub prev_mc_block_id: Option<BlockId>,
pub vm_modifiers: tycho_vm::BehaviourModifiers,
pub disable_delete_frozen_accounts: bool,
pub charge_action_fees_on_fail: bool,
pub full_body_in_bounced: bool,
pub strict_extra_currency: bool,
pub authority_marks_enabled: bool,
}
pub struct UncommittedTransaction<'a, 's> {
original: &'s ShardAccount,
exec: ExecutorState<'a>,
in_msg: Option<Cell>,
info: Lazy<TxInfo>,
}
impl<'a, 's> UncommittedTransaction<'a, 's> {
#[inline]
pub fn with_info(
exec: ExecutorState<'a>,
original: &'s ShardAccount,
in_msg: Option<Cell>,
info: impl Into<TxInfo>,
) -> Result<Self> {
let info = info.into();
let info = Lazy::new(&info)?;
Ok(Self {
original,
exec,
in_msg,
info,
})
}
pub fn build_uncommitted(&self) -> Result<Transaction, Error> {
thread_local! {
static EMPTY_STATE_UPDATE: Lazy<HashUpdate> = Lazy::new(&HashUpdate {
old: HashBytes::ZERO,
new: HashBytes::ZERO,
})
.unwrap();
}
self.build_transaction(self.exec.end_status, EMPTY_STATE_UPDATE.with(Clone::clone))
}
pub fn commit(mut self) -> Result<ExecutorOutput> {
let account_state;
let new_state_meta;
let end_status = match self.build_account_state()? {
None => {
account_state = CellBuilder::build_from(false)?;
new_state_meta = AccountMeta {
balance: CurrencyCollection::ZERO,
libraries: Dict::new(),
exists: false,
};
AccountStatus::NotExists
}
Some(state) => {
let prev_account_storage = 'prev: {
let mut cs = self.original.account.as_slice_allow_exotic();
if !cs.load_bit()? {
break 'prev None;
}
IntAddr::load_from(&mut cs)?;
let storage_info = StorageInfo::load_from(&mut cs)?;
Some((storage_info.used, cs))
};
let mut account_storage = CellBuilder::new();
account_storage.store_u64(self.exec.end_lt)?;
self.exec
.balance
.store_into(&mut account_storage, Cell::empty_context())?;
state.store_into(&mut account_storage, Cell::empty_context())?;
self.exec.storage_stat.used = compute_storage_used(
prev_account_storage,
account_storage.as_full_slice(),
&mut self.exec.cached_storage_stat,
self.exec.params.strict_extra_currency,
)?;
account_state = CellBuilder::build_from((
true, &self.exec.address, &self.exec.storage_stat, account_storage.as_full_slice(), ))?;
let libraries = match &state {
AccountState::Active(state) => state.libraries.clone(),
AccountState::Frozen(..) | AccountState::Uninit => Dict::new(),
};
new_state_meta = AccountMeta {
balance: self.exec.balance.clone(),
libraries,
exists: true,
};
state.status()
}
};
let state_update = Lazy::new(&HashUpdate {
old: *self.original.account.repr_hash(),
new: *account_state.repr_hash(),
})?;
let transaction = self
.build_transaction(end_status, state_update)
.and_then(|tx| Lazy::new(&tx))?;
let transaction_meta = TransactionMeta {
total_fees: self.exec.total_fees,
next_lt: self.exec.end_lt,
out_msgs: self.exec.out_msgs,
};
let new_state = ShardAccount {
account: unsafe { Lazy::from_raw_unchecked(account_state) },
last_trans_hash: *transaction.repr_hash(),
last_trans_lt: self.exec.start_lt,
};
Ok(ExecutorOutput {
new_state,
new_state_meta,
transaction,
transaction_meta,
burned: self.exec.burned,
})
}
fn build_account_state(&self) -> Result<Option<AccountState>> {
Ok(match self.exec.end_status {
AccountStatus::NotExists => None,
AccountStatus::Uninit if self.exec.balance.is_zero() => None,
AccountStatus::Uninit => Some(AccountState::Uninit),
AccountStatus::Active => {
debug_assert!(matches!(self.exec.state, AccountState::Active(_)));
Some(self.exec.state.clone())
}
AccountStatus::Frozen => {
let cell;
let frozen_hash = match &self.exec.state {
AccountState::Uninit => &self.exec.address.address,
AccountState::Active(state_init) => {
cell = CellBuilder::build_from(state_init)?;
cell.repr_hash()
}
AccountState::Frozen(hash_bytes) => hash_bytes,
};
Some(if frozen_hash == &self.exec.address.address {
AccountState::Uninit
} else {
AccountState::Frozen(*frozen_hash)
})
}
})
}
fn build_transaction(
&self,
end_status: AccountStatus,
state_update: Lazy<HashUpdate>,
) -> Result<Transaction, Error> {
Ok(Transaction {
account: self.exec.address.address,
lt: self.exec.start_lt,
prev_trans_hash: self.original.last_trans_hash,
prev_trans_lt: self.original.last_trans_lt,
now: self.exec.params.block_unixtime,
out_msg_count: Uint15::new(self.exec.out_msgs.len() as _),
orig_status: self.exec.orig_status,
end_status,
in_msg: self.in_msg.clone(),
out_msgs: build_out_msgs(&self.exec.out_msgs)?,
total_fees: self.exec.total_fees.into(),
state_update,
info: self.info.clone(),
})
}
}
fn compute_storage_used(
mut prev: Option<(StorageUsed, CellSlice<'_>)>,
mut new_storage: CellSlice<'_>,
cache: &mut Option<OwnedExtStorageStat>,
without_extra_currencies: bool,
) -> Result<StorageUsed> {
fn skip_extra(slice: &mut CellSlice<'_>) -> Result<bool, Error> {
let mut cs = *slice;
cs.skip_first(64, 0)?; let balance = CurrencyCollection::load_from(&mut cs)?;
Ok(if balance.other.is_empty() {
false
} else {
slice.skip_first(0, 1)?;
true
})
}
if without_extra_currencies {
if let Some((_, prev)) = &mut prev {
skip_extra(prev)?;
}
skip_extra(&mut new_storage)?;
}
if let Some((prev_used, prev_storage)) = prev {
'reuse: {
if prev_used.cells.is_zero()
|| prev_used.bits.into_inner() < prev_storage.size_bits() as u64
{
break 'reuse;
}
if prev_storage.size_refs() != new_storage.size_refs() {
break 'reuse;
}
for (prev, new) in prev_storage.references().zip(new_storage.references()) {
if prev != new {
break 'reuse;
}
}
return Ok(StorageUsed {
bits: new_varuint56_truncate(
(prev_used.bits.into_inner() - prev_storage.size_bits() as u64)
.saturating_add(new_storage.size_bits() as u64),
),
cells: prev_used.cells,
});
}
}
let cache = cache.get_or_insert_with(OwnedExtStorageStat::unlimited);
cache.set_unlimited();
for cell in new_storage.references().cloned() {
cache.add_cell(cell);
}
let stats = cache.stats();
Ok(StorageUsed {
cells: new_varuint56_truncate(stats.cell_count.saturating_add(1)),
bits: new_varuint56_truncate(stats.bit_count.saturating_add(new_storage.size_bits() as _)),
})
}
#[derive(Clone, Debug)]
pub struct ExecutorOutput {
pub new_state: ShardAccount,
pub new_state_meta: AccountMeta,
pub transaction: Lazy<Transaction>,
pub transaction_meta: TransactionMeta,
pub burned: Tokens,
}
#[derive(Clone, Debug)]
pub struct AccountMeta {
pub balance: CurrencyCollection,
pub libraries: Dict<HashBytes, SimpleLib>,
pub exists: bool,
}
#[derive(Clone, Debug)]
pub struct TransactionMeta {
pub total_fees: Tokens,
pub out_msgs: Vec<Lazy<OwnedMessage>>,
pub next_lt: u64,
}
pub trait LoadMessage {
fn load_message_root(self) -> Result<Cell>;
}
impl<T: LoadMessage + Clone> LoadMessage for &T {
#[inline]
fn load_message_root(self) -> Result<Cell> {
T::load_message_root(T::clone(self))
}
}
impl LoadMessage for Cell {
#[inline]
fn load_message_root(self) -> Result<Cell> {
Ok(self)
}
}
impl<T: EquivalentRepr<OwnedMessage>> LoadMessage for Lazy<T> {
#[inline]
fn load_message_root(self) -> Result<Cell> {
Ok(self.into_inner())
}
}
impl LoadMessage for OwnedMessage {
#[inline]
fn load_message_root(self) -> Result<Cell> {
CellBuilder::build_from(self).context("failed to serialize inbound message")
}
}
impl LoadMessage for Message<'_> {
#[inline]
fn load_message_root(self) -> Result<Cell> {
CellBuilder::build_from(self).context("failed to serialize inbound message")
}
}
fn build_out_msgs(out_msgs: &[Lazy<OwnedMessage>]) -> Result<Dict<Uint15, Cell>, Error> {
dict::build_dict_from_sorted_iter(
out_msgs
.iter()
.enumerate()
.map(|(i, msg)| (Uint15::new(i as _), msg.inner().clone())),
Cell::empty_context(),
)
.map(Dict::from_raw)
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use tycho_types::boc::BocRepr;
use tycho_types::models::{BlockchainConfig, MsgInfo, StateInit};
use super::*;
pub fn make_default_config() -> Rc<ParsedConfig> {
thread_local! {
pub static PARSED_CONFIG: Rc<ParsedConfig> = make_custom_config(|_| Ok(()));
}
PARSED_CONFIG.with(Clone::clone)
}
pub fn make_custom_config<F>(f: F) -> Rc<ParsedConfig>
where
F: FnOnce(&mut BlockchainConfig) -> anyhow::Result<()>,
{
let mut config: BlockchainConfig =
BocRepr::decode(include_bytes!("../res/config.boc")).unwrap();
config.params.set_global_id(100).unwrap();
config
.params
.set_size_limits(&ParsedConfig::DEFAULT_SIZE_LIMITS_CONFIG)
.unwrap();
f(&mut config).unwrap();
Rc::new(ParsedConfig::parse(config, u32::MAX).unwrap())
}
pub fn make_default_params() -> ExecutorParams {
ExecutorParams {
block_unixtime: 1738799198,
full_body_in_bounced: false,
strict_extra_currency: true,
vm_modifiers: tycho_vm::BehaviourModifiers {
chksig_always_succeed: true,
..Default::default()
},
..Default::default()
}
}
pub fn make_message(
info: impl Into<MsgInfo>,
init: Option<StateInit>,
body: Option<CellBuilder>,
) -> Cell {
let body = match &body {
None => Cell::empty_cell_ref().as_slice_allow_exotic(),
Some(cell) => cell.as_full_slice(),
};
CellBuilder::build_from(Message {
info: info.into(),
init,
body,
layout: None,
})
.unwrap()
}
pub fn make_big_tree(depth: u8, count: &mut u16, target: u16) -> Cell {
*count += 1;
if depth == 0 {
CellBuilder::build_from(*count).unwrap()
} else {
let mut b = CellBuilder::new();
for _ in 0..4 {
if *count < target {
b.store_reference(make_big_tree(depth - 1, count, target))
.unwrap();
}
}
b.build().unwrap()
}
}
#[allow(unused)]
fn replay_tx(config: &str, state: &str, msg: &str, lt: u64, now: u32) -> Result<()> {
use tycho_types::models::*;
let mut config: BlockchainConfig = BocRepr::decode_base64(config)?;
let state: ShardAccount = BocRepr::decode_base64(state)?;
let msg = Boc::decode_base64(msg)?;
let mut workchains = Dict::<i32, _>::new();
workchains.set(0, WorkchainDescription {
enabled_since: 0,
actual_min_split: 0,
min_split: 0,
max_split: 0,
active: true,
accept_msgs: true,
zerostate_root_hash: HashBytes::ZERO,
zerostate_file_hash: HashBytes::ZERO,
version: 0,
format: WorkchainFormat::Basic(WorkchainFormatBasic {
vm_mode: 0,
vm_version: 0,
}),
})?;
config.params.set_workchains(&workchains)?;
config.params.remove(43)?;
let (is_external, address) = match msg.parse::<MsgInfo>()? {
MsgInfo::Int(info) => (false, info.dst),
MsgInfo::ExtIn(info) => (true, info.dst),
MsgInfo::ExtOut(_) => anyhow::bail!("unexpected message"),
};
let IntAddr::Std(address) = address else {
anyhow::bail!("unexpected address");
};
let params = ExecutorParams {
block_unixtime: now,
..Default::default()
};
let parsed_config = ParsedConfig::parse(config, now).context("failed to parse config")?;
let output = Executor::new(¶ms, &parsed_config)
.with_min_lt(lt)
.begin_ordinary(&address, is_external, msg, &state)?
.commit()?;
let tx = output.transaction.load()?;
let tx_info = tx.load_info()?;
println!("{tx_info:#?}");
Ok(())
}
}