Skip to content

Commit 6f4aa73

Browse files
authored
Handle exceptions (#54)
* Add support for wrapper types * Move `FilesWithExtensionIterator` to `core::common` * Remove unneeded use of two `HashMap`s * Make metadata structs more typed * Impl new_from for wrapper types * Implement the new input handling logic * Fix edge-case in input handling * Ignore macro doc comment tests * Correct comment * Fix edge-case in deployment order * Handle calldata better * Allow for the use of function signatures * Add support for exceptions * Cached nonce allocator * Fix tests * Add support for address replacement * Cleanup implementation * Cleanup mutability * Wire up address replacement with rest of code * Implement caller replacement * Switch to callframe trace for exceptions * Add a way to skip tests if they don't match the target * Handle values from the metadata files * Remove address replacement * Correct the arguments * Remove empty impl * Remove address replacement * Correct the arguments * Remove empty impl * Fix size_requirement underflow * Add support for wildcards in exceptions * Fix calldata construction of single calldata * Better handling for length in equivalency checks * Make initial balance a constant * Fix size_requirement underflow * Add support for wildcards in exceptions * Fix calldata construction of single calldata * Better handling for length in equivalency checks * Fix tests
1 parent 589a5dc commit 6f4aa73

File tree

16 files changed

+761
-292
lines changed

16 files changed

+761
-292
lines changed

Cargo.lock

Lines changed: 75 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ revive-common = { git = "https://github.com/paritytech/revive", rev = "3389865af
5959
revive-differential = { git = "https://github.com/paritytech/revive", rev = "3389865af7c3ff6f29a586d82157e8bc573c1a8e" }
6060

6161
[workspace.dependencies.alloy]
62-
version = "1.0"
62+
version = "1.0.22"
6363
default-features = false
6464
features = [
6565
"json-abi",
@@ -73,6 +73,7 @@ features = [
7373
"network",
7474
"serde",
7575
"rpc-types-eth",
76+
"genesis",
7677
]
7778

7879
[profile.bench]

crates/config/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ pub struct Arguments {
7373
)]
7474
pub account: String,
7575

76+
/// This argument controls which private keys the nodes should have access to and be added to
77+
/// its wallet signers. With a value of N, private keys (0, N] will be added to the signer set
78+
/// of the node.
79+
#[arg(long = "private-keys-count", default_value_t = 30)]
80+
pub private_keys_to_add: usize,
81+
7682
/// The differential testing leader node implementation.
7783
#[arg(short, long = "leader", default_value = "geth")]
7884
pub leader: TestingPlatform,

crates/core/src/driver/mod.rs

Lines changed: 245 additions & 37 deletions
Large diffs are not rendered by default.

crates/core/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use revive_dt_compiler::{SolidityCompiler, revive_resolc, solc};
77
use revive_dt_config::TestingPlatform;
8-
use revive_dt_node::{geth, kitchensink::KitchensinkNode};
8+
use revive_dt_node::{Node, geth, kitchensink::KitchensinkNode};
99
use revive_dt_node_interaction::EthereumNode;
1010

1111
pub mod common;
@@ -15,7 +15,7 @@ pub mod driver;
1515
///
1616
/// For this we need a blockchain node implementation and a compiler.
1717
pub trait Platform {
18-
type Blockchain: EthereumNode;
18+
type Blockchain: EthereumNode + Node;
1919
type Compiler: SolidityCompiler;
2020

2121
/// Returns the matching [TestingPlatform] of the [revive_dt_config::Arguments].

crates/format/src/case.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use serde::Deserialize;
22

3-
use crate::{define_wrapper_type, input::Input, mode::Mode};
3+
use crate::{
4+
define_wrapper_type,
5+
input::{Expected, Input},
6+
mode::Mode,
7+
};
48

59
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
610
pub struct Case {
@@ -9,6 +13,33 @@ pub struct Case {
913
pub modes: Option<Vec<Mode>>,
1014
pub inputs: Vec<Input>,
1115
pub group: Option<String>,
16+
pub expected: Option<Expected>,
17+
}
18+
19+
impl Case {
20+
pub fn inputs_iterator(&self) -> impl Iterator<Item = Input> {
21+
let inputs_len = self.inputs.len();
22+
self.inputs
23+
.clone()
24+
.into_iter()
25+
.enumerate()
26+
.map(move |(idx, mut input)| {
27+
if idx + 1 == inputs_len {
28+
if input.expected.is_none() {
29+
input.expected = self.expected.clone();
30+
}
31+
32+
// TODO: What does it mean for us to have an `expected` field on the case itself
33+
// but the final input also has an expected field that doesn't match the one on
34+
// the case? What are we supposed to do with that final expected field on the
35+
// case?
36+
37+
input
38+
} else {
39+
input
40+
}
41+
})
42+
}
1243
}
1344

1445
define_wrapper_type!(

crates/format/src/input.rs

Lines changed: 134 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ use alloy::{
77
primitives::{Address, Bytes, U256},
88
rpc::types::TransactionRequest,
99
};
10+
use alloy_primitives::{FixedBytes, utils::parse_units};
1011
use semver::VersionReq;
11-
use serde::Deserialize;
12-
use serde_json::Value;
12+
use serde::{Deserialize, Serialize};
1313

1414
use revive_dt_node_interaction::EthereumNode;
1515

16-
use crate::metadata::ContractInstance;
16+
use crate::{define_wrapper_type, metadata::ContractInstance};
1717

1818
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
1919
pub struct Input {
@@ -26,7 +26,7 @@ pub struct Input {
2626
#[serde(default)]
2727
pub calldata: Calldata,
2828
pub expected: Option<Expected>,
29-
pub value: Option<String>,
29+
pub value: Option<EtherValue>,
3030
pub storage: Option<HashMap<String, Calldata>>,
3131
}
3232

@@ -40,16 +40,24 @@ pub enum Expected {
4040

4141
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
4242
pub struct ExpectedOutput {
43-
compiler_version: Option<VersionReq>,
44-
return_data: Option<Calldata>,
45-
events: Option<Value>,
46-
exception: Option<bool>,
43+
pub compiler_version: Option<VersionReq>,
44+
pub return_data: Option<Calldata>,
45+
pub events: Option<Vec<Event>>,
46+
#[serde(default)]
47+
pub exception: bool,
48+
}
49+
50+
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
51+
pub struct Event {
52+
pub address: Option<Address>,
53+
pub topics: Vec<String>,
54+
pub values: Calldata,
4755
}
4856

4957
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
5058
#[serde(untagged)]
5159
pub enum Calldata {
52-
Single(String),
60+
Single(Bytes),
5361
Compound(Vec<String>),
5462
}
5563

@@ -74,6 +82,58 @@ pub enum Method {
7482
FunctionName(String),
7583
}
7684

85+
define_wrapper_type!(
86+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
87+
EtherValue(U256);
88+
);
89+
90+
impl Serialize for EtherValue {
91+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92+
where
93+
S: serde::Serializer,
94+
{
95+
format!("{} wei", self.0).serialize(serializer)
96+
}
97+
}
98+
99+
impl<'de> Deserialize<'de> for EtherValue {
100+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
101+
where
102+
D: serde::Deserializer<'de>,
103+
{
104+
let string = String::deserialize(deserializer)?;
105+
let mut splitted = string.split(' ');
106+
let (Some(value), Some(unit)) = (splitted.next(), splitted.next()) else {
107+
return Err(serde::de::Error::custom("Failed to parse the value"));
108+
};
109+
let parsed = parse_units(value, unit.replace("eth", "ether"))
110+
.map_err(|_| serde::de::Error::custom("Failed to parse units"))?
111+
.into();
112+
Ok(Self(parsed))
113+
}
114+
}
115+
116+
impl ExpectedOutput {
117+
pub fn new() -> Self {
118+
Default::default()
119+
}
120+
121+
pub fn with_success(mut self) -> Self {
122+
self.exception = false;
123+
self
124+
}
125+
126+
pub fn with_failure(mut self) -> Self {
127+
self.exception = true;
128+
self
129+
}
130+
131+
pub fn with_calldata(mut self, calldata: Calldata) -> Self {
132+
self.return_data = Some(calldata);
133+
self
134+
}
135+
}
136+
77137
impl Default for Calldata {
78138
fn default() -> Self {
79139
Self::Compound(Default::default())
@@ -91,15 +151,25 @@ impl Calldata {
91151
}
92152
}
93153

94-
pub fn construct_call_data(
154+
pub fn calldata(
155+
&self,
156+
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
157+
chain_state_provider: &impl EthereumNode,
158+
) -> anyhow::Result<Vec<u8>> {
159+
let mut buffer = Vec::<u8>::with_capacity(self.size_requirement());
160+
self.calldata_into_slice(&mut buffer, deployed_contracts, chain_state_provider)?;
161+
Ok(buffer)
162+
}
163+
164+
pub fn calldata_into_slice(
95165
&self,
96166
buffer: &mut Vec<u8>,
97167
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
98168
chain_state_provider: &impl EthereumNode,
99169
) -> anyhow::Result<()> {
100170
match self {
101-
Calldata::Single(string) => {
102-
alloy::hex::decode_to_slice(string, buffer)?;
171+
Calldata::Single(bytes) => {
172+
buffer.extend_from_slice(bytes);
103173
}
104174
Calldata::Compound(items) => {
105175
for (arg_idx, arg) in items.iter().enumerate() {
@@ -120,16 +190,46 @@ impl Calldata {
120190

121191
pub fn size_requirement(&self) -> usize {
122192
match self {
123-
Calldata::Single(single) => (single.len() - 2) / 2,
193+
Calldata::Single(single) => single.len(),
124194
Calldata::Compound(items) => items.len() * 32,
125195
}
126196
}
127-
}
128197

129-
impl ExpectedOutput {
130-
pub fn find_all_contract_instances(&self, vec: &mut Vec<ContractInstance>) {
131-
if let Some(ref cd) = self.return_data {
132-
cd.find_all_contract_instances(vec);
198+
/// Checks if this [`Calldata`] is equivalent to the passed calldata bytes.
199+
pub fn is_equivalent(
200+
&self,
201+
other: &[u8],
202+
deployed_contracts: &HashMap<ContractInstance, (Address, JsonAbi)>,
203+
chain_state_provider: &impl EthereumNode,
204+
) -> anyhow::Result<bool> {
205+
match self {
206+
Calldata::Single(calldata) => Ok(calldata == other),
207+
Calldata::Compound(items) => {
208+
// Chunking the "other" calldata into 32 byte chunks since each
209+
// one of the items in the compound calldata represents 32 bytes
210+
for (this, other) in items.iter().zip(other.chunks(32)) {
211+
// The matterlabs format supports wildcards and therefore we
212+
// also need to support them.
213+
if this == "*" {
214+
continue;
215+
}
216+
217+
let other = if other.len() < 32 {
218+
let mut vec = other.to_vec();
219+
vec.resize(32, 0);
220+
std::borrow::Cow::Owned(vec)
221+
} else {
222+
std::borrow::Cow::Borrowed(other)
223+
};
224+
225+
let this = resolve_argument(this, deployed_contracts, chain_state_provider)?;
226+
let other = U256::from_be_slice(&other);
227+
if this != other {
228+
return Ok(false);
229+
}
230+
}
231+
Ok(true)
232+
}
133233
}
134234
}
135235
}
@@ -153,12 +253,9 @@ impl Input {
153253
) -> anyhow::Result<Bytes> {
154254
match self.method {
155255
Method::Deployer | Method::Fallback => {
156-
let mut calldata = Vec::<u8>::with_capacity(self.calldata.size_requirement());
157-
self.calldata.construct_call_data(
158-
&mut calldata,
159-
deployed_contracts,
160-
chain_state_provider,
161-
)?;
256+
let calldata = self
257+
.calldata
258+
.calldata(deployed_contracts, chain_state_provider)?;
162259

163260
Ok(calldata.into())
164261
}
@@ -204,7 +301,7 @@ impl Input {
204301
// a new buffer for each one of the resolved arguments.
205302
let mut calldata = Vec::<u8>::with_capacity(4 + self.calldata.size_requirement());
206303
calldata.extend(function.selector().0);
207-
self.calldata.construct_call_data(
304+
self.calldata.calldata_into_slice(
208305
&mut calldata,
209306
deployed_contracts,
210307
chain_state_provider,
@@ -222,7 +319,11 @@ impl Input {
222319
chain_state_provider: &impl EthereumNode,
223320
) -> anyhow::Result<TransactionRequest> {
224321
let input_data = self.encoded_input(deployed_contracts, chain_state_provider)?;
225-
let transaction_request = TransactionRequest::default();
322+
let transaction_request = TransactionRequest::default().from(self.caller).value(
323+
self.value
324+
.map(|value| value.into_inner())
325+
.unwrap_or_default(),
326+
);
226327
match self.method {
227328
Method::Deployer => Ok(transaction_request.with_deploy_code(input_data)),
228329
_ => Ok(transaction_request
@@ -236,20 +337,6 @@ impl Input {
236337
vec.push(self.instance.clone());
237338

238339
self.calldata.find_all_contract_instances(&mut vec);
239-
match &self.expected {
240-
Some(Expected::Calldata(cd)) => {
241-
cd.find_all_contract_instances(&mut vec);
242-
}
243-
Some(Expected::Expected(expected)) => {
244-
expected.find_all_contract_instances(&mut vec);
245-
}
246-
Some(Expected::ExpectedMany(expected)) => {
247-
for expected in expected {
248-
expected.find_all_contract_instances(&mut vec);
249-
}
250-
}
251-
None => {}
252-
}
253340

254341
vec
255342
}
@@ -259,8 +346,10 @@ fn default_instance() -> ContractInstance {
259346
ContractInstance::new_from("Test")
260347
}
261348

262-
fn default_caller() -> Address {
263-
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap()
349+
pub const fn default_caller() -> Address {
350+
Address(FixedBytes(alloy::hex!(
351+
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
352+
)))
264353
}
265354

266355
/// This function takes in the string calldata argument provided in the JSON input and resolves it
@@ -355,22 +444,19 @@ mod tests {
355444

356445
fn trace_transaction(
357446
&self,
358-
_: alloy::rpc::types::TransactionReceipt,
447+
_: &alloy::rpc::types::TransactionReceipt,
448+
_: alloy::rpc::types::trace::geth::GethDebugTracingOptions,
359449
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
360450
unimplemented!()
361451
}
362452

363453
fn state_diff(
364454
&self,
365-
_: alloy::rpc::types::TransactionReceipt,
455+
_: &alloy::rpc::types::TransactionReceipt,
366456
) -> anyhow::Result<alloy::rpc::types::trace::geth::DiffMode> {
367457
unimplemented!()
368458
}
369459

370-
fn fetch_add_nonce(&self, _: Address) -> anyhow::Result<u64> {
371-
unimplemented!()
372-
}
373-
374460
fn chain_id(&self) -> anyhow::Result<alloy_primitives::ChainId> {
375461
Ok(0x123)
376462
}

crates/format/src/metadata.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ impl Deref for MetadataFile {
4444

4545
#[derive(Debug, Default, Deserialize, Clone, Eq, PartialEq)]
4646
pub struct Metadata {
47+
pub targets: Option<Vec<String>>,
4748
pub cases: Vec<Case>,
4849
pub contracts: Option<BTreeMap<ContractInstance, ContractPathAndIdentifier>>,
4950
// TODO: Convert into wrapper types for clarity.

0 commit comments

Comments
 (0)