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