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
This commit is contained in:
Omar
2025-07-24 06:45:53 +03:00
committed by GitHub
parent 589a5dc988
commit 6f4aa731ab
16 changed files with 761 additions and 292 deletions
+97 -81
View File
@@ -1,35 +1,34 @@
use std::{
collections::HashMap,
fs::{File, OpenOptions, create_dir_all, remove_dir_all},
io::{BufRead, Write},
path::{Path, PathBuf},
process::{Child, Command, Stdio},
sync::{
Mutex,
atomic::{AtomicU32, Ordering},
},
sync::atomic::{AtomicU32, Ordering},
time::Duration,
};
use alloy::{
consensus::{BlockHeader, TxEnvelope},
eips::BlockNumberOrTag,
hex,
genesis::{Genesis, GenesisAccount},
network::{
Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError,
UnbuiltTransactionError,
Ethereum, EthereumWallet, Network, NetworkWallet, TransactionBuilder,
TransactionBuilderError, UnbuiltTransactionError,
},
primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, U256,
},
primitives::{Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
fillers::{FillProvider, TxFiller},
fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller},
},
rpc::types::{
TransactionReceipt,
eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
@@ -40,7 +39,7 @@ use tracing::Level;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::{BlockingExecutor, EthereumNode};
use crate::Node;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
@@ -55,7 +54,7 @@ pub struct KitchensinkNode {
logs_directory: PathBuf,
process_substrate: Option<Child>,
process_proxy: Option<Child>,
nonces: Mutex<HashMap<Address, u64>>,
nonce_manager: CachedNonceManager,
/// This vector stores [`File`] objects that we use for logging which we want to flush when the
/// node object is dropped. We do not store them in a structured fashion at the moment (in
/// separate fields) as the logic that we need to apply to them is all the same regardless of
@@ -127,7 +126,18 @@ impl KitchensinkNode {
None
})
.collect();
let mut eth_balances = self.extract_balance_from_genesis_file(genesis)?;
let mut eth_balances = {
let mut genesis = serde_json::from_str::<Genesis>(genesis)?;
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
genesis
.alloc
.entry(signer_address)
.or_insert(GenesisAccount::default().with_balance(U256::from(INITIAL_BALANCE)));
}
self.extract_balance_from_genesis_file(&genesis)?
};
merged_balances.append(&mut eth_balances);
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
@@ -241,42 +251,27 @@ impl KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn extract_balance_from_genesis_file(
&self,
genesis_str: &str,
genesis: &Genesis,
) -> anyhow::Result<Vec<(String, u128)>> {
let genesis_json: JsonValue = serde_json::from_str(genesis_str)?;
let alloc = genesis_json
.get("alloc")
.and_then(|a| a.as_object())
.ok_or_else(|| anyhow::anyhow!("Missing 'alloc' in genesis"))?;
let mut balances = Vec::new();
for (eth_addr, obj) in alloc.iter() {
let balance_str = obj.get("balance").and_then(|b| b.as_str()).unwrap_or("0");
let balance = if balance_str.starts_with("0x") {
u128::from_str_radix(balance_str.trim_start_matches("0x"), 16)?
} else {
balance_str.parse::<u128>()?
};
let substrate_addr = Self::eth_to_substrate_address(eth_addr)?;
balances.push((substrate_addr.clone(), balance));
}
Ok(balances)
genesis
.alloc
.iter()
.try_fold(Vec::new(), |mut vec, (address, acc)| {
let substrate_address = Self::eth_to_substrate_address(address);
let balance = acc.balance.try_into()?;
vec.push((substrate_address, balance));
Ok(vec)
})
}
fn eth_to_substrate_address(eth_addr: &str) -> anyhow::Result<String> {
let eth_bytes = hex::decode(eth_addr.trim_start_matches("0x"))?;
if eth_bytes.len() != 20 {
anyhow::bail!(
"Invalid Ethereum address length: expected 20 bytes, got {}",
eth_bytes.len()
);
}
fn eth_to_substrate_address(address: &Address) -> String {
let eth_bytes = address.0.0;
let mut padded = [0xEEu8; 32];
padded[..20].copy_from_slice(&eth_bytes);
let account_id = AccountId32::from(padded);
Ok(account_id.to_ss58check())
account_id.to_ss58check()
}
fn wait_ready(logs_file_path: &Path, marker: &str, timeout: Duration) -> anyhow::Result<()> {
@@ -350,9 +345,24 @@ impl KitchensinkNode {
> + 'static {
let connection_string = self.connection_string();
let wallet = self.wallet.clone();
// Note: We would like all providers to make use of the same nonce manager so that we have
// monotonically increasing nonces that are cached. The cached nonce manager uses Arc's in
// its implementation and therefore it means that when we clone it then it still references
// the same state.
let nonce_manager = self.nonce_manager.clone();
Box::pin(async move {
ProviderBuilder::new()
.disable_recommended_fillers()
.network::<KitchenSinkNetwork>()
.filler(FallbackGasFiller::new(
30_000_000,
200_000_000_000,
3_000_000_000,
))
.filler(ChainIdFiller::default())
.filler(NonceFiller::new(nonce_manager))
.wallet(wallet)
.connect(&connection_string)
.await
@@ -384,27 +394,28 @@ impl EthereumNode for KitchensinkNode {
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn trace_transaction(
&self,
transaction: TransactionReceipt,
transaction: &TransactionReceipt,
trace_options: GethDebugTracingOptions,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let tx_hash = transaction.transaction_hash;
let provider = self.provider();
BlockingExecutor::execute(async move {
Ok(provider
.await?
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.debug_trace_transaction(tx_hash, trace_options)
.await?)
})?
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result<DiffMode> {
fn state_diff(&self, transaction: &TransactionReceipt) -> anyhow::Result<DiffMode> {
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
match self
.trace_transaction(transaction)?
.trace_transaction(transaction, trace_options)?
.try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
@@ -412,24 +423,6 @@ impl EthereumNode for KitchensinkNode {
}
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let provider = self.provider();
let onchain_nonce = BlockingExecutor::execute::<anyhow::Result<_>>(async move {
provider
.await?
.get_transaction_count(address)
.await
.map_err(Into::into)
})??;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
}
#[tracing::instrument(skip_all, fields(geth_node_id = self.id))]
fn chain_id(&self) -> anyhow::Result<alloy::primitives::ChainId> {
let provider = self.provider();
@@ -519,17 +512,26 @@ impl Node for KitchensinkNode {
let base_directory = kitchensink_directory.join(id.to_string());
let logs_directory = base_directory.join(Self::LOGS_DIRECTORY);
let mut wallet = config.wallet();
for signer in (1..=config.private_keys_to_add)
.map(|id| U256::from(id))
.map(|id| id.to_be_bytes::<32>())
.map(|id| PrivateKeySigner::from_bytes(&FixedBytes(id)).unwrap())
{
wallet.register_signer(signer);
}
Self {
id,
substrate_binary: config.kitchensink.clone(),
eth_proxy_binary: config.eth_proxy.clone(),
rpc_url: String::new(),
wallet: config.wallet(),
wallet,
base_directory,
logs_directory,
process_substrate: None,
process_proxy: None,
nonces: Mutex::new(HashMap::new()),
nonce_manager: Default::default(),
// We know that we only need to be storing 4 files so we can specify that when creating
// the vector. It's the stdout and stderr of the substrate-node and the eth-rpc.
logs_file_to_flush: Vec::with_capacity(4),
@@ -585,6 +587,14 @@ impl Node for KitchensinkNode {
.stdout;
Ok(String::from_utf8_lossy(&output).into())
}
#[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))]
fn matches_target(&self, targets: Option<&[String]>) -> bool {
match targets {
None => true,
Some(targets) => targets.iter().any(|str| str.as_str() == "pvm"),
}
}
}
impl Drop for KitchensinkNode {
@@ -640,6 +650,12 @@ impl TransactionBuilder<KitchenSinkNetwork> for <Ethereum as Network>::Transacti
)
}
fn take_nonce(&mut self) -> Option<u64> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::take_nonce(
self,
)
}
fn input(&self) -> Option<&alloy::primitives::Bytes> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::input(self)
}
@@ -1020,7 +1036,7 @@ mod tests {
use alloy::rpc::types::TransactionRequest;
use revive_dt_config::Arguments;
use std::path::PathBuf;
use std::sync::LazyLock;
use std::sync::{LazyLock, Mutex};
use temp_dir::TempDir;
use std::fs;
@@ -1129,12 +1145,12 @@ mod tests {
let contents = fs::read_to_string(&final_chainspec_path).expect("Failed to read chainspec");
// Validate that the Substrate addresses derived from the Ethereum addresses are in the file
let first_eth_addr =
KitchensinkNode::eth_to_substrate_address("90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
.unwrap();
let second_eth_addr =
KitchensinkNode::eth_to_substrate_address("Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2")
.unwrap();
let first_eth_addr = KitchensinkNode::eth_to_substrate_address(
&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(),
);
let second_eth_addr = KitchensinkNode::eth_to_substrate_address(
&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(),
);
assert!(
contents.contains(&first_eth_addr),
@@ -1162,7 +1178,7 @@ mod tests {
let node = KitchensinkNode::new(&test_config().0);
let result = node
.extract_balance_from_genesis_file(genesis_json)
.extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
.unwrap();
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
@@ -1192,7 +1208,7 @@ mod tests {
];
for eth_addr in eth_addresses {
let ss58 = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap();
let ss58 = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}");
}
@@ -1220,7 +1236,7 @@ mod tests {
];
for (eth_addr, expected_ss58) in cases {
let result = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap();
let result = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
assert_eq!(
result, expected_ss58,
"Mismatch for Ethereum address {eth_addr}"