Skip to content

Commit

Permalink
ByteArray based errors handling (#1679)
Browse files Browse the repository at this point in the history
<!-- Reference any GitHub issues resolved by this PR -->

Closes #1627 

## Introduced changes

<!-- A brief description of the changes -->

- Adds a mechanism for parsing the errors from the trace and passing it
on to test runner
- Adds an extension for ease of further parsing the array

## Checklist

<!-- Make sure all of these are complete -->

- [x] Linked relevant issue
- [x] Updated relevant documentation
- [x] Added relevant tests
- [x] Performed self-review of the code
- [x] Added changes to `CHANGELOG.md`

---------

Co-authored-by: Piotr Magiera <[email protected]>
Co-authored-by: Kamil Jankowski <[email protected]>
  • Loading branch information
3 people authored Feb 12, 2024
1 parent 9505d79 commit 7f2ff86
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 5 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Forge

#### Added
- `map_string_error` for use with dispatchers, which automatically converts string errors from the syscall result (read more [here](https://foundry-rs.github.io/starknet-foundry/testing/contracts#handling-errors))

## [0.17.0] - 2024-02-07

### Forge
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use cairo_felt::Felt252;
use conversions::byte_array::ByteArray;
use regex::Regex;

#[must_use]
pub fn try_extract_panic_data(err: &str) -> Option<Vec<Felt252>> {
let re = Regex::new(r"Got an exception while executing a hint: Hint Error: Execution failed. Failure reason:\s\w*\s\(\'(.*)\'\)\.")
.expect("Could not create panic_data matching regex");
let re_felt_array = Regex::new(r"Got an exception while executing a hint: Hint Error: Execution failed\. Failure reason: \w+ \('(.*)'\)\.")
.expect("Could not create felt panic_data matching regex");

if let Some(captures) = re.captures(err) {
let re_string = Regex::new(r#"(?s)Got an exception while executing a hint: Hint Error: Execution failed\. Failure reason: "(.*)"\."#)
.expect("Could not create string panic_data matching regex");

if let Some(captures) = re_felt_array.captures(err) {
if let Some(panic_data_match) = captures.get(1) {
if panic_data_match.as_str().is_empty() {
return Some(vec![]);
Expand All @@ -20,16 +24,29 @@ pub fn try_extract_panic_data(err: &str) -> Option<Vec<Felt252>> {
return Some(panic_data_felts);
}
}

if let Some(captures) = re_string.captures(err) {
if let Some(string_match) = captures.get(1) {
let panic_data_felts: Vec<Felt252> =
ByteArray::from(string_match.as_str().to_string()).serialize();
return Some(panic_data_felts);
}
}

None
}

#[cfg(test)]
mod test {
use super::*;
use cairo_felt::Felt252;
use cairo_lang_utils::byte_array::BYTE_ARRAY_MAGIC;
use conversions::felt252::FromShortString;
use indoc::indoc;
use num_traits::Num;

#[test]
fn string_extracting_panic_data() {
fn extracting_plain_panic_data() {
let cases: [(&str, Option<Vec<Felt252>>); 4] = [
(
"Beginning of trace\nGot an exception while executing a hint: Hint Error: Execution failed. Failure reason: 0x434d3232 ('PANIK, DAYTA').\n
Expand All @@ -51,4 +68,66 @@ mod test {
assert_eq!(try_extract_panic_data(str), expected);
}
}

#[test]
fn extracting_string_panic_data() {
let cases: [(&str, Option<Vec<Felt252>>); 4] = [
(
indoc!(
r#"
Beginning of trace
Got an exception while executing a hint: Hint Error: Execution failed. Failure reason: "wow message is exactly 31 chars".
End of trace
"#
),
Some(vec![
Felt252::from_str_radix(BYTE_ARRAY_MAGIC, 16).unwrap(),
Felt252::from(1),
Felt252::from_short_string("wow message is exactly 31 chars").unwrap(),
Felt252::from(0),
Felt252::from(0),
]),
),
(
indoc!(
r#"
Beginning of trace
Got an exception while executing a hint: Hint Error: Execution failed. Failure reason: "".
End of trace
"#
),
Some(vec![
Felt252::from_str_radix(BYTE_ARRAY_MAGIC, 16).unwrap(),
Felt252::from(0),
Felt252::from(0),
Felt252::from(0),
]),
),
(
indoc!(
r#"
Beginning of trace
Got an exception while executing a hint: Hint Error: Execution failed. Failure reason: "A very long and multiline
thing is also being parsed, and can
also can be very long as you can see".
End of trace
"#
),
Some(vec![
Felt252::from_str_radix(BYTE_ARRAY_MAGIC, 16).unwrap(),
Felt252::from(3),
Felt252::from_short_string("A very long and multiline\nthing").unwrap(),
Felt252::from_short_string(" is also being parsed, and can\n").unwrap(),
Felt252::from_short_string("also can be very long as you ca").unwrap(),
Felt252::from_short_string("n see").unwrap(),
Felt252::from(5),
]),
),
("Custom Hint Error: Invalid trace: \"PANIC DATA\"", None),
];

for (str, expected) in cases {
assert_eq!(try_extract_panic_data(str), expected);
}
}
}
46 changes: 45 additions & 1 deletion crates/cheatnet/tests/builtins/panic_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use crate::common::state::build_runtime_state;
use crate::common::{deploy_contract, felt_selector_from_name, state::create_cached_state};
use crate::{assert_error, assert_panic};
use cairo_felt::Felt252;
use cairo_lang_utils::byte_array::BYTE_ARRAY_MAGIC;
use cheatnet::state::CheatnetState;
use conversions::felt252::FromShortString;
use num_traits::Bounded;
use conversions::IntoConv;
use num_traits::{Bounded, Num};

#[test]
fn call_contract_error() {
Expand Down Expand Up @@ -56,3 +58,45 @@ fn call_contract_panic() {
]
);
}

#[test]
fn call_proxied_contract_bytearray_panic() {
let mut cached_state = create_cached_state();
let mut cheatnet_state = CheatnetState::default();
let mut runtime_state = build_runtime_state(&mut cheatnet_state);

let proxy = deploy_contract(
&mut cached_state,
&mut runtime_state,
"ByteArrayPanickingContractProxy",
&[],
);
let bytearray_panicking_contract = deploy_contract(
&mut cached_state,
&mut runtime_state,
"ByteArrayPanickingContract",
&[],
);

let selector = felt_selector_from_name("call_bytearray_panicking_contract");

let output = call_contract(
&mut cached_state,
&mut runtime_state,
&proxy,
&selector,
&[bytearray_panicking_contract.into_()],
);

assert_panic!(
output,
vec![
Felt252::from_str_radix(BYTE_ARRAY_MAGIC, 16).unwrap(),
Felt252::from(2),
Felt252::from_short_string("This is a very long\n and multil").unwrap(),
Felt252::from_short_string("ine string, that will for sure ").unwrap(),
Felt252::from_short_string("saturate the pending_word").unwrap(),
Felt252::from(25),
]
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use starknet::ContractAddress;

#[starknet::interface]
trait IByteArrayPanickingContract<TContractState> {
fn do_panic(self: @TContractState);
}

#[starknet::contract]
mod ByteArrayPanickingContract {
#[storage]
struct Storage {}

#[abi(embed_v0)]
impl Impl of super::IByteArrayPanickingContract<ContractState> {
fn do_panic(self: @ContractState) {
assert!(
false,
"This is a very long\n and multiline string, that will for sure saturate the pending_word"
);
}
}
}

#[starknet::interface]
trait IByteArrayPanickingContractProxy<TContractState> {
fn call_bytearray_panicking_contract(self: @TContractState, contract_address: ContractAddress);
}

#[starknet::contract]
mod ByteArrayPanickingContractProxy {
use starknet::ContractAddress;
use super::{IByteArrayPanickingContractDispatcherTrait, IByteArrayPanickingContractDispatcher};

#[storage]
struct Storage {}

#[abi(embed_v0)]
impl Impl of super::IByteArrayPanickingContractProxy<ContractState> {
fn call_bytearray_panicking_contract(
self: @ContractState, contract_address: ContractAddress
) {
IByteArrayPanickingContractDispatcher { contract_address }.do_panic();
}
}
}
1 change: 1 addition & 0 deletions crates/cheatnet/tests/contracts/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ mod warp;
mod segment_arena_user;
mod panic_call;
mod store_load;
mod bytearray_string_panic_call;
1 change: 1 addition & 0 deletions crates/conversions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ thiserror.workspace = true
serde_json.workspace = true
serde.workspace = true
num-traits.workspace = true
itertools.workspace = true

[dev-dependencies]
ctor.workspace = true
Expand Down
42 changes: 42 additions & 0 deletions crates/conversions/src/byte_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use cairo_felt::Felt252;
use cairo_lang_utils::byte_array::{BYTES_IN_WORD, BYTE_ARRAY_MAGIC};
use itertools::chain;
use num_traits::Num;

pub struct ByteArray {
words: Vec<Felt252>,
pending_word_len: usize,
pending_word: Felt252,
}

impl From<String> for ByteArray {
fn from(value: String) -> Self {
let chunks = value.as_bytes().chunks_exact(BYTES_IN_WORD);
let remainder = chunks.remainder();
let pending_word_len = remainder.len();

let words = chunks.map(Felt252::from_bytes_be).collect();
let pending_word = Felt252::from_bytes_be(remainder);

Self {
words,
pending_word_len,
pending_word,
}
}
}

impl ByteArray {
#[must_use]
pub fn serialize(self) -> Vec<Felt252> {
chain!(
[
Felt252::from_str_radix(BYTE_ARRAY_MAGIC, 16).unwrap(),
self.words.len().into()
],
self.words.into_iter(),
[self.pending_word, self.pending_word_len.into()]
)
.collect()
}
}
1 change: 1 addition & 0 deletions crates/conversions/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::convert::Infallible;

pub mod byte_array;
pub mod class_hash;
pub mod contract_address;
pub mod dec_string;
Expand Down
6 changes: 6 additions & 0 deletions crates/forge/tests/data/contracts/hello_starknet.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ trait IHelloStarknet<TContractState> {
fn get_balance(self: @TContractState) -> felt252;
fn do_a_panic(self: @TContractState);
fn do_a_panic_with(self: @TContractState, panic_data: Array<felt252>);
fn do_a_panic_with_bytearray(self: @TContractState);
}

#[starknet::contract]
Expand Down Expand Up @@ -39,5 +40,10 @@ mod HelloStarknet {
fn do_a_panic_with(self: @ContractState, panic_data: Array<felt252>) {
panic(panic_data);
}

// Panics with a bytearray
fn do_a_panic_with_bytearray(self: @ContractState) {
assert!(false, "This is a very long\n and multiline message that is certain to fill the buffer");
}
}
}
50 changes: 50 additions & 0 deletions crates/forge/tests/integration/dispatchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,56 @@ fn handling_errors() {
assert_passed!(result);
}

#[test]
fn handling_bytearray_based_errors() {
let test = test_case!(
indoc!(
r#"
use starknet::ContractAddress;
use snforge_std::{ declare, ContractClassTrait };
use snforge_std::errors::{ SyscallResultStringErrorTrait, PanicDataOrString };
#[starknet::interface]
trait IHelloStarknet<TContractState> {
fn do_a_panic_with_bytearray(self: @TContractState);
}
#[test]
fn handling_errors() {
let contract = declare('HelloStarknet');
let contract_address = contract.deploy(@ArrayTrait::new()).unwrap();
let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };
#[feature("safe_dispatcher")]
match safe_dispatcher.do_a_panic_with_bytearray().map_string_error() {
Result::Ok(_) => panic_with_felt252('shouldve panicked'),
Result::Err(x) => {
match x {
PanicDataOrString::PanicData(_) => panic_with_felt252('wrong format'),
PanicDataOrString::String(str) => {
assert(
str == "This is a very long\n and multiline message that is certain to fill the buffer",
'wrong string received'
);
}
}
}
};
}
"#
),
Contract::from_code_path(
"HelloStarknet".to_string(),
Path::new("tests/data/contracts/hello_starknet.cairo"),
)
.unwrap()
);

let result = run_test_case(&test);

assert_passed!(result);
}

#[test]
fn serding() {
let test = test_case!(
Expand Down
Loading

0 comments on commit 7f2ff86

Please sign in to comment.