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