tycho_block_util/message/
ext_msg_repr.rs

1use std::cell::RefCell;
2use std::sync::OnceLock;
3
4use tycho_types::cell::CellTreeStats;
5use tycho_types::error::Error;
6use tycho_types::models::{ExtInMsgInfo, IntAddr, MsgType, StateInit};
7use tycho_types::prelude::*;
8use tycho_util::FastHashMap;
9
10/// Should run `ExtMsgRepr::set_allowed_workchains()` with expected workchains ids before usage
11pub async fn validate_external_message(body: &bytes::Bytes) -> Result<(), InvalidExtMsg> {
12    if body.len() > ExtMsgRepr::BOUNDARY_BOC_SIZE {
13        let body = body.clone();
14        // NOTE: Drop `Cell` inside rayon to not block the executor thread.
15        tycho_util::sync::rayon_run_fifo(move || ExtMsgRepr::validate(&body).map(|_| ())).await
16    } else {
17        ExtMsgRepr::validate(body).map(|_| ())
18    }
19}
20
21/// Should run `ExtMsgRepr::set_allowed_workchains()` with expected workchains ids before usage
22pub async fn parse_external_message(body: &bytes::Bytes) -> Result<Cell, InvalidExtMsg> {
23    if body.len() > ExtMsgRepr::BOUNDARY_BOC_SIZE {
24        let body = body.clone();
25        tycho_util::sync::rayon_run_fifo(move || ExtMsgRepr::validate(&body)).await
26    } else {
27        ExtMsgRepr::validate(body)
28    }
29}
30
31/// Computes a normalized message.
32///
33/// A normalized message contains only `dst` address and a body as reference.
34pub fn normalize_external_message(cell: &'_ DynCell) -> Result<Cell, Error> {
35    let mut cs = cell.as_slice()?;
36    if MsgType::load_from(&mut cs)? != MsgType::ExtIn {
37        return Err(Error::InvalidData);
38    }
39
40    let info = ExtInMsgInfo::load_from(&mut cs)?;
41
42    // Skip message state init.
43    if cs.load_bit()? {
44        if cs.load_bit()? {
45            // State init as reference.
46            cs.load_reference()?;
47        } else {
48            // Inline state init.
49            StateInit::load_from(&mut cs)?;
50        }
51    }
52
53    // Load message body.
54    let body = if cs.load_bit()? {
55        cs.load_reference_cloned()?
56    } else if cs.is_empty() {
57        Cell::empty_cell()
58    } else {
59        CellBuilder::build_from(cs)?
60    };
61
62    // Rebuild normalized message.
63    build_normalized_external_message(&info.dst, body)
64}
65
66pub fn build_normalized_external_message(dst: &IntAddr, body: Cell) -> Result<Cell, Error> {
67    let cx = Cell::empty_context();
68    let mut b = CellBuilder::new();
69
70    // message$_ -> info:CommonMsgInfo -> ext_in_msg_info$10
71    // message$_ -> info:CommonMsgInfo -> src:MsgAddressExt -> addr_none$00
72    b.store_small_uint(0b1000, 4)?;
73    // message$_ -> info:CommonMsgInfo -> dest:MsgAddressInt
74    dst.store_into(&mut b, cx)?;
75    // message$_ -> info:CommonMsgInfo -> import_fee:Grams -> 0
76    // message$_ -> init:(Maybe (Either StateInit ^StateInit)) -> nothing$0
77    // message$_ -> body:(Either X ^X) -> right$1
78    b.store_small_uint(0b000001, 6)?;
79    b.store_reference(body)?;
80
81    b.build_ext(cx)
82}
83
84static ALLOWED_WORKCHAINS: OnceLock<Vec<i8>> = OnceLock::new();
85
86pub struct ExtMsgRepr;
87
88impl ExtMsgRepr {
89    pub const MAX_BOC_SIZE: usize = 65535;
90    pub const MAX_REPR_DEPTH: u16 = 512;
91    pub const MAX_ALLOWED_MERKLE_DEPTH: u8 = 2;
92    pub const MAX_MSG_BITS: u64 = 1 << 21;
93    pub const MAX_MSG_CELLS: u64 = 1 << 13;
94    pub const BOUNDARY_BOC_SIZE: usize = 1 << 12;
95
96    pub fn set_allowed_workchains(workchains: Vec<i8>) -> anyhow::Result<()> {
97        anyhow::ensure!(
98            ALLOWED_WORKCHAINS.set(workchains).is_ok(),
99            "should run `set_allowed_workchains()` only once",
100        );
101        Ok(())
102    }
103    fn allowed_workchains() -> Option<&'static Vec<i8>> {
104        ALLOWED_WORKCHAINS.get()
105    }
106
107    // === General methods ===
108
109    /// Should run `ExtMsgRepr::set_allowed_workchains()` with expected workchains ids before usage
110    pub fn validate<T: AsRef<[u8]>>(bytes: T) -> Result<Cell, InvalidExtMsg> {
111        // Apply limits to the encoded BOC.
112        if bytes.as_ref().len() > Self::MAX_BOC_SIZE {
113            return Err(InvalidExtMsg::BocSizeExceeded);
114        }
115
116        // Decode BOC.
117        let msg_root = Self::boc_decode_with_limit(bytes.as_ref(), Self::MAX_MSG_CELLS)?;
118
119        // Cell must not contain any suspicious pruned branches not wrapped into merkle stuff.
120        if msg_root.level() != 0 {
121            return Err(InvalidExtMsg::TooBigLevel);
122        }
123
124        // Apply limits to the cell depth.
125        if msg_root.repr_depth() > Self::MAX_REPR_DEPTH {
126            return Err(InvalidExtMsg::DepthExceeded);
127        }
128
129        // External message must be an ordinary cell.
130        if msg_root.is_exotic() {
131            return Err(InvalidExtMsg::InvalidMessage(Error::InvalidData));
132        }
133
134        // Start parsing the message (we are sure now that it is an ordinary cell).
135        let mut cs = msg_root.as_slice_allow_exotic();
136
137        'info: {
138            // Only external inbound messages are allowed.
139            if MsgType::load_from(&mut cs)? == MsgType::ExtIn {
140                let info = ExtInMsgInfo::load_from(&mut cs)?;
141                if let IntAddr::Std(std_addr) = &info.dst {
142                    // Only `addr_std` (without anycast) to existing workchains is allowed.
143                    let Some(allowed_workchains) = Self::allowed_workchains() else {
144                        return Err(InvalidExtMsg::AllowedWorkchainsNotInitialized);
145                    };
146                    if allowed_workchains.contains(&std_addr.workchain)
147                        && std_addr.anycast.is_none()
148                    {
149                        break 'info;
150                    }
151                }
152            }
153
154            // All other cases are considered garbage.
155            return Err(InvalidExtMsg::InvalidMessage(Error::InvalidData));
156        }
157
158        // Check limits with the remaining slice.
159        if !MsgStorageStat::check_slice(&cs, Self::MAX_ALLOWED_MERKLE_DEPTH, CellTreeStats {
160            bit_count: Self::MAX_MSG_BITS,
161            cell_count: Self::MAX_MSG_CELLS,
162        }) {
163            return Err(InvalidExtMsg::MsgSizeExceeded);
164        }
165
166        // Process message state init.
167        if cs.load_bit()? {
168            if cs.load_bit()? {
169                // State init as reference.
170                cs.load_reference().and_then(|c| {
171                    let mut cs = c.as_slice()?;
172                    StateInit::load_from(&mut cs)?;
173
174                    // State init cell must not contain anything else.
175                    if cs.is_empty() {
176                        Ok(())
177                    } else {
178                        Err(Error::InvalidData)
179                    }
180                })?;
181            } else {
182                // Inline state init.
183                StateInit::load_from(&mut cs)?;
184            }
185        }
186
187        // Process message body.
188        if cs.load_bit()? {
189            // Message must not contain anything other than body as cell.
190            if !cs.is_data_empty() || cs.size_refs() != 1 {
191                return Err(InvalidExtMsg::InvalidMessage(Error::InvalidData));
192            }
193        }
194
195        Ok(msg_root)
196    }
197
198    fn boc_decode_with_limit(data: &[u8], max_cells: u64) -> Result<Cell, InvalidExtMsg> {
199        use tycho_types::boc::de::{self, Options};
200
201        let header = tycho_types::boc::de::BocHeader::decode(data, &Options {
202            max_roots: Some(1),
203            min_roots: Some(1),
204        })?;
205
206        // Optimistic check based on just cell data ranges.
207        if header.cells().len() as u64 > max_cells {
208            return Err(InvalidExtMsg::MsgSizeExceeded);
209        }
210
211        if let Some(&root) = header.roots().first() {
212            let cells = header.finalize(Cell::empty_context())?;
213            if let Some(root) = cells.get(root) {
214                return Ok(root);
215            }
216        }
217
218        Err(InvalidExtMsg::BocError(de::Error::RootCellNotFound))
219    }
220}
221
222#[derive(Debug, thiserror::Error)]
223pub enum InvalidExtMsg {
224    #[error("BOC size exceeds maximum allowed size")]
225    BocSizeExceeded,
226    #[error("invalid message BOC")]
227    BocError(#[from] tycho_types::boc::de::Error),
228    #[error("too big root cell level")]
229    TooBigLevel,
230    #[error("max cell repr depth exceeded")]
231    DepthExceeded,
232    #[error("invalid message")]
233    InvalidMessage(#[from] Error),
234    #[error("message size limits exceeded")]
235    MsgSizeExceeded,
236    #[error(
237        "first should run `ExtMsgRepr::set_allowed_workchains()` to set up expected workchains ids"
238    )]
239    AllowedWorkchainsNotInitialized,
240}
241
242pub struct MsgStorageStat<'a> {
243    visited: &'a mut FastHashMap<&'static HashBytes, u8>,
244    limits: CellTreeStats,
245    max_merkle_depth: u8,
246    cells: u64,
247    bits: u64,
248}
249
250impl<'a> MsgStorageStat<'a> {
251    thread_local! {
252        /// Storage to reuse for parsing messages.
253        static VISITED_CELLS: RefCell<FastHashMap<&'static HashBytes, u8>> = RefCell::new(
254            FastHashMap::with_capacity_and_hasher(128, Default::default()),
255        );
256    }
257
258    pub fn check_slice<'c: 'a>(
259        cs: &CellSlice<'c>,
260        max_merkle_depth: u8,
261        limits: CellTreeStats,
262    ) -> bool {
263        MsgStorageStat::VISITED_CELLS.with_borrow_mut(|visited| {
264            // SAFETY: We are clearing the `visited` map right after the call.
265            let res =
266                unsafe { MsgStorageStat::check_slice_impl(visited, cs, max_merkle_depth, limits) };
267            visited.clear();
268            res
269        })
270    }
271
272    /// # Safety
273    ///
274    /// The following must be true:
275    /// - `visited` must be empty;
276    /// - `visited` must be cleared right after this call.
277    unsafe fn check_slice_impl(
278        visited: &'a mut FastHashMap<&'static HashBytes, u8>,
279        cs: &CellSlice<'_>,
280        max_merkle_depth: u8,
281        limits: CellTreeStats,
282    ) -> bool {
283        debug_assert!(visited.is_empty());
284
285        let mut state = Self {
286            visited,
287            limits,
288            max_merkle_depth,
289            cells: 1,
290            bits: cs.size_bits() as u64,
291        };
292
293        for cell in cs.references() {
294            if unsafe { state.add_cell(cell) }.is_none() {
295                return false;
296            }
297        }
298
299        true
300    }
301
302    unsafe fn add_cell(&mut self, cell: &DynCell) -> Option<u8> {
303        if let Some(merkle_depth) = self.visited.get(cell.repr_hash()) {
304            return Some(*merkle_depth);
305        }
306
307        self.cells = self.cells.checked_add(1)?;
308        self.bits = self.bits.checked_add(cell.bit_len() as u64)?;
309
310        if self.cells > self.limits.cell_count || self.bits > self.limits.bit_count {
311            return None;
312        }
313
314        let mut max_merkle_depth = 0u8;
315        for cell in cell.references() {
316            max_merkle_depth = std::cmp::max(unsafe { self.add_cell(cell)? }, max_merkle_depth);
317        }
318        max_merkle_depth = max_merkle_depth.saturating_add(cell.cell_type().is_merkle() as u8);
319
320        // SAFETY: `visited` must be cleared before dropping the original cell.
321        self.visited.insert(
322            unsafe { std::mem::transmute::<&HashBytes, &'static HashBytes>(cell.repr_hash()) },
323            max_merkle_depth,
324        );
325
326        (max_merkle_depth <= self.max_merkle_depth).then_some(max_merkle_depth)
327    }
328}
329
330#[cfg(test)]
331mod test {
332    use tycho_types::error::Error;
333    use tycho_types::merkle::MerkleProof;
334    use tycho_types::models::{ExtOutMsgInfo, IntMsgInfo, MessageLayout, MsgInfo, OwnedMessage};
335
336    use super::*;
337    use crate::block::AlwaysInclude;
338
339    #[test]
340    fn fits_into_limits() -> anyhow::Result<()> {
341        let _ = ExtMsgRepr::set_allowed_workchains(vec![-1, 0]);
342
343        #[track_caller]
344        fn unwrap_msg(cell: Cell) {
345            let boc = Boc::encode(cell);
346            ExtMsgRepr::validate(boc).unwrap();
347        }
348
349        // Simple message.
350        unwrap_msg(CellBuilder::build_from(OwnedMessage {
351            info: MsgInfo::ExtIn(Default::default()),
352            init: None,
353            body: Default::default(),
354            layout: None,
355        })?);
356
357        // Big message.
358        unwrap_msg({
359            let mut count = 0;
360            let body = make_big_tree(8, &mut count, ExtMsgRepr::MAX_MSG_CELLS as u16 - 100);
361            println!("{count}");
362
363            CellBuilder::build_from(OwnedMessage {
364                info: MsgInfo::ExtIn(Default::default()),
365                init: None,
366                body: body.into(),
367                layout: None,
368            })?
369        });
370
371        // Close enough merkle depth.
372        unwrap_msg({
373            let leaf_proof = MerkleProof::create(Cell::empty_cell_ref(), AlwaysInclude)
374                .build()
375                .and_then(CellBuilder::build_from)?;
376
377            let body = MerkleProof::create(leaf_proof.as_ref(), AlwaysInclude)
378                .build()
379                .and_then(CellBuilder::build_from)?;
380
381            CellBuilder::build_from(OwnedMessage {
382                info: MsgInfo::ExtIn(Default::default()),
383                init: None,
384                body: body.into(),
385                layout: Some(MessageLayout {
386                    body_to_cell: true,
387                    init_to_cell: false,
388                }),
389            })?
390        });
391
392        Ok(())
393    }
394
395    #[test]
396    fn dont_fit_into_limits() -> anyhow::Result<()> {
397        let _ = ExtMsgRepr::set_allowed_workchains(vec![-1, 0]);
398
399        #[track_caller]
400        fn expect_err(cell: Cell) -> InvalidExtMsg {
401            let boc = Boc::encode(cell);
402            ExtMsgRepr::validate(boc).unwrap_err()
403        }
404
405        // Garbage.
406        assert!(matches!(
407            expect_err(Cell::empty_cell()),
408            InvalidExtMsg::InvalidMessage(Error::CellUnderflow)
409        ));
410
411        // Exotic cells.
412        assert!(matches!(
413            expect_err(CellBuilder::build_from(MerkleProof::default())?),
414            InvalidExtMsg::InvalidMessage(Error::InvalidData)
415        ));
416
417        // Too deep cells tree.
418        {
419            let mut cell = Cell::default();
420            for _ in 0..520 {
421                cell = CellBuilder::build_from(cell)?;
422            }
423            assert!(matches!(expect_err(cell), InvalidExtMsg::DepthExceeded));
424        }
425
426        // Non-external message.
427        {
428            let cell = CellBuilder::build_from(OwnedMessage {
429                info: MsgInfo::Int(IntMsgInfo::default()),
430                init: None,
431                body: Default::default(),
432                layout: None,
433            })?;
434            assert!(matches!(
435                expect_err(cell),
436                InvalidExtMsg::InvalidMessage(Error::InvalidData)
437            ));
438
439            let cell = CellBuilder::build_from(OwnedMessage {
440                info: MsgInfo::ExtOut(ExtOutMsgInfo::default()),
441                init: None,
442                body: Default::default(),
443                layout: None,
444            })?;
445            assert!(matches!(
446                expect_err(cell),
447                InvalidExtMsg::InvalidMessage(Error::InvalidData)
448            ));
449        }
450
451        // External message with extra data.
452        {
453            let mut b = CellBuilder::new();
454            OwnedMessage {
455                info: MsgInfo::ExtOut(ExtOutMsgInfo::default()),
456                init: None,
457                body: Default::default(),
458                layout: Some(MessageLayout {
459                    body_to_cell: true,
460                    init_to_cell: false,
461                }),
462            }
463            .store_into(&mut b, Cell::empty_context())?;
464
465            // Bits
466            assert!(matches!(
467                expect_err({
468                    let mut b = b.clone();
469                    b.store_u16(123)?;
470                    b.build()?
471                }),
472                InvalidExtMsg::InvalidMessage(Error::InvalidData)
473            ));
474
475            // Refs
476            assert!(matches!(
477                expect_err({
478                    let mut b = b.clone();
479                    b.store_reference(Cell::empty_cell())?;
480                    b.build()?
481                }),
482                InvalidExtMsg::InvalidMessage(Error::InvalidData)
483            ));
484
485            // Both
486            assert!(matches!(
487                expect_err({
488                    let mut b = b.clone();
489                    b.store_u16(123)?;
490                    b.store_reference(Cell::empty_cell())?;
491                    b.build()?
492                }),
493                InvalidExtMsg::InvalidMessage(Error::InvalidData)
494            ));
495        }
496
497        // Too big message.
498        {
499            let cell = exceed_big_message()?;
500            assert!(matches!(expect_err(cell), InvalidExtMsg::MsgSizeExceeded));
501        }
502
503        // Too big merkle depth.
504        {
505            let cell = create_deep_merkle()?;
506            assert!(matches!(expect_err(cell), InvalidExtMsg::MsgSizeExceeded));
507        }
508
509        Ok(())
510    }
511
512    fn exceed_big_message() -> anyhow::Result<Cell> {
513        let mut count = 0;
514        let body = make_big_tree(8, &mut count, ExtMsgRepr::MAX_MSG_CELLS as u16 + 100);
515
516        let cell = CellBuilder::build_from(OwnedMessage {
517            info: MsgInfo::ExtIn(Default::default()),
518            init: None,
519            body: body.into(),
520            layout: None,
521        })?;
522
523        Ok(cell)
524    }
525
526    fn create_deep_merkle() -> anyhow::Result<Cell> {
527        let leaf_proof = MerkleProof::create(Cell::empty_cell_ref(), AlwaysInclude)
528            .build()
529            .and_then(CellBuilder::build_from)?;
530
531        let inner_proof = MerkleProof::create(leaf_proof.as_ref(), AlwaysInclude)
532            .build()
533            .and_then(CellBuilder::build_from)?;
534
535        let body = MerkleProof::create(inner_proof.as_ref(), AlwaysInclude)
536            .build()
537            .and_then(CellBuilder::build_from)?;
538
539        let cell = CellBuilder::build_from(OwnedMessage {
540            info: MsgInfo::ExtIn(Default::default()),
541            init: None,
542            body: body.into(),
543            layout: Some(MessageLayout {
544                body_to_cell: true,
545                init_to_cell: false,
546            }),
547        })?;
548
549        Ok(cell)
550    }
551
552    fn make_big_tree(depth: u8, count: &mut u16, target: u16) -> Cell {
553        *count += 1;
554
555        if depth == 0 {
556            CellBuilder::build_from(*count).unwrap()
557        } else {
558            let mut b = CellBuilder::new();
559            for _ in 0..4 {
560                if *count < target {
561                    b.store_reference(make_big_tree(depth - 1, count, target))
562                        .unwrap();
563                }
564            }
565            b.build().unwrap()
566        }
567    }
568}