Skip to content

Commit e9cfa65

Browse files
authored
Using RevertError for removing VM tracebacks (#848)
1 parent a359033 commit e9cfa65

File tree

6 files changed

+441
-173
lines changed

6 files changed

+441
-173
lines changed

madara/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

madara/crates/client/exec/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mp-block = { workspace = true }
2222
mp-chain-config = { workspace = true }
2323
mp-class = { workspace = true }
2424
mp-convert = { workspace = true }
25+
mp-receipt = { workspace = true }
2526
mp-rpc = { workspace = true }
2627
mp-state-update = { workspace = true }
2728
mp-transactions = { workspace = true }

madara/crates/client/exec/src/execution.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use mp_receipt::RevertErrorExt;
12
use crate::{Error, ExecutionContext, ExecutionResult, TxExecError};
23
use blockifier::fee::gas_usage::estimate_minimal_gas_vector;
34
use blockifier::state::cached_state::TransactionalState;
@@ -59,9 +60,11 @@ impl<D: MadaraStorageRead> ExecutionContext<D> {
5960

6061
let mut transactional_state = TransactionalState::create_transactional(&mut self.state);
6162
// NB: We use execute_raw because execute already does transaactional state.
62-
let execution_info =
63+
let mut execution_info =
6364
tx.execute_raw(&mut transactional_state, &self.block_context, false).map_err(make_reexec_error)?;
6465

66+
execution_info.revert_error = execution_info.revert_error.take().map(|e| e.format_for_receipt());
67+
6568
let state_diff = transactional_state
6669
.to_state_diff()
6770
.map_err(TransactionExecutionError::StateError)

madara/crates/primitives/receipt/src/from_blockifier.rs

Lines changed: 2 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::{
22
DeclareTransactionReceipt, DeployAccountTransactionReceipt, Event, ExecutionResources, ExecutionResult, FeePayment,
33
GasVector, InvokeTransactionReceipt, L1HandlerTransactionReceipt, MsgToL1, MsgToL2, PriceUnit, TransactionReceipt,
44
};
5+
use crate::revert_error::RevertErrorExt;
56
use anyhow::anyhow;
67
use blockifier::execution::call_info::CallInfo;
78
use blockifier::transaction::{
@@ -79,119 +80,6 @@ fn recursive_call_info_iter(res: &TransactionExecutionInfo) -> impl Iterator<Ite
7980
.flat_map(|call_info| call_info.iter()) // flatmap over the roots' recursive inner call infos
8081
}
8182

82-
/// Formats the revert error string by removing redundant VM tracebacks.
83-
///
84-
/// The blockifier generates verbose error messages with VM tracebacks at every level
85-
/// of the call stack. This function filters them to show the traceback only once,
86-
/// positioned after the last regular contract call (CallContract) entry point frame
87-
/// and before any library call (LibraryCall) frames or the final error.
88-
///
89-
/// This makes error messages more concise while preserving the most relevant debugging information.
90-
fn format_revert_error(error_string: &str) -> String {
91-
// Check if the original string ends with a newline
92-
let ends_with_newline = error_string.ends_with('\n');
93-
94-
let lines: Vec<&str> = error_string.lines().collect();
95-
let mut output_lines = Vec::new();
96-
let mut i = 0;
97-
98-
while i < lines.len() {
99-
let line = lines[i];
100-
101-
// Check if this is the start of a VM exception frame
102-
// VM exception frames start with "Error at pc=..."
103-
if line.trim().starts_with("Error at pc=") {
104-
// Look ahead to see if there's another "Error in the called contract" entry
105-
// or "Error in a library call" coming up
106-
let mut has_nested_call_contract = false;
107-
let mut j = i + 1;
108-
109-
// Scan forward to find the next entry point frame
110-
while j < lines.len() {
111-
let next_line = lines[j].trim();
112-
113-
// Found the next entry point - check if it's a CallContract or LibraryCall
114-
if next_line.starts_with("Error in the called contract") {
115-
has_nested_call_contract = true;
116-
break;
117-
} else if next_line.starts_with("Error in a library call")
118-
|| next_line.starts_with("Execution failed")
119-
|| next_line.starts_with("Entry point") {
120-
// Next entry is a library call or final error - don't skip this traceback
121-
has_nested_call_contract = false;
122-
break;
123-
}
124-
125-
// Keep scanning through the traceback lines
126-
if next_line.starts_with("Unknown location")
127-
|| next_line.starts_with("Cairo traceback")
128-
|| next_line.is_empty() {
129-
j += 1;
130-
} else {
131-
// Reached a numbered entry like "1:" - check if it contains the patterns
132-
if let Some(colon_pos) = next_line.find(':') {
133-
let after_colon = &next_line[colon_pos + 1..].trim();
134-
if after_colon.starts_with("Error in the called contract") {
135-
has_nested_call_contract = true;
136-
break;
137-
} else if after_colon.starts_with("Error in a library call") {
138-
has_nested_call_contract = false;
139-
break;
140-
}
141-
}
142-
j += 1;
143-
}
144-
}
145-
146-
// Skip the VM traceback if there's a nested CallContract
147-
if has_nested_call_contract {
148-
// Skip this "Error at pc=..." line and all traceback lines until the next entry
149-
while i < lines.len() {
150-
let current_line = lines[i].trim();
151-
i += 1;
152-
153-
// Stop when we hit the next numbered entry or end
154-
if i < lines.len() {
155-
let next_line = lines[i].trim();
156-
if let Some(first_char) = next_line.chars().next() {
157-
if first_char.is_ascii_digit() && next_line.contains(':') {
158-
break;
159-
}
160-
}
161-
}
162-
163-
// Also stop at the end or at lines that look like new entries
164-
if current_line.is_empty() && i < lines.len() {
165-
let peek = lines[i].trim();
166-
if let Some(first_char) = peek.chars().next() {
167-
if first_char.is_ascii_digit() || peek.starts_with("Execution failed") {
168-
break;
169-
}
170-
}
171-
}
172-
}
173-
} else {
174-
// Keep this traceback - it's before a library call or final error
175-
output_lines.push(line);
176-
i += 1;
177-
}
178-
} else {
179-
// Not a VM traceback line - keep it
180-
output_lines.push(line);
181-
i += 1;
182-
}
183-
}
184-
185-
let mut result = output_lines.join("\n");
186-
187-
// Preserve the trailing newline if the original had one
188-
if ends_with_newline {
189-
result.push('\n');
190-
}
191-
192-
result
193-
}
194-
19583
pub fn from_blockifier_execution_info(res: &TransactionExecutionInfo, tx: &Transaction) -> TransactionReceipt {
19684
let price_unit = match blockifier_tx_fee_type(tx) {
19785
FeeType::Eth => PriceUnit::Wei,
@@ -309,8 +197,7 @@ pub fn from_blockifier_execution_info(res: &TransactionExecutionInfo, tx: &Trans
309197
};
310198

311199
let execution_result = if let Some(reason) = &res.revert_error {
312-
let formatted_reason = format_revert_error(&reason.to_string());
313-
ExecutionResult::Reverted { reason: formatted_reason }
200+
ExecutionResult::Reverted { reason: reason.format_for_receipt_string() }
314201
} else {
315202
ExecutionResult::Succeeded
316203
};
@@ -460,61 +347,4 @@ mod test {
460347

461348
assert_eq!(hash, expected_hash);
462349
}
463-
464-
#[test]
465-
fn test_format_revert_error_filters_redundant_tracebacks() {
466-
// Real example from blockifier with VM tracebacks at every CallContract level
467-
let input = "Transaction execution has failed:\n0: Error in the called contract (contract address: 0x05743c833ed33a3433f1c5587dac97753ddcc84f9844e6fa2a3268e5ae35cbc3, class hash: 0x073414441639dcd11d1846f287650a00c60c416b9d3ba45d31c651672125b2c2, selector: 0x015d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad):\nError at pc=0:35988:\nCairo traceback (most recent call last):\nUnknown location (pc=0:330)\nUnknown location (pc=0:11695)\nUnknown location (pc=0:36001)\nUnknown location (pc=0:36001)\nUnknown location (pc=0:36001)\n\n1: Error in the called contract (contract address: 0x0286003f7c7bfc3f94e8f0af48b48302e7aee2fb13c23b141479ba00832ef2c6, class hash: 0x03e283b1e8bce178469acb94700999ecc7ad180420201e16eb0a81294ae8599b, selector: 0x0056878e39e16b42520b0d7936d3fd3498f86ceda4dbad50f6ff717644c95ed6):\nError at pc=0:115867:\nCairo traceback (most recent call last):\nUnknown location (pc=0:9435)\nUnknown location (pc=0:43555)\nUnknown location (pc=0:93296)\n\n2: Error in the called contract (contract address: 0x06f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e, class hash: 0x070cdfaea3ec997bd3a8cdedfc0ffe804a58afc3d6b5a6e5c0218ec233ceea6d, selector: 0x0041b033f4a31df8067c24d1e9b550a2ce75fd4a29e1147af9752174f0e6cb20):\nError at pc=0:32:\nCairo traceback (most recent call last):\nUnknown location (pc=0:1683)\nUnknown location (pc=0:1669)\n\n3: Error in a library call (contract address: 0x06f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e, class hash: 0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed, selector: 0x0041b033f4a31df8067c24d1e9b550a2ce75fd4a29e1147af9752174f0e6cb20):\nExecution failed. Failure reason:\nError in contract (contract address: 0x06f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e, class hash: 0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed, selector: 0x0041b033f4a31df8067c24d1e9b550a2ce75fd4a29e1147af9752174f0e6cb20):\n0x753235365f737562204f766572666c6f77 ('u256_sub Overflow').\n";
468-
469-
// Expected output: only the traceback from entry 2 (last CallContract before LibraryCall) is kept
470-
let expected = "Transaction execution has failed:\n0: Error in the called contract (contract address: 0x05743c833ed33a3433f1c5587dac97753ddcc84f9844e6fa2a3268e5ae35cbc3, class hash: 0x073414441639dcd11d1846f287650a00c60c416b9d3ba45d31c651672125b2c2, selector: 0x015d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad):\n1: Error in the called contract (contract address: 0x0286003f7c7bfc3f94e8f0af48b48302e7aee2fb13c23b141479ba00832ef2c6, class hash: 0x03e283b1e8bce178469acb94700999ecc7ad180420201e16eb0a81294ae8599b, selector: 0x0056878e39e16b42520b0d7936d3fd3498f86ceda4dbad50f6ff717644c95ed6):\n2: Error in the called contract (contract address: 0x06f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e, class hash: 0x070cdfaea3ec997bd3a8cdedfc0ffe804a58afc3d6b5a6e5c0218ec233ceea6d, selector: 0x0041b033f4a31df8067c24d1e9b550a2ce75fd4a29e1147af9752174f0e6cb20):\nError at pc=0:32:\nCairo traceback (most recent call last):\nUnknown location (pc=0:1683)\nUnknown location (pc=0:1669)\n\n3: Error in a library call (contract address: 0x06f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e, class hash: 0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed, selector: 0x0041b033f4a31df8067c24d1e9b550a2ce75fd4a29e1147af9752174f0e6cb20):\nExecution failed. Failure reason:\nError in contract (contract address: 0x06f373b346561036d98ea10fb3e60d2f459c872b1933b50b21fe6ef4fda3b75e, class hash: 0x05ffbcfeb50d200a0677c48a129a11245a3fc519d1d98d76882d1c9a1b19c6ed, selector: 0x0041b033f4a31df8067c24d1e9b550a2ce75fd4a29e1147af9752174f0e6cb20):\n0x753235365f737562204f766572666c6f77 ('u256_sub Overflow').\n";
471-
472-
let result = format_revert_error(input);
473-
474-
assert_eq!(result, expected);
475-
}
476-
477-
#[test]
478-
fn test_format_revert_error_preserves_trailing_newline() {
479-
let input_with_newline = "Error at pc=0:123:\nCairo traceback\n";
480-
let input_without_newline = "Error at pc=0:123:\nCairo traceback";
481-
482-
let result_with = format_revert_error(input_with_newline);
483-
let result_without = format_revert_error(input_without_newline);
484-
485-
assert!(result_with.ends_with('\n'));
486-
assert!(!result_without.ends_with('\n'));
487-
}
488-
489-
#[test]
490-
fn test_format_revert_error_keeps_traceback_before_library_call() {
491-
let input = "0: Error in the called contract:\nError at pc=0:100:\nCairo traceback\n\n1: Error in a library call:\nExecution failed\n";
492-
let result = format_revert_error(input);
493-
494-
// Should keep the traceback because it's before a library call
495-
assert!(result.contains("Error at pc=0:100:"));
496-
assert!(result.contains("Cairo traceback"));
497-
}
498-
499-
#[test]
500-
fn test_format_revert_error_skips_traceback_with_nested_call_contract() {
501-
let input = "0: Error in the called contract:\nError at pc=0:100:\nCairo traceback\n\n1: Error in the called contract:\nError at pc=0:200:\nAnother traceback\n";
502-
let result = format_revert_error(input);
503-
504-
// Should skip the first traceback because there's a nested CallContract
505-
assert!(!result.contains("Error at pc=0:100:"));
506-
assert!(!result.contains("Cairo traceback"));
507-
// Should keep the second one
508-
assert!(result.contains("Error at pc=0:200:"));
509-
assert!(result.contains("Another traceback"));
510-
}
511-
512-
#[test]
513-
fn test_format_revert_error_no_tracebacks() {
514-
let input = "Transaction execution has failed:\nExecution failed. Failure reason: 'Some error'\n";
515-
let result = format_revert_error(input);
516-
517-
// Should preserve the error message as-is when there are no tracebacks
518-
assert_eq!(result, input);
519-
}
520350
}

madara/crates/primitives/receipt/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use starknet_types_core::{
88
};
99

1010
pub mod from_blockifier;
11+
pub mod revert_error;
1112

1213
mod to_starknet_types;
1314

1415
pub use from_blockifier::from_blockifier_execution_info;
16+
pub use revert_error::RevertErrorExt;
1517
pub use starknet_core::types::Hash256;
1618

1719
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

0 commit comments

Comments
 (0)