1use crate::frame_builder::FrameBuilder;
2use crate::frame_result::FrameResult;
3use crate::invoke_error::InvokeError;
4use crate::rv32_stack_usage::Rv32StackUsage;
5use crate::test_environment::TestEnvironment;
6use core::ffi::c_void;
7use core::ops::Range;
8use std::cell::Cell;
9use std::cell::RefCell;
10use std::panic::UnwindSafe;
11use std::panic::panic_any;
12use std::rc::Rc;
13use thruput_sys::ForeignTryCommitResult;
14use thruput_trace::InvocationFrameTrace;
15use thruput_xdr::auth_claim::AuthClaim;
16use thruput_xdr::event::TxEmittedEvent;
17use thruput_xdr::syscall_error::SyscallErrorCode;
18use thruput_xdr::transaction::InvocationFrameOutput;
19use thruput_xdr::types::SHA256Hash;
20
21use thruput_sys::FFIErrorCode;
22
23#[derive(Debug, PartialEq)]
24pub(crate) enum MockTransactionState {
25 NotStarted,
26 Running,
27 Aborted,
28 Committed,
29}
30
31#[allow(clippy::large_enum_variant)]
32#[derive(Debug, PartialEq)]
33enum MockInvocationFrameState {
34 Started,
35 Closed(
36 InvocationFrameOutput,
37 Option<InvocationFrameTrace>,
38 Vec<TxEmittedEvent>,
39 ),
40}
41
42pub struct MockInvocationFrameConfig<'a> {
45 context_pushcount: Rc<RefCell<ThreadlocalContextPushcount>>,
46 invocation: thruput_xdr::transaction::Invocation,
47 invoke_flags: thruput_xdr::invoke_flag::InvokeFlagSet,
48
49 root_handler: *mut c_void,
50 to_emplace: &'a mut Option<Box<MockInvocationFrame>>,
52 root_frame_closed: Option<Rc<Cell<bool>>>,
53}
54
55impl<'a> MockInvocationFrameConfig<'a> {
56 fn new(
62 root_handler: *mut c_void,
63 contract_address: thruput_xdr::types::ContractAddress,
64 to_emplace: &'a mut Option<Box<MockInvocationFrame>>,
65 context_pushcount: Rc<RefCell<ThreadlocalContextPushcount>>,
66 root_frame_closed: Option<Rc<Cell<bool>>>,
67 ) -> Self {
68 Self {
69 context_pushcount,
70 root_handler,
71 invocation: thruput_xdr::transaction::Invocation {
72 invoked_address: contract_address,
73 entrypoint: [0; 8].into(),
74 gas_limit: u64::MAX,
75 calldata: xdr_common::BytesM::default(),
76 },
77 invoke_flags: 0,
78 to_emplace,
79 root_frame_closed,
80 }
81 }
82
83 pub fn with_calldata(mut self, calldata: xdr_common::BytesM) -> Self {
85 self.invocation.calldata = calldata;
86 self
87 }
88
89 pub fn with_invoke_flags(
93 mut self,
94 invoke_flags: thruput_xdr::invoke_flag::InvokeFlagSet,
95 ) -> Self {
96 self.invoke_flags = invoke_flags;
97 self
98 }
99
100 pub fn with_gas_limit(mut self, gas_limit: u64) -> Self {
102 self.invocation.gas_limit = gas_limit;
103 self
104 }
105
106 pub fn build(self) -> &'a mut Box<MockInvocationFrame> {
109 let serialized_invocation: Vec<u8> =
110 xdrs::txs::encode(&self.invocation).unwrap();
111 let frame = Box::new(MockInvocationFrame::new(
112 self.root_handler,
113 &serialized_invocation,
114 self.invoke_flags,
115 self.context_pushcount.clone(),
116 self.root_frame_closed,
117 ));
118 *self.to_emplace = Some(frame);
119 self.to_emplace.as_mut().unwrap()
120 }
121}
122
123pub struct MockInvocationFrame {
129 root_handler: *mut c_void,
130 child_frame: Option<Box<MockInvocationFrame>>,
131 state: MockInvocationFrameState,
132 context_pushcount: Rc<RefCell<ThreadlocalContextPushcount>>,
133 root_frame_closed: Option<Rc<Cell<bool>>>,
134 fault_panic: Option<crate::invoke_error::PanicRecord>,
143 rv32_records_at_open: usize,
148 unclaimed_rv32_window: Option<Range<usize>>,
152 child_rv32_windows: Vec<Range<usize>>,
154 rv32_stack_usage: Vec<Rv32StackUsage>,
158}
159
160fn frame_refusal_message(code: SyscallErrorCode) -> String {
164 let rule = match code {
165 c if c == SyscallErrorCode::kSyscallErrorCodeReentrance => {
166 "that address already has a frame open on this transaction's \
167 stack -- nested frames need distinct addresses"
168 }
169 c if c == SyscallErrorCode::kSyscallErrorCodeInvokeDepthExceeded => {
170 "the transaction already holds the chain's \
171 max_cross_invoke_depth frames open"
172 }
173 c if c == SyscallErrorCode::kSyscallErrorCodeInvalidSyscallInput => {
174 "the invoke flags carry bits production does not admit (only \
175 TryInvoke and ReadOnly are implemented)"
176 }
177 _ => "production refused the invocation",
178 };
179 format!(
180 "mock invocation frame refused: {rule} ({code}); a contract handles \
181 this through Invoke (thruput_sdk::call::try_call_raw_with)"
182 )
183}
184
185impl MockInvocationFrame {
186 fn new(
187 root_handler: *mut c_void,
188 serialized_invocation: &[u8],
189 invoke_flags: thruput_xdr::invoke_flag::InvokeFlagSet,
190 context_pushcount: Rc<RefCell<ThreadlocalContextPushcount>>,
191 root_frame_closed: Option<Rc<Cell<bool>>>,
192 ) -> Self {
193 let ffi_invocation = thruput_sys::FFISerializedInvocation {
194 data: serialized_invocation.as_ptr() as *mut u8,
195 len: serialized_invocation.len().try_into().unwrap(),
196 };
197
198 let res = unsafe {
199 thruput_sys::open_mock_invocation_frame(
200 root_handler,
201 ffi_invocation,
202 invoke_flags,
203 )
204 };
205
206 let handler_res: thruput_sys::FFIErrorCode = res.ffi_error_code.into();
210 if handler_res != FFIErrorCode::None {
211 eprintln!(
215 "failed to open mock invocation frame: {:?}",
216 handler_res
217 );
218 panic_any(crate::invoke_error::unrecoverable_error());
219 }
220 let refused = SyscallErrorCode(res.syscall_error_code);
221 if refused != SyscallErrorCode::kSyscallErrorCodeNone {
222 eprintln!("{}", frame_refusal_message(refused));
223 panic_any(crate::invoke_error::unrecoverable_error());
224 }
225
226 let rv32_records_at_open =
229 crate::rv32_stack_usage::records(root_handler).len();
230
231 Self {
232 root_handler,
233 child_frame: None,
234 state: MockInvocationFrameState::Started,
235 context_pushcount,
236 root_frame_closed,
237 fault_panic: None,
238 rv32_records_at_open,
239 unclaimed_rv32_window: None,
240 child_rv32_windows: Vec::new(),
241 rv32_stack_usage: Vec::new(),
242 }
243 }
244
245 fn claim_child_rv32_window(&mut self) {
251 if let Some(window) = self
252 .child_frame
253 .as_mut()
254 .and_then(|child| child.unclaimed_rv32_window.take())
255 {
256 self.child_rv32_windows.push(window);
257 }
258 }
259
260 fn has_open_child(&self) -> bool {
264 matches!(
265 &self.child_frame,
266 Some(child) if child.state == MockInvocationFrameState::Started
267 )
268 }
269
270 pub fn open_child_frame(
279 &mut self,
280 contract_address: thruput_xdr::types::ContractAddress,
281 ) -> MockInvocationFrameConfig<'_> {
282 if self.has_open_child() {
288 eprintln!(
289 "cannot open a child frame while another child frame is still open"
290 );
291 panic_any(crate::invoke_error::unrecoverable_error());
292 }
293 self.claim_child_rv32_window();
296 let config = MockInvocationFrameConfig::new(
297 self.root_handler,
298 contract_address,
299 &mut self.child_frame,
300 self.context_pushcount.clone(),
301 None,
302 );
303 config
304 }
305
306 fn close_frame_internal(
307 &mut self,
308 error_code: thruput_trace::ffi_enums::InvokeErrorCode,
309 ) {
310 if self.has_open_child() {
317 eprintln!(
318 "cannot close a frame while its child frame is still open"
319 );
320 panic_any(crate::invoke_error::unrecoverable_error());
321 }
322
323 let handler_res = unsafe {
324 thruput_sys::finish_mock_invocation_frame(
325 self.root_handler,
326 error_code.0,
327 )
328 };
329 let err: FFIErrorCode = handler_res.ffi_error_code.into();
330 if err != FFIErrorCode::None {
331 eprintln!("failed to close mock invocation frame: {:?}", err);
335 panic_any(crate::invoke_error::unrecoverable_error());
336 }
337
338 let output: InvocationFrameOutput = xdrs::txs::decode(
339 handler_res.serialized_output.as_slice().unwrap(),
340 )
341 .unwrap();
342 let trace: Option<InvocationFrameTrace> = if let Some(trace_data) =
343 handler_res.serialized_trace.as_slice()
344 {
345 let deserialized_trace: InvocationFrameTrace =
346 xdrs::txs::decode(trace_data).unwrap();
347 Some(deserialized_trace)
348 } else {
349 None
350 };
351 let events: Vec<TxEmittedEvent> = if let Some(events_data) =
352 handler_res.serialized_events.as_slice()
353 {
354 let decoded: xdr_common::VecM<TxEmittedEvent> =
355 xdrs::txs::decode(events_data).unwrap_or_else(|e| {
356 panic!(
357 "failed to decode frame events ({} bytes): {}",
358 events_data.len(),
359 e
360 )
361 });
362 decoded.into()
363 } else {
364 Vec::new()
365 };
366 self.state = MockInvocationFrameState::Closed(output, trace, events);
367 self.claim_child_rv32_window();
371 let records = crate::rv32_stack_usage::records(self.root_handler);
372 let window = self.rv32_records_at_open..records.len();
373 self.rv32_stack_usage = crate::rv32_stack_usage::frame_records(
374 &records,
375 window.clone(),
376 &self.child_rv32_windows,
377 );
378 self.unclaimed_rv32_window = Some(window);
379 if let Some(flag) = &self.root_frame_closed {
380 flag.set(true);
381 }
382 }
383
384 fn push_context(&mut self) {
385 self.context_pushcount
386 .borrow_mut()
387 .push_context(self.root_handler);
388 }
389
390 fn pop_context(&mut self) {
391 self.context_pushcount.borrow_mut().pop_context();
392 }
393
394 pub fn execute(
399 &mut self,
400 lambda: impl FnOnce(&mut Self) -> crate::frame_builder::FrameAction
401 + UnwindSafe,
402 ) -> &mut Self {
403 use crate::frame_builder::FrameAction;
404
405 if self.state != MockInvocationFrameState::Started {
406 eprintln!("cannot execute a frame that has already been closed");
407 panic_any(crate::invoke_error::unrecoverable_error());
408 }
409
410 if self.has_open_child() {
418 eprintln!(
419 "cannot execute a frame while its child frame is still open"
420 );
421 panic_any(crate::invoke_error::unrecoverable_error());
422 }
423
424 self.push_context();
425
426 let result = {
427 let _hook = crate::invoke_error::PanicHookGuard::enter();
432 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
433 lambda(self)
434 }))
435 };
436
437 self.pop_context();
438
439 match result {
440 Ok(FrameAction::Close) => {
441 self.close_frame_internal(
442 thruput_trace::ffi_enums::InvokeErrorCode::None,
443 );
444 }
445 Ok(FrameAction::Continue) => {}
446 Err(err) => {
447 if let Some(my_error) = err.downcast_ref::<InvokeError>() {
448 self.close_frame_internal(my_error.error_type);
452 } else {
453 self.fault_panic =
464 Some(crate::invoke_error::take_panic_record(&*err));
465 self.close_frame_internal(thruput_trace::ffi_enums::InvokeErrorCode::Deterministic);
466 }
467 }
468 };
469 self
470 }
471
472 pub fn result(&mut self) -> FrameResult {
475 let res = match &self.state {
476 MockInvocationFrameState::Closed(output, trace, events) => {
477 FrameResult::new(output.clone(), trace.clone(), events.clone())
478 .with_fault_panic(self.fault_panic.clone())
479 .with_rv32_stack_usage(self.rv32_stack_usage.clone())
480 }
481 _ => {
482 eprintln!(
483 "cannot get the result of a frame while it is not closed"
484 );
485 panic_any(crate::invoke_error::unrecoverable_error());
486 }
487 };
488 self.child_frame = None;
489 res
490 }
491}
492
493impl Drop for MockInvocationFrame {
494 fn drop(&mut self) {
495 self.child_frame = None;
499
500 if self.state == MockInvocationFrameState::Started {
501 let res = unsafe {
512 thruput_sys::finish_mock_invocation_frame(
513 self.root_handler,
514 thruput_trace::ffi_enums::InvokeErrorCode::Unrecoverable.0,
515 )
516 };
517 let err: FFIErrorCode = res.ffi_error_code.into();
521 if err != FFIErrorCode::None
522 && err != FFIErrorCode::UnrecoverableError
523 {
524 eprintln!(
525 "unexpected status finishing an abandoned invocation frame \
526 during cleanup: {:?}",
527 err
528 );
529 }
530 if let Some(flag) = &self.root_frame_closed {
531 flag.set(true);
532 }
533 }
534 }
535}
536
537pub(crate) struct ThreadlocalContextPushcount {
538 count: i32,
539}
540
541impl ThreadlocalContextPushcount {
542 pub fn new() -> Self {
543 Self { count: 0 }
544 }
545
546 pub fn push_context(&mut self, handler: *mut c_void) {
547 if self.count == 0 {
548 let push_res = unsafe { thruput_sys::push_context(handler) };
549 let err: FFIErrorCode = push_res.into();
550 if err != FFIErrorCode::None {
551 panic!("failed to push context: {:?}", err);
552 }
553 }
554 self.count += 1;
555 }
556
557 pub fn pop_context(&mut self) {
558 if self.count == 0 {
559 panic!("cannot pop context if count is 0");
560 }
561 self.count -= 1;
562 if self.count == 0 {
563 let pop_res = unsafe { thruput_sys::pop_context() };
564 let err: FFIErrorCode = pop_res.into();
565 if err != FFIErrorCode::None {
566 panic!("failed to pop context: {:?}", err);
567 }
568 }
569 }
570}
571
572impl Drop for ThreadlocalContextPushcount {
573 fn drop(&mut self) {
574 if self.count != 0 {
575 eprintln!(
576 "Dropping transaction while threadlocal context active: count={}",
577 self.count
578 );
579 let pop_res = unsafe { thruput_sys::pop_context() };
580 let err: FFIErrorCode = pop_res.into();
581 if err != FFIErrorCode::None {
582 crate::drop_failure(format_args!(
583 "failed to pop context: {:?}",
584 err
585 ));
586 }
587 }
588 }
589}
590
591#[derive(Clone)]
599pub struct MockTransactionConfig {
600 pub auth_claim: AuthClaim,
603}
604
605impl MockTransactionConfig {
606 pub fn default_auth_claim() -> AuthClaim {
608 use thruput_xdr::auth_claim::AuthClaim_source_address_claim;
609 AuthClaim::kSourceAddressAuthData {
610 source_address_claim: AuthClaim_source_address_claim {
611 allowed_keys: 0,
612 required_weight: 0,
613 },
614 }
615 }
616}
617
618impl Default for MockTransactionConfig {
619 fn default() -> Self {
620 Self {
621 auth_claim: Self::default_auth_claim(),
622 }
623 }
624}
625
626pub struct MockTransactionHandler {
632 pub(crate) handler: *mut c_void,
633 pub(crate) _env: Rc<TestEnvironment>,
634 pub(crate) state: MockTransactionState,
635 pub(crate) root_frame: Option<Box<MockInvocationFrame>>,
636 pub(crate) context_pushcount: Rc<RefCell<ThreadlocalContextPushcount>>,
637 root_frame_closed: Rc<Cell<bool>>,
638}
639
640impl MockTransactionHandler {
641 pub fn new(env: Rc<TestEnvironment>) -> Self {
647 Self::new_with_config(env, MockTransactionConfig::default())
648 }
649
650 pub fn new_with_config(
654 env: Rc<TestEnvironment>,
655 config: MockTransactionConfig,
656 ) -> Self {
657 let serialized_auth_claim =
658 xdrs::txs::encode(&config.auth_claim).unwrap();
659 let source_tx_hash: SHA256Hash = [0; 32].into();
660 let handler_res = unsafe {
661 thruput_sys::open_mocked_syscall_handler(
662 env.inner.borrow().db.db,
663 env.inner.borrow().contract_context.context,
664 thruput_sys::FFISerializedAuthClaimData {
665 data: serialized_auth_claim.as_ptr() as *mut u8,
666 len: serialized_auth_claim.len().try_into().unwrap(),
667 },
668 source_tx_hash.as_ptr(),
669 1,
670 )
671 };
672 let err: FFIErrorCode = handler_res.ffi_error_code.into();
673 if err != FFIErrorCode::None {
674 panic!("failed to open mocked syscall handler: {:?}", err);
675 }
676 assert!(!handler_res.syscall_handler.is_null());
677 Self {
678 handler: handler_res.syscall_handler,
679 _env: env,
680 state: MockTransactionState::NotStarted,
681 root_frame: None,
682 context_pushcount: Rc::new(RefCell::new(
683 ThreadlocalContextPushcount::new(),
684 )),
685 root_frame_closed: Rc::new(Cell::new(false)),
686 }
687 }
688
689 pub(crate) fn is_running(&self) -> bool {
692 self.state == MockTransactionState::Running
693 }
694
695 pub fn abort_transaction(&mut self) {
700 assert_eq!(self.state, MockTransactionState::Running);
701 assert!(
702 self.root_frame_closed.get(),
703 "cannot abort: root frame is still open"
704 );
705 self.state = MockTransactionState::Aborted;
706 let err: FFIErrorCode =
707 unsafe { thruput_sys::abort_transaction(self.handler) }.into();
708 if err != FFIErrorCode::None {
709 panic!("failed to abort transaction: {:?}", err);
710 }
711 }
712
713 pub fn try_commit_transaction(&mut self) -> crate::TryCommitResult {
718 assert_eq!(self.state, MockTransactionState::Running);
719 assert!(
720 self.root_frame_closed.get(),
721 "cannot commit: root frame is still open"
722 );
723 self.state = MockTransactionState::Committed;
724 let result: ForeignTryCommitResult =
725 unsafe { thruput_sys::try_commit_transaction(self.handler) };
726 let err: FFIErrorCode = result.ffi_error_code.into();
727 if err != FFIErrorCode::None {
728 panic!("ffi error: failed to commit transaction: {:?}", err);
729 }
730
731 crate::TryCommitResult::from_ffi(
732 result.result_code,
733 &result.serialized_conflict,
734 )
735 }
736
737 pub fn call(
745 &mut self,
746 addr: &thruput_xdr::types::ContractAddress,
747 ) -> FrameBuilder<'_> {
748 let config = self.open_root_frame(*addr);
749 FrameBuilder::new(config)
750 }
751
752 pub fn open_root_frame(
760 &mut self,
761 contract_address: thruput_xdr::types::ContractAddress,
762 ) -> MockInvocationFrameConfig<'_> {
763 assert_eq!(
764 self.state,
765 MockTransactionState::NotStarted,
766 "cannot open root frame: transaction already started"
767 );
768 self.state = MockTransactionState::Running;
769 let config = MockInvocationFrameConfig::new(
770 self.handler,
771 contract_address,
772 &mut self.root_frame,
773 self.context_pushcount.clone(),
774 Some(self.root_frame_closed.clone()),
775 );
776 config
777 }
778}
779
780impl Drop for MockTransactionHandler {
781 fn drop(&mut self) {
782 self.root_frame = None;
793
794 if self.state == MockTransactionState::Running {
795 self.state = MockTransactionState::Aborted;
800 let err: FFIErrorCode =
801 unsafe { thruput_sys::abort_transaction(self.handler) }.into();
802 if err != FFIErrorCode::None {
803 crate::drop_failure(format_args!(
804 "failed to abort uncommitted transaction during cleanup: {:?}",
805 err
806 ));
807 }
808 }
809
810 let err: FFIErrorCode =
811 unsafe { thruput_sys::close_syscall_handler(self.handler) }.into();
812 if err != FFIErrorCode::None {
813 crate::drop_failure(format_args!(
814 "failed to close syscall handler: {:?}",
815 err
816 ));
817 }
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use thruput_xdr::transaction::InvocationFrameStatusCode;
824
825 use crate::frame_builder::FrameAction;
826
827 use super::*;
828
829 fn root_addr() -> thruput_xdr::types::ContractAddress {
836 [0xA0u8; 32].into()
837 }
838
839 fn child_addr() -> thruput_xdr::types::ContractAddress {
840 [0xB0u8; 32].into()
841 }
842
843 fn sibling_addr() -> thruput_xdr::types::ContractAddress {
844 [0xC0u8; 32].into()
845 }
846
847 #[test]
848 fn misuse_inside_nested_execute_propagates_instead_of_faulting_parent() {
849 let env = TestEnvironment::new(None);
854 let mut handler = MockTransactionHandler::new(env);
855 let frame = handler
856 .open_root_frame(root_addr())
857 .with_invoke_flags(0)
858 .build();
859
860 let caught =
861 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
862 frame.execute(|frame| {
863 let child = frame
864 .open_child_frame(child_addr())
865 .with_invoke_flags(0)
866 .build();
867 child.execute(|_| FrameAction::Close);
868 let _ = child.result();
869 child.execute(|_| FrameAction::Close);
871 FrameAction::Close
872 });
873 }));
874
875 assert_unrecoverable_payload(caught.expect_err(
876 "misuse inside a nested execute closure must propagate as a \
877 panic, not close the parent frame as a contract fault",
878 ));
879 }
880
881 #[test]
882 fn mock_only_transaction_reports_no_rv32_stack_usage() {
883 let env = TestEnvironment::new(None);
887 let mut handler = MockTransactionHandler::new(env);
888
889 let result = handler
890 .open_root_frame(root_addr())
891 .with_invoke_flags(0)
892 .build()
893 .execute(|frame| {
894 let child = frame
895 .open_child_frame([2u8; 32].into())
896 .build()
897 .execute(|_| FrameAction::Close)
898 .result();
899 assert_eq!(child.rv32_stack_usage(), None);
900 assert!(child.rv32_stack_usage_records().is_empty());
901 FrameAction::Close
902 })
903 .result();
904 assert_eq!(result.rv32_stack_usage(), None);
905 assert!(result.rv32_stack_usage_records().is_empty());
906 }
907
908 #[test]
909 fn test_mock_transaction_handler() {
910 let env = TestEnvironment::new(None);
911 let mut handler = MockTransactionHandler::new(env);
912
913 static mut FIRST: bool = false;
914 static mut SECOND: bool = false;
915
916 let result = handler
917 .open_root_frame(root_addr())
918 .with_invoke_flags(0)
919 .build()
920 .execute(|_| {
921 unsafe {
922 FIRST = true;
923 }
924 FrameAction::Continue
925 })
926 .execute(|_| {
927 unsafe {
928 SECOND = true;
929 }
930 FrameAction::Close
931 })
932 .result();
933
934 assert!(unsafe { FIRST });
935 assert!(unsafe { SECOND });
936 assert_eq!(
937 result.output().status_code(),
938 InvocationFrameStatusCode::Return
939 );
940 }
941
942 #[test]
943 fn panic_with_open_transaction_is_not_masked_by_drop() {
944 let caught =
956 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
957 let env = TestEnvironment::new(None);
958 let mut handler = MockTransactionHandler::new(env);
959 handler
962 .open_root_frame(root_addr())
963 .with_invoke_flags(0)
964 .build();
965 panic!("original failure message");
966 }));
967
968 let payload = caught.expect_err("the panic must propagate, not abort");
969 let msg = payload
970 .downcast_ref::<&'static str>()
971 .copied()
972 .or_else(|| payload.downcast_ref::<String>().map(String::as_str));
973 assert_eq!(
974 msg,
975 Some("original failure message"),
976 "the original panic message must survive Drop cleanup"
977 );
978 }
979
980 #[test]
981 fn test_noop_mock_transaction_handler() {
982 let env = TestEnvironment::new(None);
983 let _handler = MockTransactionHandler::new(env);
984 }
985
986 #[test]
987 fn test_mock_transaction_handler_with_error() {
988 let env = TestEnvironment::new(None);
989 let mut handler = MockTransactionHandler::new(env);
990
991 let result = handler
992 .open_root_frame(root_addr())
993 .with_invoke_flags(0)
994 .build()
995 .execute(|_| {
996 panic!("test error");
997 })
998 .result();
999 assert_eq!(
1000 result.output().status_code(),
1001 InvocationFrameStatusCode::Fault
1002 );
1003 }
1004
1005 #[test]
1006 fn test_nested_mocks() {
1007 let env = TestEnvironment::new(None);
1008 let mut handler = MockTransactionHandler::new(env);
1009
1010 static mut FIRST: bool = false;
1011 static mut SECOND: bool = false;
1012
1013 let result = handler
1014 .open_root_frame(root_addr())
1015 .with_invoke_flags(0)
1016 .build()
1017 .execute(|frame| {
1018 frame
1019 .open_child_frame(child_addr())
1020 .with_invoke_flags(0)
1021 .build()
1022 .execute(|_| {
1023 unsafe {
1024 assert!(!FIRST);
1025 SECOND = true;
1026 }
1027 FrameAction::Close
1028 })
1029 .result();
1030
1031 unsafe {
1032 assert!(SECOND);
1033 FIRST = true;
1034 }
1035 FrameAction::Close
1036 })
1037 .result();
1038 assert_eq!(
1039 result.output().status_code(),
1040 InvocationFrameStatusCode::Return
1041 );
1042 assert!(unsafe { FIRST });
1043 assert!(unsafe { SECOND });
1044 }
1045
1046 #[test]
1047 fn test_syscalls_with_context() {
1048 let env = TestEnvironment::new(None);
1049 let mut handler = MockTransactionHandler::new(env);
1050
1051 let result = handler
1052 .open_root_frame(thruput_xdr::types::ContractAddress::from(
1053 [1; 32],
1054 ))
1055 .with_invoke_flags(0)
1056 .build()
1057 .execute(|_| {
1058 std::println!("getting current address");
1059 let res = thruput_xdr::syscalls::GetCurrentAddress();
1060 std::println!("res: {:?}", res);
1061 assert_eq!(
1062 res.unwrap(),
1063 thruput_xdr::types::ContractAddress::from([1; 32])
1064 );
1065 FrameAction::Close
1066 })
1067 .result();
1068 assert_eq!(
1069 result.output().status_code(),
1070 InvocationFrameStatusCode::Return
1071 );
1072 }
1073
1074 #[test]
1075 fn test_nested_frames_keep_panic_hook_active() {
1076 let env = TestEnvironment::new(None);
1077 let mut handler = MockTransactionHandler::new(env);
1078
1079 handler
1080 .open_root_frame(root_addr())
1081 .with_invoke_flags(0)
1082 .build()
1083 .execute(|frame| {
1084 assert!(crate::invoke_error::is_panic_hook_active());
1086
1087 frame
1088 .open_child_frame(child_addr())
1089 .with_invoke_flags(0)
1090 .build()
1091 .execute(|_| {
1093 assert!(crate::invoke_error::is_panic_hook_active());
1094 FrameAction::Continue
1095 })
1096 .execute(|_| {
1098 assert!(crate::invoke_error::is_panic_hook_active());
1099 FrameAction::Close
1100 })
1101 .result();
1102
1103 assert!(crate::invoke_error::is_panic_hook_active());
1106
1107 FrameAction::Close
1108 })
1109 .result();
1110
1111 assert!(!crate::invoke_error::is_panic_hook_active());
1113 }
1114
1115 #[test]
1116 fn test_nested_frame_panic_still_caught_by_outer() {
1117 let env = TestEnvironment::new(None);
1118 let mut handler = MockTransactionHandler::new(env);
1119
1120 let result = handler
1121 .open_root_frame(root_addr())
1122 .with_invoke_flags(0)
1123 .build()
1124 .execute(|frame| {
1125 frame
1127 .open_child_frame(child_addr())
1128 .with_invoke_flags(0)
1129 .build()
1130 .execute(|_| {
1131 panic!("inner contract panic");
1132 })
1133 .result();
1134
1135 assert!(crate::invoke_error::is_panic_hook_active());
1137
1138 FrameAction::Close
1139 })
1140 .result();
1141
1142 assert_eq!(
1143 result.output().status_code(),
1144 InvocationFrameStatusCode::Return
1145 );
1146 }
1147
1148 #[test]
1149 fn test_interleaved_handlers_keep_panic_hook_active() {
1150 let env = TestEnvironment::new(None);
1151 let mut handler1 = MockTransactionHandler::new(env.clone());
1152 let mut handler2 = MockTransactionHandler::new(env);
1153
1154 assert!(!crate::invoke_error::is_panic_hook_active());
1155
1156 let frame1 = handler1
1158 .open_root_frame(root_addr())
1159 .with_invoke_flags(0)
1160 .build()
1161 .execute(|frame| {
1162 assert!(crate::invoke_error::is_panic_hook_active());
1163
1164 frame
1165 .open_child_frame(child_addr())
1166 .with_invoke_flags(0)
1167 .build()
1168 .execute(|_| {
1169 assert!(crate::invoke_error::is_panic_hook_active());
1170 FrameAction::Close
1171 })
1172 .result();
1173
1174 assert!(crate::invoke_error::is_panic_hook_active());
1175 FrameAction::Continue
1176 });
1177 assert!(!crate::invoke_error::is_panic_hook_active());
1178
1179 let frame2 = handler2
1181 .open_root_frame(root_addr())
1182 .with_invoke_flags(0)
1183 .build()
1184 .execute(|_| {
1185 assert!(crate::invoke_error::is_panic_hook_active());
1186 FrameAction::Continue
1187 });
1188 assert!(!crate::invoke_error::is_panic_hook_active());
1189
1190 let result1 = frame1
1192 .execute(|frame| {
1193 assert!(crate::invoke_error::is_panic_hook_active());
1194
1195 frame
1196 .open_child_frame(child_addr())
1197 .with_invoke_flags(0)
1198 .build()
1199 .execute(|_| {
1200 panic!("child panic in handler1");
1201 })
1202 .result();
1203
1204 assert!(crate::invoke_error::is_panic_hook_active());
1206 FrameAction::Close
1207 })
1208 .result();
1209 assert!(!crate::invoke_error::is_panic_hook_active());
1210
1211 let result2 = frame2
1213 .execute(|frame| {
1214 assert!(crate::invoke_error::is_panic_hook_active());
1215
1216 frame
1217 .open_child_frame(child_addr())
1218 .with_invoke_flags(0)
1219 .build()
1220 .execute(|_| {
1221 assert!(crate::invoke_error::is_panic_hook_active());
1222 FrameAction::Continue
1223 })
1224 .execute(|_| {
1225 assert!(crate::invoke_error::is_panic_hook_active());
1226 FrameAction::Close
1227 })
1228 .result();
1229
1230 assert!(crate::invoke_error::is_panic_hook_active());
1231 FrameAction::Close
1232 })
1233 .result();
1234 assert!(!crate::invoke_error::is_panic_hook_active());
1235
1236 assert_eq!(
1237 result1.output().status_code(),
1238 InvocationFrameStatusCode::Return
1239 );
1240 assert_eq!(
1241 result2.output().status_code(),
1242 InvocationFrameStatusCode::Return
1243 );
1244 }
1245
1246 #[track_caller]
1249 fn assert_unrecoverable_payload(payload: Box<dyn std::any::Any + Send>) {
1250 let err = payload
1251 .downcast_ref::<InvokeError>()
1252 .expect("panic payload must be an InvokeError");
1253 assert_eq!(
1254 err.error_type,
1255 thruput_trace::ffi_enums::InvokeErrorCode::Unrecoverable
1256 );
1257 }
1258
1259 #[test]
1260 fn closing_parent_with_open_child_panics_unrecoverably() {
1261 let env = TestEnvironment::new(None);
1268 let mut handler = MockTransactionHandler::new(env);
1269 let frame = handler
1270 .open_root_frame(root_addr())
1271 .with_invoke_flags(0)
1272 .build();
1273
1274 let caught =
1275 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1276 frame.execute(|frame| {
1277 frame
1279 .open_child_frame(child_addr())
1280 .with_invoke_flags(0)
1281 .build()
1282 .execute(|_| FrameAction::Continue);
1283 FrameAction::Close
1285 });
1286 }));
1287
1288 assert_unrecoverable_payload(
1289 caught.expect_err("closing with an open child must panic"),
1290 );
1291 }
1292
1293 #[test]
1294 fn parent_panic_with_open_child_panics_unrecoverably() {
1295 let env = TestEnvironment::new(None);
1299 let mut handler = MockTransactionHandler::new(env);
1300 let frame = handler
1301 .open_root_frame(root_addr())
1302 .with_invoke_flags(0)
1303 .build();
1304
1305 let caught =
1306 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1307 frame.execute(|frame| -> FrameAction {
1308 frame
1309 .open_child_frame(child_addr())
1310 .with_invoke_flags(0)
1311 .build()
1312 .execute(|_| FrameAction::Continue);
1313 panic!("original contract failure");
1314 });
1315 }));
1316
1317 assert_unrecoverable_payload(
1318 caught.expect_err("failing with an open child must panic"),
1319 );
1320 }
1321
1322 #[test]
1323 fn executing_frame_with_open_child_panics_unrecoverably() {
1324 let env = TestEnvironment::new(None);
1331 let mut handler = MockTransactionHandler::new(env);
1332 let frame = handler
1333 .open_root_frame(root_addr())
1334 .with_invoke_flags(0)
1335 .build();
1336
1337 frame.execute(|frame| {
1338 frame
1339 .open_child_frame(child_addr())
1340 .with_invoke_flags(0)
1341 .build()
1342 .execute(|_| FrameAction::Continue);
1343 FrameAction::Continue
1345 });
1346
1347 let caught =
1348 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1349 frame.execute(|_| FrameAction::Continue);
1353 }));
1354
1355 assert_unrecoverable_payload(
1356 caught
1357 .expect_err("executing a frame with an open child must panic"),
1358 );
1359 }
1360
1361 #[test]
1362 fn opening_second_child_while_first_open_panics_unrecoverably() {
1363 let env = TestEnvironment::new(None);
1367 let mut handler = MockTransactionHandler::new(env);
1368 let frame = handler
1369 .open_root_frame(root_addr())
1370 .with_invoke_flags(0)
1371 .build();
1372
1373 frame.execute(|frame| {
1374 frame
1376 .open_child_frame(child_addr())
1377 .with_invoke_flags(0)
1378 .build()
1379 .execute(|_| FrameAction::Continue);
1380 FrameAction::Continue
1381 });
1382
1383 let caught =
1384 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1385 frame.open_child_frame(sibling_addr());
1388 }));
1389
1390 assert_unrecoverable_payload(caught.expect_err(
1391 "opening a second child while one is open must panic",
1392 ));
1393 }
1394
1395 #[test]
1396 fn test_concurrent_transactions() {
1397 let env = TestEnvironment::new(None);
1398 let mut handler1 = MockTransactionHandler::new(env.clone());
1399 let mut handler2 = MockTransactionHandler::new(env);
1400
1401 let frame1 = handler1
1402 .open_root_frame(thruput_xdr::types::ContractAddress::from(
1403 [1; 32],
1404 ))
1405 .with_invoke_flags(0)
1406 .build()
1407 .execute(|_| {
1408 std::println!("getting current address");
1409 let res = thruput_xdr::syscalls::GetCurrentAddress();
1410 std::println!("res: {:?}", res);
1411 assert_eq!(
1412 res.unwrap(),
1413 thruput_xdr::types::ContractAddress::from([1; 32])
1414 );
1415 FrameAction::Continue
1416 });
1417
1418 let frame2 = handler2
1419 .open_root_frame(thruput_xdr::types::ContractAddress::from(
1420 [2; 32],
1421 ))
1422 .with_invoke_flags(0)
1423 .build()
1424 .execute(|_| {
1425 std::println!("getting current address");
1426 let res = thruput_xdr::syscalls::GetCurrentAddress();
1427 std::println!("res: {:?}", res);
1428 assert_eq!(
1429 res.unwrap(),
1430 thruput_xdr::types::ContractAddress::from([2; 32])
1431 );
1432 FrameAction::Continue
1433 });
1434 let result1 = frame1
1435 .execute(|_| {
1436 std::println!("getting current address");
1437 let res = thruput_xdr::syscalls::GetCurrentAddress();
1438 std::println!("res: {:?}", res);
1439 assert_eq!(
1440 res.unwrap(),
1441 thruput_xdr::types::ContractAddress::from([1; 32])
1442 );
1443 FrameAction::Close
1444 })
1445 .result();
1446
1447 let result2 = frame2
1448 .execute(|_| {
1449 std::println!("getting current address");
1450 let res = thruput_xdr::syscalls::GetCurrentAddress();
1451 std::println!("res: {:?}", res);
1452 assert_eq!(
1453 res.unwrap(),
1454 thruput_xdr::types::ContractAddress::from([2; 32])
1455 );
1456 FrameAction::Close
1457 })
1458 .result();
1459
1460 assert_eq!(
1461 result1.output().status_code(),
1462 InvocationFrameStatusCode::Return
1463 );
1464 assert_eq!(
1465 result2.output().status_code(),
1466 InvocationFrameStatusCode::Return
1467 );
1468 }
1469
1470 #[test]
1473 fn refusal_messages_name_the_rule() {
1474 let msg = frame_refusal_message(
1475 SyscallErrorCode::kSyscallErrorCodeReentrance,
1476 );
1477 assert!(msg.contains("distinct addresses"), "{msg}");
1478
1479 let msg = frame_refusal_message(
1480 SyscallErrorCode::kSyscallErrorCodeInvokeDepthExceeded,
1481 );
1482 assert!(msg.contains("max_cross_invoke_depth"), "{msg}");
1483
1484 let msg = frame_refusal_message(
1485 SyscallErrorCode::kSyscallErrorCodeInvalidSyscallInput,
1486 );
1487 assert!(msg.contains("TryInvoke"), "{msg}");
1488
1489 let msg = frame_refusal_message(SyscallErrorCode(9999));
1491 assert!(msg.contains("9999"), "{msg}");
1492
1493 assert!(msg.contains("try_call_raw_with"), "{msg}");
1495 }
1496
1497 #[test]
1502 fn a_refused_frame_propagates_instead_of_faulting_its_parent() {
1503 let env = TestEnvironment::new(None);
1504 let mut handler = MockTransactionHandler::new(env);
1505 let frame = handler
1506 .open_root_frame(root_addr())
1507 .with_invoke_flags(0)
1508 .build();
1509
1510 let caught =
1511 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1512 frame.execute(|frame| {
1513 frame
1516 .open_child_frame(root_addr())
1517 .with_invoke_flags(0)
1518 .build();
1519 FrameAction::Close
1520 });
1521 }));
1522
1523 assert_unrecoverable_payload(caught.expect_err(
1524 "a refused frame must propagate as unrecoverable, not close \
1525 the parent frame as a contract fault",
1526 ));
1527 }
1528
1529 #[test]
1533 fn read_only_invoke_flag_refuses_writes_in_the_frame() {
1534 let env = TestEnvironment::new(None);
1535 env.setup_grant_storage_tokens(&child_addr(), 1_000_000_000);
1536 let mut handler = MockTransactionHandler::new(env);
1537 let key = thruput_xdr::types::StorageKeyContractData::new([0x5A; 32]);
1538 let read_only = thruput_xdr::invoke_flag::InvokeFlag::ReadOnly
1539 .0
1540 .cast_unsigned();
1541
1542 let result = handler
1543 .open_root_frame(root_addr())
1544 .with_invoke_flags(0)
1545 .build()
1546 .execute(|frame| {
1547 for (flags, admitted) in [(read_only, false), (0, true)] {
1548 let child = frame
1549 .open_child_frame(child_addr())
1550 .with_invoke_flags(flags)
1551 .build();
1552 child.execute(|_| {
1553 let res = thruput_xdr::syscalls::raw_BytestringSet(
1554 &key, b"x",
1555 );
1556 if admitted {
1557 res.unwrap();
1558 } else {
1559 assert!(res.unwrap_err().is_invalid_input());
1560 }
1561 FrameAction::Close
1562 });
1563 assert_eq!(
1566 child.result().output().status_code(),
1567 InvocationFrameStatusCode::Return,
1568 "flags {flags:#x}",
1569 );
1570 }
1571 FrameAction::Close
1572 })
1573 .result();
1574 assert_eq!(
1575 result.output().status_code(),
1576 InvocationFrameStatusCode::Return
1577 );
1578 }
1579}