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 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 Err(_) if is_external => return Err(TxError::Skipped),
32 Err(e) => return Err(TxError::Fatal(e)),
33 };
34
35 let storage_phase;
38 let credit_phase;
39 if msg.bounce_enabled {
40 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 credit_phase = if is_external {
50 None
51 } else {
52 Some(self.credit_phase(&msg).context("credit phase failed")?)
53 };
54 } else {
55 credit_phase = if is_external {
57 None
58 } else {
59 Some(self.credit_phase(&msg).context("credit phase failed")?)
60 };
61
62 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 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 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 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 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 b.store_bit_one()?;
247 b.store_u256(&HashBytes::ZERO)?;
248 b.store_u256(&HashBytes::ZERO)?;
249 b.store_bit_one()?;
251 b.store_zeros(256)?;
252 b.store_u64((params.block_unixtime - 10) as u64 * 1000)?;
254 b.store_u32(params.block_unixtime + 40)?;
256 b.store_u32(0x4cee646c)?;
258 b.store_reference({
260 let mut b = CellBuilder::new();
261 address.store_into(&mut b, Cell::empty_context())?;
263 b.store_u128(10000000)?;
265 b.store_bit_zero()?;
267 b.store_u8(0b11)?;
269 b.store_reference(Cell::empty_cell())?;
271 b.build()?
273 })?;
274 b
276 }),
277 );
278
279 let state = make_uninit_with_balance(&address, CurrencyCollection::new(1_000_000_000));
280
281 let output = Executor::new(¶ms, 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 ¶ms,
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(¶ms, 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}