Provide a common node implementation for substrate chains

This commit is contained in:
Omar Abdulla
2025-09-17 20:23:31 +03:00
parent ac0f4e0cf2
commit f0f59ad024
4 changed files with 107 additions and 126 deletions
+1 -2
View File
@@ -2,14 +2,13 @@
use alloy::genesis::Genesis;
use revive_common::EVMVersion;
use revive_dt_config::*;
use revive_dt_node_interaction::EthereumNode;
pub mod common;
pub mod constants;
pub mod geth;
pub mod kitchensink;
pub mod pool;
pub mod substrate;
/// An abstract interface for testing nodes.
pub trait Node: EthereumNode {
+2 -3
View File
@@ -12,7 +12,6 @@ use revive_dt_config::{
KitchensinkConfiguration, ReviveDevNodeConfiguration, WalletConfiguration,
WorkingDirectoryConfiguration,
};
use tracing::info;
use crate::Node;
@@ -81,7 +80,7 @@ where
}
fn spawn_node<T: Node + Send>(
context: impl AsRef<WorkingDirectoryConfiguration>
_: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
@@ -91,7 +90,7 @@ fn spawn_node<T: Node + Send>(
+ AsRef<EthRpcConfiguration>
+ Clone
+ 'static,
genesis: Genesis,
_: Genesis,
) -> anyhow::Result<T> {
todo!("Remove");
}
@@ -51,12 +51,15 @@ use crate::{Node, common::FallbackGasFiller, constants::INITIAL_BALANCE};
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
/// A node implementation for Substrate based chains. Currently, this supports either kitchensink
/// or the revive-dev-node which is done by changing the path and some of the other arguments passed
/// to the command.
#[derive(Debug)]
pub struct KitchensinkNode {
pub struct SubstrateNode {
id: u32,
substrate_binary: PathBuf,
dev_node_binary: PathBuf,
node_binary: PathBuf,
eth_proxy_binary: PathBuf,
export_chainspec_command: String,
rpc_url: String,
base_directory: PathBuf,
logs_directory: PathBuf,
@@ -65,16 +68,11 @@ pub struct KitchensinkNode {
wallet: Arc<EthereumWallet>,
nonce_manager: CachedNonceManager,
chain_id_filler: ChainIdFiller,
use_kitchensink_not_dev_node: bool,
/// 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
/// what it belongs to, we just want to flush them on [`Drop`] of the node.
logs_file_to_flush: Vec<File>,
}
impl KitchensinkNode {
const BASE_DIRECTORY: &str = "kitchensink";
impl SubstrateNode {
const BASE_DIRECTORY: &str = "Substrate";
const LOGS_DIRECTORY: &str = "logs";
const DATA_DIRECTORY: &str = "chains";
@@ -87,44 +85,39 @@ impl KitchensinkNode {
const SUBSTRATE_LOG_ENV: &str = "error,evm=debug,sc_rpc_server=info,runtime::revive=debug";
const PROXY_LOG_ENV: &str = "info,eth-rpc=debug";
const KITCHENSINK_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const KITCHENSINK_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const SUBSTRATE_STDOUT_LOG_FILE_NAME: &str = "node_stdout.log";
const SUBSTRATE_STDERR_LOG_FILE_NAME: &str = "node_stderr.log";
const PROXY_STDOUT_LOG_FILE_NAME: &str = "proxy_stdout.log";
const PROXY_STDERR_LOG_FILE_NAME: &str = "proxy_stderr.log";
pub const KITCHENSINK_EXPORT_CHAINSPEC_COMMAND: &str = "export-chain-spec";
pub const REVIVE_DEV_NODE_EXPORT_CHAINSPEC_COMMAND: &str = "build-spec";
pub fn new(
node_path: PathBuf,
export_chainspec_command: &str,
context: impl AsRef<WorkingDirectoryConfiguration>
+ AsRef<ConcurrencyConfiguration>
+ AsRef<GenesisConfiguration>
+ AsRef<WalletConfiguration>
+ AsRef<GethConfiguration>
+ AsRef<KitchensinkConfiguration>
+ AsRef<ReviveDevNodeConfiguration>
+ AsRef<EthRpcConfiguration>
+ Clone,
+ AsRef<WalletConfiguration>,
) -> 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 working_directory_path =
AsRef::<WorkingDirectoryConfiguration>::as_ref(&context).as_path();
let eth_rpc_path = AsRef::<EthRpcConfiguration>::as_ref(&context)
.path
.as_path();
let wallet = AsRef::<WalletConfiguration>::as_ref(&context).wallet();
let kitchensink_directory = working_directory_configuration
.as_path()
.join(Self::BASE_DIRECTORY);
let substrate_directory = working_directory_path.join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = kitchensink_directory.join(id.to_string());
let base_directory = substrate_directory.join(id.to_string());
let logs_directory = base_directory.join(Self::LOGS_DIRECTORY);
let wallet = wallet_configuration.wallet();
Self {
id,
substrate_binary: kitchensink_configuration.path.clone(),
dev_node_binary: dev_node_configuration.path.clone(),
eth_proxy_binary: eth_rpc_configuration.path.clone(),
node_binary: node_path,
eth_proxy_binary: eth_rpc_path.to_path_buf(),
export_chainspec_command: export_chainspec_command.to_string(),
rpc_url: String::new(),
base_directory,
logs_directory,
@@ -133,9 +126,6 @@ impl KitchensinkNode {
wallet: wallet.clone(),
chain_id_filler: Default::default(),
nonce_manager: Default::default(),
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),
}
}
@@ -153,33 +143,24 @@ impl KitchensinkNode {
// Note: we do not pipe the logs of this process to a separate file since this is just a
// once-off export of the default chain spec and not part of the long-running node process.
let output = if self.use_kitchensink_not_dev_node {
Command::new(&self.substrate_binary)
.arg("export-chain-spec")
.arg("--chain")
.arg("dev")
.output()
.context("Failed to export the chain-spec")?
} else {
Command::new(&self.dev_node_binary)
.arg("build-spec")
.arg("--chain")
.arg("dev")
.output()
.context("Failed to export the chain-spec")?
};
let output = Command::new(&self.node_binary)
.arg(self.export_chainspec_command.as_str())
.arg("--chain")
.arg("dev")
.output()
.context("Failed to export the chain-spec")?;
if !output.status.success() {
anyhow::bail!(
"substrate-node export-chain-spec failed: {}",
"Substrate-node export-chain-spec failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let content = String::from_utf8(output.stdout)
.context("Failed to decode substrate export-chain-spec output as UTF-8")?;
.context("Failed to decode Substrate export-chain-spec output as UTF-8")?;
let mut chainspec_json: JsonValue =
serde_json::from_str(&content).context("Failed to parse substrate chain spec JSON")?;
serde_json::from_str(&content).context("Failed to parse Substrate chain spec JSON")?;
let existing_chainspec_balances =
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
@@ -250,17 +231,13 @@ impl KitchensinkNode {
// Start Substrate node
let kitchensink_stdout_logs_file = open_options
.clone()
.open(self.kitchensink_stdout_log_file_path())
.open(self.substrate_stdout_log_file_path())
.context("Failed to open kitchensink stdout logs file")?;
let kitchensink_stderr_logs_file = open_options
.clone()
.open(self.kitchensink_stderr_log_file_path())
.open(self.substrate_stderr_log_file_path())
.context("Failed to open kitchensink stderr logs file")?;
let node_binary_path = if self.use_kitchensink_not_dev_node {
self.substrate_binary.as_path()
} else {
self.dev_node_binary.as_path()
};
let node_binary_path = self.node_binary.as_path();
self.process_substrate = Command::new(node_binary_path)
.arg("--dev")
.arg("--chain")
@@ -290,17 +267,17 @@ impl KitchensinkNode {
.context("Failed to clone kitchensink stderr log file handle")?,
)
.spawn()
.context("Failed to spawn substrate node process")?
.context("Failed to spawn Substrate node process")?
.into();
// Give the node a moment to boot
if let Err(error) = Self::wait_ready(
self.kitchensink_stderr_log_file_path().as_path(),
self.substrate_stderr_log_file_path().as_path(),
Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(60),
) {
self.shutdown()
.context("Failed to gracefully shutdown after substrate start error")?;
.context("Failed to gracefully shutdown after Substrate start error")?;
return Err(error);
};
@@ -414,14 +391,14 @@ impl KitchensinkNode {
Ok(String::from_utf8_lossy(&output).trim().to_string())
}
fn kitchensink_stdout_log_file_path(&self) -> PathBuf {
fn substrate_stdout_log_file_path(&self) -> PathBuf {
self.logs_directory
.join(Self::KITCHENSINK_STDOUT_LOG_FILE_NAME)
.join(Self::SUBSTRATE_STDOUT_LOG_FILE_NAME)
}
fn kitchensink_stderr_log_file_path(&self) -> PathBuf {
fn substrate_stderr_log_file_path(&self) -> PathBuf {
self.logs_directory
.join(Self::KITCHENSINK_STDERR_LOG_FILE_NAME)
.join(Self::SUBSTRATE_STDERR_LOG_FILE_NAME)
}
fn proxy_stdout_log_file_path(&self) -> PathBuf {
@@ -435,15 +412,11 @@ impl KitchensinkNode {
async fn provider(
&self,
) -> anyhow::Result<
FillProvider<
impl TxFiller<KitchensinkNetwork>,
impl Provider<KitchensinkNetwork>,
KitchensinkNetwork,
>,
FillProvider<impl TxFiller<ReviveNetwork>, impl Provider<ReviveNetwork>, ReviveNetwork>,
> {
ProviderBuilder::new()
.disable_recommended_fillers()
.network::<KitchensinkNetwork>()
.network::<ReviveNetwork>()
.filler(FallbackGasFiller::new(
25_000_000,
1_000_000_000,
@@ -458,7 +431,7 @@ impl KitchensinkNode {
}
}
impl EthereumNode for KitchensinkNode {
impl EthereumNode for SubstrateNode {
fn execute_transaction(
&self,
transaction: alloy::rpc::types::TransactionRequest,
@@ -551,19 +524,18 @@ impl EthereumNode for KitchensinkNode {
Box::pin(async move {
let id = self.id;
let provider = self.provider().await?;
Ok(Box::new(KitchensinkNodeResolver { id, provider }) as Box<dyn ResolverApi>)
Ok(Box::new(SubstrateNodeResolver { id, provider }) as Box<dyn ResolverApi>)
})
}
}
pub struct KitchensinkNodeResolver<F: TxFiller<KitchensinkNetwork>, P: Provider<KitchensinkNetwork>>
{
pub struct SubstrateNodeResolver<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> {
id: u32,
provider: FillProvider<F, P, KitchensinkNetwork>,
provider: FillProvider<F, P, ReviveNetwork>,
}
impl<F: TxFiller<KitchensinkNetwork>, P: Provider<KitchensinkNetwork>> ResolverApi
for KitchensinkNodeResolver<F, P>
impl<F: TxFiller<ReviveNetwork>, P: Provider<ReviveNetwork>> ResolverApi
for SubstrateNodeResolver<F, P>
{
#[instrument(level = "info", skip_all, fields(kitchensink_node_id = self.id))]
fn chain_id(
@@ -688,7 +660,7 @@ impl<F: TxFiller<KitchensinkNetwork>, P: Provider<KitchensinkNetwork>> ResolverA
}
// TODO: Remove
impl ResolverApi for KitchensinkNode {
impl ResolverApi for SubstrateNode {
fn chain_id(
&self,
) -> Pin<Box<dyn Future<Output = anyhow::Result<alloy::primitives::ChainId>> + '_>> {
@@ -830,7 +802,7 @@ impl ResolverApi for KitchensinkNode {
}
}
impl Node for KitchensinkNode {
impl Node for SubstrateNode {
fn id(&self) -> usize {
self.id as _
}
@@ -848,7 +820,7 @@ impl Node for KitchensinkNode {
}
if let Some(mut child) = self.process_substrate.take() {
child.kill().map_err(|error| {
anyhow::anyhow!("Failed to kill the substrate process: {error:?}")
anyhow::anyhow!("Failed to kill the Substrate process: {error:?}")
})?;
}
@@ -870,7 +842,7 @@ impl Node for KitchensinkNode {
}
fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.substrate_binary)
let output = Command::new(&self.node_binary)
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::piped())
@@ -895,16 +867,16 @@ impl Node for KitchensinkNode {
}
}
impl Drop for KitchensinkNode {
impl Drop for SubstrateNode {
fn drop(&mut self) {
self.shutdown().expect("Failed to shutdown")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct KitchensinkNetwork;
pub struct ReviveNetwork;
impl Network for KitchensinkNetwork {
impl Network for ReviveNetwork {
type TxType = <Ethereum as Network>::TxType;
type TxEnvelope = <Ethereum as Network>::TxEnvelope;
@@ -913,7 +885,7 @@ impl Network for KitchensinkNetwork {
type ReceiptEnvelope = <Ethereum as Network>::ReceiptEnvelope;
type Header = KitchensinkHeader;
type Header = ReviveHeader;
type TransactionRequest = <Ethereum as Network>::TransactionRequest;
@@ -921,12 +893,12 @@ impl Network for KitchensinkNetwork {
type ReceiptResponse = <Ethereum as Network>::ReceiptResponse;
type HeaderResponse = Header<KitchensinkHeader>;
type HeaderResponse = Header<ReviveHeader>;
type BlockResponse = Block<Transaction<TxEnvelope>, Header<KitchensinkHeader>>;
type BlockResponse = Block<Transaction<TxEnvelope>, Header<ReviveHeader>>;
}
impl TransactionBuilder<KitchensinkNetwork> for <Ethereum as Network>::TransactionRequest {
impl TransactionBuilder<ReviveNetwork> for <Ethereum as Network>::TransactionRequest {
fn chain_id(&self) -> Option<alloy::primitives::ChainId> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::chain_id(self)
}
@@ -1058,7 +1030,7 @@ impl TransactionBuilder<KitchensinkNetwork> for <Ethereum as Network>::Transacti
fn complete_type(
&self,
ty: <KitchensinkNetwork as Network>::TxType,
ty: <ReviveNetwork as Network>::TxType,
) -> Result<(), Vec<&'static str>> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::complete_type(
self, ty,
@@ -1075,13 +1047,13 @@ impl TransactionBuilder<KitchensinkNetwork> for <Ethereum as Network>::Transacti
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::can_build(self)
}
fn output_tx_type(&self) -> <KitchensinkNetwork as Network>::TxType {
fn output_tx_type(&self) -> <ReviveNetwork as Network>::TxType {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::output_tx_type(
self,
)
}
fn output_tx_type_checked(&self) -> Option<<KitchensinkNetwork as Network>::TxType> {
fn output_tx_type_checked(&self) -> Option<<ReviveNetwork as Network>::TxType> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::output_tx_type_checked(
self,
)
@@ -1095,15 +1067,14 @@ impl TransactionBuilder<KitchensinkNetwork> for <Ethereum as Network>::Transacti
fn build_unsigned(
self,
) -> alloy::network::BuildResult<<KitchensinkNetwork as Network>::UnsignedTx, KitchensinkNetwork>
{
) -> alloy::network::BuildResult<<ReviveNetwork as Network>::UnsignedTx, ReviveNetwork> {
let result = <<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::build_unsigned(
self,
);
match result {
Ok(unsigned_tx) => Ok(unsigned_tx),
Err(UnbuiltTransactionError { request, error }) => {
Err(UnbuiltTransactionError::<KitchensinkNetwork> {
Err(UnbuiltTransactionError::<ReviveNetwork> {
request,
error: match error {
TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => {
@@ -1124,20 +1095,18 @@ impl TransactionBuilder<KitchensinkNetwork> for <Ethereum as Network>::Transacti
}
}
async fn build<W: alloy::network::NetworkWallet<KitchensinkNetwork>>(
async fn build<W: alloy::network::NetworkWallet<ReviveNetwork>>(
self,
wallet: &W,
) -> Result<
<KitchensinkNetwork as Network>::TxEnvelope,
TransactionBuilderError<KitchensinkNetwork>,
> {
) -> Result<<ReviveNetwork as Network>::TxEnvelope, TransactionBuilderError<ReviveNetwork>>
{
Ok(wallet.sign_request(self).await?)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KitchensinkHeader {
pub struct ReviveHeader {
/// The Keccak 256-bit hash of the parent
/// blocks header, in its entirety; formally Hp.
pub parent_hash: B256,
@@ -1240,7 +1209,7 @@ pub struct KitchensinkHeader {
pub requests_hash: Option<B256>,
}
impl BlockHeader for KitchensinkHeader {
impl BlockHeader for ReviveHeader {
fn parent_hash(&self) -> B256 {
self.parent_hash
}
@@ -1344,7 +1313,7 @@ mod tests {
context
}
fn new_node() -> (ExecutionContext, KitchensinkNode) {
fn new_node() -> (ExecutionContext, SubstrateNode) {
// 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
@@ -1364,7 +1333,11 @@ mod tests {
let _guard = NODE_START_MUTEX.lock().unwrap();
let context = test_config();
let mut node = KitchensinkNode::new(&context);
let mut node = SubstrateNode::new(
context.kitchensink_configuration.path.clone(),
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
&context,
);
node.init(context.genesis_configuration.genesis().unwrap().clone())
.expect("Failed to initialize the node")
.spawn_process()
@@ -1373,8 +1346,8 @@ mod tests {
}
/// A shared node that multiple tests can use. It starts up once.
fn shared_node() -> &'static KitchensinkNode {
static NODE: LazyLock<(ExecutionContext, KitchensinkNode)> = LazyLock::new(|| {
fn shared_node() -> &'static SubstrateNode {
static NODE: LazyLock<(ExecutionContext, SubstrateNode)> = LazyLock::new(|| {
let (context, node) = new_node();
(context, node)
});
@@ -1423,7 +1396,12 @@ mod tests {
}
"#;
let mut dummy_node = KitchensinkNode::new(&test_config());
let context = test_config();
let mut dummy_node = SubstrateNode::new(
context.kitchensink_configuration.path.clone(),
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
&context,
);
// Call `init()`
dummy_node
@@ -1433,16 +1411,16 @@ mod tests {
// Check that the patched chainspec file was generated
let final_chainspec_path = dummy_node
.base_directory
.join(KitchensinkNode::CHAIN_SPEC_JSON_FILE);
.join(SubstrateNode::CHAIN_SPEC_JSON_FILE);
assert!(final_chainspec_path.exists(), "Chainspec file should exist");
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(
let first_eth_addr = SubstrateNode::eth_to_substrate_address(
&"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1".parse().unwrap(),
);
let second_eth_addr = KitchensinkNode::eth_to_substrate_address(
let second_eth_addr = SubstrateNode::eth_to_substrate_address(
&"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2".parse().unwrap(),
);
@@ -1469,7 +1447,12 @@ mod tests {
}
"#;
let node = KitchensinkNode::new(&test_config());
let context = test_config();
let node = SubstrateNode::new(
context.kitchensink_configuration.path.clone(),
SubstrateNode::KITCHENSINK_EXPORT_CHAINSPEC_COMMAND,
&context,
);
let result = node
.extract_balance_from_genesis_file(&serde_json::from_str(genesis_json).unwrap())
@@ -1502,7 +1485,7 @@ mod tests {
];
for eth_addr in eth_addresses {
let ss58 = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
let ss58 = SubstrateNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}");
}
@@ -1530,7 +1513,7 @@ mod tests {
];
for (eth_addr, expected_ss58) in cases {
let result = KitchensinkNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
let result = SubstrateNode::eth_to_substrate_address(&eth_addr.parse().unwrap());
assert_eq!(
result, expected_ss58,
"Mismatch for Ethereum address {eth_addr}"
@@ -1545,8 +1528,8 @@ mod tests {
let version = node.version().unwrap();
assert!(
version.starts_with("substrate-node"),
"Expected substrate-node version string, got: {version}"
version.starts_with("Substrate-node"),
"Expected Substrate-node version string, got: {version}"
);
}