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 && 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#[derive(Default)]
211pub struct ExecutorInspector<'e> {
212 pub actions: Option<Cell>,
214 pub public_libs_diff: Vec<PublicLibraryChange>,
219 pub exit_code: Option<i32>,
221 pub missing_library: Option<HashBytes>,
223 pub total_gas_used: u64,
226 pub debug: Option<&'e mut dyn std::fmt::Write>,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum PublicLibraryChange {
233 Add(Cell),
234 Remove(HashBytes),
235}
236
237impl PublicLibraryChange {
238 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
247pub 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#[derive(Default, Clone)]
364pub struct ExecutorParams {
365 pub libraries: Dict<HashBytes, LibDescr>,
367 pub rand_seed: HashBytes,
369 pub block_unixtime: u32,
371 pub block_lt: u64,
373 pub vm_modifiers: tycho_vm::BehaviourModifiers,
375 pub disable_delete_frozen_accounts: bool,
380 pub charge_action_fees_on_fail: bool,
383 pub full_body_in_bounced: bool,
386 pub strict_extra_currency: bool,
388 pub authority_marks_enabled: bool,
390}
391
392pub 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 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 pub fn commit(mut self) -> Result<ExecutorOutput> {
436 let account_state;
438 let new_state_meta;
439 let end_status = match self.build_account_state()? {
440 None => {
441 account_state = CellBuilder::build_from(false)?;
443
444 new_state_meta = AccountMeta {
446 balance: CurrencyCollection::ZERO,
447 libraries: Dict::new(),
448 exists: false,
449 };
450
451 AccountStatus::NotExists
453 }
454 Some(state) => {
455 let prev_account_storage = 'prev: {
457 let mut cs = self.original.account.as_slice_allow_exotic();
458 if !cs.load_bit()? {
459 break 'prev None;
461 }
462 IntAddr::load_from(&mut cs)?;
465 let storage_info = StorageInfo::load_from(&mut cs)?;
467 Some((storage_info.used, cs))
469 };
470
471 let mut account_storage = CellBuilder::new();
473 account_storage.store_u64(self.exec.end_lt)?;
475 self.exec
477 .balance
478 .store_into(&mut account_storage, Cell::empty_context())?;
479 state.store_into(&mut account_storage, Cell::empty_context())?;
481
482 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 account_state = CellBuilder::build_from((
492 true, &self.exec.address, &self.exec.storage_stat, account_storage.as_full_slice(), ))?;
497
498 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 state.status()
511 }
512 };
513
514 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 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 let new_state = ShardAccount {
532 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 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 AccountStatus::NotExists => None,
552 AccountStatus::Uninit if self.exec.balance.is_zero() => None,
554 AccountStatus::Uninit => Some(AccountState::Uninit),
556 AccountStatus::Active => {
558 debug_assert!(matches!(self.exec.state, AccountState::Active(_)));
559 Some(self.exec.state.clone())
560 }
561 AccountStatus::Frozen => {
563 let cell;
564 let frozen_hash = match &self.exec.state {
565 AccountState::Uninit => &self.exec.address.address,
569 AccountState::Active(state_init) => {
571 cell = CellBuilder::build_from(state_init)?;
572 cell.repr_hash()
573 }
574 AccountState::Frozen(hash_bytes) => hash_bytes,
576 };
577
578 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)?; 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 if let Some((prev_used, prev_storage)) = prev {
638 'reuse: {
639 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 if prev_storage.size_refs() != new_storage.size_refs() {
648 break 'reuse;
649 }
650
651 for (prev, new) in prev_storage.references().zip(new_storage.references()) {
653 if prev != new {
654 break 'reuse;
655 }
656 }
657
658 return Ok(StorageUsed {
660 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 cells: prev_used.cells,
667 });
668 }
669 }
670
671 let cache = cache.get_or_insert_with(OwnedExtStorageStat::unlimited);
673 cache.set_unlimited();
674
675 for cell in new_storage.references().cloned() {
677 cache.add_cell(cell);
678 }
679 let stats = cache.stats();
680
681 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#[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#[derive(Clone, Debug)]
700pub struct AccountMeta {
701 pub balance: CurrencyCollection,
702 pub libraries: Dict<HashBytes, SimpleLib>,
703 pub exists: bool,
704}
705
706#[derive(Clone, Debug)]
708pub struct TransactionMeta {
709 pub total_fees: Tokens,
711 pub out_msgs: Vec<Lazy<OwnedMessage>>,
713 pub next_lt: u64,
715}
716
717pub 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 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}