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