use anyhow::Result;
use tycho_types::models::{
AccountState, AccountStatus, BlockId, ComputeGasParams, ComputePhase, ComputePhaseSkipReason,
CurrencyCollection, ExecutedComputePhase, IntAddr, IntMsgInfo, MsgType, SkippedComputePhase,
StateInit, TickTock,
};
use tycho_types::num::Tokens;
use tycho_types::prelude::*;
use tycho_vm::{SafeRc, SmcInfoBase, Stack, Tuple, UnpackedInMsgSmcInfo, VmState, tuple};
use crate::phase::receive::{MsgStateInit, ReceivedMessage};
use crate::util::{
StateLimitsResult, check_state_limits_diff, new_varuint24_truncate, new_varuint56_truncate,
unlikely,
};
use crate::{ExecutorInspector, ExecutorState};
pub type ComputePhaseSmcInfo = tycho_vm::SmcInfoTonV11;
pub struct ComputePhaseContext<'a, 'e> {
pub input: TransactionInput<'a>,
pub storage_fee: Tokens,
pub force_accept: bool,
pub stop_on_accept: bool,
pub inspector: Option<&'a mut ExecutorInspector<'e>>,
}
#[derive(Debug, Clone, Copy)]
pub enum TransactionInput<'a> {
Ordinary(&'a ReceivedMessage),
TickTock(TickTock),
}
impl<'a> TransactionInput<'a> {
const fn is_ordinary(&self) -> bool {
matches!(self, Self::Ordinary(_))
}
fn in_msg(&self) -> Option<&'a ReceivedMessage> {
match self {
Self::Ordinary(msg) => Some(msg),
Self::TickTock(_) => None,
}
}
fn in_msg_init(&self) -> Option<&'a MsgStateInit> {
match self {
Self::Ordinary(msg) => msg.init.as_ref(),
Self::TickTock(_) => None,
}
}
}
#[derive(Debug)]
pub struct ComputePhaseFull {
pub compute_phase: ComputePhase,
pub accepted: bool,
pub original_balance: CurrencyCollection,
pub new_state: StateInit,
pub actions: Cell,
}
impl ExecutorState<'_> {
pub fn compute_phase(
&mut self,
mut ctx: ComputePhaseContext<'_, '_>,
) -> Result<ComputePhaseFull> {
let is_masterchain = self.address.is_masterchain();
let mut original_balance = self.balance.clone();
if let Some(msg) = ctx.input.in_msg() {
original_balance.try_sub_assign(&msg.balance_remaining)?;
}
let new_state = if let AccountState::Active(current) = &self.state {
current.clone()
} else {
Default::default()
};
let mut res = ComputePhaseFull {
compute_phase: ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::NoGas,
}),
accepted: false,
original_balance,
new_state,
actions: Cell::empty_cell(),
};
if self.balance.tokens.is_zero() {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::NoGas,
});
return Ok(res);
}
if self.is_suspended_by_marks {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::Suspended,
});
return Ok(res);
}
let (msg_balance_remaining, is_external) = match ctx.input.in_msg() {
Some(msg) => (msg.balance_remaining.clone(), msg.is_external),
None => (CurrencyCollection::ZERO, false),
};
let gas = if unlikely(ctx.force_accept) {
tycho_vm::GasParams::getter()
} else {
let prices = self.config.gas_prices(is_masterchain);
let computed = prices.compute_gas_params(ComputeGasParams {
account_balance: &self.balance.tokens,
message_balance: &msg_balance_remaining.tokens,
is_special: self.is_special,
is_tx_ordinary: ctx.input.is_ordinary(),
is_in_msg_external: is_external,
});
tycho_vm::GasParams {
max: computed.max,
limit: computed.limit,
credit: computed.credit,
price: prices.gas_price,
}
};
if gas.limit == 0 && gas.credit == 0 {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::NoGas,
});
return Ok(res);
}
let state_libs;
let msg_libs;
let msg_state_used;
match (ctx.input.in_msg_init(), &self.state) {
(None, AccountState::Uninit) => {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::NoState,
});
return Ok(res);
}
(None, AccountState::Frozen { .. }) => {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::BadState,
});
return Ok(res);
}
(None, AccountState::Active(StateInit { libraries, .. })) => {
state_libs = Some(libraries);
msg_libs = None;
msg_state_used = false;
}
(Some(from_msg), AccountState::Uninit | AccountState::Frozen(..)) => {
let target_hash = if let AccountState::Frozen(old_hash) = &self.state {
old_hash
} else {
&self.address.address
};
if from_msg.root_hash() != target_hash || from_msg.parsed.split_depth.is_some() {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::BadState,
});
return Ok(res);
}
let mut limits = self.config.size_limits.clone();
if is_masterchain && matches!(&self.state, AccountState::Uninit) {
limits.max_acc_public_libraries = 0;
}
if matches!(
check_state_limits_diff(
&res.new_state,
&from_msg.parsed,
&limits,
is_masterchain,
&mut self.cached_storage_stat,
),
StateLimitsResult::Exceeds
) {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::BadState,
});
return Ok(res);
}
res.new_state = from_msg.parsed.clone();
self.state = AccountState::Active(res.new_state.clone());
msg_state_used = true;
state_libs = None;
msg_libs = Some(from_msg.parsed.libraries.clone());
}
(Some(from_msg), AccountState::Active(StateInit { libraries, .. })) => {
if is_external && from_msg.root_hash() != &self.address.address {
res.compute_phase = ComputePhase::Skipped(SkippedComputePhase {
reason: ComputePhaseSkipReason::BadState,
});
return Ok(res);
}
msg_state_used = false;
state_libs = Some(libraries);
msg_libs = Some(from_msg.parsed.libraries.clone());
}
}
let unpacked_in_msg = match ctx.input.in_msg() {
Some(msg) => msg.make_tuple()?,
None => None,
};
let stack = self.prepare_vm_stack(ctx.input);
let code = res.new_state.code.clone();
let prev_blocks = self
.params
.prev_mc_block_id
.as_ref()
.map(make_prev_blocks_tuple)
.unwrap_or_default();
let mut smc_info = SmcInfoBase::new()
.with_now(self.params.block_unixtime)
.with_block_lt(self.params.block_lt)
.with_tx_lt(self.start_lt)
.with_mixed_rand_seed(&self.params.rand_seed, &self.address.address)
.with_account_balance(self.balance.clone())
.with_account_addr(self.address.clone().into())
.with_config(self.config.raw.params.clone())
.require_ton_v4()
.with_code(code.clone().unwrap_or_default())
.with_message_balance(msg_balance_remaining.clone())
.with_prev_blocks_info(prev_blocks)
.with_storage_fees(ctx.storage_fee)
.require_ton_v6()
.with_unpacked_config(self.config.unpacked.as_tuple())
.with_due_payment(self.storage_stat.due_payment.unwrap_or_default())
.require_ton_v11()
.with_unpacked_in_msg(unpacked_in_msg);
let real_version = tycho_vm::VmVersion::Ton(12);
if let Some(inspector) = ctx.inspector.as_deref_mut()
&& let Some(modify_smc_info) = inspector.modify_smc_info.as_deref_mut()
{
modify_smc_info(&mut smc_info)?;
}
let libraries = (msg_libs, state_libs, &self.params.libraries);
let mut modifiers = self.params.vm_modifiers;
modifiers.stop_on_accept |= ctx.stop_on_accept;
let mut vm = VmState::builder()
.with_smc_info(smc_info)
.with_version(real_version)
.with_code(code)
.with_data(res.new_state.data.clone().unwrap_or_default())
.with_libraries(&libraries)
.with_init_selector(false)
.with_raw_stack(stack)
.with_gas(gas)
.with_modifiers(modifiers)
.build();
let mut inspector_actions = None;
let mut inspector_exit_code = None;
let mut inspector_total_gas_used = None;
let mut missing_library = None;
if let Some(inspector) = ctx.inspector {
inspector_actions = Some(&mut inspector.actions);
inspector_exit_code = Some(&mut inspector.exit_code);
inspector_total_gas_used = Some(&mut inspector.total_gas_used);
missing_library = Some(&mut inspector.missing_library);
if let Some(debug) = inspector.debug.as_deref_mut() {
vm.debug = Some(debug);
}
}
let exit_code = !vm.run();
if let Some(inspector_exit_code) = inspector_exit_code {
*inspector_exit_code = Some(exit_code);
}
let consumed_paid_gas = vm.gas.consumed();
if let Some(total_gas_used) = inspector_total_gas_used {
*total_gas_used = consumed_paid_gas.saturating_add(vm.gas.free_gas_consumed());
}
res.accepted = ctx.force_accept || vm.gas.credit() == 0;
debug_assert!(
is_external || res.accepted,
"internal messages must be accepted"
);
let success = res.accepted && vm.committed_state.is_some();
let gas_used = std::cmp::min(consumed_paid_gas, vm.gas.limit());
let gas_fees = if res.accepted && !self.is_special {
self.config
.gas_prices(is_masterchain)
.compute_gas_fee(gas_used)
} else {
Tokens::ZERO
};
let mut account_activated = false;
if res.accepted && msg_state_used {
account_activated = self.orig_status != AccountStatus::Active;
self.end_status = AccountStatus::Active;
}
if let Some(committed) = vm.committed_state
&& res.accepted
{
res.new_state.data = Some(committed.c4);
res.actions = committed.c5;
if let Some(actions) = inspector_actions {
*actions = Some(res.actions.clone());
}
}
if let Some(missing_library) = missing_library {
*missing_library = vm.gas.missing_library();
}
self.balance.try_sub_assign_tokens(gas_fees)?;
self.total_fees.try_add_assign(gas_fees)?;
res.compute_phase = ComputePhase::Executed(ExecutedComputePhase {
success,
msg_state_used,
account_activated,
gas_fees,
gas_used: new_varuint56_truncate(gas_used),
gas_limit: new_varuint56_truncate(gas.limit),
gas_credit: (gas.credit != 0).then(|| new_varuint24_truncate(gas.credit)),
mode: 0,
exit_code,
exit_arg: if success {
None
} else {
vm.stack.get_exit_arg().filter(|x| *x != 0)
},
vm_steps: vm.steps.try_into().unwrap_or(u32::MAX),
vm_init_state_hash: HashBytes::ZERO,
vm_final_state_hash: HashBytes::ZERO,
});
Ok(res)
}
fn prepare_vm_stack(&self, input: TransactionInput<'_>) -> SafeRc<Stack> {
SafeRc::new(Stack::with_items(match input {
TransactionInput::Ordinary(msg) => {
tuple![
int self.balance.tokens,
int msg.balance_remaining.tokens,
cell msg.root.clone(),
slice msg.body.clone(),
int if msg.is_external { -1 } else { 0 },
]
}
TransactionInput::TickTock(ty) => {
tuple![
int self.balance.tokens,
int self.address.address.as_bigint(),
int match ty {
TickTock::Tick => 0,
TickTock::Tock => -1,
},
int -2,
]
}
}))
}
}
impl ReceivedMessage {
fn make_tuple(&self) -> Result<Option<SafeRc<Tuple>>, tycho_types::error::Error> {
let mut cs = self.root.as_slice()?;
if MsgType::load_from(&mut cs)? != MsgType::Int {
return Ok(None);
}
let src_addr_slice = {
let mut cs = cs;
cs.skip_first(3, 0)?;
let mut addr_slice = cs;
IntAddr::load_from(&mut cs)?;
addr_slice.skip_last(cs.size_bits(), cs.size_refs())?;
addr_slice.range()
};
let info = IntMsgInfo::load_from(&mut cs)?;
let unpacked = UnpackedInMsgSmcInfo {
bounce: info.bounce,
bounced: info.bounced,
src_addr: (src_addr_slice, self.root.clone()).into(),
fwd_fee: info.fwd_fee,
created_lt: info.created_lt,
created_at: info.created_at,
original_value: info.value.tokens,
remaining_value: self.balance_remaining.clone(),
state_init: self.init.as_ref().map(|init| init.root.clone()),
};
Ok(Some(unpacked.into_tuple()))
}
}
fn make_prev_blocks_tuple(prev_mc_block_id: &BlockId) -> SafeRc<tycho_vm::Tuple> {
SafeRc::new(tuple![
[
raw block_id_to_tuple(prev_mc_block_id),
]
])
}
fn block_id_to_tuple(block_id: &BlockId) -> SafeRc<tycho_vm::Tuple> {
SafeRc::new(tuple![
int block_id.shard.workchain(),
int block_id.shard.prefix(),
int block_id.seqno,
int block_id.root_hash.as_bigint(),
int block_id.file_hash.as_bigint(),
])
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use tycho_asm_macros::tvmasm;
use tycho_types::models::{
AuthorityMarksConfig, ExtInMsgInfo, IntMsgInfo, LibDescr, SimpleLib, StdAddr,
};
use tycho_types::num::{VarUint24, VarUint56, VarUint248};
use super::*;
use crate::ExecutorParams;
use crate::tests::{
make_custom_config, make_default_config, make_default_params, make_message,
};
const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
const OK_BALANCE: Tokens = Tokens::new(1_000_000_000);
fn empty_ext_in_msg(addr: &StdAddr) -> Cell {
make_message(
ExtInMsgInfo {
dst: addr.clone().into(),
..Default::default()
},
None,
None,
)
}
fn empty_int_msg(addr: &StdAddr, value: impl Into<CurrencyCollection>) -> Cell {
make_message(
IntMsgInfo {
src: addr.clone().into(),
dst: addr.clone().into(),
value: value.into(),
..Default::default()
},
None,
None,
)
}
fn simple_state(code: &[u8]) -> StateInit {
StateInit {
split_depth: None,
special: None,
code: Some(Boc::decode(code).unwrap()),
data: None,
libraries: Dict::new(),
}
}
fn init_tracing() {
tracing_subscriber::fmt::fmt()
.with_env_filter("tycho_vm=trace")
.with_writer(tracing_subscriber::fmt::TestWriter::new)
.try_init()
.ok();
}
fn make_lib_ref(code: &DynCell) -> Cell {
let mut b = CellBuilder::new();
b.set_exotic(true);
b.store_u8(CellType::LibraryReference.to_byte()).unwrap();
b.store_u256(code.repr_hash()).unwrap();
b.build().unwrap()
}
#[test]
fn ext_in_run_no_accept() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("INT 123 NOP"),
);
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(!compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, Tokens::ZERO);
assert_eq!(compute_phase.gas_used, VarUint56::new(0)); assert_eq!(compute_phase.gas_limit, VarUint56::new(0)); assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, Some(123)); assert_eq!(compute_phase.vm_steps, 3);
Ok(())
}
#[test]
fn ext_in_run_uninit_no_accept() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::NoState);
Ok(())
}
#[test]
fn ext_in_run_no_code_no_accept() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
state.state = AccountState::Active(StateInit::default());
state.orig_status = AccountStatus::Active;
state.end_status = AccountStatus::Active;
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(!compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, Tokens::ZERO);
assert_eq!(compute_phase.gas_used, VarUint56::new(0)); assert_eq!(compute_phase.gas_limit, VarUint56::new(0)); assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(
compute_phase.exit_code,
tycho_vm::VmException::Fatal.as_exit_code()
);
assert_eq!(compute_phase.exit_arg, Some(-1)); assert_eq!(compute_phase.vm_steps, 0);
Ok(())
}
#[test]
fn ext_in_accept_no_commit() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("ACCEPT THROW 42"),
);
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(!compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, (10 + 16) * 2 + 50); assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(compute_phase.exit_code, 42);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn ext_in_accept_invalid_commit() -> Result<()> {
init_tracing();
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!(
r#"
ACCEPT
NEWC
INT 1 STUR 8
INT 7 STUR 8
INT 816 STZEROES
TRUE ENDXC
POP c5
"#
),
);
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(!compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, 783);
assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(
compute_phase.exit_code,
tycho_vm::VmException::CellOverflow as i32
);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 12);
Ok(())
}
#[test]
fn suspended_account_skips_compute() -> Result<()> {
let params = ExecutorParams {
authority_marks_enabled: true,
..make_default_params()
};
let config = make_custom_config(|config| {
config.set_authority_marks_config(&AuthorityMarksConfig {
authority_addresses: Dict::new(),
black_mark_id: 100,
white_mark_id: 101,
})?;
Ok(())
});
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([
(100u32, VarUint248::new(500)), ])
.try_into()?,
},
CellBuilder::build_from(u32::MIN)?,
tvmasm!("ACCEPT"),
);
let msg = state.receive_in_msg(empty_int_msg(&STUB_ADDR, OK_BALANCE))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::Suspended);
Ok(())
}
#[test]
fn cant_suspend_authority_address() -> Result<()> {
let params = ExecutorParams {
authority_marks_enabled: true,
..make_default_params()
};
let config = make_custom_config(|config| {
config.set_authority_marks_config(&AuthorityMarksConfig {
authority_addresses: Dict::try_from_btree(&BTreeMap::from_iter([(
HashBytes::ZERO,
(),
)]))?,
black_mark_id: 100,
white_mark_id: 101,
})?;
Ok(())
});
let mut state = ExecutorState::new_active(
¶ms,
&config,
&StdAddr::new(-1, HashBytes::ZERO),
CurrencyCollection {
tokens: OK_BALANCE,
other: BTreeMap::from_iter([
(100u32, VarUint248::new(1000)), (101u32, VarUint248::new(100)),
])
.try_into()?,
},
Cell::default(),
tvmasm!("ACCEPT"),
);
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.mc_gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, 31);
assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn ext_in_accept_simple() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("ACCEPT NEWC INT 0xdeafbeaf STUR 32 ENDC POP c5"),
);
let msg = state.receive_in_msg(empty_ext_in_msg(&state.address))?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(prev_balance, compute_phase.original_balance);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(
compute_phase.actions,
CellBuilder::build_from(0xdeafbeafu32)?
);
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, 650);
assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 7);
Ok(())
}
#[test]
fn internal_accept_simple() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("ACCEPT"),
);
let msg =
state.receive_in_msg(empty_int_msg(&state.address, Tokens::new(1_000_000_000)))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, (10 + 16) + 5); assert_eq!(
compute_phase.gas_limit,
VarUint56::new(config.gas_prices.gas_limit)
);
assert_eq!(compute_phase.gas_credit, None);
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn internal_no_accept() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("NOP"),
);
let msg = state.receive_in_msg(empty_int_msg(&state.address, Tokens::new(1)))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE)
);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::NoGas);
Ok(())
}
#[test]
fn internal_no_accept_empty_balance() -> Result<()> {
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
Tokens::ZERO,
Cell::empty_cell(),
tvmasm!("NOP"),
);
let msg = state.receive_in_msg(empty_int_msg(&state.address, Tokens::ZERO))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(compute_phase.original_balance, CurrencyCollection::ZERO);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::NoGas);
Ok(())
}
#[test]
fn ext_in_deploy_account() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
let addr = StdAddr::new(0, state_init_hash);
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &addr, OK_BALANCE);
let msg = state.receive_in_msg(make_message(
ExtInMsgInfo {
dst: addr.clone().into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE - prev_total_fees)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, AccountState::Active(state_init));
assert_eq!(state.end_status, AccountStatus::Active);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(compute_phase.msg_state_used);
assert!(compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, (10 + 16) + 5); assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn internal_deploy_account() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
let addr = StdAddr::new(0, state_init_hash);
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &addr, OK_BALANCE);
let msg = state.receive_in_msg(make_message(
IntMsgInfo {
src: addr.clone().into(),
dst: addr.clone().into(),
value: Tokens::new(1_000_000_000).into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, AccountState::Active(state_init));
assert_eq!(state.end_status, AccountStatus::Active);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(compute_phase.msg_state_used);
assert!(compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, (10 + 16) + 5); assert_eq!(
compute_phase.gas_limit,
VarUint56::new(config.gas_prices.gas_limit)
);
assert_eq!(compute_phase.gas_credit, None);
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn ext_in_unfreeze_account() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
let params = make_default_params();
let config = make_default_config();
let mut state =
ExecutorState::new_frozen(¶ms, &config, &STUB_ADDR, OK_BALANCE, state_init_hash);
let msg = state.receive_in_msg(make_message(
ExtInMsgInfo {
dst: STUB_ADDR.into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE - prev_total_fees)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, AccountState::Active(state_init));
assert_eq!(state.end_status, AccountStatus::Active);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(compute_phase.msg_state_used);
assert!(compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, (10 + 16) + 5); assert_eq!(compute_phase.gas_limit, VarUint56::ZERO);
assert_eq!(compute_phase.gas_credit, Some(VarUint24::new(10_000)));
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn internal_unfreeze_account() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let state_init_hash = *CellBuilder::build_from(&state_init)?.repr_hash();
let params = make_default_params();
let config = make_default_config();
let mut state =
ExecutorState::new_frozen(¶ms, &config, &STUB_ADDR, Tokens::ZERO, state_init_hash);
let msg = state.receive_in_msg(make_message(
IntMsgInfo {
src: STUB_ADDR.into(),
dst: STUB_ADDR.into(),
value: Tokens::new(1_000_000_000).into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(compute_phase.original_balance, CurrencyCollection::ZERO);
assert!(compute_phase.accepted);
assert_eq!(state.state, AccountState::Active(state_init));
assert_eq!(state.end_status, AccountStatus::Active);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(compute_phase.msg_state_used);
assert!(compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, (10 + 16) + 5); assert_eq!(
compute_phase.gas_limit,
VarUint56::new(config.gas_prices.gas_limit)
);
assert_eq!(compute_phase.gas_credit, None);
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 2);
Ok(())
}
#[test]
fn ext_in_unfreeze_hash_mismatch() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let params = make_default_params();
let config = make_default_config();
let mut state =
ExecutorState::new_frozen(¶ms, &config, &STUB_ADDR, OK_BALANCE, HashBytes::ZERO);
let msg = state.receive_in_msg(make_message(
ExtInMsgInfo {
dst: STUB_ADDR.into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
let prev_state = state.state.clone();
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE - prev_total_fees)
);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(state.end_status, AccountStatus::Frozen);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
Ok(())
}
#[test]
fn ext_in_deploy_hash_mismatch() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let msg = state.receive_in_msg(make_message(
ExtInMsgInfo {
dst: STUB_ADDR.into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
let prev_state = state.state.clone();
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE - prev_total_fees)
);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(state.end_status, AccountStatus::Uninit);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
Ok(())
}
#[test]
fn internal_unfreeze_hash_mismatch() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let params = make_default_params();
let config = make_default_config();
let mut state =
ExecutorState::new_frozen(¶ms, &config, &STUB_ADDR, OK_BALANCE, HashBytes::ZERO);
let msg = state.receive_in_msg(make_message(
IntMsgInfo {
src: STUB_ADDR.into(),
dst: STUB_ADDR.into(),
value: Tokens::new(1_000_000_000).into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
state.credit_phase(&msg)?;
let prev_state = state.state.clone();
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE - prev_total_fees)
);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(state.end_status, AccountStatus::Frozen);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
Ok(())
}
#[test]
fn internal_deploy_hash_mismatch() -> Result<()> {
let state_init = simple_state(tvmasm!("ACCEPT"));
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
let msg = state.receive_in_msg(make_message(
IntMsgInfo {
src: STUB_ADDR.into(),
dst: STUB_ADDR.into(),
value: Tokens::new(1_000_000_000).into(),
..Default::default()
},
Some(state_init.clone()),
None,
))?;
state.credit_phase(&msg)?;
let prev_state = state.state.clone();
let prev_balance = state.balance.clone();
let prev_total_fees = state.total_fees;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE - prev_total_fees)
);
assert!(!compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(state.end_status, AccountStatus::Uninit);
assert_eq!(compute_phase.actions, Cell::empty_cell());
assert_eq!(state.total_fees, prev_total_fees);
assert_eq!(state.balance, prev_balance);
let ComputePhase::Skipped(compute_phase) = compute_phase.compute_phase else {
panic!("expected skipped compute phase");
};
assert_eq!(compute_phase.reason, ComputePhaseSkipReason::BadState);
Ok(())
}
#[test]
fn tick_special() -> Result<()> {
init_tracing();
let params = make_default_params();
let config = make_default_config();
let mut state = ExecutorState::new_active(
¶ms,
&config,
&STUB_ADDR,
OK_BALANCE,
Cell::empty_cell(),
tvmasm!("DROP THROWIF 42 ACCEPT"),
);
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::TickTock(TickTock::Tick),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, 75);
assert_eq!(
compute_phase.gas_limit,
VarUint56::new(config.gas_prices.gas_limit)
);
assert_eq!(compute_phase.gas_credit, None);
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 4);
Ok(())
}
#[test]
fn code_as_library() -> Result<()> {
init_tracing();
let mut params = make_default_params();
let config = make_default_config();
let code = Boc::decode(tvmasm!("DROP THROWIF 42 ACCEPT"))?;
params.libraries.set(code.repr_hash(), LibDescr {
lib: code.clone(),
publishers: {
let mut p = Dict::new();
p.set(HashBytes::ZERO, ())?;
p
},
})?;
let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
state.state = AccountState::Active(StateInit {
code: Some(make_lib_ref(code.as_ref())),
..Default::default()
});
state.orig_status = AccountStatus::Active;
state.end_status = AccountStatus::Active;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::TickTock(TickTock::Tick),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(config.gas_prices.flat_gas_price as _);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, 185);
assert_eq!(
compute_phase.gas_limit,
VarUint56::new(config.gas_prices.gas_limit)
);
assert_eq!(compute_phase.gas_credit, None);
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 5);
Ok(())
}
#[test]
fn internal_use_libraries() -> Result<()> {
init_tracing();
let mut params = make_default_params();
let config = make_default_config();
let state_lib_code = Boc::decode(tvmasm!("THROWIF 42 INT 0"))?;
let account_lib_code = Boc::decode(tvmasm!("THROWIF 43 INT -1"))?;
let msg_lib_code = Boc::decode(tvmasm!("THROWIFNOT 44"))?;
println!("State lib hash: {}", state_lib_code.repr_hash());
println!("Account lib hash: {}", account_lib_code.repr_hash());
println!("Message lib hash: {}", msg_lib_code.repr_hash());
params.libraries.set(state_lib_code.repr_hash(), LibDescr {
lib: state_lib_code.clone(),
publishers: {
let mut p = Dict::new();
p.set(HashBytes([0x00; 32]), ())?;
p
},
})?;
let msg_state_init = StateInit {
libraries: {
let mut p = Dict::new();
p.set(msg_lib_code.repr_hash(), SimpleLib {
public: false,
root: msg_lib_code.clone(),
})?;
p
},
..Default::default()
};
let addr = StdAddr::new(0, *CellBuilder::build_from(&msg_state_init)?.repr_hash());
let mut state = ExecutorState::new_uninit(¶ms, &config, &addr, OK_BALANCE);
state.state = AccountState::Active(StateInit {
code: Some(Boc::decode(tvmasm!(
r#"
ACCEPT
PUSHCONT {
DUMPSTK
XLOAD
CTOS
BLESS
EXECUTE
}
// Execute state lib code
OVER
PUSHREF @{f3898d9454fc288ac160d46487de1303db2244da85d6908356b89090c7839e4a}
PUSH s2
EXECUTE
// Execute account lib code
PUSHREF @{d8b2b44711e73fece0f00b41be3aef8f3850169a7834852e1c8abf80c228cf57}
PUSH s2
EXECUTE
// Execute msg lib code
PUSHREF @{bc17092cb9d0bad5c5a523cd866d37f20d3783de23b891e48b903f7bca470998}
ROT
EXECUTE
"#
))?),
libraries: {
let mut p = Dict::new();
p.set(account_lib_code.repr_hash(), SimpleLib {
public: false,
root: account_lib_code.clone(),
})?;
p
},
..Default::default()
});
state.orig_status = AccountStatus::Active;
state.end_status = AccountStatus::Active;
let msg = state.receive_in_msg(make_message(
IntMsgInfo {
src: addr.clone().into(),
dst: addr.clone().into(),
value: Tokens::new(1_000_000_000).into(),
..Default::default()
},
Some(msg_state_init),
None,
))?;
state.credit_phase(&msg)?;
let prev_balance = state.balance.clone();
let prev_state = state.state.clone();
let prev_total_fees = state.total_fees;
let prev_end_status = state.end_status;
let compute_phase = state.compute_phase(ComputePhaseContext {
input: TransactionInput::Ordinary(&msg),
storage_fee: Tokens::ZERO,
force_accept: false,
stop_on_accept: false,
inspector: None,
})?;
assert_eq!(
compute_phase.original_balance,
CurrencyCollection::from(OK_BALANCE)
);
assert!(compute_phase.accepted);
assert_eq!(state.state, prev_state);
assert_eq!(prev_end_status, state.end_status);
assert_eq!(compute_phase.actions, Cell::empty_cell());
let expected_gas_fee = Tokens::new(1315000);
assert_eq!(state.total_fees, prev_total_fees + expected_gas_fee);
assert_eq!(state.balance.other, prev_balance.other);
assert_eq!(state.balance.tokens, prev_balance.tokens - expected_gas_fee);
let ComputePhase::Executed(compute_phase) = compute_phase.compute_phase else {
panic!("expected executed compute phase");
};
assert!(compute_phase.success);
assert!(!compute_phase.msg_state_used);
assert!(!compute_phase.account_activated);
assert_eq!(compute_phase.gas_fees, expected_gas_fee);
assert_eq!(compute_phase.gas_used, 1315);
assert_eq!(
compute_phase.gas_limit,
VarUint56::new(config.gas_prices.gas_limit)
);
assert_eq!(compute_phase.gas_credit, None);
assert_eq!(compute_phase.exit_code, 0);
assert_eq!(compute_phase.exit_arg, None);
assert_eq!(compute_phase.vm_steps, 39);
Ok(())
}
}