diff --git a/Cargo.lock b/Cargo.lock index 6cc1615332..dd7c9a838d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3122,6 +3122,7 @@ dependencies = [ "clap", "compile-time", "dialoguer", + "dirs 5.0.1", "e3-config", "e3-crypto", "e3-entrypoint", @@ -3129,6 +3130,7 @@ dependencies = [ "e3-evm", "e3-init", "e3-support-scripts", + "e3-utils", "e3-zk-prover", "hex", "opentelemetry", @@ -3259,6 +3261,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_yaml", "tokio", "tracing", "zeroize", @@ -3324,6 +3327,7 @@ dependencies = [ "e3-trbfv", "e3-utils", "futures-util", + "hex", "num-bigint", "serde", "tokio", diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 34aa511e09..b44edddbbf 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -30,7 +30,7 @@ use e3_sortition::{ NodeStateRepositoryFactory, Sortition, SortitionBackend, SortitionRepositoryFactory, }; use e3_sync::sync; -use e3_utils::{rand_eth_addr, SharedRng}; +use e3_utils::SharedRng; use e3_zk_prover::{setup_zk_actors, ZkBackend}; use std::time::Duration; use std::{collections::HashMap, path::PathBuf, sync::Arc}; @@ -61,7 +61,6 @@ pub struct CiphernodeBuilder { multithread_cache: Option>, multithread_concurrent_jobs: Option, multithread_report: Option>, - name: String, pubkey_agg: bool, rng: SharedRng, sortition_backend: SortitionBackend, @@ -74,7 +73,7 @@ pub struct CiphernodeBuilder { threshold_plaintext_agg: bool, zk_backend: Option, net_config: Option, - start_buffer: bool, + ignore_address_check: bool, } // Simple Net Configuration @@ -115,7 +114,7 @@ impl CiphernodeBuilder { /// - name - Unique name for the ciphernode /// - rng - Arc Mutex wrapped random number generator /// - cipher - Cipher for encryption and decryption of sensitive data - pub fn new(name: &str, rng: SharedRng, cipher: Arc) -> Self { + pub fn new(rng: SharedRng, cipher: Arc) -> Self { Self { address: None, chains: vec![], @@ -128,7 +127,6 @@ impl CiphernodeBuilder { multithread_cache: None, multithread_concurrent_jobs: None, multithread_report: None, - name: name.to_owned(), pubkey_agg: false, rng, sortition_backend: SortitionBackend::score(), @@ -140,8 +138,8 @@ impl CiphernodeBuilder { testmode_signer: None, threshold_plaintext_agg: false, net_config: None, - start_buffer: false, zk_backend: None, + ignore_address_check: false, } } @@ -193,12 +191,6 @@ impl CiphernodeBuilder { self.testmode_errors = true; self } - /// Ensure SnapshotBuffer starts immediately instead of waiting for SyncEnded. This is important - /// for tests that don't specifically - pub fn testmode_start_buffer_immediately(mut self) -> Self { - self.start_buffer = true; - self - } /// Use the node configuration on these specific chains. This will overwrite any previously /// given chains. @@ -207,12 +199,6 @@ impl CiphernodeBuilder { self } - /// Use the given Address to represent the node. This should be unique. - pub fn with_address(mut self, addr: &str) -> Self { - self.address = Some(addr.to_owned()); - self - } - /// Log data actor events pub fn with_logging(mut self) -> Self { self.logging = true; @@ -316,6 +302,11 @@ impl CiphernodeBuilder { self } + pub fn testmode_ignore_address_check(mut self) -> Self { + self.ignore_address_check = true; + self + } + fn create_local_bus() -> Addr> { EventBus::::new(EventBusConfig { deduplicate: true }).start() } @@ -368,15 +359,6 @@ impl CiphernodeBuilder { None }; - let addr = if let Some(addr) = self.address.clone() { - info!("Using eth address = {}", addr); - addr - } else { - info!("Using random eth address"); - // TODO: This is for testing and should not be used for production if we use this to create ciphernodes in production - rand_eth_addr(&self.rng) - }; - // Create provider cache early to use for chain validation let mut provider_cache = if let Some(signer) = self.testmode_signer.take() { ProviderCache::new().with_signer(signer) @@ -388,33 +370,34 @@ impl CiphernodeBuilder { // Get an event system instance. let event_system = if let EventSystemType::Persisted { kv_path, log_path } = self.event_system.clone() { - EventSystem::persisted(&addr, log_path, kv_path) + EventSystem::persisted(log_path, kv_path) .with_event_bus(local_bus) .with_aggregate_config(aggregate_config.clone()) } else { if let Some(ref store) = self.in_mem_store { - EventSystem::in_mem_from_store(&addr, store) + EventSystem::in_mem_from_store(store) .with_event_bus(local_bus) .with_aggregate_config(aggregate_config.clone()) } else { - EventSystem::in_mem(&addr) + EventSystem::in_mem() .with_event_bus(local_bus) .with_aggregate_config(aggregate_config.clone()) } }; - let bus = event_system.handle()?; let store = event_system.store()?; let eventstore_ts = event_system.eventstore_getter_ts()?; let eventstore_seq = event_system.eventstore_getter_seq()?; let cipher = &self.cipher; let repositories = Arc::new(store.repositories()); - - // Now we add write support as store depends on event system let mut provider_cache = provider_cache.with_write_support(Arc::clone(cipher), Arc::clone(&repositories)); - // Use the configured backend directly + // We need to supply the Hlc to the bus handle in order to enable it + let addr = provider_cache.ensure_signer().await?.address().to_string(); + let bus = event_system.handle()?.enable(&addr); + + // Use the configured sortition backend directly let default_backend = self.sortition_backend.clone(); let ciphernode_selector = diff --git a/crates/ciphernode-builder/src/event_system.rs b/crates/ciphernode-builder/src/event_system.rs index 26c28fadcb..055030f417 100644 --- a/crates/ciphernode-builder/src/event_system.rs +++ b/crates/ciphernode-builder/src/event_system.rs @@ -11,16 +11,15 @@ use e3_data::{ CommitLogEventLog, DataStore, InMemEventLog, InMemSequenceIndex, InMemStore, SledSequenceIndex, SledStore, }; -use e3_events::hlc::Hlc; +use e3_events::hlc_factory::HlcFactory; use e3_events::{ - AggregateConfig, BusHandle, EnclaveEvent, EventBus, EventBusConfig, EventStore, + AggregateConfig, BusHandle, Disabled, EnclaveEvent, EventBus, EventBusConfig, EventStore, EventStoreQueryBy, EventStoreRouter, EventSubscriber, EventType, InsertBatch, SeqAgg, Sequencer, SnapshotBuffer, StoreEventRequested, TsAgg, UpdateDestination, }; use e3_utils::enumerate_path; use once_cell::sync::OnceCell; use std::collections::HashMap; -use std::hash::{DefaultHasher, Hash, Hasher}; use std::path::PathBuf; struct InMemBackend { @@ -45,7 +44,7 @@ struct PersistedBackend { } impl PersistedBackend { - fn get_or_init_store(&self, handle: &BusHandle) -> Result> { + fn get_or_init_store(&self, handle: &BusHandle) -> Result> { self.store .get_or_try_init(|| SledStore::new(handle, &self.sled_path)) .cloned() @@ -74,8 +73,6 @@ pub enum EventStoreAddrs { /// - **WriteBuffer** for batching inserts from actors into a snapshot /// pub struct EventSystem { - /// A nodes id to be used as a tiebreaker in logical clock timestamp differentiation - node_id: u32, /// EventSystem backend either persisted or in memory backend: EventSystemBackend, /// WriteBuffer for batching inserts from actors into a snapshot @@ -85,9 +82,7 @@ pub struct EventSystem { /// EventSystem eventbus eventbus: OnceCell>>, /// EventSystem BusHandle - handle: OnceCell, - /// Hlc override - hlc: OnceCell, + handle: OnceCell>, /// Central configuration for aggregates, including delays and other settings aggregate_config: OnceCell, /// Cached EventStoreAddrs for idempotency @@ -96,14 +91,13 @@ pub struct EventSystem { impl EventSystem { /// Create a new in memory EventSystem with default settings - pub fn new(name: &str) -> Self { - EventSystem::in_mem(name) + pub fn new() -> Self { + EventSystem::in_mem() } /// Create an in memory EventSystem - pub fn in_mem(node_id: &str) -> Self { + pub fn in_mem() -> Self { Self { - node_id: EventSystem::node_id(node_id), backend: EventSystemBackend::InMem(InMemBackend { eventstores: OnceCell::new(), store: OnceCell::new(), @@ -112,16 +106,14 @@ impl EventSystem { sequencer: OnceCell::new(), eventbus: OnceCell::new(), handle: OnceCell::new(), - hlc: OnceCell::new(), aggregate_config: OnceCell::new(), eventstore_addrs: OnceCell::new(), } } /// Create an in memory EventSystem with a given store - pub fn in_mem_from_store(node_id: &str, store: &Addr) -> Self { + pub fn in_mem_from_store(store: &Addr) -> Self { Self { - node_id: EventSystem::node_id(node_id), backend: EventSystemBackend::InMem(InMemBackend { eventstores: OnceCell::new(), store: OnceCell::from(store.to_owned()), @@ -130,16 +122,14 @@ impl EventSystem { sequencer: OnceCell::new(), eventbus: OnceCell::new(), handle: OnceCell::new(), - hlc: OnceCell::new(), aggregate_config: OnceCell::new(), eventstore_addrs: OnceCell::new(), } } /// Create a persisted EventSystem with datafiles at the given paths - pub fn persisted(node_id: &str, log_path: PathBuf, sled_path: PathBuf) -> Self { + pub fn persisted(log_path: PathBuf, sled_path: PathBuf) -> Self { Self { - node_id: EventSystem::node_id(node_id), backend: EventSystemBackend::Persisted(PersistedBackend { log_path, sled_path, @@ -150,7 +140,6 @@ impl EventSystem { sequencer: OnceCell::new(), eventbus: OnceCell::new(), handle: OnceCell::new(), - hlc: OnceCell::new(), aggregate_config: OnceCell::new(), eventstore_addrs: OnceCell::new(), } @@ -170,12 +159,6 @@ impl EventSystem { self } - /// Add an injected hlc - pub fn with_hlc(self, hlc: Hlc) -> Self { - let _ = self.hlc.set(hlc); - self - } - /// Add aggregate configuration including delays and other settings pub fn with_aggregate_config(self, config: AggregateConfig) -> Self { let _ = self.aggregate_config.set(config); @@ -320,18 +303,11 @@ impl EventSystem { } } - /// Get an instance of the Hlc - pub fn hlc(&self) -> Result { - self.hlc - .get_or_try_init(|| Ok(Hlc::new(self.node_id))) - .cloned() - } - /// Get the BusHandle - pub fn handle(&self) -> Result { + pub fn handle(&self) -> Result> { self.handle .get_or_try_init(|| { - let handle = BusHandle::new(self.eventbus(), self.sequencer()?, self.hlc()?); + let handle = BusHandle::new(self.eventbus(), self.sequencer()?, HlcFactory::new()); // Buffer subscribes to all events first // This is important so as to open up a batch for each sequence handle.subscribe(EventType::All, self.buffer()?.recipient()); @@ -363,12 +339,6 @@ impl EventSystem { Ok(store) } - - fn node_id(name: &str) -> u32 { - let mut hasher = DefaultHasher::new(); - name.hash(&mut hasher); - hasher.finish() as u32 - } } struct NoopBatchReceiver; @@ -480,7 +450,7 @@ mod tests { async fn test_persisted() -> Result<()> { let _guard = with_tracing("debug"); let tmp = TempDir::new().unwrap(); - let system = EventSystem::persisted("cn2", tmp.path().join("log"), tmp.path().join("sled")); + let system = EventSystem::persisted(tmp.path().join("log"), tmp.path().join("sled")); let _handle = system.handle().expect("Failed to get handle"); system.store().expect("Failed to get store"); Ok(()) @@ -489,7 +459,7 @@ mod tests { #[actix::test] async fn test_in_mem() { let eventbus = EventBus::::default().start(); - let system = EventSystem::in_mem("cn1").with_event_bus(eventbus); + let system = EventSystem::in_mem().with_event_bus(eventbus); let _handle = system.handle().expect("Failed to get handle"); system.store().expect("Failed to get store"); @@ -505,11 +475,11 @@ mod tests { delays.insert(AggregateId::new(0), Duration::from_secs(1)); // Ag0 is default let config = AggregateConfig::new(delays); - let system = EventSystem::in_mem("cn1") + let system = EventSystem::in_mem() .with_fresh_bus() .with_aggregate_config(config); - let handle = system.handle()?; + let handle = system.handle()?.enable("test"); let datastore = system.store()?; let buffer = system.buffer()?; @@ -661,7 +631,7 @@ mod tests { let aggregate_config = AggregateConfig::new(delays); // Test in-memory eventstores - let system = EventSystem::in_mem("test_multi").with_aggregate_config(aggregate_config); + let system = EventSystem::in_mem().with_aggregate_config(aggregate_config); let Ok(EventStoreAddrs::InMem(addrs)) = system.eventstore_addrs() else { panic!("Expected InMem event store addrs"); }; @@ -675,12 +645,9 @@ mod tests { // Test persistent eventstores let tmp = TempDir::new().unwrap(); - let persisted_system = EventSystem::persisted( - "test_persisted", - tmp.path().join("log"), - tmp.path().join("sled"), - ) - .with_aggregate_config(AggregateConfig::new(HashMap::new())); + let persisted_system = + EventSystem::persisted(tmp.path().join("log"), tmp.path().join("sled")) + .with_aggregate_config(AggregateConfig::new(HashMap::new())); let Ok(EventStoreAddrs::Persisted(addrs)) = persisted_system.eventstore_addrs() else { panic!("Expected Persisted event store addrs"); diff --git a/crates/ciphernode-builder/src/eventbus_factory.rs b/crates/ciphernode-builder/src/eventbus_factory.rs index d2ddc5b6b0..268678ea2a 100644 --- a/crates/ciphernode-builder/src/eventbus_factory.rs +++ b/crates/ciphernode-builder/src/eventbus_factory.rs @@ -7,7 +7,10 @@ use actix::Actor; use actix::Addr; use e3_config::AppConfig; +use e3_data::Repositories; +use e3_events::Disabled; use e3_events::EventType; +use e3_evm::EthPrivateKeyRepositoryFactory; use once_cell::sync::Lazy; use std::any::Any; use std::any::TypeId; @@ -100,9 +103,9 @@ pub fn get_error_collector() -> Addr> { EventBusFactory::instance().get_error_collector() } -pub fn get_enclave_bus_handle(config: &AppConfig) -> anyhow::Result { +pub fn get_enclave_bus_handle() -> anyhow::Result> { let bus = get_enclave_event_bus(); - let system = EventSystem::new(&config.name()).with_event_bus(bus); + let system = EventSystem::new().with_event_bus(bus); system.store()?; // Ensure store is initialized before returning to avoid potentially dropping // events. Ok(system.handle()?) diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1d1a05355e..1ed13553a6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -12,20 +12,22 @@ name = "enclave" path = "src/main.rs" [dependencies] -alloy = { workspace = true } actix = { workspace = true } +alloy = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } compile-time = { workspace = true } dialoguer = { workspace = true } +dirs = { workspace = true } e3-config = { workspace = true } e3-crypto = { workspace = true } e3-entrypoint = { workspace = true } -e3-evm = { workspace = true } e3-events = { workspace = true } +e3-evm = { workspace = true } e3-init = { workspace = true } -e3-zk-prover = { workspace = true } e3-support-scripts = { workspace = true } +e3-utils = { workspace = true } +e3-zk-prover = { workspace = true } hex = { workspace = true } opentelemetry = { workspace = true } opentelemetry-otlp = { workspace = true } diff --git a/crates/cli/src/ciphernode/context.rs b/crates/cli/src/ciphernode/context.rs index 5d01af5bbb..853ac1389d 100644 --- a/crates/cli/src/ciphernode/context.rs +++ b/crates/cli/src/ciphernode/context.rs @@ -145,7 +145,7 @@ fn select_chain<'a>(config: &'a AppConfig, name: Option<&str>) -> Result<&'a Cha None => config .chains() .first() - .ok_or_else(|| anyhow!("No chains configured. Run `enclave config-set` first.")), + .ok_or_else(|| anyhow!("No chains configured. Run `enclave ciphernode setup` first.")), } } diff --git a/crates/cli/src/ciphernode/mod.rs b/crates/cli/src/ciphernode/mod.rs index bb4fcf8474..dadceae7a9 100644 --- a/crates/cli/src/ciphernode/mod.rs +++ b/crates/cli/src/ciphernode/mod.rs @@ -4,17 +4,21 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use anyhow::Result; +use anyhow::{bail, Result}; use clap::{Args, Subcommand}; use e3_config::AppConfig; mod context; mod license; mod lifecycle; +pub mod setup; mod tickets; mod utils; use context::ChainContext; +use zeroize::Zeroizing; + +use crate::helpers::{ensure_hex_zeroizing, parse_zeroizing}; #[derive(Debug, Args, Clone, Default)] pub struct ChainArgs { @@ -31,6 +35,20 @@ impl ChainArgs { #[derive(Subcommand, Debug)] pub enum CiphernodeCommands { + /// Setup local ciphernode configuration + Setup { + /// An rpc url for enclave to connect to + #[arg(long = "rpc-url", short = 'r')] + rpc_url: Option, + + /// The password + #[arg(short, long, value_parser = parse_zeroizing)] + password: Option>, + + /// Wallet Private Key + #[arg(short, long, value_parser = ensure_hex_zeroizing)] + private_key: Option>, + }, /// Manage ENCL license tokens and bonding state License { #[command(subcommand)] @@ -150,6 +168,12 @@ pub async fn execute(command: CiphernodeCommands, config: &AppConfig) -> Result< let ctx = ChainContext::new(config, chain.selection()).await?; lifecycle::status(&ctx).await? } + CiphernodeCommands::Setup { .. } => { + bail!( + "Cannot run `enclave ciphernode setup` when a configuration already exists: {:?}", + config.config_file() + ); + } } Ok(()) diff --git a/crates/cli/src/ciphernode/setup.rs b/crates/cli/src/ciphernode/setup.rs new file mode 100644 index 0000000000..f351ac1cac --- /dev/null +++ b/crates/cli/src/ciphernode/setup.rs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use alloy::primitives::Address; +use anyhow::Result; +use dialoguer::{theme::ColorfulTheme, Input}; +use e3_config::AppConfig; +use e3_entrypoint::config::setup; +use e3_utils::{colorize, eth_address_from_private_key, Color}; +use std::path::PathBuf; +use tracing::instrument; +use zeroize::Zeroizing; + +use crate::password_set; +use crate::password_set::ask_for_password; +use crate::wallet_set::ask_for_private_key; + +#[instrument(name = "app", skip_all)] +pub async fn execute( + rpc_url: Option, + password: Option>, + private_key: Option>, +) -> Result<()> { + let pw = ask_for_password(password)?; + let rpc_url = match rpc_url { + Some(url) => { + setup::validate_rpc_url(&url)?; + url + } + None => Input::::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter WebSocket devnet RPC URL") + .default("wss://ethereum-sepolia-rpc.publicnode.com".to_string()) + .validate_with(setup::validate_rpc_url) + .interact_text()?, + }; + + let private_key = ask_for_private_key(private_key)?; + let default_config_dir = dirs::config_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))? + .join("enclave"); + + let config_dir: PathBuf = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter config directory") + .default(default_config_dir.display().to_string()) + .validate_with(|input: &String| -> Result<(), &str> { + let path = PathBuf::from(input); + if input.is_empty() { + Err("Path cannot be empty") + } else if path.is_file() { + Err("Path is a file, not a directory") + } else { + Ok(()) + } + }) + .interact_text()? + .into(); + + // Execute + let config = setup::execute(&rpc_url, &config_dir)?; + + e3_entrypoint::password::set::preflight(&config).await?; + e3_entrypoint::password::set::execute(&config, pw).await?; + + let (address, peer_id) = e3_entrypoint::wallet::set::execute(&config, private_key).await?; + print_info(&config, address, &peer_id.to_string(), &rpc_url)?; + Ok(()) +} + +fn print_info(config: &AppConfig, address: Address, peer_id: &str, rpc_url: &str) -> Result<()> { + let abs_config = config.config_file().canonicalize()?; + + println!("\nEnclave configuration successfully created!"); + println!( + "Editable configuration has been written to:\n\n {}", + colorize(abs_config.to_string_lossy(), Color::Yellow) + ); + println!(""); + println!("Data written:"); + println!(" address: {}", colorize(address, Color::Cyan)); + println!(" peer_id: {}", colorize(peer_id, Color::Cyan)); + println!(" rpc_url: {}", colorize(rpc_url, Color::Cyan)); + println!(""); + if config.using_custom_config() { + println!( + "Run future commands from within this directory tree, or pass\n {}\n", + colorize( + format!("--config {}", abs_config.to_string_lossy()), + Color::Yellow + ) + ); + } + println!( + "You can start your node using:\n `{}`\n", + colorize("enclave start", Color::Yellow) + ); + Ok(()) +} diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index e9d25c0dee..1a43d45e79 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -8,13 +8,13 @@ use std::path::PathBuf; use crate::ciphernode::{self, CiphernodeCommands}; use crate::helpers::telemetry::{setup_simple_tracing, setup_tracing}; -use crate::net::NetCommands; +use crate::net::{self, NetCommands}; use crate::nodes::{self, NodeCommands}; use crate::noir::NoirCommands; use crate::password::PasswordCommands; use crate::program::{self, ProgramCommands}; use crate::wallet::WalletCommands; -use crate::{config_set, init, net, noir, password, purge_all, rev, wallet}; +use crate::{init, noir, password, purge_all, rev, wallet}; use crate::{print_env, start}; use anyhow::{bail, Result}; use clap::{command, ArgAction, Parser, Subcommand}; @@ -99,34 +99,26 @@ impl Cli { setup_simple_tracing(log_level); init::execute(path, template, skip_cleanup, self.verbose > 0).await? }, - Commands::ConfigSet { - rpc_url, - eth_address, - password, - skip_eth, - net_keypair, - generate_net_keypair, + Commands::Ciphernode { + command: CiphernodeCommands::Setup { + rpc_url, + password, + private_key, + } } => { - config_set::execute( + ciphernode::setup::execute( rpc_url, - eth_address, password, - skip_eth, - net_keypair, - generate_net_keypair, + private_key, ) .await?; - println!("You can start your node using `enclave start`"); } Commands::Start { .. } => { println!("No configuration found. Setting up enclave configuration..."); - config_set::execute( + ciphernode::setup::execute( None, None, None, - false, - None, - false, ) .await?; }, @@ -135,7 +127,7 @@ impl Cli { noir::execute_without_config(command).await? }, _ => bail!( - "Configuration file not found. Run `enclave config-set` to create a configuration." + "Configuration file not found. Run `enclave ciphernode setup` to create a configuration." ), }; return Ok(()); @@ -151,10 +143,6 @@ impl Cli { e3_entrypoint::password::set::autopassword(&config).await?; } - if config.autonetkey() { - e3_entrypoint::net::keypair::generate::autonetkey(&config).await?; - } - if config.autowallet() { e3_entrypoint::wallet::set::autowallet(&config).await?; } @@ -172,9 +160,6 @@ impl Cli { Commands::PurgeAll => { purge_all::execute().await?; } - Commands::ConfigSet { .. } => { - bail!("Cannot run `enclave config-set` when a configuration already exists."); - } Commands::Nodes { command } => { nodes::execute( command, @@ -188,8 +173,8 @@ impl Cli { Commands::Password { command } => password::execute(command, &config).await?, Commands::Wallet { command } => wallet::execute(command, config).await?, Commands::Ciphernode { command } => ciphernode::execute(command, &config).await?, - Commands::Net { command } => net::execute(command, &config).await?, Commands::Noir { command } => noir::execute(command, &config).await?, + Commands::Net { command } => net::execute(command, &config).await?, Commands::Rev => rev::execute().await?, } @@ -283,12 +268,6 @@ pub enum Commands { command: WalletCommands, }, - /// Networking related commands - Net { - #[command(subcommand)] - command: NetCommands, - }, - /// Noir prover management and proof generation Noir { #[command(subcommand)] @@ -301,36 +280,15 @@ pub enum Commands { command: CiphernodeCommands, }, - /// Set configuration values (similar to solana config set) - ConfigSet { - /// An rpc url for enclave to connect to - #[arg(long = "rpc-url", short = 'r')] - rpc_url: Option, - - /// An Ethereum address that enclave should use to identify the node - #[arg(long = "eth-address", short = 'e')] - eth_address: Option, - - /// The password - #[arg(short, long)] - password: Option, - - /// Skip asking for eth - #[arg(long = "skip-eth", short = 's')] - skip_eth: bool, - - /// The network private key (ed25519) - #[arg(long = "net-keypair", short = 'n')] - net_keypair: Option, - - /// Generate a new network keypair - #[arg(long = "generate-net-keypair", short = 'g')] - generate_net_keypair: bool, - }, - /// Manage multiple node processes together as a set Nodes { #[command(subcommand)] command: NodeCommands, }, + + /// Manage net configuration + Net { + #[command(subcommand)] + command: NetCommands, + }, } diff --git a/crates/cli/src/config_set.rs b/crates/cli/src/config_set.rs deleted file mode 100644 index cff843e1bb..0000000000 --- a/crates/cli/src/config_set.rs +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use anyhow::Result; -use dialoguer::{theme::ColorfulTheme, Input}; -use e3_entrypoint::config_set; -use tracing::instrument; - -use crate::net; -use crate::net::{NetCommands, NetKeypairCommands}; -use crate::password; -use crate::password::PasswordCommands; - -#[instrument(name = "app", skip_all)] -pub async fn execute( - rpc_url: Option, - eth_address: Option, - password: Option, - skip_eth: bool, - net_keypair: Option, - generate_net_keypair: bool, -) -> Result<()> { - let rpc_url = match rpc_url { - Some(url) => { - config_set::validate_rpc_url(&url)?; - url - } - None => Input::::new() - .with_prompt("Enter WebSocket devnet RPC URL") - .default("wss://ethereum-sepolia-rpc.publicnode.com".to_string()) - .validate_with(config_set::validate_rpc_url) - .interact_text()?, - }; - - let eth_address: Option = match eth_address { - Some(address) => { - config_set::validate_eth_address(&address)?; - Some(address) - } - None => { - if skip_eth { - None - } else { - Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your Ethereum address (press Enter to skip)") - .allow_empty(true) - .validate_with(config_set::validate_eth_address) - .interact() - .ok() - .map(|s| if s.is_empty() { None } else { Some(s) }) - .flatten() - } - } - }; - - let config = config_set::execute(rpc_url, eth_address).await?; - - password::execute(PasswordCommands::Set { password }, &config).await?; - - if generate_net_keypair { - net::execute( - NetCommands::Keypair { - command: NetKeypairCommands::Generate, - }, - &config, - ) - .await?; - } else { - net::execute( - NetCommands::Keypair { - command: NetKeypairCommands::Set { net_keypair }, - }, - &config, - ) - .await?; - } - - println!("Enclave configuration successfully created!"); - - Ok(()) -} diff --git a/crates/cli/src/helpers/mod.rs b/crates/cli/src/helpers/mod.rs index 89a9cce809..7b4b8bd22e 100644 --- a/crates/cli/src/helpers/mod.rs +++ b/crates/cli/src/helpers/mod.rs @@ -4,6 +4,31 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +use anyhow::{bail, Result}; +use zeroize::{Zeroize, Zeroizing}; + pub mod compile_id; pub mod prompt_password; pub mod telemetry; + +/// Parse to a Zeroizing String +pub fn parse_zeroizing(s: &str) -> Result> { + Ok(Zeroizing::new(s.to_string())) +} + +/// Ensure hex is of the form 0x12435687abcdef... +pub fn ensure_hex_zeroizing(s: &str) -> Result> { + Ok(parse_zeroizing(ensure_hex(s)?)?) +} + +/// Ensure a hexadecimal number +fn ensure_hex(s: &str) -> Result<&str> { + if !s.starts_with("0x") { + bail!("hex value must start with '0x'") + } + if !s[2..].chars().all(|c| c.is_ascii_hexdigit()) { + bail!("private key must only contain hex characters [0-9a-fA-F]"); + } + hex::decode(&s[2..])?.zeroize(); + Ok(s) +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index e5a09ba751..ca271a26cd 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -6,17 +6,15 @@ use clap::Parser; use cli::Cli; +use e3_utils::{colorize, Color}; use tracing::info; mod ciphernode; mod cli; -mod config_set; pub mod helpers; mod init; -pub mod net; -mod net_generate; -mod net_purge; -mod net_set; +mod net; +mod net_get_peer_id; mod nodes; mod nodes_daemon; mod nodes_down; @@ -37,6 +35,7 @@ mod purge_all; mod rev; mod start; mod wallet; +mod wallet_get; mod wallet_set; const OWO: &str = r#" @@ -65,7 +64,7 @@ pub async fn main() { // Execute the cli if let Err(err) = Cli::parse().execute().await { - eprintln!("{}", err); + eprintln!("{}", colorize(err, Color::Red)); std::process::exit(1); } } diff --git a/crates/cli/src/net.rs b/crates/cli/src/net.rs index ee6029e583..bcb54d0e6c 100644 --- a/crates/cli/src/net.rs +++ b/crates/cli/src/net.rs @@ -8,53 +8,17 @@ use anyhow::*; use clap::Subcommand; use e3_config::AppConfig; -use crate::{net_generate, net_purge, net_set}; +use crate::net_get_peer_id; #[derive(Subcommand, Debug)] pub enum NetCommands { - /// Generate new net keypair - Keypair { - #[command(subcommand)] - command: NetKeypairCommands, - }, - - /// Purge peer ID - #[command(name = "peer-id")] - PeerId { - #[command(subcommand)] - command: NetPeerIdCommands, - }, -} - -#[derive(Subcommand, Debug)] -pub enum NetKeypairCommands { - /// Generate new net keypair - Generate, - - /// Set net private key - Set { - #[arg(long = "net-keypair")] - net_keypair: Option, - }, -} - -#[derive(Subcommand, Debug)] -pub enum NetPeerIdCommands { - /// Purge peer ID - Purge, + /// Get the ciphernode's libp2p PeerId + GetPeerId, } pub async fn execute(command: NetCommands, config: &AppConfig) -> Result<()> { match command { - NetCommands::Keypair { command } => match command { - NetKeypairCommands::Generate => net_generate::execute(&config).await?, - NetKeypairCommands::Set { net_keypair } => { - net_set::execute(&config, net_keypair).await? - } - }, - NetCommands::PeerId { command } => match command { - NetPeerIdCommands::Purge => net_purge::execute(&config).await?, - }, + NetCommands::GetPeerId => net_get_peer_id::execute(config).await?, }; Ok(()) diff --git a/crates/cli/src/net_generate.rs b/crates/cli/src/net_generate.rs deleted file mode 100644 index 5cfac6f8ca..0000000000 --- a/crates/cli/src/net_generate.rs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use anyhow::Result; -use e3_config::AppConfig; -use e3_entrypoint::net; - -pub async fn execute(config: &AppConfig) -> Result<()> { - let peer_id = net::keypair::generate::execute(config).await?; - println!("Generated new keypair with peer ID: {}", peer_id); - println!("Network keypair has been successfully generated and encrypted."); - Ok(()) -} diff --git a/crates/cli/src/net_purge.rs b/crates/cli/src/net_get_peer_id.rs similarity index 61% rename from crates/cli/src/net_purge.rs rename to crates/cli/src/net_get_peer_id.rs index 8e6622c0ca..be7a397208 100644 --- a/crates/cli/src/net_purge.rs +++ b/crates/cli/src/net_get_peer_id.rs @@ -4,12 +4,11 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use anyhow::*; +use anyhow::Result; use e3_config::AppConfig; -use e3_entrypoint::net; pub async fn execute(config: &AppConfig) -> Result<()> { - net::peer_id::purge::execute(config).await?; - println!("Peer ID has been purged. A new Peer ID will be generated upon restart."); + let peer_id = e3_entrypoint::net::get_peer_id::execute(config).await?; + println!("{}", peer_id); Ok(()) } diff --git a/crates/cli/src/net_set.rs b/crates/cli/src/net_set.rs deleted file mode 100644 index c80930d9a9..0000000000 --- a/crates/cli/src/net_set.rs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use anyhow::Result; -use dialoguer::{theme::ColorfulTheme, Password}; -use e3_config::AppConfig; -use e3_entrypoint::net::{self, keypair::set::validate_keypair_input}; - -pub async fn execute(config: &AppConfig, net_keypair: Option) -> Result<()> { - let input = if let Some(nkp) = net_keypair { - nkp - } else { - Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your network private key") - .validate_with(validate_keypair_input) - .interact()? - .trim() - .to_string() - }; - - net::keypair::set::execute(config, input).await?; - - println!("Network keypair has been successfully stored and encrypted."); - - Ok(()) -} diff --git a/crates/cli/src/password.rs b/crates/cli/src/password.rs index 4587d77973..4cc08b9553 100644 --- a/crates/cli/src/password.rs +++ b/crates/cli/src/password.rs @@ -7,16 +7,17 @@ use anyhow::*; use clap::Subcommand; use e3_config::AppConfig; +use zeroize::Zeroizing; -use crate::{password_delete, password_set}; +use crate::{helpers::parse_zeroizing, password_delete, password_set}; #[derive(Subcommand, Debug)] pub enum PasswordCommands { /// Set (or overwrite) a password Set { /// The new password - #[arg(short, long)] - password: Option, + #[arg(short, long, value_parser = parse_zeroizing)] + password: Option>, }, /// Delete the current password diff --git a/crates/cli/src/password_set.rs b/crates/cli/src/password_set.rs index 092f8ce66f..5b302098eb 100644 --- a/crates/cli/src/password_set.rs +++ b/crates/cli/src/password_set.rs @@ -10,14 +10,12 @@ use zeroize::{Zeroize, Zeroizing}; use crate::helpers::prompt_password::prompt_password; -fn get_zeroizing_pw_vec(input: Option) -> Result>> { - if let Some(mut pw_str) = input { +pub fn ask_for_password(input: Option>) -> Result> { + if let Some(pw_str) = input { if pw_str.trim().is_empty() { bail!("Password must not be blank") } - let pw = Zeroizing::new(pw_str.trim().as_bytes().to_owned()); - pw_str.zeroize(); - return Ok(pw); + return Ok(pw_str); } // First password entry @@ -37,20 +35,19 @@ fn get_zeroizing_pw_vec(input: Option) -> Result>> { bail!("Passwords do not match") } - let pw = Zeroizing::new(pw_str.trim().as_bytes().to_owned()); - // Clean up sensitive data - pw_str.zeroize(); confirm_pw_str.zeroize(); + let trimmed = Zeroizing::new(pw_str.trim().to_owned()); + pw_str.zeroize(); - Ok(pw) + Ok(trimmed) } -pub async fn execute(config: &AppConfig, input: Option) -> Result<()> { +pub async fn execute(config: &AppConfig, input: Option>) -> Result<()> { println!("Setting password..."); e3_entrypoint::password::set::preflight(config).await?; - let pw = get_zeroizing_pw_vec(input)?; + let pw = ask_for_password(input)?; e3_entrypoint::password::set::execute(config, pw).await?; diff --git a/crates/cli/src/start.rs b/crates/cli/src/start.rs index 2150cd4e2c..2eada86c1e 100644 --- a/crates/cli/src/start.rs +++ b/crates/cli/src/start.rs @@ -14,10 +14,6 @@ use tracing::{info, instrument}; pub async fn execute(mut config: AppConfig, peers: Vec) -> Result<()> { owo(); - let Some(address) = config.address() else { - return Err(anyhow!("You must provide an address")); - }; - // add cli peers to the config config.add_peers(peers); @@ -29,7 +25,6 @@ pub async fn execute(mut config: AppConfig, peers: Vec) -> Result<()> { } => { e3_entrypoint::start::aggregator_start::execute( &config, - address, pubkey_write_path, plaintext_write_path, ) @@ -37,13 +32,13 @@ pub async fn execute(mut config: AppConfig, peers: Vec) -> Result<()> { } // Launch in ciphernode configuration - NodeRole::Ciphernode => e3_entrypoint::start::start::execute(&config, address).await?, + NodeRole::Ciphernode => e3_entrypoint::start::start::execute(&config).await?, }; info!( "LAUNCHING CIPHERNODE: ({}/{}/{})", config.name(), - address, + node.address, node.peer_id ); diff --git a/crates/cli/src/wallet.rs b/crates/cli/src/wallet.rs index 3df58d37e7..36225b6ebc 100644 --- a/crates/cli/src/wallet.rs +++ b/crates/cli/src/wallet.rs @@ -7,8 +7,9 @@ use anyhow::*; use clap::Subcommand; use e3_config::AppConfig; +use zeroize::Zeroizing; -use crate::wallet_set; +use crate::{helpers::ensure_hex_zeroizing, wallet_get, wallet_set}; #[derive(Subcommand, Debug)] pub enum WalletCommands { @@ -16,22 +17,17 @@ pub enum WalletCommands { Set { /// The private key - note we are leaving as hex string as it is easier to manage with /// the allow Signer coercion - #[arg(long = "private-key", value_parser = ensure_hex)] - private_key: Option, + #[arg(long = "private-key", value_parser = ensure_hex_zeroizing)] + private_key: Option>, }, -} - -fn ensure_hex(s: &str) -> Result { - if !s.starts_with("0x") { - bail!("hex value must start with '0x'") - } - hex::decode(&s[2..])?; - Ok(s.to_string()) + /// Get your wallet address + Get, } pub async fn execute(command: WalletCommands, config: AppConfig) -> Result<()> { match command { WalletCommands::Set { private_key } => wallet_set::execute(&config, private_key).await?, + WalletCommands::Get => wallet_get::execute(&config).await?, }; Ok(()) diff --git a/crates/entrypoint/src/net/peer_id/purge.rs b/crates/cli/src/wallet_get.rs similarity index 59% rename from crates/entrypoint/src/net/peer_id/purge.rs rename to crates/cli/src/wallet_get.rs index e025591236..383ae256f1 100644 --- a/crates/entrypoint/src/net/peer_id/purge.rs +++ b/crates/cli/src/wallet_get.rs @@ -4,13 +4,12 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::helpers::datastore::get_repositories; -use anyhow::*; +use anyhow::Result; use e3_config::AppConfig; -use e3_net::NetRepositoryFactory; pub async fn execute(config: &AppConfig) -> Result<()> { - let repositories = get_repositories(config)?; - repositories.libp2p_keypair().clear(); + let address = e3_entrypoint::wallet::get::execute(config).await?; + println!("{}", address); + Ok(()) } diff --git a/crates/cli/src/wallet_set.rs b/crates/cli/src/wallet_set.rs index 5d4b64158e..27b38d2964 100644 --- a/crates/cli/src/wallet_set.rs +++ b/crates/cli/src/wallet_set.rs @@ -8,23 +8,30 @@ use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Password}; use e3_config::AppConfig; use e3_entrypoint::wallet::set::validate_private_key; +use zeroize::Zeroizing; -pub async fn execute(config: &AppConfig, private_key: Option) -> Result<()> { - let input = if let Some(private_key) = private_key { - validate_private_key(&private_key)?; - private_key +pub fn ask_for_private_key(given_key: Option>) -> Result> { + let key = if let Some(given_key) = given_key { + validate_private_key(&given_key)?; + given_key } else { - Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your Ethereum private key") - .validate_with(validate_private_key) - .interact()? - .trim() - .to_string() + Zeroizing::new( + Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your Ethereum private key (0x...)") + .validate_with(validate_private_key) + .interact()? + .trim() + .to_string(), + ) }; - e3_entrypoint::wallet::set::execute(config, input).await?; + Ok(key) +} - println!("WalletKey key has been successfully stored and encrypted."); +pub async fn execute(config: &AppConfig, private_key: Option>) -> Result<()> { + let input = ask_for_private_key(private_key)?; + e3_entrypoint::wallet::set::execute(config, input).await?; + println!("Wallet key has been successfully stored and encrypted."); Ok(()) } diff --git a/crates/config/src/app_config.rs b/crates/config/src/app_config.rs index 9b254c73da..a5f7f27c8f 100644 --- a/crates/config/src/app_config.rs +++ b/crates/config/src/app_config.rs @@ -166,6 +166,8 @@ pub struct AppConfig { peers: Vec, /// Store all paths in the paths engine paths: PathsEngine, + /// The config yaml path + config_yaml: PathBuf, /// Set the Open Telemetry collector grpc endpoint. Eg. 127.0.0.1:4317 otel: Option, /// If a net key has not been set autogenerate one on start @@ -243,13 +245,14 @@ impl AppConfig { Some(&node.log_file), config.custom_bb.as_ref(), ); - + let found_config_file = config.found_config_file.clone(); Ok(AppConfig { name: name.to_owned(), nodes: config.nodes, chains: config.chains, peers: vec![], paths, + config_yaml: found_config_file.clone().unwrap_or_default(), otel: config.otel, autopassword: node.autopassword, autowallet: node.autowallet, @@ -289,6 +292,11 @@ impl AppConfig { } } + /// Whether the config is changed from the default + pub fn using_custom_config(&self) -> bool { + !self.paths.is_default_config_file() + } + /// Get the circuits directory pub fn circuits_dir(&self) -> PathBuf { self.paths.circuits_dir() @@ -332,6 +340,11 @@ impl AppConfig { self.paths.config_file() } + /// Get the config yaml path + pub fn config_yaml(&self) -> PathBuf { + self.config_yaml.clone() + } + /// Get the chains config pub fn chains(&self) -> &Vec { &self.chains @@ -407,7 +420,7 @@ pub struct UnscopedAppConfig { data_dir: Option, /// The config file as found before initialization this is for testing purposes and you should /// not use this in your configurations - found_config_file: Option, + found_config_file: Option, // This is set regardless as the file is resolved /// The default node that runs during commands like `enclave start` without supplying the /// `--name` argument. node: NodeDefinition, @@ -474,6 +487,7 @@ impl UnscopedAppConfig { struct CliOverrides { pub otel: Option, pub found_config_file: Option, + pub using_custom_config: bool, } /// Load the config at the config_file or the default location if not provided @@ -483,7 +497,6 @@ pub fn load_config( otel: Option, ) -> Result { let found_config_file = found_config_file.map(PathBuf::from); - let resolved_config_path = resolve_config_path( find_in_parent, // finding strategy env::current_dir()?, // cwd @@ -502,6 +515,7 @@ pub fn load_config( .merge(Serialized::defaults(&CliOverrides { otel, found_config_file: Some(resolved_config_path), + using_custom_config: found_config_file.is_some(), })) .extract() .context("Could not parse configuration")?; diff --git a/crates/config/src/paths_engine.rs b/crates/config/src/paths_engine.rs index bed99573e6..93efd8bf02 100644 --- a/crates/config/src/paths_engine.rs +++ b/crates/config/src/paths_engine.rs @@ -86,6 +86,12 @@ impl PathsEngine { clean(self.default_config_dir.join(DEFAULT_CONFIG_NAME)) } + /// Returns true if the config file is the default config file (i.e., not a custom path) + pub fn is_default_config_file(&self) -> bool { + let default_path = clean(self.default_config_dir.join(DEFAULT_CONFIG_NAME)); + self.config_file() == default_path + } + /// Full path to the key file containing secret key pub fn key_file(&self) -> PathBuf { if let Some(key_file) = self.key_file_override.clone() { @@ -600,4 +606,79 @@ mod test { }, ]); } + + #[test] + fn test_is_default_config_file() { + let default_data_dir = PathBuf::from("/home/user/.local/share/enclave"); + let default_config_dir = PathBuf::from("/home/user/.config/enclave"); + let cwd = PathBuf::from("/no/matter"); + + // Test case 1: No found_config_file - should be default + let paths = PathsEngine::new( + "test", + &cwd, + &default_data_dir, + &default_config_dir, + None, + None, + None, + None, + None, + None, + None, + ); + assert!(paths.is_default_config_file()); + + // Test case 2: found_config_file set to different path - not default + let paths = PathsEngine::new( + "test", + &cwd, + &default_data_dir, + &default_config_dir, + Some(&PathBuf::from("/foo/some.config.yaml")), + None, + None, + None, + None, + None, + None, + ); + assert!(!paths.is_default_config_file()); + + // Test case 3: found_config_file set to exact default path - should be default + let paths = PathsEngine::new( + "test", + &cwd, + &default_data_dir, + &default_config_dir, + Some(&PathBuf::from( + "/home/user/.config/enclave/enclave.config.yaml", + )), + None, + None, + None, + None, + None, + None, + ); + assert!(paths.is_default_config_file()); + + // Test case 4: found_config_file set to same path with different representation - should be default + let paths = PathsEngine::new( + "test", + &cwd, + &default_data_dir, + &default_config_dir, + Some(&PathBuf::from( + "/home/user/.config/enclave/./enclave.config.yaml", + )), + None, + None, + None, + None, + None, + None, + ); + assert!(paths.is_default_config_file()); + } } diff --git a/crates/data/src/data_store.rs b/crates/data/src/data_store.rs index 64e8ea4deb..49578a335f 100644 --- a/crates/data/src/data_store.rs +++ b/crates/data/src/data_store.rs @@ -12,7 +12,7 @@ use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use e3_events::IntoKey; -use e3_events::{Get, Insert, InsertSync, Remove}; +use e3_events::{Flush, Get, Insert, InsertSync, Remove}; use serde::{Deserialize, Serialize}; use tracing::error; @@ -41,6 +41,7 @@ pub struct DataStore { insert: Recipient, insert_sync: Recipient, remove: Recipient, + flush: Recipient, } impl DataStore { @@ -57,7 +58,6 @@ impl DataStore { if bytes == [0] { return Ok(None); } - Ok(Some(bincode::deserialize(&bytes)?)) } @@ -82,6 +82,7 @@ impl DataStore { let msg = InsertSync::new(&self.scope, serialized); self.insert_sync.send(msg).await??; + self.flush.send(Flush).await?; // Write sync will flush all pending writes Ok(()) } @@ -158,6 +159,7 @@ impl DataStore { insert_sync: self.insert_sync.clone(), remove: self.remove.clone(), scope, + flush: self.flush.clone(), } } @@ -169,6 +171,7 @@ impl DataStore { insert_sync: self.insert_sync.clone(), remove: self.remove.clone(), scope: key.into_key(), + flush: self.flush.clone(), } } @@ -184,6 +187,7 @@ impl DataStore { insert_sync: addr.clone().recipient(), remove: addr.clone().recipient(), scope: vec![], + flush: addr.clone().recipient(), } } @@ -198,6 +202,7 @@ impl DataStore { insert_sync: addr.clone().recipient(), remove: addr.clone().recipient(), scope: vec![], + flush: addr.clone().recipient(), } } @@ -209,6 +214,7 @@ impl DataStore { insert_sync: addr.clone().recipient(), remove: addr.clone().recipient(), scope: vec![], + flush: addr.clone().recipient(), } } @@ -220,6 +226,7 @@ impl DataStore { insert_sync: addr.clone().recipient(), remove: addr.clone().recipient(), scope: vec![], + flush: addr.clone().recipient(), } } } @@ -233,6 +240,7 @@ impl From<&Addr> for DataStore { insert_sync: addr.clone().recipient(), remove: addr.clone().recipient(), scope: vec![], + flush: addr.clone().recipient(), } } } @@ -246,6 +254,7 @@ impl From<&Addr> for DataStore { insert_sync: addr.clone().recipient(), remove: addr.clone().recipient(), scope: vec![], + flush: addr.clone().recipient(), } } } diff --git a/crates/data/src/in_mem.rs b/crates/data/src/in_mem.rs index c689acd9fb..6f0f2d894c 100644 --- a/crates/data/src/in_mem.rs +++ b/crates/data/src/in_mem.rs @@ -6,10 +6,9 @@ use actix::{Actor, Handler, Message}; use anyhow::{Context, Result}; -use e3_events::{Get, Insert, InsertBatch, InsertSync, Remove}; +use e3_events::{Flush, Get, Insert, InsertBatch, InsertSync, Remove}; use e3_utils::MAILBOX_LIMIT; use std::collections::BTreeMap; -use tracing::info; #[derive(Message, Clone, Debug, PartialEq, Eq, Hash)] #[rtype(result = "Vec")] @@ -121,6 +120,13 @@ impl Handler for InMemStore { } } +impl Handler for InMemStore { + type Result = (); + fn handle(&mut self, _: Flush, _: &mut Self::Context) -> Self::Result { + // noop + } +} + impl Handler for InMemStore { type Result = Vec; fn handle(&mut self, _: GetLog, _: &mut Self::Context) -> Vec { diff --git a/crates/data/src/sled_db.rs b/crates/data/src/sled_db.rs index 72e508d425..e5bb993789 100644 --- a/crates/data/src/sled_db.rs +++ b/crates/data/src/sled_db.rs @@ -58,9 +58,13 @@ impl SledDb { .db .get(key) .context(format!("Failed to fetch {}", str_key))?; - Ok(res.map(|v| v.to_vec())) } + + pub fn flush(&self) -> Result<()> { + self.db.flush()?; + Ok(()) + } } #[cfg(test)] diff --git a/crates/data/src/sled_store.rs b/crates/data/src/sled_store.rs index 12cc3eeb0b..5acb4566fd 100644 --- a/crates/data/src/sled_store.rs +++ b/crates/data/src/sled_store.rs @@ -7,15 +7,18 @@ use crate::SledDb; use actix::{Actor, ActorContext, Addr, Handler}; use anyhow::Result; -use e3_events::{prelude::*, BusHandle, EType, EnclaveEvent, EnclaveEventData, EventType}; +use e3_events::{ + prelude::*, BusHandle, EType, EnclaveEvent, EnclaveEventData, + EnclaveUnsequencedErrorDispatcher, EventType, Flush, +}; use e3_events::{Get, Insert, InsertBatch, InsertSync, Remove}; -use e3_utils::MAILBOX_LIMIT; +use e3_utils::{NotifySync, MAILBOX_LIMIT}; use std::path::PathBuf; use tracing::{error, info}; pub struct SledStore { db: Option, - bus: BusHandle, // Only used for Shutdown + bus: Box, // TODO: fix to work with trap } impl Actor for SledStore { @@ -26,13 +29,18 @@ impl Actor for SledStore { } impl SledStore { - pub fn new(bus: &BusHandle, path: &PathBuf) -> Result> { + pub fn new(bus: &BusHandle, path: &PathBuf) -> Result> { + // Note we pass in a generic BusHandle which supports the err method for passing on errors. + // This was as stores are required before we can initialize the BusHandle to retrieve the + // address so we have a unique node_id. + // If BusHandle is Disabled that is fine as our subscriptions and error publishing function + // remains intact despite it being enabled elsewhere at a later point info!("Starting SledStore with {:?}", path); let db = SledDb::new(path, "datastore")?; let store = Self { db: Some(db), - bus: bus.clone(), + bus: Box::new(bus.clone()), } .start(); @@ -111,10 +119,24 @@ impl Handler for SledStore { } } } + +impl Handler for SledStore { + type Result = (); + fn handle(&mut self, _: Flush, _: &mut Self::Context) -> Self::Result { + if let Some(ref db) = self.db { + match db.flush() { + Err(err) => self.bus.err(EType::Data, err), + _ => (), + } + } + } +} + impl Handler for SledStore { type Result = (); fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { if let EnclaveEventData::Shutdown(_) = msg.get_data() { + self.notify_sync(ctx, Flush); // Flush all pending writes let _db = self.db.take(); // db will be dropped ctx.stop() } diff --git a/crates/entrypoint/Cargo.toml b/crates/entrypoint/Cargo.toml index 9770304087..947ecdba46 100644 --- a/crates/entrypoint/Cargo.toml +++ b/crates/entrypoint/Cargo.toml @@ -35,6 +35,7 @@ e3-request = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } e3-sortition = { workspace = true } +serde_yaml = { workspace = true } e3-test-helpers = { workspace = true } e3-zk-prover = { workspace = true } tokio = { workspace = true } diff --git a/crates/entrypoint/src/net/peer_id/mod.rs b/crates/entrypoint/src/config/mod.rs similarity index 92% rename from crates/entrypoint/src/net/peer_id/mod.rs rename to crates/entrypoint/src/config/mod.rs index 66ca85f79f..598fd7416f 100644 --- a/crates/entrypoint/src/net/peer_id/mod.rs +++ b/crates/entrypoint/src/config/mod.rs @@ -4,4 +4,4 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -pub mod purge; +pub mod setup; diff --git a/crates/entrypoint/src/config_set/mod.rs b/crates/entrypoint/src/config/setup.rs similarity index 85% rename from crates/entrypoint/src/config_set/mod.rs rename to crates/entrypoint/src/config/setup.rs index a2ba685379..b8764ff05f 100644 --- a/crates/entrypoint/src/config_set/mod.rs +++ b/crates/entrypoint/src/config/setup.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: LGPL-3.0-only // // This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY +// without even even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. use alloy::primitives::Address; @@ -10,6 +10,7 @@ use e3_config::load_config; use e3_config::AppConfig; use e3_config::RPC; use std::fs; +use std::path::PathBuf; use tracing::instrument; // Import a built file: @@ -37,11 +38,7 @@ pub fn validate_eth_address(address: &String) -> Result<()> { } #[instrument(name = "app", skip_all)] -pub async fn execute(rpc_url: String, eth_address: Option) -> Result { - let config_dir = dirs::config_dir() - .ok_or_else(|| anyhow!("Could not determine home directory"))? - .join("enclave"); - +pub fn execute(rpc_url: &str, config_dir: &PathBuf) -> Result { fs::create_dir_all(&config_dir)?; let config_path = config_dir.join("enclave.config.yaml"); @@ -49,7 +46,6 @@ pub async fn execute(rpc_url: String, eth_address: Option) -> Result Result { +pub fn get_sled_store(bus: &BusHandle, db_file: &PathBuf) -> Result { Ok((&SledStore::new(bus, db_file)?).into()) } @@ -22,7 +22,7 @@ pub fn get_in_mem_store() -> DataStore { (&InMemStore::new(true).start()).into() } -pub fn setup_datastore(config: &AppConfig, bus: &BusHandle) -> Result { +pub fn setup_datastore(config: &AppConfig, bus: &BusHandle) -> Result { let store: DataStore = if !config.use_in_mem_store() { get_sled_store(&bus, &config.db_file())? } else { @@ -32,7 +32,7 @@ pub fn setup_datastore(config: &AppConfig, bus: &BusHandle) -> Result } pub fn get_repositories(config: &AppConfig) -> Result { - let bus = get_enclave_bus_handle(config)?; + let bus = get_enclave_bus_handle()?; let store = setup_datastore(config, &bus)?; Ok(store.repositories()) } diff --git a/crates/entrypoint/src/lib.rs b/crates/entrypoint/src/lib.rs index fa63bc9100..7fbfbb0247 100644 --- a/crates/entrypoint/src/lib.rs +++ b/crates/entrypoint/src/lib.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -pub mod config_set; +pub mod config; pub mod helpers; pub mod net; pub mod nodes; diff --git a/crates/entrypoint/src/net/get_peer_id.rs b/crates/entrypoint/src/net/get_peer_id.rs new file mode 100644 index 0000000000..4cb81fd12c --- /dev/null +++ b/crates/entrypoint/src/net/get_peer_id.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::helpers::datastore::get_repositories; +use anyhow::Context; +use anyhow::Result; +use e3_config::AppConfig; +use e3_crypto::Cipher; +use e3_net::NetRepositoryFactory; +use libp2p::identity::ed25519; +use libp2p::PeerId; +use zeroize::Zeroizing; + +pub async fn execute(config: &AppConfig) -> Result { + let repositories = get_repositories(config)?; + let cipher = Cipher::from_file(config.key_file()).await?; + let encrypted = repositories + .libp2p_keypair() + .read() + .await? + .context("No wallet has been set.")?; + let mut bytes = Zeroizing::new(cipher.decrypt_data(&encrypted)?); + let keypair: libp2p::identity::Keypair = + ed25519::Keypair::try_from_bytes(&mut bytes)?.try_into()?; + let peer_id = PeerId::from(keypair.public()); + Ok(peer_id) +} diff --git a/crates/entrypoint/src/net/keypair/generate.rs b/crates/entrypoint/src/net/keypair/generate.rs deleted file mode 100644 index 512cfbcb3f..0000000000 --- a/crates/entrypoint/src/net/keypair/generate.rs +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use anyhow::Result; -use e3_config::AppConfig; -use e3_crypto::Cipher; -use e3_net::NetRepositoryFactory; -use libp2p::{identity::Keypair, PeerId}; -use tracing::warn; -use zeroize::Zeroize; - -use crate::helpers::datastore::get_repositories; - -pub async fn execute(config: &AppConfig) -> Result { - let kp = Keypair::generate_ed25519(); - let peer_id = kp.public().to_peer_id(); - let mut bytes = kp.try_into_ed25519()?.to_bytes().to_vec(); - let cipher = Cipher::from_file(config.key_file()).await?; - let encrypted = cipher.encrypt_data(&mut bytes.clone())?; - let repositories = get_repositories(config)?; - bytes.zeroize(); - repositories.libp2p_keypair().write_sync(&encrypted).await?; - - Ok(peer_id) -} - -pub async fn autonetkey(config: &AppConfig) -> Result<()> { - let repositories = get_repositories(config)?; - if !repositories.libp2p_keypair().has().await { - warn!( - "Auto-generating network keypair because 'autonetkey: true' is set and no keypair exists. \ - This will create a NEW peer identity. If your data directory is not persistent \ - (e.g., running in Docker without volumes), a new identity will be generated on each restart, \ - which will cause network connectivity issues with other peers. \ - For production use, run 'enclave net keypair (generate)/(set --net-keypair )' once and ensure data persistence." - ); - execute(config).await?; - } - Ok(()) -} diff --git a/crates/entrypoint/src/net/keypair/mod.rs b/crates/entrypoint/src/net/keypair/mod.rs deleted file mode 100644 index 82736a3458..0000000000 --- a/crates/entrypoint/src/net/keypair/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -pub mod generate; -pub mod set; diff --git a/crates/entrypoint/src/net/keypair/set.rs b/crates/entrypoint/src/net/keypair/set.rs deleted file mode 100644 index 967a0bdbe7..0000000000 --- a/crates/entrypoint/src/net/keypair/set.rs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use alloy::primitives::hex; -use anyhow::Result; -use e3_config::AppConfig; -use e3_crypto::Cipher; -use e3_net::NetRepositoryFactory; -use libp2p::identity::Keypair; - -use crate::helpers::datastore::get_repositories; - -fn create_keypair(input: &String) -> Result { - hex::check(&input)?; - let kp = Keypair::ed25519_from_bytes(hex::decode(&input)?)?; - Ok(kp) -} - -pub fn validate_keypair_input(input: &String) -> Result<()> { - create_keypair(input).map(|_| ()) -} - -pub async fn execute(config: &AppConfig, value: String) -> Result<()> { - let kp = create_keypair(&value)?; - let mut secret = kp.try_into_ed25519()?.to_bytes().to_vec(); - let cipher = Cipher::from_file(config.key_file()).await?; - let encrypted = cipher.encrypt_data(&mut secret)?; - let repositories = get_repositories(config)?; - repositories.libp2p_keypair().write_sync(&encrypted).await?; - Ok(()) -} diff --git a/crates/entrypoint/src/net/mod.rs b/crates/entrypoint/src/net/mod.rs index cb89278823..f07ae66a42 100644 --- a/crates/entrypoint/src/net/mod.rs +++ b/crates/entrypoint/src/net/mod.rs @@ -4,5 +4,4 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -pub mod keypair; -pub mod peer_id; +pub mod get_peer_id; diff --git a/crates/entrypoint/src/password/set.rs b/crates/entrypoint/src/password/set.rs index eee50cc4d7..a0946e28a3 100644 --- a/crates/entrypoint/src/password/set.rs +++ b/crates/entrypoint/src/password/set.rs @@ -11,6 +11,7 @@ use zeroize::Zeroizing; use crate::helpers::rand::generate_random_bytes; +/// Checks if the Keyfile already exists and fail with a constructive error pub async fn preflight(config: &AppConfig) -> Result<()> { let key_file = config.key_file(); let pm = FilePasswordManager::new(key_file); @@ -22,7 +23,15 @@ pub async fn preflight(config: &AppConfig) -> Result<()> { Ok(()) } -pub async fn execute(config: &AppConfig, pw: Zeroizing>) -> Result<()> { +pub async fn execute(config: &AppConfig, input: Zeroizing) -> Result<()> { + let pw = Zeroizing::new(input.as_bytes().to_owned()); + + execute_bytes(config, pw).await?; + + Ok(()) +} + +pub async fn execute_bytes(config: &AppConfig, input: Zeroizing>) -> Result<()> { let key_file = config.key_file(); let mut pm = FilePasswordManager::new(key_file); @@ -31,8 +40,7 @@ pub async fn execute(config: &AppConfig, pw: Zeroizing>) -> Result<()> { pm.delete_key().await?; } - pm.set_key(pw).await?; - + pm.set_key(input).await?; Ok(()) } @@ -41,7 +49,7 @@ pub async fn autopassword(config: &AppConfig) -> Result<()> { let pm = FilePasswordManager::new(key_file); if !pm.is_set() { let pw = generate_random_bytes(128); - execute(config, pw.into()).await?; + execute_bytes(config, pw.into()).await?; } Ok(()) } diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 3032e43d0c..1ad2275633 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -19,14 +19,12 @@ use std::{ pub async fn execute( config: &AppConfig, - address: Address, pubkey_write_path: Option, plaintext_write_path: Option, ) -> Result { let rng = Arc::new(Mutex::new(ChaCha20Rng::from_rng(OsRng)?)); let cipher = Arc::new(Cipher::from_file(config.key_file()).await?); - let node = CiphernodeBuilder::new(&config.name(), rng.clone(), cipher.clone()) - .with_address(&address.to_string()) + let node = CiphernodeBuilder::new(rng.clone(), cipher.clone()) .with_persistence(&config.log_file(), &config.db_file()) .with_chains(&config.chains()) .with_sortition_score() diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index b541a38d16..6a16d9d5cd 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -4,7 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use alloy::primitives::Address; use anyhow::Result; use e3_ciphernode_builder::{CiphernodeBuilder, CiphernodeHandle}; use e3_config::AppConfig; @@ -16,7 +15,7 @@ use std::sync::{Arc, Mutex}; use tracing::instrument; #[instrument(name = "app", skip_all)] -pub async fn execute(config: &AppConfig, address: Address) -> Result { +pub async fn execute(config: &AppConfig) -> Result { let rng = Arc::new(Mutex::new(rand_chacha::ChaCha20Rng::from_rng(OsRng)?)); let cipher = Arc::new(Cipher::from_file(&config.key_file()).await?); let zk_config = e3_zk_prover::ZkConfig::fetch_or_default().await; @@ -28,8 +27,7 @@ pub async fn execute(config: &AppConfig, address: Address) -> Result Result
{ + let repositories = get_repositories(config)?; + let cipher = Cipher::from_file(config.key_file()).await?; + let encrypted = repositories + .eth_private_key() + .read() + .await? + .context("No wallet has been set.")?; + let private_key = Zeroizing::new(cipher.decrypt_data(&encrypted)?); + let address = + PrivateKeySigner::from_bytes(&FixedBytes::<32>::from_slice(&private_key))?.address(); + + Ok(address) +} diff --git a/crates/entrypoint/src/wallet/mod.rs b/crates/entrypoint/src/wallet/mod.rs index cf1ede937a..a4deda7b2f 100644 --- a/crates/entrypoint/src/wallet/mod.rs +++ b/crates/entrypoint/src/wallet/mod.rs @@ -4,4 +4,5 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +pub mod get; pub mod set; diff --git a/crates/entrypoint/src/wallet/set.rs b/crates/entrypoint/src/wallet/set.rs index 14b6a2e21f..07715d6a9c 100644 --- a/crates/entrypoint/src/wallet/set.rs +++ b/crates/entrypoint/src/wallet/set.rs @@ -5,10 +5,14 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use alloy::{hex::FromHex, primitives::FixedBytes, signers::local::PrivateKeySigner}; +use alloy_primitives::Address; use anyhow::{anyhow, Result}; use e3_config::AppConfig; use e3_crypto::Cipher; use e3_evm::EthPrivateKeyRepositoryFactory; +use e3_net::NetRepositoryFactory; +use libp2p::{identity::Keypair, PeerId}; +use zeroize::{Zeroize, Zeroizing}; use crate::helpers::{datastore::get_repositories, rand::generate_random_bytes}; @@ -20,20 +24,74 @@ pub fn validate_private_key(input: &String) -> Result<()> { Ok(()) } -pub async fn execute(config: &AppConfig, input: String) -> Result<()> { +pub async fn execute(config: &AppConfig, input: Zeroizing) -> Result<(Address, PeerId)> { let cipher = Cipher::from_file(config.key_file()).await?; - let encrypted = cipher.encrypt_data(&mut input.as_bytes().to_vec())?; + + let (encrypted_private_key, encrypted_keypair, address, peer_id) = process_key(&cipher, input)?; + + // Save the encrypted keys let repositories = get_repositories(config)?; repositories .eth_private_key() - .write_sync(&encrypted) + .write_sync(&encrypted_private_key) .await?; - Ok(()) + + repositories + .libp2p_keypair() + .write_sync(&encrypted_keypair) + .await?; + Ok((address, peer_id)) +} + +fn process_key( + cipher: &Cipher, + private_key: Zeroizing, +) -> Result<(Vec, Vec, Address, PeerId)> { + let private_key_bytes = FixedBytes::<32>::from_hex(private_key)?; + let keypair = Keypair::ed25519_from_bytes(&mut private_key_bytes.clone())?; + let peer_id = PeerId::from(&keypair.public()); + let mut keypair = keypair.try_into_ed25519()?.to_bytes().to_vec(); + let address = PrivateKeySigner::from_bytes(&private_key_bytes)?.address(); + let encrypted_private_key = cipher.encrypt_data(&mut private_key_bytes.to_vec())?; + let encrypted_keypair = cipher.encrypt_data(&mut keypair)?; + + Ok((encrypted_private_key, encrypted_keypair, address, peer_id)) } pub async fn autowallet(config: &AppConfig) -> Result<()> { - let bytes = generate_random_bytes(32); - let input = hex::encode(&bytes); + let mut bytes = generate_random_bytes(32); + let input = Zeroizing::new(hex::encode(&bytes)); + bytes.zeroize(); execute(config, input).await?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_process_key() -> Result<()> { + let cipher = Cipher::from_password("test_password").await?; + // Hardhat default private key + let input = Zeroizing::new( + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string(), + ); + + let (encrypted_private_key, encrypted_keypair, address, peer_id) = + process_key(&cipher, input)?; + + assert!(!encrypted_private_key.is_empty()); + assert!(!encrypted_keypair.is_empty()); + assert_eq!( + address, + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse::
()? + ); + assert_eq!( + &peer_id.to_string(), + "12D3KooWEZiPVmEZkwCFEWYxPL6xts6LnPHRFqsSEDGmt1vQ17By" + ); + + Ok(()) + } +} diff --git a/crates/events/src/bus_handle.rs b/crates/events/src/bus_handle.rs index 36ec3958a7..94ccc6ba25 100644 --- a/crates/events/src/bus_handle.rs +++ b/crates/events/src/bus_handle.rs @@ -4,17 +4,17 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use std::sync::Arc; - use actix::{Actor, Addr, Handler, Recipient}; use anyhow::Result; use derivative::Derivative; use e3_utils::MAILBOX_LIMIT; +use std::marker::PhantomData; use tracing::error; use crate::{ event_context::EventContext, - hlc::Hlc, + hlc::{Hlc, HlcMethods}, + hlc_factory::HlcFactory, sequencer::Sequencer, traits::{ ErrorDispatcher, ErrorFactory, EventConstructorWithTimestamp, EventContextAccessors, @@ -24,35 +24,83 @@ use crate::{ EventType, HistoryCollector, Sequenced, Subscribe, Unsequenced, Unsubscribe, }; -#[derive(Clone, Derivative)] -#[derivative(Debug, PartialEq, Eq)] -pub struct BusHandle { +// TODO: this wont work with trap need to fix +pub trait EnclaveUnsequencedErrorDispatcher { + fn err(&self, err_type: EType, error: anyhow::Error); +} + +/// Typestate marker indicating the BusHandle has not yet been enabled with an HLC clock. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Disabled; + +/// Typestate marker indicating the BusHandle has been enabled and is ready for use. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Enabled; + +#[derive(Derivative)] +#[derivative( + Clone(bound = ""), + Debug(bound = ""), + PartialEq(bound = ""), + Eq(bound = "") +)] +pub struct BusHandle { /// EventBus that actors can consume sequenced events from event_bus: Addr>>, /// Sequencer that new events should be produced from sequencer: Addr, /// Hlc clock used to time all events created on this BusHandle - #[derivative(Debug = "ignore")] - hlc: Arc, + #[derivative(Debug = "ignore", PartialEq = "ignore")] + hlc: HlcFactory, /// Temporary context for events the bus publishes ctx: Option>, + #[derivative(Debug = "ignore", PartialEq = "ignore")] + _state: PhantomData, } -impl BusHandle { - /// Create a new BusHandle +impl BusHandle { + /// Create a new disabled BusHandle. Call `enable()` or `enable_with_hlc()` to activate it. pub fn new( event_bus: Addr>>, sequencer: Addr, - hlc: Hlc, + hlc: HlcFactory, ) -> Self { Self { event_bus, sequencer, - hlc: Arc::new(hlc), + hlc, + ctx: None, + _state: PhantomData, + } + } + + /// Enable the BusHandle by providing a node ID string used to create the HLC clock. + pub fn enable(self, node_id: &str) -> BusHandle { + let hlc = Hlc::from_str(node_id); + self.hlc.enable(hlc); + BusHandle { + event_bus: self.event_bus, + sequencer: self.sequencer, + hlc: self.hlc, ctx: None, + _state: PhantomData, } } + /// Enable the BusHandle by providing a pre-configured HLC clock. + pub fn enable_with_hlc(self, hlc: Hlc) -> BusHandle { + self.hlc.enable(hlc); + BusHandle { + event_bus: self.event_bus, + sequencer: self.sequencer, + hlc: self.hlc, + ctx: None, + _state: PhantomData, + } + } +} + +impl BusHandle { /// Return a HistoryCollector for examining events that have passed through on the events bus pub fn history(&self) -> Addr>> { EventBus::>::history(&self.event_bus) @@ -75,7 +123,7 @@ impl BusHandle { } /// Pipe events from this handle to the other handle only when the predicate returns true - pub fn pipe_to(&self, other: &BusHandle, predicate: F) + pub fn pipe_to(&self, other: &BusHandle, predicate: F) where F: Fn(&EnclaveEvent) -> bool + Unpin + 'static, { @@ -90,7 +138,7 @@ impl BusHandle { } } -impl EventPublisher> for BusHandle { +impl EventPublisher> for BusHandle { fn publish( &self, data: impl Into, @@ -129,7 +177,7 @@ impl EventPublisher> for BusHandle { } } -impl BusHandle { +impl BusHandle { fn publish_from_remote_impl( &self, data: impl Into, @@ -153,7 +201,7 @@ impl BusHandle { } } -impl ErrorDispatcher> for BusHandle { +impl ErrorDispatcher> for BusHandle { fn err(&self, err_type: EType, error: impl Into) { match self.event_from_error(err_type, error, self.get_ctx()) { Ok(evt) => self.sequencer.do_send(evt), @@ -162,7 +210,16 @@ impl ErrorDispatcher> for BusHandle { } } -impl EventFactory> for BusHandle { +impl EnclaveUnsequencedErrorDispatcher for BusHandle { + fn err(&self, err_type: EType, error: anyhow::Error) { + match self.event_from_error(err_type, error, self.get_ctx()) { + Ok(evt) => self.sequencer.do_send(evt), + Err(e) => error!("{e}"), + } + } +} + +impl EventFactory> for BusHandle { fn event_from( &self, data: impl Into, @@ -197,7 +254,7 @@ impl EventFactory> for BusHandle { } } -impl ErrorFactory> for BusHandle { +impl ErrorFactory> for BusHandle { fn event_from_error( &self, err_type: EType, @@ -209,7 +266,7 @@ impl ErrorFactory> for BusHandle { } } -impl EventSubscriber> for BusHandle { +impl EventSubscriber> for BusHandle { fn subscribe(&self, event_type: EventType, recipient: Recipient>) { self.event_bus .do_send(Subscribe::new(event_type, recipient)) @@ -232,7 +289,7 @@ impl EventSubscriber> for BusHandle { } } -impl EventContextManager for BusHandle { +impl EventContextManager for BusHandle { fn set_ctx(&mut self, value: C) where C: Into>, @@ -312,18 +369,20 @@ mod tests { // 1. setup up two separate busses with out of sync clocks A and B. B should be 30 seconds // faster than A. - let bus_a = EventSystem::new("a") + let bus_a = EventSystem::new() .with_fresh_bus() - .with_hlc(Hlc::new(1).with_clock(move || now_micros().saturating_sub(30_000_000))) // Late - .handle()?; - let bus_b = EventSystem::new("b") + .handle()? + .enable_with_hlc( + Hlc::new(1).with_clock(move || now_micros().saturating_sub(30_000_000)), + ); // Late + let bus_b = EventSystem::new() .with_fresh_bus() - .with_hlc(Hlc::new(2)) - .handle()?; - let bus_c = EventSystem::new("c") + .handle()? + .enable_with_hlc(Hlc::new(2)); + let bus_c = EventSystem::new() .with_fresh_bus() - .with_hlc(Hlc::new(3)) - .handle()?; + .handle()? + .enable_with_hlc(Hlc::new(3)); let forwarder = Forwarder { dest: bus_c.clone(), @@ -399,7 +458,7 @@ pub struct BusHandlePipe where F: Fn(&EnclaveEvent) -> bool + Unpin + 'static, { - handle: BusHandle, + handle: BusHandle, predicate: F, } @@ -409,7 +468,7 @@ where { /// Create a new BusHandlePipe only forwarding events to the wrapped handle when the predicate /// function returns true - pub fn new(handle: BusHandle, predicate: F) -> Self { + pub fn new(handle: BusHandle, predicate: F) -> Self { Self { handle, predicate } } } diff --git a/crates/events/src/data_events.rs b/crates/events/src/data_events.rs index 5964049eaa..96020db3ed 100644 --- a/crates/events/src/data_events.rs +++ b/crates/events/src/data_events.rs @@ -111,3 +111,7 @@ impl Remove { &self.0 } } + +#[derive(Message)] +#[rtype(result = "()")] +pub struct Flush; diff --git a/crates/events/src/hlc.rs b/crates/events/src/hlc.rs index c476fdca62..e0b201f81c 100644 --- a/crates/events/src/hlc.rs +++ b/crates/events/src/hlc.rs @@ -139,7 +139,7 @@ impl From for HlcTimestamp { /// # Example /// /// ``` -/// # use e3_events::hlc::{Hlc, HlcTimestamp, HlcError}; +/// # use e3_events::hlc::{Hlc, HlcTimestamp, HlcError, HlcMethods}; /// # fn main() -> Result<(), HlcError> { /// let hlc = Hlc::new(1); // Node id /// let ts1 = hlc.tick()?; @@ -270,8 +270,11 @@ impl Hlc { .map(|d| d.as_micros() as u64) .map_err(|_| HlcError::ClockError) } +} - pub fn tick(&self) -> Result { +impl HlcMethods for Hlc { + type Error = HlcError; + fn tick(&self) -> Result { let now = self.now_physical()?; let mut inner = self.inner.lock().unwrap(); @@ -298,7 +301,7 @@ impl Hlc { }) } - pub fn receive(&self, remote: &HlcTimestamp) -> Result { + fn receive(&self, remote: &HlcTimestamp) -> Result { let now = self.now_physical()?; if remote.ts > now.saturating_add(self.max_drift) { @@ -352,6 +355,12 @@ impl Hlc { } } +pub trait HlcMethods { + type Error: From; + fn tick(&self) -> Result; + fn receive(&self, remote: &HlcTimestamp) -> Result; +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/events/src/hlc_factory.rs b/crates/events/src/hlc_factory.rs new file mode 100644 index 0000000000..5141b0e218 --- /dev/null +++ b/crates/events/src/hlc_factory.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use std::sync::{Arc, Mutex, PoisonError}; + +use thiserror::Error; + +use crate::hlc::{Hlc, HlcError, HlcMethods, HlcTimestamp}; + +#[derive(Debug, Error)] +pub enum HlcFactoryError { + #[error(transparent)] + Hlc(#[from] HlcError), + + #[error("HLC not initialized")] + NotReady, + + #[error("lock poisoned: {0}")] + LockPoisoned(String), +} + +impl From> for HlcFactoryError { + fn from(e: PoisonError) -> Self { + HlcFactoryError::LockPoisoned(e.to_string()) + } +} + +enum HlcState { + Init, + Ready(Hlc), +} + +impl HlcState { + pub fn ready(&mut self, hlc: Hlc) { + if let HlcState::Init = self { + *self = HlcState::Ready(hlc); + } + } + + pub fn is_ready(&self) -> bool { + if let HlcState::Init = self { + false + } else { + true + } + } +} + +impl HlcMethods for HlcState { + type Error = HlcFactoryError; + fn receive(&self, remote: &HlcTimestamp) -> Result { + let HlcState::Ready(hlc) = self else { + return Err(HlcFactoryError::NotReady); + }; + + Ok(hlc.receive(remote)?) + } + fn tick(&self) -> Result { + let HlcState::Ready(hlc) = self else { + return Err(HlcFactoryError::NotReady); + }; + + Ok(hlc.tick()?) + } +} + +/// This solves an issue where Hlc needs a node_id which is derived from the address but the +/// address is located in the store but the store needs the handle but the handle needs the hlc +/// which needs the node_id. +#[derive(Clone)] +pub struct HlcFactory { + hlc: Arc>, +} + +impl HlcFactory { + pub fn new() -> Self { + Self { + hlc: Arc::new(Mutex::new(HlcState::Init)), + } + } + + pub fn enable(&self, hlc: Hlc) { + let mut guard = self.hlc.lock().unwrap(); + guard.ready(hlc); + } + + pub fn is_ready(&self) -> bool { + match self.hlc.lock() { + Err(_) => false, + Ok(g) => g.is_ready(), + } + } +} + +impl HlcMethods for HlcFactory { + type Error = HlcFactoryError; + fn tick(&self) -> Result { + let guard = self.hlc.lock()?; + guard.tick() + } + fn receive(&self, remote: &HlcTimestamp) -> Result { + let guard = self.hlc.lock()?; + guard.receive(remote) + } +} diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs index d772e652bd..2d0e7b9050 100644 --- a/crates/events/src/lib.rs +++ b/crates/events/src/lib.rs @@ -17,6 +17,7 @@ mod events; mod eventstore; mod eventstore_router; pub mod hlc; +pub mod hlc_factory; mod into_key; mod ordered_set; pub mod prelude; diff --git a/crates/events/src/sequencer.rs b/crates/events/src/sequencer.rs index cb8a929243..976143f742 100644 --- a/crates/events/src/sequencer.rs +++ b/crates/events/src/sequencer.rs @@ -74,8 +74,8 @@ mod tests { #[actix::test] async fn it_adds_seqence_numbers_to_events() -> anyhow::Result<()> { - let system = EventSystem::new("test"); - let bus = system.handle()?; + let system = EventSystem::new(); + let bus = system.handle()?.enable("test"); let history = bus.history(); let event_data = vec![ diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index c8446c4095..062431cdfb 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -15,19 +15,20 @@ anyhow = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } bloom = { workspace = true } -e3-fhe-params = { workspace = true } -e3-crypto = { workspace = true } e3-config = { workspace = true } +e3-crypto = { workspace = true } e3-data = { workspace = true } e3-events = { workspace = true } +e3-fhe-params = { workspace = true } +e3-sortition = { workspace = true } e3-trbfv = { workspace = true } e3-utils = { workspace = true } futures-util = { workspace = true } -e3-sortition = { workspace = true } +hex = { workspace = true } +num-bigint = { workspace = true } serde = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -num-bigint = { workspace = true } url = { workspace = true } zeroize = { workspace = true } diff --git a/crates/evm/src/evm_chain_gateway.rs b/crates/evm/src/evm_chain_gateway.rs index ca43f25d7f..65c7677fc4 100644 --- a/crates/evm/src/evm_chain_gateway.rs +++ b/crates/evm/src/evm_chain_gateway.rs @@ -279,8 +279,8 @@ mod tests { .finish(), ); - let system = EventSystem::new("test").with_fresh_bus(); - let bus: BusHandle = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus: BusHandle = system.handle()?.enable("test"); let history_collector = bus.history(); diff --git a/crates/evm/src/helpers.rs b/crates/evm/src/helpers.rs index f43bd295f0..5233b0d4ad 100644 --- a/crates/evm/src/helpers.rs +++ b/crates/evm/src/helpers.rs @@ -35,6 +35,7 @@ use e3_data::Repository; use e3_utils::{retry_with_backoff, RetryError}; use std::{env, future::Future, sync::Arc}; use tracing::info; +use zeroize::{Zeroize, Zeroizing}; pub trait AuthConversions { fn to_header_value(&self) -> Option; @@ -197,11 +198,11 @@ pub async fn load_signer_from_repository( let encrypted_key = repository .read() .await? - .ok_or_else(|| anyhow::anyhow!("No private key found in repository"))?; - - let decrypted = cipher.decrypt_data(&encrypted_key)?; - let private_key = String::from_utf8(decrypted)?; + .context("No private key found in repository")?; + let mut decrypted = cipher.decrypt_data(&encrypted_key)?; + let private_key = Zeroizing::new(hex::encode(&decrypted)); + decrypted.zeroize(); private_key.parse().map_err(Into::into) } diff --git a/crates/evm/tests/integration.rs b/crates/evm/tests/integration.rs index a2b0736d6b..6810925ee8 100644 --- a/crates/evm/tests/integration.rs +++ b/crates/evm/tests/integration.rs @@ -109,8 +109,8 @@ async fn evm_reader() -> Result<()> { .await?, ); let contract = EmitLogs::deploy(provider.provider()).await?; - let system = EventSystem::new("test").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test"); let history_collector = bus.history(); let chain_id = provider.chain_id(); @@ -179,8 +179,8 @@ async fn ensure_historical_events() -> Result<()> { let contract = EmitLogs::deploy(provider.provider()).await?; let contract_address = contract.address().clone(); let chain_id = provider.chain_id(); - let system = EventSystem::new("test").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test"); let history_collector = bus.history(); let historical_msgs = vec!["these", "are", "historical", "events"]; let live_events = vec!["these", "events", "are", "live"]; diff --git a/crates/net/src/document_publisher.rs b/crates/net/src/document_publisher.rs index 3669b96903..610a556805 100644 --- a/crates/net/src/document_publisher.rs +++ b/crates/net/src/document_publisher.rs @@ -645,8 +645,8 @@ mod tests { let guard = tracing::subscriber::set_default(subscriber); - let system = EventSystem::new("test").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test"); let (net_cmd_tx, net_cmd_rx) = mpsc::channel(100); let (net_evt_tx, net_evt_rx) = broadcast::channel(100); let net_evt_rx = Arc::new(net_evt_rx); diff --git a/crates/net/src/net_event_buffer.rs b/crates/net/src/net_event_buffer.rs index 77f830eece..fd00109fa3 100644 --- a/crates/net/src/net_event_buffer.rs +++ b/crates/net/src/net_event_buffer.rs @@ -152,8 +152,8 @@ mod tests { #[actix::test] async fn test_buffers_until_sync_ended() -> Result<()> { // Setup - let system = EventSystem::new("test").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test"); let (input_tx, input_rx) = broadcast::channel(16); let mut output_rx = NetEventBuffer::setup(&bus, &input_rx); diff --git a/crates/sync/src/sync.rs b/crates/sync/src/sync.rs index 4d41bbb6ef..19d97b3dfc 100644 --- a/crates/sync/src/sync.rs +++ b/crates/sync/src/sync.rs @@ -327,8 +327,8 @@ mod tests { /// re-publishes later, causing it to be silently dropped. #[actix::test] async fn infrastructure_events_are_filtered_during_replay() -> anyhow::Result<()> { - let system = EventSystem::new("test-sync-replay").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test-sync-replay"); let history = bus.history(); let events: Vec = vec![ diff --git a/crates/test-helpers/src/ciphernode_system.rs b/crates/test-helpers/src/ciphernode_system.rs index 8d577670d1..042a34da09 100644 --- a/crates/test-helpers/src/ciphernode_system.rs +++ b/crates/test-helpers/src/ciphernode_system.rs @@ -207,7 +207,11 @@ mod tests { let bus = EventBus::::new(EventBusConfig { deduplicate: true }).start(); let history = EventBus::::history(&bus); let errors = EventBus::::error(&bus); - let bus = EventSystem::new("test").with_event_bus(bus).handle()?; + + let bus = EventSystem::new() + .with_event_bus(bus) + .handle()? + .enable("test"); let handle: JoinHandle> = tokio::spawn(async { Ok(()) }); Ok(CiphernodeHandle { diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs index 16ef1612f8..f7e958cb04 100644 --- a/crates/test-helpers/src/lib.rs +++ b/crates/test-helpers/src/lib.rs @@ -99,7 +99,10 @@ pub fn get_common_setup( let moduli = param_set.moduli; let (crp_bytes, params) = create_crp_bytes_params(moduli, degree, plaintext_modulus, &seed); let crpoly = CommonRandomPoly::deserialize(&crp_bytes.clone(), ¶ms)?; - let handle = EventSystem::in_mem("cn1").with_event_bus(bus).handle()?; + let handle = EventSystem::in_mem() + .with_event_bus(bus) + .handle()? + .enable("cn1"); Ok((handle, rng, seed, params, crpoly, errors, history)) } diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 4414c02fb5..e4bd5291ac 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -30,6 +30,7 @@ use e3_test_helpers::{ use e3_trbfv::helpers::calculate_error_size; use e3_utils::rand_eth_addr; use e3_utils::utility_types::ArcBytes; +use e3_zk_prover::test_utils::get_tempdir; use e3_zk_prover::{ZkBackend, ZkConfig}; use fhe::bfv::PublicKey; use fhe_traits::{DeserializeParametrized, Serialize}; @@ -75,7 +76,7 @@ async fn find_bb() -> Option { /// If a local bb binary is found, uses it with fixture files (fast path). /// Otherwise, calls `ensure_installed()` to download bb + circuits (CI path). async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { - let temp = tempfile::tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let temp_path = temp.path(); let noir_dir = temp_path.join("noir"); let bb_binary = noir_dir.join("bin").join("bb"); @@ -318,8 +319,8 @@ async fn test_trbfv_actor() -> Result<()> { let rng = create_shared_rng_from_u64(42); // Create "trigger" bus - let system = EventSystem::new("test").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test"); // Parameters (128bits of security) let params_raw = BfvParamSet::from(DEFAULT_BFV_PRESET).build_arc(); @@ -364,8 +365,7 @@ async fn test_trbfv_actor() -> Result<()> { .add_group(1, || async { let addr = rand_eth_addr(&rng); println!("Building collector {}!", addr); - CiphernodeBuilder::new(&addr, rng.clone(), cipher.clone()) - .with_address(&addr) + CiphernodeBuilder::new(rng.clone(), cipher.clone()) .testmode_with_history() .with_shared_taskpool(&task_pool) .with_multithread_concurrent_jobs(concurrent_jobs) @@ -376,8 +376,8 @@ async fn test_trbfv_actor() -> Result<()> { .with_pubkey_aggregation() .with_sortition_score() .with_threshold_plaintext_aggregation() - .testmode_start_buffer_immediately() .testmode_with_forked_bus(bus.event_bus()) + .testmode_ignore_address_check() .with_logging() .build() .await @@ -385,8 +385,7 @@ async fn test_trbfv_actor() -> Result<()> { .add_group(19, || async { let addr = rand_eth_addr(&rng); println!("Building normal {}", &addr); - CiphernodeBuilder::new(&addr, rng.clone(), cipher.clone()) - .with_address(&addr) + CiphernodeBuilder::new(rng.clone(), cipher.clone()) .with_shared_taskpool(&task_pool) .with_multithread_concurrent_jobs(concurrent_jobs) .with_shared_multithread_report(&multithread_report) @@ -394,8 +393,8 @@ async fn test_trbfv_actor() -> Result<()> { .with_zkproof(zk_backend.clone()) .testmode_with_signer(PrivateKeySigner::random()) .with_sortition_score() - .testmode_start_buffer_immediately() .testmode_with_forked_bus(bus.event_bus()) + .testmode_ignore_address_check() .with_logging() .build() .await @@ -683,8 +682,8 @@ async fn test_p2p_actor_forwards_events_to_network() -> Result<()> { // Setup elements in test let (cmd_tx, mut cmd_rx) = mpsc::channel(100); // Transmit byte events to the network let (event_tx, _) = broadcast::channel(100); // Receive byte events from the network - let system = EventSystem::new("test"); - let bus = system.handle()?; + let system = EventSystem::new(); + let bus = system.handle()?.enable("test"); let history_collector = bus.history(); let event_rx = Arc::new(event_tx.subscribe()); // Pas cmd and event channels to NetEventTranslator @@ -771,8 +770,8 @@ async fn test_p2p_actor_forwards_events_to_bus() -> Result<()> { // Setup elements in test let (cmd_tx, _) = mpsc::channel(100); // Transmit byte events to the network let (event_tx, event_rx) = broadcast::channel(100); // Receive byte events from the network - let system = EventSystem::new("test").with_fresh_bus(); - let bus = system.handle()?; + let system = EventSystem::new().with_fresh_bus(); + let bus = system.handle()?.enable("test"); let history_collector = bus.history(); NetEventTranslator::setup(&bus, &cmd_tx, &Arc::new(event_rx), "mytopic"); @@ -837,9 +836,8 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { store: Option>, cipher: &Arc, ) -> Result { - let mut builder = CiphernodeBuilder::new(&addr, rng.clone(), cipher.clone()) + let mut builder = CiphernodeBuilder::new(rng.clone(), cipher.clone()) .with_trbfv() - .with_address(addr) .testmode_with_forked_bus(bus.event_bus()) .testmode_with_history() .testmode_with_errors() @@ -937,11 +935,12 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { ) }; - let bus = EventSystem::in_mem("cn2") + let bus = EventSystem::in_mem() .with_event_bus( EventBus::::new(EventBusConfig { deduplicate: true }).start(), ) - .handle()?; + .handle()? + .enable("cn2"); let cn1 = setup_local_ciphernode( &bus, &rng, @@ -1044,9 +1043,8 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { store: Option>, cipher: &Arc, ) -> Result { - let mut builder = CiphernodeBuilder::new(&addr, rng.clone(), cipher.clone()) + let mut builder = CiphernodeBuilder::new(rng.clone(), cipher.clone()) .with_trbfv() - .with_address(addr) .testmode_with_forked_bus(bus.event_bus()) .testmode_with_history() .testmode_with_errors() diff --git a/crates/utils/src/alloy.rs b/crates/utils/src/alloy.rs index c8c3085676..776f5d3653 100644 --- a/crates/utils/src/alloy.rs +++ b/crates/utils/src/alloy.rs @@ -4,10 +4,11 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use alloy::primitives::Address; - use crate::SharedRng; +use alloy::{primitives::Address, signers::local::PrivateKeySigner}; +use anyhow::{Context, Result}; use rand::Rng; +use std::str::FromStr; pub fn rand_eth_addr(rng: &SharedRng) -> String { { @@ -15,3 +16,8 @@ pub fn rand_eth_addr(rng: &SharedRng) -> String { Address::from_slice(rnum).to_string() } } + +pub fn eth_address_from_private_key(private_key: &str) -> Result
{ + let signer = PrivateKeySigner::from_str(private_key).context("invalid private key")?; + Ok(signer.address()) +} diff --git a/crates/zk-prover/build.rs b/crates/zk-prover/build.rs new file mode 100644 index 0000000000..22d5a15c5f --- /dev/null +++ b/crates/zk-prover/build.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-env-changed=FORCE_BUILD"); + + assert!(Command::new("bash") + .arg("./scripts/build_fixtures.sh") + .status() + .unwrap() + .success()); + + println!("cargo:rerun-if-changed=./scripts/build_fixtures.sh"); +} diff --git a/crates/zk-prover/scripts/build_fixtures.sh b/crates/zk-prover/scripts/build_fixtures.sh new file mode 100644 index 0000000000..c7d17a3d68 --- /dev/null +++ b/crates/zk-prover/scripts/build_fixtures.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -e + + +cd "$(git rev-parse --show-toplevel)" + +# if this is a clean checkout we need to have some artifacts to test against +if find ./circuits/bin -name '*.json' -print -quit | grep -q .; then + exit 0 +fi + +# if we are in CI where circuits have been built ignore +if ! command -v nargo &> /dev/null; then + exit 0 +fi + +if ! command -v bb &> /dev/null; then + exit 0 +fi + +echo "Building circuits..." + +pnpm install && pnpm build:circuits diff --git a/crates/zk-prover/src/backend/download.rs b/crates/zk-prover/src/backend/download.rs index 3a220839c5..5b27ec1e9f 100644 --- a/crates/zk-prover/src/backend/download.rs +++ b/crates/zk-prover/src/backend/download.rs @@ -21,6 +21,7 @@ use super::ZkBackend; impl ZkBackend { pub async fn download_bb(&self) -> Result<(), ZkError> { if self.using_custom_bb { + println!("IGNORING DOWNLOAD BECAUSE WE ARE USING A CUSTOM BB"); return Ok(()); } diff --git a/crates/zk-prover/src/backend/tests.rs b/crates/zk-prover/src/backend/tests.rs index daa66af83a..5f478141f2 100644 --- a/crates/zk-prover/src/backend/tests.rs +++ b/crates/zk-prover/src/backend/tests.rs @@ -5,8 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use super::*; -use crate::config::VersionInfo; -use tempfile::tempdir; +use crate::{config::VersionInfo, test_utils::get_tempdir}; use tokio::fs; fn test_backend(temp_path: &std::path::Path, config: ZkConfig) -> ZkBackend { @@ -19,7 +18,7 @@ fn test_backend(temp_path: &std::path::Path, config: ZkConfig) -> ZkBackend { #[tokio::test] async fn test_backend_creates_directories() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); fs::create_dir_all(&backend.base_dir).await.unwrap(); @@ -37,7 +36,7 @@ async fn test_backend_creates_directories() { #[tokio::test] async fn test_version_info_roundtrip() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let path = temp.path().join("version.json"); let info = VersionInfo { @@ -59,7 +58,7 @@ async fn test_version_info_roundtrip() { #[tokio::test] async fn test_check_status_full_setup_needed() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); let status = backend.check_status().await; @@ -72,7 +71,7 @@ async fn test_check_status_full_setup_needed() { #[tokio::test] async fn test_check_status_ready_when_installed() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let config = ZkConfig::default(); let backend = test_backend(temp.path(), config.clone()); @@ -101,7 +100,7 @@ async fn test_check_status_ready_when_installed() { #[tokio::test] async fn test_check_status_bb_needs_update() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let config = ZkConfig::default(); let backend = test_backend(temp.path(), config.clone()); @@ -130,7 +129,7 @@ async fn test_check_status_bb_needs_update() { #[tokio::test] async fn test_work_dir_cleanup() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); fs::create_dir_all(&backend.work_dir).await.unwrap(); diff --git a/crates/zk-prover/src/config.rs b/crates/zk-prover/src/config.rs index 87efa2bb38..6411168589 100644 --- a/crates/zk-prover/src/config.rs +++ b/crates/zk-prover/src/config.rs @@ -220,7 +220,7 @@ pub fn verify_checksum(file: &str, data: &[u8], expected: Option<&str>) -> Resul #[cfg(test)] mod tests { - use tempfile::tempdir; + use crate::test_utils::get_tempdir; use super::*; @@ -466,7 +466,7 @@ mod tests { println!("checksum verified for {}", target); // Test saving and loading through VersionInfo - let temp = tempdir().expect("failed to create temp dir"); + let temp = get_tempdir().expect("failed to create temp dir"); let tarball_path = temp.path().join("bb.tar.gz"); fs::write(&tarball_path, &bytes) diff --git a/crates/zk-prover/src/lib.rs b/crates/zk-prover/src/lib.rs index f3ee939522..3958210e2b 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -10,6 +10,7 @@ mod circuits; mod config; mod error; mod prover; +pub mod test_utils; mod traits; mod witness; diff --git a/crates/zk-prover/src/prover.rs b/crates/zk-prover/src/prover.rs index 14ceb9d778..03a3fb53fd 100644 --- a/crates/zk-prover/src/prover.rs +++ b/crates/zk-prover/src/prover.rs @@ -219,13 +219,12 @@ impl ZkProver { #[cfg(test)] mod tests { use super::*; - use crate::config::ZkConfig; + use crate::{config::ZkConfig, test_utils::get_tempdir}; use e3_config::BBPath; - use tempfile::tempdir; #[test] fn test_prover_requires_bb() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let temp_path = temp.path(); let noir_dir = temp_path.join("noir"); let bb_binary = noir_dir.join("bin").join("bb"); diff --git a/crates/zk-prover/src/test_utils.rs b/crates/zk-prover/src/test_utils.rs new file mode 100644 index 0000000000..dd3d9b14b0 --- /dev/null +++ b/crates/zk-prover/src/test_utils.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use std::{fs, path::Path}; + +use anyhow::Result; +use tempfile::TempDir; + +/// Get the tempdir within ./target/tmp. This is important since some virtual environments such as nix +/// won't necessarily have access to bb globaly. Not all tmp operations need to use this path only +/// operations that require tools to exist within a shell at that location. +pub fn get_tempdir() -> Result { + let tmp = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("target") + .join("tmp"); + fs::create_dir_all(tmp.clone())?; + Ok(TempDir::new_in(tmp)?) +} diff --git a/crates/zk-prover/tests/backend_tests.rs b/crates/zk-prover/tests/backend_tests.rs index ecc69f5053..b3cfc3b2df 100644 --- a/crates/zk-prover/tests/backend_tests.rs +++ b/crates/zk-prover/tests/backend_tests.rs @@ -7,13 +7,12 @@ mod common; use common::test_backend; -use e3_zk_prover::{ZkConfig, ZkProver}; -use tempfile::tempdir; +use e3_zk_prover::{test_utils::get_tempdir, ZkConfig, ZkProver}; use tokio::fs; #[tokio::test] async fn test_backend_creates_directories() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); fs::create_dir_all(&backend.base_dir).await.unwrap(); @@ -31,7 +30,7 @@ async fn test_backend_creates_directories() { #[tokio::test] async fn test_work_dir_cleanup() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); fs::create_dir_all(&backend.work_dir).await.unwrap(); @@ -58,7 +57,7 @@ async fn test_work_dir_cleanup() { #[tokio::test] async fn test_work_dir_path_traversal_protection() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); // Test path traversal attempts @@ -85,7 +84,7 @@ async fn test_work_dir_path_traversal_protection() { #[test] fn test_prover_requires_bb() { - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), ZkConfig::default()); let prover = ZkProver::new(&backend); diff --git a/crates/zk-prover/tests/common/helpers.rs b/crates/zk-prover/tests/common/helpers.rs index d16606195f..a157f88d08 100644 --- a/crates/zk-prover/tests/common/helpers.rs +++ b/crates/zk-prover/tests/common/helpers.rs @@ -6,7 +6,7 @@ use e3_config::BBPath; use e3_zk_prover::{ZkBackend, ZkConfig}; -use std::path::PathBuf; +use std::{env, path::PathBuf}; use tempfile::TempDir; use tokio::{fs, process::Command}; @@ -120,7 +120,10 @@ pub async fn setup_test_prover(bb: &PathBuf) -> (ZkBackend, TempDir) { /// Lightweight backend for tests that don't need a real bb binary. pub fn test_backend(temp_path: &std::path::Path, config: ZkConfig) -> ZkBackend { let noir_dir = temp_path.join("noir"); - let bb_binary = BBPath::Default(noir_dir.join("bin").join("bb")); + let bb_binary = match env::var("E3_CUSTOM_BB") { + Ok(path) => BBPath::Custom(PathBuf::from(path)), + Err(_) => BBPath::Default(noir_dir.join("bin").join("bb")), + }; let circuits_dir = noir_dir.join("circuits"); let work_dir = noir_dir.join("work").join("test_node"); ZkBackend::new(bb_binary, circuits_dir, work_dir, config) diff --git a/crates/zk-prover/tests/integration_tests.rs b/crates/zk-prover/tests/integration_tests.rs index 86921c30f1..46a4740a17 100644 --- a/crates/zk-prover/tests/integration_tests.rs +++ b/crates/zk-prover/tests/integration_tests.rs @@ -4,13 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -// TODO: Remove feature flag & network access requirement for this test. -// a) There are very few situations where we should be reliant on external -// network access for tests to pass - if we can avoid it by using virtualization -// or proxies we should. -// b) This feature flag makes it so that rust_analyzer will not work by default -// without adjusting global editor configuration which makes this code quite hard to work on. - //! Integration tests that require network access to download binaries. //! Run with: cargo test --features integration-tests @@ -21,9 +14,8 @@ mod common; use common::test_backend; use e3_fhe_params::BfvPreset; use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; -use e3_zk_prover::{BbTarget, Provable, SetupStatus, ZkConfig, ZkProver}; -use std::path::PathBuf; -use tempfile::tempdir; +use e3_zk_prover::{test_utils::get_tempdir, BbTarget, Provable, SetupStatus, ZkConfig, ZkProver}; +use std::{env, path::PathBuf}; fn versions_json_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("versions.json") @@ -37,55 +29,57 @@ async fn test_full_flow_download_circuits_prove_and_verify() { .expect("versions.json should exist"); eprintln!(">>> Config loaded"); - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), config); - // --- Step 1: Fresh state should need full setup --- - eprintln!(">>> Step 1: Checking fresh state..."); - assert!(matches!( - backend.check_status().await, - SetupStatus::FullSetupNeeded - )); - eprintln!(">>> Step 1: Done"); - - // --- Step 2: Download bb and verify structure --- - eprintln!(">>> Step 2: Downloading bb..."); - let result = backend.download_bb().await; - eprintln!(">>> Step 2: bb downloaded"); - assert!(result.is_ok(), "download failed: {:?}", result); - assert!(backend.bb_binary.exists(), "bb binary not found"); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let perms = std::fs::metadata(&backend.bb_binary).unwrap().permissions(); - assert_eq!(perms.mode() & 0o111, 0o111, "bb should be executable"); - } - - let metadata = std::fs::metadata(&backend.bb_binary).unwrap(); - assert!(metadata.len() > 0, "bb binary should not be empty"); - - let version_info = backend.load_version_info().await; - assert_eq!( - version_info.bb_version.as_deref(), - Some(backend.config.required_bb_version.as_str()) - ); - assert!(version_info.last_updated.is_some()); - - if backend - .config - .bb_checksum_for(BbTarget::current().unwrap()) - .is_some() - { - assert!( - version_info.bb_checksum.is_some(), - "checksum should be saved in version.json" + if !backend.using_custom_bb { + // --- Step 1: Fresh state should need full setup --- + eprintln!(">>> Step 1: Checking fresh state..."); + assert!(matches!( + backend.check_status().await, + SetupStatus::FullSetupNeeded + )); + eprintln!(">>> Step 1: Done"); + + // --- Step 2: Download bb and verify structure --- + eprintln!(">>> Step 2: Downloading bb..."); + let result = backend.download_bb().await; + eprintln!(">>> Step 2: bb downloaded"); + assert!(result.is_ok(), "download failed: {:?}", result); + assert!(backend.bb_binary.exists(), "bb binary not found"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::metadata(&backend.bb_binary).unwrap().permissions(); + assert_eq!(perms.mode() & 0o111, 0o111, "bb should be executable"); + } + + let metadata = std::fs::metadata(&backend.bb_binary).unwrap(); + assert!(metadata.len() > 0, "bb binary should not be empty"); + + let version_info = backend.load_version_info().await; + assert_eq!( + version_info.bb_version.as_deref(), + Some(backend.config.required_bb_version.as_str()) ); + assert!(version_info.last_updated.is_some()); + + if backend + .config + .bb_checksum_for(BbTarget::current().unwrap()) + .is_some() + { + assert!( + version_info.bb_checksum.is_some(), + "checksum should be saved in version.json" + ); + } + + assert!(backend.base_dir.exists()); + assert!(backend.base_dir.join("bin").exists()); } - assert!(backend.base_dir.exists()); - assert!(backend.base_dir.join("bin").exists()); - let version = backend.verify_bb().await; assert!(version.is_ok(), "bb --version failed: {:?}", version); println!("bb version: {}", version.unwrap()); @@ -164,6 +158,10 @@ async fn test_full_flow_download_circuits_prove_and_verify() { #[tokio::test] async fn test_download_bb_rejects_wrong_checksum() { + if env::var("E3_CUSTOM_BB").is_ok() { + return; + } + let mut config = ZkConfig::load(&versions_json_path()) .await .expect("versions.json should exist"); @@ -172,7 +170,7 @@ async fn test_download_bb_rejects_wrong_checksum() { *checksum = "0".repeat(64); } - let temp = tempdir().unwrap(); + let temp = get_tempdir().unwrap(); let backend = test_backend(temp.path(), config); let result = backend.download_bb().await; diff --git a/docs/pages/ciphernode-operators/running.mdx b/docs/pages/ciphernode-operators/running.mdx index efeeaade39..33f973af7d 100644 --- a/docs/pages/ciphernode-operators/running.mdx +++ b/docs/pages/ciphernode-operators/running.mdx @@ -59,9 +59,7 @@ enclaveup install ### Initialize Configuration ```bash -enclave config-set \ - --rpc-url wss://ethereum-sepolia-rpc.publicnode.com \ - --eth-address 0xYourAddress +enclave ciphernode setup ``` This creates `~/.config/enclave/enclave.config.yaml`. You'll be prompted for a password to encrypt diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 842922e460..0ecb869db9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -712,7 +712,7 @@ importers: version: 5.3.0 '@risc0/ethereum': specifier: file:lib/risc0-ethereum - version: file:templates/default/lib/risc0-ethereum + version: risc0-ethereum@file:templates/default/lib/risc0-ethereum '@types/chai': specifier: ^4.2.0 version: 4.3.20 @@ -3071,9 +3071,6 @@ packages: '@reown/appkit@1.7.8': resolution: {integrity: sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==} - '@risc0/ethereum@file:templates/default/lib/risc0-ethereum': - resolution: {directory: templates/default/lib/risc0-ethereum, type: directory} - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -8727,6 +8724,9 @@ packages: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} + risc0-ethereum@file:templates/default/lib/risc0-ethereum: + resolution: {directory: templates/default/lib/risc0-ethereum, type: directory} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -10235,11 +10235,11 @@ snapshots: '@babel/helpers': 7.28.4 '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -10280,7 +10280,7 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -10297,7 +10297,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.11 transitivePeerDependencies: @@ -10320,7 +10320,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -10335,9 +10342,9 @@ snapshots: '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10352,7 +10359,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10361,13 +10368,13 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -10385,7 +10392,7 @@ snapshots: '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -10403,7 +10410,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10430,7 +10437,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10474,14 +10481,14 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.5) transitivePeerDependencies: @@ -10521,7 +10528,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10535,7 +10542,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10592,7 +10599,7 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10638,7 +10645,7 @@ snapshots: '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10678,7 +10685,7 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.5) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) - '@babel/traverse': 7.28.5(supports-color@5.5.0) + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -10761,7 +10768,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) '@babel/types': 7.28.5 @@ -10976,11 +10983,23 @@ snapshots: '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.5 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.5 + '@babel/template': 7.27.2 + '@babel/types': 7.28.5 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.28.5(supports-color@5.5.0)': dependencies: '@babel/code-frame': 7.27.1 @@ -11182,7 +11201,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/helper-module-imports': 7.27.1 '@babel/runtime': 7.28.4 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 @@ -11502,7 +11521,7 @@ snapshots: '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -11518,7 +11537,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -12175,7 +12194,7 @@ snapshots: '@scure/base': 1.2.6 '@types/debug': 4.1.12 '@types/lodash': 4.17.20 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) lodash: 4.17.21 pony-cause: 2.1.11 semver: 7.7.3 @@ -12187,7 +12206,7 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) semver: 7.7.3 superstruct: 1.0.4 transitivePeerDependencies: @@ -12200,7 +12219,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -12214,7 +12233,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -12594,7 +12613,7 @@ snapshots: dependencies: '@nomicfoundation/hardhat-errors': 3.0.3 '@nomicfoundation/hardhat-utils': 3.0.5 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) ethereum-cryptography: 2.2.1 ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) hardhat: 3.0.11(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -12623,7 +12642,7 @@ snapshots: '@nomicfoundation/ignition-core': 3.0.4(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@nomicfoundation/ignition-ui': 3.0.4 chalk: 5.6.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) hardhat: 3.0.11(bufferutil@4.0.9)(utf-8-validate@5.0.10) json5: 2.2.3 prompts: 2.4.2 @@ -12640,7 +12659,7 @@ snapshots: '@nomicfoundation/hardhat-utils': 3.0.5 '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) chalk: 5.6.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) hardhat: 3.0.11(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 3.25.76 transitivePeerDependencies: @@ -12743,7 +12762,7 @@ snapshots: '@nomicfoundation/hardhat-utils': 3.0.5 '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) '@typechain/ethers-v6': 0.5.1(ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typechain@8.3.2(typescript@5.8.3))(typescript@5.8.3) - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) hardhat: 3.0.11(bufferutil@4.0.9)(utf-8-validate@5.0.10) typechain: 8.3.2(typescript@5.8.3) @@ -12755,7 +12774,7 @@ snapshots: '@nomicfoundation/hardhat-utils@3.0.5': dependencies: '@streamparser/json-node': 0.0.22 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) env-paths: 2.2.1 ethereum-cryptography: 2.2.1 fast-equals: 5.3.2 @@ -12773,7 +12792,7 @@ snapshots: '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) cbor2: 1.12.0 chalk: 5.6.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) hardhat: 3.0.11(bufferutil@4.0.9)(utf-8-validate@5.0.10) semver: 7.7.3 zod: 3.25.76 @@ -12795,7 +12814,7 @@ snapshots: '@nomicfoundation/hardhat-utils': 3.0.5 '@nomicfoundation/solidity-analyzer': 0.1.2 cbor2: 1.12.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) immer: 10.0.2 lodash-es: 4.17.21 @@ -13141,8 +13160,6 @@ snapshots: - utf-8-validate - zod - '@risc0/ethereum@file:templates/default/lib/risc0-ethereum': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/plugin-inject@5.0.5(rollup@4.52.5)': @@ -14017,7 +14034,7 @@ snapshots: '@depay/web3-mock-evm': 14.19.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@playwright/test': 1.52.0 '@synthetixio/synpress-core': 0.0.13(@playwright/test@1.52.0) - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.38.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@depay/solana-web3.js' - '@depay/web3-blockchains' @@ -14489,7 +14506,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.1(jiti@1.21.7) optionalDependencies: typescript: 5.8.3 @@ -14502,7 +14519,7 @@ snapshots: '@typescript-eslint/types': 8.47.0 '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.47.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.1(jiti@1.21.7) typescript: 5.8.3 transitivePeerDependencies: @@ -14512,7 +14529,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.8.3) '@typescript-eslint/types': 8.47.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -14535,7 +14552,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.3) - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: @@ -14548,7 +14565,7 @@ snapshots: '@typescript-eslint/types': 8.47.0 '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.8.3) '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.8.3) - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) eslint: 9.39.1(jiti@1.21.7) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -14563,7 +14580,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -14580,7 +14597,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.8.3) '@typescript-eslint/types': 8.47.0 '@typescript-eslint/visitor-keys': 8.47.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15774,7 +15791,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.0 iconv-lite: 0.6.3 on-finished: 2.4.1 @@ -17024,7 +17041,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -17296,7 +17313,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -17409,7 +17426,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -17465,7 +17482,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -17828,7 +17845,7 @@ snapshots: axios: 0.21.4(debug@4.4.3) chalk: 4.1.2 chokidar: 3.6.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) enquirer: 2.4.1 ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) form-data: 4.0.4 @@ -17871,7 +17888,7 @@ snapshots: lodash: 4.17.21 markdown-table: 2.0.0 sha1: 1.1.1 - viem: 2.38.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.38.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - debug @@ -17890,7 +17907,7 @@ snapshots: adm-zip: 0.4.16 chalk: 5.6.2 chokidar: 4.0.3 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) enquirer: 2.4.1 ethereum-cryptography: 2.2.1 micro-eth-signer: 0.14.0 @@ -17916,7 +17933,7 @@ snapshots: adm-zip: 0.4.16 chalk: 5.6.2 chokidar: 4.0.3 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) enquirer: 2.4.1 ethereum-cryptography: 2.2.1 micro-eth-signer: 0.14.0 @@ -18566,7 +18583,7 @@ snapshots: dependencies: chalk: 5.6.2 commander: 13.1.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) execa: 8.0.1 lilconfig: 3.1.3 listr2: 8.3.3 @@ -19397,7 +19414,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.2.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -19419,7 +19436,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -19928,6 +19945,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.9.6(typescript@5.8.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.1(typescript@5.8.3)(zod@3.22.4) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - zod + ox@0.9.6(typescript@5.8.3)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -20793,6 +20825,8 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 + risc0-ethereum@file:templates/default/lib/risc0-ethereum: {} + robust-predicates@3.0.2: {} rollup@4.52.5: @@ -20825,7 +20859,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -20916,7 +20950,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -21646,7 +21680,7 @@ snapshots: cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) esbuild: 0.25.12 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 @@ -21700,7 +21734,7 @@ snapshots: typechain@8.3.2(typescript@5.8.3): dependencies: '@types/prettier': 2.7.3 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) fs-extra: 7.0.1 glob: 7.1.7 js-sha3: 0.8.0 @@ -21995,6 +22029,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + viem@2.38.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.8.3)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.8.3) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + viem@2.38.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 @@ -22049,7 +22100,7 @@ snapshots: vite-node@1.6.1(@types/node@22.7.5): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.4.21(@types/node@22.7.5) @@ -22071,7 +22122,7 @@ snapshots: '@volar/typescript': 2.4.23 '@vue/language-core': 2.2.0(typescript@5.8.3) compare-versions: 6.1.1 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 1.1.2 magic-string: 0.30.21 @@ -22123,7 +22174,7 @@ snapshots: vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@5.4.21(@types/node@22.7.5)): dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: @@ -22165,7 +22216,7 @@ snapshots: '@vitest/utils': 1.6.1 acorn-walk: 8.3.4 chai: 4.5.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) execa: 8.0.1 local-pkg: 0.5.1 magic-string: 0.30.21