diff --git a/Cargo.lock b/Cargo.lock index 376fec1f..906dcb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,7 +653,7 @@ dependencies = [ "ark-serialize 0.3.0", "ark-std 0.3.0", "derivative", - "num-bigint 0.4.5", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version 0.3.3", @@ -673,7 +673,7 @@ dependencies = [ "derivative", "digest 0.10.7", "itertools 0.10.5", - "num-bigint 0.4.5", + "num-bigint 0.4.6", "num-traits", "paste", "rustc_version 0.4.0", @@ -706,7 +706,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" dependencies = [ - "num-bigint 0.4.5", + "num-bigint 0.4.6", "num-traits", "quote", "syn 1.0.109", @@ -718,7 +718,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint 0.4.5", + "num-bigint 0.4.6", "num-traits", "proc-macro2", "quote", @@ -757,7 +757,7 @@ dependencies = [ "ark-serialize-derive", "ark-std 0.4.0", "digest 0.10.7", - "num-bigint 0.4.5", + "num-bigint 0.4.6", ] [[package]] @@ -1973,13 +1973,27 @@ dependencies = [ "alloy-primitives", "alloy-provider", "alloy-transport", + "ark-ec", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", "clap", + "eigen-crypto-bls", "eigen-testing-utils", + "eigen-types", "eigen-utils", + "eth-keystore", + "hex", + "k256", + "num-bigint 0.4.6", + "rand", + "rand_core", + "rstest", "serde", "serde_json", + "tempfile", "thiserror", "tokio", + "uuid 1.10.0", ] [[package]] @@ -2001,7 +2015,7 @@ dependencies = [ "eigen-types", "eigen-utils", "hex", - "num-bigint 0.4.5", + "num-bigint 0.4.6", "once_cell", "reqwest 0.12.5", "thiserror", @@ -2085,7 +2099,7 @@ dependencies = [ "ark-bn254", "ark-ec", "ark-ff 0.4.2", - "num-bigint 0.4.5", + "num-bigint 0.4.6", "rand", "rust-bls-bn254", "tokio", @@ -2237,7 +2251,7 @@ dependencies = [ "eigen-crypto-bls", "eigen-utils", "ethers", - "num-bigint 0.4.5", + "num-bigint 0.4.6", ] [[package]] @@ -2358,7 +2372,7 @@ dependencies = [ "hmac", "pbkdf2 0.11.0", "rand", - "scrypt", + "scrypt 0.10.0", "serde", "serde_json", "sha2", @@ -3154,6 +3168,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3971,12 +3994,13 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -4222,6 +4246,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -4242,7 +4277,7 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest 0.10.7", "hmac", - "password-hash", + "password-hash 0.4.2", "sha2", ] @@ -4729,6 +4764,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.11.27" @@ -4904,6 +4945,36 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.0", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.0", + "syn 2.0.67", + "unicode-ident", +] + [[package]] name = "ruint" version = "1.12.3" @@ -4915,7 +4986,7 @@ dependencies = [ "ark-ff 0.4.2", "bytes", "fastrlp", - "num-bigint 0.4.5", + "num-bigint 0.4.6", "num-traits", "parity-scale-codec", "primitive-types", @@ -4937,14 +5008,28 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rust-bls-bn254" version = "0.1.0" -source = "git+https://github.com/Layr-Labs/rust-bls-bn254.git?rev=bd712a7#bd712a7d556f7869a423956deee702f4a5546aa8" +source = "git+https://github.com/Layr-Labs/rust-bls-bn254.git?rev=be3ef87#be3ef873f484bb09c2db7b6491715236df8c6174" dependencies = [ + "aes", "ark-bn254", "ark-ec", "ark-ff 0.4.2", + "ark-serialize 0.4.2", "ark-std 0.4.0", - "num-bigint 0.4.5", + "ctr", + "hex", + "hkdf", + "num-bigint 0.4.6", + "num-traits", + "pbkdf2 0.12.2", + "rand", + "scrypt 0.11.0", + "serde", + "serde_json", "sha2", + "thiserror", + "unicode-normalization", + "uuid 0.8.2", ] [[package]] @@ -5200,6 +5285,18 @@ dependencies = [ "sha2", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash 0.5.0", + "pbkdf2 0.12.2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -5467,7 +5564,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ - "num-bigint 0.4.5", + "num-bigint 0.4.6", "num-traits", "thiserror", "time", diff --git a/Cargo.toml b/Cargo.toml index 3f2feda0..c7b48d0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,29 +4,29 @@ members = [ "crates/chainio/clients/elcontracts/", "crates/chainio/clients/eth/", "crates/chainio/clients/fireblocks/", - "crates/contracts/bindings/", "crates/chainio/utils/", - "crates/crypto/bn254/", - "crates/utils/", + "crates/contracts/bindings/", "crates/crypto/bls/", + "crates/crypto/bn254/", "crates/eigen-cli/", + "crates/logging/", + "crates/metrics/", "crates/metrics/collectors/economic/", "crates/metrics/collectors/rpc_calls/", + "crates/metrics/metrics-derive", "crates/services/avsregistry/", "crates/services/bls_aggregation/", - "crates/metrics/metrics-derive", "crates/services/operatorsinfo/", - "crates/types/", - "crates/metrics/", - "crates/types/", "crates/signer/", - "crates/logging/", "crates/signer/", - "examples/info-operator-service/", - "testing/testing-utils/", + "crates/types/", + "crates/types/", + "crates/utils/", + "examples/anvil-utils", "examples/avsregistry-read", "examples/avsregistry-write", - "examples/anvil-utils", + "examples/info-operator-service/", + "testing/testing-utils/", ] resolver = "2" @@ -51,6 +51,7 @@ rustdoc.all = "warn" ark-bn254 = "0.4.0" ark-ec = "0.4.2" ark-ff = "0.4.0" +ark-serialize = "0.4.2" async-trait = "0.1.81" aws-config = "1.5.4" aws-sdk-kms = "1.37.0" @@ -80,27 +81,37 @@ ethers = "2.0.14" ethers-signers = "2.0.14" eyre = "0.6.12" futures-util = "0.3.30" +hex = "0.4" hex-literal = "0.4.1" hyper = "0.14.25" info-operator-service = { path = "examples/info-operator-service" } k256 = "0.13.3" metrics = "0.21.1" metrics-exporter-prometheus = "0.12.0" +num-bigint = "0.4.6" once_cell = "1.17" prometheus-client = "0.22.2" quote = "1.0" +rand = "0.8" +rand_core = "0.6" reqwest = "0.12.4" reth = { git = "https://github.com/paradigmxyz/reth" } +rstest = "0.22.0" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.121" syn = "2.0" +tempfile = "3" +testcontainers = "0.20.1" thiserror = "1.0" tokio = { version = "1.37.0", features = ["test-util", "full", "sync"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3", features = ["json"] } url = "2.5.2" -testcontainers = "0.20.1" +uuid = { version = "1.10.0", features = ["v4"] } + +#misc rust-bls-bn254 = {git = "https://github.com/Layr-Labs/rust-bls-bn254.git", rev = "be3ef87", features = ["std"] } + #misc parking_lot = "0.12" diff --git a/crates/eigen-cli/Cargo.toml b/crates/eigen-cli/Cargo.toml index 6bc3a992..27b4bb67 100644 --- a/crates/eigen-cli/Cargo.toml +++ b/crates/eigen-cli/Cargo.toml @@ -11,10 +11,28 @@ alloy-json-rpc.workspace = true alloy-primitives.workspace = true alloy-provider.workspace = true alloy-transport.workspace = true +ark-ec.workspace = true +ark-ff.workspace = true +ark-serialize.workspace = true clap.workspace = true +eigen-crypto-bls.workspace = true eigen-testing-utils.workspace = true +eigen-types.workspace = true eigen-utils.workspace = true +eth-keystore.workspace = true +hex.workspace = true +k256.workspace = true +num-bigint.workspace = true +rand.workspace = true +rand_core.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true +uuid.workspace = true + + +[dev-dependencies] +eigen-testing-utils.workspace = true +rstest.workspace = true +tempfile.workspace = true diff --git a/crates/eigen-cli/src/args.rs b/crates/eigen-cli/src/args.rs index d09c2cdc..40afea61 100644 --- a/crates/eigen-cli/src/args.rs +++ b/crates/eigen-cli/src/args.rs @@ -17,14 +17,14 @@ pub enum Commands { #[command( about = "Given an initial contract address, which can be either a registry coordinator or service manager address, outputs addresses for all relevant Eigenlayer and AVS contracts within the network", - alias = "a", + name = "egnaddrs", group( ArgGroup::new("manager_or_coordinator") .required(true) .args(&["service_manager", "registry_coordinator"]), ) )] - GetAddresses { + EigenAddress { #[arg( long, help = "ServiceManager contract address", @@ -42,4 +42,70 @@ outputs addresses for all relevant Eigenlayer and AVS contracts within the netwo #[arg(long, help = "rpc url", default_value = ANVIL_RPC_URL)] rpc_url: String, }, + + #[command(about = "EigenLayer CLI Key tools", name = "egnkey")] + EigenKey { + #[command(subcommand)] + subcommand: EigenKeyCommand, + }, +} + +#[derive(Subcommand, Debug)] +pub enum EigenKeyCommand { + #[command( + about = "Generate keys for testing purpose. +This command creates ecdsa or bls key pair for testing purposes. It generates +all the relevant files for reading the key and decrypts it and also gets +you the private keys in plaintext. + +It creates the following artifacts based on arguments +- passwords.txt - contains all passwords to decrypt keys +- private_key_hex.txt - will create plaintext private keys +- keys/* - create all the encrypted json files in this folder", + alias = "g" + )] + Generate { + #[arg(long, help = "key type to create (ecdsa or bls)")] + #[clap(value_enum)] + key_type: KeyType, + + #[arg(long, help = "number of keys to create", default_value = "1")] + num_keys: u32, + + #[arg(long, help = "folder to store keys")] + output_dir: Option, + }, + + #[command( + about = "Given a private key, output its associated operatorId (hash of bn254 G1 pubkey).", + alias = "d" + )] + DeriveOperatorId { + #[arg( + long, + help = "(bn254) private key from which to derive operatorId from" + )] + private_key: String, + }, + + #[command( + about = "Stores an ecdsa key to a file, in web3 secret storage format.", + alias = "c" + )] + ConvertECDSA { + #[arg(long, help = "private key to store (in hex)")] + private_key: String, + + #[arg(long, help = "file to store key")] + output_file: Option, + + #[arg(long, help = "password to encrypt key")] + password: Option, + }, +} + +#[derive(clap::ValueEnum, Debug, Clone)] +pub enum KeyType { + Ecdsa, + Bls, } diff --git a/crates/eigen-cli/src/convert.rs b/crates/eigen-cli/src/convert.rs new file mode 100644 index 00000000..74b59585 --- /dev/null +++ b/crates/eigen-cli/src/convert.rs @@ -0,0 +1,34 @@ +use eth_keystore::{encrypt_key, KeystoreError}; +use rand_core::OsRng; +use std::path::Path; + +const DEFAULT_KEYSTORE_NAME: &str = "key.json"; + +/// Stores an ecdsa key to a file, in web3 secret storage format. +/// +/// # Arguments +/// +/// * `private_key` - A private key to store. +/// * `output_file` - The name of the file where the key is going to be stored. +/// * `password` - The password used to encrypt the key. *Note:* If `password` is `None` then the empty string is used as password. +/// +/// # Errors +/// +/// - If the key encryption fails. +pub fn store( + private_key: Vec, + output_file: Option, + password: Option, +) -> Result<(), KeystoreError> { + let dir = Path::new("."); + + encrypt_key( + dir, + &mut OsRng, + private_key, + password.unwrap_or_default(), + Some(&output_file.unwrap_or(DEFAULT_KEYSTORE_NAME.into())), + )?; + + Ok(()) +} diff --git a/crates/eigen-cli/src/eigen_address.rs b/crates/eigen-cli/src/eigen_address.rs index e477955a..086bc6c1 100644 --- a/crates/eigen-cli/src/eigen_address.rs +++ b/crates/eigen-cli/src/eigen_address.rs @@ -105,7 +105,7 @@ impl ContractAddresses { /// # Returns /// /// * `(Address, Address)` - The registry coordinator and service manager contract addresses, - /// used to call `get_avs_contract_addresses` and `get_eigenlayer_contract_addresses` functions. + /// used to call `get_avs_contract_addresses` and `get_eigenlayer_contract_addresses` functions. async fn get_registry_coord_and_service_manager_addr( registry_coordinator: Option
, service_manager: Option
, diff --git a/crates/eigen-cli/src/generate.rs b/crates/eigen-cli/src/generate.rs new file mode 100644 index 00000000..94899067 --- /dev/null +++ b/crates/eigen-cli/src/generate.rs @@ -0,0 +1,169 @@ +use crate::args::KeyType; +use crate::EigenKeyCliError; +use ark_ff::UniformRand; +use ark_serialize::{CanonicalSerialize, SerializationError}; +use eth_keystore::encrypt_key; +use rand::{distributions::Alphanumeric, Rng}; +use rand_core::OsRng; +use std::io::Write; +use std::{ + fs::{self, File}, + path::Path, +}; +use uuid::Uuid; + +const PASSWORD_LENGTH: usize = 20; +pub const DEFAULT_KEY_FOLDER: &str = "keys"; +pub const PASSWORD_FILE: &str = "password.txt"; +pub const PRIVATE_KEY_HEX_FILE: &str = "private_key_hex.txt"; + +pub enum KeyGenerator { + ECDSAKeyGenerator, + BLSKeyGenerator, +} + +impl KeyGenerator { + /// Generates a number of private keys in the given directory. + /// + /// # Arguments + /// + /// * `num_keys` - The number of keys to generate. + /// * `output_dir` - The directory where the key files are generated. + pub fn generate( + self, + num_keys: u32, + output_dir: Option, + ) -> Result<(), EigenKeyCliError> { + let dir_name = match output_dir { + None => { + let id = Uuid::new_v4(); + format!("{}-{}", self.key_name(), id) + } + Some(dir) => dir, + }; + let dir_path = Path::new(&dir_name); + let key_path = dir_path.join(DEFAULT_KEY_FOLDER); + fs::create_dir_all(&key_path).map_err(EigenKeyCliError::FileError)?; + + self.generate_keys(num_keys, dir_path) + } + + /// Generates a number of private keys and stores them both encrypted and in plaintext. + /// It creates the following files: + /// - `passwords.txt`: contains all passwords to decrypt keys + /// - `private_key_hex.txt`: plaintext private keys + /// - `keys/*`: all the encrypted json files in this folder + /// + /// # Arguments + /// + /// * `num_keys` - The number of keys to generate. + /// * `path` - The path to the directory where the generated files are stored. + fn generate_keys(self, num_keys: u32, path: &Path) -> Result<(), EigenKeyCliError> { + let key_path = path.join(DEFAULT_KEY_FOLDER); + let private_key_path = path.join(PRIVATE_KEY_HEX_FILE); + let password_path = path.join(PASSWORD_FILE); + + for i in 0..num_keys { + let password = KeyGenerator::generate_random_password(); + let private_key = self + .random_key() + .map_err(EigenKeyCliError::SerializationError)?; + let private_key_hex = hex::encode(private_key.clone()); + + // encrypt the private key into `path` directory + let name = format!("{}.{}.key.json", i + 1, self.key_name()); + encrypt_key( + key_path.clone(), + &mut OsRng, + private_key, + password.clone(), + Some(&name), + ) + .map_err(EigenKeyCliError::KeystoreError)?; + + // write the private key into `private_key_file` + File::create(private_key_path.clone()) + .and_then(|mut file| file.write_all(private_key_hex.as_bytes())) + .map_err(EigenKeyCliError::FileError)?; + + // write the password into `password_file` + File::create(password_path.clone()) + .and_then(|mut file| file.write_all(password.as_bytes())) + .map_err(EigenKeyCliError::FileError)?; + + if (i + 1) % 50 == 0 { + println!("Generated {} keys\n", i + 1); + } + } + Ok(()) + } + + /// Generates a random key which can be type ecdsa or BLS. + /// + /// # Returns + /// + /// * A private key as a vector of bytes. + fn random_key(&self) -> Result, SerializationError> { + match self { + KeyGenerator::ECDSAKeyGenerator => Ok(Self::random_ecdsa_key()), + KeyGenerator::BLSKeyGenerator => Ok(Self::random_bls_key()?), + } + } + + /// Generates a random ecdsa key. + /// + /// # Returns + /// + /// * An ecdsa private key as a vector of bytes. + pub fn random_ecdsa_key() -> Vec { + let private_key = k256::SecretKey::random(&mut OsRng); + private_key.to_bytes().as_slice().to_vec() + } + + /// Generates a random BLS key. + /// + /// # Returns + /// + /// * A BLS private key as a vector of bytes. + fn random_bls_key() -> Result, SerializationError> { + let mut buffer = Vec::new(); + let private_key = eigen_crypto_bls::PrivateKey::rand(&mut OsRng); + private_key.serialize_uncompressed(&mut buffer)?; + Ok(buffer) + } + + /// Generates a 20-character random password. + /// + /// # Returns + /// + /// * A random password. + pub fn generate_random_password() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(PASSWORD_LENGTH) + .map(char::from) + .collect() + } + + /// Get the key type. + /// + /// # Returns + /// + /// * The key type as a string. + fn key_name(&self) -> String { + match self { + KeyGenerator::ECDSAKeyGenerator => "ecdsa", + KeyGenerator::BLSKeyGenerator => "bls", + } + .to_string() + } +} + +impl From for KeyGenerator { + fn from(value: KeyType) -> Self { + match value { + KeyType::Ecdsa => KeyGenerator::ECDSAKeyGenerator, + KeyType::Bls => KeyGenerator::BLSKeyGenerator, + } + } +} diff --git a/crates/eigen-cli/src/lib.rs b/crates/eigen-cli/src/lib.rs index 89fc68cf..0a50d4fa 100644 --- a/crates/eigen-cli/src/lib.rs +++ b/crates/eigen-cli/src/lib.rs @@ -2,15 +2,37 @@ html_logo_url = "https://github.com/Layr-Labs/eigensdk-rs/assets/91280922/bd13caec-3c00-4afc-839a-b83d2890beb5", issue_tracker_base_url = "https://github.com/Layr-Labs/eigensdk-rs/issues/" )] +pub mod args; +mod convert; +pub mod eigen_address; +mod generate; +mod operator_id; +use eth_keystore::KeystoreError; +use tokio::runtime::Runtime; + +use crate::eigen_address::ContractAddresses; use alloy_contract::Error as ContractError; use alloy_json_rpc::RpcError; use alloy_transport::TransportErrorKind; +use args::{Commands, EigenKeyCommand}; +use ark_serialize::SerializationError; +use convert::store; +use eigen_crypto_bls::error::BlsError; +use generate::KeyGenerator; +use operator_id::derive_operator_id; use thiserror::Error; -pub mod args; -pub mod eigen_address; pub const ANVIL_RPC_URL: &str = "http://localhost:8545"; +/// Possible errors raised while executing a CLI command +#[derive(Error, Debug)] +pub enum EigenCliError { + #[error("address error")] + EigenAddressCliError(EigenAddressCliError), + #[error("key error")] + EigenKeyCliError(EigenKeyCliError), +} + /// Possible errors raised while trying to get contract addresses #[derive(Error, Debug)] pub enum EigenAddressCliError { @@ -20,17 +42,165 @@ pub enum EigenAddressCliError { RpcError(RpcError), } +/// Possible errors raised while executing egnkey commands +#[derive(Error, Debug)] +pub enum EigenKeyCliError { + #[error("file error")] + FileError(std::io::Error), + #[error("keystore error")] + KeystoreError(KeystoreError), + #[error("BLS error")] + BLSError(BlsError), + #[error("serialization error")] + SerializationError(SerializationError), +} + +/// Executes an `egnkey` subcommand. +/// +/// # Arguments +/// +/// * `subcommand` - An egnkey subcommand which can be `generate`, `convert` or `derive-operator-id`. +/// +/// # Errors +/// +/// - If the subcommand execution fails (`EigenKeyCliError`). +pub fn execute_egnkey_subcommand(subcommand: EigenKeyCommand) -> Result<(), EigenKeyCliError> { + match subcommand { + EigenKeyCommand::Generate { + key_type, + num_keys, + output_dir, + } => KeyGenerator::from(key_type).generate(num_keys, output_dir), + + EigenKeyCommand::ConvertECDSA { + private_key, + output_file, + password, + } => store(private_key.into(), output_file, password) + .map_err(EigenKeyCliError::KeystoreError), + + EigenKeyCommand::DeriveOperatorId { private_key } => { + let operator_id = + derive_operator_id(private_key).map_err(EigenKeyCliError::BLSError)?; + println!("{}", operator_id); + Ok(()) + } + } +} + +/// Executes a CLI command. +/// +/// # Arguments +/// +/// * `command` - A CLI command which can be `Commands::EigenAddress` or `Commands::EigenKey` +/// +/// # Errors +/// +/// - If the command execution fails (`EigenCliError`). +pub fn execute_command(command: Commands) -> Result<(), EigenCliError> { + match command { + Commands::EigenAddress { + service_manager, + registry_coordinator, + rpc_url, + } => { + let rt = Runtime::new().unwrap(); + let addresses = rt.block_on(async { + ContractAddresses::get_addresses(service_manager, registry_coordinator, rpc_url) + .await + .map_err(EigenCliError::EigenAddressCliError) + })?; + println!("{}", serde_json::to_string_pretty(&addresses).unwrap()); + Ok(()) + } + Commands::EigenKey { subcommand } => { + execute_egnkey_subcommand(subcommand).map_err(EigenCliError::EigenKeyCliError)?; + Ok(()) + } + } +} + #[cfg(test)] mod test { use super::ANVIL_RPC_URL; + use crate::args::EigenKeyCommand; + use crate::convert::store; use crate::eigen_address::ContractAddresses; + use crate::{ + args::{Commands, KeyType}, + execute_command, + generate::{KeyGenerator, DEFAULT_KEY_FOLDER, PASSWORD_FILE, PRIVATE_KEY_HEX_FILE}, + operator_id::derive_operator_id, + }; use eigen_testing_utils::anvil_constants::{ get_registry_coordinator_address, get_service_manager_address, }; - use tokio; + use eth_keystore::decrypt_key; + use k256::SecretKey; + use rstest::rstest; + use std::fs; + use tempfile::tempdir; + + #[rstest] + #[case(KeyType::Ecdsa)] + #[case(KeyType::Bls)] + fn test_generate_key(#[case] key_type: KeyType) { + let output_dir = tempdir().unwrap(); + let output_path = output_dir.path(); + let subcommand = EigenKeyCommand::Generate { + key_type: key_type.clone(), + num_keys: 1, + output_dir: output_path.to_str().map(String::from), + }; + let command = Commands::EigenKey { subcommand }; + + execute_command(command).unwrap(); + + let private_key_hex = fs::read_to_string(output_path.join(PRIVATE_KEY_HEX_FILE)).unwrap(); + let password = fs::read_to_string(output_path.join(PASSWORD_FILE)).unwrap(); + let key_name = match key_type { + KeyType::Ecdsa => "ecdsa", + KeyType::Bls => "bls", + }; + let key_path = output_path + .join(DEFAULT_KEY_FOLDER) + .join(format!("1.{}.key.json", key_name)); + + let decrypted_bytes = decrypt_key(key_path, password).unwrap(); + let decrypted_private_key = SecretKey::from_slice(&decrypted_bytes).unwrap().to_bytes(); + + let private_key = hex::decode(private_key_hex).unwrap(); + + assert_eq!(private_key, decrypted_private_key.as_slice()); + } + + #[test] + fn test_egnkey_derive_operator_id() { + let private_key = + "13710126902690889134622698668747132666439281256983827313388062967626731803599" + .to_string(); + let operator_id = derive_operator_id(private_key).unwrap(); + let expected_operator_id = + "48beccce16ccdf8000c13d5af5f91c7c3dac6c47b339d993d229af1500dbe4a9".to_string(); + assert_eq!(expected_operator_id, operator_id); + } + + #[test] + fn test_convert_ecdsa() { + let private_key = KeyGenerator::random_ecdsa_key(); + let password = KeyGenerator::generate_random_password(); + let file = "key.json".to_string(); + let path = "./key.json".to_string(); + + store(private_key.clone(), Some(file), Some(password.clone())).unwrap(); + let decrypted_key = decrypt_key(path.clone(), password).unwrap(); + std::fs::remove_file(path).unwrap(); + + assert_eq!(private_key, decrypted_key); + } #[tokio::test] - async fn egnaddrs_with_service_manager_flag() { + async fn test_egnaddrs_with_service_manager_flag() { let service_manager_address = get_service_manager_address().await; let expected_addresses: ContractAddresses = serde_json::from_str( @@ -66,7 +236,7 @@ mod test { } #[tokio::test] - async fn egnaddrs_with_registry_coordinator_flag() { + async fn test_egnaddrs_with_registry_coordinator_flag() { let registry_coordinator_address = get_registry_coordinator_address().await; let expected_addresses: ContractAddresses = serde_json::from_str( diff --git a/crates/eigen-cli/src/main.rs b/crates/eigen-cli/src/main.rs index 3c14127e..2eb13bc0 100644 --- a/crates/eigen-cli/src/main.rs +++ b/crates/eigen-cli/src/main.rs @@ -1,23 +1,7 @@ use clap::Parser; -use eigen_cli::{ - args::{Args, Commands}, - eigen_address::ContractAddresses, -}; +use eigen_cli::{args::Args, execute_command}; -#[tokio::main] -async fn main() { +fn main() { let args = Args::parse(); - match args.command { - Commands::GetAddresses { - service_manager, - registry_coordinator, - rpc_url, - } => { - let addresses = - ContractAddresses::get_addresses(service_manager, registry_coordinator, rpc_url) - .await - .unwrap(); - println!("{}", serde_json::to_string_pretty(&addresses).unwrap()); - } - } + execute_command(args.command).unwrap(); } diff --git a/crates/eigen-cli/src/operator_id.rs b/crates/eigen-cli/src/operator_id.rs new file mode 100644 index 00000000..4928d11c --- /dev/null +++ b/crates/eigen-cli/src/operator_id.rs @@ -0,0 +1,30 @@ +use alloy_primitives::keccak256; +use eigen_crypto_bls::{error::BlsError, BlsKeyPair}; + +/// Derives an operator ID from a private key +/// +/// # Arguments +/// +/// * `private_key` - A private key used to derive the operator ID +/// +/// # Returns +/// +/// * The operator ID as `String` +/// +/// # Errors +/// +/// * If the private key is not valid +pub fn derive_operator_id(private_key: String) -> Result { + let key_pair = BlsKeyPair::new(private_key)?; + let pub_key = key_pair.public_key(); + let pub_key_affine = pub_key.g1(); + + let x_int: num_bigint::BigUint = pub_key_affine.x.into(); + let y_int: num_bigint::BigUint = pub_key_affine.y.into(); + + let x_bytes = x_int.to_bytes_be(); + let y_bytes = y_int.to_bytes_be(); + + let hash = keccak256([x_bytes, y_bytes].concat()); + Ok(hex::encode(hash)) +}