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