diff --git a/Cargo.lock b/Cargo.lock index ede15b26..efea7cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1320,6 +1320,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "tempfile", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 45c2d1d1..6c5fdb04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ ethers-contract = { version = "^1.0.0", default-features = false, path = "./ethe ethers-providers = { version = "^1.0.0", default-features = false, path = "./ethers-providers", features = [ "ws", ] } +tempfile = "3.3.0" [target.'cfg(target_family = "unix")'.dev-dependencies] ethers-providers = { version = "^1.0.0", default-features = false, path = "./ethers-providers", features = [ diff --git a/ethers-core/src/utils/genesis.rs b/ethers-core/src/utils/genesis.rs index 77d072ee..a19f8354 100644 --- a/ethers-core/src/utils/genesis.rs +++ b/ethers-core/src/utils/genesis.rs @@ -40,6 +40,63 @@ pub struct Genesis { pub alloc: HashMap, } +impl Genesis { + /// Creates a chain config using the given chain id. + /// and funds the given address with max coins. + /// + /// Enables all hard forks up to London at genesis. + pub fn new(chain_id: u64, signer_addr: Address) -> Genesis { + // set up a clique config with an instant sealing period and short (8 block) epoch + let clique_config = CliqueConfig { period: 0, epoch: 8 }; + + let config = ChainConfig { + chain_id, + eip155_block: Some(0), + eip150_block: Some(0), + eip158_block: Some(0), + + homestead_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + muir_glacier_block: Some(0), + berlin_block: Some(0), + london_block: Some(0), + clique: Some(clique_config), + ..Default::default() + }; + + // fund account + let mut alloc = HashMap::new(); + alloc.insert( + signer_addr, + GenesisAccount { balance: U256::MAX, nonce: None, code: None, storage: None }, + ); + + // put signer address in the extra data, padded by the required amount of zeros + // Clique issue: https://github.com/ethereum/EIPs/issues/225 + // Clique EIP: https://eips.ethereum.org/EIPS/eip-225 + // + // The first 32 bytes are vanity data, so we will populate it with zeros + // This is followed by the signer address, which is 20 bytes + // There are 65 bytes of zeros after the signer address, which is usually populated with the + // proposer signature. Because the genesis does not have a proposer signature, it will be + // populated with zeros. + let extra_data_bytes = [&[0u8; 32][..], signer_addr.as_bytes(), &[0u8; 65][..]].concat(); + let extra_data = Bytes::from(extra_data_bytes); + + Genesis { + config, + alloc, + difficulty: U256::one(), + gas_limit: U64::from(5000000), + extra_data, + ..Default::default() + } + } +} + /// An account in the state of the genesis block. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GenesisAccount { diff --git a/ethers-core/src/utils/geth.rs b/ethers-core/src/utils/geth.rs index 4e8f72c5..25d7a553 100644 --- a/ethers-core/src/utils/geth.rs +++ b/ethers-core/src/utils/geth.rs @@ -1,5 +1,10 @@ +use k256::ecdsa::SigningKey; + use super::{unused_port, Genesis}; -use crate::types::H256; +use crate::{ + types::{Bytes, H256}, + utils::secret_key_to_address, +}; use std::{ env::temp_dir, fs::{create_dir, File}, @@ -181,8 +186,9 @@ pub struct Geth { data_dir: Option, chain_id: Option, insecure_unlock: bool, - genesis: Option, + pub genesis: Option, mode: GethMode, + pub clique_private_key: Option, } impl Geth { @@ -208,6 +214,11 @@ impl Geth { Self::new().path(path) } + /// Returns whether the node is launched in Clique consensus mode + pub fn is_clique(&self) -> bool { + self.clique_private_key.is_some() + } + /// Sets the `path` to the `geth` executable /// /// By default, it's expected that `geth` is in `$PATH`, see also @@ -218,6 +229,14 @@ impl Geth { self } + /// Sets the Clique Private Key to the `geth` executable, which will be later + /// loaded on the node. + #[must_use] + pub fn set_clique_private_key>(mut self, private_key: T) -> Self { + self.clique_private_key = Some(private_key.into()); + self + } + /// Sets the port which will be used when the `geth-cli` instance is launched. #[must_use] pub fn port>(mut self, port: T) -> Self { @@ -273,6 +292,11 @@ impl Geth { /// options. #[must_use] pub fn disable_discovery(mut self) -> Self { + self.inner_disable_discovery(); + self + } + + fn inner_disable_discovery(&mut self) { match self.mode { GethMode::Dev(_) => { self.mode = @@ -280,7 +304,6 @@ impl Geth { } GethMode::NonDev(ref mut opts) => opts.discovery = false, } - self } /// Manually sets the IPC path for the socket manually. @@ -318,7 +341,7 @@ impl Geth { /// Consumes the builder and spawns `geth` with stdout redirected /// to /dev/null. - pub fn spawn(self) -> GethInstance { + pub fn spawn(mut self) -> GethInstance { let mut cmd = if let Some(ref prg) = self.program { Command::new(prg) } else { Command::new(GETH) }; // geth uses stderr for its logs @@ -337,15 +360,47 @@ impl Geth { cmd.arg("--ws.api").arg(API); // pass insecure unlock flag if set - if self.insecure_unlock { + let is_clique = self.is_clique(); + if self.insecure_unlock || is_clique { cmd.arg("--allow-insecure-unlock"); } + if is_clique { + self.inner_disable_discovery(); + } + // Set the port for authenticated APIs cmd.arg("--authrpc.port").arg(authrpc_port.to_string()); // use geth init to initialize the datadir if the genesis exists - if let Some(genesis) = self.genesis { + if let Some(ref mut genesis) = self.genesis { + if is_clique { + use super::CliqueConfig; + // set up a clique config with an instant sealing period and short (8 block) epoch + let clique_config = CliqueConfig { period: 0, epoch: 8 }; + genesis.config.clique = Some(clique_config); + + // set the extraData field + let extra_data_bytes = [ + &[0u8; 32][..], + secret_key_to_address( + self.clique_private_key.as_ref().expect("is_clique == true"), + ) + .as_ref(), + &[0u8; 65][..], + ] + .concat(); + let extra_data = Bytes::from(extra_data_bytes); + genesis.extra_data = extra_data; + } + } else if is_clique { + self.genesis = Some(Genesis::new( + self.chain_id.expect("chain id must be set in clique mode"), + secret_key_to_address(self.clique_private_key.as_ref().expect("is_clique == true")), + )); + } + + if let Some(ref genesis) = self.genesis { // create a temp dir to store the genesis file let temp_genesis_path = temp_dir().join("genesis.json"); diff --git a/examples/geth_clique.rs b/examples/geth_clique.rs new file mode 100644 index 00000000..4df83b73 --- /dev/null +++ b/examples/geth_clique.rs @@ -0,0 +1,30 @@ +use ethers::{ + core::{rand::thread_rng, utils::Geth}, + signers::LocalWallet, +}; +use eyre::Result; + +#[tokio::main] +/// Shows how to instantiate a Geth with Clique enabled. +async fn main() -> Result<()> { + // Generate a random clique signer and set it on Geth. + let data_dir = tempfile::tempdir().expect("should be able to create temp geth datadir"); + let dir_path = data_dir.into_path(); + println!("Using {}", dir_path.display()); + + // Create a random signer + let key = LocalWallet::new(&mut thread_rng()); + + let clique_key = key.signer().clone(); + let _geth = Geth::new() + // set the signer + .set_clique_private_key(clique_key) + // must always set the chain id here + .chain_id(199u64) + // set the datadir to a temp dir + .data_dir(dir_path) + // spawn it + .spawn(); + + Ok(()) +}