tycho_executor/phase/
bounce.rs

1use anyhow::Result;
2use tycho_types::cell::{Cell, CellBuilder, CellFamily, Lazy, Store};
3use tycho_types::models::{
4    BouncePhase, ExecutedBouncePhase, MsgInfo, NoFundsBouncePhase, StorageUsedShort,
5};
6use tycho_types::num::Tokens;
7
8use crate::ExecutorState;
9use crate::phase::receive::ReceivedMessage;
10use crate::util::{
11    ExtStorageStat, StorageStatLimits, check_rewrite_dst_addr, new_varuint56_truncate,
12};
13
14/// Bounce phase input context.
15pub struct BouncePhaseContext<'a> {
16    /// Gas fees from the compute phase (if any).
17    pub gas_fees: Tokens,
18    /// Action fine from the action phase (if any).
19    pub action_fine: Tokens,
20    /// Received message (internal only).
21    pub received_message: &'a ReceivedMessage,
22}
23
24impl ExecutorState<'_> {
25    /// Bounce phase of ordinary transactions.
26    ///
27    /// - Tries to send an inbound message back to the sender;
28    /// - Defined only for internal inbound messages;
29    /// - Remaining message balance is substracted from the account balance;
30    /// - Fees are paid using the remaining inbound message balance;
31    ///
32    /// Returns an executed [`BouncePhase`].
33    ///
34    /// Fails if the origin workchain of the message doesn't exist or
35    /// disabled. Can also fail on [`total_fees`] overflow, but this should
36    /// not happen on networks with valid value flow.
37    ///
38    /// [`total_fees`]: Self::total_fees
39    pub fn bounce_phase(&mut self, ctx: BouncePhaseContext<'_>) -> Result<BouncePhase> {
40        let mut info = ctx.received_message.root.parse::<MsgInfo>()?;
41        let MsgInfo::Int(int_msg_info) = &mut info else {
42            anyhow::bail!("bounce phase is defined only for internal messages");
43        };
44
45        // Reverse message direction.
46        std::mem::swap(&mut int_msg_info.src, &mut int_msg_info.dst);
47        if !check_rewrite_dst_addr(&self.config.workchains, &mut int_msg_info.dst) {
48            // FIXME: Just ignore this phase in that case? What if we disable
49            // the message origin workchain and this message bounces? However,
50            // for that we should at least have other workchains .
51            anyhow::bail!("invalid destination address in a bounced message");
52        }
53
54        // Compute additional full body cell.
55        let full_body = if self.params.full_body_in_bounced {
56            let (range, cell) = &ctx.received_message.body;
57            Some(if range.is_full(cell) {
58                cell.clone()
59            } else {
60                CellBuilder::build_from(range.apply_allow_exotic(cell))?
61            })
62        } else {
63            None
64        };
65
66        // Overwrite msg balance.
67        let mut msg_value = ctx.received_message.balance_remaining.clone();
68
69        // Authority marks cannot be bounced back.
70        if self.params.authority_marks_enabled
71            && let Some(marks) = &self.config.authority_marks
72        {
73            marks.remove_authority_marks_in(&mut msg_value)?;
74        }
75
76        // Compute message storage stats.
77        let stats = 'stats: {
78            let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
79                bit_count: self.config.size_limits.max_msg_bits,
80                cell_count: self.config.size_limits.max_msg_cells,
81            });
82
83            // Root cell is free, but all children must be accounted.
84            'valid: {
85                // Msg value can contain some cells.
86                if let Some(extra_currencies) = msg_value.other.as_dict().root()
87                    && !stats.add_cell(extra_currencies.as_ref())
88                {
89                    break 'valid;
90                }
91
92                // We must also include a msg body if `params.full_body_in_bounce` is enabled.
93                if let Some(body) = &full_body
94                    && !stats.add_cell(body.as_ref())
95                {
96                    break 'valid;
97                }
98
99                // Exit this block with a valid storage stats info.
100                break 'stats stats.stats();
101            }
102
103            // Fallback to NoFunds if the returned message cannot fit into the limits.
104            // We require an "infinite" amount of tokens here if storage overflows.
105            let stats = stats.stats();
106            return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
107                msg_size: StorageUsedShort {
108                    bits: new_varuint56_truncate(stats.bit_count),
109                    cells: new_varuint56_truncate(stats.cell_count),
110                },
111                req_fwd_fees: Tokens::MAX,
112            }));
113        };
114
115        // Compute forwarding fee.
116        let use_mc_prices = self.address.is_masterchain() || int_msg_info.dst.is_masterchain();
117        let prices = self.config.fwd_prices(use_mc_prices);
118
119        let mut fwd_fees = prices.compute_fwd_fee(stats);
120        let msg_size = StorageUsedShort {
121            cells: new_varuint56_truncate(stats.cell_count),
122            bits: new_varuint56_truncate(stats.bit_count),
123        };
124
125        // Try to substract all fees from the remaining message balance.
126        msg_value.tokens = match msg_value
127            .tokens
128            .checked_sub(ctx.gas_fees)
129            .and_then(|t| t.checked_sub(ctx.action_fine))
130        {
131            Some(msg_balance) if msg_balance >= fwd_fees => msg_balance,
132            msg_balance => {
133                return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
134                    msg_size,
135                    req_fwd_fees: fwd_fees - msg_balance.unwrap_or_default(),
136                }));
137            }
138        };
139
140        // Take message balance back from the account balance.
141        self.balance.try_sub_assign(&msg_value)?;
142
143        // Take forwarding fee from the message balance.
144        msg_value.tokens -= fwd_fees;
145
146        // Split forwarding fee.
147        let msg_fees = prices.get_first_part(fwd_fees);
148        fwd_fees -= msg_fees;
149        self.total_fees.try_add_assign(msg_fees)?;
150
151        // Finalize message.
152        int_msg_info.ihr_disabled = true;
153        int_msg_info.bounce = false;
154        int_msg_info.bounced = true;
155        int_msg_info.value = msg_value;
156        int_msg_info.ihr_fee = Tokens::ZERO;
157        int_msg_info.fwd_fee = fwd_fees;
158        int_msg_info.created_lt = self.end_lt;
159        int_msg_info.created_at = self.params.block_unixtime;
160
161        let msg = {
162            const ROOT_BODY_BITS: u16 = 256;
163            const BOUNCE_SELECTOR: u32 = u32::MAX;
164
165            let body_prefix = {
166                let (range, cell) = &ctx.received_message.body;
167                range.apply_allow_exotic(cell).get_prefix(ROOT_BODY_BITS, 0)
168            };
169
170            let c = Cell::empty_context();
171            let mut b = CellBuilder::new();
172            info.store_into(&mut b, c)?;
173            b.store_bit_zero()?; // init:(Maybe ...) -> nothing$0
174
175            if b.has_capacity(body_prefix.size_bits() + 33, 0) {
176                b.store_bit_zero()?; // body:(Either X ^X) -> left$0 X
177                b.store_u32(BOUNCE_SELECTOR)?;
178                b.store_slice_data(body_prefix)?;
179                if let Some(full_body) = full_body {
180                    b.store_reference(full_body)?;
181                }
182            } else {
183                let child = {
184                    let mut b = CellBuilder::new();
185                    b.store_u32(BOUNCE_SELECTOR)?;
186                    b.store_slice_data(body_prefix)?;
187                    if let Some(full_body) = full_body {
188                        b.store_reference(full_body)?;
189                    }
190                    b.build()?
191                };
192
193                b.store_bit_one()?; // body:(Either X ^X) -> right$1 ^X
194                b.store_reference(child)?
195            }
196
197            // SAFETY: `b` is an ordinary cell.
198            unsafe { Lazy::from_raw_unchecked(b.build()?) }
199        };
200
201        // Add message to output.
202        self.out_msgs.push(msg);
203        self.end_lt += 1;
204
205        // Done
206        Ok(BouncePhase::Executed(ExecutedBouncePhase {
207            msg_size,
208            msg_fees,
209            fwd_fees,
210        }))
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use std::collections::BTreeMap;
217
218    use tycho_types::models::{AuthorityMarksConfig, CurrencyCollection, IntMsgInfo, StdAddr};
219    use tycho_types::num::VarUint248;
220    use tycho_types::prelude::*;
221
222    use super::*;
223    use crate::ExecutorParams;
224    use crate::tests::{
225        make_custom_config, make_default_config, make_default_params, make_message,
226    };
227
228    #[test]
229    fn bounce_with_enough_funds() {
230        let mut params = make_default_params();
231        params.full_body_in_bounced = false;
232
233        let config = make_default_config();
234
235        let src_addr = StdAddr::new(0, HashBytes([0; 32]));
236        let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
237
238        let gas_fees = Tokens::new(100);
239        let action_fine = Tokens::new(200);
240
241        let mut state =
242            ExecutorState::new_uninit(&params, &config, &dst_addr, Tokens::new(1_000_000_000));
243        state.balance.tokens -= gas_fees;
244        state.balance.tokens -= action_fine;
245        let prev_balance = state.balance.clone();
246        let prev_total_fees = state.total_fees;
247        let prev_start_lt = state.start_lt;
248
249        let received_msg = state
250            .receive_in_msg(make_message(
251                IntMsgInfo {
252                    src: src_addr.clone().into(),
253                    dst: dst_addr.clone().into(),
254                    value: Tokens::new(1_000_000_000).into(),
255                    bounce: true,
256                    created_lt: prev_start_lt + 1000,
257                    ..Default::default()
258                },
259                None,
260                None,
261            ))
262            .unwrap();
263        assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
264        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
265
266        let bounce_phase = state
267            .bounce_phase(BouncePhaseContext {
268                gas_fees,
269                action_fine,
270                received_message: &received_msg,
271            })
272            .unwrap();
273
274        let BouncePhase::Executed(bounce_phase) = bounce_phase else {
275            panic!("expected bounce phase to execute")
276        };
277
278        // Only msg fees are collected during the transaction.
279        let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
280        let collected_fees = config.fwd_prices.get_first_part(full_fwd_fee);
281        assert_eq!(state.total_fees, prev_total_fees + collected_fees);
282        assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
283        assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
284
285        // There were no extra currencies in the inbound message.
286        assert_eq!(state.out_msgs.len(), 1);
287        let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
288        assert!(bounced_msg.init.is_none());
289        assert_eq!(bounced_msg.body.0.size_bits(), 32);
290        assert_eq!(
291            CellSlice::apply(&bounced_msg.body)
292                .unwrap()
293                .load_u32()
294                .unwrap(),
295            u32::MAX
296        );
297
298        let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
299            panic!("expected bounced internal message");
300        };
301        assert_eq!(state.balance.other, prev_balance.other);
302        assert!(bounced_msg_info.value.other.is_empty());
303        assert_eq!(
304            state.balance.tokens,
305            prev_balance.tokens - (received_msg.balance_remaining.tokens - gas_fees - action_fine)
306        );
307        assert_eq!(
308            bounced_msg_info.value.tokens,
309            received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
310        );
311        assert!(bounced_msg_info.ihr_disabled);
312        assert!(!bounced_msg_info.bounce);
313        assert!(bounced_msg_info.bounced);
314        assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
315        assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
316        assert_eq!(bounced_msg_info.ihr_fee, Tokens::ZERO);
317        assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
318        assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
319        assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
320
321        // Root cell is free and the bounced message has no child cells.
322        assert_eq!(bounce_phase.msg_size, StorageUsedShort {
323            bits: Default::default(),
324            cells: Default::default()
325        });
326
327        // End LT must increase.
328        assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
329    }
330
331    #[test]
332    fn bounce_does_not_return_marks() {
333        let params = ExecutorParams {
334            authority_marks_enabled: true,
335            ..make_default_params()
336        };
337        let config = make_custom_config(|config| {
338            config.set_authority_marks_config(&AuthorityMarksConfig {
339                authority_addresses: BTreeMap::from_iter([(HashBytes::ZERO, ())]).try_into()?,
340                black_mark_id: 100,
341                white_mark_id: 101,
342            })?;
343            Ok(())
344        });
345
346        let src_addr = StdAddr::new(0, HashBytes([123; 32]));
347        let dst_addr = StdAddr::new(-1, HashBytes::ZERO);
348
349        let gas_fees = Tokens::new(100);
350        let action_fine = Tokens::new(200);
351
352        let mut state =
353            ExecutorState::new_uninit(&params, &config, &dst_addr, CurrencyCollection {
354                tokens: Tokens::new(1_000_000_000),
355                other: BTreeMap::from_iter([
356                    (100u32, VarUint248::new(1000)), // more black barks than white
357                    (101u32, VarUint248::new(100)),
358                ])
359                .try_into()
360                .unwrap(),
361            });
362        state.balance.tokens -= gas_fees;
363        state.balance.tokens -= action_fine;
364        let prev_balance = state.balance.clone();
365        let prev_total_fees = state.total_fees;
366        let prev_start_lt = state.start_lt;
367
368        let msg_balance = CurrencyCollection {
369            tokens: Tokens::new(1_000_000_000),
370            other: BTreeMap::from_iter([(100u32, VarUint248::new(1))])
371                .try_into()
372                .unwrap(),
373        };
374        let received_msg = state
375            .receive_in_msg(make_message(
376                IntMsgInfo {
377                    src: src_addr.clone().into(),
378                    dst: dst_addr.clone().into(),
379                    value: msg_balance.clone(),
380                    bounce: true,
381                    created_lt: prev_start_lt + 1000,
382                    ..Default::default()
383                },
384                None,
385                None,
386            ))
387            .unwrap();
388        assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
389        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
390
391        let credit_phase = state.credit_phase(&received_msg).unwrap();
392        assert_eq!(credit_phase.credit, msg_balance);
393
394        let bounce_phase = state
395            .bounce_phase(BouncePhaseContext {
396                gas_fees,
397                action_fine,
398                received_message: &received_msg,
399            })
400            .unwrap();
401
402        let BouncePhase::Executed(bounce_phase) = bounce_phase else {
403            panic!("expected bounce phase to execute")
404        };
405
406        // Only msg fees are collected during the transaction.
407        let full_fwd_fee = Tokens::new(config.mc_fwd_prices.lump_price as _);
408        let collected_fees = config.mc_fwd_prices.get_first_part(full_fwd_fee);
409        assert_eq!(state.total_fees, prev_total_fees + collected_fees);
410        assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
411        assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
412
413        // There were no extra currencies in the inbound message.
414        assert_eq!(state.out_msgs.len(), 1);
415        let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
416        assert!(bounced_msg.init.is_none());
417        assert_eq!(bounced_msg.body.0.size_bits(), 32);
418        assert_eq!(
419            CellSlice::apply(&bounced_msg.body)
420                .unwrap()
421                .load_u32()
422                .unwrap(),
423            u32::MAX
424        );
425
426        let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
427            panic!("expected bounced internal message");
428        };
429        assert_eq!(
430            state.balance.other,
431            prev_balance.other.checked_add(&msg_balance.other).unwrap()
432        );
433        assert!(bounced_msg_info.value.other.is_empty());
434        assert_eq!(
435            state.balance.tokens,
436            prev_balance.tokens + gas_fees + action_fine
437        );
438        assert_eq!(
439            bounced_msg_info.value.tokens,
440            received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
441        );
442        assert!(bounced_msg_info.ihr_disabled);
443        assert!(!bounced_msg_info.bounce);
444        assert!(bounced_msg_info.bounced);
445        assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
446        assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
447        assert_eq!(bounced_msg_info.ihr_fee, Tokens::ZERO);
448        assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
449        assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
450        assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
451
452        // Root cell is free and the bounced message has no child cells.
453        assert_eq!(bounce_phase.msg_size, StorageUsedShort {
454            bits: Default::default(),
455            cells: Default::default()
456        });
457
458        // End LT must increase.
459        assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
460    }
461
462    #[test]
463    fn bounce_with_no_funds() {
464        let mut params = make_default_params();
465        params.full_body_in_bounced = false;
466
467        let config = make_default_config();
468
469        let src_addr = StdAddr::new(0, HashBytes([0; 32]));
470        let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
471
472        let mut state =
473            ExecutorState::new_uninit(&params, &config, &dst_addr, Tokens::new(1_000_000_001));
474        let prev_balance = state.balance.clone();
475        let prev_total_fees = state.total_fees;
476        let prev_start_lt = state.start_lt;
477
478        let received_msg = state
479            .receive_in_msg(make_message(
480                IntMsgInfo {
481                    src: src_addr.clone().into(),
482                    dst: dst_addr.clone().into(),
483                    value: Tokens::new(1).into(),
484                    bounce: true,
485                    created_lt: prev_start_lt + 1000,
486                    ..Default::default()
487                },
488                None,
489                None,
490            ))
491            .unwrap();
492        assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
493        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
494
495        let bounce_phase = state
496            .bounce_phase(BouncePhaseContext {
497                gas_fees: Tokens::ZERO,
498                action_fine: Tokens::ZERO,
499                received_message: &received_msg,
500            })
501            .unwrap();
502
503        let BouncePhase::NoFunds(bounce_phase) = bounce_phase else {
504            panic!("expected bounce phase to execute")
505        };
506
507        // Balance must not change.
508        assert_eq!(state.balance.other, prev_balance.other);
509        assert_eq!(state.balance.tokens, prev_balance.tokens);
510        assert_eq!(state.total_fees, prev_total_fees);
511
512        // Required fees must be computed correctly.
513        let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
514        assert_eq!(
515            bounce_phase.req_fwd_fees,
516            full_fwd_fee - received_msg.balance_remaining.tokens
517        );
518
519        // Root cell is free and the bounced message has no child cells.
520        assert_eq!(bounce_phase.msg_size, StorageUsedShort {
521            bits: Default::default(),
522            cells: Default::default()
523        });
524
525        // No messages must be produced.
526        assert_eq!(state.out_msgs.len(), 0);
527
528        // End LT must not change.
529        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
530    }
531}