thruput_native_exec/try_commit_result.rs
1use thruput_sys::FFIDataBuffer;
2use thruput_xdr::exec_result::ExecResult;
3
4// Re-exported so callers matching on `TxNonDeterministicError` have the
5// conflict types in scope without depending on thruput-trace directly.
6pub use thruput_trace::db_conflict::{
7 DbConflictCode, DbConflictDiagnostic, DbConflictResult,
8};
9
10/// Outcome of a two-phase commit (`try_commit_transaction`).
11///
12/// Mirrors the codes of [`ExecResult`], one variant per code, so a commit
13/// outcome is matched exhaustively. Only `ExecResultTxNonDeterministicError`
14/// carries a two-phase-commit conflict diagnostic, so the [`DbConflictResult`]
15/// rides on that variant alone — there is no way to reach a conflict after a
16/// successful (or otherwise-failed) commit.
17///
18/// [`ExecResult`]: thruput_xdr::exec_result::ExecResult
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum TryCommitResult {
21 /// `ExecResultOK` — the commit succeeded.
22 Ok,
23 /// `ExecResultTxDeterministicError` — the root frame returned a
24 /// deterministic error.
25 TxDeterministicError,
26 /// `ExecResultTxRootFrameReverted` — the root frame chose to revert.
27 TxRootFrameReverted,
28 /// `ExecResultTxNonDeterministicError` — nondeterminism during the
29 /// post-execution two-phase commit (a storage conflict). Carries the
30 /// conflict diagnostic; `DbConflictResult::kDbDiagnosticNone` when the
31 /// database reported no per-conflict detail for this execution mode.
32 TxNonDeterministicError(DbConflictResult),
33 /// `ExecResultFailedGasSponsorship` — no longer produced; retained to
34 /// mirror [`ExecResult`].
35 FailedGasSponsorship,
36 /// `ExecResultTxValidationFailure` — attached validation data was bad.
37 TxValidationFailure,
38 /// `ExecResultSystemError` — recoverable system error (e.g. broken
39 /// connection).
40 SystemError,
41 /// `ExecResultUnrecoverable` — critical, unrecoverable executor failure.
42 Unrecoverable,
43}
44
45impl TryCommitResult {
46 /// Build from the raw `try_commit_transaction` FFI result. The conflict
47 /// buffer is only decoded for the nondeterministic arm; every other code
48 /// ignores it.
49 pub(crate) fn from_ffi(
50 result_code: i32,
51 serialized_conflict: &FFIDataBuffer,
52 ) -> Self {
53 match result_code {
54 x if x == ExecResult::ExecResultOK.0 => Self::Ok,
55 x if x == ExecResult::ExecResultTxDeterministicError.0 => {
56 Self::TxDeterministicError
57 }
58 x if x == ExecResult::ExecResultTxRootFrameReverted.0 => {
59 Self::TxRootFrameReverted
60 }
61 x if x == ExecResult::ExecResultTxNonDeterministicError.0 => {
62 Self::TxNonDeterministicError(decode_conflict(
63 serialized_conflict,
64 ))
65 }
66 x if x == ExecResult::ExecResultFailedGasSponsorship.0 => {
67 Self::FailedGasSponsorship
68 }
69 x if x == ExecResult::ExecResultTxValidationFailure.0 => {
70 Self::TxValidationFailure
71 }
72 x if x == ExecResult::ExecResultSystemError.0 => Self::SystemError,
73 x if x == ExecResult::ExecResultUnrecoverable.0 => {
74 Self::Unrecoverable
75 }
76 other => panic!(
77 "try_commit_transaction returned unknown ExecResult code: {other}"
78 ),
79 }
80 }
81
82 /// The [`ExecResult`] code this outcome corresponds to, discarding any
83 /// conflict diagnostic.
84 pub fn exec_result(&self) -> ExecResult {
85 match self {
86 Self::Ok => ExecResult::ExecResultOK,
87 Self::TxDeterministicError => {
88 ExecResult::ExecResultTxDeterministicError
89 }
90 Self::TxRootFrameReverted => {
91 ExecResult::ExecResultTxRootFrameReverted
92 }
93 Self::TxNonDeterministicError(_) => {
94 ExecResult::ExecResultTxNonDeterministicError
95 }
96 Self::FailedGasSponsorship => {
97 ExecResult::ExecResultFailedGasSponsorship
98 }
99 Self::TxValidationFailure => {
100 ExecResult::ExecResultTxValidationFailure
101 }
102 Self::SystemError => ExecResult::ExecResultSystemError,
103 Self::Unrecoverable => ExecResult::ExecResultUnrecoverable,
104 }
105 }
106}
107
108/// Decode the XDR-serialized [`DbConflictResult`] attached to a
109/// nondeterministic commit failure.
110///
111/// Per the test_ffi contract (`ForeignTryCommitResult::serialized_conflict`),
112/// the buffer is *always* populated with the whole union — the
113/// `kDbDiagnosticNone` arm when the execution mode reports no per-slot detail —
114/// and is null *only* when the host's own serialization failed. So a null
115/// buffer is a genuine FFI fault, not the no-detail case, and we panic rather
116/// than silently substituting an empty diagnostic.
117fn decode_conflict(serialized_conflict: &FFIDataBuffer) -> DbConflictResult {
118 match serialized_conflict.as_slice() {
119 Some(bytes) => xdrs::txs::decode(bytes).unwrap_or_else(|e| {
120 panic!(
121 "failed to decode DbConflictResult ({} bytes): {e}",
122 bytes.len()
123 )
124 }),
125 None => panic!(
126 "try_commit_transaction reported a nondeterministic error but the \
127 serialized conflict buffer was null (host-side serialization failed)"
128 ),
129 }
130}