1use anyhow::Result;
2use tycho_types::models::{CurrencyCollection, IntAddr, MsgInfo, StateInit};
3use tycho_types::num::Tokens;
4use tycho_types::prelude::*;
5
6use crate::ExecutorState;
7use crate::util::{ExtStorageStat, StorageStatLimits};
8
9impl ExecutorState<'_> {
10 pub fn receive_in_msg(&mut self, msg_root: Cell) -> Result<ReceivedMessage> {
24 let is_masterchain = self.address.is_masterchain();
25
26 let is_external;
27 let bounce_enabled;
28 let mut msg_balance_remaining;
29
30 let mut slice = msg_root.as_slice_allow_exotic();
32 match MsgInfo::load_from(&mut slice)? {
33 MsgInfo::Int(info) => {
35 self.check_message_dst(&info.dst)?;
36
37 is_external = false;
39 bounce_enabled = info.bounce;
40
41 msg_balance_remaining = info.value;
43
44 if info.created_lt >= self.start_lt {
46 self.start_lt = info.created_lt + 1;
47 self.end_lt = self.start_lt + 1;
48 }
49 }
50 MsgInfo::ExtIn(info) => {
52 if self.is_suspended_by_marks {
53 anyhow::bail!("account was suspended by authority marks");
56 }
57
58 self.check_message_dst(&info.dst)?;
59
60 is_external = true;
62 bounce_enabled = false;
63
64 let Some(mut stats) =
66 ExtStorageStat::compute_for_slice(&slice, StorageStatLimits {
67 bit_count: self.config.size_limits.max_msg_bits,
68 cell_count: self.config.size_limits.max_msg_cells,
69 })
70 else {
71 anyhow::bail!("inbound message limits exceeded");
72 };
73
74 stats.cell_count -= 1; stats.bit_count -= slice.size_bits() as u64; let fwd_fee = if self.is_special {
78 Tokens::ZERO
81 } else {
82 self.config
83 .fwd_prices(is_masterchain)
84 .compute_fwd_fee(stats)
85 };
86
87 if self.balance.tokens < fwd_fee {
89 anyhow::bail!("cannot pay for importing an external message");
90 }
91 self.balance.tokens -= fwd_fee;
92 self.total_fees.try_add_assign(fwd_fee)?;
93
94 msg_balance_remaining = CurrencyCollection::ZERO;
96 }
97 MsgInfo::ExtOut(_) => anyhow::bail!("unexpected incoming ExtOut message"),
99 }
100
101 let init = if slice.load_bit()? {
103 Some(if slice.load_bit()? {
104 let state_root = slice.load_reference_cloned()?;
106 anyhow::ensure!(
107 !state_root.is_exotic(),
108 "state init must be an ordinary cell"
109 );
110
111 let mut slice = state_root.as_slice_allow_exotic();
112 let parsed = StateInit::load_from(&mut slice)?;
113 anyhow::ensure!(slice.is_empty(), "state init contains extra data");
114
115 MsgStateInit {
116 root: state_root,
117 parsed,
118 }
119 } else {
120 let mut state_init_cs = slice;
122
123 let parsed = StateInit::load_from(&mut slice)?;
125 state_init_cs.skip_last(slice.size_bits(), slice.size_refs())?;
127 let state_root = CellBuilder::build_from(state_init_cs)?;
128
129 MsgStateInit {
130 root: state_root,
131 parsed,
132 }
133 })
134 } else {
135 None
136 };
137
138 let body = if slice.load_bit()? {
140 let body_cell = slice.load_reference_cloned()?;
142 anyhow::ensure!(slice.is_empty(), "message contains extra data");
143
144 CellSliceParts::from(body_cell)
145 } else {
146 (slice.range(), msg_root.clone())
148 };
149
150 if self.config.is_blackhole(&self.address) {
152 self.burned = msg_balance_remaining.tokens;
153 msg_balance_remaining.tokens = Tokens::ZERO;
154 }
155
156 Ok(ReceivedMessage {
158 root: msg_root,
159 init,
160 body,
161 is_external,
162 bounce_enabled,
163 balance_remaining: msg_balance_remaining,
164 })
165 }
166
167 fn check_message_dst(&self, dst: &IntAddr) -> Result<()> {
168 match dst {
169 IntAddr::Std(dst) => {
170 anyhow::ensure!(dst.anycast.is_none(), "anycast is not supported");
171 anyhow::ensure!(*dst == self.address, "message destination address mismatch");
172 Ok(())
173 }
174 IntAddr::Var(_) => anyhow::bail!("`addr_var` is not supported"),
175 }
176 }
177}
178
179#[derive(Debug, Clone)]
181pub struct ReceivedMessage {
182 pub root: Cell,
184 pub init: Option<MsgStateInit>,
186 pub body: CellSliceParts,
188
189 pub is_external: bool,
191 pub bounce_enabled: bool,
193
194 pub balance_remaining: CurrencyCollection,
197}
198
199#[derive(Debug, Clone)]
201pub struct MsgStateInit {
202 pub root: Cell,
204 pub parsed: StateInit,
206}
207
208impl MsgStateInit {
209 pub fn root_hash(&self) -> &HashBytes {
210 self.root.repr_hash()
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use std::collections::BTreeMap;
217
218 use tycho_types::models::{
219 AuthorityMarksConfig, BurningConfig, ExtInMsgInfo, ExtOutMsgInfo, IntMsgInfo, StdAddr,
220 };
221 use tycho_types::num::{Tokens, VarUint248};
222
223 use super::*;
224 use crate::ExecutorParams;
225 use crate::tests::{
226 make_big_tree, make_custom_config, make_default_config, make_default_params, make_message,
227 };
228
229 const OK_BALANCE: Tokens = Tokens::new(10_000_000_000);
230 const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
231
232 #[test]
235 fn receive_ext_in_works() {
236 let params = make_default_params();
237 let config = make_default_config();
238
239 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE);
240 let prev_start_lt = state.start_lt;
241 let prev_end_lt = state.end_lt;
242 let prev_balance = state.balance.clone();
243 let prev_acc_state = state.state.clone();
244
245 let msg_root = make_message(
246 ExtInMsgInfo {
247 dst: STUB_ADDR.into(),
248 ..Default::default()
249 },
250 Some(StateInit::default()),
251 None,
252 );
253 let msg = state.receive_in_msg(msg_root.clone()).unwrap();
254 assert!(msg.is_external);
256 assert!(!msg.bounce_enabled);
257 {
258 let init = msg.init.unwrap();
259 let target = StateInit::default();
260 assert_eq!(init.parsed, target);
261 let target_hash = *CellBuilder::build_from(&target).unwrap().repr_hash();
262 assert_eq!(init.root_hash(), &target_hash);
263 }
264 assert!(msg.body.0.is_empty());
265 assert_eq!(msg.balance_remaining, CurrencyCollection::ZERO);
266
267 assert_eq!(state.start_lt, prev_start_lt);
269 assert_eq!(state.end_lt, prev_end_lt);
270 assert_eq!(
273 state.total_fees,
274 Tokens::new(config.fwd_prices.lump_price as _)
275 );
276 assert_eq!(state.balance.other, prev_balance.other);
278 assert_eq!(state.balance.tokens, prev_balance.tokens - state.total_fees);
280 assert_eq!(state.state, prev_acc_state);
282 }
283
284 #[test]
285 fn receive_int_to_non_existent() {
286 let params = make_default_params();
287 let config = make_default_config();
288
289 let mut state = ExecutorState::new_non_existent(¶ms, &config, &STUB_ADDR);
290 let prev_start_lt = state.start_lt;
291 assert_eq!(prev_start_lt, 0);
292 let prev_balance = state.balance.clone();
293 let prev_acc_state = state.state.clone();
294
295 let msg_lt = 1000;
296 let msg_root = make_message(
297 IntMsgInfo {
298 dst: STUB_ADDR.into(),
299 value: OK_BALANCE.into(),
300 bounce: true,
301 created_lt: msg_lt,
302 ..Default::default()
303 },
304 None,
305 Some({
306 let mut b = CellBuilder::new();
307 b.store_u32(0xdeafbeaf).unwrap();
308 b
309 }),
310 );
311 let msg = state.receive_in_msg(msg_root.clone()).unwrap();
312 assert!(!msg.is_external);
314 assert!(msg.bounce_enabled);
315 assert!(msg.init.is_none());
316 assert_eq!(
317 CellSlice::apply(&msg.body).unwrap().load_u32().unwrap(),
318 0xdeafbeaf
319 );
320 assert_eq!(msg.balance_remaining, OK_BALANCE.into());
321
322 assert_eq!(state.start_lt, msg_lt + 1);
324 assert_eq!(state.end_lt, state.start_lt + 1);
325 assert_eq!(state.total_fees, Tokens::ZERO);
327 assert_eq!(state.balance, prev_balance);
329 assert_eq!(state.state, prev_acc_state);
331 }
332
333 #[test]
334 fn receive_int_to_blackhole() {
335 let addr = StdAddr::new(-1, HashBytes::ZERO);
336
337 let params = make_default_params();
338 let config = make_custom_config(|config| {
339 config.set_burning_config(&BurningConfig {
340 blackhole_addr: Some(addr.address),
341 ..Default::default()
342 })?;
343 Ok(())
344 });
345
346 let mut state = ExecutorState::new_uninit(¶ms, &config, &addr, OK_BALANCE);
347 let prev_start_lt = state.start_lt;
348 assert_eq!(prev_start_lt, 0);
349 let prev_balance = state.balance.clone();
350 let prev_acc_state = state.state.clone();
351
352 let msg_lt = 1000;
353 let msg_root = make_message(
354 IntMsgInfo {
355 dst: addr.into(),
356 value: OK_BALANCE.into(),
357 bounce: true,
358 created_lt: msg_lt,
359 ..Default::default()
360 },
361 None,
362 None,
363 );
364 let msg = state.receive_in_msg(msg_root).unwrap();
365 assert!(!msg.is_external);
367 assert!(msg.bounce_enabled);
368 assert!(msg.init.is_none());
369 assert!(msg.body.0.is_empty());
370 assert_eq!(msg.balance_remaining, CurrencyCollection::ZERO);
371
372 assert_eq!(state.start_lt, msg_lt + 1);
374 assert_eq!(state.end_lt, state.start_lt + 1);
375 assert_eq!(state.total_fees, Tokens::ZERO);
377 assert_eq!(state.balance, prev_balance);
379 assert_eq!(state.state, prev_acc_state);
381 assert_eq!(state.burned, OK_BALANCE);
383 }
384
385 #[test]
388 fn receive_ext_out() {
389 let params = make_default_params();
390 let config = make_default_config();
391
392 ExecutorState::new_non_existent(¶ms, &config, &STUB_ADDR)
393 .receive_in_msg(make_message(
394 ExtOutMsgInfo {
395 dst: None,
396 src: STUB_ADDR.into(),
397 created_at: 1,
398 created_lt: 1,
399 },
400 None,
401 None,
402 ))
403 .inspect_err(|e| println!("{e}"))
404 .unwrap_err();
405 }
406
407 #[test]
408 fn receive_ext_in_on_non_existent() {
409 let params = make_default_params();
410 let config = make_default_config();
411
412 ExecutorState::new_non_existent(¶ms, &config, &STUB_ADDR)
413 .receive_in_msg(make_message(
414 ExtInMsgInfo {
415 dst: STUB_ADDR.into(),
416 ..Default::default()
417 },
418 None,
419 None,
420 ))
421 .inspect_err(|e| println!("{e}"))
422 .unwrap_err();
423 }
424
425 #[test]
426 fn receive_ext_in_not_enough_balance() {
427 let params = make_default_params();
428 let config = make_default_config();
429
430 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::new(1))
431 .receive_in_msg(make_message(
432 ExtInMsgInfo {
433 dst: STUB_ADDR.into(),
434 ..Default::default()
435 },
436 None,
437 None,
438 ))
439 .inspect_err(|e| println!("{e}"))
440 .unwrap_err();
441 }
442
443 #[test]
444 fn receive_ext_in_suspended_by_marks() -> anyhow::Result<()> {
445 let params = ExecutorParams {
446 authority_marks_enabled: true,
447 ..make_default_params()
448 };
449
450 let config = make_custom_config(|config| {
451 config.set_authority_marks_config(&AuthorityMarksConfig {
452 authority_addresses: Dict::new(),
453 black_mark_id: 100,
454 white_mark_id: 101,
455 })?;
456 Ok(())
457 });
458
459 let balance = CurrencyCollection {
460 tokens: OK_BALANCE,
461 other: BTreeMap::from_iter([
462 (100u32, VarUint248::new(1)), ])
464 .try_into()?,
465 };
466
467 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, balance)
468 .receive_in_msg(make_message(
469 ExtInMsgInfo {
470 dst: STUB_ADDR.into(),
471 ..Default::default()
472 },
473 None,
474 None,
475 ))
476 .inspect_err(|e| println!("{e}"))
477 .unwrap_err();
478
479 Ok(())
480 }
481
482 #[test]
483 fn receive_internal_balance_overflow() {
484 let params = make_default_params();
485 let config = make_default_config();
486
487 let mut state = ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, Tokens::MAX);
488
489 let received = state
490 .receive_in_msg(make_message(
491 IntMsgInfo {
492 dst: STUB_ADDR.into(),
493 value: Tokens::MAX.into(),
494 ..Default::default()
495 },
496 None,
497 None,
498 ))
499 .unwrap();
500
501 state
502 .credit_phase(&received)
503 .inspect_err(|e| println!("{e}"))
504 .unwrap_err();
505 }
506
507 #[test]
508 fn receive_with_invalid_dst() {
509 let params = make_default_params();
510 let config = make_default_config();
511
512 let other_addr = StdAddr::new(0, HashBytes([1; 32]));
513
514 ExecutorState::new_non_existent(¶ms, &config, &STUB_ADDR)
516 .receive_in_msg(make_message(
517 ExtInMsgInfo {
518 dst: other_addr.clone().into(),
519 ..Default::default()
520 },
521 None,
522 None,
523 ))
524 .inspect_err(|e| println!("{e}"))
525 .unwrap_err();
526
527 ExecutorState::new_non_existent(¶ms, &config, &STUB_ADDR)
529 .receive_in_msg(make_message(
530 IntMsgInfo {
531 dst: other_addr.clone().into(),
532 ..Default::default()
533 },
534 None,
535 None,
536 ))
537 .inspect_err(|e| println!("{e}"))
538 .unwrap_err();
539 }
540
541 #[test]
542 fn invalid_message_structure() -> anyhow::Result<()> {
543 let params = make_default_params();
544 let config = make_default_config();
545 let cx = Cell::empty_context();
546
547 let run_on_uninit = |msg| {
548 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE)
549 .receive_in_msg(msg)
550 .inspect_err(|e| println!("{e}"))
551 .unwrap_err();
552 };
553
554 run_on_uninit(CellBuilder::build_from(0xdeafbeafu32)?);
556
557 run_on_uninit({
559 let mut b = CellBuilder::new();
560 MsgInfo::Int(IntMsgInfo {
562 dst: STUB_ADDR.into(),
563 ..Default::default()
564 })
565 .store_into(&mut b, cx)?;
566
567 b.store_bit_one()?;
569 b.store_bit_one()?;
570 b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
571
572 b.store_bit_zero()?;
574
575 b.build()?
577 });
578
579 run_on_uninit({
581 let mut b = CellBuilder::new();
582 MsgInfo::Int(IntMsgInfo {
584 dst: STUB_ADDR.into(),
585 ..Default::default()
586 })
587 .store_into(&mut b, cx)?;
588
589 b.store_bit_one()?;
591 b.store_bit_one()?;
592 b.store_reference({
593 let mut b = CellBuilder::new();
594 StateInit::default().store_into(&mut b, cx)?;
595 b.store_u32(0xdeafbeaf)?;
596 b.build()?
597 })?;
598
599 b.store_bit_zero()?;
601
602 b.build()?
604 });
605
606 run_on_uninit({
608 let mut b = CellBuilder::new();
609 MsgInfo::Int(IntMsgInfo {
611 dst: STUB_ADDR.into(),
612 ..Default::default()
613 })
614 .store_into(&mut b, cx)?;
615
616 b.store_bit_zero()?;
618
619 b.store_bit_one()?;
621 b.store_reference(Cell::empty_cell())?;
622 b.store_u32(0xdeafbeaf)?;
623
624 b.build()?
626 });
627
628 Ok(())
629 }
630
631 #[test]
632 #[cfg_attr(miri, ignore)]
633 fn msg_out_of_limits() {
634 let params = make_default_params();
635 let config = make_default_config();
636
637 let body = make_big_tree(8, &mut 0, config.size_limits.max_msg_cells as u16 + 10);
638
639 ExecutorState::new_uninit(¶ms, &config, &STUB_ADDR, OK_BALANCE)
640 .receive_in_msg(make_message(
641 ExtInMsgInfo {
642 dst: STUB_ADDR.into(),
643 ..Default::default()
644 },
645 None,
646 Some({
647 let mut b = CellBuilder::new();
648 b.store_slice(body.as_slice_allow_exotic()).unwrap();
649 b
650 }),
651 ))
652 .inspect_err(|e| println!("{e}"))
653 .unwrap_err();
654 }
655}