Skip to main content

thruput_native_exec/
transaction_handler.rs

1use crate::rv32_stack_usage::Rv32StackUsage;
2use crate::test_environment::TestEnvironment;
3use core::ffi::c_void;
4use std::rc::Rc;
5use thruput_trace::InvocationFrameTrace;
6use thruput_xdr::event::TxEmittedEvent;
7use thruput_xdr::transaction::InvocationFrameOutput;
8use thruput_xdr::transaction::SignedTransaction;
9use xdrs::txs;
10
11use thruput_sys::FFIErrorCode;
12
13/// Executes one, and only one, full [`SignedTransaction`] against the
14/// environment: [`root_invoke`](Self::root_invoke), then
15/// [`try_commit_transaction`](Self::try_commit_transaction) or
16/// [`abort_transaction`](Self::abort_transaction).
17pub struct TransactionHandler {
18    handler: *mut c_void,
19    _env: Rc<TestEnvironment>,
20    stx: SignedTransaction,
21    invoked: bool,
22    committed: bool,
23    aborted: bool,
24}
25
26/// Result of a transaction's root invocation ([`TransactionHandler::root_invoke`]).
27pub struct RootInvokeResult {
28    /// The root frame's outcome.
29    pub output: InvocationFrameOutput,
30    /// Gas consumed by the invocation.
31    pub gas_consumed: u64,
32    /// The execution trace, when tracing was enabled.
33    pub trace: Option<InvocationFrameTrace>,
34    /// Events emitted by the transaction.
35    pub events: Vec<TxEmittedEvent>,
36    /// The stack records of the rv32 invocations the transaction ran, in
37    /// the order they finished: a callee's before its caller's. When the
38    /// root invocation targets an rv32 contract its own record is the last;
39    /// when it targets a mock contract, every record is a call made under
40    /// it. Empty if it ran none. See [`Rv32StackUsage`].
41    pub rv32_stack_usage_records: Vec<Rv32StackUsage>,
42}
43
44impl RootInvokeResult {
45    /// The most recent of
46    /// [`rv32_stack_usage_records`](Self::rv32_stack_usage_records): the
47    /// root invocation's own stack record when it targets an rv32 contract,
48    /// else the last rv32 call made under it. `None` if the transaction ran
49    /// none.
50    pub fn rv32_stack_usage(&self) -> Option<Rv32StackUsage> {
51        self.rv32_stack_usage_records.last().copied()
52    }
53}
54
55impl TransactionHandler {
56    /// Open a handler for executing `stx` against `env`.
57    pub fn new(env: Rc<TestEnvironment>, stx: &SignedTransaction) -> Self {
58        let tx_bytes = txs::encode(stx).unwrap();
59
60        let handler_res = unsafe {
61            thruput_sys::open_syscall_handler(
62                env.inner.borrow().db.db,
63                env.inner.borrow().contract_context.context,
64                thruput_sys::FFISerializedSignedTransaction {
65                    data: tx_bytes.as_ptr() as *mut u8,
66                    len: tx_bytes.len().try_into().unwrap(),
67                },
68            )
69        };
70        let err: FFIErrorCode = handler_res.ffi_error_code.into();
71        if err != FFIErrorCode::None {
72            panic!("failed to open syscall handler: {:?}", err);
73        }
74        assert!(!handler_res.syscall_handler.is_null());
75        Self {
76            handler: handler_res.syscall_handler,
77            _env: env.clone(),
78            stx: stx.clone(),
79            invoked: false,
80            committed: false,
81            aborted: false,
82        }
83    }
84
85    /// Invokes the transaction represented by the TransactionHandler.
86    /// This must be called before other methods that rely on actions taken within the transaction.
87    /// This method must only be called once per transaction.
88    ///
89    /// Returns the result of the root invocation.
90    /// This does NOT commit the transaction to the database.
91    /// The caller should subsequently call try_commit_transaction or abort_transaction
92    /// before advancing to the next block.
93    ///
94    /// try_commit_transaction checks the result of the root invocation,
95    /// and will abort the transaction if the root invocation failed.
96    /// So it is safe to call either try_commit_transaction or abort_transaction
97    /// after root_invoke, regardless of whether the root invocation succeeded or failed.
98    pub fn root_invoke(&mut self) -> RootInvokeResult {
99        if self.invoked {
100            panic!("cannot double invoke on a TransactionHandler")
101        }
102        self.invoked = true;
103        assert!(!self.committed && !self.aborted);
104
105        // No panic-hook activation here: contract bodies activate it on entry
106        // (see `PanicHookGuard`), and nothing else under this call is contract
107        // code -- rv32 contracts run in the interpreter, and the rest is this
108        // crate's own marshalling, where a panic is a harness bug that should
109        // stay unrecoverable and print in full.
110        let foreign_result: thruput_sys::ForeignTracedInvokeResult = unsafe {
111            thruput_sys::root_invoke(self.handler, /*tracing*/ 1)
112        };
113
114        let err: FFIErrorCode = foreign_result.ffi_error_code.into();
115        if err != FFIErrorCode::None {
116            panic!("FFI error: failed to invoke transaction: {err}");
117        }
118
119        let trace = foreign_result.serialized_trace.as_slice().map(|s| {
120            txs::decode(s)
121                .unwrap_or_else(|e| panic!("failed to decode trace: {}", e))
122        });
123
124        let output = foreign_result.serialized_output.as_slice().map(|s| {
125            txs::decode(s)
126                .unwrap_or_else(|e| panic!("failed to decode output: {}", e))
127        });
128
129        let events: Vec<TxEmittedEvent> = if let Some(events_data) =
130            foreign_result.serialized_events.as_slice()
131        {
132            let decoded: xdr_common::VecM<TxEmittedEvent> =
133                txs::decode(events_data).unwrap_or_else(|e| {
134                    panic!(
135                        "failed to decode events ({} bytes): {}",
136                        events_data.len(),
137                        e
138                    )
139                });
140            decoded.into()
141        } else {
142            Vec::new()
143        };
144
145        RootInvokeResult {
146            output: output.unwrap(),
147            gas_consumed: foreign_result.gas_consumed,
148            trace,
149            events,
150            // Every record on the handler is this transaction's: the
151            // handler is opened for the one transaction, and the root
152            // invocation is the only thing that runs on it.
153            rv32_stack_usage_records: crate::rv32_stack_usage::records(
154                self.handler,
155            ),
156        }
157    }
158
159    /// The signed transaction this handler executes.
160    pub fn get_signed_transaction(&self) -> &SignedTransaction {
161        &self.stx
162    }
163
164    /// Tries to commit the transaction to the database.
165    /// This must be called after root_invoke, and only once per transaction.
166    ///
167    /// Returns the [`TryCommitResult`](crate::TryCommitResult) reported by the
168    /// executor; check it to see whether the commit succeeded. On a
169    /// nondeterministic (two-phase commit) failure it carries the
170    /// storage-conflict diagnostic.
171    ///
172    /// This will abort the transaction if the root invocation failed.
173    /// So it is safe to call either try_commit_transaction or abort_transaction
174    /// after root_invoke, regardless of whether the root invocation succeeded or failed.
175    ///
176    /// # Panics
177    ///
178    /// Panics if called before `root_invoke`, or if the transaction was
179    /// already committed or aborted.
180    pub fn try_commit_transaction(&mut self) -> crate::TryCommitResult {
181        if !self.invoked {
182            panic!(
183                "cannot try to commit a transaction that has not been invoked"
184            );
185        }
186        if self.committed || self.aborted {
187            panic!(
188                "cannot try to commit a transaction that has already been committed or aborted"
189            );
190        }
191        self.committed = true;
192        let res = unsafe { thruput_sys::try_commit_transaction(self.handler) };
193        let err: FFIErrorCode = res.ffi_error_code.into();
194        if err != FFIErrorCode::None {
195            panic!("failed to try to commit transaction: {err}");
196        }
197
198        crate::TryCommitResult::from_ffi(
199            res.result_code,
200            &res.serialized_conflict,
201        )
202    }
203
204    /// Aborts the transaction.
205    /// This can only be called after root_invoke, and can only be called once per transaction.
206    pub fn abort_transaction(&mut self) {
207        if !self.invoked {
208            panic!("cannot abort a transaction that has not been invoked");
209        }
210        if self.committed || self.aborted {
211            panic!(
212                "cannot abort a transaction that has already been committed or aborted"
213            );
214        }
215        self.aborted = true;
216
217        let res = unsafe { thruput_sys::abort_transaction(self.handler) };
218        let err: FFIErrorCode = res.into();
219        if err != FFIErrorCode::None {
220            panic!("failed to abort transaction: {:?}", err);
221        }
222    }
223}
224
225impl Drop for TransactionHandler {
226    fn drop(&mut self) {
227        if self.invoked && !self.committed && !self.aborted {
228            // Abort the uncommitted transaction. Inlined rather than calling
229            // `abort_transaction` to avoid its panic on FFI error: we may be
230            // unwinding from a test failure, and a panic in `drop` would
231            // double-panic into a SIGABRT that hides the original message.
232            self.aborted = true;
233            let res = unsafe { thruput_sys::abort_transaction(self.handler) };
234            let err: FFIErrorCode = res.into();
235            if err != FFIErrorCode::None {
236                crate::drop_failure(format_args!(
237                    "failed to abort uncommitted transaction during cleanup: {:?}",
238                    err
239                ));
240            }
241        }
242
243        let res = unsafe { thruput_sys::close_syscall_handler(self.handler) };
244        let err: FFIErrorCode = res.into();
245        if err != FFIErrorCode::None {
246            crate::drop_failure(format_args!(
247                "failed to close syscall handler: {:?}",
248                err
249            ));
250        }
251    }
252}