Files
revive-differential-tests/crates/node/src/kitchensink.rs
T
Omar 5eb3a0e1b5 Fix for "transaction indexing is in progress" (#32)
* Retry getting transaction receipt

* Small fix to logging consistency

* Introduce a custom kitchensink network

* Fix formtting and clippy
2025-07-14 09:32:57 +00:00

994 lines
33 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{
collections::HashMap,
fs::create_dir_all,
io::BufRead,
path::PathBuf,
process::{Child, Command, Stdio},
sync::{
Mutex,
atomic::{AtomicU32, Ordering},
},
time::Duration,
};
use alloy::{
consensus::{BlockHeader, TxEnvelope},
hex,
network::{
Ethereum, EthereumWallet, Network, TransactionBuilder, TransactionBuilderError,
UnbuiltTransactionError,
},
primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256},
providers::{Provider, ProviderBuilder, ext::DebugApi},
rpc::types::{
TransactionReceipt,
eth::{Block, Header, Transaction},
trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame},
},
};
use serde::{Deserialize, Serialize};
use serde_json::{Value as JsonValue, json};
use sp_core::crypto::Ss58Codec;
use sp_runtime::AccountId32;
use revive_dt_config::Arguments;
use revive_dt_node_interaction::{
EthereumNode, nonce::fetch_onchain_nonce, trace::trace_transaction,
transaction::execute_transaction,
};
use crate::Node;
static NODE_COUNT: AtomicU32 = AtomicU32::new(0);
#[derive(Debug)]
pub struct KitchensinkNode {
id: u32,
substrate_binary: PathBuf,
eth_proxy_binary: PathBuf,
rpc_url: String,
wallet: EthereumWallet,
base_directory: PathBuf,
process_substrate: Option<Child>,
process_proxy: Option<Child>,
nonces: Mutex<HashMap<Address, u64>>,
}
impl KitchensinkNode {
const BASE_DIRECTORY: &str = "kitchensink";
const SUBSTRATE_READY_MARKER: &str = "Running JSON-RPC server";
const ETH_PROXY_READY_MARKER: &str = "Running JSON-RPC server";
const CHAIN_SPEC_JSON_FILE: &str = "template_chainspec.json";
const BASE_SUBSTRATE_RPC_PORT: u16 = 9944;
const BASE_PROXY_RPC_PORT: u16 = 8545;
const SUBSTRATE_LOG_ENV: &str = "error,evm=debug,sc_rpc_server=info,runtime::revive=debug";
const PROXY_LOG_ENV: &str = "info,eth-rpc=debug";
fn init(&mut self, genesis: &str) -> anyhow::Result<&mut Self> {
create_dir_all(&self.base_directory)?;
let template_chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE);
let output = Command::new(&self.substrate_binary)
.arg("export-chain-spec")
.arg("--chain")
.arg("dev")
.output()?;
if !output.status.success() {
anyhow::bail!(
"substrate-node export-chain-spec failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let content = String::from_utf8(output.stdout)?;
let mut chainspec_json: JsonValue = serde_json::from_str(&content)?;
let existing_chainspec_balances =
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"]
.as_array()
.cloned()
.unwrap_or_default();
let mut merged_balances: Vec<(String, u128)> = existing_chainspec_balances
.into_iter()
.filter_map(|val| {
if let Some(arr) = val.as_array() {
if arr.len() == 2 {
let account = arr[0].as_str()?.to_string();
let balance = arr[1].as_f64()? as u128;
return Some((account, balance));
}
}
None
})
.collect();
let mut eth_balances = self.extract_balance_from_genesis_file(genesis)?;
merged_balances.append(&mut eth_balances);
chainspec_json["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] =
json!(merged_balances);
serde_json::to_writer_pretty(
std::fs::File::create(&template_chainspec_path)?,
&chainspec_json,
)?;
Ok(self)
}
fn spawn_process(&mut self) -> anyhow::Result<()> {
let substrate_rpc_port = Self::BASE_SUBSTRATE_RPC_PORT + self.id as u16;
let proxy_rpc_port = Self::BASE_PROXY_RPC_PORT + self.id as u16;
self.rpc_url = format!("http://127.0.0.1:{proxy_rpc_port}");
let chainspec_path = self.base_directory.join(Self::CHAIN_SPEC_JSON_FILE);
// Start Substrate node
let mut substrate_process = Command::new(&self.substrate_binary)
.arg("--chain")
.arg(chainspec_path)
.arg("--base-path")
.arg(&self.base_directory)
.arg("--rpc-port")
.arg(substrate_rpc_port.to_string())
.arg("--name")
.arg(format!("revive-kitchensink-{}", self.id))
.arg("--force-authoring")
.arg("--rpc-methods")
.arg("Unsafe")
.arg("--rpc-cors")
.arg("all")
.env("RUST_LOG", Self::SUBSTRATE_LOG_ENV)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()?;
// Give the node a moment to boot
Self::wait_ready(
&mut substrate_process,
Self::SUBSTRATE_READY_MARKER,
Duration::from_secs(30),
)?;
let mut proxy_process = Command::new(&self.eth_proxy_binary)
.arg("--dev")
.arg("--rpc-port")
.arg(proxy_rpc_port.to_string())
.arg("--node-rpc-url")
.arg(format!("ws://127.0.0.1:{substrate_rpc_port}"))
.env("RUST_LOG", Self::PROXY_LOG_ENV)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()?;
Self::wait_ready(
&mut proxy_process,
Self::ETH_PROXY_READY_MARKER,
Duration::from_secs(30),
)?;
self.process_substrate = Some(substrate_process);
self.process_proxy = Some(proxy_process);
Ok(())
}
fn extract_balance_from_genesis_file(
&self,
genesis_str: &str,
) -> 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)
}
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()
);
}
let mut padded = [0xEEu8; 32];
padded[..20].copy_from_slice(&eth_bytes);
let account_id = AccountId32::from(padded);
Ok(account_id.to_ss58check())
}
fn wait_ready(child: &mut Child, marker: &str, timeout: Duration) -> anyhow::Result<()> {
let start_time = std::time::Instant::now();
let stderr = child.stderr.take().expect("stderr must be piped");
let mut lines = std::io::BufReader::new(stderr).lines();
loop {
if let Some(Ok(line)) = lines.next() {
println!("Kitchensink log: {line:?}");
if line.contains(marker) {
std::thread::spawn(move || for _ in lines.by_ref() {});
return Ok(());
}
}
if start_time.elapsed() > timeout {
let _ = child.kill();
anyhow::bail!("Timeout waiting for process readiness: {marker}");
}
}
}
pub fn eth_rpc_version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.eth_proxy_binary)
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?
.wait_with_output()?
.stdout;
Ok(String::from_utf8_lossy(&output).trim().to_string())
}
}
impl EthereumNode for KitchensinkNode {
fn execute_transaction(
&self,
transaction: alloy::rpc::types::TransactionRequest,
) -> anyhow::Result<TransactionReceipt> {
let url = self.rpc_url.clone();
let wallet = self.wallet.clone();
tracing::debug!("Submitting transaction: {transaction:#?}");
tracing::info!("Submitting tx to kitchensink");
let receipt = execute_transaction(Box::pin(async move {
Ok(ProviderBuilder::new()
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&url)
.await?
.send_transaction(transaction)
.await?
.get_receipt()
.await?)
}));
tracing::info!(?receipt, "Submitted tx to kitchensink");
receipt
}
fn trace_transaction(
&self,
transaction: TransactionReceipt,
) -> anyhow::Result<alloy::rpc::types::trace::geth::GethTrace> {
let url = self.rpc_url.clone();
let trace_options = GethDebugTracingOptions::prestate_tracer(PreStateConfig {
diff_mode: Some(true),
disable_code: None,
disable_storage: None,
});
let wallet = self.wallet.clone();
trace_transaction(Box::pin(async move {
Ok(ProviderBuilder::new()
.network::<KitchenSinkNetwork>()
.wallet(wallet)
.connect(&url)
.await?
.debug_trace_transaction(transaction.transaction_hash, trace_options)
.await?)
}))
}
fn state_diff(&self, transaction: TransactionReceipt) -> anyhow::Result<DiffMode> {
match self
.trace_transaction(transaction)?
.try_into_pre_state_frame()?
{
PreStateFrame::Diff(diff) => Ok(diff),
_ => anyhow::bail!("expected a diff mode trace"),
}
}
fn fetch_add_nonce(&self, address: Address) -> anyhow::Result<u64> {
let url = self.rpc_url.clone();
let wallet = self.wallet.clone();
let onchain_nonce = fetch_onchain_nonce(url, wallet, address)?;
let mut nonces = self.nonces.lock().unwrap();
let current = nonces.entry(address).or_insert(onchain_nonce);
let value = *current;
*current += 1;
Ok(value)
}
}
impl Node for KitchensinkNode {
fn new(config: &Arguments) -> Self {
let kitchensink_directory = config.directory().join(Self::BASE_DIRECTORY);
let id = NODE_COUNT.fetch_add(1, Ordering::SeqCst);
let base_directory = kitchensink_directory.join(id.to_string());
Self {
id,
substrate_binary: config.kitchensink.clone(),
eth_proxy_binary: config.eth_proxy.clone(),
rpc_url: String::new(),
wallet: config.wallet(),
base_directory,
process_substrate: None,
process_proxy: None,
nonces: Mutex::new(HashMap::new()),
}
}
fn connection_string(&self) -> String {
self.rpc_url.clone()
}
fn shutdown(mut self) -> anyhow::Result<()> {
if let Some(mut child) = self.process_proxy.take() {
let _ = child.kill();
}
if let Some(mut child) = self.process_substrate.take() {
let _ = child.kill();
}
Ok(())
}
fn spawn(&mut self, genesis: String) -> anyhow::Result<()> {
self.init(&genesis)?.spawn_process()
}
fn version(&self) -> anyhow::Result<String> {
let output = Command::new(&self.substrate_binary)
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?
.wait_with_output()?
.stdout;
Ok(String::from_utf8_lossy(&output).into())
}
}
impl Drop for KitchensinkNode {
fn drop(&mut self) {
if let Some(mut child) = self.process_proxy.take() {
let _ = child.kill();
}
if let Some(mut child) = self.process_substrate.take() {
let _ = child.kill();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct KitchenSinkNetwork;
impl Network for KitchenSinkNetwork {
type TxType = <Ethereum as Network>::TxType;
type TxEnvelope = <Ethereum as Network>::TxEnvelope;
type UnsignedTx = <Ethereum as Network>::UnsignedTx;
type ReceiptEnvelope = <Ethereum as Network>::ReceiptEnvelope;
type Header = KitchenSinkHeader;
type TransactionRequest = <Ethereum as Network>::TransactionRequest;
type TransactionResponse = <Ethereum as Network>::TransactionResponse;
type ReceiptResponse = <Ethereum as Network>::ReceiptResponse;
type HeaderResponse = Header<KitchenSinkHeader>;
type BlockResponse = Block<Transaction<TxEnvelope>, Header<KitchenSinkHeader>>;
}
impl TransactionBuilder<KitchenSinkNetwork> for <Ethereum as Network>::TransactionRequest {
fn chain_id(&self) -> Option<alloy::primitives::ChainId> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::chain_id(self)
}
fn set_chain_id(&mut self, chain_id: alloy::primitives::ChainId) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_chain_id(
self, chain_id,
)
}
fn nonce(&self) -> Option<u64> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::nonce(self)
}
fn set_nonce(&mut self, nonce: u64) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_nonce(
self, nonce,
)
}
fn input(&self) -> Option<&alloy::primitives::Bytes> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::input(self)
}
fn set_input<T: Into<alloy::primitives::Bytes>>(&mut self, input: T) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_input(
self, input,
)
}
fn from(&self) -> Option<Address> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::from(self)
}
fn set_from(&mut self, from: Address) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_from(
self, from,
)
}
fn kind(&self) -> Option<alloy::primitives::TxKind> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::kind(self)
}
fn clear_kind(&mut self) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::clear_kind(
self,
)
}
fn set_kind(&mut self, kind: alloy::primitives::TxKind) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_kind(
self, kind,
)
}
fn value(&self) -> Option<alloy::primitives::U256> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::value(self)
}
fn set_value(&mut self, value: alloy::primitives::U256) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_value(
self, value,
)
}
fn gas_price(&self) -> Option<u128> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::gas_price(self)
}
fn set_gas_price(&mut self, gas_price: u128) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_gas_price(
self, gas_price,
)
}
fn max_fee_per_gas(&self) -> Option<u128> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::max_fee_per_gas(
self,
)
}
fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_max_fee_per_gas(
self, max_fee_per_gas
)
}
fn max_priority_fee_per_gas(&self) -> Option<u128> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::max_priority_fee_per_gas(
self,
)
}
fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_max_priority_fee_per_gas(
self, max_priority_fee_per_gas
)
}
fn gas_limit(&self) -> Option<u64> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::gas_limit(self)
}
fn set_gas_limit(&mut self, gas_limit: u64) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_gas_limit(
self, gas_limit,
)
}
fn access_list(&self) -> Option<&alloy::rpc::types::AccessList> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::access_list(
self,
)
}
fn set_access_list(&mut self, access_list: alloy::rpc::types::AccessList) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::set_access_list(
self,
access_list,
)
}
fn complete_type(
&self,
ty: <KitchenSinkNetwork as Network>::TxType,
) -> Result<(), Vec<&'static str>> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::complete_type(
self, ty,
)
}
fn can_submit(&self) -> bool {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::can_submit(
self,
)
}
fn can_build(&self) -> bool {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::can_build(self)
}
fn output_tx_type(&self) -> <KitchenSinkNetwork 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> {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::output_tx_type_checked(
self,
)
}
fn prep_for_submission(&mut self) {
<<Ethereum as Network>::TransactionRequest as TransactionBuilder<Ethereum>>::prep_for_submission(
self,
)
}
fn build_unsigned(
self,
) -> alloy::network::BuildResult<<KitchenSinkNetwork as Network>::UnsignedTx, KitchenSinkNetwork>
{
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> {
request,
error: match error {
TransactionBuilderError::InvalidTransactionRequest(tx_type, items) => {
TransactionBuilderError::InvalidTransactionRequest(tx_type, items)
}
TransactionBuilderError::UnsupportedSignatureType => {
TransactionBuilderError::UnsupportedSignatureType
}
TransactionBuilderError::Signer(error) => {
TransactionBuilderError::Signer(error)
}
TransactionBuilderError::Custom(error) => {
TransactionBuilderError::Custom(error)
}
},
})
}
}
}
async fn build<W: alloy::network::NetworkWallet<KitchenSinkNetwork>>(
self,
wallet: &W,
) -> Result<
<KitchenSinkNetwork as Network>::TxEnvelope,
TransactionBuilderError<KitchenSinkNetwork>,
> {
Ok(wallet.sign_request(self).await?)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KitchenSinkHeader {
/// The Keccak 256-bit hash of the parent
/// blocks header, in its entirety; formally Hp.
pub parent_hash: B256,
/// The Keccak 256-bit hash of the ommers list portion of this block; formally Ho.
#[serde(rename = "sha3Uncles", alias = "ommersHash")]
pub ommers_hash: B256,
/// The 160-bit address to which all fees collected from the successful mining of this block
/// be transferred; formally Hc.
#[serde(rename = "miner", alias = "beneficiary")]
pub beneficiary: Address,
/// The Keccak 256-bit hash of the root node of the state trie, after all transactions are
/// executed and finalisations applied; formally Hr.
pub state_root: B256,
/// The Keccak 256-bit hash of the root node of the trie structure populated with each
/// transaction in the transactions list portion of the block; formally Ht.
pub transactions_root: B256,
/// The Keccak 256-bit hash of the root node of the trie structure populated with the receipts
/// of each transaction in the transactions list portion of the block; formally He.
pub receipts_root: B256,
/// The Bloom filter composed from indexable information (logger address and log topics)
/// contained in each log entry from the receipt of each transaction in the transactions list;
/// formally Hb.
pub logs_bloom: Bloom,
/// A scalar value corresponding to the difficulty level of this block. This can be calculated
/// from the previous blocks difficulty level and the timestamp; formally Hd.
pub difficulty: U256,
/// A scalar value equal to the number of ancestor blocks. The genesis block has a number of
/// zero; formally Hi.
#[serde(with = "alloy::serde::quantity")]
pub number: BlockNumber,
/// A scalar value equal to the current limit of gas expenditure per block; formally Hl.
// This is the main difference over the Ethereum network implementation. We use u128 here and
// not u64.
#[serde(with = "alloy::serde::quantity")]
pub gas_limit: u128,
/// A scalar value equal to the total gas used in transactions in this block; formally Hg.
#[serde(with = "alloy::serde::quantity")]
pub gas_used: u64,
/// A scalar value equal to the reasonable output of Unixs time() at this blocks inception;
/// formally Hs.
#[serde(with = "alloy::serde::quantity")]
pub timestamp: u64,
/// An arbitrary byte array containing data relevant to this block. This must be 32 bytes or
/// fewer; formally Hx.
pub extra_data: Bytes,
/// A 256-bit hash which, combined with the
/// nonce, proves that a sufficient amount of computation has been carried out on this block;
/// formally Hm.
pub mix_hash: B256,
/// A 64-bit value which, combined with the mixhash, proves that a sufficient amount of
/// computation has been carried out on this block; formally Hn.
pub nonce: B64,
/// A scalar representing EIP1559 base fee which can move up or down each block according
/// to a formula which is a function of gas used in parent block and gas target
/// (block gas limit divided by elasticity multiplier) of parent block.
/// The algorithm results in the base fee per gas increasing when blocks are
/// above the gas target, and decreasing when blocks are below the gas target. The base fee per
/// gas is burned.
#[serde(
default,
with = "alloy::serde::quantity::opt",
skip_serializing_if = "Option::is_none"
)]
pub base_fee_per_gas: Option<u64>,
/// The Keccak 256-bit hash of the withdrawals list portion of this block.
/// <https://eips.ethereum.org/EIPS/eip-4895>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub withdrawals_root: Option<B256>,
/// The total amount of blob gas consumed by the transactions within the block, added in
/// EIP-4844.
#[serde(
default,
with = "alloy::serde::quantity::opt",
skip_serializing_if = "Option::is_none"
)]
pub blob_gas_used: Option<u64>,
/// A running total of blob gas consumed in excess of the target, prior to the block. Blocks
/// with above-target blob gas consumption increase this value, blocks with below-target blob
/// gas consumption decrease it (bounded at 0). This was added in EIP-4844.
#[serde(
default,
with = "alloy::serde::quantity::opt",
skip_serializing_if = "Option::is_none"
)]
pub excess_blob_gas: Option<u64>,
/// The hash of the parent beacon block's root is included in execution blocks, as proposed by
/// EIP-4788.
///
/// This enables trust-minimized access to consensus state, supporting staking pools, bridges,
/// and more.
///
/// The beacon roots contract handles root storage, enhancing Ethereum's functionalities.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_beacon_block_root: Option<B256>,
/// The Keccak 256-bit hash of the an RLP encoded list with each
/// [EIP-7685] request in the block body.
///
/// [EIP-7685]: https://eips.ethereum.org/EIPS/eip-7685
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requests_hash: Option<B256>,
}
impl BlockHeader for KitchenSinkHeader {
fn parent_hash(&self) -> B256 {
self.parent_hash
}
fn ommers_hash(&self) -> B256 {
self.ommers_hash
}
fn beneficiary(&self) -> Address {
self.beneficiary
}
fn state_root(&self) -> B256 {
self.state_root
}
fn transactions_root(&self) -> B256 {
self.transactions_root
}
fn receipts_root(&self) -> B256 {
self.receipts_root
}
fn withdrawals_root(&self) -> Option<B256> {
self.withdrawals_root
}
fn logs_bloom(&self) -> Bloom {
self.logs_bloom
}
fn difficulty(&self) -> U256 {
self.difficulty
}
fn number(&self) -> BlockNumber {
self.number
}
// There's sadly nothing that we can do about this. We're required to implement this trait on
// any type that represents a header and the gas limit type used here is a u64.
fn gas_limit(&self) -> u64 {
self.gas_limit.try_into().unwrap_or(u64::MAX)
}
fn gas_used(&self) -> u64 {
self.gas_used
}
fn timestamp(&self) -> u64 {
self.timestamp
}
fn mix_hash(&self) -> Option<B256> {
Some(self.mix_hash)
}
fn nonce(&self) -> Option<B64> {
Some(self.nonce)
}
fn base_fee_per_gas(&self) -> Option<u64> {
self.base_fee_per_gas
}
fn blob_gas_used(&self) -> Option<u64> {
self.blob_gas_used
}
fn excess_blob_gas(&self) -> Option<u64> {
self.excess_blob_gas
}
fn parent_beacon_block_root(&self) -> Option<B256> {
self.parent_beacon_block_root
}
fn requests_hash(&self) -> Option<B256> {
self.requests_hash
}
fn extra_data(&self) -> &Bytes {
&self.extra_data
}
}
#[cfg(test)]
mod tests {
use revive_dt_config::Arguments;
use std::path::PathBuf;
use temp_dir::TempDir;
use std::fs;
use super::KitchensinkNode;
use crate::{GENESIS_JSON, Node};
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.kitchensink = PathBuf::from("substrate-node");
config.eth_proxy = PathBuf::from("eth-rpc");
(config, temp_dir)
}
#[test]
fn test_init_generates_chainspec_with_balances() {
let genesis_content = r#"
{
"alloc": {
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": {
"balance": "1000000000000000000"
},
"Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2": {
"balance": "2000000000000000000"
}
}
}
"#;
let mut dummy_node = KitchensinkNode::new(&test_config().0);
// Call `init()`
dummy_node.init(genesis_content).expect("init failed");
// Check that the patched chainspec file was generated
let final_chainspec_path = dummy_node
.base_directory
.join(KitchensinkNode::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("90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
.unwrap();
let second_eth_addr =
KitchensinkNode::eth_to_substrate_address("Ab8483F64d9C6d1EcF9b849Ae677dD3315835cb2")
.unwrap();
assert!(
contents.contains(&first_eth_addr),
"Chainspec should contain Substrate address for first Ethereum account"
);
assert!(
contents.contains(&second_eth_addr),
"Chainspec should contain Substrate address for second Ethereum account"
);
}
#[test]
fn test_parse_genesis_alloc() {
// Create test genesis file
let genesis_json = r#"
{
"alloc": {
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1": { "balance": "1000000000000000000" },
"0x0000000000000000000000000000000000000000": { "balance": "0xDE0B6B3A7640000" },
"0xffffffffffffffffffffffffffffffffffffffff": { "balance": "123456789" }
}
}
"#;
let node = KitchensinkNode::new(&test_config().0);
let result = node
.extract_balance_from_genesis_file(genesis_json)
.unwrap();
let result_map: std::collections::HashMap<_, _> = result.into_iter().collect();
assert_eq!(
result_map.get("5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV"),
Some(&1_000_000_000_000_000_000u128)
);
assert_eq!(
result_map.get("5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1"),
Some(&1_000_000_000_000_000_000u128)
);
assert_eq!(
result_map.get("5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4"),
Some(&123_456_789u128)
);
}
#[test]
fn print_eth_to_substrate_mappings() {
let eth_addresses = vec![
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
"0xffffffffffffffffffffffffffffffffffffffff",
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
];
for eth_addr in eth_addresses {
let ss58 = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap();
println!("Ethereum: {eth_addr} -> Substrate SS58: {ss58}");
}
}
#[test]
fn test_eth_to_substrate_address() {
let cases = vec![
(
"0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
"5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV",
),
(
"90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
"5FLneRcWAfk3X3tg6PuGyLNGAquPAZez5gpqvyuf3yUK8VaV",
),
(
"0x0000000000000000000000000000000000000000",
"5C4hrfjw9DjXZTzV3MwzrrAr9P1MLDHajjSidz9bR544LEq1",
),
(
"0xffffffffffffffffffffffffffffffffffffffff",
"5HrN7fHLXWcFiXPwwtq2EkSGns9eMmoUQnbVKweNz3VVr6N4",
),
];
for (eth_addr, expected_ss58) in cases {
let result = KitchensinkNode::eth_to_substrate_address(eth_addr).unwrap();
assert_eq!(
result, expected_ss58,
"Mismatch for Ethereum address {eth_addr}"
);
}
}
#[test]
fn spawn_works() {
let (config, _temp_dir) = test_config();
let mut node = KitchensinkNode::new(&config);
node.spawn(GENESIS_JSON.to_string()).unwrap();
}
#[test]
fn version_works() {
let (config, _temp_dir) = test_config();
let node = KitchensinkNode::new(&config);
let version = node.version().unwrap();
assert!(
version.starts_with("substrate-node"),
"Expected substrate-node version string, got: {version}"
);
}
#[test]
fn eth_rpc_version_works() {
let (config, _temp_dir) = test_config();
let node = KitchensinkNode::new(&config);
let version = node.eth_rpc_version().unwrap();
assert!(
version.starts_with("pallet-revive-eth-rpc"),
"Expected eth-rpc version string, got: {version}"
);
}
}