Skip to main content

tycho_vm/
getter.rs

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        // TODO: Also use libraries from the message here.
298        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    // Skip flags.
437    cs.skip_first(3, 0)?;
438    let mut addr_slice = cs;
439    // Read `src`.
440    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    /// Current unix timestamp.
491    ///
492    /// Default: current timestamp on non-wasm platforms and `0` on wasm.
493    pub now: u32,
494    /// Random seed.
495    ///
496    /// Default: [`HashBytes::ZERO`].
497    pub rand_seed: HashBytes,
498    /// Custom gas limits for execution.
499    ///
500    /// Default: max gas.
501    pub override_gas_params: Option<GasParams>,
502    /// Set custom account balance.
503    ///
504    /// Default: `None`.
505    pub override_balance: Option<CurrencyCollection>,
506    /// Override whether the account is specia.
507    ///
508    /// Default: `None` (uses config).
509    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    /// Current unix timestamp.
561    ///
562    /// Default: current timestamp on non-wasm platforms and `0` on wasm.
563    pub now: u32,
564    /// Random seed.
565    ///
566    /// Default: [`HashBytes::ZERO`].
567    pub rand_seed: HashBytes,
568    /// Gas limits for execution.
569    ///
570    /// Default: [`GasParams::getter`]
571    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}