Skip to main content

tycho_executor/
lib.rs

1use anyhow::{Context, Result};
2use tycho_types::cell::Lazy;
3use tycho_types::dict;
4use tycho_types::error::Error;
5use tycho_types::models::{
6    Account, AccountState, AccountStatus, BlockId, CurrencyCollection, HashUpdate, IntAddr,
7    LibDescr, Message, OwnedMessage, ShardAccount, SimpleLib, StdAddr, StorageInfo, StorageUsed,
8    TickTock, Transaction, TxInfo,
9};
10use tycho_types::num::{Tokens, Uint15};
11use tycho_types::prelude::*;
12
13pub use self::config::ParsedConfig;
14pub use self::error::{TxError, TxResult};
15use self::util::new_varuint56_truncate;
16pub use self::util::{ExtStorageStat, OwnedExtStorageStat, StorageStatLimits};
17
18mod config;
19mod error;
20mod util;
21
22pub mod phase {
23    pub use self::action::{ActionPhaseContext, ActionPhaseFull};
24    pub use self::bounce::BouncePhaseContext;
25    pub use self::compute::{
26        ComputePhaseContext, ComputePhaseFull, ComputePhaseSmcInfo, TransactionInput,
27    };
28    pub use self::receive::{MsgStateInit, ReceivedMessage};
29    pub use self::storage::StoragePhaseContext;
30
31    mod action;
32    mod bounce;
33    mod compute;
34    mod credit;
35    mod receive;
36    mod storage;
37}
38
39mod tx {
40    mod ordinary;
41    mod ticktock;
42}
43
44/// Transaction executor.
45#[derive(Debug)]
46pub struct Executor<'a> {
47    params: &'a ExecutorParams,
48    config: &'a ParsedConfig,
49    min_lt: u64,
50    override_special: Option<bool>,
51}
52
53impl<'a> Executor<'a> {
54    pub fn new(params: &'a ExecutorParams, config: &'a ParsedConfig) -> Self {
55        Self {
56            params,
57            config,
58            min_lt: 0,
59            override_special: None,
60        }
61    }
62
63    pub fn with_min_lt(mut self, min_lt: u64) -> Self {
64        self.set_min_lt(min_lt);
65        self
66    }
67
68    pub fn set_min_lt(&mut self, min_lt: u64) {
69        self.min_lt = min_lt;
70    }
71
72    pub fn override_special(mut self, is_special: bool) -> Self {
73        self.override_special = Some(is_special);
74        self
75    }
76
77    #[inline]
78    pub fn begin_ordinary<'s, M>(
79        &self,
80        address: &StdAddr,
81        is_external: bool,
82        msg: M,
83        state: &'s ShardAccount,
84    ) -> TxResult<UncommittedTransaction<'a, 's>>
85    where
86        M: LoadMessage,
87    {
88        self.begin_ordinary_ext(address, is_external, msg, state, None)
89    }
90
91    pub fn begin_ordinary_ext<'s, M>(
92        &self,
93        address: &StdAddr,
94        is_external: bool,
95        msg: M,
96        state: &'s ShardAccount,
97        inspector: Option<&mut ExecutorInspector<'_>>,
98    ) -> TxResult<UncommittedTransaction<'a, 's>>
99    where
100        M: LoadMessage,
101    {
102        let msg_root = msg.load_message_root()?;
103
104        let account = state.load_account()?;
105        let mut exec = self.begin(address, account)?;
106        let info = exec.run_ordinary_transaction(is_external, msg_root.clone(), inspector)?;
107
108        UncommittedTransaction::with_info(exec, state, Some(msg_root), info).map_err(TxError::Fatal)
109    }
110
111    #[inline]
112    pub fn check_ordinary<M>(&self, address: &StdAddr, msg: M, state: &ShardAccount) -> TxResult<()>
113    where
114        M: LoadMessage,
115    {
116        self.check_ordinary_ext(address, msg, state, None)
117    }
118
119    pub fn check_ordinary_ext<M>(
120        &self,
121        address: &StdAddr,
122        msg: M,
123        state: &ShardAccount,
124        inspector: Option<&mut ExecutorInspector<'_>>,
125    ) -> TxResult<()>
126    where
127        M: LoadMessage,
128    {
129        let msg_root = msg.load_message_root()?;
130        let account = state.load_account()?;
131        let mut exec = self.begin(address, account)?;
132        exec.check_ordinary_transaction(msg_root, inspector)
133    }
134
135    #[inline]
136    pub fn begin_tick_tock<'s>(
137        &self,
138        address: &StdAddr,
139        kind: TickTock,
140        state: &'s ShardAccount,
141    ) -> TxResult<UncommittedTransaction<'a, 's>> {
142        self.begin_tick_tock_ext(address, kind, state, None)
143    }
144
145    pub fn begin_tick_tock_ext<'s>(
146        &self,
147        address: &StdAddr,
148        kind: TickTock,
149        state: &'s ShardAccount,
150        inspector: Option<&mut ExecutorInspector<'_>>,
151    ) -> TxResult<UncommittedTransaction<'a, 's>> {
152        let account = state.load_account()?;
153        let mut exec = self.begin(address, account)?;
154        let info = exec.run_tick_tock_transaction(kind, inspector)?;
155
156        UncommittedTransaction::with_info(exec, state, None, info).map_err(TxError::Fatal)
157    }
158
159    pub fn begin(&self, address: &StdAddr, account: Option<Account>) -> Result<ExecutorState<'a>> {
160        let is_special = self
161            .override_special
162            .unwrap_or_else(|| self.config.is_special(address));
163
164        let acc_address;
165        let acc_storage_stat;
166        let acc_balance;
167        let acc_state;
168        let orig_status;
169        let end_status;
170        let start_lt;
171        match account {
172            Some(acc) => {
173                acc_address = 'addr: {
174                    if let IntAddr::Std(acc_addr) = acc.address
175                        && acc_addr == *address
176                    {
177                        break 'addr acc_addr;
178                    }
179                    anyhow::bail!("account address mismatch");
180                };
181                acc_storage_stat = acc.storage_stat;
182                acc_balance = acc.balance;
183                acc_state = acc.state;
184                orig_status = acc_state.status();
185                end_status = orig_status;
186                start_lt = std::cmp::max(self.min_lt, acc.last_trans_lt);
187            }
188            None => {
189                acc_address = address.clone();
190                acc_storage_stat = StorageInfo {
191                    used: StorageUsed::ZERO,
192                    storage_extra: Default::default(),
193                    last_paid: 0,
194                    due_payment: None,
195                };
196                acc_balance = CurrencyCollection::ZERO;
197                acc_state = AccountState::Uninit;
198                orig_status = AccountStatus::NotExists;
199                end_status = AccountStatus::Uninit;
200                start_lt = self.min_lt;
201            }
202        };
203
204        let mut is_marks_authority = false;
205        let mut is_suspended_by_marks = false;
206        if self.params.authority_marks_enabled
207            && let Some(marks) = &self.config.authority_marks
208        {
209            is_marks_authority = marks.is_authority(address);
210            is_suspended_by_marks =
211                !is_special && !is_marks_authority && marks.is_suspended(&acc_balance)?;
212        }
213
214        Ok(ExecutorState {
215            params: self.params,
216            config: self.config,
217            is_special,
218            is_marks_authority,
219            is_suspended_by_marks,
220            address: acc_address,
221            storage_stat: acc_storage_stat,
222            balance: acc_balance,
223            state: acc_state,
224            orig_status,
225            end_status,
226            start_lt,
227            end_lt: start_lt + 1,
228            out_msgs: Vec::new(),
229            total_fees: Tokens::ZERO,
230            burned: Tokens::ZERO,
231            cached_storage_stat: None,
232        })
233    }
234}
235
236/// Executor internals inspector.
237#[derive(Default)]
238pub struct ExecutorInspector<'e> {
239    /// Actions list from compute phase.
240    pub actions: Option<Cell>,
241    /// A set of changes of the public libraries dict.
242    pub public_libs_diff: ahash::HashMap<HashBytes, PublicLibraryChange>,
243    /// Compute phase exit code.
244    pub exit_code: Option<i32>,
245    /// Hash of the library in case it was missing during execution.
246    pub missing_library: Option<HashBytes>,
247    /// Total gas consumed (including the remaining "free" gas
248    /// and everything that exceeds the limit).
249    pub total_gas_used: u64,
250    /// Debug output target.
251    pub debug: Option<&'e mut dyn std::fmt::Write>,
252    /// Hook for a compute phase to modify C7 register data.
253    pub modify_smc_info: Option<&'e mut ModifySmcInfoFn>,
254}
255
256/// Public library diff operation.
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub enum PublicLibraryChange {
259    Add(Cell),
260    Remove,
261}
262
263/// Hook for a compute phase to modify C7 register data.
264pub type ModifySmcInfoFn = dyn FnMut(&mut phase::ComputePhaseSmcInfo) -> Result<()>;
265
266/// Shared state for executor phases.
267#[derive(Debug)]
268pub struct ExecutorState<'a> {
269    pub params: &'a ExecutorParams,
270    pub config: &'a ParsedConfig,
271
272    pub is_special: bool,
273    pub is_marks_authority: bool,
274    pub is_suspended_by_marks: bool,
275
276    pub address: StdAddr,
277    pub storage_stat: StorageInfo,
278    pub balance: CurrencyCollection,
279    pub state: AccountState,
280
281    pub orig_status: AccountStatus,
282    pub end_status: AccountStatus,
283    pub start_lt: u64,
284    pub end_lt: u64,
285
286    pub out_msgs: Vec<Lazy<OwnedMessage>>,
287    pub total_fees: Tokens,
288
289    pub burned: Tokens,
290
291    pub cached_storage_stat: Option<OwnedExtStorageStat>,
292}
293
294#[cfg(test)]
295impl<'a> ExecutorState<'a> {
296    pub(crate) fn new_non_existent(
297        params: &'a ExecutorParams,
298        config: &'a impl AsRef<ParsedConfig>,
299        address: &StdAddr,
300    ) -> Self {
301        Self {
302            params,
303            config: config.as_ref(),
304            is_special: false,
305            is_marks_authority: false,
306            is_suspended_by_marks: false,
307            address: address.clone(),
308            storage_stat: Default::default(),
309            balance: CurrencyCollection::ZERO,
310            state: AccountState::Uninit,
311            orig_status: AccountStatus::NotExists,
312            end_status: AccountStatus::Uninit,
313            start_lt: 0,
314            end_lt: 1,
315            out_msgs: Vec::new(),
316            total_fees: Tokens::ZERO,
317            burned: Tokens::ZERO,
318            cached_storage_stat: None,
319        }
320    }
321
322    pub(crate) fn new_uninit(
323        params: &'a ExecutorParams,
324        config: &'a impl AsRef<ParsedConfig>,
325        address: &StdAddr,
326        balance: impl Into<CurrencyCollection>,
327    ) -> Self {
328        let mut res = Self::new_non_existent(params, config, address);
329        res.balance = balance.into();
330        res.orig_status = AccountStatus::Uninit;
331
332        if params.authority_marks_enabled
333            && let Some(marks) = &config.as_ref().authority_marks
334        {
335            res.is_marks_authority = marks.is_authority(address);
336            res.is_suspended_by_marks = !res.is_special
337                && !res.is_marks_authority
338                && marks.is_suspended(&res.balance).unwrap();
339        }
340
341        res
342    }
343
344    pub(crate) fn new_frozen(
345        params: &'a ExecutorParams,
346        config: &'a impl AsRef<ParsedConfig>,
347        address: &StdAddr,
348        balance: impl Into<CurrencyCollection>,
349        state_hash: HashBytes,
350    ) -> Self {
351        let mut res = Self::new_uninit(params, config, address, balance);
352        res.state = AccountState::Frozen(state_hash);
353        res.orig_status = AccountStatus::Frozen;
354        res.end_status = AccountStatus::Frozen;
355        res
356    }
357
358    pub(crate) fn new_active(
359        params: &'a ExecutorParams,
360        config: &'a impl AsRef<ParsedConfig>,
361        address: &StdAddr,
362        balance: impl Into<CurrencyCollection>,
363        data: Cell,
364        code_boc: impl AsRef<[u8]>,
365    ) -> Self {
366        use tycho_types::models::StateInit;
367
368        let mut res = Self::new_uninit(params, config, address, balance);
369        res.state = AccountState::Active(StateInit {
370            split_depth: None,
371            special: None,
372            code: Some(Boc::decode(code_boc).unwrap()),
373            data: Some(data),
374            libraries: Dict::new(),
375        });
376        res.orig_status = AccountStatus::Active;
377        res.end_status = AccountStatus::Active;
378        res
379    }
380}
381
382/// Executor configuration parameters.
383#[derive(Debug, Default, Clone)]
384pub struct ExecutorParams {
385    /// Public libraries from the referenced masterchain state.
386    pub libraries: Dict<HashBytes, LibDescr>,
387    /// Rand seed of the block.
388    pub rand_seed: HashBytes,
389    /// Unix timestamp in seconds of the block.
390    pub block_unixtime: u32,
391    /// Logical time of the block.
392    pub block_lt: u64,
393    /// Last known previous masterchain block before current.
394    pub prev_mc_block_id: Option<BlockId>,
395    /// VM behaviour modifiers.
396    pub vm_modifiers: tycho_vm::BehaviourModifiers,
397    /// Prevent [`Frozen`] accounts from being deleted
398    /// when their storage due is too high.
399    ///
400    /// [`Frozen`]: tycho_types::models::AccountState::Frozen
401    pub disable_delete_frozen_accounts: bool,
402    /// Charge account balance for additional `total_action_fees`
403    /// when action phase fails.
404    pub charge_action_fees_on_fail: bool,
405    /// Attaches an original message body as an additional cell
406    /// to a bounced message body.
407    pub full_body_in_bounced: bool,
408    /// More gas-predictable extra currency behaviour.
409    pub strict_extra_currency: bool,
410    /// Whether accounts can be suspended by authority marks.
411    pub authority_marks_enabled: bool,
412}
413
414/// Executed transaction.
415pub struct UncommittedTransaction<'a, 's> {
416    original: &'s ShardAccount,
417    exec: ExecutorState<'a>,
418    in_msg: Option<Cell>,
419    info: Lazy<TxInfo>,
420}
421
422impl<'a, 's> UncommittedTransaction<'a, 's> {
423    #[inline]
424    pub fn with_info(
425        exec: ExecutorState<'a>,
426        original: &'s ShardAccount,
427        in_msg: Option<Cell>,
428        info: impl Into<TxInfo>,
429    ) -> Result<Self> {
430        let info = info.into();
431        let info = Lazy::new(&info)?;
432        Ok(Self {
433            original,
434            exec,
435            in_msg,
436            info,
437        })
438    }
439
440    /// Creates a partially finalized transaction.
441    ///
442    /// It differs from the normal transaction by having a stub `state_update`
443    /// and possibly denormalized `end_status`.
444    pub fn build_uncommitted(&self) -> Result<Transaction, Error> {
445        thread_local! {
446            static EMPTY_STATE_UPDATE: Lazy<HashUpdate> = Lazy::new(&HashUpdate {
447                old: HashBytes::ZERO,
448                new: HashBytes::ZERO,
449            })
450            .unwrap();
451        }
452
453        self.build_transaction(self.exec.end_status, EMPTY_STATE_UPDATE.with(Clone::clone))
454    }
455
456    /// Creates a final transaction and a new contract state.
457    pub fn commit(mut self) -> Result<ExecutorOutput> {
458        // Collect brief account state info and build new account state.
459        let account_state;
460        let new_state_meta;
461        let end_status = match self.build_account_state()? {
462            None => {
463                // TODO: Replace with a constant?
464                account_state = CellBuilder::build_from(false)?;
465
466                // Brief meta.
467                new_state_meta = AccountMeta {
468                    balance: CurrencyCollection::ZERO,
469                    libraries: Dict::new(),
470                    exists: false,
471                };
472
473                // Done
474                AccountStatus::NotExists
475            }
476            Some(state) => {
477                // Load previous account storage info.
478                let prev_account_storage = 'prev: {
479                    let mut cs = self.original.account.as_slice_allow_exotic();
480                    if !cs.load_bit()? {
481                        // account_none$0
482                        break 'prev None;
483                    }
484                    // account$1
485                    // addr:MsgAddressInt
486                    IntAddr::load_from(&mut cs)?;
487                    // storage_stat:StorageInfo
488                    let storage_info = StorageInfo::load_from(&mut cs)?;
489                    // storage:AccountStorage
490                    Some((storage_info.used, cs))
491                };
492
493                // Serialize part of the new account state to compute new storage stats.
494                let mut account_storage = CellBuilder::new();
495                // last_trans_lt:uint64
496                account_storage.store_u64(self.exec.end_lt)?;
497                // balance:CurrencyCollection
498                self.exec
499                    .balance
500                    .store_into(&mut account_storage, Cell::empty_context())?;
501                // state:AccountState
502                state.store_into(&mut account_storage, Cell::empty_context())?;
503
504                // Update storage info.
505                self.exec.storage_stat.used = compute_storage_used(
506                    prev_account_storage,
507                    account_storage.as_full_slice(),
508                    &mut self.exec.cached_storage_stat,
509                    self.exec.params.strict_extra_currency,
510                )?;
511
512                // Build new account state.
513                account_state = CellBuilder::build_from((
514                    true,                            // account$1
515                    &self.exec.address,              // addr:MsgAddressInt
516                    &self.exec.storage_stat,         // storage_stat:StorageInfo
517                    account_storage.as_full_slice(), // storage:AccountStorage
518                ))?;
519
520                // Brief meta.
521                let libraries = match &state {
522                    AccountState::Active(state) => state.libraries.clone(),
523                    AccountState::Frozen(..) | AccountState::Uninit => Dict::new(),
524                };
525                new_state_meta = AccountMeta {
526                    balance: self.exec.balance.clone(),
527                    libraries,
528                    exists: true,
529                };
530
531                // Done
532                state.status()
533            }
534        };
535
536        // Serialize transaction.
537        let state_update = Lazy::new(&HashUpdate {
538            old: *self.original.account.repr_hash(),
539            new: *account_state.repr_hash(),
540        })?;
541        let transaction = self
542            .build_transaction(end_status, state_update)
543            .and_then(|tx| Lazy::new(&tx))?;
544
545        // Collect brief transaction info.
546        let transaction_meta = TransactionMeta {
547            total_fees: self.exec.total_fees,
548            next_lt: self.exec.end_lt,
549            out_msgs: self.exec.out_msgs,
550        };
551
552        // New shard account state.
553        let new_state = ShardAccount {
554            // SAFETY: `account_state` is an ordinary cell.
555            account: unsafe { Lazy::from_raw_unchecked(account_state) },
556            last_trans_hash: *transaction.repr_hash(),
557            last_trans_lt: self.exec.start_lt,
558        };
559
560        // Done
561        Ok(ExecutorOutput {
562            new_state,
563            new_state_meta,
564            transaction,
565            transaction_meta,
566            burned: self.exec.burned,
567        })
568    }
569
570    fn build_account_state(&self) -> Result<Option<AccountState>> {
571        Ok(match self.exec.end_status {
572            // Account was deleted.
573            AccountStatus::NotExists => None,
574            // Uninit account with zero balance is also treated as deleted.
575            AccountStatus::Uninit if self.exec.balance.is_zero() => None,
576            // Uninit account stays the same.
577            AccountStatus::Uninit => Some(AccountState::Uninit),
578            // Active account must have a known active state.
579            AccountStatus::Active => {
580                debug_assert!(matches!(self.exec.state, AccountState::Active(_)));
581                Some(self.exec.state.clone())
582            }
583            // Normalize frozen state.
584            AccountStatus::Frozen => {
585                let cell;
586                let frozen_hash = match &self.exec.state {
587                    // Uninit accounts can't be frozen, but if they accidentialy can
588                    // just use the account address as frozen state hash to produce the
589                    // same uninit state.
590                    AccountState::Uninit => &self.exec.address.address,
591                    // To freeze an active account we must compute a hash of its state.
592                    AccountState::Active(state_init) => {
593                        cell = CellBuilder::build_from(state_init)?;
594                        cell.repr_hash()
595                    }
596                    // Account is already frozen.
597                    AccountState::Frozen(hash_bytes) => hash_bytes,
598                };
599
600                // Normalize account state.
601                Some(if frozen_hash == &self.exec.address.address {
602                    AccountState::Uninit
603                } else {
604                    AccountState::Frozen(*frozen_hash)
605                })
606            }
607        })
608    }
609
610    fn build_transaction(
611        &self,
612        end_status: AccountStatus,
613        state_update: Lazy<HashUpdate>,
614    ) -> Result<Transaction, Error> {
615        Ok(Transaction {
616            account: self.exec.address.address,
617            lt: self.exec.start_lt,
618            prev_trans_hash: self.original.last_trans_hash,
619            prev_trans_lt: self.original.last_trans_lt,
620            now: self.exec.params.block_unixtime,
621            out_msg_count: Uint15::new(self.exec.out_msgs.len() as _),
622            orig_status: self.exec.orig_status,
623            end_status,
624            in_msg: self.in_msg.clone(),
625            out_msgs: build_out_msgs(&self.exec.out_msgs)?,
626            total_fees: self.exec.total_fees.into(),
627            state_update,
628            info: self.info.clone(),
629        })
630    }
631}
632
633fn compute_storage_used(
634    mut prev: Option<(StorageUsed, CellSlice<'_>)>,
635    mut new_storage: CellSlice<'_>,
636    cache: &mut Option<OwnedExtStorageStat>,
637    without_extra_currencies: bool,
638) -> Result<StorageUsed> {
639    fn skip_extra(slice: &mut CellSlice<'_>) -> Result<bool, Error> {
640        let mut cs = *slice;
641        cs.skip_first(64, 0)?; // skip lt
642        let balance = CurrencyCollection::load_from(&mut cs)?;
643        Ok(if balance.other.is_empty() {
644            false
645        } else {
646            slice.skip_first(0, 1)?;
647            true
648        })
649    }
650
651    if without_extra_currencies {
652        if let Some((_, prev)) = &mut prev {
653            skip_extra(prev)?;
654        }
655        skip_extra(&mut new_storage)?;
656    }
657
658    // Try to reuse previous storage stats if no cells were changed.
659    if let Some((prev_used, prev_storage)) = prev {
660        'reuse: {
661            // Always recompute when previous stats are invalid.
662            if prev_used.cells.is_zero()
663                || prev_used.bits.into_inner() < prev_storage.size_bits() as u64
664            {
665                break 'reuse;
666            }
667
668            // Simple check that some cells were changed.
669            if prev_storage.size_refs() != new_storage.size_refs() {
670                break 'reuse;
671            }
672
673            // Compare each cell.
674            for (prev, new) in prev_storage.references().zip(new_storage.references()) {
675                if prev != new {
676                    break 'reuse;
677                }
678            }
679
680            // Adjust bit size of the root slice.
681            return Ok(StorageUsed {
682                // Compute the truncated difference of the previous storage root and a new one.
683                bits: new_varuint56_truncate(
684                    (prev_used.bits.into_inner() - prev_storage.size_bits() as u64)
685                        .saturating_add(new_storage.size_bits() as u64),
686                ),
687                // All other stats are unchanged.
688                cells: prev_used.cells,
689            });
690        }
691    }
692
693    // Init cache.
694    let cache = cache.get_or_insert_with(OwnedExtStorageStat::unlimited);
695    cache.set_unlimited();
696
697    // Compute stats for childern.
698    for cell in new_storage.references().cloned() {
699        cache.add_cell(cell);
700    }
701    let stats = cache.stats();
702
703    // Done.
704    Ok(StorageUsed {
705        cells: new_varuint56_truncate(stats.cell_count.saturating_add(1)),
706        bits: new_varuint56_truncate(stats.bit_count.saturating_add(new_storage.size_bits() as _)),
707    })
708}
709
710/// Committed transaction output.
711#[derive(Clone, Debug)]
712pub struct ExecutorOutput {
713    pub new_state: ShardAccount,
714    pub new_state_meta: AccountMeta,
715    pub transaction: Lazy<Transaction>,
716    pub transaction_meta: TransactionMeta,
717    pub burned: Tokens,
718}
719
720/// Short account description.
721#[derive(Clone, Debug)]
722pub struct AccountMeta {
723    pub balance: CurrencyCollection,
724    pub libraries: Dict<HashBytes, SimpleLib>,
725    pub exists: bool,
726}
727
728/// Short transaction description.
729#[derive(Clone, Debug)]
730pub struct TransactionMeta {
731    /// A sum of all collected fees from all phases.
732    pub total_fees: Tokens,
733    /// List of outbound messages.
734    pub out_msgs: Vec<Lazy<OwnedMessage>>,
735    /// Minimal logical time for the next transaction on this account.
736    pub next_lt: u64,
737}
738
739/// Message cell source.
740pub trait LoadMessage {
741    fn load_message_root(self) -> Result<Cell>;
742}
743
744impl<T: LoadMessage + Clone> LoadMessage for &T {
745    #[inline]
746    fn load_message_root(self) -> Result<Cell> {
747        T::load_message_root(T::clone(self))
748    }
749}
750
751impl LoadMessage for Cell {
752    #[inline]
753    fn load_message_root(self) -> Result<Cell> {
754        Ok(self)
755    }
756}
757
758impl<T: EquivalentRepr<OwnedMessage>> LoadMessage for Lazy<T> {
759    #[inline]
760    fn load_message_root(self) -> Result<Cell> {
761        Ok(self.into_inner())
762    }
763}
764
765impl LoadMessage for OwnedMessage {
766    #[inline]
767    fn load_message_root(self) -> Result<Cell> {
768        CellBuilder::build_from(self).context("failed to serialize inbound message")
769    }
770}
771
772impl LoadMessage for Message<'_> {
773    #[inline]
774    fn load_message_root(self) -> Result<Cell> {
775        CellBuilder::build_from(self).context("failed to serialize inbound message")
776    }
777}
778
779fn build_out_msgs(out_msgs: &[Lazy<OwnedMessage>]) -> Result<Dict<Uint15, Cell>, Error> {
780    dict::build_dict_from_sorted_iter(
781        out_msgs
782            .iter()
783            .enumerate()
784            .map(|(i, msg)| (Uint15::new(i as _), msg.inner().clone())),
785        Cell::empty_context(),
786    )
787    .map(Dict::from_raw)
788}
789
790#[cfg(test)]
791mod tests {
792    use std::rc::Rc;
793
794    use tycho_types::boc::BocRepr;
795    use tycho_types::models::{BlockchainConfig, MsgInfo, StateInit};
796
797    use super::*;
798
799    pub fn make_default_config() -> Rc<ParsedConfig> {
800        thread_local! {
801            pub static PARSED_CONFIG: Rc<ParsedConfig> = make_custom_config(|_| Ok(()));
802        }
803
804        PARSED_CONFIG.with(Clone::clone)
805    }
806
807    pub fn make_custom_config<F>(f: F) -> Rc<ParsedConfig>
808    where
809        F: FnOnce(&mut BlockchainConfig) -> anyhow::Result<()>,
810    {
811        let mut config: BlockchainConfig =
812            BocRepr::decode(include_bytes!("../res/config.boc")).unwrap();
813
814        config.params.set_global_id(100).unwrap();
815
816        // TODO: Update config BOC
817        config
818            .params
819            .set_size_limits(&ParsedConfig::DEFAULT_SIZE_LIMITS_CONFIG)
820            .unwrap();
821
822        f(&mut config).unwrap();
823
824        Rc::new(ParsedConfig::parse(config, u32::MAX).unwrap())
825    }
826
827    pub fn make_default_params() -> ExecutorParams {
828        ExecutorParams {
829            block_unixtime: 1738799198,
830            full_body_in_bounced: false,
831            strict_extra_currency: true,
832            vm_modifiers: tycho_vm::BehaviourModifiers {
833                chksig_always_succeed: true,
834                ..Default::default()
835            },
836            ..Default::default()
837        }
838    }
839
840    pub fn make_message(
841        info: impl Into<MsgInfo>,
842        init: Option<StateInit>,
843        body: Option<CellBuilder>,
844    ) -> Cell {
845        let body = match &body {
846            None => Cell::empty_cell_ref().as_slice_allow_exotic(),
847            Some(cell) => cell.as_full_slice(),
848        };
849        CellBuilder::build_from(Message {
850            info: info.into(),
851            init,
852            body,
853            layout: None,
854        })
855        .unwrap()
856    }
857
858    pub fn make_big_tree(depth: u8, count: &mut u16, target: u16) -> Cell {
859        *count += 1;
860
861        if depth == 0 {
862            CellBuilder::build_from(*count).unwrap()
863        } else {
864            let mut b = CellBuilder::new();
865            for _ in 0..4 {
866                if *count < target {
867                    b.store_reference(make_big_tree(depth - 1, count, target))
868                        .unwrap();
869                }
870            }
871            b.build().unwrap()
872        }
873    }
874
875    /// Can be used to replay TON transactions on the Tycho executor.
876    #[allow(unused)]
877    fn replay_tx(config: &str, state: &str, msg: &str, lt: u64, now: u32) -> Result<()> {
878        use tycho_types::models::*;
879
880        let mut config: BlockchainConfig = BocRepr::decode_base64(config)?;
881        let state: ShardAccount = BocRepr::decode_base64(state)?;
882        let msg = Boc::decode_base64(msg)?;
883
884        let mut workchains = Dict::<i32, _>::new();
885        workchains.set(0, WorkchainDescription {
886            enabled_since: 0,
887            actual_min_split: 0,
888            min_split: 0,
889            max_split: 0,
890            active: true,
891            accept_msgs: true,
892            zerostate_root_hash: HashBytes::ZERO,
893            zerostate_file_hash: HashBytes::ZERO,
894            version: 0,
895            format: WorkchainFormat::Basic(WorkchainFormatBasic {
896                vm_mode: 0,
897                vm_version: 0,
898            }),
899        })?;
900        config.params.set_workchains(&workchains)?;
901        config.params.remove(43)?;
902
903        let (is_external, address) = match msg.parse::<MsgInfo>()? {
904            MsgInfo::Int(info) => (false, info.dst),
905            MsgInfo::ExtIn(info) => (true, info.dst),
906            MsgInfo::ExtOut(_) => anyhow::bail!("unexpected message"),
907        };
908        let IntAddr::Std(address) = address else {
909            anyhow::bail!("unexpected address");
910        };
911
912        let params = ExecutorParams {
913            block_unixtime: now,
914            ..Default::default()
915        };
916
917        let parsed_config = ParsedConfig::parse(config, now).context("failed to parse config")?;
918        let output = Executor::new(&params, &parsed_config)
919            .with_min_lt(lt)
920            .begin_ordinary(&address, is_external, msg, &state)?
921            .commit()?;
922
923        let tx = output.transaction.load()?;
924        let tx_info = tx.load_info()?;
925        println!("{tx_info:#?}");
926        Ok(())
927    }
928}