Skip to main content

tycho_executor/phase/
receive.rs

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    /// "Pre" phase of ordinary transactions.
11    ///
12    /// - Validates the inbound message cell;
13    /// - For internal messages updates an LT range;
14    /// - For external messages charges a fwd fee
15    ///   (updates [`self.balance`] and [`self.total_fees`]).
16    ///
17    /// Returns a parsed received message ([`ReceivedMessage`]).
18    ///
19    /// Fails if the message is invalid or can't be imported.
20    ///
21    /// [`self.balance`]: Self::balance
22    /// [`self.total_fees`]: Self::total_fees
23    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        // Process message header.
31        let mut slice = msg_root.as_slice_allow_exotic();
32        match MsgInfo::load_from(&mut slice)? {
33            // Handle internal message.
34            MsgInfo::Int(info) => {
35                self.check_message_dst(&info.dst)?;
36
37                // Update flags.
38                is_external = false;
39                bounce_enabled = info.bounce;
40
41                // Update message balance
42                msg_balance_remaining = info.value;
43
44                // Adjust LT range.
45                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            // Handle external (in) message.
51            MsgInfo::ExtIn(info) => {
52                if self.is_suspended_by_marks {
53                    // Accounts suspended by authority marks cannot receive
54                    // external messages until they are unsuspended.
55                    anyhow::bail!("account was suspended by authority marks");
56                }
57
58                self.check_message_dst(&info.dst)?;
59
60                // Update flags.
61                is_external = true;
62                bounce_enabled = false;
63
64                // Compute forwarding fees.
65                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; // root cell is ignored.
75                stats.bit_count -= slice.size_bits() as u64; // bits in the root cells are free.
76
77                let fwd_fee = if self.is_special {
78                    // Importing external messages on special accounts is free.
79                    // NOTE: We still need to compute and check `ExtStorageStat`.
80                    Tokens::ZERO
81                } else {
82                    self.config
83                        .fwd_prices(is_masterchain)
84                        .compute_fwd_fee(stats)
85                };
86
87                // Deduct fees.
88                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                // External message cannot carry value.
95                msg_balance_remaining = CurrencyCollection::ZERO;
96            }
97            // Reject all other message types.
98            MsgInfo::ExtOut(_) => anyhow::bail!("unexpected incoming ExtOut message"),
99        }
100
101        // Process message state init.
102        let init = if slice.load_bit()? {
103            Some(if slice.load_bit()? {
104                // State init as reference.
105                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                // Inline state init.
121                let mut state_init_cs = slice;
122
123                // Read StateInit.
124                let parsed = StateInit::load_from(&mut slice)?;
125                // Rebuild it as cell to get hash.
126                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        // Process message body.
139        let body = if slice.load_bit()? {
140            // Body as cell.
141            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            // Inline body.
147            (slice.range(), msg_root.clone())
148        };
149
150        // Handle messages to the blackhole.
151        if self.config.is_blackhole(&self.address) {
152            self.burned = msg_balance_remaining.tokens;
153            msg_balance_remaining.tokens = Tokens::ZERO;
154        }
155
156        // Done
157        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/// Parsed inbound message.
180#[derive(Debug, Clone)]
181pub struct ReceivedMessage {
182    /// Message root cell.
183    pub root: Cell,
184    /// Parsed message [`StateInit`].
185    pub init: Option<MsgStateInit>,
186    /// Message body.
187    pub body: CellSliceParts,
188
189    /// Whether this message is an `ExtIn`.
190    pub is_external: bool,
191    /// Whether this message can be bounced back on error.
192    pub bounce_enabled: bool,
193
194    /// The remaining attached value of the received message.
195    /// NOTE: Always zero for external messages.
196    pub balance_remaining: CurrencyCollection,
197}
198
199/// Message state init.
200#[derive(Debug, Clone)]
201pub struct MsgStateInit {
202    /// Serialized [`StateInit`].
203    pub root: Cell,
204    /// Parsed [`StateInit`].
205    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    // === Positive ===
233
234    #[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(&params, &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        // Received message must be parsed correctly.
255        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        // LT must not change.
268        assert_eq!(state.start_lt, prev_start_lt);
269        assert_eq!(state.end_lt, prev_end_lt);
270        // This simple external message has no child cells,
271        // so it consumes only the fixed amount for fwd_fee.
272        assert_eq!(
273            state.total_fees,
274            Tokens::new(config.fwd_prices.lump_price as _)
275        );
276        // Extra currencies must not change.
277        assert_eq!(state.balance.other, prev_balance.other);
278        // Forward fee must be withdrawn from the account balance.
279        assert_eq!(state.balance.tokens, prev_balance.tokens - state.total_fees);
280        // Account state must not change.
281        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(&params, &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        // Received message must be parsed correctly.
313        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        // LT must change to the message LT.
323        assert_eq!(state.start_lt, msg_lt + 1);
324        assert_eq!(state.end_lt, state.start_lt + 1);
325        // Internal message dous not require any fwd_fee on receive.
326        assert_eq!(state.total_fees, Tokens::ZERO);
327        // Balance must not change (it will change on a credit phase).
328        assert_eq!(state.balance, prev_balance);
329        // Account state must not change.
330        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(&params, &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        // Received message must be parsed correctly.
366        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        // LT must change to the message LT.
373        assert_eq!(state.start_lt, msg_lt + 1);
374        assert_eq!(state.end_lt, state.start_lt + 1);
375        // Internal message dous not require any fwd_fee on receive.
376        assert_eq!(state.total_fees, Tokens::ZERO);
377        // Balance must not change (it will change on a credit phase).
378        assert_eq!(state.balance, prev_balance);
379        // Account state must not change.
380        assert_eq!(state.state, prev_acc_state);
381        // Burned tokens must increase.
382        assert_eq!(state.burned, OK_BALANCE);
383    }
384
385    // === Negative ===
386
387    #[test]
388    fn receive_ext_out() {
389        let params = make_default_params();
390        let config = make_default_config();
391
392        ExecutorState::new_non_existent(&params, &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(&params, &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(&params, &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)), // blocked by black marks
463            ])
464            .try_into()?,
465        };
466
467        ExecutorState::new_uninit(&params, &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(&params, &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        // External
515        ExecutorState::new_non_existent(&params, &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        // Internal
528        ExecutorState::new_non_existent(&params, &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(&params, &config, &STUB_ADDR, OK_BALANCE)
549                .receive_in_msg(msg)
550                .inspect_err(|e| println!("{e}"))
551                .unwrap_err();
552        };
553
554        // Invalid msg info
555        run_on_uninit(CellBuilder::build_from(0xdeafbeafu32)?);
556
557        // Invalid state init.
558        run_on_uninit({
559            let mut b = CellBuilder::new();
560            // MsgInfo
561            MsgInfo::Int(IntMsgInfo {
562                dst: STUB_ADDR.into(),
563                ..Default::default()
564            })
565            .store_into(&mut b, cx)?;
566
567            // just$1 (right$1 ^StateInit)
568            b.store_bit_one()?;
569            b.store_bit_one()?;
570            b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
571
572            // left$0 X
573            b.store_bit_zero()?;
574
575            //
576            b.build()?
577        });
578
579        // State init with extra data.
580        run_on_uninit({
581            let mut b = CellBuilder::new();
582            // MsgInfo
583            MsgInfo::Int(IntMsgInfo {
584                dst: STUB_ADDR.into(),
585                ..Default::default()
586            })
587            .store_into(&mut b, cx)?;
588
589            // just$1 (right$1 ^StateInit)
590            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            // left$0 X
600            b.store_bit_zero()?;
601
602            //
603            b.build()?
604        });
605
606        // Body with extra data.
607        run_on_uninit({
608            let mut b = CellBuilder::new();
609            // MsgInfo
610            MsgInfo::Int(IntMsgInfo {
611                dst: STUB_ADDR.into(),
612                ..Default::default()
613            })
614            .store_into(&mut b, cx)?;
615
616            // nont$0
617            b.store_bit_zero()?;
618
619            // left$1 ^X
620            b.store_bit_one()?;
621            b.store_reference(Cell::empty_cell())?;
622            b.store_u32(0xdeafbeaf)?;
623
624            //
625            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(&params, &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}