Refactor the Global Configuration & Context (#157)

* Cleanup the config

* Update usage guides

* Update the run script

* Fix tests

* Use kitchensink in tests

* Use shared node more often in tests
This commit is contained in:
Omar
2025-09-04 17:25:05 +03:00
committed by GitHub
parent 7878f68c26
commit c2526e48e7
23 changed files with 805 additions and 441 deletions
+46 -65
View File
@@ -17,9 +17,7 @@ use alloy::{
eips::BlockNumberOrTag,
genesis::{Genesis, GenesisAccount},
network::{Ethereum, EthereumWallet, NetworkWallet},
primitives::{
Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256,
},
primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, StorageKey, TxHash, U256},
providers::{
Provider, ProviderBuilder,
ext::DebugApi,
@@ -29,9 +27,8 @@ use alloy::{
EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest,
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use anyhow::Context;
use anyhow::Context as _;
use revive_common::EVMVersion;
use tracing::{Instrument, instrument};
@@ -39,7 +36,7 @@ use revive_dt_common::{
fs::clear_directory,
futures::{PollingWaitBehavior, poll},
};
use revive_dt_config::Arguments;
use revive_dt_config::*;
use revive_dt_format::traits::ResolverApi;
use revive_dt_node_interaction::EthereumNode;
@@ -64,7 +61,7 @@ pub struct GethNode {
geth: PathBuf,
id: u32,
handle: Option<Child>,
start_timeout: u64,
start_timeout: Duration,
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
@@ -97,7 +94,7 @@ impl GethNode {
/// Create the node directory and call `geth init` to configure the genesis.
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn init(&mut self, genesis: String) -> anyhow::Result<&mut Self> {
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
@@ -106,8 +103,6 @@ impl GethNode {
create_dir_all(&self.logs_directory)
.context("Failed to create logs directory for geth node")?;
let mut genesis = serde_json::from_str::<Genesis>(&genesis)
.context("Failed to deserialize geth genesis JSON")?;
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
@@ -240,7 +235,7 @@ impl GethNode {
.open(self.geth_stderr_log_file_path())
.context("Failed to open geth stderr logs file for readiness check")?;
let maximum_wait_time = Duration::from_millis(self.start_timeout);
let maximum_wait_time = self.start_timeout;
let mut stderr = BufReader::new(logs_file).lines();
let mut lines = vec![];
loop {
@@ -256,7 +251,7 @@ impl GethNode {
if Instant::now().duration_since(start_time) > maximum_wait_time {
anyhow::bail!(
"Timeout in starting geth: took longer than {}ms. stdout:\n\n{}\n",
self.start_timeout,
self.start_timeout.as_millis(),
lines.join("\n")
);
}
@@ -556,30 +551,40 @@ impl ResolverApi for GethNode {
}
impl Node for GethNode {
fn new(config: &Arguments) -> Self {
let geth_directory = config.directory().join(Self::BASE_DIRECTORY);
fn new(
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
+ AsRef<GethConfiguration>
+ AsRef<KitchensinkConfiguration>
+ AsRef<ReviveDevNodeConfiguration>
+ AsRef<EthRpcConfiguration>
+ Clone,
) -> Self {
let working_directory_configuration =
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
let geth_configuration = AsRef::<GethConfiguration>::as_ref(&context);
let geth_directory = working_directory_configuration
.as_path()
.join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = geth_directory.join(id.to_string());
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);
}
let wallet = wallet_configuration.wallet();
Self {
connection_string: base_directory.join(Self::IPC_FILE).display().to_string(),
data_directory: base_directory.join(Self::DATA_DIRECTORY),
logs_directory: base_directory.join(Self::LOGS_DIRECTORY),
base_directory,
geth: config.geth.clone(),
geth: geth_configuration.path.clone(),
id,
handle: None,
start_timeout: config.geth_start_timeout,
wallet: Arc::new(wallet),
start_timeout: geth_configuration.start_timeout_ms,
wallet: wallet.clone(),
chain_id_filler: Default::default(),
nonce_manager: Default::default(),
// We know that we only need to be storing 2 files so we can specify that when creating
@@ -621,7 +626,7 @@ impl Node for GethNode {
}
#[instrument(level = "info", skip_all, fields(geth_node_id = self.id))]
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
self.init(genesis)?.spawn_process()?;
Ok(())
}
@@ -662,49 +667,25 @@ impl Drop for GethNode {
#[cfg(test)]
mod tests {
use revive_dt_config::Arguments;
use temp_dir::TempDir;
use crate::{GENESIS_JSON, Node};
use super::*;
fn test_config() -> (Arguments, TempDir) {
let mut config = Arguments::default();
let temp_dir = TempDir::new().unwrap();
config.working_directory = temp_dir.path().to_path_buf().into();
(config, temp_dir)
fn test_config() -> ExecutionContext {
ExecutionContext::default()
}
fn new_node() -> (GethNode, TempDir) {
let (args, temp_dir) = test_config();
let mut node = GethNode::new(&args);
node.init(GENESIS_JSON.to_owned())
fn new_node() -> (ExecutionContext, GethNode) {
let context = test_config();
let mut node = GethNode::new(&context);
node.init(context.genesis_configuration.genesis().unwrap().clone())
.expect("Failed to initialize the node")
.spawn_process()
.expect("Failed to spawn the node process");
(node, temp_dir)
}
#[test]
fn init_works() {
GethNode::new(&test_config().0)
.init(GENESIS_JSON.to_string())
.unwrap();
}
#[test]
fn spawn_works() {
GethNode::new(&test_config().0)
.spawn(GENESIS_JSON.to_string())
.unwrap();
(context, node)
}
#[test]
fn version_works() {
let version = GethNode::new(&test_config().0).version().unwrap();
let version = GethNode::new(&test_config()).version().unwrap();
assert!(
version.starts_with("geth version"),
"expected version string, got: '{version}'"
@@ -714,7 +695,7 @@ mod tests {
#[tokio::test]
async fn can_get_chain_id_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let chain_id = node.chain_id().await;
@@ -727,7 +708,7 @@ mod tests {
#[tokio::test]
async fn can_get_gas_limit_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let gas_limit = node.block_gas_limit(BlockNumberOrTag::Latest).await;
@@ -740,7 +721,7 @@ mod tests {
#[tokio::test]
async fn can_get_coinbase_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let coinbase = node.block_coinbase(BlockNumberOrTag::Latest).await;
@@ -753,7 +734,7 @@ mod tests {
#[tokio::test]
async fn can_get_block_difficulty_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let block_difficulty = node.block_difficulty(BlockNumberOrTag::Latest).await;
@@ -766,7 +747,7 @@ mod tests {
#[tokio::test]
async fn can_get_block_hash_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let block_hash = node.block_hash(BlockNumberOrTag::Latest).await;
@@ -778,7 +759,7 @@ mod tests {
#[tokio::test]
async fn can_get_block_timestamp_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let block_timestamp = node.block_timestamp(BlockNumberOrTag::Latest).await;
@@ -790,7 +771,7 @@ mod tests {
#[tokio::test]
async fn can_get_block_number_from_node() {
// Arrange
let (node, _temp_dir) = new_node();
let (_context, node) = new_node();
// Act
let block_number = node.last_block_number().await;
+59 -60
View File
@@ -19,8 +19,8 @@ use alloy::{
TransactionBuilderError, UnbuiltTransactionError,
},
primitives::{
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes,
StorageKey, TxHash, U256,
Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, StorageKey,
TxHash, U256,
},
providers::{
Provider, ProviderBuilder,
@@ -32,9 +32,8 @@ use alloy::{
eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
signers::local::PrivateKeySigner,
};
use anyhow::Context;
use anyhow::Context as _;
use revive_common::EVMVersion;
use revive_dt_common::fs::clear_directory;
use revive_dt_format::traits::ResolverApi;
@@ -43,7 +42,7 @@ use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use revive_dt_config::Arguments;
use revive_dt_config::*;
use revive_dt_node_interaction::EthereumNode;
use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
@@ -92,7 +91,7 @@ impl KitchensinkNode {
const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log";
const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log";
fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> {
fn init(&mut self, mut genesis: Genesis) -> anyhow::Result<&mut Self> {
let _ = clear_directory(&self.base_directory);
let _ = clear_directory(&self.logs_directory);
@@ -153,8 +152,6 @@ impl KitchensinkNode {
})
.collect();
let mut eth_balances = {
let mut genesis = serde_json::from_str::<Genesis>(genesis)
.context("Failed to deserialize EVM genesis JSON for kitchensink")?;
for signer_address in
<EthereumWallet as NetworkWallet<Ethereum>>::signer_addresses(&self.wallet)
{
@@ -586,35 +583,47 @@ impl ResolverApi for KitchensinkNode {
}
impl Node for KitchensinkNode {
fn new(config: &Arguments) -> Self {
let kitchensink_directory = config.directory().join(Self::BASE_DIRECTORY);
fn new(
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
+ AsRef<GethConfiguration>
+ AsRef<KitchensinkConfiguration>
+ AsRef<ReviveDevNodeConfiguration>
+ AsRef<EthRpcConfiguration>
+ Clone,
) -> Self {
let kitchensink_configuration = AsRef::<KitchensinkConfiguration>::as_ref(&context);
let dev_node_configuration = AsRef::<ReviveDevNodeConfiguration>::as_ref(&context);
let eth_rpc_configuration = AsRef::<EthRpcConfiguration>::as_ref(&context);
let working_directory_configuration =
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context);
let wallet_configuration = AsRef::<WalletConfiguration>::as_ref(&context);
let kitchensink_directory = working_directory_configuration
.as_path()
.join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
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);
}
let wallet = wallet_configuration.wallet();
Self {
id,
substrate_binary: config.kitchensink.clone(),
dev_node_binary: config.revive_dev_node.clone(),
eth_proxy_binary: config.eth_proxy.clone(),
substrate_binary: kitchensink_configuration.path.clone(),
dev_node_binary: dev_node_configuration.path.clone(),
eth_proxy_binary: eth_rpc_configuration.path.clone(),
rpc_url: String::new(),
base_directory,
logs_directory,
process_substrate: None,
process_proxy: None,
wallet: Arc::new(wallet),
wallet: wallet.clone(),
chain_id_filler: Default::default(),
nonce_manager: Default::default(),
use_kitchensink_not_dev_node: config.use_kitchensink_not_dev_node,
use_kitchensink_not_dev_node: kitchensink_configuration.use_kitchensink,
// 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),
@@ -655,8 +664,8 @@ impl Node for KitchensinkNode {
Ok(())
}
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(&genesis)?.spawn_process()
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()> {
self.init(genesis)?.spawn_process()
}
fn version(&self) -> anyhow::Result<String> {
@@ -1121,25 +1130,20 @@ impl BlockHeader for KitchenSinkHeader {
#[cfg(test)]
mod tests {
use alloy::rpc::types::TransactionRequest;
use revive_dt_config::Arguments;
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex};
use std::fs;
use super::*;
use crate::{GENESIS_JSON, Node};
use crate::Node;
fn test_config() -> Arguments {
Arguments {
kitchensink: PathBuf::from("substrate-node"),
eth_proxy: PathBuf::from("eth-rpc"),
use_kitchensink_not_dev_node: true,
..Default::default()
}
fn test_config() -> ExecutionContext {
let mut context = ExecutionContext::default();
context.kitchensink_configuration.use_kitchensink = true;
context
}
fn new_node() -> (KitchensinkNode, Arguments) {
fn new_node() -> (ExecutionContext, KitchensinkNode) {
// Note: When we run the tests in the CI we found that if they're all
// run in parallel then the CI is unable to start all of the nodes in
// time and their start up times-out. Therefore, we want all of the
@@ -1158,32 +1162,36 @@ mod tests {
static NODE_START_MUTEX: Mutex<()> = Mutex::new(());
let _guard = NODE_START_MUTEX.lock().unwrap();
let args = test_config();
let mut node = KitchensinkNode::new(&args);
node.init(GENESIS_JSON)
let context = test_config();
let mut node = KitchensinkNode::new(&context);
node.init(context.genesis_configuration.genesis().unwrap().clone())
.expect("Failed to initialize the node")
.spawn_process()
.expect("Failed to spawn the node process");
(node, args)
(context, node)
}
/// A shared node that multiple tests can use. It starts up once.
fn shared_node() -> &'static KitchensinkNode {
static NODE: LazyLock<(KitchensinkNode, Arguments)> = LazyLock::new(|| {
let (node, args) = new_node();
(node, args)
static NODE: LazyLock<(ExecutionContext, KitchensinkNode)> = LazyLock::new(|| {
let (context, node) = new_node();
(context, node)
});
&NODE.0
&NODE.1
}
#[tokio::test]
async fn node_mines_simple_transfer_transaction_and_returns_receipt() {
// Arrange
let (node, args) = new_node();
let (context, node) = new_node();
let provider = node.provider().await.expect("Failed to create provider");
let account_address = args.wallet().default_signer().address();
let account_address = context
.wallet_configuration
.wallet()
.default_signer()
.address();
let transaction = TransactionRequest::default()
.to(account_address)
.value(U256::from(100_000_000_000_000u128));
@@ -1217,7 +1225,9 @@ mod tests {
let mut dummy_node = KitchensinkNode::new(&test_config());
// Call `init()`
dummy_node.init(genesis_content).expect("init failed");
dummy_node
.init(serde_json::from_str(genesis_content).unwrap())
.expect("init failed");
// Check that the patched chainspec file was generated
let final_chainspec_path = dummy_node
@@ -1327,20 +1337,10 @@ mod tests {
}
}
#[test]
fn spawn_works() {
let config = test_config();
let mut node = KitchensinkNode::new(&config);
node.spawn(GENESIS_JSON.to_string()).unwrap();
}
#[test]
fn version_works() {
let config = test_config();
let node = shared_node();
let node = KitchensinkNode::new(&config);
let version = node.version().unwrap();
assert!(
@@ -1351,9 +1351,8 @@ mod tests {
#[test]
fn eth_rpc_version_works() {
let config = test_config();
let node = shared_node();
let node = KitchensinkNode::new(&config);
let version = node.eth_rpc_version().unwrap();
assert!(
+14 -6
View File
@@ -1,7 +1,8 @@
//! This crate implements the testing nodes.
use alloy::genesis::Genesis;
use revive_common::EVMVersion;
use revive_dt_config::Arguments;
use revive_dt_config::*;
use revive_dt_node_interaction::EthereumNode;
pub mod common;
@@ -10,13 +11,20 @@ pub mod geth;
pub mod kitchensink;
pub mod pool;
/// The default genesis configuration.
pub const GENESIS_JSON: &str = include_str!("../../../genesis.json");
/// An abstract interface for testing nodes.
pub trait Node: EthereumNode {
/// Create a new uninitialized instance.
fn new(config: &Arguments) -> Self;
fn new(
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
+ AsRef<GethConfiguration>
+ AsRef<KitchensinkConfiguration>
+ AsRef<ReviveDevNodeConfiguration>
+ AsRef<EthRpcConfiguration>
+ Clone,
) -> Self;
/// Returns the identifier of the node.
fn id(&self) -> usize;
@@ -24,7 +32,7 @@ pub trait Node: EthereumNode {
/// Spawns a node configured according to the genesis json.
///
/// Blocking until it's ready to accept transactions.
fn spawn(&mut self, genesis: String) -> anyhow::Result<()>;
fn spawn(&mut self, genesis: Genesis) -> anyhow::Result<()>;
/// Prune the node instance and related data.
///
+42 -14
View File
@@ -5,10 +5,13 @@ use std::{
thread,
};
use revive_dt_common::cached_fs::read_to_string;
use anyhow::Context;
use revive_dt_config::Arguments;
use alloy::genesis::Genesis;
use anyhow::Context as _;
use revive_dt_config::{
ConcurrencyConfiguration, EthRpcConfiguration, GenesisConfiguration, GethConfiguration,
KitchensinkConfiguration, ReviveDevNodeConfiguration, WalletConfiguration,
WorkingDirectoryConfiguration,
};
use tracing::info;
use crate::Node;
@@ -25,18 +28,31 @@ where
T: Node + Send + 'static,
{
/// Create a new Pool. This will start as many nodes as there are workers in `config`.
pub fn new(config: &Arguments) -> anyhow::Result<Self> {
let nodes = config.number_of_nodes;
let genesis = read_to_string(&config.genesis_file).context(format!(
"can not read genesis file: {}",
config.genesis_file.display()
))?;
pub fn new(
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
+ AsRef<GethConfiguration>
+ AsRef<KitchensinkConfiguration>
+ AsRef<ReviveDevNodeConfiguration>
+ AsRef<EthRpcConfiguration>
+ Send
+ Sync
+ Clone
+ 'static,
) -> anyhow::Result<Self> {
let concurrency_configuration = AsRef::<ConcurrencyConfiguration>::as_ref(&context);
let genesis_configuration = AsRef::<GenesisConfiguration>::as_ref(&context);
let nodes = concurrency_configuration.number_of_nodes;
let genesis = genesis_configuration.genesis()?;
let mut handles = Vec::with_capacity(nodes);
for _ in 0..nodes {
let config = config.clone();
let context = context.clone();
let genesis = genesis.clone();
handles.push(thread::spawn(move || spawn_node::<T>(&config, genesis)));
handles.push(thread::spawn(move || spawn_node::<T>(context, genesis)));
}
let mut nodes = Vec::with_capacity(nodes);
@@ -64,8 +80,20 @@ where
}
}
fn spawn_node<T: Node + Send>(args: &Arguments, genesis: String) -> anyhow::Result<T> {
let mut node = T::new(args);
fn spawn_node<T: Node + Send>(
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
+ AsRef<GethConfiguration>
+ AsRef<KitchensinkConfiguration>
+ AsRef<ReviveDevNodeConfiguration>
+ AsRef<EthRpcConfiguration>
+ Clone
+ 'static,
genesis: Genesis,
) -> anyhow::Result<T> {
let mut node = T::new(context);
info!(
id = node.id(),
connection_string = node.connection_string(),