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
44pub struct Executor<'a> {
46 params: &'a ExecutorParams,
47 config: &'a ParsedConfig,
48 min_lt: u64,
49 override_special: Option<bool>,
50}
51
52impl<'a> Executor<'a> {
53 pub fn new(params: &'a ExecutorParams, config: &'a ParsedConfig) -> Self {
54 Self {
55 params,
56 config,
57 min_lt: 0,
58 override_special: None,
59 }
60 }
61
62 pub fn with_min_lt(mut self, min_lt: u64) -> Self {
63 self.set_min_lt(min_lt);
64 self
65 }
66
67 pub fn set_min_lt(&mut self, min_lt: u64) {
68 self.min_lt = min_lt;
69 }
70
71 pub fn override_special(mut self, is_special: bool) -> Self {
72 self.override_special = Some(is_special);
73 self
74 }
75
76 #[inline]
77 pub fn begin_ordinary<'s, M>(
78 &self,
79 address: &StdAddr,
80 is_external: bool,
81 msg: M,
82 state: &'s ShardAccount,
83 ) -> TxResult<UncommittedTransaction<'a, 's>>
84 where
85 M: LoadMessage,
86 {
87 self.begin_ordinary_ext(address, is_external, msg, state, None)
88 }
89
90 pub fn begin_ordinary_ext<'s, M>(
91 &self,
92 address: &StdAddr,
93 is_external: bool,
94 msg: M,
95 state: &'s ShardAccount,
96 inspector: Option<&mut ExecutorInspector<'_>>,
97 ) -> TxResult<UncommittedTransaction<'a, 's>>
98 where
99 M: LoadMessage,
100 {
101 let msg_root = msg.load_message_root()?;
102
103 let account = state.load_account()?;
104 let mut exec = self.begin(address, account)?;
105 let info = exec.run_ordinary_transaction(is_external, msg_root.clone(), inspector)?;
106
107 UncommittedTransaction::with_info(exec, state, Some(msg_root), info).map_err(TxError::Fatal)
108 }
109
110 #[inline]
111 pub fn begin_tick_tock<'s>(
112 &self,
113 address: &StdAddr,
114 kind: TickTock,
115 state: &'s ShardAccount,
116 ) -> TxResult<UncommittedTransaction<'a, 's>> {
117 self.begin_tick_tock_ext(address, kind, state, None)
118 }
119
120 pub fn begin_tick_tock_ext<'s>(
121 &self,
122 address: &StdAddr,
123 kind: TickTock,
124 state: &'s ShardAccount,
125 inspector: Option<&mut ExecutorInspector<'_>>,
126 ) -> TxResult<UncommittedTransaction<'a, 's>> {
127 let account = state.load_account()?;
128 let mut exec = self.begin(address, account)?;
129 let info = exec.run_tick_tock_transaction(kind, inspector)?;
130
131 UncommittedTransaction::with_info(exec, state, None, info).map_err(TxError::Fatal)
132 }
133
134 pub fn begin(&self, address: &StdAddr, account: Option<Account>) -> Result<ExecutorState<'a>> {
135 let is_special = self
136 .override_special
137 .unwrap_or_else(|| self.config.is_special(address));
138
139 let acc_address;
140 let acc_storage_stat;
141 let acc_balance;
142 let acc_state;
143 let orig_status;
144 let end_status;
145 let start_lt;
146 match account {
147 Some(acc) => {
148 acc_address = 'addr: {
149 if let IntAddr::Std(acc_addr) = acc.address
150 && acc_addr == *address
151 {
152 break 'addr acc_addr;
153 }
154 anyhow::bail!("account address mismatch");
155 };
156 acc_storage_stat = acc.storage_stat;
157 acc_balance = acc.balance;
158 acc_state = acc.state;
159 orig_status = acc_state.status();
160 end_status = orig_status;
161 start_lt = std::cmp::max(self.min_lt, acc.last_trans_lt);
162 }
163 None => {
164 acc_address = address.clone();
165 acc_storage_stat = StorageInfo {
166 used: StorageUsed::ZERO,
167 storage_extra: Default::default(),
168 last_paid: 0,
169 due_payment: None,
170 };
171 acc_balance = CurrencyCollection::ZERO;
172 acc_state = AccountState::Uninit;
173 orig_status = AccountStatus::NotExists;
174 end_status = AccountStatus::Uninit;
175 start_lt = self.min_lt;
176 }
177 };
178
179 let mut is_marks_authority = false;
180 let mut is_suspended_by_marks = false;
181 if self.params.authority_marks_enabled
182 && let Some(marks) = &self.config.authority_marks
183 {
184 is_marks_authority = marks.is_authority(address);
185 is_suspended_by_marks =
186 !is_special && !is_marks_authority && marks.is_suspended(&acc_balance)?;
187 }
188
189 Ok(ExecutorState {
190 params: self.params,
191 config: self.config,
192 is_special,
193 is_marks_authority,
194 is_suspended_by_marks,
195 address: acc_address,
196 storage_stat: acc_storage_stat,
197 balance: acc_balance,
198 state: acc_state,
199 orig_status,
200 end_status,
201 start_lt,
202 end_lt: start_lt + 1,
203 out_msgs: Vec::new(),
204 total_fees: Tokens::ZERO,
205 burned: Tokens::ZERO,
206 cached_storage_stat: None,
207 })
208 }
209}
210
211#[derive(Default)]
213pub struct ExecutorInspector<'e> {
214 pub actions: Option<Cell>,
216 pub public_libs_diff: Vec<PublicLibraryChange>,
221 pub exit_code: Option<i32>,
223 pub missing_library: Option<HashBytes>,
225 pub total_gas_used: u64,
228 pub debug: Option<&'e mut dyn std::fmt::Write>,
230 pub modify_smc_info: Option<&'e mut ModifySmcInfoFn>,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub enum PublicLibraryChange {
237 Add(Cell),
238 Remove(HashBytes),
239}
240
241impl PublicLibraryChange {
242 pub fn lib_hash(&self) -> &HashBytes {
244 match self {
245 Self::Add(cell) => cell.repr_hash(),
246 Self::Remove(hash) => hash,
247 }
248 }
249}
250
251pub type ModifySmcInfoFn = dyn FnMut(&mut phase::ComputePhaseSmcInfo) -> Result<()>;
253
254pub struct ExecutorState<'a> {
256 pub params: &'a ExecutorParams,
257 pub config: &'a ParsedConfig,
258
259 pub is_special: bool,
260 pub is_marks_authority: bool,
261 pub is_suspended_by_marks: bool,
262
263 pub address: StdAddr,
264 pub storage_stat: StorageInfo,
265 pub balance: CurrencyCollection,
266 pub state: AccountState,
267
268 pub orig_status: AccountStatus,
269 pub end_status: AccountStatus,
270 pub start_lt: u64,
271 pub end_lt: u64,
272
273 pub out_msgs: Vec<Lazy<OwnedMessage>>,
274 pub total_fees: Tokens,
275
276 pub burned: Tokens,
277
278 pub cached_storage_stat: Option<OwnedExtStorageStat>,
279}
280
281#[cfg(test)]
282impl<'a> ExecutorState<'a> {
283 pub(crate) fn new_non_existent(
284 params: &'a ExecutorParams,
285 config: &'a impl AsRef<ParsedConfig>,
286 address: &StdAddr,
287 ) -> Self {
288 Self {
289 params,
290 config: config.as_ref(),
291 is_special: false,
292 is_marks_authority: false,
293 is_suspended_by_marks: false,
294 address: address.clone(),
295 storage_stat: Default::default(),
296 balance: CurrencyCollection::ZERO,
297 state: AccountState::Uninit,
298 orig_status: AccountStatus::NotExists,
299 end_status: AccountStatus::Uninit,
300 start_lt: 0,
301 end_lt: 1,
302 out_msgs: Vec::new(),
303 total_fees: Tokens::ZERO,
304 burned: Tokens::ZERO,
305 cached_storage_stat: None,
306 }
307 }
308
309 pub(crate) fn new_uninit(
310 params: &'a ExecutorParams,
311 config: &'a impl AsRef<ParsedConfig>,
312 address: &StdAddr,
313 balance: impl Into<CurrencyCollection>,
314 ) -> Self {
315 let mut res = Self::new_non_existent(params, config, address);
316 res.balance = balance.into();
317 res.orig_status = AccountStatus::Uninit;
318
319 if params.authority_marks_enabled
320 && let Some(marks) = &config.as_ref().authority_marks
321 {
322 res.is_marks_authority = marks.is_authority(address);
323 res.is_suspended_by_marks = !res.is_special
324 && !res.is_marks_authority
325 && marks.is_suspended(&res.balance).unwrap();
326 }
327
328 res
329 }
330
331 pub(crate) fn new_frozen(
332 params: &'a ExecutorParams,
333 config: &'a impl AsRef<ParsedConfig>,
334 address: &StdAddr,
335 balance: impl Into<CurrencyCollection>,
336 state_hash: HashBytes,
337 ) -> Self {
338 let mut res = Self::new_uninit(params, config, address, balance);
339 res.state = AccountState::Frozen(state_hash);
340 res.orig_status = AccountStatus::Frozen;
341 res.end_status = AccountStatus::Frozen;
342 res
343 }
344
345 pub(crate) fn new_active(
346 params: &'a ExecutorParams,
347 config: &'a impl AsRef<ParsedConfig>,
348 address: &StdAddr,
349 balance: impl Into<CurrencyCollection>,
350 data: Cell,
351 code_boc: impl AsRef<[u8]>,
352 ) -> Self {
353 use tycho_types::models::StateInit;
354
355 let mut res = Self::new_uninit(params, config, address, balance);
356 res.state = AccountState::Active(StateInit {
357 split_depth: None,
358 special: None,
359 code: Some(Boc::decode(code_boc).unwrap()),
360 data: Some(data),
361 libraries: Dict::new(),
362 });
363 res.orig_status = AccountStatus::Active;
364 res.end_status = AccountStatus::Active;
365 res
366 }
367}
368
369#[derive(Default, Clone)]
371pub struct ExecutorParams {
372 pub libraries: Dict<HashBytes, LibDescr>,
374 pub rand_seed: HashBytes,
376 pub block_unixtime: u32,
378 pub block_lt: u64,
380 pub prev_mc_block_id: Option<BlockId>,
382 pub vm_modifiers: tycho_vm::BehaviourModifiers,
384 pub disable_delete_frozen_accounts: bool,
389 pub charge_action_fees_on_fail: bool,
392 pub full_body_in_bounced: bool,
395 pub strict_extra_currency: bool,
397 pub authority_marks_enabled: bool,
399}
400
401pub struct UncommittedTransaction<'a, 's> {
403 original: &'s ShardAccount,
404 exec: ExecutorState<'a>,
405 in_msg: Option<Cell>,
406 info: Lazy<TxInfo>,
407}
408
409impl<'a, 's> UncommittedTransaction<'a, 's> {
410 #[inline]
411 pub fn with_info(
412 exec: ExecutorState<'a>,
413 original: &'s ShardAccount,
414 in_msg: Option<Cell>,
415 info: impl Into<TxInfo>,
416 ) -> Result<Self> {
417 let info = info.into();
418 let info = Lazy::new(&info)?;
419 Ok(Self {
420 original,
421 exec,
422 in_msg,
423 info,
424 })
425 }
426
427 pub fn build_uncommitted(&self) -> Result<Transaction, Error> {
432 thread_local! {
433 static EMPTY_STATE_UPDATE: Lazy<HashUpdate> = Lazy::new(&HashUpdate {
434 old: HashBytes::ZERO,
435 new: HashBytes::ZERO,
436 })
437 .unwrap();
438 }
439
440 self.build_transaction(self.exec.end_status, EMPTY_STATE_UPDATE.with(Clone::clone))
441 }
442
443 pub fn commit(mut self) -> Result<ExecutorOutput> {
445 let account_state;
447 let new_state_meta;
448 let end_status = match self.build_account_state()? {
449 None => {
450 account_state = CellBuilder::build_from(false)?;
452
453 new_state_meta = AccountMeta {
455 balance: CurrencyCollection::ZERO,
456 libraries: Dict::new(),
457 exists: false,
458 };
459
460 AccountStatus::NotExists
462 }
463 Some(state) => {
464 let prev_account_storage = 'prev: {
466 let mut cs = self.original.account.as_slice_allow_exotic();
467 if !cs.load_bit()? {
468 break 'prev None;
470 }
471 IntAddr::load_from(&mut cs)?;
474 let storage_info = StorageInfo::load_from(&mut cs)?;
476 Some((storage_info.used, cs))
478 };
479
480 let mut account_storage = CellBuilder::new();
482 account_storage.store_u64(self.exec.end_lt)?;
484 self.exec
486 .balance
487 .store_into(&mut account_storage, Cell::empty_context())?;
488 state.store_into(&mut account_storage, Cell::empty_context())?;
490
491 self.exec.storage_stat.used = compute_storage_used(
493 prev_account_storage,
494 account_storage.as_full_slice(),
495 &mut self.exec.cached_storage_stat,
496 self.exec.params.strict_extra_currency,
497 )?;
498
499 account_state = CellBuilder::build_from((
501 true, &self.exec.address, &self.exec.storage_stat, account_storage.as_full_slice(), ))?;
506
507 let libraries = match &state {
509 AccountState::Active(state) => state.libraries.clone(),
510 AccountState::Frozen(..) | AccountState::Uninit => Dict::new(),
511 };
512 new_state_meta = AccountMeta {
513 balance: self.exec.balance.clone(),
514 libraries,
515 exists: true,
516 };
517
518 state.status()
520 }
521 };
522
523 let state_update = Lazy::new(&HashUpdate {
525 old: *self.original.account.repr_hash(),
526 new: *account_state.repr_hash(),
527 })?;
528 let transaction = self
529 .build_transaction(end_status, state_update)
530 .and_then(|tx| Lazy::new(&tx))?;
531
532 let transaction_meta = TransactionMeta {
534 total_fees: self.exec.total_fees,
535 next_lt: self.exec.end_lt,
536 out_msgs: self.exec.out_msgs,
537 };
538
539 let new_state = ShardAccount {
541 account: unsafe { Lazy::from_raw_unchecked(account_state) },
543 last_trans_hash: *transaction.repr_hash(),
544 last_trans_lt: self.exec.start_lt,
545 };
546
547 Ok(ExecutorOutput {
549 new_state,
550 new_state_meta,
551 transaction,
552 transaction_meta,
553 burned: self.exec.burned,
554 })
555 }
556
557 fn build_account_state(&self) -> Result<Option<AccountState>> {
558 Ok(match self.exec.end_status {
559 AccountStatus::NotExists => None,
561 AccountStatus::Uninit if self.exec.balance.is_zero() => None,
563 AccountStatus::Uninit => Some(AccountState::Uninit),
565 AccountStatus::Active => {
567 debug_assert!(matches!(self.exec.state, AccountState::Active(_)));
568 Some(self.exec.state.clone())
569 }
570 AccountStatus::Frozen => {
572 let cell;
573 let frozen_hash = match &self.exec.state {
574 AccountState::Uninit => &self.exec.address.address,
578 AccountState::Active(state_init) => {
580 cell = CellBuilder::build_from(state_init)?;
581 cell.repr_hash()
582 }
583 AccountState::Frozen(hash_bytes) => hash_bytes,
585 };
586
587 Some(if frozen_hash == &self.exec.address.address {
589 AccountState::Uninit
590 } else {
591 AccountState::Frozen(*frozen_hash)
592 })
593 }
594 })
595 }
596
597 fn build_transaction(
598 &self,
599 end_status: AccountStatus,
600 state_update: Lazy<HashUpdate>,
601 ) -> Result<Transaction, Error> {
602 Ok(Transaction {
603 account: self.exec.address.address,
604 lt: self.exec.start_lt,
605 prev_trans_hash: self.original.last_trans_hash,
606 prev_trans_lt: self.original.last_trans_lt,
607 now: self.exec.params.block_unixtime,
608 out_msg_count: Uint15::new(self.exec.out_msgs.len() as _),
609 orig_status: self.exec.orig_status,
610 end_status,
611 in_msg: self.in_msg.clone(),
612 out_msgs: build_out_msgs(&self.exec.out_msgs)?,
613 total_fees: self.exec.total_fees.into(),
614 state_update,
615 info: self.info.clone(),
616 })
617 }
618}
619
620fn compute_storage_used(
621 mut prev: Option<(StorageUsed, CellSlice<'_>)>,
622 mut new_storage: CellSlice<'_>,
623 cache: &mut Option<OwnedExtStorageStat>,
624 without_extra_currencies: bool,
625) -> Result<StorageUsed> {
626 fn skip_extra(slice: &mut CellSlice<'_>) -> Result<bool, Error> {
627 let mut cs = *slice;
628 cs.skip_first(64, 0)?; let balance = CurrencyCollection::load_from(&mut cs)?;
630 Ok(if balance.other.is_empty() {
631 false
632 } else {
633 slice.skip_first(0, 1)?;
634 true
635 })
636 }
637
638 if without_extra_currencies {
639 if let Some((_, prev)) = &mut prev {
640 skip_extra(prev)?;
641 }
642 skip_extra(&mut new_storage)?;
643 }
644
645 if let Some((prev_used, prev_storage)) = prev {
647 'reuse: {
648 if prev_used.cells.is_zero()
650 || prev_used.bits.into_inner() < prev_storage.size_bits() as u64
651 {
652 break 'reuse;
653 }
654
655 if prev_storage.size_refs() != new_storage.size_refs() {
657 break 'reuse;
658 }
659
660 for (prev, new) in prev_storage.references().zip(new_storage.references()) {
662 if prev != new {
663 break 'reuse;
664 }
665 }
666
667 return Ok(StorageUsed {
669 bits: new_varuint56_truncate(
671 (prev_used.bits.into_inner() - prev_storage.size_bits() as u64)
672 .saturating_add(new_storage.size_bits() as u64),
673 ),
674 cells: prev_used.cells,
676 });
677 }
678 }
679
680 let cache = cache.get_or_insert_with(OwnedExtStorageStat::unlimited);
682 cache.set_unlimited();
683
684 for cell in new_storage.references().cloned() {
686 cache.add_cell(cell);
687 }
688 let stats = cache.stats();
689
690 Ok(StorageUsed {
692 cells: new_varuint56_truncate(stats.cell_count.saturating_add(1)),
693 bits: new_varuint56_truncate(stats.bit_count.saturating_add(new_storage.size_bits() as _)),
694 })
695}
696
697#[derive(Clone, Debug)]
699pub struct ExecutorOutput {
700 pub new_state: ShardAccount,
701 pub new_state_meta: AccountMeta,
702 pub transaction: Lazy<Transaction>,
703 pub transaction_meta: TransactionMeta,
704 pub burned: Tokens,
705}
706
707#[derive(Clone, Debug)]
709pub struct AccountMeta {
710 pub balance: CurrencyCollection,
711 pub libraries: Dict<HashBytes, SimpleLib>,
712 pub exists: bool,
713}
714
715#[derive(Clone, Debug)]
717pub struct TransactionMeta {
718 pub total_fees: Tokens,
720 pub out_msgs: Vec<Lazy<OwnedMessage>>,
722 pub next_lt: u64,
724}
725
726pub trait LoadMessage {
728 fn load_message_root(self) -> Result<Cell>;
729}
730
731impl<T: LoadMessage + Clone> LoadMessage for &T {
732 #[inline]
733 fn load_message_root(self) -> Result<Cell> {
734 T::load_message_root(T::clone(self))
735 }
736}
737
738impl LoadMessage for Cell {
739 #[inline]
740 fn load_message_root(self) -> Result<Cell> {
741 Ok(self)
742 }
743}
744
745impl<T: EquivalentRepr<OwnedMessage>> LoadMessage for Lazy<T> {
746 #[inline]
747 fn load_message_root(self) -> Result<Cell> {
748 Ok(self.into_inner())
749 }
750}
751
752impl LoadMessage for OwnedMessage {
753 #[inline]
754 fn load_message_root(self) -> Result<Cell> {
755 CellBuilder::build_from(self).context("failed to serialize inbound message")
756 }
757}
758
759impl LoadMessage for Message<'_> {
760 #[inline]
761 fn load_message_root(self) -> Result<Cell> {
762 CellBuilder::build_from(self).context("failed to serialize inbound message")
763 }
764}
765
766fn build_out_msgs(out_msgs: &[Lazy<OwnedMessage>]) -> Result<Dict<Uint15, Cell>, Error> {
767 dict::build_dict_from_sorted_iter(
768 out_msgs
769 .iter()
770 .enumerate()
771 .map(|(i, msg)| (Uint15::new(i as _), msg.inner().clone())),
772 Cell::empty_context(),
773 )
774 .map(Dict::from_raw)
775}
776
777#[cfg(test)]
778mod tests {
779 use std::rc::Rc;
780
781 use tycho_types::boc::BocRepr;
782 use tycho_types::models::{BlockchainConfig, MsgInfo, StateInit};
783
784 use super::*;
785
786 pub fn make_default_config() -> Rc<ParsedConfig> {
787 thread_local! {
788 pub static PARSED_CONFIG: Rc<ParsedConfig> = make_custom_config(|_| Ok(()));
789 }
790
791 PARSED_CONFIG.with(Clone::clone)
792 }
793
794 pub fn make_custom_config<F>(f: F) -> Rc<ParsedConfig>
795 where
796 F: FnOnce(&mut BlockchainConfig) -> anyhow::Result<()>,
797 {
798 let mut config: BlockchainConfig =
799 BocRepr::decode(include_bytes!("../res/config.boc")).unwrap();
800
801 config.params.set_global_id(100).unwrap();
802
803 config
805 .params
806 .set_size_limits(&ParsedConfig::DEFAULT_SIZE_LIMITS_CONFIG)
807 .unwrap();
808
809 f(&mut config).unwrap();
810
811 Rc::new(ParsedConfig::parse(config, u32::MAX).unwrap())
812 }
813
814 pub fn make_default_params() -> ExecutorParams {
815 ExecutorParams {
816 block_unixtime: 1738799198,
817 full_body_in_bounced: false,
818 strict_extra_currency: true,
819 vm_modifiers: tycho_vm::BehaviourModifiers {
820 chksig_always_succeed: true,
821 ..Default::default()
822 },
823 ..Default::default()
824 }
825 }
826
827 pub fn make_message(
828 info: impl Into<MsgInfo>,
829 init: Option<StateInit>,
830 body: Option<CellBuilder>,
831 ) -> Cell {
832 let body = match &body {
833 None => Cell::empty_cell_ref().as_slice_allow_exotic(),
834 Some(cell) => cell.as_full_slice(),
835 };
836 CellBuilder::build_from(Message {
837 info: info.into(),
838 init,
839 body,
840 layout: None,
841 })
842 .unwrap()
843 }
844
845 pub fn make_big_tree(depth: u8, count: &mut u16, target: u16) -> Cell {
846 *count += 1;
847
848 if depth == 0 {
849 CellBuilder::build_from(*count).unwrap()
850 } else {
851 let mut b = CellBuilder::new();
852 for _ in 0..4 {
853 if *count < target {
854 b.store_reference(make_big_tree(depth - 1, count, target))
855 .unwrap();
856 }
857 }
858 b.build().unwrap()
859 }
860 }
861}