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        // Compute message storage stats.
70        let stats = 'stats: {
71            let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
72                bit_count: self.config.size_limits.max_msg_bits,
73                cell_count: self.config.size_limits.max_msg_cells,
74            });
75
76            // Root cell is free, but all children must be accounted.
77            'valid: {
78                // Msg value can contain some cells.
79                if let Some(extra_currencies) = msg_value.other.as_dict().root() {
80                    if !stats.add_cell(extra_currencies.as_ref()) {
81                        break 'valid;
82                    }
83                }
84
85                // We must also include a msg body if `params.full_body_in_bounce` is enabled.
86                if let Some(body) = &full_body {
87                    if !stats.add_cell(body.as_ref()) {
88                        break 'valid;
89                    }
90                }
91
92                // Exit this block with a valid storage stats info.
93                break 'stats stats.stats();
94            }
95
96            // Fallback to NoFunds if the returned message cannot fit into the limits.
97            // We require an "infinite" amount of tokens here if storage overflows.
98            let stats = stats.stats();
99            return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
100                msg_size: StorageUsedShort {
101                    bits: new_varuint56_truncate(stats.bit_count),
102                    cells: new_varuint56_truncate(stats.cell_count),
103                },
104                req_fwd_fees: Tokens::MAX,
105            }));
106        };
107
108        // Compute forwarding fee.
109        let use_mc_prices = self.address.is_masterchain() || int_msg_info.dst.is_masterchain();
110        let prices = self.config.fwd_prices(use_mc_prices);
111
112        let mut fwd_fees = prices.compute_fwd_fee(stats);
113        let msg_size = StorageUsedShort {
114            cells: new_varuint56_truncate(stats.cell_count),
115            bits: new_varuint56_truncate(stats.bit_count),
116        };
117
118        // Try to substract all fees from the remaining message balance.
119        msg_value.tokens = match msg_value
120            .tokens
121            .checked_sub(ctx.gas_fees)
122            .and_then(|t| t.checked_sub(ctx.action_fine))
123        {
124            Some(msg_balance) if msg_balance >= fwd_fees => msg_balance,
125            msg_balance => {
126                return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
127                    msg_size,
128                    req_fwd_fees: fwd_fees - msg_balance.unwrap_or_default(),
129                }));
130            }
131        };
132
133        // Take message balance back from the account balance.
134        self.balance.try_sub_assign(&msg_value)?;
135
136        // Take forwarding fee from the message balance.
137        msg_value.tokens -= fwd_fees;
138
139        // Split forwarding fee.
140        let msg_fees = prices.get_first_part(fwd_fees);
141        fwd_fees -= msg_fees;
142        self.total_fees.try_add_assign(msg_fees)?;
143
144        // Finalize message.
145        int_msg_info.ihr_disabled = true;
146        int_msg_info.bounce = false;
147        int_msg_info.bounced = true;
148        int_msg_info.value = msg_value;
149        int_msg_info.ihr_fee = Tokens::ZERO;
150        int_msg_info.fwd_fee = fwd_fees;
151        int_msg_info.created_lt = self.end_lt;
152        int_msg_info.created_at = self.params.block_unixtime;
153
154        let msg = {
155            const ROOT_BODY_BITS: u16 = 256;
156            const BOUNCE_SELECTOR: u32 = u32::MAX;
157
158            let body_prefix = {
159                let (range, cell) = &ctx.received_message.body;
160                range.apply_allow_exotic(cell).get_prefix(ROOT_BODY_BITS, 0)
161            };
162
163            let c = Cell::empty_context();
164            let mut b = CellBuilder::new();
165            info.store_into(&mut b, c)?;
166            b.store_bit_zero()?; // init:(Maybe ...) -> nothing$0
167
168            if b.has_capacity(body_prefix.size_bits() + 33, 0) {
169                b.store_bit_zero()?; // body:(Either X ^X) -> left$0 X
170                b.store_u32(BOUNCE_SELECTOR)?;
171                b.store_slice_data(body_prefix)?;
172                if let Some(full_body) = full_body {
173                    b.store_reference(full_body)?;
174                }
175            } else {
176                let child = {
177                    let mut b = CellBuilder::new();
178                    b.store_u32(BOUNCE_SELECTOR)?;
179                    b.store_slice_data(body_prefix)?;
180                    if let Some(full_body) = full_body {
181                        b.store_reference(full_body)?;
182                    }
183                    b.build()?
184                };
185
186                b.store_bit_one()?; // body:(Either X ^X) -> right$1 ^X
187                b.store_reference(child)?
188            }
189
190            // SAFETY: `b` is an ordinary cell.
191            unsafe { Lazy::from_raw_unchecked(b.build()?) }
192        };
193
194        // Add message to output.
195        self.out_msgs.push(msg);
196        self.end_lt += 1;
197
198        // Done
199        Ok(BouncePhase::Executed(ExecutedBouncePhase {
200            msg_size,
201            msg_fees,
202            fwd_fees,
203        }))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use tycho_types::models::{IntMsgInfo, StdAddr};
210    use tycho_types::prelude::*;
211
212    use super::*;
213    use crate::tests::{make_default_config, make_default_params, make_message};
214
215    #[test]
216    fn bounce_with_enough_funds() {
217        let mut params = make_default_params();
218        params.full_body_in_bounced = false;
219
220        let config = make_default_config();
221
222        let src_addr = StdAddr::new(0, HashBytes([0; 32]));
223        let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
224
225        let gas_fees = Tokens::new(100);
226        let action_fine = Tokens::new(200);
227
228        let mut state =
229            ExecutorState::new_uninit(&params, &config, &dst_addr, Tokens::new(1_000_000_000));
230        state.balance.tokens -= gas_fees;
231        state.balance.tokens -= action_fine;
232        let prev_balance = state.balance.clone();
233        let prev_total_fees = state.total_fees;
234        let prev_start_lt = state.start_lt;
235
236        let received_msg = state
237            .receive_in_msg(make_message(
238                IntMsgInfo {
239                    src: src_addr.clone().into(),
240                    dst: dst_addr.clone().into(),
241                    value: Tokens::new(1_000_000_000).into(),
242                    bounce: true,
243                    created_lt: prev_start_lt + 1000,
244                    ..Default::default()
245                },
246                None,
247                None,
248            ))
249            .unwrap();
250        assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
251        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
252
253        let bounce_phase = state
254            .bounce_phase(BouncePhaseContext {
255                gas_fees,
256                action_fine,
257                received_message: &received_msg,
258            })
259            .unwrap();
260
261        let BouncePhase::Executed(bounce_phase) = bounce_phase else {
262            panic!("expected bounce phase to execute")
263        };
264
265        // Only msg fees are collected during the transaction.
266        let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
267        let collected_fees = config.fwd_prices.get_first_part(full_fwd_fee);
268        assert_eq!(state.total_fees, prev_total_fees + collected_fees);
269        assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
270        assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
271
272        // There were no extra currencies in the inbound message.
273        assert_eq!(state.out_msgs.len(), 1);
274        let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
275        assert!(bounced_msg.init.is_none());
276        assert_eq!(bounced_msg.body.0.size_bits(), 32);
277        assert_eq!(
278            CellSlice::apply(&bounced_msg.body)
279                .unwrap()
280                .load_u32()
281                .unwrap(),
282            u32::MAX
283        );
284
285        let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
286            panic!("expected bounced internal message");
287        };
288        assert_eq!(state.balance.other, prev_balance.other);
289        assert!(bounced_msg_info.value.other.is_empty());
290        assert_eq!(
291            state.balance.tokens,
292            prev_balance.tokens - (received_msg.balance_remaining.tokens - gas_fees - action_fine)
293        );
294        assert_eq!(
295            bounced_msg_info.value.tokens,
296            received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
297        );
298        assert!(bounced_msg_info.ihr_disabled);
299        assert!(!bounced_msg_info.bounce);
300        assert!(bounced_msg_info.bounced);
301        assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
302        assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
303        assert_eq!(bounced_msg_info.ihr_fee, Tokens::ZERO);
304        assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
305        assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
306        assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
307
308        // Root cell is free and the bounced message has no child cells.
309        assert_eq!(bounce_phase.msg_size, StorageUsedShort {
310            bits: Default::default(),
311            cells: Default::default()
312        });
313
314        // End LT must increase.
315        assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
316    }
317
318    #[test]
319    fn bounce_with_no_funds() {
320        let mut params = make_default_params();
321        params.full_body_in_bounced = false;
322
323        let config = make_default_config();
324
325        let src_addr = StdAddr::new(0, HashBytes([0; 32]));
326        let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
327
328        let mut state =
329            ExecutorState::new_uninit(&params, &config, &dst_addr, Tokens::new(1_000_000_001));
330        let prev_balance = state.balance.clone();
331        let prev_total_fees = state.total_fees;
332        let prev_start_lt = state.start_lt;
333
334        let received_msg = state
335            .receive_in_msg(make_message(
336                IntMsgInfo {
337                    src: src_addr.clone().into(),
338                    dst: dst_addr.clone().into(),
339                    value: Tokens::new(1).into(),
340                    bounce: true,
341                    created_lt: prev_start_lt + 1000,
342                    ..Default::default()
343                },
344                None,
345                None,
346            ))
347            .unwrap();
348        assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
349        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
350
351        let bounce_phase = state
352            .bounce_phase(BouncePhaseContext {
353                gas_fees: Tokens::ZERO,
354                action_fine: Tokens::ZERO,
355                received_message: &received_msg,
356            })
357            .unwrap();
358
359        let BouncePhase::NoFunds(bounce_phase) = bounce_phase else {
360            panic!("expected bounce phase to execute")
361        };
362
363        // Balance must not change.
364        assert_eq!(state.balance.other, prev_balance.other);
365        assert_eq!(state.balance.tokens, prev_balance.tokens);
366        assert_eq!(state.total_fees, prev_total_fees);
367
368        // Required fees must be computed correctly.
369        let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
370        assert_eq!(
371            bounce_phase.req_fwd_fees,
372            full_fwd_fee - received_msg.balance_remaining.tokens
373        );
374
375        // Root cell is free and the bounced message has no child cells.
376        assert_eq!(bounce_phase.msg_size, StorageUsedShort {
377            bits: Default::default(),
378            cells: Default::default()
379        });
380
381        // No messages must be produced.
382        assert_eq!(state.out_msgs.len(), 0);
383
384        // End LT must not change.
385        assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
386    }
387}