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(())
+}