diff --git a/packages/ciphernode/Cargo.lock b/packages/ciphernode/Cargo.lock index 09f1d15d..acae3568 100644 --- a/packages/ciphernode/Cargo.lock +++ b/packages/ciphernode/Cargo.lock @@ -2169,8 +2169,11 @@ dependencies = [ "enclave_node", "hex", "once_cell", + "phf", "router", "rpassword", + "serde", + "serde_json", "tokio", "tracing", "tracing-subscriber", @@ -4588,6 +4591,48 @@ dependencies = [ "rustc_version 0.4.0", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -5576,9 +5621,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -5687,6 +5732,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" diff --git a/packages/ciphernode/Cargo.toml b/packages/ciphernode/Cargo.toml index c7df1e8c..382ac6b7 100644 --- a/packages/ciphernode/Cargo.toml +++ b/packages/ciphernode/Cargo.toml @@ -51,6 +51,7 @@ num = "0.4.3" rand_chacha = "0.3.1" rand = "0.8.5" serde = { version = "1.0.208", features = ["derive"] } +serde_json = { version = "1.0.133" } sled = "0.34.7" sha2 = "0.10.8" tokio = { version = "1.38", features = ["full"] } diff --git a/packages/ciphernode/cipher/src/password_manager.rs b/packages/ciphernode/cipher/src/password_manager.rs index 550eadf2..7f30866d 100644 --- a/packages/ciphernode/cipher/src/password_manager.rs +++ b/packages/ciphernode/cipher/src/password_manager.rs @@ -14,6 +14,7 @@ pub trait PasswordManager { async fn get_key(&self) -> Result>>; async fn delete_key(&mut self) -> Result<()>; async fn set_key(&mut self, contents: Zeroizing>) -> Result<()>; + fn is_set(&self) -> bool; } pub struct InMemPasswordManager(pub Option>>); @@ -54,6 +55,10 @@ impl PasswordManager for EnvPasswordManager { self.0 = None; Ok(()) } + + fn is_set(&self) -> bool { + self.0 == None + } } #[async_trait] @@ -73,6 +78,10 @@ impl PasswordManager for InMemPasswordManager { self.0 = None; Ok(()) } + + fn is_set(&self) -> bool { + self.0 == None + } } pub struct FilePasswordManager { @@ -149,6 +158,11 @@ impl PasswordManager for FilePasswordManager { Ok(()) } + + fn is_set(&self) -> bool { + let path = &self.path; + path.exists() + } } fn ensure_file_permissions(path: &PathBuf, perms: u32) -> Result<()> { diff --git a/packages/ciphernode/enclave/Cargo.toml b/packages/ciphernode/enclave/Cargo.toml index 74a4dde6..9df066dd 100644 --- a/packages/ciphernode/enclave/Cargo.toml +++ b/packages/ciphernode/enclave/Cargo.toml @@ -4,8 +4,7 @@ version = "0.1.0" edition = "2021" description = ": coordinates the encryption and decryption of enclave computations" repository = "https://github.com/gnosisguild/enclave/packages/ciphernode" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +build = "build.rs" [dependencies] actix = { workspace = true } @@ -24,6 +23,12 @@ once_cell = "1.20.2" router = { path = "../router" } rpassword = "7.3.1" tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } zeroize = { workspace = true } +phf = { version = "0.11", features = ["macros"] } + +[build-dependencies] +serde_json = { workspace = true } diff --git a/packages/ciphernode/enclave/build.rs b/packages/ciphernode/enclave/build.rs new file mode 100644 index 00000000..01a47dd1 --- /dev/null +++ b/packages/ciphernode/enclave/build.rs @@ -0,0 +1,63 @@ +// Here we build some contract information from the EVM deployment artifacts that we can use within +// our binaries. Specifically we wbuild out a rust file that has a structure we can import and use +// within our configuration builder +use serde_json::{from_reader, Value}; +use std::env; +use std::fs::{self, File}; +use std::path::Path; + +fn main() -> std::io::Result<()> { + // Get the manifest directory (where Cargo.toml is located) + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + // Path to deployment artifacts + let deployments_path = Path::new(&manifest_dir) + .join("..") // Adjust based on your actual path structure + .join("..") + .join("evm") + .join("deployments") + .join("sepolia"); + + // Create output string for contract info + let mut contract_info = String::from( + "pub struct ContractInfo {\n pub address: &'static str,\n pub deploy_block: u64,\n}\n\n" + ); + contract_info.push_str( + "pub static CONTRACT_DEPLOYMENTS: phf::Map<&'static str, ContractInfo> = phf::phf_map! {\n", + ); + + // Process each JSON file in the deployments directory + for entry in fs::read_dir(deployments_path)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("json") { + let contract_name = path.file_stem().and_then(|s| s.to_str()).unwrap(); + + let file = File::open(&path)?; + let json: Value = from_reader(file)?; + + // Extract address and block number + if let (Some(address), Some(deploy_block)) = ( + json["address"].as_str(), + json["receipt"]["blockNumber"].as_u64(), + ) { + contract_info.push_str(&format!( + " \"{}\" => ContractInfo {{\n address: \"{}\",\n deploy_block: {},\n }},\n", + contract_name, address, deploy_block + )); + } + } + } + + contract_info.push_str("};\n"); + + // Write the generated code to a file + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("contract_deployments.rs"); + fs::write(dest_path, contract_info)?; + + println!("cargo:rerun-if-changed=../packages/evm/deployments/sepolia"); + + Ok(()) +} diff --git a/packages/ciphernode/enclave/src/commands/init.rs b/packages/ciphernode/enclave/src/commands/init.rs index e3a2ebd4..a28eebfe 100644 --- a/packages/ciphernode/enclave/src/commands/init.rs +++ b/packages/ciphernode/enclave/src/commands/init.rs @@ -1,3 +1,6 @@ +use crate::commands::password::{self, PasswordCommands}; +use alloy::transports::http::reqwest::Url; +use anyhow::anyhow; use anyhow::Result; use config::load_config; use dialoguer::{theme::ColorfulTheme, Input}; @@ -5,57 +8,104 @@ use enclave_core::get_tag; use std::fs; use tracing::instrument; -use crate::commands::password::{self, PasswordCommands}; +// Import a built file: +// see /target/debug/enclave-xxxxxx/out/contract_deployments.rs +// also see build.rs +include!(concat!(env!("OUT_DIR"), "/contract_deployments.rs")); + +// Get the ContractInfo object +fn get_contract_info(name: &str) -> Result<&ContractInfo> { + Ok(CONTRACT_DEPLOYMENTS + .get(name) + .ok_or(anyhow!("Could not get contract info"))?) +} #[instrument(name = "app", skip_all, fields(id = get_tag()))] pub async fn execute() -> Result<()> { - // Prompt for Ethereum address - let eth_address: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your Ethereum address") + let rpc_url = Input::::new() + .with_prompt("Enter WebSocket devnet RPC URL") + .default("wss://ethereum-sepolia-rpc.publicnode.com".to_string()) + .validate_with(|input: &String| { + if let Ok(url) = Url::parse(input) { + if url.scheme() == "wss" { + return Ok(()); + } + } + Err("Please enter a valid WSS URL") + }) + .interact_text()?; + + let eth_address: Option = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your Ethereum address (press Enter to skip)") + .allow_empty(true) .validate_with(|input: &String| -> Result<(), &str> { - // Basic Ethereum address validation + if input.is_empty() { + return Ok(()); + } if !input.starts_with("0x") { return Err("Address must start with '0x'"); } if input.len() != 42 { return Err("Address must be 42 characters long (including '0x')"); } - if !input[2..].chars().all(|c| c.is_ascii_hexdigit()) { - return Err("Address must contain only hexadecimal characters"); + for c in input[2..].chars() { + if !c.is_ascii_hexdigit() { + return Err("Address must contain only hexadecimal characters"); + } } Ok(()) }) - .interact()?; + .interact() + .ok() + .map(|s| if s.is_empty() { None } else { Some(s) }) + .flatten(); - // Create config directory if it doesn't exist let config_dir = dirs::home_dir() .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))? .join(".config") .join("enclave"); fs::create_dir_all(&config_dir)?; - // Create config file path let config_path = config_dir.join("config.yaml"); - // Create YAML content using indented heredoc style let config_content = format!( r#"--- # Enclave Configuration File -# Ethereum Account Configuration -address: "{}" +{} +chains: + - name: "devnet" + rpc_url: "{}" + contracts: + enclave: + address: "{}" + deploy_block: {} + ciphernode_registry: + address: "{}" + deploy_block: {} + filter_registry: + address: "{}" + deploy_block: {} "#, - eth_address + eth_address.map_or(String::new(), |addr| format!( + "# Ethereum Account Configuration\naddress: \"{}\"", + addr + )), + rpc_url, + get_contract_info("Enclave")?.address, + get_contract_info("Enclave")?.deploy_block, + get_contract_info("CiphernodeRegistryOwnable")?.address, + get_contract_info("CiphernodeRegistryOwnable")?.deploy_block, + get_contract_info("NaiveRegistryFilter")?.address, + get_contract_info("NaiveRegistryFilter")?.deploy_block, ); - // Write to file fs::write(config_path.clone(), config_content)?; // Load with default location let config = load_config(Some(&config_path.display().to_string()))?; password::execute(PasswordCommands::Create { password: None }, config).await?; - - // password::execute(/* command */, config) + println!("Enclave configuration successfully created!"); println!("You can start your node using `enclave start`"); diff --git a/packages/ciphernode/enclave/src/commands/password/create.rs b/packages/ciphernode/enclave/src/commands/password/create.rs index 5af3fa46..4a9a454e 100644 --- a/packages/ciphernode/enclave/src/commands/password/create.rs +++ b/packages/ciphernode/enclave/src/commands/password/create.rs @@ -43,6 +43,11 @@ fn get_zeroizing_pw_vec(input: Option) -> Result>> { pub async fn execute(config: &AppConfig, input: Option) -> Result<()> { let key_file = config.key_file(); let mut pm = FilePasswordManager::new(key_file); + + if pm.is_set() { + bail!("Keyfile already exists. Refusing to overwrite. Try using `enclave password overwrite` or `enclave password delete` in order to change or delete your password.") + } + let pw = get_zeroizing_pw_vec(input)?; match pm.set_key(pw).await { diff --git a/packages/ciphernode/enclave/src/commands/password/delete.rs b/packages/ciphernode/enclave/src/commands/password/delete.rs index bffdf6a0..76d2e8ae 100644 --- a/packages/ciphernode/enclave/src/commands/password/delete.rs +++ b/packages/ciphernode/enclave/src/commands/password/delete.rs @@ -29,9 +29,13 @@ pub async fn prompt_delete(config: &AppConfig, delete_mode: DeleteMode) -> Resul .interact()?; if proceed { - let mut pw_str = prompt_password("\n\nPlease enter the current password: ")?; let key_file = config.key_file(); let mut pm = FilePasswordManager::new(key_file); + if !pm.is_set() { + println!("Password is not set. Nothing to do."); + return Ok(false); + } + let mut pw_str = prompt_password("\n\nPlease enter the current password: ")?; let mut cur_pw = pm.get_key().await?; if pw_str != String::from_utf8_lossy(&cur_pw) {