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