Skip to main content

thruput_native_exec/
mock_transaction_handler.rs

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
42/// Builder for one invocation frame's configuration, opened with
43/// [`build`](Self::build).
44pub 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    // when we build the frame, the new frame will be emplaced into this value
51    to_emplace: &'a mut Option<Box<MockInvocationFrame>>,
52    root_frame_closed: Option<Rc<Cell<bool>>>,
53}
54
55impl<'a> MockInvocationFrameConfig<'a> {
56    // The invoked address is a constructor parameter rather than an optional
57    // builder step: the test FFI admits mock frames under production's
58    // reentrance rule, which refuses an address that already has an active
59    // frame. A default would make "forgot to set it" the
60    // reentrant case, and only at run time.
61    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    /// Set the calldata the invoked frame observes (default: empty).
84    pub fn with_calldata(mut self, calldata: xdr_common::BytesM) -> Self {
85        self.invocation.calldata = calldata;
86        self
87    }
88
89    /// Set the frame's invoke flags, a bitset of
90    /// [`InvokeFlag`](thruput_xdr::invoke_flag::InvokeFlag) values
91    /// (default: none).
92    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    /// Set the frame's gas limit (default: `u64::MAX`).
101    pub fn with_gas_limit(mut self, gas_limit: u64) -> Self {
102        self.invocation.gas_limit = gas_limit;
103        self
104    }
105
106    /// Open the frame at the FFI level, returning the
107    /// [`MockInvocationFrame`].
108    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
123/// One open invocation frame in a mocked transaction. Drive it with
124/// [`execute`](Self::execute), then read the outcome with
125/// [`result`](Self::result). Prefer the fluent
126/// [`crate::frame_builder::FrameBuilder`] API unless a test needs this
127/// level of control.
128pub 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    /// The panic that faulted THIS frame's body, recorded at `execute`'s
135    /// catch site.
136    ///
137    /// Set only for a plain panic out of this frame's own lambda, which is
138    /// what makes it this frame's rather than a nested one's: a child frame
139    /// catches its own panic and closes itself, so the panic never reaches
140    /// here; and a `raw_Invoke`ed callee is caught by `invoke_wrapper`,
141    /// which has no frame to record against at all (see `fault_panic`).
142    fault_panic: Option<crate::invoke_error::PanicRecord>,
143    /// Where this frame's rv32 stack records begin: the handler's record
144    /// count when the frame opened. The handler appends one record per rv32
145    /// invocation as it finishes, for the life of the transaction, so every
146    /// record from this index on was appended while this frame was open.
147    rv32_records_at_open: usize,
148    /// This frame's record window, set when it closes and taken by its
149    /// parent (see `claim_child_rv32_window`), so the calls made under a
150    /// child frame are never counted as its parent's own.
151    unclaimed_rv32_window: Option<Range<usize>>,
152    /// The record windows of this frame's closed child frames.
153    child_rv32_windows: Vec<Range<usize>>,
154    /// This frame's own rv32 stack records (its window less its children's),
155    /// in finish order, read as it closed; see
156    /// [`FrameResult::rv32_stack_usage_records`].
157    rv32_stack_usage: Vec<Rv32StackUsage>,
158}
159
160/// Name the rule that refused a frame, rather than leaving a caller with a
161/// bare code. These are production's own refusals: a contract's `Invoke` is
162/// turned away for the same reasons, and sees the same codes.
163fn 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        // Two distinct failures, and the FFI keeps them apart: an FFI code
207        // means this harness called the API wrong, a syscall code means the
208        // call was well-formed and production refused it. Both open no frame.
209        let handler_res: thruput_sys::FFIErrorCode = res.ffi_error_code.into();
210        if handler_res != FFIErrorCode::None {
211            // An InvokeError payload, not a plain panic!: the Unrecoverable
212            // code propagates out of any enclosing frame instead of being
213            // recorded as a contract panic.
214            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        // The frame is open at the FFI level from here on, so a record the
227        // handler appends from now on is for a call made under it.
228        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    /// Note the held child's rv32 record window as the child's, not this
246    /// frame's. Called before the child is replaced by the next one and when
247    /// this frame closes, and idempotent, so each window is noted once. A
248    /// child abandoned to its `Drop` set no window and leaves nothing to
249    /// note; whatever ran under it then counts as this frame's.
250    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    /// True if this frame's child frame exists and is still open at the FFI
261    /// level. A frame may hold at most one open child at a time, and must
262    /// not be closed before its child is.
263    fn has_open_child(&self) -> bool {
264        matches!(
265            &self.child_frame,
266            Some(child) if child.state == MockInvocationFrameState::Started
267        )
268    }
269
270    /// Configure a child frame of this one, invoking `contract_address`.
271    ///
272    /// The address must differ from every frame already open on the stack: the
273    /// test FFI admits mock frames under the same reentrance rule production
274    /// applies to `Invoke`, so an address that already has an active frame is
275    /// refused and `build()` panics unrecoverably. Frames that
276    /// have already finished, and frames in other transactions, do not count —
277    /// only the ones open at the same time.
278    pub fn open_child_frame(
279        &mut self,
280        contract_address: thruput_xdr::types::ContractAddress,
281    ) -> MockInvocationFrameConfig<'_> {
282        // One open child at a time: `FrameAction::Continue` exists to
283        // interleave frames across separate transactions, not to nest
284        // multiple open frames inside one parent. (The test FFI lib guards
285        // this too, but replacing the open child Box would first Drop it,
286        // force-finishing the wrong FFI frame before the lib could object.)
287        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        // The outgoing child (closed, if there is one) is replaced when the
294        // new one is built; keep its rv32 calls its own first.
295        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        // Closing a frame while its child is still open is a user error.
311        // It cannot be honored: `finish_mock_invocation_frame` takes no
312        // frame identifier — it finishes the handler's innermost open frame
313        // — so proceeding would silently finish the CHILD instead, and the
314        // abandoned child's Drop would later finish what is at FFI level
315        // THIS frame, poisoning the transaction.
316        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            // An InvokeError payload, not a plain panic!: consumers assert
332            // on the Unrecoverable payload (see
333            // example-contracts/storage_errors).
334            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        // This frame's rv32 stack records, read now, while the handler's
368        // list ends with the calls made under this frame: the records
369        // appended since it opened, less those under its child frames.
370        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    /// Run `lambda` as this frame's contract code, mapping panics out of it
395    /// to frame outcomes. Unrecoverable [`InvokeError`] payloads (harness
396    /// misuse, FFI refusals) propagate instead. The returned `FrameAction`
397    /// closes the frame or leaves it open for another `execute`.
398    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        // Executing a frame while its child frame is still open would run
411        // this frame's code over the CHILD's FFI frame (the innermost open
412        // frame), misattributing syscalls, gas, and trace to the child. The
413        // child must be closed first. (Holding the child's handle already
414        // borrows this frame mutably, so this state is only reachable by
415        // losing the handle: opening a child inside an `execute` lambda and
416        // returning Continue with the child left open.)
417        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            // The lambda stands in for the frame's contract body -- an
428            // ordinary panic out of it is classified below as this frame
429            // failing deterministically -- so the hook covers it, and nests
430            // with the guards `trampoline` holds around real mock bodies.
431            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                    // Unrecoverable codes (harness misuse, FFI refusals)
449                    // propagate back out of the close as Unrecoverable
450                    // rather than becoming this frame's outcome.
451                    self.close_frame_internal(my_error.error_type);
452                } else {
453                    // The lambda always runs with the panic hook active, so
454                    // an ordinary panic payload is contract code panicking
455                    // (already printed by the hook): a deterministic failure
456                    // of this frame.
457                    //
458                    // Keep the panic before closing: this is the one point
459                    // where the payload and the frame it belongs to are both
460                    // in hand, on one thread, with no ambiguity about which
461                    // frame faulted. The hook's record (which carries the
462                    // location) is taken here too, on that same thread.
463                    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    /// The closed frame's [`FrameResult`]. Panics if the frame is still
473    /// open.
474    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        // Close any open child frame first — the FFI requires inner frames be
496        // finished before their parent. (The `child_frame` field would
497        // otherwise drop after this body.)
498        self.child_frame = None;
499
500        if self.state == MockInvocationFrameState::Started {
501            // The frame's FFI handle is still open: a test almost certainly
502            // panicked (or returned early) before driving it to Close. Finish
503            // it at the FFI level so the frame's coroutine completes and the
504            // handler can be torn down cleanly. Leaving it open orphans the
505            // coroutine, which later wedges the libtest_ffi global
506            // `CoroPoll::~CoroPoll` -> `DestroyInhibit::destroy_wait()` and
507            // hangs the process at exit. We must NOT panic here: we may already
508            // be unwinding from the original failure, and a second panic would
509            // abort with a SIGABRT that hides it. Force-finishing is the
510            // recoverable cleanup the old `panic!` never performed.
511            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            // An `UnrecoverableError` status is expected here — we finished an
518            // abandoned frame with the Unrecoverable code; it still completes
519            // the coroutine. Log only so an unexpected status isn't silent.
520            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/// Configuration for a mocked transaction, applied when the handler is
592/// created. Build one with [`TestEnvironment::transaction`], or use
593/// [`MockTransactionConfig::default`] for the standard setup.
594///
595/// This is the extension point for the mocked transaction context: new knobs
596/// (e.g. the source-tx hash) become fields here rather than new constructor
597/// overloads.
598#[derive(Clone)]
599pub struct MockTransactionConfig {
600    /// The auth-claim attached to the mocked transaction, observed by the
601    /// contract under test (e.g. via `get_auth_claim`).
602    pub auth_claim: AuthClaim,
603}
604
605impl MockTransactionConfig {
606    /// A `kSourceAddressAuthData` claim with no allowed keys and zero required weight.
607    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
626/// A mocked transaction context: runs one, and only one, transaction, with
627/// exactly one root frame ([`call`](Self::call) /
628/// [`open_root_frame`](Self::open_root_frame) panic on a second
629/// invocation). Nest further calls as child frames, or use a second
630/// transaction.
631pub 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    /// Create a mock transaction handler with the default configuration
642    /// (see [`MockTransactionConfig`]).
643    ///
644    /// Use [`TestEnvironment::transaction`] to customize the configuration
645    /// before creating the handler.
646    pub fn new(env: Rc<TestEnvironment>) -> Self {
647        Self::new_with_config(env, MockTransactionConfig::default())
648    }
649
650    /// Create a mock transaction handler from an explicit
651    /// [`MockTransactionConfig`], letting a test configure the context of the
652    /// mocked transaction.
653    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                /*enable_tracing*/ 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    /// True while a transaction has started but is neither committed nor
690    /// aborted.
691    pub(crate) fn is_running(&self) -> bool {
692        self.state == MockTransactionState::Running
693    }
694
695    /// Abort the running transaction, discarding its writes.
696    ///
697    /// Panics unless the transaction is running and its root frame is
698    /// closed.
699    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    /// Attempt the two-phase commit of the running transaction.
714    ///
715    /// Panics unless the transaction is running and its root frame is
716    /// closed.
717    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    /// Convenience: open a root frame targeting the given contract address.
738    /// Returns a FrameBuilder for configuring and executing the frame.
739    ///
740    /// A transaction has exactly one root frame, so a second `call` panics;
741    /// call further contracts via child frames (`FrameHandle::child_call`,
742    /// [`MockInvocationFrame::open_child_frame`]) or a separate
743    /// transaction.
744    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    /// Configure this transaction's root frame, invoking `contract_address`.
753    ///
754    /// A transaction has exactly one root frame: calling this (or
755    /// [`call`](Self::call)) a second time on the same handler panics.
756    ///
757    /// Child frames opened underneath it must name different addresses; see
758    /// [`MockInvocationFrame::open_child_frame`].
759    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        // Tear down in the order the FFI requires: finish any still-open
783        // invocation frame BEFORE aborting the transaction and closing the
784        // handler. Field-drop order would otherwise run the frame's
785        // `finish_mock_invocation_frame` dead-last — after `close_syscall_handler`
786        // (handler body) and after `_env` drops `free_db`/`close_runtime_context`
787        // — so every FFI call returns `UnrecoverableError` and the frame's
788        // coroutine is left in-flight, wedging the libtest_ffi global
789        // `CoroPoll::~CoroPoll` -> `DestroyInhibit::destroy_wait()` at process
790        // exit. Dropping `root_frame` here finishes the frame while the handler
791        // is still live.
792        self.root_frame = None;
793
794        if self.state == MockTransactionState::Running {
795            // Abort the uncommitted transaction. Inlined rather than calling
796            // `abort_transaction` to avoid its asserts: we may be unwinding from
797            // a test failure, and a panic in `drop` would double-panic into a
798            // SIGABRT that hides the original message.
799            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    // Frames that are open at the same time need distinct invoked_addresses:
830    // the test FFI admits mock frames under production's reentrance rule, which
831    // refuses an address that already has an active frame on the stack.
832    // Addresses may be reused across separate handlers (separate
833    // transactions, separate frame stacks) and by sequential frames at the same
834    // depth, so these three cover every nesting the tests below build.
835    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        // Regression: a misuse guard firing inside an enclosing frame's
850        // `execute` lambda (here: executing an already-closed child) must
851        // propagate out as Unrecoverable, not be recorded as a
852        // deterministic contract fault of the parent.
853        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                    // Misuse: drive the already-closed child again.
870                    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        // Mock contracts run natively, so nothing records a stack: a frame
884        // closes with no records rather than failing on the read, and so
885        // does a child frame opened and closed under it.
886        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        // Regression: a test that panics while a root frame is still open used
945        // to abort the whole process. Originally the Drop impls double-panicked
946        // (`MockInvocationFrame::drop` asserted the frame wasn't `Started`;
947        // `MockTransactionHandler::drop`->`abort_transaction` asserted the root
948        // frame was closed), turning a clean test failure into a SIGABRT that
949        // hid the message. Once those panics were removed, a second bug showed:
950        // the Drops tore the FFI down in the wrong order (handler/db freed
951        // before the open frame was finished), orphaning the frame's coroutine
952        // and hanging the process at exit in `CoroPoll::~CoroPoll`. The Drops
953        // now finish the open frame first and never panic, so the original
954        // panic propagates cleanly and the process exits.
955        let caught =
956            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
957                let env = TestEnvironment::new(None);
958                let mut handler = MockTransactionHandler::new(env);
959                // Transaction -> Running, root frame -> Started, but never
960                // closed or committed.
961                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                // Outer frame: the hook covers the frame's body.
1085                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                    // First execute: Continue, then the child's guard drops.
1092                    .execute(|_| {
1093                        assert!(crate::invoke_error::is_panic_hook_active());
1094                        FrameAction::Continue
1095                    })
1096                    // Second execute: still covered inside, then Close.
1097                    .execute(|_| {
1098                        assert!(crate::invoke_error::is_panic_hook_active());
1099                        FrameAction::Close
1100                    })
1101                    .result();
1102
1103                // The child finishing must not uncover the outer frame. This
1104                // is why the marker counts rather than flags.
1105                assert!(crate::invoke_error::is_panic_hook_active());
1106
1107                FrameAction::Close
1108            })
1109            .result();
1110
1111        // After everything: no frame is running.
1112        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                // Child panics
1126                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                // Outer frame should still be active and running
1136                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        // handler1: Continue with nested child frame
1157        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        // handler2: Continue (no children)
1180        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        // handler1: Close with nested child that panics
1191        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                // The child panicking must not uncover this frame.
1205                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        // handler2: Close with nested child doing Continue then Close
1212        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    /// Assert a caught panic payload is the unrecoverable [`InvokeError`]
1247    /// raised by the frame-nesting guards.
1248    #[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        // Closing a frame while its child frame is still open is a clear
1262        // user error: `finish_mock_invocation_frame` takes no frame
1263        // identifier — it finishes the handler's innermost open frame — so
1264        // the close would silently finish the CHILD instead, and the
1265        // abandoned child's Drop would later finish what is at FFI level the
1266        // PARENT frame, poisoning the transaction. Guard it loudly.
1267        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                    // Leave the child frame open (Continue, never Close)...
1278                    frame
1279                        .open_child_frame(child_addr())
1280                        .with_invoke_flags(0)
1281                        .build()
1282                        .execute(|_| FrameAction::Continue);
1283                    // ...and close the parent anyway.
1284                    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        // Same guard via `execute()`'s Err arm: the lambda fails while a
1296        // child frame is still open. The frame cannot be closed out of
1297        // order, so the propagated panic is the unrecoverable guard error.
1298        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        // Executing a frame while its child frame is still open would run
1325        // the parent's code over the CHILD's FFI frame (the innermost open
1326        // frame), misattributing syscalls, gas, and trace to the child.
1327        // This state is only reachable by opening a child inside an
1328        // `execute` lambda and returning Continue with the child left open
1329        // — losing the child handle. Guard it loudly.
1330        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            // Child handle is lost here, child still open.
1344            FrameAction::Continue
1345        });
1346
1347        let caught =
1348            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1349                // Continue, not Close: the guard must fire on entry, before
1350                // any parent code runs over the child's FFI frame — not
1351                // merely when a close path happens to be reached.
1352                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        // A frame holds at most ONE open child frame at a time. `Continue`
1364        // exists to interleave frames across separate transactions, not to
1365        // nest multiple open frames inside one parent.
1366        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            // First child stays open.
1375            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                // A fresh address, so the panic is the one-open-child guard
1386                // rather than anything the reentrance rule would have caught.
1387                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    // The point of test-ffi 0.32.0: a refused frame says which rule refused
1471    // it. Production hands a contract the same codes for the same reasons.
1472    #[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        // An unnamed code still reports itself rather than going silent.
1490        let msg = frame_refusal_message(SyscallErrorCode(9999));
1491        assert!(msg.contains("9999"), "{msg}");
1492
1493        // Every message points at the path that can handle the condition.
1494        assert!(msg.contains("try_call_raw_with"), "{msg}");
1495    }
1496
1497    // The diagnostic only reaches the developer if the panic carrying it
1498    // gets out. An enclosing frame's catch site records any other payload
1499    // as a deterministic fault in its own body, which would close the
1500    // parent as failed and leave the refusal unreported.
1501    #[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                    // Reentrance: the root's address already has a frame
1514                    // open on this stack.
1515                    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    // Invoke flags reach the frame with their bits intact: a child opened
1530    // ReadOnly refuses the write that the same child, opened without flags,
1531    // makes.
1532    #[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                    // A failed assertion above faults the child rather than
1564                    // failing the test, so check how it closed.
1565                    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}