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
42pub 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 if acc_addr == *address {
149 break 'addr acc_addr;
150 }
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 Ok(ExecutorState {
178 params: self.params,
179 config: self.config,
180 is_special,
181 address: acc_address,
182 storage_stat: acc_storage_stat,
183 balance: acc_balance,
184 state: acc_state,
185 orig_status,
186 end_status,
187 start_lt,
188 end_lt: start_lt + 1,
189 out_msgs: Vec::new(),
190 total_fees: Tokens::ZERO,
191 burned: Tokens::ZERO,
192 cached_storage_stat: None,
193 })
194 }
195}
196
197#[derive(Default)]
199pub struct ExecutorInspector<'e> {
200 pub actions: Option<Cell>,
202 pub public_libs_diff: Vec<PublicLibraryChange>,
207 pub exit_code: Option<i32>,
209 pub total_gas_used: u64,
212 pub debug: Option<&'e mut dyn std::fmt::Write>,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum PublicLibraryChange {
219 Add(Cell),
220 Remove(HashBytes),
221}
222
223impl PublicLibraryChange {
224 pub fn lib_hash(&self) -> &HashBytes {
226 match self {
227 Self::Add(cell) => cell.repr_hash(),
228 Self::Remove(hash) => hash,
229 }
230 }
231}
232
233pub struct ExecutorState<'a> {
235 pub params: &'a ExecutorParams,
236 pub config: &'a ParsedConfig,
237
238 pub is_special: bool,
239
240 pub address: StdAddr,
241 pub storage_stat: StorageInfo,
242 pub balance: CurrencyCollection,
243 pub state: AccountState,
244
245 pub orig_status: AccountStatus,
246 pub end_status: AccountStatus,
247 pub start_lt: u64,
248 pub end_lt: u64,
249
250 pub out_msgs: Vec<Lazy<OwnedMessage>>,
251 pub total_fees: Tokens,
252
253 pub burned: Tokens,
254
255 pub cached_storage_stat: Option<OwnedExtStorageStat>,
256}
257
258#[cfg(test)]
259impl<'a> ExecutorState<'a> {
260 pub(crate) fn new_non_existent(
261 params: &'a ExecutorParams,
262 config: &'a impl AsRef<ParsedConfig>,
263 address: &StdAddr,
264 ) -> Self {
265 Self {
266 params,
267 config: config.as_ref(),
268 is_special: false,
269 address: address.clone(),
270 storage_stat: Default::default(),
271 balance: CurrencyCollection::ZERO,
272 state: AccountState::Uninit,
273 orig_status: AccountStatus::NotExists,
274 end_status: AccountStatus::Uninit,
275 start_lt: 0,
276 end_lt: 1,
277 out_msgs: Vec::new(),
278 total_fees: Tokens::ZERO,
279 burned: Tokens::ZERO,
280 cached_storage_stat: None,
281 }
282 }
283
284 pub(crate) fn new_uninit(
285 params: &'a ExecutorParams,
286 config: &'a impl AsRef<ParsedConfig>,
287 address: &StdAddr,
288 balance: impl Into<CurrencyCollection>,
289 ) -> Self {
290 let mut res = Self::new_non_existent(params, config, address);
291 res.balance = balance.into();
292 res.orig_status = AccountStatus::Uninit;
293 res
294 }
295
296 pub(crate) fn new_frozen(
297 params: &'a ExecutorParams,
298 config: &'a impl AsRef<ParsedConfig>,
299 address: &StdAddr,
300 balance: impl Into<CurrencyCollection>,
301 state_hash: HashBytes,
302 ) -> Self {
303 let mut res = Self::new_non_existent(params, config, address);
304 res.balance = balance.into();
305 res.state = AccountState::Frozen(state_hash);
306 res.orig_status = AccountStatus::Frozen;
307 res.end_status = AccountStatus::Frozen;
308 res
309 }
310
311 pub(crate) fn new_active(
312 params: &'a ExecutorParams,
313 config: &'a impl AsRef<ParsedConfig>,
314 address: &StdAddr,
315 balance: impl Into<CurrencyCollection>,
316 data: Cell,
317 code_boc: impl AsRef<[u8]>,
318 ) -> Self {
319 use tycho_types::models::StateInit;
320
321 let mut res = Self::new_non_existent(params, config, address);
322 res.balance = balance.into();
323 res.state = AccountState::Active(StateInit {
324 split_depth: None,
325 special: None,
326 code: Some(Boc::decode(code_boc).unwrap()),
327 data: Some(data),
328 libraries: Dict::new(),
329 });
330 res.orig_status = AccountStatus::Active;
331 res.end_status = AccountStatus::Active;
332 res
333 }
334}
335
336#[derive(Default, Clone)]
338pub struct ExecutorParams {
339 pub libraries: Dict<HashBytes, LibDescr>,
341 pub rand_seed: HashBytes,
343 pub block_unixtime: u32,
345 pub block_lt: u64,
347 pub vm_modifiers: tycho_vm::BehaviourModifiers,
349 pub disable_delete_frozen_accounts: bool,
354 pub charge_action_fees_on_fail: bool,
357 pub full_body_in_bounced: bool,
360 pub strict_extra_currency: bool,
362}
363
364pub struct UncommittedTransaction<'a, 's> {
366 original: &'s ShardAccount,
367 exec: ExecutorState<'a>,
368 in_msg: Option<Cell>,
369 info: Lazy<TxInfo>,
370}
371
372impl<'a, 's> UncommittedTransaction<'a, 's> {
373 #[inline]
374 pub fn with_info(
375 exec: ExecutorState<'a>,
376 original: &'s ShardAccount,
377 in_msg: Option<Cell>,
378 info: impl Into<TxInfo>,
379 ) -> Result<Self> {
380 let info = info.into();
381 let info = Lazy::new(&info)?;
382 Ok(Self {
383 original,
384 exec,
385 in_msg,
386 info,
387 })
388 }
389
390 pub fn build_uncommitted(&self) -> Result<Transaction, Error> {
395 thread_local! {
396 static EMPTY_STATE_UPDATE: Lazy<HashUpdate> = Lazy::new(&HashUpdate {
397 old: HashBytes::ZERO,
398 new: HashBytes::ZERO,
399 })
400 .unwrap();
401 }
402
403 self.build_transaction(self.exec.end_status, EMPTY_STATE_UPDATE.with(Clone::clone))
404 }
405
406 pub fn commit(mut self) -> Result<ExecutorOutput> {
408 let account_state;
410 let new_state_meta;
411 let end_status = match self.build_account_state()? {
412 None => {
413 account_state = CellBuilder::build_from(false)?;
415
416 new_state_meta = AccountMeta {
418 balance: CurrencyCollection::ZERO,
419 libraries: Dict::new(),
420 exists: false,
421 };
422
423 AccountStatus::NotExists
425 }
426 Some(state) => {
427 let prev_account_storage = 'prev: {
429 let mut cs = self.original.account.as_slice_allow_exotic();
430 if !cs.load_bit()? {
431 break 'prev None;
433 }
434 IntAddr::load_from(&mut cs)?;
437 let storage_info = StorageInfo::load_from(&mut cs)?;
439 Some((storage_info.used, cs))
441 };
442
443 let mut account_storage = CellBuilder::new();
445 account_storage.store_u64(self.exec.end_lt)?;
447 self.exec
449 .balance
450 .store_into(&mut account_storage, Cell::empty_context())?;
451 state.store_into(&mut account_storage, Cell::empty_context())?;
453
454 self.exec.storage_stat.used = compute_storage_used(
456 prev_account_storage,
457 account_storage.as_full_slice(),
458 &mut self.exec.cached_storage_stat,
459 self.exec.params.strict_extra_currency,
460 )?;
461
462 account_state = CellBuilder::build_from((
464 true, &self.exec.address, &self.exec.storage_stat, account_storage.as_full_slice(), ))?;
469
470 let libraries = match &state {
472 AccountState::Active(state) => state.libraries.clone(),
473 AccountState::Frozen(..) | AccountState::Uninit => Dict::new(),
474 };
475 new_state_meta = AccountMeta {
476 balance: self.exec.balance.clone(),
477 libraries,
478 exists: true,
479 };
480
481 state.status()
483 }
484 };
485
486 let state_update = Lazy::new(&HashUpdate {
488 old: *self.original.account.repr_hash(),
489 new: *account_state.repr_hash(),
490 })?;
491 let transaction = self
492 .build_transaction(end_status, state_update)
493 .and_then(|tx| Lazy::new(&tx))?;
494
495 let transaction_meta = TransactionMeta {
497 total_fees: self.exec.total_fees,
498 next_lt: self.exec.end_lt,
499 out_msgs: self.exec.out_msgs,
500 };
501
502 let new_state = ShardAccount {
504 account: unsafe { Lazy::from_raw_unchecked(account_state) },
506 last_trans_hash: *transaction.repr_hash(),
507 last_trans_lt: self.exec.start_lt,
508 };
509
510 Ok(ExecutorOutput {
512 new_state,
513 new_state_meta,
514 transaction,
515 transaction_meta,
516 burned: self.exec.burned,
517 })
518 }
519
520 fn build_account_state(&self) -> Result<Option<AccountState>> {
521 Ok(match self.exec.end_status {
522 AccountStatus::NotExists => None,
524 AccountStatus::Uninit if self.exec.balance.is_zero() => None,
526 AccountStatus::Uninit => Some(AccountState::Uninit),
528 AccountStatus::Active => {
530 debug_assert!(matches!(self.exec.state, AccountState::Active(_)));
531 Some(self.exec.state.clone())
532 }
533 AccountStatus::Frozen => {
535 let cell;
536 let frozen_hash = match &self.exec.state {
537 AccountState::Uninit => &self.exec.address.address,
541 AccountState::Active(state_init) => {
543 cell = CellBuilder::build_from(state_init)?;
544 cell.repr_hash()
545 }
546 AccountState::Frozen(hash_bytes) => hash_bytes,
548 };
549
550 Some(if frozen_hash == &self.exec.address.address {
552 AccountState::Uninit
553 } else {
554 AccountState::Frozen(*frozen_hash)
555 })
556 }
557 })
558 }
559
560 fn build_transaction(
561 &self,
562 end_status: AccountStatus,
563 state_update: Lazy<HashUpdate>,
564 ) -> Result<Transaction, Error> {
565 Ok(Transaction {
566 account: self.exec.address.address,
567 lt: self.exec.start_lt,
568 prev_trans_hash: self.original.last_trans_hash,
569 prev_trans_lt: self.original.last_trans_lt,
570 now: self.exec.params.block_unixtime,
571 out_msg_count: Uint15::new(self.exec.out_msgs.len() as _),
572 orig_status: self.exec.orig_status,
573 end_status,
574 in_msg: self.in_msg.clone(),
575 out_msgs: build_out_msgs(&self.exec.out_msgs)?,
576 total_fees: self.exec.total_fees.into(),
577 state_update,
578 info: self.info.clone(),
579 })
580 }
581}
582
583fn compute_storage_used(
584 mut prev: Option<(StorageUsed, CellSlice<'_>)>,
585 mut new_storage: CellSlice<'_>,
586 cache: &mut Option<OwnedExtStorageStat>,
587 without_extra_currencies: bool,
588) -> Result<StorageUsed> {
589 fn skip_extra(slice: &mut CellSlice<'_>) -> Result<bool, Error> {
590 let mut cs = *slice;
591 cs.skip_first(64, 0)?; let balance = CurrencyCollection::load_from(&mut cs)?;
593 Ok(if balance.other.is_empty() {
594 false
595 } else {
596 slice.skip_first(0, 1)?;
597 true
598 })
599 }
600
601 if without_extra_currencies {
602 if let Some((_, prev)) = &mut prev {
603 skip_extra(prev)?;
604 }
605 skip_extra(&mut new_storage)?;
606 }
607
608 if let Some((prev_used, prev_storage)) = prev {
610 'reuse: {
611 if prev_used.cells.is_zero()
613 || prev_used.bits.into_inner() < prev_storage.size_bits() as u64
614 {
615 break 'reuse;
616 }
617
618 if prev_storage.size_refs() != new_storage.size_refs() {
620 break 'reuse;
621 }
622
623 for (prev, new) in prev_storage.references().zip(new_storage.references()) {
625 if prev != new {
626 break 'reuse;
627 }
628 }
629
630 return Ok(StorageUsed {
632 bits: new_varuint56_truncate(
634 (prev_used.bits.into_inner() - prev_storage.size_bits() as u64)
635 .saturating_add(new_storage.size_bits() as u64),
636 ),
637 cells: prev_used.cells,
639 });
640 }
641 }
642
643 let cache = cache.get_or_insert_with(OwnedExtStorageStat::unlimited);
645 cache.set_unlimited();
646
647 for cell in new_storage.references().cloned() {
649 cache.add_cell(cell);
650 }
651 let stats = cache.stats();
652
653 Ok(StorageUsed {
655 cells: new_varuint56_truncate(stats.cell_count.saturating_add(1)),
656 bits: new_varuint56_truncate(stats.bit_count.saturating_add(new_storage.size_bits() as _)),
657 })
658}
659
660#[derive(Clone, Debug)]
662pub struct ExecutorOutput {
663 pub new_state: ShardAccount,
664 pub new_state_meta: AccountMeta,
665 pub transaction: Lazy<Transaction>,
666 pub transaction_meta: TransactionMeta,
667 pub burned: Tokens,
668}
669
670#[derive(Clone, Debug)]
672pub struct AccountMeta {
673 pub balance: CurrencyCollection,
674 pub libraries: Dict<HashBytes, SimpleLib>,
675 pub exists: bool,
676}
677
678#[derive(Clone, Debug)]
680pub struct TransactionMeta {
681 pub total_fees: Tokens,
683 pub out_msgs: Vec<Lazy<OwnedMessage>>,
685 pub next_lt: u64,
687}
688
689pub trait LoadMessage {
691 fn load_message_root(self) -> Result<Cell>;
692}
693
694impl<T: LoadMessage + Clone> LoadMessage for &T {
695 #[inline]
696 fn load_message_root(self) -> Result<Cell> {
697 T::load_message_root(T::clone(self))
698 }
699}
700
701impl LoadMessage for Cell {
702 #[inline]
703 fn load_message_root(self) -> Result<Cell> {
704 Ok(self)
705 }
706}
707
708impl<T: EquivalentRepr<OwnedMessage>> LoadMessage for Lazy<T> {
709 #[inline]
710 fn load_message_root(self) -> Result<Cell> {
711 Ok(self.into_inner())
712 }
713}
714
715impl LoadMessage for OwnedMessage {
716 #[inline]
717 fn load_message_root(self) -> Result<Cell> {
718 CellBuilder::build_from(self).context("failed to serialize inbound message")
719 }
720}
721
722impl LoadMessage for Message<'_> {
723 #[inline]
724 fn load_message_root(self) -> Result<Cell> {
725 CellBuilder::build_from(self).context("failed to serialize inbound message")
726 }
727}
728
729fn build_out_msgs(out_msgs: &[Lazy<OwnedMessage>]) -> Result<Dict<Uint15, Cell>, Error> {
730 dict::build_dict_from_sorted_iter(
731 out_msgs
732 .iter()
733 .enumerate()
734 .map(|(i, msg)| (Uint15::new(i as _), msg.inner().clone())),
735 Cell::empty_context(),
736 )
737 .map(Dict::from_raw)
738}
739
740#[cfg(test)]
741mod tests {
742 use std::rc::Rc;
743
744 use tycho_types::boc::BocRepr;
745 use tycho_types::models::{BlockchainConfig, MsgInfo, StateInit};
746
747 use super::*;
748
749 pub fn make_default_config() -> Rc<ParsedConfig> {
750 thread_local! {
751 pub static PARSED_CONFIG: Rc<ParsedConfig> = make_custom_config(|_| Ok(()));
752 }
753
754 PARSED_CONFIG.with(Clone::clone)
755 }
756
757 pub fn make_custom_config<F>(f: F) -> Rc<ParsedConfig>
758 where
759 F: FnOnce(&mut BlockchainConfig) -> anyhow::Result<()>,
760 {
761 let mut config: BlockchainConfig =
762 BocRepr::decode(include_bytes!("../res/config.boc")).unwrap();
763
764 config.params.set_global_id(100).unwrap();
765
766 config
768 .params
769 .set_size_limits(&ParsedConfig::DEFAULT_SIZE_LIMITS_CONFIG)
770 .unwrap();
771
772 f(&mut config).unwrap();
773
774 Rc::new(ParsedConfig::parse(config, u32::MAX).unwrap())
775 }
776
777 pub fn make_default_params() -> ExecutorParams {
778 ExecutorParams {
779 block_unixtime: 1738799198,
780 full_body_in_bounced: false,
781 strict_extra_currency: true,
782 vm_modifiers: tycho_vm::BehaviourModifiers {
783 chksig_always_succeed: true,
784 ..Default::default()
785 },
786 ..Default::default()
787 }
788 }
789
790 pub fn make_message(
791 info: impl Into<MsgInfo>,
792 init: Option<StateInit>,
793 body: Option<CellBuilder>,
794 ) -> Cell {
795 let body = match &body {
796 None => Cell::empty_cell_ref().as_slice_allow_exotic(),
797 Some(cell) => cell.as_full_slice(),
798 };
799 CellBuilder::build_from(Message {
800 info: info.into(),
801 init,
802 body,
803 layout: None,
804 })
805 .unwrap()
806 }
807
808 pub fn make_big_tree(depth: u8, count: &mut u16, target: u16) -> Cell {
809 *count += 1;
810
811 if depth == 0 {
812 CellBuilder::build_from(*count).unwrap()
813 } else {
814 let mut b = CellBuilder::new();
815 for _ in 0..4 {
816 if *count < target {
817 b.store_reference(make_big_tree(depth - 1, count, target))
818 .unwrap();
819 }
820 }
821 b.build().unwrap()
822 }
823 }
824}