tycho_executor/tx/
ordinary.rs

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