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                self.check_message_dst(&info.dst)?;
54
55                // Update flags.
56                is_external = true;
57                bounce_enabled = false;
58
59                // Compute forwarding fees.
60                let Some(mut stats) =
61                    ExtStorageStat::compute_for_slice(&slice, StorageStatLimits {
62                        bit_count: self.config.size_limits.max_msg_bits,
63                        cell_count: self.config.size_limits.max_msg_cells,
64                    })
65                else {
66                    anyhow::bail!("inbound message limits exceeded");
67                };
68
69                stats.cell_count -= 1; // root cell is ignored.
70                stats.bit_count -= slice.size_bits() as u64; // bits in the root cells are free.
71
72                let fwd_fee = if self.is_special {
73                    // Importing external messages on special accounts is free.
74                    // NOTE: We still need to compute and check `ExtStorageStat`.
75                    Tokens::ZERO
76                } else {
77                    self.config
78                        .fwd_prices(is_masterchain)
79                        .compute_fwd_fee(stats)
80                };
81
82                // Deduct fees.
83                if self.balance.tokens < fwd_fee {
84                    anyhow::bail!("cannot pay for importing an external message");
85                }
86                self.balance.tokens -= fwd_fee;
87                self.total_fees.try_add_assign(fwd_fee)?;
88
89                // External message cannot carry value.
90                msg_balance_remaining = CurrencyCollection::ZERO;
91            }
92            // Reject all other message types.
93            MsgInfo::ExtOut(_) => anyhow::bail!("unexpected incoming ExtOut message"),
94        }
95
96        // Process message state init.
97        let init = if slice.load_bit()? {
98            Some(if slice.load_bit()? {
99                // State init as reference.
100                let state_root = slice.load_reference_cloned()?;
101                anyhow::ensure!(
102                    !state_root.is_exotic(),
103                    "state init must be an ordinary cell"
104                );
105
106                let mut slice = state_root.as_slice_allow_exotic();
107                let parsed = StateInit::load_from(&mut slice)?;
108                anyhow::ensure!(slice.is_empty(), "state init contains extra data");
109
110                MsgStateInit {
111                    root: state_root,
112                    parsed,
113                }
114            } else {
115                // Inline state init.
116                let mut state_init_cs = slice;
117
118                // Read StateInit.
119                let parsed = StateInit::load_from(&mut slice)?;
120                // Rebuild it as cell to get hash.
121                state_init_cs.skip_last(slice.size_bits(), slice.size_refs())?;
122                let state_root = CellBuilder::build_from(state_init_cs)?;
123
124                MsgStateInit {
125                    root: state_root,
126                    parsed,
127                }
128            })
129        } else {
130            None
131        };
132
133        // Process message body.
134        let body = if slice.load_bit()? {
135            // Body as cell.
136            let body_cell = slice.load_reference_cloned()?;
137            anyhow::ensure!(slice.is_empty(), "message contains extra data");
138
139            CellSliceParts::from(body_cell)
140        } else {
141            // Inline body.
142            (slice.range(), msg_root.clone())
143        };
144
145        // Handle messages to the blackhole.
146        if self.config.is_blackhole(&self.address) {
147            self.burned = msg_balance_remaining.tokens;
148            msg_balance_remaining.tokens = Tokens::ZERO;
149        }
150
151        // Done
152        Ok(ReceivedMessage {
153            root: msg_root,
154            init,
155            body,
156            is_external,
157            bounce_enabled,
158            balance_remaining: msg_balance_remaining,
159        })
160    }
161
162    fn check_message_dst(&self, dst: &IntAddr) -> Result<()> {
163        match dst {
164            IntAddr::Std(dst) => {
165                anyhow::ensure!(dst.anycast.is_none(), "anycast is not supported");
166                anyhow::ensure!(*dst == self.address, "message destination address mismatch");
167                Ok(())
168            }
169            IntAddr::Var(_) => anyhow::bail!("`addr_var` is not supported"),
170        }
171    }
172}
173
174/// Parsed inbound message.
175#[derive(Debug, Clone)]
176pub struct ReceivedMessage {
177    /// Message root cell.
178    pub root: Cell,
179    /// Parsed message [`StateInit`].
180    pub init: Option<MsgStateInit>,
181    /// Message body.
182    pub body: CellSliceParts,
183
184    /// Whether this message is an `ExtIn`.
185    pub is_external: bool,
186    /// Whether this message can be bounced back on error.
187    pub bounce_enabled: bool,
188
189    /// The remaining attached value of the received message.
190    /// NOTE: Always zero for external messages.
191    pub balance_remaining: CurrencyCollection,
192}
193
194/// Message state init.
195#[derive(Debug, Clone)]
196pub struct MsgStateInit {
197    /// Serialized [`StateInit`].
198    pub root: Cell,
199    /// Parsed [`StateInit`].
200    pub parsed: StateInit,
201}
202
203impl MsgStateInit {
204    pub fn root_hash(&self) -> &HashBytes {
205        self.root.repr_hash()
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use tycho_types::models::{BurningConfig, ExtInMsgInfo, ExtOutMsgInfo, IntMsgInfo, StdAddr};
212    use tycho_types::num::Tokens;
213
214    use super::*;
215    use crate::tests::{
216        make_big_tree, make_custom_config, make_default_config, make_default_params, make_message,
217    };
218
219    const OK_BALANCE: Tokens = Tokens::new(10_000_000_000);
220    const STUB_ADDR: StdAddr = StdAddr::new(0, HashBytes::ZERO);
221
222    // === Positive ===
223
224    #[test]
225    fn receive_ext_in_works() {
226        let params = make_default_params();
227        let config = make_default_config();
228
229        let mut state = ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE);
230        let prev_start_lt = state.start_lt;
231        let prev_end_lt = state.end_lt;
232        let prev_balance = state.balance.clone();
233        let prev_acc_state = state.state.clone();
234
235        let msg_root = make_message(
236            ExtInMsgInfo {
237                dst: STUB_ADDR.into(),
238                ..Default::default()
239            },
240            Some(StateInit::default()),
241            None,
242        );
243        let msg = state.receive_in_msg(msg_root.clone()).unwrap();
244        // Received message must be parsed correctly.
245        assert!(msg.is_external);
246        assert!(!msg.bounce_enabled);
247        {
248            let init = msg.init.unwrap();
249            let target = StateInit::default();
250            assert_eq!(init.parsed, target);
251            let target_hash = *CellBuilder::build_from(&target).unwrap().repr_hash();
252            assert_eq!(init.root_hash(), &target_hash);
253        }
254        assert!(msg.body.0.is_empty());
255        assert_eq!(msg.balance_remaining, CurrencyCollection::ZERO);
256
257        // LT must not change.
258        assert_eq!(state.start_lt, prev_start_lt);
259        assert_eq!(state.end_lt, prev_end_lt);
260        // This simple external message has no child cells,
261        // so it consumes only the fixed amount for fwd_fee.
262        assert_eq!(
263            state.total_fees,
264            Tokens::new(config.fwd_prices.lump_price as _)
265        );
266        // Extra currencies must not change.
267        assert_eq!(state.balance.other, prev_balance.other);
268        // Forward fee must be withdrawn from the account balance.
269        assert_eq!(state.balance.tokens, prev_balance.tokens - state.total_fees);
270        // Account state must not change.
271        assert_eq!(state.state, prev_acc_state);
272    }
273
274    #[test]
275    fn receive_int_to_non_existent() {
276        let params = make_default_params();
277        let config = make_default_config();
278
279        let mut state = ExecutorState::new_non_existent(&params, &config, &STUB_ADDR);
280        let prev_start_lt = state.start_lt;
281        assert_eq!(prev_start_lt, 0);
282        let prev_balance = state.balance.clone();
283        let prev_acc_state = state.state.clone();
284
285        let msg_lt = 1000;
286        let msg_root = make_message(
287            IntMsgInfo {
288                dst: STUB_ADDR.into(),
289                value: OK_BALANCE.into(),
290                bounce: true,
291                created_lt: msg_lt,
292                ..Default::default()
293            },
294            None,
295            Some({
296                let mut b = CellBuilder::new();
297                b.store_u32(0xdeafbeaf).unwrap();
298                b
299            }),
300        );
301        let msg = state.receive_in_msg(msg_root.clone()).unwrap();
302        // Received message must be parsed correctly.
303        assert!(!msg.is_external);
304        assert!(msg.bounce_enabled);
305        assert!(msg.init.is_none());
306        assert_eq!(
307            CellSlice::apply(&msg.body).unwrap().load_u32().unwrap(),
308            0xdeafbeaf
309        );
310        assert_eq!(msg.balance_remaining, OK_BALANCE.into());
311
312        // LT must change to the message LT.
313        assert_eq!(state.start_lt, msg_lt + 1);
314        assert_eq!(state.end_lt, state.start_lt + 1);
315        // Internal message dous not require any fwd_fee on receive.
316        assert_eq!(state.total_fees, Tokens::ZERO);
317        // Balance must not change (it will change on a credit phase).
318        assert_eq!(state.balance, prev_balance);
319        // Account state must not change.
320        assert_eq!(state.state, prev_acc_state);
321    }
322
323    #[test]
324    fn receive_int_to_blackhole() {
325        let addr = StdAddr::new(-1, HashBytes::ZERO);
326
327        let params = make_default_params();
328        let config = make_custom_config(|config| {
329            config.set_burning_config(&BurningConfig {
330                blackhole_addr: Some(addr.address),
331                ..Default::default()
332            })?;
333            Ok(())
334        });
335
336        let mut state = ExecutorState::new_uninit(&params, &config, &addr, OK_BALANCE);
337        let prev_start_lt = state.start_lt;
338        assert_eq!(prev_start_lt, 0);
339        let prev_balance = state.balance.clone();
340        let prev_acc_state = state.state.clone();
341
342        let msg_lt = 1000;
343        let msg_root = make_message(
344            IntMsgInfo {
345                dst: addr.into(),
346                value: OK_BALANCE.into(),
347                bounce: true,
348                created_lt: msg_lt,
349                ..Default::default()
350            },
351            None,
352            None,
353        );
354        let msg = state.receive_in_msg(msg_root).unwrap();
355        // Received message must be parsed correctly.
356        assert!(!msg.is_external);
357        assert!(msg.bounce_enabled);
358        assert!(msg.init.is_none());
359        assert!(msg.body.0.is_empty());
360        assert_eq!(msg.balance_remaining, CurrencyCollection::ZERO);
361
362        // LT must change to the message LT.
363        assert_eq!(state.start_lt, msg_lt + 1);
364        assert_eq!(state.end_lt, state.start_lt + 1);
365        // Internal message dous not require any fwd_fee on receive.
366        assert_eq!(state.total_fees, Tokens::ZERO);
367        // Balance must not change (it will change on a credit phase).
368        assert_eq!(state.balance, prev_balance);
369        // Account state must not change.
370        assert_eq!(state.state, prev_acc_state);
371        // Burned tokens must increase.
372        assert_eq!(state.burned, OK_BALANCE);
373    }
374
375    // === Negative ===
376
377    #[test]
378    fn receive_ext_out() {
379        let params = make_default_params();
380        let config = make_default_config();
381
382        ExecutorState::new_non_existent(&params, &config, &STUB_ADDR)
383            .receive_in_msg(make_message(
384                ExtOutMsgInfo {
385                    dst: None,
386                    src: STUB_ADDR.into(),
387                    created_at: 1,
388                    created_lt: 1,
389                },
390                None,
391                None,
392            ))
393            .inspect_err(|e| println!("{e}"))
394            .unwrap_err();
395    }
396
397    #[test]
398    fn receive_ext_in_on_non_existent() {
399        let params = make_default_params();
400        let config = make_default_config();
401
402        ExecutorState::new_non_existent(&params, &config, &STUB_ADDR)
403            .receive_in_msg(make_message(
404                ExtInMsgInfo {
405                    dst: STUB_ADDR.into(),
406                    ..Default::default()
407                },
408                None,
409                None,
410            ))
411            .inspect_err(|e| println!("{e}"))
412            .unwrap_err();
413    }
414
415    #[test]
416    fn receive_ext_in_not_enough_balance() {
417        let params = make_default_params();
418        let config = make_default_config();
419
420        ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::new(1))
421            .receive_in_msg(make_message(
422                ExtInMsgInfo {
423                    dst: STUB_ADDR.into(),
424                    ..Default::default()
425                },
426                None,
427                None,
428            ))
429            .inspect_err(|e| println!("{e}"))
430            .unwrap_err();
431    }
432
433    #[test]
434    fn receive_internal_balance_overflow() {
435        let params = make_default_params();
436        let config = make_default_config();
437
438        ExecutorState::new_uninit(&params, &config, &STUB_ADDR, Tokens::MAX)
439            .receive_in_msg(make_message(
440                IntMsgInfo {
441                    dst: STUB_ADDR.into(),
442                    value: Tokens::MAX.into(),
443                    ihr_fee: Tokens::MAX,
444                    ..Default::default()
445                },
446                None,
447                None,
448            ))
449            .inspect_err(|e| println!("{e}"))
450            .unwrap_err();
451    }
452
453    #[test]
454    fn receive_with_invalid_dst() {
455        let params = make_default_params();
456        let config = make_default_config();
457
458        let other_addr = StdAddr::new(0, HashBytes([1; 32]));
459
460        // External
461        ExecutorState::new_non_existent(&params, &config, &STUB_ADDR)
462            .receive_in_msg(make_message(
463                ExtInMsgInfo {
464                    dst: other_addr.clone().into(),
465                    ..Default::default()
466                },
467                None,
468                None,
469            ))
470            .inspect_err(|e| println!("{e}"))
471            .unwrap_err();
472
473        // Internal
474        ExecutorState::new_non_existent(&params, &config, &STUB_ADDR)
475            .receive_in_msg(make_message(
476                IntMsgInfo {
477                    dst: other_addr.clone().into(),
478                    ..Default::default()
479                },
480                None,
481                None,
482            ))
483            .inspect_err(|e| println!("{e}"))
484            .unwrap_err();
485    }
486
487    #[test]
488    fn invalid_message_structure() -> anyhow::Result<()> {
489        let params = make_default_params();
490        let config = make_default_config();
491        let cx = Cell::empty_context();
492
493        let run_on_uninit = |msg| {
494            ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE)
495                .receive_in_msg(msg)
496                .inspect_err(|e| println!("{e}"))
497                .unwrap_err();
498        };
499
500        // Invalid msg info
501        run_on_uninit(CellBuilder::build_from(0xdeafbeafu32)?);
502
503        // Invalid state init.
504        run_on_uninit({
505            let mut b = CellBuilder::new();
506            // MsgInfo
507            MsgInfo::Int(IntMsgInfo {
508                dst: STUB_ADDR.into(),
509                ..Default::default()
510            })
511            .store_into(&mut b, cx)?;
512
513            // just$1 (right$1 ^StateInit)
514            b.store_bit_one()?;
515            b.store_bit_one()?;
516            b.store_reference(CellBuilder::build_from(0xdeafbeafu32)?)?;
517
518            // left$0 X
519            b.store_bit_zero()?;
520
521            //
522            b.build()?
523        });
524
525        // State init with extra data.
526        run_on_uninit({
527            let mut b = CellBuilder::new();
528            // MsgInfo
529            MsgInfo::Int(IntMsgInfo {
530                dst: STUB_ADDR.into(),
531                ..Default::default()
532            })
533            .store_into(&mut b, cx)?;
534
535            // just$1 (right$1 ^StateInit)
536            b.store_bit_one()?;
537            b.store_bit_one()?;
538            b.store_reference({
539                let mut b = CellBuilder::new();
540                StateInit::default().store_into(&mut b, cx)?;
541                b.store_u32(0xdeafbeaf)?;
542                b.build()?
543            })?;
544
545            // left$0 X
546            b.store_bit_zero()?;
547
548            //
549            b.build()?
550        });
551
552        // Body with extra data.
553        run_on_uninit({
554            let mut b = CellBuilder::new();
555            // MsgInfo
556            MsgInfo::Int(IntMsgInfo {
557                dst: STUB_ADDR.into(),
558                ..Default::default()
559            })
560            .store_into(&mut b, cx)?;
561
562            // nont$0
563            b.store_bit_zero()?;
564
565            // left$1 ^X
566            b.store_bit_one()?;
567            b.store_reference(Cell::empty_cell())?;
568            b.store_u32(0xdeafbeaf)?;
569
570            //
571            b.build()?
572        });
573
574        Ok(())
575    }
576
577    #[test]
578    #[cfg_attr(miri, ignore)]
579    fn msg_out_of_limits() {
580        let params = make_default_params();
581        let config = make_default_config();
582
583        let body = make_big_tree(8, &mut 0, config.size_limits.max_msg_cells as u16 + 10);
584
585        ExecutorState::new_uninit(&params, &config, &STUB_ADDR, OK_BALANCE)
586            .receive_in_msg(make_message(
587                ExtInMsgInfo {
588                    dst: STUB_ADDR.into(),
589                    ..Default::default()
590                },
591                None,
592                Some({
593                    let mut b = CellBuilder::new();
594                    b.store_slice(body.as_slice_allow_exotic()).unwrap();
595                    b
596                }),
597            ))
598            .inspect_err(|e| println!("{e}"))
599            .unwrap_err();
600    }
601}