Skip to main content

tycho_executor/tx/
ordinary.rs

1use anyhow::{Context, anyhow};
2use tycho_types::models::{
3    AccountStatus, BounceReason, ComputePhase, NewBounceComputePhaseInfo, OrdinaryTxInfo,
4};
5use tycho_types::num::Tokens;
6use tycho_types::prelude::*;
7
8use crate::error::{TxError, TxResult};
9use crate::phase::{
10    ActionPhaseContext, BouncePhaseContext, ComputePhaseContext, ComputePhaseFull,
11    StoragePhaseContext, TransactionInput,
12};
13use crate::{ExecutorInspector, ExecutorState};
14
15impl ExecutorState<'_> {
16    pub fn run_ordinary_transaction(
17        &mut self,
18        is_external: bool,
19        msg_root: Cell,
20        mut inspector: Option<&mut ExecutorInspector<'_>>,
21    ) -> TxResult<OrdinaryTxInfo> {
22        // Receive inbound message.
23        let mut msg = match self.receive_in_msg(msg_root) {
24            Ok(msg) if msg.is_external == is_external => msg,
25            Ok(_) => {
26                return Err(TxError::Fatal(anyhow!(
27                    "received an unexpected inbound message"
28                )));
29            }
30            // Invalid external messages can be safely skipped.
31            Err(_) if is_external => return Err(TxError::Skipped),
32            Err(e) => return Err(TxError::Fatal(e)),
33        };
34
35        // Order of credit and storage phases depends on the `bounce` flag
36        // of the inbound message.
37        let storage_phase;
38        let credit_phase;
39        if msg.bounce_enabled {
40            // Run storage phase.
41            storage_phase = self
42                .storage_phase(StoragePhaseContext {
43                    adjust_msg_balance: false,
44                    received_message: Some(&mut msg),
45                })
46                .context("storage phase failed")?;
47
48            // Run credit phase (only for internal messages).
49            credit_phase = if is_external {
50                None
51            } else {
52                Some(self.credit_phase(&msg).context("credit phase failed")?)
53            };
54        } else {
55            // Run credit phase (only for internal messages).
56            credit_phase = if is_external {
57                None
58            } else {
59                Some(self.credit_phase(&msg).context("credit phase failed")?)
60            };
61
62            // Run storage phase.
63            storage_phase = self
64                .storage_phase(StoragePhaseContext {
65                    adjust_msg_balance: true,
66                    received_message: Some(&mut msg),
67                })
68                .context("storage phase failed")?;
69        }
70
71        // Run compute phase.
72        let ComputePhaseFull {
73            compute_phase,
74            accepted,
75            original_balance,
76            new_state,
77            actions,
78        } = self
79            .compute_phase(ComputePhaseContext {
80                input: TransactionInput::Ordinary(&msg),
81                storage_fee: storage_phase.storage_fees_collected,
82                force_accept: false,
83                stop_on_accept: false,
84                inspector: inspector.as_deref_mut(),
85            })
86            .context("compute phase failed")?;
87
88        if is_external && !accepted {
89            return Err(TxError::Skipped);
90        }
91
92        // Run action phase only if compute phase succeeded.
93        let mut aborted = true;
94        let mut state_exceeds_limits = false;
95        let mut bounce_required = false;
96        let mut action_fine = Tokens::ZERO;
97        let mut destroyed = false;
98
99        let mut action_phase = None;
100        if let ComputePhase::Executed(compute_phase) = &compute_phase
101            && compute_phase.success
102        {
103            let res = self
104                .action_phase(ActionPhaseContext {
105                    received_message: Some(&mut msg),
106                    original_balance,
107                    new_state,
108                    actions,
109                    compute_phase,
110                    inspector,
111                })
112                .context("action phase failed")?;
113
114            aborted = !res.action_phase.success;
115            state_exceeds_limits = res.state_exceeds_limits;
116            bounce_required = res.bounce;
117            action_fine = res.action_fine;
118            destroyed = self.end_status == AccountStatus::NotExists;
119
120            action_phase = Some(res.action_phase);
121        }
122
123        // Run bounce phase for internal messages if something failed.
124        let mut bounce_phase = None;
125        if msg.bounce_enabled
126            && (!matches!(&compute_phase, ComputePhase::Executed(p) if p.success)
127                || state_exceeds_limits
128                || bounce_required)
129        {
130            debug_assert!(!is_external);
131
132            let reason = if let Some(phase) = &action_phase {
133                BounceReason::ActionPhaseFailed {
134                    result_code: phase.result_code,
135                }
136            } else {
137                match &compute_phase {
138                    ComputePhase::Executed(phase) => BounceReason::ComputePhaseFailed {
139                        exit_code: phase.exit_code,
140                    },
141                    ComputePhase::Skipped(skipped) => {
142                        BounceReason::ComputePhaseSkipped(skipped.reason)
143                    }
144                }
145            };
146
147            let (gas_fees, compute_phase_info) = match &compute_phase {
148                ComputePhase::Executed(phase) => (
149                    phase.gas_fees,
150                    Some(NewBounceComputePhaseInfo {
151                        gas_used: phase.gas_used.into_inner().try_into().unwrap_or(u32::MAX),
152                        vm_steps: phase.vm_steps,
153                    }),
154                ),
155                ComputePhase::Skipped(_) => (Tokens::ZERO, None),
156            };
157
158            bounce_phase = Some(
159                self.bounce_phase(BouncePhaseContext {
160                    gas_fees,
161                    action_fine,
162                    received_message: &msg,
163                    reason,
164                    compute_phase_info,
165                })
166                .context("bounce phase failed")?,
167            );
168        }
169
170        // Build transaction info.
171        Ok(OrdinaryTxInfo {
172            credit_first: !msg.bounce_enabled,
173            storage_phase: Some(storage_phase),
174            credit_phase,
175            compute_phase,
176            action_phase,
177            aborted,
178            bounce_phase,
179            destroyed,
180        })
181    }
182
183    pub fn check_ordinary_transaction(
184        &mut self,
185        msg_root: Cell,
186        inspector: Option<&mut ExecutorInspector<'_>>,
187    ) -> TxResult<()> {
188        // Receive phase
189        let mut msg = match self.receive_in_msg(msg_root) {
190            Ok(msg) if msg.is_external => msg,
191            _ => return Err(TxError::Skipped),
192        };
193
194        // Storage phase
195        let storage_phase = self
196            .storage_phase(StoragePhaseContext {
197                adjust_msg_balance: false,
198                received_message: Some(&mut msg),
199            })
200            .context("storage phase failed")?;
201
202        // Compute phase with partial execution
203        let ComputePhaseFull { accepted, .. } = self
204            .compute_phase(ComputePhaseContext {
205                input: TransactionInput::Ordinary(&msg),
206                storage_fee: storage_phase.storage_fees_collected,
207                force_accept: false,
208                stop_on_accept: true,
209                inspector,
210            })
211            .context("compute phase failed")?;
212
213        if !accepted {
214            return Err(TxError::Skipped);
215        }
216
217        Ok(())
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use anyhow::Result;
224    use tycho_asm_macros::tvmasm;
225    use tycho_types::cell::Lazy;
226    use tycho_types::models::{
227        Account, AccountState, AccountStatusChange, CurrencyCollection, ExtInMsgInfo, IntMsgInfo,
228        MsgInfo, OptionalAccount, ShardAccount, StateInit, StdAddr, StorageInfo, StorageUsed,
229        TxInfo,
230    };
231    use tycho_types::num::VarUint56;
232
233    use super::*;
234    use crate::Executor;
235    use crate::tests::{make_default_config, make_default_params, make_message};
236
237    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
238
239    pub fn make_uninit_with_balance<T: Into<CurrencyCollection>>(
240        address: &StdAddr,
241        balance: T,
242    ) -> ShardAccount {
243        ShardAccount {
244            account: Lazy::new(&OptionalAccount(Some(Account {
245                address: address.clone().into(),
246                storage_stat: StorageInfo::default(),
247                last_trans_lt: 1001,
248                balance: balance.into(),
249                state: AccountState::Uninit,
250            })))
251            .unwrap(),
252            last_trans_hash: HashBytes([0x11; 32]),
253            last_trans_lt: 1000,
254        }
255    }
256
257    #[test]
258    fn ever_wallet_deploys() -> Result<()> {
259        let config = make_default_config();
260        let params = make_default_params();
261
262        let code = Boc::decode(include_bytes!("../../res/ever_wallet_code.boc"))?;
263        let data = CellBuilder::build_from((HashBytes::ZERO, 0u64))?;
264
265        let state_init = StateInit {
266            split_depth: None,
267            special: None,
268            code: Some(code),
269            data: Some(data),
270            libraries: Dict::new(),
271        };
272        let address = StdAddr::new(0, *CellBuilder::build_from(&state_init)?.repr_hash());
273
274        let msg = make_message(
275            ExtInMsgInfo {
276                src: None,
277                dst: address.clone().into(),
278                import_fee: Tokens::ZERO,
279            },
280            Some(state_init),
281            Some({
282                let mut b = CellBuilder::new();
283                // just$1 Signature
284                b.store_bit_one()?;
285                b.store_u256(&HashBytes::ZERO)?;
286                b.store_u256(&HashBytes::ZERO)?;
287                // just$1 Pubkey
288                b.store_bit_one()?;
289                b.store_zeros(256)?;
290                // header_time:u64
291                b.store_u64((params.block_unixtime - 10) as u64 * 1000)?;
292                // header_expire:u32
293                b.store_u32(params.block_unixtime + 40)?;
294                // sendTransaction
295                b.store_u32(0x4cee646c)?;
296                // ...
297                b.store_reference({
298                    let mut b = CellBuilder::new();
299                    // dest:address
300                    address.store_into(&mut b, Cell::empty_context())?;
301                    // value:uint128
302                    b.store_u128(10000000)?;
303                    // bounce:false
304                    b.store_bit_zero()?;
305                    // mode:uint8
306                    b.store_u8(0b11)?;
307                    // payload:cell
308                    b.store_reference(Cell::empty_cell())?;
309                    //
310                    b.build()?
311                })?;
312                //
313                b
314            }),
315        );
316
317        let state = make_uninit_with_balance(&address, CurrencyCollection::new(1_000_000_000));
318
319        let output = Executor::new(&params, config.as_ref())
320            .begin_ordinary(&address, true, &msg, &state)?
321            .commit()?;
322
323        println!("SHARD_STATE: {:#?}", output.new_state);
324        let account = output.new_state.load_account()?;
325        println!("ACCOUNT: {:#?}", account);
326
327        let tx = output.transaction.load()?;
328        println!("TX: {tx:#?}");
329        let info = tx.load_info()?;
330        println!("INFO: {info:#?}");
331
332        Ok(())
333    }
334
335    #[test]
336    fn freeze_account() -> Result<()> {
337        let params = make_default_params();
338        let config = make_default_config();
339
340        let code = tvmasm!(
341            r#"
342            NEWC INT 123 STUR 32 ENDC
343            POP c4
344            ACCEPT
345            "#
346        );
347        let mut state = ExecutorState::new_active(
348            &params,
349            &config,
350            &STUB_ADDR,
351            Tokens::ZERO,
352            CellBuilder::build_from(u32::MIN)?,
353            code,
354        );
355        state.storage_stat = StorageInfo {
356            used: StorageUsed {
357                bits: VarUint56::new(128),
358                cells: VarUint56::new(1),
359            },
360            storage_extra: Default::default(),
361            last_paid: params.block_unixtime - 1000,
362            due_payment: Some(Tokens::new(2 * config.gas_prices.freeze_due_limit as u128)),
363        };
364
365        let info = state.run_ordinary_transaction(
366            false,
367            make_message(
368                IntMsgInfo {
369                    src: STUB_ADDR.into(),
370                    dst: STUB_ADDR.into(),
371                    value: CurrencyCollection::new(1_000_000),
372                    bounce: true,
373                    ..Default::default()
374                },
375                None,
376                None,
377            ),
378            None,
379        )?;
380
381        assert!(!info.aborted);
382        assert_eq!(
383            info.storage_phase.unwrap().status_change,
384            AccountStatusChange::Frozen
385        );
386
387        let ComputePhase::Executed(compute_phase) = info.compute_phase else {
388            panic!("expected an executed compute phase");
389        };
390        assert!(compute_phase.success);
391
392        let action_phase = info.action_phase.unwrap();
393        assert!(action_phase.success);
394        assert_eq!(action_phase.messages_created, 0);
395
396        assert_eq!(info.bounce_phase, None);
397
398        assert_eq!(state.orig_status, AccountStatus::Active);
399        assert_eq!(state.end_status, AccountStatus::Frozen);
400        assert_eq!(state.balance, CurrencyCollection::ZERO);
401
402        assert_eq!(
403            state.state,
404            AccountState::Active(StateInit {
405                code: Boc::decode(code).map(Some)?,
406                data: CellBuilder::build_from(123u32).map(Some)?,
407                ..Default::default()
408            })
409        );
410
411        Ok(())
412    }
413
414    #[test]
415    fn deploy_delete_in_same_tx() -> Result<()> {
416        let params = make_default_params();
417        let config = make_default_config();
418
419        let code = Boc::decode(tvmasm!(
420            r#"
421            ACCEPT
422            NEWC
423            // int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
424            INT 0b011000 STUR 6
425            MYADDR
426            STSLICER
427            INT 0 STGRAMS
428            // extra:$0 ihr_fee:Tokens fwd_fee:Tokens created_lt:uint64 created_at:uint32
429            // 1       + 4            + 4            + 64              + 32
430            // init:none$0 body:left$0
431            // 1          + 1
432            INT 107 STZEROES
433            ENDC INT 160 SENDRAWMSG
434            "#
435        ))?;
436        let state_init = StateInit {
437            code: Some(code),
438            ..Default::default()
439        };
440
441        let address = StdAddr::new(0, *CellBuilder::build_from(&state_init)?.repr_hash());
442
443        let msg_balance = Tokens::new(1_000_000_000);
444        let msg = make_message(
445            IntMsgInfo {
446                src: address.clone().into(),
447                dst: address.clone().into(),
448                value: msg_balance.into(),
449                ..Default::default()
450            },
451            Some(state_init),
452            None,
453        );
454
455        let state = ShardAccount {
456            account: Lazy::new(&OptionalAccount::EMPTY)?,
457            last_trans_hash: HashBytes::ZERO,
458            last_trans_lt: 0,
459        };
460
461        let output = Executor::new(&params, config.as_ref())
462            .begin_ordinary(&address, false, msg, &state)?
463            .commit()?;
464
465        let new_account_state = output.new_state.load_account()?;
466        assert_eq!(new_account_state, None);
467
468        let tx = output.transaction.load()?;
469        assert_eq!(tx.orig_status, AccountStatus::NotExists);
470        assert_eq!(tx.end_status, AccountStatus::NotExists);
471
472        let TxInfo::Ordinary(info) = tx.load_info()? else {
473            panic!("expected an ordinary transaction info");
474        };
475        println!("{info:#?}");
476
477        assert!(!info.aborted);
478        assert!(info.destroyed);
479
480        let ComputePhase::Executed(compute_phase) = info.compute_phase else {
481            panic!("expected an executed compute phase");
482        };
483        assert!(compute_phase.success);
484        let action_phase = info.action_phase.unwrap();
485        assert!(action_phase.success);
486        assert_eq!(action_phase.total_actions, 1);
487        assert_eq!(action_phase.messages_created, 1);
488
489        assert_eq!(output.transaction_meta.out_msgs.len(), 1);
490        let out_msg = output.transaction_meta.out_msgs.last().unwrap().load()?;
491
492        {
493            let MsgInfo::Int(info) = out_msg.info else {
494                panic!("expected an internal outbound message");
495            };
496
497            assert_eq!(info.src, address.clone().into());
498            assert_eq!(info.dst, address.clone().into());
499            assert!(info.value.other.is_empty());
500            assert_eq!(
501                info.value.tokens,
502                msg_balance
503                    - compute_phase.gas_fees
504                    - action_phase.total_fwd_fees.unwrap_or_default()
505            );
506
507            assert_eq!(out_msg.init, None);
508            assert_eq!(out_msg.body, Default::default());
509        }
510
511        Ok(())
512    }
513}