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}