1use anyhow::Result;
2use tycho_types::cell::{Cell, CellBuilder, CellFamily, Lazy, Store};
3use tycho_types::models::{
4 BouncePhase, BounceReason, ExecutedBouncePhase, MsgInfo, NewBounceBody,
5 NewBounceComputePhaseInfo, NewBounceOriginalInfo, NoFundsBouncePhase, StorageUsedShort,
6};
7use tycho_types::num::Tokens;
8
9use crate::ExecutorState;
10use crate::phase::receive::ReceivedMessage;
11use crate::util::{
12 ExtStorageStat, StorageStatLimits, check_rewrite_dst_addr, new_varuint56_truncate,
13};
14
15pub struct BouncePhaseContext<'a> {
17 pub gas_fees: Tokens,
19 pub action_fine: Tokens,
21 pub received_message: &'a ReceivedMessage,
23 pub reason: BounceReason,
25 pub compute_phase_info: Option<NewBounceComputePhaseInfo>,
27}
28
29impl ExecutorState<'_> {
30 pub fn bounce_phase(&mut self, ctx: BouncePhaseContext<'_>) -> Result<BouncePhase> {
45 let mut info = ctx.received_message.root.parse::<MsgInfo>()?;
46 let MsgInfo::Int(int_msg_info) = &mut info else {
47 anyhow::bail!("bounce phase is defined only for internal messages");
48 };
49
50 std::mem::swap(&mut int_msg_info.src, &mut int_msg_info.dst);
52 if !check_rewrite_dst_addr(&self.config.workchains, &mut int_msg_info.dst) {
53 anyhow::bail!("invalid destination address in a bounced message");
57 }
58
59 let mut body = CellBuilder::new();
61 if int_msg_info.extra_flags.is_new_bounce_format() && !self.params.full_body_in_bounced {
62 let (bounced_by_phase, exit_code) = ctx.reason.flatten();
64 let (range, cell) = &ctx.received_message.body;
65
66 NewBounceBody {
67 original_body: if int_msg_info.extra_flags.is_full_body_in_bounced() {
68 if range.is_full(cell) {
69 cell.clone()
70 } else {
71 CellBuilder::build_from(range.apply_allow_exotic(cell))?
72 }
73 } else {
74 CellBuilder::build_from(range.apply_allow_exotic(cell).without_references())?
75 },
76 original_info: Lazy::new(&NewBounceOriginalInfo {
77 value: ctx.received_message.balance_remaining.clone(),
80 created_lt: int_msg_info.created_lt,
81 created_at: int_msg_info.created_at,
82 })?,
83 bounced_by_phase,
84 exit_code,
85 compute_phase: ctx.compute_phase_info,
86 }
87 .store_into(&mut body, Cell::empty_context())?;
88 } else {
89 const ROOT_BODY_BITS: u16 = 256;
90
91 let (range, cell) = &ctx.received_message.body;
94
95 body.store_u32(u32::MAX)?;
96 body.store_slice(range.apply_allow_exotic(cell).get_prefix(ROOT_BODY_BITS, 0))?;
97 if self.params.full_body_in_bounced {
98 body.store_reference(if range.is_full(cell) {
99 cell.clone()
100 } else {
101 CellBuilder::build_from(range.apply_allow_exotic(cell))?
102 })?;
103 }
104 };
105
106 let mut msg_value = ctx.received_message.balance_remaining.clone();
108
109 if self.params.authority_marks_enabled
111 && let Some(marks) = &self.config.authority_marks
112 {
113 marks.remove_authority_marks_in(&mut msg_value)?;
114 }
115
116 let stats = 'stats: {
118 let mut stats = ExtStorageStat::with_limits(StorageStatLimits {
119 bit_count: self.config.size_limits.max_msg_bits,
120 cell_count: self.config.size_limits.max_msg_cells,
121 });
122
123 'valid: {
125 if let Some(extra_currencies) = msg_value.other.as_dict().root()
127 && !stats.add_cell(extra_currencies.as_ref())
128 {
129 break 'valid;
130 }
131 for cell in body.references() {
133 if !stats.add_cell(cell.as_ref()) {
134 break 'valid;
135 }
136 }
137 break 'stats stats.stats();
139 }
140
141 let stats = stats.stats();
144 return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
145 msg_size: StorageUsedShort {
146 bits: new_varuint56_truncate(stats.bit_count),
147 cells: new_varuint56_truncate(stats.cell_count),
148 },
149 req_fwd_fees: Tokens::MAX,
150 }));
151 };
152
153 let use_mc_prices = self.address.is_masterchain() || int_msg_info.dst.is_masterchain();
155 let prices = self.config.fwd_prices(use_mc_prices);
156
157 let mut fwd_fees = prices.compute_fwd_fee(stats);
158 let msg_size = StorageUsedShort {
159 cells: new_varuint56_truncate(stats.cell_count),
160 bits: new_varuint56_truncate(stats.bit_count),
161 };
162
163 msg_value.tokens = match msg_value
165 .tokens
166 .checked_sub(ctx.gas_fees)
167 .and_then(|t| t.checked_sub(ctx.action_fine))
168 {
169 Some(msg_balance) if msg_balance >= fwd_fees => msg_balance,
170 msg_balance => {
171 return Ok(BouncePhase::NoFunds(NoFundsBouncePhase {
172 msg_size,
173 req_fwd_fees: fwd_fees - msg_balance.unwrap_or_default(),
174 }));
175 }
176 };
177
178 self.balance.try_sub_assign(&msg_value)?;
180
181 msg_value.tokens -= fwd_fees;
183
184 let msg_fees = prices.get_first_part(fwd_fees);
186 fwd_fees -= msg_fees;
187 self.total_fees.try_add_assign(msg_fees)?;
188
189 int_msg_info.ihr_disabled = true;
191 int_msg_info.bounce = false;
192 int_msg_info.bounced = true;
193 int_msg_info.value = msg_value;
194 int_msg_info.fwd_fee = fwd_fees;
195 int_msg_info.created_lt = self.end_lt;
196 int_msg_info.created_at = self.params.block_unixtime;
197 let msg = {
200 let c = Cell::empty_context();
201 let mut b = CellBuilder::new();
202 info.store_into(&mut b, c)?;
203 b.store_bit_zero()?; if b.has_capacity(1 + body.size_bits(), body.size_refs()) {
206 b.store_bit_zero()?; b.store_builder(&body)?;
208 } else {
209 b.store_bit_one()?; b.store_reference(body.build()?)?
211 }
212
213 unsafe { Lazy::from_raw_unchecked(b.build()?) }
215 };
216
217 self.out_msgs.push(msg);
219 self.end_lt += 1;
220
221 Ok(BouncePhase::Executed(ExecutedBouncePhase {
223 msg_size,
224 msg_fees,
225 fwd_fees,
226 }))
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use std::collections::BTreeMap;
233
234 use tycho_types::cell::CellTreeStats;
235 use tycho_types::models::{
236 AuthorityMarksConfig, ComputePhaseSkipReason, CurrencyCollection, IntMsgInfo,
237 MessageExtraFlags, StdAddr,
238 };
239 use tycho_types::num::VarUint248;
240 use tycho_types::prelude::*;
241
242 use super::*;
243 use crate::ExecutorParams;
244 use crate::tests::{
245 make_custom_config, make_default_config, make_default_params, make_message,
246 };
247
248 #[test]
249 fn bounce_with_enough_funds() {
250 let mut params = make_default_params();
251 params.full_body_in_bounced = false;
252
253 let config = make_default_config();
254
255 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
256 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
257
258 let gas_fees = Tokens::new(100);
259 let action_fine = Tokens::new(200);
260
261 let mut state =
262 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_000));
263 state.balance.tokens -= gas_fees;
264 state.balance.tokens -= action_fine;
265 let prev_balance = state.balance.clone();
266 let prev_total_fees = state.total_fees;
267 let prev_start_lt = state.start_lt;
268
269 let int_msg_info = IntMsgInfo {
270 src: src_addr.clone().into(),
271 dst: dst_addr.clone().into(),
272 value: Tokens::new(1_000_000_000).into(),
273 bounce: true,
274 extra_flags: MessageExtraFlags::NEW_BOUNCE_FORMAT,
275 created_lt: prev_start_lt + 1000,
276 ..Default::default()
277 };
278
279 let received_msg = state
280 .receive_in_msg(make_message(int_msg_info.clone(), None, None))
281 .unwrap();
282
283 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
284 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
285
286 let bounce_phase = state
287 .bounce_phase(BouncePhaseContext {
288 gas_fees,
289 action_fine,
290 received_message: &received_msg,
291 reason: BounceReason::ComputePhaseSkipped(ComputePhaseSkipReason::NoState),
292 compute_phase_info: None,
293 })
294 .unwrap();
295
296 let BouncePhase::Executed(bounce_phase) = bounce_phase else {
297 panic!("expected bounce phase to execute")
298 };
299
300 assert_eq!(state.out_msgs.len(), 1);
302 let bounced_msg = state.out_msgs.last().unwrap().load().unwrap();
303 assert!(bounced_msg.init.is_none());
304
305 let expected_fwd_fees = config.fwd_prices.compute_fwd_fee(CellTreeStats {
307 bit_count: int_msg_info.value.bit_len() as u64 + 64 + 32,
309 cell_count: 2,
311 });
312
313 let collected_fees = config.fwd_prices.get_first_part(expected_fwd_fees);
314 assert_eq!(state.total_fees, prev_total_fees + collected_fees);
315 assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
316 assert_eq!(bounce_phase.fwd_fees, expected_fwd_fees - collected_fees);
317
318 let mut body_cs = CellSlice::apply(&bounced_msg.body).unwrap();
319 let parsed_body = NewBounceBody::load_from(&mut body_cs).unwrap();
320 assert!(body_cs.is_empty());
321
322 assert_eq!(parsed_body, NewBounceBody {
323 original_body: Cell::empty_cell(),
324 original_info: Lazy::new(&NewBounceOriginalInfo {
325 value: int_msg_info.value,
326 created_lt: int_msg_info.created_lt,
327 created_at: int_msg_info.created_at
328 })
329 .unwrap(),
330 bounced_by_phase: 0,
331 exit_code: -1,
332 compute_phase: None
333 });
334
335 let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
336 panic!("expected bounced internal message");
337 };
338 assert_eq!(state.balance.other, prev_balance.other);
339 assert!(bounced_msg_info.value.other.is_empty());
340 assert_eq!(
341 state.balance.tokens,
342 prev_balance.tokens - (received_msg.balance_remaining.tokens - gas_fees - action_fine)
343 );
344 assert_eq!(
345 bounced_msg_info.value.tokens,
346 received_msg.balance_remaining.tokens - gas_fees - action_fine - expected_fwd_fees
347 );
348 assert!(bounced_msg_info.ihr_disabled);
349 assert!(!bounced_msg_info.bounce);
350 assert!(bounced_msg_info.bounced);
351 assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
352 assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
353 assert_eq!(bounced_msg_info.extra_flags, int_msg_info.extra_flags);
354 assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
355 assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
356 assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
357
358 assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
360 }
361
362 #[test]
363 fn should_bounce_full_body_in_new_format() {
364 let params = make_default_params();
365
366 let config = make_default_config();
367
368 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
369 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
370
371 let gas_fees = Tokens::new(100);
372 let action_fine = Tokens::new(200);
373
374 let mut state =
375 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_000));
376 state.balance.tokens -= gas_fees;
377 state.balance.tokens -= action_fine;
378
379 let msg_balance = CurrencyCollection {
380 tokens: Tokens::new(1_000_000_000),
381 other: Default::default(),
382 };
383
384 let extra_flags =
385 MessageExtraFlags::NEW_BOUNCE_FORMAT | MessageExtraFlags::FULL_BODY_IN_BOUNCED;
386
387 let mut cb = CellBuilder::new();
388 cb.store_reference(CellBuilder::new().build().unwrap())
389 .unwrap();
390 cb.store_u32(322).unwrap();
391
392 let received_msg = state
393 .receive_in_msg(make_message(
394 IntMsgInfo {
395 src: src_addr.clone().into(),
396 dst: dst_addr.clone().into(),
397 value: msg_balance.clone(),
398 bounce: true,
399 extra_flags,
400 created_lt: state.start_lt + 1000,
401 ..Default::default()
402 },
403 None,
404 Some(cb.clone()),
405 ))
406 .unwrap();
407
408 let bounce_phase = state
409 .bounce_phase(BouncePhaseContext {
410 gas_fees,
411 action_fine,
412 received_message: &received_msg,
413 reason: BounceReason::ComputePhaseSkipped(ComputePhaseSkipReason::NoState),
414 compute_phase_info: None,
415 })
416 .unwrap();
417
418 let BouncePhase::Executed(_) = bounce_phase else {
419 panic!("expected bounce phase to execute")
420 };
421 assert_eq!(state.out_msgs.len(), 1);
422 let msg = state.out_msgs.first().unwrap().load().unwrap();
423
424 let mut slice = CellSlice::apply(&msg.body).unwrap();
425 let body = NewBounceBody::load_from(&mut slice).unwrap();
426 assert!(slice.is_empty());
427 assert_eq!(body.original_body, cb.build().unwrap());
428 assert_eq!(body.compute_phase, None);
429 assert_eq!(body.bounced_by_phase, 0);
430 assert_eq!(body.exit_code, -1);
431 }
432
433 #[test]
434 fn bounce_does_not_return_marks() {
435 let params = ExecutorParams {
436 authority_marks_enabled: true,
437 ..make_default_params()
438 };
439 let config = make_custom_config(|config| {
440 config.set_authority_marks_config(&AuthorityMarksConfig {
441 authority_addresses: BTreeMap::from_iter([(HashBytes::ZERO, ())]).try_into()?,
442 black_mark_id: 100,
443 white_mark_id: 101,
444 })?;
445 Ok(())
446 });
447
448 let src_addr = StdAddr::new(0, HashBytes([123; 32]));
449 let dst_addr = StdAddr::new(-1, HashBytes::ZERO);
450
451 let gas_fees = Tokens::new(100);
452 let action_fine = Tokens::new(200);
453
454 let mut state =
455 ExecutorState::new_uninit(¶ms, &config, &dst_addr, CurrencyCollection {
456 tokens: Tokens::new(1_000_000_000),
457 other: BTreeMap::from_iter([
458 (100u32, VarUint248::new(1000)), (101u32, VarUint248::new(100)),
460 ])
461 .try_into()
462 .unwrap(),
463 });
464 state.balance.tokens -= gas_fees;
465 state.balance.tokens -= action_fine;
466 let prev_balance = state.balance.clone();
467 let prev_total_fees = state.total_fees;
468 let prev_start_lt = state.start_lt;
469
470 let msg_balance = CurrencyCollection {
471 tokens: Tokens::new(1_000_000_000),
472 other: BTreeMap::from_iter([(100u32, VarUint248::new(1))])
473 .try_into()
474 .unwrap(),
475 };
476
477 let received_msg = state
478 .receive_in_msg(make_message(
479 IntMsgInfo {
480 src: src_addr.clone().into(),
481 dst: dst_addr.clone().into(),
482 value: msg_balance.clone(),
483 bounce: true,
484 created_lt: prev_start_lt + 1000,
485 ..Default::default()
486 },
487 None,
488 None,
489 ))
490 .unwrap();
491 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
492 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
493
494 let credit_phase = state.credit_phase(&received_msg).unwrap();
495 assert_eq!(credit_phase.credit, msg_balance);
496
497 let bounce_phase = state
498 .bounce_phase(BouncePhaseContext {
499 gas_fees,
500 action_fine,
501 received_message: &received_msg,
502 reason: BounceReason::ComputePhaseSkipped(ComputePhaseSkipReason::NoState),
503 compute_phase_info: None,
504 })
505 .unwrap();
506
507 let BouncePhase::Executed(bounce_phase) = bounce_phase else {
508 panic!("expected bounce phase to execute")
509 };
510
511 let full_fwd_fee = Tokens::new(config.mc_fwd_prices.lump_price as _);
513 let collected_fees = config.mc_fwd_prices.get_first_part(full_fwd_fee);
514 assert_eq!(state.total_fees, prev_total_fees + collected_fees);
515 assert_eq!(state.total_fees, prev_total_fees + bounce_phase.msg_fees);
516 assert_eq!(bounce_phase.fwd_fees, full_fwd_fee - collected_fees);
517
518 assert_eq!(state.out_msgs.len(), 1);
520 let message = state.out_msgs.last().unwrap();
521 let bounced_msg = message.load().unwrap();
522
523 assert!(bounced_msg.init.is_none());
524 assert_eq!(bounced_msg.body.0.size_bits(), 32);
525 assert_eq!(
526 CellSlice::apply(&bounced_msg.body)
527 .unwrap()
528 .load_u32()
529 .unwrap(),
530 u32::MAX
531 );
532
533 let MsgInfo::Int(bounced_msg_info) = bounced_msg.info else {
534 panic!("expected bounced internal message");
535 };
536 assert_eq!(
537 state.balance.other,
538 prev_balance.other.checked_add(&msg_balance.other).unwrap()
539 );
540 assert!(bounced_msg_info.value.other.is_empty());
541 assert_eq!(
542 state.balance.tokens,
543 prev_balance.tokens + gas_fees + action_fine
544 );
545 assert_eq!(
546 bounced_msg_info.value.tokens,
547 received_msg.balance_remaining.tokens - gas_fees - action_fine - full_fwd_fee
548 );
549 assert!(bounced_msg_info.ihr_disabled);
550 assert!(!bounced_msg_info.bounce);
551 assert!(bounced_msg_info.bounced);
552 assert_eq!(bounced_msg_info.src, dst_addr.clone().into());
553 assert_eq!(bounced_msg_info.dst, src_addr.clone().into());
554 assert_eq!(bounced_msg_info.extra_flags, MessageExtraFlags::empty());
555 assert_eq!(bounced_msg_info.fwd_fee, bounce_phase.fwd_fees);
556 assert_eq!(bounced_msg_info.created_at, params.block_unixtime);
557 assert_eq!(bounced_msg_info.created_lt, prev_start_lt + 1000 + 2);
558
559 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
561 bits: Default::default(),
562 cells: Default::default()
563 });
564
565 assert_eq!(state.end_lt, prev_start_lt + 1000 + 3);
567 }
568
569 #[test]
570 fn bounce_with_no_funds() {
571 let mut params = make_default_params();
572 params.full_body_in_bounced = false;
573
574 let config = make_default_config();
575
576 let src_addr = StdAddr::new(0, HashBytes([0; 32]));
577 let dst_addr = StdAddr::new(0, HashBytes([1; 32]));
578
579 let mut state =
580 ExecutorState::new_uninit(¶ms, &config, &dst_addr, Tokens::new(1_000_000_001));
581 let prev_balance = state.balance.clone();
582 let prev_total_fees = state.total_fees;
583 let prev_start_lt = state.start_lt;
584
585 let received_msg = state
586 .receive_in_msg(make_message(
587 IntMsgInfo {
588 src: src_addr.clone().into(),
589 dst: dst_addr.clone().into(),
590 value: Tokens::new(1).into(),
591 bounce: true,
592 created_lt: prev_start_lt + 1000,
593 extra_flags: MessageExtraFlags::empty(),
594 ..Default::default()
595 },
596 None,
597 None,
598 ))
599 .unwrap();
600 assert_eq!(state.start_lt, prev_start_lt + 1000 + 1);
601 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
602
603 let bounce_phase = state
604 .bounce_phase(BouncePhaseContext {
605 gas_fees: Tokens::ZERO,
606 action_fine: Tokens::ZERO,
607 received_message: &received_msg,
608 reason: BounceReason::ComputePhaseSkipped(ComputePhaseSkipReason::NoState),
609 compute_phase_info: None,
610 })
611 .unwrap();
612
613 let BouncePhase::NoFunds(bounce_phase) = bounce_phase else {
614 panic!("expected bounce phase to execute")
615 };
616
617 assert_eq!(state.balance.other, prev_balance.other);
619 assert_eq!(state.balance.tokens, prev_balance.tokens);
620 assert_eq!(state.total_fees, prev_total_fees);
621
622 let full_fwd_fee = Tokens::new(config.fwd_prices.lump_price as _);
624 assert_eq!(
625 bounce_phase.req_fwd_fees,
626 full_fwd_fee - received_msg.balance_remaining.tokens
627 );
628
629 assert_eq!(bounce_phase.msg_size, StorageUsedShort {
631 bits: Default::default(),
632 cells: Default::default()
633 });
634
635 assert_eq!(state.out_msgs.len(), 0);
637
638 assert_eq!(state.end_lt, prev_start_lt + 1000 + 2);
640 }
641}