1use num_bigint::BigInt;
2use tycho_types::crc::crc_16;
3use tycho_types::models::{
4 Account, AccountState, BlockchainConfigParams, ComputeGasParams, CurrencyCollection,
5 ExtInMsgInfo, IntAddr, IntMsgInfo, LibDescr, MsgInfo, MsgType, OwnedMessage, StdAddr, TickTock,
6};
7use tycho_types::num::Tokens;
8use tycho_types::prelude::*;
9
10use crate::{
11 BehaviourModifiers, CommittedState, GasParams, RcStackValue, SafeRc, SmcInfoBase, Stack,
12 UnpackedInMsgSmcInfo, VmStateBuilder, VmVersion,
13};
14
15pub trait VmGetterMethodId {
16 fn as_getter_method_id(&self) -> u32;
17}
18
19impl<T: VmGetterMethodId + ?Sized> VmGetterMethodId for &T {
20 fn as_getter_method_id(&self) -> u32 {
21 T::as_getter_method_id(*self)
22 }
23}
24
25impl<T: VmGetterMethodId + ?Sized> VmGetterMethodId for &mut T {
26 fn as_getter_method_id(&self) -> u32 {
27 T::as_getter_method_id(*self)
28 }
29}
30
31impl VmGetterMethodId for u32 {
32 fn as_getter_method_id(&self) -> u32 {
33 *self
34 }
35}
36
37impl VmGetterMethodId for str {
38 fn as_getter_method_id(&self) -> u32 {
39 let crc = crc_16(self.as_bytes());
40 crc as u32 | 0x10000
41 }
42}
43
44pub enum VmCallerTxInput {
45 TickTock(TickTock),
46 Ordinary(Cell),
47}
48
49pub struct VmCaller {
50 pub libraries: Dict<HashBytes, LibDescr>,
51 pub behaviour_modifiers: BehaviourModifiers,
52 pub config: BlockchainConfigParams,
53}
54
55impl VmCaller {
56 pub fn call_with_external_message_body(
57 &self,
58 account: &Account,
59 body: Cell,
60 ) -> Result<VmMessageOutput, VmMessageError> {
61 self.call_with_external_message_body_ext(account, body, &Default::default(), None)
62 }
63
64 pub fn call_with_external_message_body_ext(
65 &self,
66 account: &Account,
67 body: Cell,
68 args: &VmMessageArgs,
69 debug: Option<&mut dyn std::fmt::Write>,
70 ) -> Result<VmMessageOutput, VmMessageError> {
71 let msg = build_external_message(&account.address, body)
72 .map_err(VmMessageError::InvalidMessage)?;
73 self.call_with_message_ext(account, msg, args, debug)
74 }
75
76 pub fn call_with_internal_message_body(
77 &self,
78 account: &Account,
79 amount: CurrencyCollection,
80 body: Cell,
81 ) -> Result<VmMessageOutput, VmMessageError> {
82 self.call_with_internal_message_body_ext(account, amount, body, &Default::default(), None)
83 }
84
85 pub fn call_with_internal_message_body_ext(
86 &self,
87 account: &Account,
88 amount: CurrencyCollection,
89 body: Cell,
90 args: &VmMessageArgs,
91 debug: Option<&mut dyn std::fmt::Write>,
92 ) -> Result<VmMessageOutput, VmMessageError> {
93 let msg = build_internal_message(&account.address, amount, body)
94 .map_err(VmMessageError::InvalidMessage)?;
95 self.call_with_message_ext(account, msg, args, debug)
96 }
97
98 pub fn call_with_message(
99 &self,
100 account: &Account,
101 msg: Cell,
102 ) -> Result<VmMessageOutput, VmMessageError> {
103 self.call_with_message_ext(account, msg, &Default::default(), None)
104 }
105
106 pub fn call_with_message_ext(
107 &self,
108 account: &Account,
109 msg: Cell,
110 args: &VmMessageArgs,
111 debug: Option<&mut dyn std::fmt::Write>,
112 ) -> Result<VmMessageOutput, VmMessageError> {
113 self.call_tx_ext(account, VmCallerTxInput::Ordinary(msg), args, debug)
114 }
115
116 pub fn call_tick_tock(
117 &self,
118 account: &Account,
119 tick_tock: TickTock,
120 ) -> Result<VmMessageOutput, VmMessageError> {
121 self.call_tick_tock_ext(account, tick_tock, &Default::default(), None)
122 }
123
124 pub fn call_tick_tock_ext(
125 &self,
126 account: &Account,
127 tick_tock: TickTock,
128 args: &VmMessageArgs,
129 debug: Option<&mut dyn std::fmt::Write>,
130 ) -> Result<VmMessageOutput, VmMessageError> {
131 self.call_tx_ext(account, VmCallerTxInput::TickTock(tick_tock), args, debug)
132 }
133
134 pub fn call_tx_ext(
135 &self,
136 account: &Account,
137 tx_type: VmCallerTxInput,
138 args: &VmMessageArgs,
139 debug: Option<&mut dyn std::fmt::Write>,
140 ) -> Result<VmMessageOutput, VmMessageError> {
141 let state = match &account.state {
142 AccountState::Active(state_init) => state_init,
143 _ => return Err(VmMessageError::NoCode),
144 };
145 let code = state.code.clone().ok_or(VmMessageError::NoCode)?;
146
147 let mut balance = args
148 .override_balance
149 .clone()
150 .unwrap_or_else(|| account.balance.clone());
151
152 let address_hash = match &account.address {
153 IntAddr::Std(addr) => addr.address,
154 IntAddr::Var(_) => HashBytes::ZERO,
155 };
156
157 let is_tx_ordinary;
158 let msg_lt;
159 let message_balance;
160 let unpacked_in_msg;
161
162 let stack = match tx_type {
163 VmCallerTxInput::TickTock(ty) => {
164 is_tx_ordinary = false;
165 msg_lt = 0;
166 message_balance = CurrencyCollection::ZERO;
167 unpacked_in_msg = None;
168
169 tuple![
170 int balance.tokens,
171 int address_hash.as_bigint(),
172 int match ty {
173 TickTock::Tick => 0,
174 TickTock::Tock => -1,
175 },
176 int -2,
177 ]
178 }
179 VmCallerTxInput::Ordinary(msg) => {
180 is_tx_ordinary = true;
181 let parsed = msg
182 .parse::<OwnedMessage>()
183 .map_err(VmMessageError::InvalidMessage)?;
184
185 let selector;
186 match &parsed.info {
187 MsgInfo::ExtIn(_) => {
188 msg_lt = 0;
189 selector = -1;
190 message_balance = CurrencyCollection::ZERO;
191 unpacked_in_msg = None;
192 }
193 MsgInfo::Int(info) => {
194 let src_addr_slice =
195 load_int_msg_src_addr(&msg).map_err(VmMessageError::InvalidMessage)?;
196
197 msg_lt = info.created_lt;
198 selector = 0;
199 message_balance = info.value.clone();
200 unpacked_in_msg = Some(
201 UnpackedInMsgSmcInfo {
202 bounce: info.bounce,
203 bounced: info.bounced,
204 src_addr: src_addr_slice.into(),
205 fwd_fee: info.fwd_fee,
206 created_lt: info.created_lt,
207 created_at: info.created_at,
208 original_value: info.value.tokens,
209 remaining_value: info.value.clone(),
210 state_init: parsed
211 .init
212 .as_ref()
213 .map(CellBuilder::build_from)
214 .transpose()
215 .map_err(VmMessageError::InvalidMessage)?,
216 }
217 .into_tuple(),
218 );
219
220 balance.try_add_assign(&info.value).map_err(|e| match e {
221 tycho_types::error::Error::IntOverflow => {
222 VmMessageError::BalanceOverflow
223 }
224 _ => VmMessageError::InvalidMessage(e),
225 })?;
226 }
227 MsgInfo::ExtOut(_) => return Err(VmMessageError::ExtOut),
228 }
229
230 tuple![
231 int balance.tokens,
232 int message_balance.tokens,
233 cell msg,
234 slice parsed.body,
235 int selector,
236 ]
237 }
238 };
239
240 let gas_params = match args.override_gas_params {
241 Some(params) => params,
242 None => {
243 let masterchain = account.address.is_masterchain();
244 let prices = self
245 .config
246 .get_gas_prices(masterchain)
247 .map_err(VmMessageError::InvalidConfig)?;
248
249 let is_special = match args.is_special {
250 Some(is_special) => is_special,
251 None if masterchain => (|| {
252 self.config
253 .get_fundamental_addresses()?
254 .contains_key(address_hash)
255 })()
256 .map_err(VmMessageError::InvalidConfig)?,
257 None => false,
258 };
259
260 let computed = prices.compute_gas_params(ComputeGasParams {
261 account_balance: &balance.tokens,
262 message_balance: &message_balance.tokens,
263 is_special,
264 is_tx_ordinary,
265 is_in_msg_external: unpacked_in_msg.is_none(),
266 });
267
268 GasParams {
269 max: computed.max,
270 limit: computed.limit,
271 credit: computed.credit,
272 price: prices.gas_price,
273 }
274 }
275 };
276
277 let lt = std::cmp::max(account.last_trans_lt, msg_lt);
278 let smc_info = SmcInfoBase::new()
279 .with_now(args.now)
280 .with_block_lt(lt)
281 .with_tx_lt(lt)
282 .with_mixed_rand_seed(&args.rand_seed, &address_hash)
283 .with_account_balance(balance.clone())
284 .with_account_addr(account.address.clone())
285 .with_config(self.config.clone())
286 .require_ton_v4()
287 .with_code(code.clone())
288 .with_message_balance(message_balance)
289 .require_ton_v6()
290 .fill_unpacked_config()
291 .map_err(VmMessageError::InvalidConfig)?
292 .require_ton_v11()
293 .with_unpacked_in_msg(unpacked_in_msg);
294
295 let data = state.data.clone().unwrap_or_default();
296
297 let libraries = (&state.libraries, &self.libraries);
299 let mut vm = VmStateBuilder::new()
300 .with_smc_info(smc_info)
301 .with_code(code)
302 .with_data(data)
303 .with_libraries(&libraries)
304 .with_init_selector(false)
305 .with_raw_stack(SafeRc::new(Stack { items: stack }))
306 .with_gas(gas_params)
307 .with_modifiers(self.behaviour_modifiers)
308 .with_version(VmVersion::LATEST_TON)
309 .build();
310
311 if let Some(debug) = debug {
312 vm.debug = Some(debug);
313 }
314
315 let exit_code = !vm.run();
316
317 let accepted = vm.gas.credit() == 0;
318 let success = accepted && vm.committed_state.is_some();
319
320 Ok(VmMessageOutput {
321 exit_code,
322 exit_arg: if success {
323 None
324 } else {
325 vm.stack.get_exit_arg().filter(|x| *x != 0)
326 },
327 stack: vm.stack.items.clone(),
328 success,
329 gas_used: vm.gas.consumed(),
330 missing_library: vm.gas.missing_library(),
331
332 accepted,
333 commited: vm.committed_state.filter(|_| accepted),
334 })
335 }
336
337 pub fn call_getter<T: VmGetterMethodId>(
338 &self,
339 account: &Account,
340 method: T,
341 stack: Vec<RcStackValue>,
342 ) -> Result<VmGetterOutput, VmGetterError> {
343 self.call_getter_impl(
344 account,
345 method.as_getter_method_id(),
346 stack,
347 &Default::default(),
348 None,
349 )
350 }
351
352 #[inline]
353 pub fn call_getter_ext<T: VmGetterMethodId>(
354 &self,
355 account: &Account,
356 method: T,
357 stack: Vec<RcStackValue>,
358 args: &VmGetterArgs,
359 debug: Option<&mut dyn std::fmt::Write>,
360 ) -> Result<VmGetterOutput, VmGetterError> {
361 self.call_getter_impl(account, method.as_getter_method_id(), stack, args, debug)
362 }
363
364 fn call_getter_impl(
365 &self,
366 account: &Account,
367 method_id: u32,
368 mut stack: Vec<RcStackValue>,
369 args: &VmGetterArgs,
370 debug: Option<&mut dyn std::fmt::Write>,
371 ) -> Result<VmGetterOutput, VmGetterError> {
372 let state = match &account.state {
373 AccountState::Active(state_init) => state_init,
374 _ => return Err(VmGetterError::NoCode),
375 };
376 let code = state.code.clone().ok_or(VmGetterError::NoCode)?;
377
378 stack.push(RcStackValue::new_dyn_value(BigInt::from(method_id)));
379
380 let address_hash = match &account.address {
381 IntAddr::Std(addr) => addr.address,
382 IntAddr::Var(_) => HashBytes::ZERO,
383 };
384
385 let smc_info = SmcInfoBase::new()
386 .with_now(args.now)
387 .with_block_lt(account.last_trans_lt)
388 .with_tx_lt(account.last_trans_lt)
389 .with_mixed_rand_seed(&args.rand_seed, &address_hash)
390 .with_account_balance(account.balance.clone())
391 .with_account_addr(account.address.clone())
392 .with_config(self.config.clone())
393 .require_ton_v4()
394 .with_code(code.clone())
395 .require_ton_v6()
396 .fill_unpacked_config()
397 .map_err(VmGetterError::InvalidConfig)?
398 .require_ton_v11();
399
400 let data = state.data.clone().unwrap_or_default();
401
402 let libraries = (&state.libraries, &self.libraries);
403 let mut vm = VmStateBuilder::new()
404 .with_smc_info(smc_info)
405 .with_code(code)
406 .with_data(data)
407 .with_libraries(&libraries)
408 .with_init_selector(false)
409 .with_stack(stack)
410 .with_gas(args.gas_params)
411 .with_modifiers(self.behaviour_modifiers)
412 .build();
413
414 if let Some(debug) = debug {
415 vm.debug = Some(debug);
416 }
417
418 let exit_code = !vm.run();
419
420 Ok(VmGetterOutput {
421 exit_code,
422 stack: vm.stack.items.clone(),
423 success: exit_code == 0 || exit_code == 1,
424 gas_used: vm.gas.consumed(),
425 missing_library: vm.gas.missing_library(),
426 })
427 }
428}
429
430fn load_int_msg_src_addr(msg_root: &Cell) -> Result<CellSliceParts, tycho_types::error::Error> {
431 let mut cs = msg_root.as_slice()?;
432 if MsgType::load_from(&mut cs)? != MsgType::Int {
433 return Err(tycho_types::error::Error::InvalidTag);
434 }
435
436 cs.skip_first(3, 0)?;
438 let mut addr_slice = cs;
439 IntAddr::load_from(&mut cs)?;
441 addr_slice.skip_last(cs.size_bits(), cs.size_refs())?;
442 let range = addr_slice.range();
443
444 Ok((range, msg_root.clone()))
445}
446
447fn build_internal_message(
448 address: &IntAddr,
449 amount: CurrencyCollection,
450 body: Cell,
451) -> Result<Cell, tycho_types::error::Error> {
452 CellBuilder::build_from(OwnedMessage {
453 info: MsgInfo::Int(IntMsgInfo {
454 ihr_disabled: true,
455 bounce: true,
456 bounced: false,
457 src: StdAddr::default().into(),
458 dst: address.clone(),
459 value: amount,
460 extra_flags: Default::default(),
461 fwd_fee: Tokens::ZERO,
462 created_lt: 0,
463 created_at: 0,
464 }),
465 init: None,
466 body: body.into(),
467 layout: None,
468 })
469}
470
471fn build_external_message(
472 address: &IntAddr,
473 body: Cell,
474) -> Result<Cell, tycho_types::error::Error> {
475 CellBuilder::build_from(OwnedMessage {
476 info: MsgInfo::ExtIn(ExtInMsgInfo {
477 src: None,
478 dst: address.clone(),
479 import_fee: Tokens::ZERO,
480 }),
481 init: None,
482 body: body.into(),
483 layout: None,
484 })
485}
486
487#[derive(Debug)]
488#[non_exhaustive]
489pub struct VmMessageArgs {
490 pub now: u32,
494 pub rand_seed: HashBytes,
498 pub override_gas_params: Option<GasParams>,
502 pub override_balance: Option<CurrencyCollection>,
506 pub is_special: Option<bool>,
510}
511
512impl Default for VmMessageArgs {
513 fn default() -> Self {
514 Self {
515 #[cfg(target_arch = "wasm32")]
516 now: 0,
517 #[cfg(not(target_arch = "wasm32"))]
518 now: std::time::SystemTime::now()
519 .duration_since(std::time::SystemTime::UNIX_EPOCH)
520 .unwrap()
521 .as_secs() as u32,
522 rand_seed: HashBytes::ZERO,
523 override_gas_params: None,
524 override_balance: None,
525 is_special: None,
526 }
527 }
528}
529
530#[derive(Debug)]
531pub struct VmMessageOutput {
532 pub exit_code: i32,
533 pub exit_arg: Option<i32>,
534 pub stack: Vec<RcStackValue>,
535 pub success: bool,
536 pub gas_used: u64,
537 pub missing_library: Option<HashBytes>,
538
539 pub accepted: bool,
540 pub commited: Option<CommittedState>,
541}
542
543#[derive(Debug, thiserror::Error)]
544pub enum VmMessageError {
545 #[error("external outbound message cannot be executed")]
546 ExtOut,
547 #[error("invalid message: {0}")]
548 InvalidMessage(tycho_types::error::Error),
549 #[error("invalid config: {0}")]
550 InvalidConfig(tycho_types::error::Error),
551 #[error("account balance overflowed")]
552 BalanceOverflow,
553 #[error("account has no code")]
554 NoCode,
555}
556
557#[derive(Debug)]
558#[non_exhaustive]
559pub struct VmGetterArgs {
560 pub now: u32,
564 pub rand_seed: HashBytes,
568 pub gas_params: GasParams,
572}
573
574impl Default for VmGetterArgs {
575 fn default() -> Self {
576 Self {
577 #[cfg(target_arch = "wasm32")]
578 now: 0,
579 #[cfg(not(target_arch = "wasm32"))]
580 now: std::time::SystemTime::now()
581 .duration_since(std::time::SystemTime::UNIX_EPOCH)
582 .unwrap()
583 .as_secs() as u32,
584 rand_seed: HashBytes::ZERO,
585 gas_params: GasParams::getter(),
586 }
587 }
588}
589
590#[derive(Debug)]
591pub struct VmGetterOutput {
592 pub exit_code: i32,
593 pub stack: Vec<RcStackValue>,
594 pub success: bool,
595 pub gas_used: u64,
596 pub missing_library: Option<HashBytes>,
597}
598
599#[derive(Debug, thiserror::Error)]
600pub enum VmGetterError {
601 #[error("account has no code")]
602 NoCode,
603 #[error("invalid config: {0}")]
604 InvalidConfig(tycho_types::error::Error),
605}