use num_bigint::BigInt;
use tycho_types::crc::crc_16;
use tycho_types::models::{
Account, AccountState, BlockchainConfigParams, ComputeGasParams, CurrencyCollection,
ExtInMsgInfo, IntAddr, IntMsgInfo, LibDescr, MsgInfo, MsgType, OwnedMessage, StdAddr, TickTock,
};
use tycho_types::num::Tokens;
use tycho_types::prelude::*;
use crate::{
BehaviourModifiers, CommittedState, GasParams, RcStackValue, SafeRc, SmcInfoBase, Stack,
UnpackedInMsgSmcInfo, VmStateBuilder, VmVersion,
};
pub trait VmGetterMethodId {
fn as_getter_method_id(&self) -> u32;
}
impl<T: VmGetterMethodId + ?Sized> VmGetterMethodId for &T {
fn as_getter_method_id(&self) -> u32 {
T::as_getter_method_id(*self)
}
}
impl<T: VmGetterMethodId + ?Sized> VmGetterMethodId for &mut T {
fn as_getter_method_id(&self) -> u32 {
T::as_getter_method_id(*self)
}
}
impl VmGetterMethodId for u32 {
fn as_getter_method_id(&self) -> u32 {
*self
}
}
impl VmGetterMethodId for str {
fn as_getter_method_id(&self) -> u32 {
let crc = crc_16(self.as_bytes());
crc as u32 | 0x10000
}
}
pub enum VmCallerTxInput {
TickTock(TickTock),
Ordinary(Cell),
}
pub struct VmCaller {
pub libraries: Dict<HashBytes, LibDescr>,
pub behaviour_modifiers: BehaviourModifiers,
pub config: BlockchainConfigParams,
}
impl VmCaller {
pub fn call_with_external_message_body(
&self,
account: &Account,
body: Cell,
) -> Result<VmMessageOutput, VmMessageError> {
self.call_with_external_message_body_ext(account, body, &Default::default(), None)
}
pub fn call_with_external_message_body_ext(
&self,
account: &Account,
body: Cell,
args: &VmMessageArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmMessageOutput, VmMessageError> {
let msg = build_external_message(&account.address, body)
.map_err(VmMessageError::InvalidMessage)?;
self.call_with_message_ext(account, msg, args, debug)
}
pub fn call_with_internal_message_body(
&self,
account: &Account,
amount: CurrencyCollection,
body: Cell,
) -> Result<VmMessageOutput, VmMessageError> {
self.call_with_internal_message_body_ext(account, amount, body, &Default::default(), None)
}
pub fn call_with_internal_message_body_ext(
&self,
account: &Account,
amount: CurrencyCollection,
body: Cell,
args: &VmMessageArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmMessageOutput, VmMessageError> {
let msg = build_internal_message(&account.address, amount, body)
.map_err(VmMessageError::InvalidMessage)?;
self.call_with_message_ext(account, msg, args, debug)
}
pub fn call_with_message(
&self,
account: &Account,
msg: Cell,
) -> Result<VmMessageOutput, VmMessageError> {
self.call_with_message_ext(account, msg, &Default::default(), None)
}
pub fn call_with_message_ext(
&self,
account: &Account,
msg: Cell,
args: &VmMessageArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmMessageOutput, VmMessageError> {
self.call_tx_ext(account, VmCallerTxInput::Ordinary(msg), args, debug)
}
pub fn call_tick_tock(
&self,
account: &Account,
tick_tock: TickTock,
) -> Result<VmMessageOutput, VmMessageError> {
self.call_tick_tock_ext(account, tick_tock, &Default::default(), None)
}
pub fn call_tick_tock_ext(
&self,
account: &Account,
tick_tock: TickTock,
args: &VmMessageArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmMessageOutput, VmMessageError> {
self.call_tx_ext(account, VmCallerTxInput::TickTock(tick_tock), args, debug)
}
pub fn call_tx_ext(
&self,
account: &Account,
tx_type: VmCallerTxInput,
args: &VmMessageArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmMessageOutput, VmMessageError> {
let state = match &account.state {
AccountState::Active(state_init) => state_init,
_ => return Err(VmMessageError::NoCode),
};
let code = state.code.clone().ok_or(VmMessageError::NoCode)?;
let mut balance = args
.override_balance
.clone()
.unwrap_or_else(|| account.balance.clone());
let address_hash = match &account.address {
IntAddr::Std(addr) => addr.address,
IntAddr::Var(_) => HashBytes::ZERO,
};
let is_tx_ordinary;
let msg_lt;
let message_balance;
let unpacked_in_msg;
let stack = match tx_type {
VmCallerTxInput::TickTock(ty) => {
is_tx_ordinary = false;
msg_lt = 0;
message_balance = CurrencyCollection::ZERO;
unpacked_in_msg = None;
tuple![
int balance.tokens,
int address_hash.as_bigint(),
int match ty {
TickTock::Tick => 0,
TickTock::Tock => -1,
},
int -2,
]
}
VmCallerTxInput::Ordinary(msg) => {
is_tx_ordinary = true;
let parsed = msg
.parse::<OwnedMessage>()
.map_err(VmMessageError::InvalidMessage)?;
let selector;
match &parsed.info {
MsgInfo::ExtIn(_) => {
msg_lt = 0;
selector = -1;
message_balance = CurrencyCollection::ZERO;
unpacked_in_msg = None;
}
MsgInfo::Int(info) => {
let src_addr_slice =
load_int_msg_src_addr(&msg).map_err(VmMessageError::InvalidMessage)?;
msg_lt = info.created_lt;
selector = 0;
message_balance = info.value.clone();
unpacked_in_msg = Some(
UnpackedInMsgSmcInfo {
bounce: info.bounce,
bounced: info.bounced,
src_addr: src_addr_slice.into(),
fwd_fee: info.fwd_fee,
created_lt: info.created_lt,
created_at: info.created_at,
original_value: info.value.tokens,
remaining_value: info.value.clone(),
state_init: parsed
.init
.as_ref()
.map(CellBuilder::build_from)
.transpose()
.map_err(VmMessageError::InvalidMessage)?,
}
.into_tuple(),
);
balance.try_add_assign(&info.value).map_err(|e| match e {
tycho_types::error::Error::IntOverflow => {
VmMessageError::BalanceOverflow
}
_ => VmMessageError::InvalidMessage(e),
})?;
}
MsgInfo::ExtOut(_) => return Err(VmMessageError::ExtOut),
}
tuple![
int balance.tokens,
int message_balance.tokens,
cell msg,
slice parsed.body,
int selector,
]
}
};
let gas_params = match args.override_gas_params {
Some(params) => params,
None => {
let masterchain = account.address.is_masterchain();
let prices = self
.config
.get_gas_prices(masterchain)
.map_err(VmMessageError::InvalidConfig)?;
let is_special = match args.is_special {
Some(is_special) => is_special,
None if masterchain => (|| {
self.config
.get_fundamental_addresses()?
.contains_key(address_hash)
})()
.map_err(VmMessageError::InvalidConfig)?,
None => false,
};
let computed = prices.compute_gas_params(ComputeGasParams {
account_balance: &balance.tokens,
message_balance: &message_balance.tokens,
is_special,
is_tx_ordinary,
is_in_msg_external: unpacked_in_msg.is_none(),
});
GasParams {
max: computed.max,
limit: computed.limit,
credit: computed.credit,
price: prices.gas_price,
}
}
};
let lt = std::cmp::max(account.last_trans_lt, msg_lt);
let smc_info = SmcInfoBase::new()
.with_now(args.now)
.with_block_lt(lt)
.with_tx_lt(lt)
.with_mixed_rand_seed(&args.rand_seed, &address_hash)
.with_account_balance(balance.clone())
.with_account_addr(account.address.clone())
.with_config(self.config.clone())
.require_ton_v4()
.with_code(code.clone())
.with_message_balance(message_balance)
.require_ton_v6()
.fill_unpacked_config()
.map_err(VmMessageError::InvalidConfig)?
.require_ton_v11()
.with_unpacked_in_msg(unpacked_in_msg);
let data = state.data.clone().unwrap_or_default();
let libraries = (&state.libraries, &self.libraries);
let mut vm = VmStateBuilder::new()
.with_smc_info(smc_info)
.with_code(code)
.with_data(data)
.with_libraries(&libraries)
.with_init_selector(false)
.with_raw_stack(SafeRc::new(Stack { items: stack }))
.with_gas(gas_params)
.with_modifiers(self.behaviour_modifiers)
.with_version(VmVersion::LATEST_TON)
.build();
if let Some(debug) = debug {
vm.debug = Some(debug);
}
let exit_code = !vm.run();
let accepted = vm.gas.credit() == 0;
let success = accepted && vm.committed_state.is_some();
Ok(VmMessageOutput {
exit_code,
exit_arg: if success {
None
} else {
vm.stack.get_exit_arg().filter(|x| *x != 0)
},
stack: vm.stack.items.clone(),
success,
gas_used: vm.gas.consumed(),
missing_library: vm.gas.missing_library(),
accepted,
commited: vm.committed_state.filter(|_| accepted),
})
}
pub fn call_getter<T: VmGetterMethodId>(
&self,
account: &Account,
method: T,
stack: Vec<RcStackValue>,
) -> Result<VmGetterOutput, VmGetterError> {
self.call_getter_impl(
account,
method.as_getter_method_id(),
stack,
&Default::default(),
None,
)
}
#[inline]
pub fn call_getter_ext<T: VmGetterMethodId>(
&self,
account: &Account,
method: T,
stack: Vec<RcStackValue>,
args: &VmGetterArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmGetterOutput, VmGetterError> {
self.call_getter_impl(account, method.as_getter_method_id(), stack, args, debug)
}
fn call_getter_impl(
&self,
account: &Account,
method_id: u32,
mut stack: Vec<RcStackValue>,
args: &VmGetterArgs,
debug: Option<&mut dyn std::fmt::Write>,
) -> Result<VmGetterOutput, VmGetterError> {
let state = match &account.state {
AccountState::Active(state_init) => state_init,
_ => return Err(VmGetterError::NoCode),
};
let code = state.code.clone().ok_or(VmGetterError::NoCode)?;
stack.push(RcStackValue::new_dyn_value(BigInt::from(method_id)));
let address_hash = match &account.address {
IntAddr::Std(addr) => addr.address,
IntAddr::Var(_) => HashBytes::ZERO,
};
let smc_info = SmcInfoBase::new()
.with_now(args.now)
.with_block_lt(account.last_trans_lt)
.with_tx_lt(account.last_trans_lt)
.with_mixed_rand_seed(&args.rand_seed, &address_hash)
.with_account_balance(account.balance.clone())
.with_account_addr(account.address.clone())
.with_config(self.config.clone())
.require_ton_v4()
.with_code(code.clone())
.require_ton_v6()
.fill_unpacked_config()
.map_err(VmGetterError::InvalidConfig)?
.require_ton_v11();
let data = state.data.clone().unwrap_or_default();
let libraries = (&state.libraries, &self.libraries);
let mut vm = VmStateBuilder::new()
.with_smc_info(smc_info)
.with_code(code)
.with_data(data)
.with_libraries(&libraries)
.with_init_selector(false)
.with_stack(stack)
.with_gas(args.gas_params)
.with_modifiers(self.behaviour_modifiers)
.build();
if let Some(debug) = debug {
vm.debug = Some(debug);
}
let exit_code = !vm.run();
Ok(VmGetterOutput {
exit_code,
stack: vm.stack.items.clone(),
success: exit_code == 0 || exit_code == 1,
gas_used: vm.gas.consumed(),
missing_library: vm.gas.missing_library(),
})
}
}
fn load_int_msg_src_addr(msg_root: &Cell) -> Result<CellSliceParts, tycho_types::error::Error> {
let mut cs = msg_root.as_slice()?;
if MsgType::load_from(&mut cs)? != MsgType::Int {
return Err(tycho_types::error::Error::InvalidTag);
}
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())?;
let range = addr_slice.range();
Ok((range, msg_root.clone()))
}
fn build_internal_message(
address: &IntAddr,
amount: CurrencyCollection,
body: Cell,
) -> Result<Cell, tycho_types::error::Error> {
CellBuilder::build_from(OwnedMessage {
info: MsgInfo::Int(IntMsgInfo {
ihr_disabled: true,
bounce: true,
bounced: false,
src: StdAddr::default().into(),
dst: address.clone(),
value: amount,
extra_flags: Default::default(),
fwd_fee: Tokens::ZERO,
created_lt: 0,
created_at: 0,
}),
init: None,
body: body.into(),
layout: None,
})
}
fn build_external_message(
address: &IntAddr,
body: Cell,
) -> Result<Cell, tycho_types::error::Error> {
CellBuilder::build_from(OwnedMessage {
info: MsgInfo::ExtIn(ExtInMsgInfo {
src: None,
dst: address.clone(),
import_fee: Tokens::ZERO,
}),
init: None,
body: body.into(),
layout: None,
})
}
#[derive(Debug)]
#[non_exhaustive]
pub struct VmMessageArgs {
pub now: u32,
pub rand_seed: HashBytes,
pub override_gas_params: Option<GasParams>,
pub override_balance: Option<CurrencyCollection>,
pub is_special: Option<bool>,
}
impl Default for VmMessageArgs {
fn default() -> Self {
Self {
#[cfg(target_arch = "wasm32")]
now: 0,
#[cfg(not(target_arch = "wasm32"))]
now: std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as u32,
rand_seed: HashBytes::ZERO,
override_gas_params: None,
override_balance: None,
is_special: None,
}
}
}
#[derive(Debug)]
pub struct VmMessageOutput {
pub exit_code: i32,
pub exit_arg: Option<i32>,
pub stack: Vec<RcStackValue>,
pub success: bool,
pub gas_used: u64,
pub missing_library: Option<HashBytes>,
pub accepted: bool,
pub commited: Option<CommittedState>,
}
#[derive(Debug, thiserror::Error)]
pub enum VmMessageError {
#[error("external outbound message cannot be executed")]
ExtOut,
#[error("invalid message: {0}")]
InvalidMessage(tycho_types::error::Error),
#[error("invalid config: {0}")]
InvalidConfig(tycho_types::error::Error),
#[error("account balance overflowed")]
BalanceOverflow,
#[error("account has no code")]
NoCode,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct VmGetterArgs {
pub now: u32,
pub rand_seed: HashBytes,
pub gas_params: GasParams,
}
impl Default for VmGetterArgs {
fn default() -> Self {
Self {
#[cfg(target_arch = "wasm32")]
now: 0,
#[cfg(not(target_arch = "wasm32"))]
now: std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as u32,
rand_seed: HashBytes::ZERO,
gas_params: GasParams::getter(),
}
}
}
#[derive(Debug)]
pub struct VmGetterOutput {
pub exit_code: i32,
pub stack: Vec<RcStackValue>,
pub success: bool,
pub gas_used: u64,
pub missing_library: Option<HashBytes>,
}
#[derive(Debug, thiserror::Error)]
pub enum VmGetterError {
#[error("account has no code")]
NoCode,
#[error("invalid config: {0}")]
InvalidConfig(tycho_types::error::Error),
}