From a1a2c8e042ef5004cd472abfe028aaaf6ad96580 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 13:08:09 -0300 Subject: [PATCH 01/45] Initial `dkg/disk` module - Add `load_definition` --- Cargo.lock | 8 +++ crates/cluster/src/definition.rs | 8 ++- crates/dkg/Cargo.toml | 8 +++ crates/dkg/src/disk.rs | 110 +++++++++++++++++++++++++++++++ crates/dkg/src/dkg.rs | 15 +++++ crates/dkg/src/lib.rs | 6 ++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 crates/dkg/src/disk.rs create mode 100644 crates/dkg/src/dkg.rs diff --git a/Cargo.lock b/Cargo.lock index 4a3dbdf3..915c2973 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5525,8 +5525,16 @@ name = "pluto-dkg" version = "1.7.1" dependencies = [ "pluto-build-proto", + "pluto-cluster", + "pluto-eth1wrap", + "pluto-eth2util", "prost 0.14.3", "prost-types 0.14.3", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", ] [[package]] diff --git a/crates/cluster/src/definition.rs b/crates/cluster/src/definition.rs index 8e5cf2de..34620c33 100644 --- a/crates/cluster/src/definition.rs +++ b/crates/cluster/src/definition.rs @@ -441,7 +441,9 @@ impl Definition { return Err(InvalidGasLimitError::GasLimitNotSet.into()); } - def.set_definition_hashes() + def.set_definition_hashes()?; + + Ok(def) } /// Returns the timestamp of the definition. @@ -664,7 +666,7 @@ impl Definition { } /// Sets the definition hashes. - pub fn set_definition_hashes(mut self) -> Result { + pub fn set_definition_hashes(&mut self) -> Result<(), DefinitionError> { let config_hash = hash_definition(&self, true).map_err(|e| DefinitionError::SSZError(Box::new(e)))?; @@ -675,7 +677,7 @@ impl Definition { self.definition_hash = definition_hash.to_vec(); - Ok(self) + Ok(()) } /// `verify_hashes` returns an error if hashes populated from json object diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index ef9f1053..d8293a7d 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -9,6 +9,14 @@ publish.workspace = true [dependencies] prost.workspace = true prost-types.workspace = true +pluto-cluster.workspace = true +pluto-eth1wrap.workspace = true +pluto-eth2util.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +url.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs new file mode 100644 index 00000000..331c46c0 --- /dev/null +++ b/crates/dkg/src/disk.rs @@ -0,0 +1,110 @@ +use crate::dkg; +use tracing::{info, warn}; + +/// Error type for DKG disk operations. +#[derive(Debug, thiserror::Error)] +pub(crate) enum DiskError { + /// Invalid URL. + #[error("Invalid URL: {0}")] + InvalidUrl(#[from] url::ParseError), + + /// I/O error. + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + /// JSON parsing error. + #[error("JSON parsing error: {0}")] + JsonError(#[from] serde_json::Error), + + /// Cluster definition verification error. + #[error( + "Cluster definition verification failed. Run with `--no-verify` to bypass verification at own risk: {0}" + )] + ClusterDefinitionVerificationError(pluto_cluster::definition::DefinitionError), + + /// Cluster definition error. + #[error("Cluster definition error: {0}")] + ClusterDefinitionError(#[from] pluto_cluster::definition::DefinitionError), + + /// Deposit amounts verification error. + #[error("Deposit amounts verification failed: {0}")] + DepositAmountsVerificationError(#[from] pluto_eth2util::deposit::DepositError), +} + +type Result = std::result::Result; + +/// Returns the [`pluto_cluster::definition::Definition`] from disk or an HTTP +/// URL. It returns the test definition if configured. +pub(crate) async fn load_definition( + conf: &dkg::Config, + eth1cl: Option<&pluto_eth1wrap::EthClient>, +) -> Result { + if let Some(definition) = &conf.test_config.def { + return Ok(definition.clone()); + } + + // Fetch definition from URI or disk + + let parsed_url = url::Url::parse(&conf.def_file)?; + let mut def = if parsed_url.has_host() { + if parsed_url.scheme() != "https" { + warn!( + addr = conf.def_file, + "Definition file URL does not use https protocol" + ); + } + + let def: pluto_cluster::definition::Definition = todo!(); + let definition_hash = pluto_cluster::helpers::to_0x_hex(&def.definition_hash); + + info!( + url = conf.def_file, + definition_hash, "Cluster definition downloaded from URL" + ); + + def + } else { + let buf = tokio::fs::read_to_string(&conf.def_file).await?; + + let def: pluto_cluster::definition::Definition = serde_json::from_str(&buf)?; + let definition_hash = pluto_cluster::helpers::to_0x_hex(&def.definition_hash); + + info!( + path = conf.def_file, + definition_hash, "Cluster definition loaded from disk" + ); + + def + }; + + // Verify + if let Err(error) = def.verify_hashes() { + if conf.no_verify { + warn!( + error = %error, + "Ignoring failed cluster definition hashes verification due to --no-verify flag" + ); + } else { + return Err(DiskError::ClusterDefinitionVerificationError(error)); + } + } + if let Err(error) = def.verify_signatures(eth1cl).await { + if conf.no_verify { + warn!( + error = %error, + "Ignoring failed cluster definition signatures verification due to --no-verify flag" + ); + } else { + return Err(DiskError::ClusterDefinitionVerificationError(error)); + } + } + + // Ensure we have a definition hash in case of no-verify. + if def.definition_hash.is_empty() { + def.set_definition_hashes()?; + } + + pluto_eth2util::deposit::verify_deposit_amounts(&def.deposit_amounts, def.compounding)?; + + Ok(def) +} diff --git a/crates/dkg/src/dkg.rs b/crates/dkg/src/dkg.rs new file mode 100644 index 00000000..509b4c77 --- /dev/null +++ b/crates/dkg/src/dkg.rs @@ -0,0 +1,15 @@ +/// DKG configuration +pub struct Config { + /// Path to the definition file. Can be an URL or an absolute path on disk. + pub def_file: String, + /// Skip cluster definition verification. + pub no_verify: bool, + /// Test configuration, used for testing purposes. + pub test_config: TestConfig, +} + +/// Additional test-only config for DKG. +pub struct TestConfig { + /// Provides the cluster definition explicitly, skips loading from disk. + pub def: Option, +} diff --git a/crates/dkg/src/lib.rs b/crates/dkg/src/lib.rs index 097bec20..8b9c00ee 100644 --- a/crates/dkg/src/lib.rs +++ b/crates/dkg/src/lib.rs @@ -7,3 +7,9 @@ /// Protobuf definitions. pub mod dkgpb; + +/// General DKG IO operations. +pub mod disk; + +/// TODO +pub mod dkg; From 0a9c61c38f09d4850dc887aca26b0ddeb2d70565 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 13:24:36 -0300 Subject: [PATCH 02/45] Add `write_to_keymanager` - Requires `shares` --- Cargo.lock | 3 +++ crates/dkg/Cargo.toml | 3 +++ crates/dkg/src/disk.rs | 46 ++++++++++++++++++++++++++++++++++++++++- crates/dkg/src/lib.rs | 3 +++ crates/dkg/src/share.rs | 10 +++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 crates/dkg/src/share.rs diff --git a/Cargo.lock b/Cargo.lock index 915c2973..31b61355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5524,12 +5524,15 @@ dependencies = [ name = "pluto-dkg" version = "1.7.1" dependencies = [ + "hex", "pluto-build-proto", "pluto-cluster", + "pluto-crypto", "pluto-eth1wrap", "pluto-eth2util", "prost 0.14.3", "prost-types 0.14.3", + "rand 0.8.5", "serde_json", "thiserror 2.0.18", "tokio", diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index d8293a7d..f35712e0 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -10,8 +10,11 @@ publish.workspace = true prost.workspace = true prost-types.workspace = true pluto-cluster.workspace = true +pluto-crypto.workspace = true pluto-eth1wrap.workspace = true pluto-eth2util.workspace = true +hex.workspace = true +rand.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 331c46c0..44d877de 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -1,4 +1,5 @@ -use crate::dkg; +use crate::{dkg, share}; +use rand::RngCore; use tracing::{info, warn}; /// Error type for DKG disk operations. @@ -29,6 +30,12 @@ pub(crate) enum DiskError { /// Deposit amounts verification error. #[error("Deposit amounts verification failed: {0}")] DepositAmountsVerificationError(#[from] pluto_eth2util::deposit::DepositError), + + #[error("Keystore error: {0}")] + KeystoreError(#[from] pluto_eth2util::keystore::KeystoreError), + + #[error("Keymanager error: {0}")] + KeymanagerClientError(#[from] pluto_eth2util::keymanager::KeymanagerError), } type Result = std::result::Result; @@ -108,3 +115,40 @@ pub(crate) async fn load_definition( Ok(def) } + +/// Writes validator private keyshares for the node to the provided keymanager +/// address. +pub(crate) async fn write_to_keymanager( + keymanager_url: impl AsRef, + auth_token: impl AsRef, + shares: &[share::Share], +) -> Result<()> { + let mut keystores = Vec::new(); + let mut passwords = Vec::new(); + + for share in shares { + let password = random_hex64(); + let store = pluto_eth2util::keystore::encrypt( + &share.secret_share, + &password, + None, // TODO: What to use here as argument? + &mut rand::rngs::OsRng, + )?; + + passwords.push(password); + keystores.push(store); + } + + let cl = pluto_eth2util::keymanager::Client::new(keymanager_url, auth_token)?; + cl.import_keystores(&keystores, &passwords).await?; + + Ok(()) +} + +pub(crate) fn random_hex64() -> String { + let mut rng = rand::rngs::OsRng; + + let mut bytes = [0u8; 32]; + rng.fill_bytes(&mut bytes); + hex::encode(bytes) +} diff --git a/crates/dkg/src/lib.rs b/crates/dkg/src/lib.rs index 8b9c00ee..2cfd6192 100644 --- a/crates/dkg/src/lib.rs +++ b/crates/dkg/src/lib.rs @@ -13,3 +13,6 @@ pub mod disk; /// TODO pub mod dkg; + +/// TODO +pub mod share; diff --git a/crates/dkg/src/share.rs b/crates/dkg/src/share.rs new file mode 100644 index 00000000..db1ccbc2 --- /dev/null +++ b/crates/dkg/src/share.rs @@ -0,0 +1,10 @@ +use std::collections::HashMap; + +/// The co-validator public key, tbls public shares, and private key share. +/// Each node in the cluster will receive one for each distributed validator. +pub(crate) struct Share { + pub pub_key: pluto_crypto::types::PublicKey, + pub secret_share: pluto_crypto::types::PrivateKey, + + pub public_shares: HashMap, // u64 == Share index +} From 8bcf24c5877595863d48396fbf8bc6af12d10758 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 13:49:43 -0300 Subject: [PATCH 03/45] Add `write_keys_to_disk` - Adjust some pending TODOs. --- crates/dkg/src/disk.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 44d877de..7c8815fc 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -61,7 +61,8 @@ pub(crate) async fn load_definition( ); } - let def: pluto_cluster::definition::Definition = todo!(); + let def: pluto_cluster::definition::Definition = + todo!("requires `cluster.FetchDefinition`"); let definition_hash = pluto_cluster::helpers::to_0x_hex(&def.definition_hash); info!( @@ -145,6 +146,29 @@ pub(crate) async fn write_to_keymanager( Ok(()) } +pub(crate) async fn write_keys_to_disk( + conf: &dkg::Config, + shares: &[share::Share], + insecure: bool, +) -> Result<()> { + let secret_shares = shares.iter().map(|s| s.secret_share).collect::>(); + + let keys_dir: String = todo!("requires `cluster.CreateValidatorKeysDir`"); + + if insecure { + pluto_eth2util::keystore::store_keys_insecure( + &secret_shares, + keys_dir, + &pluto_eth2util::keystore::CONFIRM_INSECURE_KEYS, + ) + .await?; + } else { + pluto_eth2util::keystore::store_keys(&secret_shares, keys_dir).await?; + } + + Ok(()) +} + pub(crate) fn random_hex64() -> String { let mut rng = rand::rngs::OsRng; From 211fcb404791b6a760a9f813911e15ec4a719b2e Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 14:03:46 -0300 Subject: [PATCH 04/45] Add `write_lock` --- Cargo.lock | 1 + crates/dkg/Cargo.toml | 1 + crates/dkg/src/disk.rs | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 31b61355..090c3451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5533,6 +5533,7 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "rand 0.8.5", + "serde", "serde_json", "thiserror 2.0.18", "tokio", diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index f35712e0..33de8647 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -15,6 +15,7 @@ pluto-eth1wrap.workspace = true pluto-eth2util.workspace = true hex.workspace = true rand.workspace = true +serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 7c8815fc..79c909cc 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -169,6 +169,31 @@ pub(crate) async fn write_keys_to_disk( Ok(()) } +pub(crate) async fn write_lock( + data_dir: impl AsRef, + lock: &pluto_cluster::lock::Lock, +) -> Result<()> { + use serde::Serialize; + use tokio::io::AsyncWriteExt; + + let b = { + let mut buf = Vec::new(); + let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); + let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter); + + lock.serialize(&mut ser)?; + buf + }; + + let path = std::path::Path::new(data_dir.as_ref()).join("cluster-lock.json"); + + let mut file = tokio::fs::File::open(path).await?; + file.write_all(&b).await?; + file.metadata().await?.permissions().set_readonly(true); // File needs to be read-only for everybody + + Ok(()) +} + pub(crate) fn random_hex64() -> String { let mut rng = rand::rngs::OsRng; From 123947a183271752fa70dd848ed501c243b75bcc Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 16:38:03 -0300 Subject: [PATCH 05/45] Add `check_clear_data_dir` --- crates/dkg/src/disk.rs | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 79c909cc..c64c541a 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -1,3 +1,8 @@ +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; + use crate::{dkg, share}; use rand::RngCore; use tracing::{info, warn}; @@ -36,6 +41,28 @@ pub(crate) enum DiskError { #[error("Keymanager error: {0}")] KeymanagerClientError(#[from] pluto_eth2util::keymanager::KeymanagerError), + + /// Data directory does not exist. + #[error("data directory doesn't exist, cannot continue")] + DataDirNotFound(PathBuf), + + /// Data directory path points to a file, not a directory. + #[error("data directory already exists and is a file, cannot continue")] + DataDirIsFile(PathBuf), + + /// Data directory contains disallowed entries. + #[error("data directory not clean, cannot continue")] + DataDirNotClean { + disallowed_entity: String, + data_dir: PathBuf, + }, + + /// Data directory is missing required files. + #[error("missing required files, cannot continue")] + MissingRequiredFiles { + file_name: String, + data_dir: PathBuf, + }, } type Result = std::result::Result; @@ -194,6 +221,58 @@ pub(crate) async fn write_lock( Ok(()) } +/// Ensures `data_dir` exists, is a directory, and does not contain any +/// disallowed entries, while checking for the presence of necessary files. +pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<()> { + let path = std::path::PathBuf::from(data_dir.as_ref()); + + match tokio::fs::metadata(&path).await { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Err(DiskError::DataDirNotFound(path)); + } + Err(e) => { + return Err(DiskError::IoError(e)); + } + Ok(meta) if !meta.is_dir() => { + return Err(DiskError::DataDirIsFile(path)); + } + Ok(_) => {} + } + + let disallowed = HashSet::from(["validator_keys", "cluster-lock.json"]); + let mut necessary = HashMap::from([("cluster-lock.json", false)]); + + let mut read_dir = tokio::fs::read_dir(&path).await?; + while let Some(entry) = read_dir.next_entry().await? { + let os_string = entry.file_name(); + let name = os_string.to_string_lossy(); + + let is_deposit_data = name.starts_with("deposit-data"); + + if disallowed.contains(name.as_ref()) || is_deposit_data { + return Err(DiskError::DataDirNotClean { + disallowed_entity: name.into(), + data_dir: path, + }); + } + + if let Some(found) = necessary.get_mut(name.as_ref()) { + *found = true; + } + } + + for (file_name, found) in &necessary { + if !found { + return Err(DiskError::MissingRequiredFiles { + file_name: file_name.to_string(), + data_dir: path, + }); + } + } + + Ok(()) +} + pub(crate) fn random_hex64() -> String { let mut rng = rand::rngs::OsRng; From c1caa5d4e654efd4056c7324b5dda9b693400f60 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:09:12 -0300 Subject: [PATCH 06/45] Add `check_writes` --- crates/dkg/src/disk.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index c64c541a..4e41b1ed 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -273,6 +273,42 @@ pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<() Ok(()) } +/// Writes sample files to check disk writes and removes sample files after +/// verification. +pub(crate) async fn check_writes(data_dir: impl AsRef) -> Result<()> { + const CHECK_BODY: &str = "delete me: dummy file used to check write permissions"; + + let base = std::path::Path::new(data_dir.as_ref()); + + for file in [ + "cluster-lock.json", + "deposit-data.json", + "validator_keys/keystore-0.json", + ] { + let file_path = std::path::Path::new(file); + let subdir = file_path.parent().filter(|p| !p.as_os_str().is_empty()); + + if let Some(subdir) = subdir { + tokio::fs::create_dir_all(base.join(subdir)).await?; + } + + let full_path = base.join(file_path); + tokio::fs::write(&full_path, CHECK_BODY).await?; + + let mut perms = tokio::fs::metadata(&full_path).await?.permissions(); + perms.set_readonly(true); + tokio::fs::set_permissions(&full_path, perms).await?; + + tokio::fs::remove_file(&full_path).await?; + + if let Some(subdir) = subdir { + tokio::fs::remove_dir_all(base.join(subdir)).await?; + } + } + + Ok(()) +} + pub(crate) fn random_hex64() -> String { let mut rng = rand::rngs::OsRng; From 450cf6ef9931fd812e0d9051eab45e0519911ccd Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:13:34 -0300 Subject: [PATCH 07/45] Test `clear_data_dir_does_not_exist` --- Cargo.lock | 1 + crates/dkg/Cargo.toml | 3 +++ crates/dkg/src/disk.rs | 25 +++++++++++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 090c3451..93d53963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5535,6 +5535,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", + "tempfile", "thiserror 2.0.18", "tokio", "tracing", diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index 33de8647..9c6e799c 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -25,5 +25,8 @@ url.workspace = true [build-dependencies] pluto-build-proto.workspace = true +[dev-dependencies] +tempfile.workspace = true + [lints] workspace = true diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 4e41b1ed..1cb89ba8 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -1,6 +1,6 @@ use std::{ collections::{HashMap, HashSet}, - path::PathBuf, + path::{self, PathBuf}, }; use crate::{dkg, share}; @@ -212,7 +212,7 @@ pub(crate) async fn write_lock( buf }; - let path = std::path::Path::new(data_dir.as_ref()).join("cluster-lock.json"); + let path = path::Path::new(data_dir.as_ref()).join("cluster-lock.json"); let mut file = tokio::fs::File::open(path).await?; file.write_all(&b).await?; @@ -223,8 +223,8 @@ pub(crate) async fn write_lock( /// Ensures `data_dir` exists, is a directory, and does not contain any /// disallowed entries, while checking for the presence of necessary files. -pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<()> { - let path = std::path::PathBuf::from(data_dir.as_ref()); +pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<()> { + let path = path::PathBuf::from(data_dir.as_ref()); match tokio::fs::metadata(&path).await { Err(e) if e.kind() == std::io::ErrorKind::NotFound => { @@ -278,14 +278,14 @@ pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<() pub(crate) async fn check_writes(data_dir: impl AsRef) -> Result<()> { const CHECK_BODY: &str = "delete me: dummy file used to check write permissions"; - let base = std::path::Path::new(data_dir.as_ref()); + let base = path::Path::new(data_dir.as_ref()); for file in [ "cluster-lock.json", "deposit-data.json", "validator_keys/keystore-0.json", ] { - let file_path = std::path::Path::new(file); + let file_path = path::Path::new(file); let subdir = file_path.parent().filter(|p| !p.as_os_str().is_empty()); if let Some(subdir) = subdir { @@ -316,3 +316,16 @@ pub(crate) fn random_hex64() -> String { rng.fill_bytes(&mut bytes); hex::encode(bytes) } + +#[cfg(test)] +mod tests { + + #[tokio::test] + async fn clear_data_dir_does_not_exist() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path().join("nonexistent"); + + let result = super::check_clear_data_dir(&data_dir).await; + assert!(matches!(result, Err(super::DiskError::DataDirNotFound(_)))); + } +} From b6931b77a401a2576f0ec048d4ce7e6c6985abc4 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:16:13 -0300 Subject: [PATCH 08/45] Test `clear_data_dir_is_file` --- crates/dkg/src/disk.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 1cb89ba8..6869bcd3 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -319,7 +319,6 @@ pub(crate) fn random_hex64() -> String { #[cfg(test)] mod tests { - #[tokio::test] async fn clear_data_dir_does_not_exist() { let temp_dir = tempfile::tempdir().unwrap(); @@ -328,4 +327,15 @@ mod tests { let result = super::check_clear_data_dir(&data_dir).await; assert!(matches!(result, Err(super::DiskError::DataDirNotFound(_)))); } + + #[tokio::test] + async fn clear_data_dir_is_file() { + let temp_file = tempfile::NamedTempFile::new().unwrap(); + tokio::fs::write(temp_file.path(), [0x0, 0x1, 0x2]) + .await + .unwrap(); + + let result = super::check_clear_data_dir(temp_file.path()).await; + assert!(matches!(result, Err(super::DiskError::DataDirIsFile(_)))); + } } From 9691bf315bc5303dc09d3fb5bcb4a93c4df38016 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:28:46 -0300 Subject: [PATCH 09/45] Test `clear_data_dir_contains_validator_keys_file` --- crates/dkg/src/disk.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 6869bcd3..95192b9a 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -338,4 +338,19 @@ mod tests { let result = super::check_clear_data_dir(temp_file.path()).await; assert!(matches!(result, Err(super::DiskError::DataDirIsFile(_)))); } + + #[tokio::test] + async fn clear_data_dir_contains_validator_keys_file() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + tokio::fs::write(data_dir.join("validator_keys"), [0x0, 0x1, 0x2]) + .await + .unwrap(); + + let result = super::check_clear_data_dir(data_dir).await; + assert!(matches!( + result, + Err(super::DiskError::DataDirNotClean { .. }) + )); + } } From f0c9bf6ef1dc24bf9e6810efcafccd98ba9b6290 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:29:35 -0300 Subject: [PATCH 10/45] Test `clear_data_dir_contains_validator_keys_dir` --- crates/dkg/src/disk.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 95192b9a..70fd172b 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -353,4 +353,5 @@ mod tests { Err(super::DiskError::DataDirNotClean { .. }) )); } + async fn clear_data_dir_contains_validator_keys_dir() { } From f4b835004e672e4b12ed207a84184f6d2acbab6d Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:29:55 -0300 Subject: [PATCH 11/45] Test `clear_data_dir_contains_validator_keys_dir` --- crates/dkg/src/disk.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 70fd172b..5c5045fe 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -353,5 +353,19 @@ mod tests { Err(super::DiskError::DataDirNotClean { .. }) )); } + + #[tokio::test] async fn clear_data_dir_contains_validator_keys_dir() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + tokio::fs::create_dir_all(data_dir.join("validator_keys")) + .await + .unwrap(); + + let result = super::check_clear_data_dir(data_dir).await; + assert!(matches!( + result, + Err(super::DiskError::DataDirNotClean { .. }) + )); + } } From 8b3016a3f1ad4f20dfa84c2c218426259b0d2efb Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:30:48 -0300 Subject: [PATCH 12/45] Test `clear_data_dir_contains_cluster_lock` --- crates/dkg/src/disk.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 5c5045fe..49eb64b9 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -368,4 +368,19 @@ mod tests { Err(super::DiskError::DataDirNotClean { .. }) )); } + + #[tokio::test] + async fn clear_data_dir_contains_cluster_lock() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + tokio::fs::write(data_dir.join("cluster-lock.json"), [0x0, 0x1, 0x2]) + .await + .unwrap(); + + let result = super::check_clear_data_dir(data_dir).await; + assert!(matches!( + result, + Err(super::DiskError::DataDirNotClean { .. }) + )); + } } From a67baeca88febae7812a6c78cbd214bb82d4a0bd Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:31:44 -0300 Subject: [PATCH 13/45] Test `clear_data_dir_contains_deposit_data` --- crates/dkg/src/disk.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 49eb64b9..60538244 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -383,4 +383,19 @@ mod tests { Err(super::DiskError::DataDirNotClean { .. }) )); } + + #[tokio::test] + async fn clear_data_dir_contains_deposit_data() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + tokio::fs::write(data_dir.join("deposit-data-32eth.json"), [0x0, 0x1, 0x2]) + .await + .unwrap(); + + let result = super::check_clear_data_dir(data_dir).await; + assert!(matches!( + result, + Err(super::DiskError::DataDirNotClean { .. }) + )); + } } From d9c8e0d95f8938e821da5023ef9ad2159ff7e335 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:37:04 -0300 Subject: [PATCH 14/45] Test `clear_data_dir_missing_private_key` --- crates/dkg/src/disk.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 60538244..497370a0 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -398,4 +398,16 @@ mod tests { Err(super::DiskError::DataDirNotClean { .. }) )); } + + #[tokio::test] + async fn clear_data_dir_missing_private_key() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + + let result = super::check_clear_data_dir(data_dir).await; + assert!(matches!( + result, + Err(super::DiskError::MissingRequiredFiles { .. }) + )); + } } From 75600c782a4aea53da49dbfabe43ddd1c48318a1 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:39:18 -0300 Subject: [PATCH 15/45] Test `clear_data_dir_contains_private_key` - Fix required file name --- crates/dkg/src/disk.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 497370a0..c895566d 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -240,7 +240,7 @@ pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Re } let disallowed = HashSet::from(["validator_keys", "cluster-lock.json"]); - let mut necessary = HashMap::from([("cluster-lock.json", false)]); + let mut necessary = HashMap::from([("charon-enr-private-key", false)]); let mut read_dir = tokio::fs::read_dir(&path).await?; while let Some(entry) = read_dir.next_entry().await? { @@ -410,4 +410,16 @@ mod tests { Err(super::DiskError::MissingRequiredFiles { .. }) )); } + + #[tokio::test] + async fn clear_data_dir_contains_private_key() { + let temp_dir = tempfile::tempdir().unwrap(); + let data_dir = temp_dir.path(); + tokio::fs::write(data_dir.join("charon-enr-private-key"), [0x0, 0x1, 0x2]) + .await + .unwrap(); + + let result = super::check_clear_data_dir(data_dir).await; + assert!(result.is_ok()); + } } From da2c43ebcdd0d7d53ef735a1a966249efba526be Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:43:47 -0300 Subject: [PATCH 16/45] Formatting --- crates/dkg/src/disk.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index c895566d..2728caee 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -1,10 +1,9 @@ +use crate::{dkg, share}; +use rand::RngCore; use std::{ collections::{HashMap, HashSet}, path::{self, PathBuf}, }; - -use crate::{dkg, share}; -use rand::RngCore; use tracing::{info, warn}; /// Error type for DKG disk operations. From f5cdb1c740620dd584ae4817347fd2a80652beee Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:44:14 -0300 Subject: [PATCH 17/45] Fix clippy lints --- crates/cluster/src/definition.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cluster/src/definition.rs b/crates/cluster/src/definition.rs index 34620c33..ceff7a7e 100644 --- a/crates/cluster/src/definition.rs +++ b/crates/cluster/src/definition.rs @@ -668,12 +668,12 @@ impl Definition { /// Sets the definition hashes. pub fn set_definition_hashes(&mut self) -> Result<(), DefinitionError> { let config_hash = - hash_definition(&self, true).map_err(|e| DefinitionError::SSZError(Box::new(e)))?; + hash_definition(self, true).map_err(|e| DefinitionError::SSZError(Box::new(e)))?; self.config_hash = config_hash.to_vec(); let definition_hash = - hash_definition(&self, false).map_err(|e| DefinitionError::SSZError(Box::new(e)))?; + hash_definition(self, false).map_err(|e| DefinitionError::SSZError(Box::new(e)))?; self.definition_hash = definition_hash.to_vec(); From 269888a83521dade6e0461682c6a926c1f1c8595 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:51:58 -0300 Subject: [PATCH 18/45] Test `load_definition_file_does_not_exist` --- crates/dkg/src/disk.rs | 14 ++++++++++++++ crates/dkg/src/dkg.rs | 2 ++ 2 files changed, 16 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 2728caee..2ba311a6 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -318,6 +318,20 @@ pub(crate) fn random_hex64() -> String { #[cfg(test)] mod tests { + use crate::dkg; + + #[tokio::test] + async fn load_definition_file_does_not_exist() { + let cfg = dkg::Config { + def_file: "".into(), + no_verify: false, + ..Default::default() + }; + let result = super::load_definition(&cfg, None).await; + dbg!(&result); + assert!(matches!(result, Err(super::DiskError::InvalidUrl(_)))); + } + #[tokio::test] async fn clear_data_dir_does_not_exist() { let temp_dir = tempfile::tempdir().unwrap(); diff --git a/crates/dkg/src/dkg.rs b/crates/dkg/src/dkg.rs index 509b4c77..35978a19 100644 --- a/crates/dkg/src/dkg.rs +++ b/crates/dkg/src/dkg.rs @@ -1,4 +1,5 @@ /// DKG configuration +#[derive(Debug, Clone, Default)] pub struct Config { /// Path to the definition file. Can be an URL or an absolute path on disk. pub def_file: String, @@ -9,6 +10,7 @@ pub struct Config { } /// Additional test-only config for DKG. +#[derive(Debug, Clone, Default)] pub struct TestConfig { /// Provides the cluster definition explicitly, skips loading from disk. pub def: Option, From 3fee2d6f0f6e44d8175fd1ebf5864800c7f3b2bb Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 11 Mar 2026 17:56:46 -0300 Subject: [PATCH 19/45] Test `load_definition_invalid_file` - Fix `load_definition_file_does_not_exist` error --- crates/dkg/src/disk.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 2ba311a6..ab63a196 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -78,9 +78,11 @@ pub(crate) async fn load_definition( // Fetch definition from URI or disk - let parsed_url = url::Url::parse(&conf.def_file)?; - let mut def = if parsed_url.has_host() { - if parsed_url.scheme() != "https" { + let parsed_url = url::Url::parse(&conf.def_file); + let mut def = if let Ok(url) = parsed_url + && url.has_host() + { + if url.scheme() != "https" { warn!( addr = conf.def_file, "Definition file URL does not use https protocol" @@ -327,9 +329,26 @@ mod tests { no_verify: false, ..Default::default() }; + let result = super::load_definition(&cfg, None).await; - dbg!(&result); - assert!(matches!(result, Err(super::DiskError::InvalidUrl(_)))); + + assert!(matches!(result, Err(super::DiskError::IoError(_)))); + } + + #[tokio::test] + async fn load_definition_invalid_file() { + let tempfile = tempfile::NamedTempFile::new().unwrap(); + tokio::fs::write(tempfile.path(), r#"{}"#).await.unwrap(); + + let cfg = dkg::Config { + def_file: tempfile.path().to_string_lossy().into(), + no_verify: false, + ..Default::default() + }; + + let result = super::load_definition(&cfg, None).await; + + assert!(matches!(result, Err(super::DiskError::JsonError(_)))); } #[tokio::test] From 771fd61711050aa8ffeacd7df73c6f1c8bfcc775 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 14:11:37 -0300 Subject: [PATCH 20/45] Add `Generalized Parameter Types` rule --- AGENTS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 2d92ba75..d0bfffd2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,6 +145,43 @@ Rules: - Prefer copying doc comments from Go and adapting to Rust conventions (avoid “Type is a …”). - Avoid leaving TODOs in merged code. If a short-lived internal note is necessary, use `// TODO:` and remove before PR merge. +## Generalized Parameter Types + +Prefer generic parameters over concrete types when a function only needs the behavior of a trait. This mirrors the standard library's own conventions and makes functions callable with a wider range of inputs without extra allocations. + +| Instead of | Prefer | Accepts | +| --- | --- | --- | +| `&str` | `impl AsRef` | `&str`, `String`, `&String`, … | +| `&Path` | `impl AsRef` | `&str`, `String`, `PathBuf`, `&Path`, … | +| `&[u8]` | `impl AsRef<[u8]>` | `&[u8]`, `Vec`, arrays, … | +| `&Vec` | `impl AsRef<[T]>` | `Vec`, slices, arrays, … | +| `String` (owned, read-only) | `impl Into` | `&str`, `String`, … | + +Examples: + +```rust +// accepts &str, String, PathBuf, &Path, … +fn read_file(path: impl AsRef) -> std::io::Result { + std::fs::read_to_string(path.as_ref()) +} + +// accepts &str, String, &String, … +fn print_message(msg: impl AsRef) { + println!(“{}”, msg.as_ref()); +} + +// accepts &[u8], Vec, arrays, … +fn hash_bytes(data: impl AsRef<[u8]>) -> [u8; 32] { + sha256(data.as_ref()) +} +``` + +Rules: + +- Call `.as_ref()` once at the top of the function and bind it to a local variable when the value is used in multiple places. +- Do not use `impl AsRef` if the function immediately converts to an owned type anyway — use `impl Into` (or just accept the owned type) in that case. +- Applies to public and private functions alike; the gain is ergonomics, not just API surface. + ## Testing - Translate Go tests to Rust where applicable; keep similar test names for cross-reference. From 72c476ef78b5cfbe07c257e0b59ac4fadf1159c0 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 14:13:35 -0300 Subject: [PATCH 21/45] Replace `todo!` with actual calls --- crates/cluster/src/helpers.rs | 6 ++++-- crates/dkg/src/disk.rs | 11 ++++++++--- crates/dkg/src/dkg.rs | 4 ++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/cluster/src/helpers.rs b/crates/cluster/src/helpers.rs index b5be6f86..fc7d51a4 100644 --- a/crates/cluster/src/helpers.rs +++ b/crates/cluster/src/helpers.rs @@ -65,8 +65,10 @@ pub async fn fetch_definition( /// Creates a new directory for validator keys. /// If the directory "validator_keys" exists, it checks if the directory is /// empty. -pub async fn create_validator_keys_dir(parent_dir: &std::path::Path) -> std::io::Result { - let vk_dir = parent_dir.join("validator_keys"); +pub async fn create_validator_keys_dir( + parent_dir: impl AsRef, +) -> std::io::Result { + let vk_dir = parent_dir.as_ref().join("validator_keys"); if let Err(e) = tokio::fs::create_dir(&vk_dir).await { if e.kind() != std::io::ErrorKind::AlreadyExists { diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index efa99e09..95ed023f 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -13,6 +13,10 @@ pub(crate) enum DiskError { #[error("Invalid URL: {0}")] InvalidUrl(#[from] url::ParseError), + /// Cluster definition fetch error. + #[error("Cluster definition fetch error: {0}")] + FetchError(#[from] pluto_cluster::helpers::FetchError), + /// I/O error. #[error("IO error: {0}")] IoError(#[from] std::io::Error), @@ -89,8 +93,7 @@ pub(crate) async fn load_definition( ); } - let def: pluto_cluster::definition::Definition = - todo!("requires `cluster.FetchDefinition`"); + let def = pluto_cluster::helpers::fetch_definition(url).await?; let definition_hash = pluto_cluster::helpers::to_0x_hex(&def.definition_hash); info!( @@ -181,7 +184,9 @@ pub(crate) async fn write_keys_to_disk( ) -> Result<()> { let secret_shares = shares.iter().map(|s| s.secret_share).collect::>(); - let keys_dir: String = todo!("requires `cluster.CreateValidatorKeysDir`"); + let keys_dir = pluto_cluster::helpers::create_validator_keys_dir(&conf.data_dir).await?; + // TODO: All paths should be handled using `std::path::*` instead of strings. + let keys_dir = keys_dir.to_string_lossy().to_owned(); if insecure { pluto_eth2util::keystore::store_keys_insecure( diff --git a/crates/dkg/src/dkg.rs b/crates/dkg/src/dkg.rs index 35978a19..f186a618 100644 --- a/crates/dkg/src/dkg.rs +++ b/crates/dkg/src/dkg.rs @@ -5,6 +5,10 @@ pub struct Config { pub def_file: String, /// Skip cluster definition verification. pub no_verify: bool, + + /// Data directory to store generated keys and other DKG artifacts. + pub data_dir: String, + /// Test configuration, used for testing purposes. pub test_config: TestConfig, } From 47a8a98933482de1282fdb66a1d72215915dca5f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 16:40:09 -0300 Subject: [PATCH 22/45] Make `test_cluster` non test-only - Workaround to be used by dkg --- crates/cluster/Cargo.toml | 3 +-- crates/cluster/src/lib.rs | 3 +-- crates/cluster/src/test_cluster.rs | 19 ++++++++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/cluster/Cargo.toml b/crates/cluster/Cargo.toml index a881a110..b756e567 100644 --- a/crates/cluster/Cargo.toml +++ b/crates/cluster/Cargo.toml @@ -19,7 +19,7 @@ pluto-core.workspace = true pluto-crypto.workspace = true pluto-eth2api.workspace = true thiserror.workspace = true -rand_core.workspace = true +rand.workspace = true libp2p.workspace = true pluto-p2p.workspace = true pluto-eth2util.workspace = true @@ -35,7 +35,6 @@ prost-build.workspace = true [dev-dependencies] test-case.workspace = true pluto-testutil.workspace = true -rand.workspace = true tempfile.workspace = true wiremock.workspace = true diff --git a/crates/cluster/src/lib.rs b/crates/cluster/src/lib.rs index fc85adc6..c34cb279 100644 --- a/crates/cluster/src/lib.rs +++ b/crates/cluster/src/lib.rs @@ -28,8 +28,7 @@ pub mod registration; pub mod ssz; /// Cluster SSZ hashing management and coordination. pub mod ssz_hasher; -/// Cluster test cluster management and coordination. -#[cfg(test)] +/// Constructing clusters for tests. pub mod test_cluster; /// Cluster version management and coordination. pub mod version; diff --git a/crates/cluster/src/test_cluster.rs b/crates/cluster/src/test_cluster.rs index 7b7e6dc3..b2605abb 100644 --- a/crates/cluster/src/test_cluster.rs +++ b/crates/cluster/src/test_cluster.rs @@ -53,7 +53,7 @@ pub fn new_for_test( priv_shares.push(share_priv_key); } - let fee_recipient_address = pluto_testutil::random::random_eth_address(&mut rng); + let fee_recipient_address = random_eth_address(&mut rng); let network_name = pluto_eth2util::network::GOERLI.name; let reg = get_signed_registration(&root_secret, fee_recipient_address, network_name); @@ -69,9 +69,7 @@ pub fn new_for_test( dv_shares.push(priv_shares); fee_recipient_addresses.push(helpers::to_0x_hex(&fee_recipient_address)); - withdrawal_addresses.push(helpers::to_0x_hex( - &pluto_testutil::random::random_eth_address(&mut rng), - )); + withdrawal_addresses.push(helpers::to_0x_hex(&random_eth_address(&mut rng))); } let mut ops = Vec::with_capacity(n as usize); @@ -87,7 +85,7 @@ pub fn new_for_test( clippy::cast_possible_truncation, reason = "intentional truncation for testing purposes" )] - let p2p_key = pluto_testutil::random::generate_insecure_k1_key(seed as u8 + i); + let p2p_key = generate_insecure_k1_key(seed as u8 + i); let addr = pluto_eth2util::helpers::public_key_to_address(&p2p_key.public_key()); let record = pluto_eth2util::enr::Record::new(&p2p_key, Vec::new()).unwrap(); let op = operator::Operator { @@ -202,3 +200,14 @@ fn get_signed_registration( signature, } } + +fn generate_insecure_k1_key(seed: impl Into) -> k256::SecretKey { + let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); + k256::SecretKey::random(&mut rng) +} + +fn random_eth_address(rand: &mut impl rand::Rng) -> [u8; 20] { + let mut bytes = [0u8; 20]; + rand.fill(&mut bytes[..]); + bytes +} From acb72da9c958fef0573dc21b638c6b192d1b41e4 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 16:58:52 -0300 Subject: [PATCH 23/45] Update module docs --- crates/cluster/src/lib.rs | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/cluster/src/lib.rs b/crates/cluster/src/lib.rs index c34cb279..c5357714 100644 --- a/crates/cluster/src/lib.rs +++ b/crates/cluster/src/lib.rs @@ -4,31 +4,38 @@ //! This crate handles the formation, management, and coordination of validator //! clusters in the Charon network. -/// Cluster definition management and coordination. +/// `Definition` type representing the intended cluster configuration +/// (operators, validators, fork version) with EIP-712 hashing and verification. pub mod definition; -/// Cluster deposit management and coordination. +/// `DepositData` type for activating validators. pub mod deposit; -/// Cluster distributed validator management and coordination. +/// `DistValidator` type representing a distributed validator with its group +/// public key, per-node public shares, and deposit data. pub mod distvalidator; -/// Cluster EIP-712 signatures management and coordination. +/// EIP-712 typed data construction and signing for cluster definition config +/// hashes and operator ENR signatures. pub mod eip712sigs; -/// Cluster helpers management and coordination. +/// General helper utilities. pub mod helpers; -/// Cluster lock management and coordination. +/// `Lock` type representing the finalized cluster configuration, including +/// distributed validators and node signatures. pub mod lock; -/// Manifest +/// Cluster manifest types, loading, mutation, and materialization. pub mod manifest; -/// Manifest protocol buffers. +/// Generated protobuf types for the cluster manifest (v1). pub mod manifestpb; -/// Cluster operator management and coordination. +/// `Operator` type representing a charon node operator with Ethereum address, +/// ENR, and config/ENR signatures. pub mod operator; -/// Cluster registration management and coordination. +/// `BuilderRegistration` and `Registration` types for pre-generated signed +/// validator registrations sent to the builder network. pub mod registration; -/// Cluster SSZ management and coordination. +/// SSZ serialization for various cluster types. pub mod ssz; -/// Cluster SSZ hashing management and coordination. +/// Core SSZ utilities. pub mod ssz_hasher; -/// Constructing clusters for tests. +/// Factory for constructing deterministic or random cluster locks for use in +/// tests. pub mod test_cluster; -/// Cluster version management and coordination. +/// Supported cluster definition version constants and feature-flag helpers. pub mod version; From 65c4aa9a54f8ed02912cbc01f7b746eea23bb6a4 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 17:01:00 -0300 Subject: [PATCH 24/45] Test `load_definition_valid` --- crates/dkg/src/disk.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 95ed023f..ce34f4f2 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -327,6 +327,28 @@ pub(crate) fn random_hex64() -> String { mod tests { use crate::dkg; + #[tokio::test] + async fn load_definition_valid() { + let tempdir = tempfile::tempdir().unwrap(); + let defintion_path = tempdir.path().join("definition.json"); + + let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); + let definition = &lock.definition; + let json = serde_json::to_string(definition).unwrap(); + tokio::fs::write(&defintion_path, json).await.unwrap(); + + let cfg = dkg::Config { + def_file: defintion_path.to_string_lossy().into(), + no_verify: false, + ..Default::default() + }; + + let client = noop_eth1_client().await; + let actual = super::load_definition(&cfg, &client).await.unwrap(); + + assert_eq!(actual, *definition); + } + #[tokio::test] async fn load_definition_file_does_not_exist() { let cfg = dkg::Config { From 64f9fe8dac82d86db3cc41dcf53d0165161da3f0 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 17:27:03 -0300 Subject: [PATCH 25/45] Use defaults when key is missing --- crates/cluster/src/definition.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/cluster/src/definition.rs b/crates/cluster/src/definition.rs index 13d6bba2..d3f60692 100644 --- a/crates/cluster/src/definition.rs +++ b/crates/cluster/src/definition.rs @@ -933,10 +933,12 @@ pub struct DefinitionV1x2or3 { /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub definition_hash: Vec, } @@ -1047,10 +1049,12 @@ pub struct DefinitionV1x4 { /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub definition_hash: Vec, } @@ -1159,10 +1163,12 @@ pub struct DefinitionV1x5to7 { /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub definition_hash: Vec, } @@ -1260,10 +1266,12 @@ pub struct DefinitionV1x8 { /// ConfigHash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub config_hash: Vec, /// DefinitionHash uniquely identifies a cluster definition including /// operator ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub definition_hash: Vec, } @@ -1365,10 +1373,12 @@ pub struct DefinitionV1x9 { /// ConfigHash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub config_hash: Vec, /// DefinitionHash uniquely identifies a cluster definition including /// operator ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub definition_hash: Vec, } @@ -1477,10 +1487,12 @@ pub struct DefinitionV1x10 { /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. #[serde_as(as = "EthHex")] + #[serde(default)] pub definition_hash: Vec, } From c5861369d81fe6add9bb81f7b16245102c0fd99f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 17:27:26 -0300 Subject: [PATCH 26/45] Test `load_definition_invalid_definition_no_verify` --- crates/dkg/src/disk.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index ce34f4f2..2880f61a 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -380,6 +380,37 @@ mod tests { assert!(matches!(result, Err(super::DiskError::JsonError(_)))); } + #[tokio::test] + async fn load_definition_invalid_definition_no_verify() { + let tempdir = tempfile::tempdir().unwrap(); + let defintion_path = tempdir.path().join("definition.json"); + + let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); + let definition = &lock.definition; + + let json = { + let mut json = serde_json::to_value(&definition).unwrap(); + let as_object = json.as_object_mut().unwrap(); + // Intentionally remove the hashes to make the definition invalid + as_object.remove("config_hash"); + as_object.remove("definition_hash"); + + serde_json::to_string(&json).unwrap() + }; + tokio::fs::write(&defintion_path, json).await.unwrap(); + + let cfg = dkg::Config { + def_file: defintion_path.to_string_lossy().into(), + no_verify: true, // Intentionally set to `true` to bypass verification + ..Default::default() + }; + + let client = noop_eth1_client().await; + let actual = super::load_definition(&cfg, &client).await.unwrap(); + + assert_eq!(actual, *definition); + } + #[tokio::test] async fn clear_data_dir_does_not_exist() { let temp_dir = tempfile::tempdir().unwrap(); From 05f0121c07bf6140b323240a65684d299cf34e3c Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 17:30:20 -0300 Subject: [PATCH 27/45] Test `load_definition_invalid_definition_verify` --- crates/dkg/src/disk.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 2880f61a..81004c17 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -411,6 +411,40 @@ mod tests { assert_eq!(actual, *definition); } + #[tokio::test] + async fn load_definition_invalid_definition_verify() { + let tempdir = tempfile::tempdir().unwrap(); + let defintion_path = tempdir.path().join("definition.json"); + + let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); + let definition = &lock.definition; + + let json = { + let mut json = serde_json::to_value(&definition).unwrap(); + let as_object = json.as_object_mut().unwrap(); + // Intentionally remove the hashes to make the definition invalid + as_object.remove("config_hash"); + as_object.remove("definition_hash"); + + serde_json::to_string(&json).unwrap() + }; + tokio::fs::write(&defintion_path, json).await.unwrap(); + + let cfg = dkg::Config { + def_file: defintion_path.to_string_lossy().into(), + no_verify: false, // Verify the defintion + ..Default::default() + }; + + let client = noop_eth1_client().await; + let result = super::load_definition(&cfg, &client).await; + + assert!(matches!( + result, + Err(super::DiskError::ClusterDefinitionVerificationError { .. }) + )); + } + #[tokio::test] async fn clear_data_dir_does_not_exist() { let temp_dir = tempfile::tempdir().unwrap(); From 0952f9a010a69865feb2bf7ea57a15d4efd81b81 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 17:59:17 -0300 Subject: [PATCH 28/45] Add `ShareMsg` - Impl `From<&Share> for ShareMsg` --- crates/dkg/src/share.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/dkg/src/share.rs b/crates/dkg/src/share.rs index db1ccbc2..83b9869b 100644 --- a/crates/dkg/src/share.rs +++ b/crates/dkg/src/share.rs @@ -8,3 +8,30 @@ pub(crate) struct Share { pub public_shares: HashMap, // u64 == Share index } + +/// The [`Share`] message wire format sent by the dealer. +pub(crate) struct ShareMsg { + pub pub_key: Vec, + pub pub_shares: Vec>, + pub secret_share: Vec, +} + +impl From<&Share> for ShareMsg { + fn from(share: &Share) -> Self { + let pub_key = share.pub_key.to_vec(); + let secret_share = share.secret_share.to_vec(); + + // Sort pub shares by id. + let pub_shares = { + let mut entries: Vec<_> = share.public_shares.iter().collect(); + entries.sort_by_key(|(id, _)| *id); + entries.into_iter().map(|(_, pk)| pk.to_vec()).collect() + }; + + Self { + pub_key, + pub_shares, + secret_share, + } + } +} From 2d53e6bb066c8aeacb675bf8d077164e10337c8d Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 17:59:36 -0300 Subject: [PATCH 29/45] Update `Cargo.lock` --- Cargo.lock | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b81f340..ddbf3a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -729,9 +729,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -1749,9 +1749,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -5495,7 +5495,6 @@ dependencies = [ "prost-build", "prost-types 0.14.3", "rand 0.8.5", - "rand_core 0.6.4", "reqwest 0.13.2", "serde", "serde_json", From 5557910cc3601f2cb63beab052224e3e4848dbf8 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:02:55 -0300 Subject: [PATCH 30/45] Allow unwrap in test code --- crates/cluster/src/test_cluster.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/cluster/src/test_cluster.rs b/crates/cluster/src/test_cluster.rs index b2605abb..f142da56 100644 --- a/crates/cluster/src/test_cluster.rs +++ b/crates/cluster/src/test_cluster.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unwrap_used, reason = "test code")] + use crate::{definition, distvalidator, helpers, lock, operator, registration, version}; use chrono::{TimeZone, Utc}; use pluto_crypto::tbls::Tbls; From 29ab23420fa0724ff0faf2cc00f324f8ad354575 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:08:57 -0300 Subject: [PATCH 31/45] Adjust visibility and docs - Make everything public - Add docs to all exposed symbols --- crates/dkg/src/disk.rs | 29 +++++++++++++++++------------ crates/dkg/src/share.rs | 11 ++++++++--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 81004c17..bd883e5b 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -8,7 +8,7 @@ use tracing::{info, warn}; /// Error type for DKG disk operations. #[derive(Debug, thiserror::Error)] -pub(crate) enum DiskError { +pub enum DiskError { /// Invalid URL. #[error("Invalid URL: {0}")] InvalidUrl(#[from] url::ParseError), @@ -39,9 +39,11 @@ pub(crate) enum DiskError { #[error("Deposit amounts verification failed: {0}")] DepositAmountsVerificationError(#[from] pluto_eth2util::deposit::DepositError), + /// Keystore operation error. #[error("Keystore error: {0}")] KeystoreError(#[from] pluto_eth2util::keystore::KeystoreError), + /// Keymanager client error. #[error("Keymanager error: {0}")] KeymanagerClientError(#[from] pluto_eth2util::keymanager::KeymanagerError), @@ -56,14 +58,18 @@ pub(crate) enum DiskError { /// Data directory contains disallowed entries. #[error("data directory not clean, cannot continue")] DataDirNotClean { + /// Name of the disallowed file or directory. disallowed_entity: String, + /// Path where the disallowed entity was found. data_dir: PathBuf, }, /// Data directory is missing required files. #[error("missing required files, cannot continue")] MissingRequiredFiles { + /// Name of the missing required file. file_name: String, + /// Path where required file was expected. data_dir: PathBuf, }, } @@ -72,7 +78,7 @@ type Result = std::result::Result; /// Returns the [`pluto_cluster::definition::Definition`] from disk or an HTTP /// URL. It returns the test definition if configured. -pub(crate) async fn load_definition( +pub async fn load_definition( conf: &dkg::Config, eth1cl: &pluto_eth1wrap::EthClient, ) -> Result { @@ -150,7 +156,7 @@ pub(crate) async fn load_definition( /// Writes validator private keyshares for the node to the provided keymanager /// address. -pub(crate) async fn write_to_keymanager( +pub async fn write_to_keymanager( keymanager_url: impl AsRef, auth_token: impl AsRef, shares: &[share::Share], @@ -177,7 +183,8 @@ pub(crate) async fn write_to_keymanager( Ok(()) } -pub(crate) async fn write_keys_to_disk( +/// Writes validator private keyshares for the node to disk. +pub async fn write_keys_to_disk( conf: &dkg::Config, shares: &[share::Share], insecure: bool, @@ -201,11 +208,8 @@ pub(crate) async fn write_keys_to_disk( Ok(()) } - -pub(crate) async fn write_lock( - data_dir: impl AsRef, - lock: &pluto_cluster::lock::Lock, -) -> Result<()> { +/// Writes a [`pluto_cluster::lock::Lock`] to disk. +pub async fn write_lock(data_dir: impl AsRef, lock: &pluto_cluster::lock::Lock) -> Result<()> { use serde::Serialize; use tokio::io::AsyncWriteExt; @@ -229,7 +233,7 @@ pub(crate) async fn write_lock( /// Ensures `data_dir` exists, is a directory, and does not contain any /// disallowed entries, while checking for the presence of necessary files. -pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<()> { +pub async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<()> { let path = path::PathBuf::from(data_dir.as_ref()); match tokio::fs::metadata(&path).await { @@ -281,7 +285,7 @@ pub(crate) async fn check_clear_data_dir(data_dir: impl AsRef) -> Re /// Writes sample files to check disk writes and removes sample files after /// verification. -pub(crate) async fn check_writes(data_dir: impl AsRef) -> Result<()> { +pub async fn check_writes(data_dir: impl AsRef) -> Result<()> { const CHECK_BODY: &str = "delete me: dummy file used to check write permissions"; let base = path::Path::new(data_dir.as_ref()); @@ -315,7 +319,8 @@ pub(crate) async fn check_writes(data_dir: impl AsRef) -> Result<()> { Ok(()) } -pub(crate) fn random_hex64() -> String { +/// Generate a random 32-byte value and return it as a hex string. +pub fn random_hex64() -> String { let mut rng = rand::rngs::OsRng; let mut bytes = [0u8; 32]; diff --git a/crates/dkg/src/share.rs b/crates/dkg/src/share.rs index 83b9869b..345a06bb 100644 --- a/crates/dkg/src/share.rs +++ b/crates/dkg/src/share.rs @@ -2,17 +2,22 @@ use std::collections::HashMap; /// The co-validator public key, tbls public shares, and private key share. /// Each node in the cluster will receive one for each distributed validator. -pub(crate) struct Share { +pub struct Share { + /// Public key pub pub_key: pluto_crypto::types::PublicKey, + /// Private key share pub secret_share: pluto_crypto::types::PrivateKey, - + /// TBLS public shares, indexed by share id. pub public_shares: HashMap, // u64 == Share index } /// The [`Share`] message wire format sent by the dealer. -pub(crate) struct ShareMsg { +pub struct ShareMsg { + /// Public key pub pub_key: Vec, + /// TBLS public shares, sorted in ascending order by share id. pub pub_shares: Vec>, + /// Private key share pub secret_share: Vec, } From ffc492c715850c53de4367ff546a0b42cbc7258e Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:10:03 -0300 Subject: [PATCH 32/45] Fix clippy lints --- crates/dkg/src/disk.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index bd883e5b..dc0ccbbc 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -193,7 +193,7 @@ pub async fn write_keys_to_disk( let keys_dir = pluto_cluster::helpers::create_validator_keys_dir(&conf.data_dir).await?; // TODO: All paths should be handled using `std::path::*` instead of strings. - let keys_dir = keys_dir.to_string_lossy().to_owned(); + let keys_dir = keys_dir.to_string_lossy().into_owned(); if insecure { pluto_eth2util::keystore::store_keys_insecure( @@ -391,7 +391,7 @@ mod tests { let defintion_path = tempdir.path().join("definition.json"); let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); - let definition = &lock.definition; + let definition = lock.definition; let json = { let mut json = serde_json::to_value(&definition).unwrap(); @@ -413,7 +413,7 @@ mod tests { let client = noop_eth1_client().await; let actual = super::load_definition(&cfg, &client).await.unwrap(); - assert_eq!(actual, *definition); + assert_eq!(actual, definition); } #[tokio::test] @@ -422,7 +422,7 @@ mod tests { let defintion_path = tempdir.path().join("definition.json"); let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); - let definition = &lock.definition; + let definition = lock.definition; let json = { let mut json = serde_json::to_value(&definition).unwrap(); From 23e7ebf385b1c717b52e5a9acb9837341c0150c8 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:20:47 -0300 Subject: [PATCH 33/45] Use `File::create` instead of `File::open` - We need to create the file first. --- crates/dkg/src/disk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index dc0ccbbc..63714c7d 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -224,7 +224,7 @@ pub async fn write_lock(data_dir: impl AsRef, lock: &pluto_cluster::lock::L let path = path::Path::new(data_dir.as_ref()).join("cluster-lock.json"); - let mut file = tokio::fs::File::open(path).await?; + let mut file = tokio::fs::File::create(path).await?; file.write_all(&b).await?; file.metadata().await?.permissions().set_readonly(true); // File needs to be read-only for everybody From 5b8f9a955bf86d22fe568113d85b14bec69ce7d6 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:26:30 -0300 Subject: [PATCH 34/45] Persist readonly permissions --- crates/dkg/src/disk.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 63714c7d..e65ebf4c 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -211,7 +211,6 @@ pub async fn write_keys_to_disk( /// Writes a [`pluto_cluster::lock::Lock`] to disk. pub async fn write_lock(data_dir: impl AsRef, lock: &pluto_cluster::lock::Lock) -> Result<()> { use serde::Serialize; - use tokio::io::AsyncWriteExt; let b = { let mut buf = Vec::new(); @@ -224,9 +223,11 @@ pub async fn write_lock(data_dir: impl AsRef, lock: &pluto_cluster::lock::L let path = path::Path::new(data_dir.as_ref()).join("cluster-lock.json"); - let mut file = tokio::fs::File::create(path).await?; - file.write_all(&b).await?; - file.metadata().await?.permissions().set_readonly(true); // File needs to be read-only for everybody + tokio::fs::write(&path, &b).await?; + + let mut permissions = tokio::fs::metadata(&path).await?.permissions(); + permissions.set_readonly(true); + tokio::fs::set_permissions(&path, permissions).await?; Ok(()) } From ac3a97113a51bc7cf5101eeb40b8a547fc6bae84 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:31:59 -0300 Subject: [PATCH 35/45] Resolve TODOs in module docs --- crates/dkg/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/dkg/src/lib.rs b/crates/dkg/src/lib.rs index 2cfd6192..34739a8f 100644 --- a/crates/dkg/src/lib.rs +++ b/crates/dkg/src/lib.rs @@ -11,8 +11,8 @@ pub mod dkgpb; /// General DKG IO operations. pub mod disk; -/// TODO +/// Main DKG protocol implementation. pub mod dkg; -/// TODO +/// Shares distributed to each node in the cluster. pub mod share; From 7127fc495a1b3703a3ee030c95c3bce10b6d1907 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:34:49 -0300 Subject: [PATCH 36/45] Prefer `AsRef` --- crates/dkg/src/disk.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index e65ebf4c..517283bb 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -209,7 +209,10 @@ pub async fn write_keys_to_disk( Ok(()) } /// Writes a [`pluto_cluster::lock::Lock`] to disk. -pub async fn write_lock(data_dir: impl AsRef, lock: &pluto_cluster::lock::Lock) -> Result<()> { +pub async fn write_lock( + data_dir: impl AsRef, + lock: &pluto_cluster::lock::Lock, +) -> Result<()> { use serde::Serialize; let b = { @@ -221,7 +224,7 @@ pub async fn write_lock(data_dir: impl AsRef, lock: &pluto_cluster::lock::L buf }; - let path = path::Path::new(data_dir.as_ref()).join("cluster-lock.json"); + let path = data_dir.as_ref().join("cluster-lock.json"); tokio::fs::write(&path, &b).await?; @@ -286,10 +289,10 @@ pub async fn check_clear_data_dir(data_dir: impl AsRef) -> Result<() /// Writes sample files to check disk writes and removes sample files after /// verification. -pub async fn check_writes(data_dir: impl AsRef) -> Result<()> { +pub async fn check_writes(data_dir: impl AsRef) -> Result<()> { const CHECK_BODY: &str = "delete me: dummy file used to check write permissions"; - let base = path::Path::new(data_dir.as_ref()); + let base = data_dir.as_ref(); for file in [ "cluster-lock.json", From 13d3eed258738bd35fe722e72a8c65ed62ae2364 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:36:50 -0300 Subject: [PATCH 37/45] Fix typo --- crates/dkg/src/disk.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 517283bb..b557f31c 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -339,15 +339,15 @@ mod tests { #[tokio::test] async fn load_definition_valid() { let tempdir = tempfile::tempdir().unwrap(); - let defintion_path = tempdir.path().join("definition.json"); + let definition_path = tempdir.path().join("definition.json"); let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); let definition = &lock.definition; let json = serde_json::to_string(definition).unwrap(); - tokio::fs::write(&defintion_path, json).await.unwrap(); + tokio::fs::write(&definition_path, json).await.unwrap(); let cfg = dkg::Config { - def_file: defintion_path.to_string_lossy().into(), + def_file: definition_path.to_string_lossy().into(), no_verify: false, ..Default::default() }; @@ -392,7 +392,7 @@ mod tests { #[tokio::test] async fn load_definition_invalid_definition_no_verify() { let tempdir = tempfile::tempdir().unwrap(); - let defintion_path = tempdir.path().join("definition.json"); + let definition_path = tempdir.path().join("definition.json"); let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); let definition = lock.definition; @@ -406,10 +406,10 @@ mod tests { serde_json::to_string(&json).unwrap() }; - tokio::fs::write(&defintion_path, json).await.unwrap(); + tokio::fs::write(&definition_path, json).await.unwrap(); let cfg = dkg::Config { - def_file: defintion_path.to_string_lossy().into(), + def_file: definition_path.to_string_lossy().into(), no_verify: true, // Intentionally set to `true` to bypass verification ..Default::default() }; @@ -423,7 +423,7 @@ mod tests { #[tokio::test] async fn load_definition_invalid_definition_verify() { let tempdir = tempfile::tempdir().unwrap(); - let defintion_path = tempdir.path().join("definition.json"); + let definition_path = tempdir.path().join("definition.json"); let (lock, ..) = pluto_cluster::test_cluster::new_for_test(1, 2, 3, 0); let definition = lock.definition; @@ -437,10 +437,10 @@ mod tests { serde_json::to_string(&json).unwrap() }; - tokio::fs::write(&defintion_path, json).await.unwrap(); + tokio::fs::write(&definition_path, json).await.unwrap(); let cfg = dkg::Config { - def_file: defintion_path.to_string_lossy().into(), + def_file: definition_path.to_string_lossy().into(), no_verify: false, // Verify the defintion ..Default::default() }; From b1391c157ad5f8b3b042ad50365582040505705f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:37:08 -0300 Subject: [PATCH 38/45] Reduce visibility of utility --- crates/dkg/src/disk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index b557f31c..b981424d 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -324,7 +324,7 @@ pub async fn check_writes(data_dir: impl AsRef) -> Result<()> { } /// Generate a random 32-byte value and return it as a hex string. -pub fn random_hex64() -> String { +fn random_hex64() -> String { let mut rng = rand::rngs::OsRng; let mut bytes = [0u8; 32]; From 495bf5f54d0062e6a4380af14b6e55dd21f58bb8 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:41:50 -0300 Subject: [PATCH 39/45] Use `PathBuf` for the data_dir --- crates/dkg/src/dkg.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/dkg/src/dkg.rs b/crates/dkg/src/dkg.rs index f186a618..b3c751fa 100644 --- a/crates/dkg/src/dkg.rs +++ b/crates/dkg/src/dkg.rs @@ -1,3 +1,5 @@ +use std::path; + /// DKG configuration #[derive(Debug, Clone, Default)] pub struct Config { @@ -7,7 +9,7 @@ pub struct Config { pub no_verify: bool, /// Data directory to store generated keys and other DKG artifacts. - pub data_dir: String, + pub data_dir: path::PathBuf, /// Test configuration, used for testing purposes. pub test_config: TestConfig, From f1aa06ebeb090a9386289060e0598936ec8cdb80 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:44:48 -0300 Subject: [PATCH 40/45] Simplify error handling --- crates/dkg/src/disk.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index b981424d..530981af 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -25,12 +25,6 @@ pub enum DiskError { #[error("JSON parsing error: {0}")] JsonError(#[from] serde_json::Error), - /// Cluster definition verification error. - #[error( - "Cluster definition verification failed. Run with `--no-verify` to bypass verification at own risk: {0}" - )] - ClusterDefinitionVerificationError(pluto_cluster::definition::DefinitionError), - /// Cluster definition error. #[error("Cluster definition error: {0}")] ClusterDefinitionError(#[from] pluto_cluster::definition::DefinitionError), @@ -130,7 +124,7 @@ pub async fn load_definition( "Ignoring failed cluster definition hashes verification due to --no-verify flag" ); } else { - return Err(DiskError::ClusterDefinitionVerificationError(error)); + return Err(DiskError::ClusterDefinitionError(error)); } } if let Err(error) = def.verify_signatures(eth1cl).await { @@ -140,7 +134,7 @@ pub async fn load_definition( "Ignoring failed cluster definition signatures verification due to --no-verify flag" ); } else { - return Err(DiskError::ClusterDefinitionVerificationError(error)); + return Err(DiskError::ClusterDefinitionError(error)); } } @@ -450,7 +444,7 @@ mod tests { assert!(matches!( result, - Err(super::DiskError::ClusterDefinitionVerificationError { .. }) + Err(super::DiskError::ClusterDefinitionError { .. }) )); } From 9146957050130f52cc5c3983d83c12b17b118e6f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:45:23 -0300 Subject: [PATCH 41/45] Add `Debug` and `Clone` to `Share` and `ShareMsg` --- crates/dkg/src/share.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/dkg/src/share.rs b/crates/dkg/src/share.rs index 345a06bb..5cb4370a 100644 --- a/crates/dkg/src/share.rs +++ b/crates/dkg/src/share.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; /// The co-validator public key, tbls public shares, and private key share. /// Each node in the cluster will receive one for each distributed validator. +#[derive(Debug, Clone)] pub struct Share { /// Public key pub pub_key: pluto_crypto::types::PublicKey, @@ -12,6 +13,7 @@ pub struct Share { } /// The [`Share`] message wire format sent by the dealer. +#[derive(Debug, Clone)] pub struct ShareMsg { /// Public key pub pub_key: Vec, From cc6d2a38ceb9ca960433a6ecb59484ea80fcb9be Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 18:46:34 -0300 Subject: [PATCH 42/45] Fix typo & formatting --- crates/dkg/src/disk.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/dkg/src/disk.rs b/crates/dkg/src/disk.rs index 530981af..d47af091 100644 --- a/crates/dkg/src/disk.rs +++ b/crates/dkg/src/disk.rs @@ -202,6 +202,7 @@ pub async fn write_keys_to_disk( Ok(()) } + /// Writes a [`pluto_cluster::lock::Lock`] to disk. pub async fn write_lock( data_dir: impl AsRef, @@ -435,7 +436,7 @@ mod tests { let cfg = dkg::Config { def_file: definition_path.to_string_lossy().into(), - no_verify: false, // Verify the defintion + no_verify: false, // Verify the definition ..Default::default() }; From 4e1d71fe8edeb71cd189470d898df6e1c51f2bdf Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:04:45 +0100 Subject: [PATCH 43/45] feat: make test-cluster be feature based --- crates/cluster/Cargo.toml | 3 +++ crates/cluster/src/lib.rs | 1 + crates/dkg/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/cluster/Cargo.toml b/crates/cluster/Cargo.toml index b756e567..22b2e0db 100644 --- a/crates/cluster/Cargo.toml +++ b/crates/cluster/Cargo.toml @@ -40,3 +40,6 @@ wiremock.workspace = true [lints] workspace = true + +[features] +test-cluster = [] \ No newline at end of file diff --git a/crates/cluster/src/lib.rs b/crates/cluster/src/lib.rs index c5357714..52af8adb 100644 --- a/crates/cluster/src/lib.rs +++ b/crates/cluster/src/lib.rs @@ -36,6 +36,7 @@ pub mod ssz; pub mod ssz_hasher; /// Factory for constructing deterministic or random cluster locks for use in /// tests. +#[cfg(any(test, feature = "test-cluster"))] pub mod test_cluster; /// Supported cluster definition version constants and feature-flag helpers. pub mod version; diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index 9c6e799c..1876e096 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -9,7 +9,7 @@ publish.workspace = true [dependencies] prost.workspace = true prost-types.workspace = true -pluto-cluster.workspace = true +pluto-cluster = { workspace = true, features = ["test-cluster"] } pluto-crypto.workspace = true pluto-eth1wrap.workspace = true pluto-eth2util.workspace = true From d361ca4f23334f58990ae2aa567d4246e857cc45 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:10:32 +0100 Subject: [PATCH 44/45] feat: update dkg Cargo.toml to split testing and release versions of cluster --- crates/dkg/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index 1876e096..75cb7bc0 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -9,7 +9,7 @@ publish.workspace = true [dependencies] prost.workspace = true prost-types.workspace = true -pluto-cluster = { workspace = true, features = ["test-cluster"] } +pluto-cluster.workspace = true pluto-crypto.workspace = true pluto-eth1wrap.workspace = true pluto-eth2util.workspace = true @@ -26,6 +26,7 @@ url.workspace = true pluto-build-proto.workspace = true [dev-dependencies] +pluto-cluster = { workspace = true, features = ["test-cluster"] } tempfile.workspace = true [lints] From fdbcd9607ec949ac747c540c8a887889a821f81b Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 17 Mar 2026 15:13:23 -0300 Subject: [PATCH 45/45] Remove inlined definitions --- crates/cluster/Cargo.toml | 5 ++++- crates/cluster/src/test_cluster.rs | 19 +++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/cluster/Cargo.toml b/crates/cluster/Cargo.toml index 22b2e0db..c3a4be5c 100644 --- a/crates/cluster/Cargo.toml +++ b/crates/cluster/Cargo.toml @@ -28,6 +28,9 @@ pluto-k1util.workspace = true k256.workspace = true tokio.workspace = true reqwest = { workspace = true, features = ["json"] } +# Workaround to use test code from different crate. +# See: https://github.com/NethermindEth/pluto/pull/285 +pluto-testutil = { workspace = true, optional = true } [build-dependencies] prost-build.workspace = true @@ -42,4 +45,4 @@ wiremock.workspace = true workspace = true [features] -test-cluster = [] \ No newline at end of file +test-cluster = [] diff --git a/crates/cluster/src/test_cluster.rs b/crates/cluster/src/test_cluster.rs index f142da56..94d733c6 100644 --- a/crates/cluster/src/test_cluster.rs +++ b/crates/cluster/src/test_cluster.rs @@ -55,7 +55,7 @@ pub fn new_for_test( priv_shares.push(share_priv_key); } - let fee_recipient_address = random_eth_address(&mut rng); + let fee_recipient_address = pluto_testutil::random::random_eth_address(&mut rng); let network_name = pluto_eth2util::network::GOERLI.name; let reg = get_signed_registration(&root_secret, fee_recipient_address, network_name); @@ -71,7 +71,9 @@ pub fn new_for_test( dv_shares.push(priv_shares); fee_recipient_addresses.push(helpers::to_0x_hex(&fee_recipient_address)); - withdrawal_addresses.push(helpers::to_0x_hex(&random_eth_address(&mut rng))); + withdrawal_addresses.push(helpers::to_0x_hex( + &pluto_testutil::random::random_eth_address(&mut rng), + )); } let mut ops = Vec::with_capacity(n as usize); @@ -87,7 +89,7 @@ pub fn new_for_test( clippy::cast_possible_truncation, reason = "intentional truncation for testing purposes" )] - let p2p_key = generate_insecure_k1_key(seed as u8 + i); + let p2p_key = pluto_testutil::random::generate_insecure_k1_key(seed as u8 + i); let addr = pluto_eth2util::helpers::public_key_to_address(&p2p_key.public_key()); let record = pluto_eth2util::enr::Record::new(&p2p_key, Vec::new()).unwrap(); let op = operator::Operator { @@ -202,14 +204,3 @@ fn get_signed_registration( signature, } } - -fn generate_insecure_k1_key(seed: impl Into) -> k256::SecretKey { - let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into()); - k256::SecretKey::random(&mut rng) -} - -fn random_eth_address(rand: &mut impl rand::Rng) -> [u8; 20] { - let mut bytes = [0u8; 20]; - rand.fill(&mut bytes[..]); - bytes -}