diff --git a/.gitmodules b/.gitmodules index 9dde29907d..281f92acc3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/risc0/risc0-ethereum [submodule "templates/default/lib/risc0-ethereum"] path = templates/default/lib/risc0-ethereum - url = https://github.com/gnosisguild/risc0-ethereum + url = https://github.com/gnosisguild/risc0-ethereum diff --git a/Cargo.lock b/Cargo.lock index d1d949e6ae..eaa6cb98e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2652,6 +2652,7 @@ dependencies = [ "e3-config", "e3-data", "e3-events", + "e3-evm", "e3-fhe", "e3-multithread", "e3-request", @@ -3104,6 +3105,7 @@ dependencies = [ "e3-config", "e3-data", "e3-events", + "e3-request", "num", "num-bigint", "rand 0.8.5", diff --git a/README.md b/README.md index fc61a11ac2..d8dc7a8061 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ sequenceDiagram E3Program-->>Enclave: inputValidator Enclave->>ComputeProvider: validate(computeProviderParams) ComputeProvider-->>Enclave: decryptionVerifier - Enclave->>CiphernodeRegistry: requestCommittee(e3Id, filter, threshold) + Enclave->>CiphernodeRegistry: requestCommittee(e3Id, seed, threshold) CiphernodeRegistry-->>Enclave: success Enclave-->>Users: e3Id, E3 struct diff --git a/crates/aggregator/Cargo.toml b/crates/aggregator/Cargo.toml index 6ac89ac0e0..f41f43110a 100644 --- a/crates/aggregator/Cargo.toml +++ b/crates/aggregator/Cargo.toml @@ -14,6 +14,7 @@ bincode = { workspace = true } e3-config = { workspace = true } e3-data = { workspace = true } e3-events = { workspace = true } +e3-evm = { workspace = true } e3-fhe = { workspace = true } e3-multithread = { workspace = true } e3-trbfv = { workspace = true } diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs new file mode 100644 index 0000000000..587c8311ea --- /dev/null +++ b/crates/aggregator/src/committee_finalizer.rs @@ -0,0 +1,148 @@ +// 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 actix::prelude::*; +use e3_events::{ + CommitteeFinalizeRequested, CommitteeRequested, EnclaveEvent, EventBus, Shutdown, Subscribe, +}; +use std::collections::HashMap; +use std::time::Duration; +use tracing::{error, info}; + +/// CommitteeFinalizer is an actor that listens to CommitteeRequested events and dispatches +/// CommitteeFinalizeRequested events after the submission deadline has passed. +pub struct CommitteeFinalizer { + bus: Addr>, + pending_committees: HashMap, +} + +impl CommitteeFinalizer { + pub fn new(bus: &Addr>) -> Self { + Self { + bus: bus.clone(), + pending_committees: HashMap::new(), + } + } + + pub fn attach(bus: &Addr>) -> Addr { + let addr = CommitteeFinalizer::new(bus).start(); + + bus.do_send(Subscribe::new( + "CommitteeRequested", + addr.clone().recipient(), + )); + bus.do_send(Subscribe::new("Shutdown", addr.clone().recipient())); + + addr + } +} + +impl Actor for CommitteeFinalizer { + type Context = Context; +} + +impl Handler for CommitteeFinalizer { + type Result = (); + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::CommitteeRequested { data, .. } => ctx.notify(data), + EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), + _ => (), + } + } +} + +impl Handler for CommitteeFinalizer { + type Result = (); + + fn handle(&mut self, msg: CommitteeRequested, ctx: &mut Self::Context) -> Self::Result { + let e3_id = msg.e3_id.clone(); + let submission_deadline = msg.submission_deadline; + + const FINALIZATION_BUFFER_SECONDS: u64 = 1; + + let e3_id_for_log = e3_id.clone(); + let fut = async move { + match e3_evm::helpers::get_current_timestamp().await { + Ok(timestamp) => Some(timestamp), + Err(e) => { + error!( + e3_id = %e3_id_for_log, + error = %e, + "Failed to get current timestamp from RPC" + ); + None + } + } + }; + + let e3_id_for_async = e3_id; + ctx.spawn( + fut.into_actor(self) + .then(move |current_timestamp, act, ctx| { + if let Some(current_timestamp) = current_timestamp { + let seconds_until_deadline = if submission_deadline > current_timestamp { + (submission_deadline - current_timestamp) + FINALIZATION_BUFFER_SECONDS + } else { + info!( + e3_id = %e3_id_for_async, + submission_deadline = submission_deadline, + current_timestamp = current_timestamp, + "Submission deadline already passed, finalizing with buffer" + ); + FINALIZATION_BUFFER_SECONDS + }; + + info!( + e3_id = %e3_id_for_async, + submission_deadline = submission_deadline, + current_timestamp = current_timestamp, + seconds_to_wait = seconds_until_deadline, + "Scheduling committee finalization" + ); + + let bus = act.bus.clone(); + let e3_id_clone = e3_id_for_async.clone(); + + let handle = ctx.run_later( + Duration::from_secs(seconds_until_deadline), + move |act, _ctx| { + info!(e3_id = %e3_id_clone, "Dispatching CommitteeFinalizeRequested event"); + + bus.do_send(EnclaveEvent::from(CommitteeFinalizeRequested { + e3_id: e3_id_clone.clone(), + })); + + act.pending_committees.remove(&e3_id_clone.to_string()); + }, + ); + + act.pending_committees + .insert(e3_id_for_async.to_string(), handle); + } else { + error!( + e3_id = %e3_id_for_async, + "Skipping committee finalization due to timestamp fetch failure" + ); + } + + async {}.into_actor(act) + }), + ); + } +} + +impl Handler for CommitteeFinalizer { + type Result = (); + fn handle(&mut self, _msg: Shutdown, ctx: &mut Self::Context) -> Self::Result { + info!("Killing CommitteeFinalizer"); + // Cancel all pending finalization tasks + for (_, handle) in self.pending_committees.drain() { + ctx.cancel_future(handle); + } + ctx.stop(); + } +} diff --git a/crates/aggregator/src/lib.rs b/crates/aggregator/src/lib.rs index f05f1558d7..1b25bee22c 100644 --- a/crates/aggregator/src/lib.rs +++ b/crates/aggregator/src/lib.rs @@ -4,11 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod committee_finalizer; pub mod ext; mod plaintext_aggregator; mod publickey_aggregator; mod repo; mod threshold_plaintext_aggregator; +pub use committee_finalizer::CommitteeFinalizer; pub use plaintext_aggregator::{ PlaintextAggregator, PlaintextAggregatorParams, PlaintextAggregatorState, }; diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index 88b31d24ac..af7664c5f4 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -11,7 +11,7 @@ use e3_events::{ Die, E3id, EnclaveEvent, EventBus, KeyshareCreated, OrderedSet, PublicKeyAggregated, Seed, }; use e3_fhe::{Fhe, GetAggregatePublicKey}; -use e3_sortition::{GetNodeIndex, GetNodes, Sortition}; +use e3_sortition::{GetNodesForE3, Sortition}; use e3_utils::ArcBytes; use std::sync::Arc; use tracing::{error, trace}; @@ -148,47 +148,32 @@ impl Handler for PublicKeyAggregator { type Result = ResponseActFuture>; fn handle(&mut self, event: KeyshareCreated, _: &mut Self::Context) -> Self::Result { - let Some(PublicKeyAggregatorState::Collecting { - threshold_n, seed, .. - }) = self.state.get() - else { - error!(state=?self.state, "Aggregator has been closed for collecting keyshares."); - return Box::pin(fut::ready(Ok(()))); - }; - - let size = threshold_n; - let address = event.node; - let chain_id = event.e3_id.chain_id(); + let address = event.node.clone(); let e3_id = event.e3_id.clone(); let pubkey = event.pubkey.clone(); Box::pin( self.sortition - .send(GetNodeIndex { - chain_id, - address, - size, - seed, + .send(GetNodesForE3 { + e3_id: e3_id.clone(), + chain_id: e3_id.chain_id(), }) .into_actor(self) .map(move |res, act, ctx| { - // NOTE: Returning Ok(()) on errors as we probably dont need a result type here since - // we will not be doing a send - let maybe_found_index = res?; - let Some(_) = maybe_found_index else { - trace!("Node not found in committee"); + let nodes = res?; + + if !nodes.contains(&address) { + trace!("Node {} not found in finalized committee", address); return Ok(()); - }; + } if e3_id != act.e3_id { error!("Wrong e3_id sent to aggregator. This should not happen."); return Ok(()); } - // add the keyshare and act.add_keyshare(pubkey)?; - // Check the state and if it has changed to the computing if let Some(PublicKeyAggregatorState::Computing { keyshares }) = &act.state.get() { @@ -228,7 +213,8 @@ impl Handler for PublicKeyAggregator { fn handle(&mut self, msg: NotifyNetwork, _: &mut Self::Context) -> Self::Result { Box::pin( self.sortition - .send(GetNodes { + .send(GetNodesForE3 { + e3_id: msg.e3_id.clone(), chain_id: msg.e3_id.chain_id(), }) .into_actor(self) diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index de6056ea14..9beb053e77 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -14,7 +14,7 @@ use e3_events::{ Seed, }; use e3_multithread::Multithread; -use e3_sortition::{GetNodeIndex, Sortition}; +use e3_sortition::{GetNodesForE3, Sortition}; use e3_trbfv::{ calculate_threshold_decryption::{ CalculateThresholdDecryptionRequest, CalculateThresholdDecryptionResponse, @@ -22,7 +22,7 @@ use e3_trbfv::{ TrBFVConfig, TrBFVRequest, }; use e3_utils::utility_types::ArcBytes; -use tracing::{error, info}; +use tracing::{error, info, trace}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Collecting { @@ -248,46 +248,23 @@ impl Handler for ThresholdPlaintextAggregator { fn handle(&mut self, event: DecryptionshareCreated, _: &mut Self::Context) -> Self::Result { info!(event=?event, "Processing DecryptionShareCreated..."); - let Some(ThresholdPlaintextAggregatorState::Collecting(Collecting { - threshold_n, - seed, - .. - })) = self.state.get() - else { - error!(state=?self.state, "Aggregator has been closed for collecting."); - return Box::pin(fut::ready(Ok(()))); - }; - - let size = threshold_n as usize; - let address = event.node; + let address = event.node.clone(); let party_id = event.party_id; - let chain_id = event.e3_id.chain_id(); let e3_id = event.e3_id.clone(); let decryption_share = event.decryption_share.clone(); - // Why do we need to get the node index when the event contains the party_id? I guess we - // don't trust the event. Maybe that is fine. Box::pin( self.sortition - .send(GetNodeIndex { - chain_id, - address: address.clone(), - size, - seed, + .send(GetNodesForE3 { + e3_id: e3_id.clone(), + chain_id: e3_id.chain_id(), }) .into_actor(self) .map(move |res, act, ctx| { - let maybe_found_index = res?; - let Some(party) = maybe_found_index else { - error!("Attempting to aggregate share but party not found in committee"); - return Ok(()); - }; + let nodes = res?; - if party != party_id { - error!( - "Bad aggregation state! Address {} not found at index {} instead it was found at {}", - address, party_id, party - ); + if !nodes.contains(&address) { + trace!("Node {} not found in finalized committee", address); return Ok(()); } @@ -296,10 +273,10 @@ impl Handler for ThresholdPlaintextAggregator { return Ok(()); } - // add the keyshare and + // Trust the party_id from the event - it's based on CommitteeFinalized order + // which is the authoritative source of truth for party IDs act.add_share(party_id, decryption_share)?; - // Check the state and if it has changed to the computing if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { threshold_m, threshold_n, diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 4d6008f970..fdff7df363 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -22,17 +22,21 @@ use e3_evm::{ load_signer_from_repository, ConcreteReadProvider, ConcreteWriteProvider, EthProvider, ProviderConfig, }, + BondingRegistryReaderRepositoryFactory, BondingRegistrySol, CiphernodeRegistryReaderRepositoryFactory, CiphernodeRegistrySol, EnclaveSol, EnclaveSolReader, - EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, RegistryFilterSol, + EnclaveSolReaderRepositoryFactory, EthPrivateKeyRepositoryFactory, }; use e3_fhe::ext::FheExtension; use e3_keyshare::ext::{KeyshareExtension, ThresholdKeyshareExtension}; use e3_multithread::Multithread; use e3_request::E3Router; -use e3_sortition::{CiphernodeSelector, Sortition, SortitionRepositoryFactory}; +use e3_sortition::{ + CiphernodeSelector, FinalizedCommitteesRepositoryFactory, NodeStateRepositoryFactory, + Sortition, SortitionBackend, SortitionRepositoryFactory, +}; use e3_utils::{rand_eth_addr, SharedRng}; use std::{collections::HashMap, sync::Arc}; -use tracing::info; +use tracing::{error, info}; /// Build a ciphernode configuration. // NOTE: We could use a typestate pattern here to separate production and testing methods. I hummed @@ -54,6 +58,7 @@ pub struct CiphernodeBuilder { pubkey_agg: bool, rng: SharedRng, source_bus: Option>>>, + sortition_backend: SortitionBackend, testmode_errors: bool, testmode_history: bool, threads: Option, @@ -64,8 +69,8 @@ pub struct CiphernodeBuilder { pub struct ContractComponents { enclave_reader: bool, enclave: bool, - registry_filter: bool, ciphernode_registry: bool, + bonding_registry: bool, } #[derive(Clone, Debug)] @@ -95,6 +100,7 @@ impl CiphernodeBuilder { pubkey_agg: false, rng, source_bus: None, + sortition_backend: SortitionBackend::score(), testmode_errors: false, testmode_history: false, threads: None, @@ -198,6 +204,12 @@ impl CiphernodeBuilder { self } + /// Use score-based sortition (recommended) + pub fn with_sortition_score(mut self) -> Self { + self.sortition_backend = SortitionBackend::score(); + self + } + /// Setup an Enclave contract reader for every evm chain provided pub fn with_contract_enclave_reader(mut self) -> Self { self.contract_components.enclave_reader = true; @@ -210,12 +222,11 @@ impl CiphernodeBuilder { self } - /// Setup a writable RegistryFilter for every evm chain provided - pub fn with_contract_registry_filter(mut self) -> Self { - self.contract_components.registry_filter = true; + /// Setup a writable BondingRegistry for every evm chain provided + pub fn with_contract_bonding_registry(mut self) -> Self { + self.contract_components.bonding_registry = true; self } - /// Setup a CiphernodeRegistry listener for every evm chain provided pub fn with_contract_ciphernode_registry(mut self) -> Self { self.contract_components.ciphernode_registry = true; @@ -274,10 +285,20 @@ impl CiphernodeBuilder { .unwrap_or_else(|| (&InMemStore::new(self.logging).start()).into()); let repositories = store.repositories(); - let sortition = Sortition::attach(&local_bus, repositories.sortition()).await?; - // Ciphernode Selector - CiphernodeSelector::attach(&local_bus, &sortition, &addr); + // Use the configured backend directly + let default_backend = self.sortition_backend.clone(); + + let sortition = Sortition::attach( + &local_bus, + repositories.sortition(), + repositories.node_state(), + repositories.finalized_committees(), + default_backend, + ) + .await?; + + CiphernodeSelector::attach(&local_bus, &sortition, &addr, &store); let mut provider_cache = ProviderCaches::new(); let cipher = &self.cipher; @@ -319,14 +340,15 @@ impl CiphernodeBuilder { .await?; } - if self.contract_components.registry_filter { - let write_provider = provider_cache - .ensure_write_provider(&repositories, chain, cipher) - .await?; - RegistryFilterSol::attach( + if self.contract_components.bonding_registry { + let read_provider = provider_cache.ensure_read_provider(chain).await?; + BondingRegistrySol::attach( &local_bus, - write_provider.clone(), - &chain.contracts.filter_registry.address(), + read_provider.clone(), + &chain.contracts.bonding_registry.address(), + &repositories.bonding_registry_reader(read_provider.chain_id()), + chain.contracts.bonding_registry.deploy_block(), + chain.rpc_url.clone(), ) .await?; } @@ -342,6 +364,31 @@ impl CiphernodeBuilder { chain.rpc_url.clone(), ) .await?; + + match provider_cache + .ensure_write_provider(&repositories, chain, cipher) + .await + { + Ok(write_provider) => { + let writer = CiphernodeRegistrySol::attach_writer( + &local_bus, + write_provider.clone(), + &chain.contracts.ciphernode_registry.address(), + self.pubkey_agg, + ) + .await?; + info!("CiphernodeRegistrySolWriter attached for publishing committees"); + + if self.pubkey_agg && matches!(self.sortition_backend, SortitionBackend::Score(_)) { + info!("Attaching CommitteeFinalizer for score sortition"); + e3_aggregator::CommitteeFinalizer::attach(&local_bus); + } + } + Err(e) => error!( + "Failed to create write provider (likely no wallet configured), skipping writer attachment: {}", + e + ), + } } } diff --git a/crates/cli/src/print_env.rs b/crates/cli/src/print_env.rs index 4cdcb8dd32..0a6ffa25f5 100644 --- a/crates/cli/src/print_env.rs +++ b/crates/cli/src/print_env.rs @@ -14,17 +14,20 @@ pub fn extract_env_vars_vite(config: &AppConfig, chain: &str) -> String { if let Some(chain) = config.chains().iter().find(|c| c.name == chain.to_string()) { let enclave_addr = &chain.contracts.enclave; let registry_addr = &chain.contracts.ciphernode_registry; - let filter_addr = &chain.contracts.filter_registry; + let bonding_registry_addr = &chain.contracts.bonding_registry; env_vars.push(format!("VITE_ENCLAVE_ADDRESS={}", enclave_addr.address())); env_vars.push(format!("VITE_REGISTRY_ADDRESS={}", registry_addr.address())); env_vars.push(format!("VITE_RPC_URL={}", chain.rpc_url)); env_vars.push(format!( - "VITE_FILTER_REGISTRY_ADDRESS={}", - filter_addr.address() + "VITE_BONDING_REGISTRY_ADDRESS={}", + bonding_registry_addr.address() )); if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("VITE_E3_PROGRAM_ADDRESS={}", e3_program.address())); } + if let Some(fee_token) = &chain.contracts.fee_token { + env_vars.push(format!("VITE_FEE_TOKEN_ADDRESS={}", fee_token.address())); + } } env_vars.join(" ") @@ -37,14 +40,20 @@ pub fn extract_env_vars(config: &AppConfig, chain: &str) -> String { if let Some(chain) = config.chains().iter().find(|c| c.name == chain.to_string()) { let enclave_addr = &chain.contracts.enclave; let registry_addr = &chain.contracts.ciphernode_registry; - let filter_addr = &chain.contracts.filter_registry; + let bonding_registry_addr = &chain.contracts.bonding_registry; env_vars.push(format!("ENCLAVE_ADDRESS={}", enclave_addr.address())); env_vars.push(format!("RPC_URL={}", chain.rpc_url)); env_vars.push(format!("REGISTRY_ADDRESS={}", registry_addr.address())); - env_vars.push(format!("FILTER_REGISTRY_ADDRESS={}", filter_addr.address())); + env_vars.push(format!( + "BONDING_REGISTRY_ADDRESS={}", + bonding_registry_addr.address() + )); if let Some(e3_program) = &chain.contracts.e3_program { env_vars.push(format!("E3_PROGRAM_ADDRESS={}", e3_program.address())); } + if let Some(fee_token) = &chain.contracts.fee_token { + env_vars.push(format!("FEE_TOKEN_ADDRESS={}", fee_token.address())); + } } env_vars.join(" ") diff --git a/crates/config/src/app_config.rs b/crates/config/src/app_config.rs index 8322954b05..a12e66c6a1 100644 --- a/crates/config/src/app_config.rs +++ b/crates/config/src/app_config.rs @@ -482,7 +482,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" node: config_dir: "/myconfig/override" @@ -651,7 +651,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; @@ -693,7 +693,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; config = load_config("_default", None, None).map_err(|err| err.to_string())?; @@ -715,7 +715,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; @@ -760,7 +760,7 @@ chains: ciphernode_registry: address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" deploy_block: 1764352873645 - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + bonding_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" "#, )?; diff --git a/crates/config/src/contract.rs b/crates/config/src/contract.rs index d041feff82..cf8115d978 100644 --- a/crates/config/src/contract.rs +++ b/crates/config/src/contract.rs @@ -38,6 +38,7 @@ impl Contract { pub struct ContractAddresses { pub enclave: Contract, pub ciphernode_registry: Contract, - pub filter_registry: Contract, + pub bonding_registry: Contract, pub e3_program: Option, + pub fee_token: Option, } diff --git a/crates/config/src/store_keys.rs b/crates/config/src/store_keys.rs index ffb9f76cf5..5cdc6ee26f 100644 --- a/crates/config/src/store_keys.rs +++ b/crates/config/src/store_keys.rs @@ -60,4 +60,16 @@ impl StoreKeys { pub fn ciphernode_registry_reader(chain_id: u64) -> String { format!("//evm_readers/ciphernode_registry/{chain_id}") } + + pub fn bonding_registry_reader(chain_id: u64) -> String { + format!("//evm_readers/bonding_registry/{chain_id}") + } + + pub fn node_state() -> String { + String::from("//node_state") + } + + pub fn finalized_committees() -> String { + String::from("//finalized_committees") + } } diff --git a/crates/docs/user_guide.md b/crates/docs/user_guide.md index e36765a4f6..bb179ec488 100644 --- a/crates/docs/user_guide.md +++ b/crates/docs/user_guide.md @@ -35,7 +35,7 @@ chains: ciphernode_registry: address: "0x0952388f6028a9Eda93a5041a3B216Ea331d97Ab" deploy_block: 7073318 - filter_registry: + bonding_registry: address: "0xcBaCE7C360b606bb554345b20884A28e41436934" deploy_block: 7073319 ``` @@ -99,7 +99,7 @@ Ciphernodes need a registration address to identify themselves within a committe ``` # ~/.config/enclave/config.yaml -address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" +address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" ``` ## Setting your encryption password diff --git a/crates/entrypoint/src/config_set/mod.rs b/crates/entrypoint/src/config_set/mod.rs index d083d543ae..a2ba685379 100644 --- a/crates/entrypoint/src/config_set/mod.rs +++ b/crates/entrypoint/src/config_set/mod.rs @@ -60,7 +60,7 @@ chains: ciphernode_registry: address: "{}" deploy_block: {} - filter_registry: + bonding_registry: address: "{}" deploy_block: {} "#, @@ -73,8 +73,8 @@ chains: get_contract_info("Enclave")?.deploy_block, get_contract_info("CiphernodeRegistryOwnable")?.address, get_contract_info("CiphernodeRegistryOwnable")?.deploy_block, - get_contract_info("NaiveRegistryFilter")?.address, - get_contract_info("NaiveRegistryFilter")?.deploy_block, + get_contract_info("BondingRegistry")?.address, + get_contract_info("BondingRegistry")?.deploy_block, ); fs::write(config_path.clone(), config_content)?; diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 9154d9d884..d0cfd37d1e 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -38,8 +38,9 @@ pub async fn execute( .with_source_bus(&bus) .with_datastore(store) .with_chains(&config.chains()) + .with_sortition_score() .with_contract_enclave_full() - .with_contract_registry_filter() + .with_contract_bonding_registry() .with_contract_ciphernode_registry() .with_plaintext_aggregation() .with_pubkey_aggregation() diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index c88d0d026c..1fa874a5d6 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -39,8 +39,10 @@ pub async fn execute( .with_keyshare() .with_source_bus(&bus) .with_datastore(store) + .with_sortition_score() .with_chains(&config.chains()) .with_contract_enclave_reader() + .with_contract_bonding_registry() .with_contract_ciphernode_registry() .build() .await?; diff --git a/crates/events/src/enclave_event/committee_finalize_requested.rs b/crates/events/src/enclave_event/committee_finalize_requested.rs new file mode 100644 index 0000000000..b91780f62c --- /dev/null +++ b/crates/events/src/enclave_event/committee_finalize_requested.rs @@ -0,0 +1,22 @@ +// 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::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeFinalizeRequested { + pub e3_id: E3id, +} + +impl Display for CommitteeFinalizeRequested { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/committee_finalized.rs b/crates/events/src/enclave_event/committee_finalized.rs new file mode 100644 index 0000000000..69bdb9cfa3 --- /dev/null +++ b/crates/events/src/enclave_event/committee_finalized.rs @@ -0,0 +1,24 @@ +// 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::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeFinalized { + pub e3_id: E3id, + pub committee: Vec, + pub chain_id: u64, +} + +impl Display for CommitteeFinalized { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/committee_published.rs b/crates/events/src/enclave_event/committee_published.rs new file mode 100644 index 0000000000..abb2e79dfa --- /dev/null +++ b/crates/events/src/enclave_event/committee_published.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::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteePublished { + pub e3_id: E3id, + pub nodes: Vec, + pub public_key: Vec, +} + +impl Display for CommitteePublished { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, nodes: {:?}, public_key_len: {}", + self.e3_id, + self.nodes, + self.public_key.len() + ) + } +} diff --git a/crates/events/src/enclave_event/committee_requested.rs b/crates/events/src/enclave_event/committee_requested.rs new file mode 100644 index 0000000000..ebe8b4a08c --- /dev/null +++ b/crates/events/src/enclave_event/committee_requested.rs @@ -0,0 +1,31 @@ +// 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::{E3id, Seed}; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeRequested { + pub e3_id: E3id, + pub seed: Seed, + pub threshold: [usize; 2], + pub request_block: u64, + pub submission_deadline: u64, + pub chain_id: u64, +} + +impl Display for CommitteeRequested { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, seed: {:?}, threshold: [{}, {}], request_block: {}, submission_deadline: {}, chain_id: {}", + self.e3_id, self.seed, self.threshold[0], self.threshold[1], self.request_block, self.submission_deadline, self.chain_id + ) + } +} diff --git a/crates/events/src/enclave_event/configuration_updated.rs b/crates/events/src/enclave_event/configuration_updated.rs new file mode 100644 index 0000000000..2185e53ea9 --- /dev/null +++ b/crates/events/src/enclave_event/configuration_updated.rs @@ -0,0 +1,29 @@ +// 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 actix::Message; +use alloy::primitives::U256; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct ConfigurationUpdated { + pub parameter: String, + pub old_value: U256, + pub new_value: U256, + pub chain_id: u64, +} + +impl Display for ConfigurationUpdated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "parameter: {}, old_value: {}, new_value: {}, chain_id: {}", + self.parameter, self.old_value, self.new_value, self.chain_id + ) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index bccc0aee7e..4c6cb2cf39 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -8,37 +8,57 @@ mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; mod ciphertext_output_published; +mod committee_finalize_requested; +mod committee_finalized; +mod committee_published; +mod committee_requested; mod compute_request; +mod configuration_updated; mod decryptionshare_created; mod die; mod e3_request_complete; mod e3_requested; mod enclave_error; mod keyshare_created; +mod operator_activation_changed; mod plaintext_aggregated; +mod plaintext_output_published; mod publickey_aggregated; mod publish_document; mod shutdown; mod test_event; mod threshold_share_created; +mod ticket_balance_updated; +mod ticket_generated; +mod ticket_submitted; pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; pub use ciphertext_output_published::*; +pub use committee_finalize_requested::*; +pub use committee_finalized::*; +pub use committee_published::*; +pub use committee_requested::*; pub use compute_request::*; +pub use configuration_updated::*; pub use decryptionshare_created::*; pub use die::*; pub use e3_request_complete::*; pub use e3_requested::*; pub use enclave_error::*; pub use keyshare_created::*; +pub use operator_activation_changed::*; pub use plaintext_aggregated::*; +pub use plaintext_output_published::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use shutdown::*; pub use test_event::*; pub use threshold_share_created::*; +pub use ticket_balance_updated::*; +pub use ticket_generated::*; +pub use ticket_submitted::*; use crate::{E3id, ErrorEvent, Event, EventId}; use actix::Message; @@ -107,6 +127,46 @@ pub enum EnclaveEvent { id: EventId, data: CiphernodeRemoved, }, + TicketBalanceUpdated { + id: EventId, + data: TicketBalanceUpdated, + }, + ConfigurationUpdated { + id: EventId, + data: ConfigurationUpdated, + }, + OperatorActivationChanged { + id: EventId, + data: OperatorActivationChanged, + }, + CommitteePublished { + id: EventId, + data: CommitteePublished, + }, + CommitteeRequested { + id: EventId, + data: CommitteeRequested, + }, + CommitteeFinalizeRequested { + id: EventId, + data: CommitteeFinalizeRequested, + }, + CommitteeFinalized { + id: EventId, + data: CommitteeFinalized, + }, + TicketGenerated { + id: EventId, + data: TicketGenerated, + }, + TicketSubmitted { + id: EventId, + data: TicketSubmitted, + }, + PlaintextOutputPublished { + id: EventId, + data: PlaintextOutputPublished, + }, EnclaveError { id: EventId, data: EnclaveError, @@ -189,12 +249,22 @@ impl From for EventId { EnclaveEvent::CiphernodeSelected { id, .. } => id, EnclaveEvent::CiphernodeAdded { id, .. } => id, EnclaveEvent::CiphernodeRemoved { id, .. } => id, + EnclaveEvent::TicketBalanceUpdated { id, .. } => id, + EnclaveEvent::ConfigurationUpdated { id, .. } => id, + EnclaveEvent::OperatorActivationChanged { id, .. } => id, + EnclaveEvent::CommitteePublished { id, .. } => id, + EnclaveEvent::CommitteeRequested { id, .. } => id, + EnclaveEvent::CommitteeFinalizeRequested { id, .. } => id, + EnclaveEvent::PlaintextOutputPublished { id, .. } => id, EnclaveEvent::EnclaveError { id, .. } => id, EnclaveEvent::E3RequestComplete { id, .. } => id, EnclaveEvent::Shutdown { id, .. } => id, EnclaveEvent::TestEvent { id, .. } => id, EnclaveEvent::DocumentReceived { id, .. } => id, EnclaveEvent::ThresholdShareCreated { id, .. } => id, + EnclaveEvent::CommitteeFinalized { id, .. } => id, + EnclaveEvent::TicketGenerated { id, .. } => id, + EnclaveEvent::TicketSubmitted { id, .. } => id, } } } @@ -210,6 +280,13 @@ impl EnclaveEvent { EnclaveEvent::PlaintextAggregated { data, .. } => Some(data.e3_id), EnclaveEvent::CiphernodeSelected { data, .. } => Some(data.e3_id), EnclaveEvent::ThresholdShareCreated { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteePublished { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteeRequested { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteeFinalizeRequested { data, .. } => Some(data.e3_id), + EnclaveEvent::PlaintextOutputPublished { data, .. } => Some(data.e3_id), + EnclaveEvent::CommitteeFinalized { data, .. } => Some(data.e3_id), + EnclaveEvent::TicketGenerated { data, .. } => Some(data.e3_id), + EnclaveEvent::TicketSubmitted { data, .. } => Some(data.e3_id), _ => None, } } @@ -225,12 +302,22 @@ impl EnclaveEvent { EnclaveEvent::CiphernodeSelected { data, .. } => format!("{}", data), EnclaveEvent::CiphernodeAdded { data, .. } => format!("{}", data), EnclaveEvent::CiphernodeRemoved { data, .. } => format!("{}", data), + EnclaveEvent::TicketBalanceUpdated { data, .. } => format!("{:?}", data), + EnclaveEvent::ConfigurationUpdated { data, .. } => format!("{:?}", data), + EnclaveEvent::OperatorActivationChanged { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteePublished { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteeRequested { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteeFinalizeRequested { data, .. } => format!("{:?}", data), + EnclaveEvent::PlaintextOutputPublished { data, .. } => format!("{:?}", data), EnclaveEvent::E3RequestComplete { data, .. } => format!("{}", data), EnclaveEvent::EnclaveError { data, .. } => format!("{:?}", data), EnclaveEvent::Shutdown { data, .. } => format!("{:?}", data), EnclaveEvent::ThresholdShareCreated { data, .. } => format!("{:?}", data), EnclaveEvent::TestEvent { data, .. } => format!("{:?}", data), EnclaveEvent::DocumentReceived { data, .. } => format!("{:?}", data), + EnclaveEvent::CommitteeFinalized { data, .. } => format!("{:?}", data), + EnclaveEvent::TicketGenerated { data, .. } => format!("{:?}", data), + EnclaveEvent::TicketSubmitted { data, .. } => format!("{:?}", data), // _ => "".to_string(), } } @@ -248,6 +335,16 @@ impl_from_event!( CiphernodeSelected, CiphernodeAdded, CiphernodeRemoved, + TicketBalanceUpdated, + ConfigurationUpdated, + OperatorActivationChanged, + CommitteePublished, + CommitteeRequested, + CommitteeFinalizeRequested, + CommitteeFinalized, + TicketGenerated, + TicketSubmitted, + PlaintextOutputPublished, EnclaveError, Shutdown, TestEvent, diff --git a/crates/events/src/enclave_event/operator_activation_changed.rs b/crates/events/src/enclave_event/operator_activation_changed.rs new file mode 100644 index 0000000000..c979b5e4c4 --- /dev/null +++ b/crates/events/src/enclave_event/operator_activation_changed.rs @@ -0,0 +1,16 @@ +// 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 actix::Message; +use serde::{Deserialize, Serialize}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct OperatorActivationChanged { + pub operator: String, + pub active: bool, + pub chain_id: u64, +} diff --git a/crates/events/src/enclave_event/plaintext_output_published.rs b/crates/events/src/enclave_event/plaintext_output_published.rs new file mode 100644 index 0000000000..aa0c715afc --- /dev/null +++ b/crates/events/src/enclave_event/plaintext_output_published.rs @@ -0,0 +1,28 @@ +// 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::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct PlaintextOutputPublished { + pub e3_id: E3id, + pub plaintext_output: Vec, +} + +impl Display for PlaintextOutputPublished { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, plaintext_output_len: {}", + self.e3_id, + self.plaintext_output.len() + ) + } +} diff --git a/crates/events/src/enclave_event/ticket_balance_updated.rs b/crates/events/src/enclave_event/ticket_balance_updated.rs new file mode 100644 index 0000000000..43133f19b7 --- /dev/null +++ b/crates/events/src/enclave_event/ticket_balance_updated.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 actix::Message; +use alloy::primitives::{FixedBytes, I256, U256}; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct TicketBalanceUpdated { + pub operator: String, + pub delta: I256, + pub new_balance: U256, + pub reason: FixedBytes<32>, + pub chain_id: u64, +} + +impl Display for TicketBalanceUpdated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "operator: {}, delta: {}, new_balance: {}, chain_id: {}", + self.operator, self.delta, self.new_balance, self.chain_id + ) + } +} diff --git a/crates/events/src/enclave_event/ticket_generated.rs b/crates/events/src/enclave_event/ticket_generated.rs new file mode 100644 index 0000000000..1530240705 --- /dev/null +++ b/crates/events/src/enclave_event/ticket_generated.rs @@ -0,0 +1,33 @@ +// 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::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TicketId { + Score(u64), +} + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct TicketGenerated { + pub e3_id: E3id, + pub ticket_id: TicketId, + pub node: String, +} + +impl Display for TicketGenerated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "e3_id: {}, ticket_id: {:?}, node: {}", + self.e3_id, self.ticket_id, self.node + ) + } +} diff --git a/crates/events/src/enclave_event/ticket_submitted.rs b/crates/events/src/enclave_event/ticket_submitted.rs new file mode 100644 index 0000000000..4b969f3de6 --- /dev/null +++ b/crates/events/src/enclave_event/ticket_submitted.rs @@ -0,0 +1,26 @@ +// 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::E3id; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct TicketSubmitted { + pub e3_id: E3id, + pub node: String, + pub ticket_id: u64, + pub score: String, + pub chain_id: u64, +} + +impl Display for TicketSubmitted { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index 6c9bf7d230..db56937b86 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -59,7 +59,6 @@ sol! { #[derive(Debug)] struct E3RequestParams { - address filter; uint32[2] threshold; uint256[2] startWindow; uint256 duration; @@ -76,7 +75,7 @@ sol! { mapping(uint256 e3Id => uint256 inputCount) public inputCounts; mapping(uint256 e3Id => bytes params) public e3Params; mapping(address e3Program => bool allowed) public e3Programs; - function request(E3RequestParams memory request) external payable returns (uint256 e3Id, E3 memory e3); + function request(E3RequestParams calldata requestParams) external returns (uint256 e3Id, E3 memory e3); function activate(uint256 e3Id,bytes calldata publicKey) external returns (bool success); function enableE3Program(address e3Program) public onlyOwner returns (bool success); function publishInput(uint256 e3Id, bytes calldata data) external returns (bool success); @@ -84,6 +83,7 @@ sol! { function publishPlaintextOutput(uint256 e3Id, bytes calldata data, bytes calldata proof) external returns (bool success); function getE3(uint256 e3Id) external view returns (E3 memory e3); function getInputRoot(uint256 e3Id) public view returns (uint256); + function getE3Quote(E3RequestParams memory request) external view returns (uint256 fee); } } @@ -110,6 +110,17 @@ pub trait EnclaveRead { /// Check if an E3 program is enabled async fn is_e3_program_enabled(&self, e3_program: Address) -> Result; + + /// Get the fee quote for an E3 request + async fn get_e3_quote( + &self, + threshold: [u32; 2], + start_window: [U256; 2], + duration: U256, + e3_program: Address, + e3_params: Bytes, + compute_provider_params: Bytes, + ) -> Result; } /// Trait for write operations on the Enclave contract @@ -118,7 +129,6 @@ pub trait EnclaveWrite { /// Request a new E3 async fn request_e3( &self, - filter: Address, threshold: [u32; 2], start_window: [U256; 2], duration: U256, @@ -334,6 +344,30 @@ where let enabled = contract.e3Programs(e3_program).call().await?; Ok(enabled) } + + async fn get_e3_quote( + &self, + threshold: [u32; 2], + start_window: [U256; 2], + duration: U256, + e3_program: Address, + e3_params: Bytes, + compute_provider_params: Bytes, + ) -> Result { + let e3_request = E3RequestParams { + threshold, + startWindow: start_window, + duration, + e3Program: e3_program, + e3ProgramParams: e3_params, + computeProviderParams: compute_provider_params, + customParams: Bytes::new(), + }; + + let contract = Enclave::new(self.contract_address, &self.provider); + let fee = contract.getE3Quote(e3_request).call().await?; + Ok(fee) + } } // Implement EnclaveWrite only for contracts with ReadWrite marker @@ -341,7 +375,6 @@ where impl EnclaveWrite for EnclaveContract { async fn request_e3( &self, - filter: Address, threshold: [u32; 2], start_window: [U256; 2], duration: U256, @@ -357,7 +390,6 @@ impl EnclaveWrite for EnclaveContract { let nonce = get_next_nonce(&*self.provider, wallet_addr).await?; let e3_request = E3RequestParams { - filter, threshold, startWindow: start_window, duration, @@ -368,10 +400,7 @@ impl EnclaveWrite for EnclaveContract { }; let contract = Enclave::new(self.contract_address, &self.provider); - let builder = contract - .request(e3_request) - .value(U256::from(1)) - .nonce(nonce); + let builder = contract.request(e3_request).nonce(nonce); let receipt = builder.send().await?.get_receipt().await?; Ok(receipt) diff --git a/crates/evm-helpers/src/events.rs b/crates/evm-helpers/src/events.rs index c455bf3f8c..56566c8b71 100644 --- a/crates/evm-helpers/src/events.rs +++ b/crates/evm-helpers/src/events.rs @@ -13,7 +13,7 @@ sol! { event E3Activated(uint256 e3Id, uint256 expiration, bytes committeePublicKey); #[derive(Debug)] - event E3Requested(uint256 e3Id, E3 e3, address filter, IE3Program indexed e3Program); + event E3Requested(uint256 e3Id, E3 e3, IE3Program indexed e3Program); #[derive(Debug)] interface IE3Program { @@ -59,5 +59,5 @@ sol! { event PlaintextOutputPublished(uint256 indexed e3Id, bytes plaintextOutput); #[derive(Debug)] - event CommitteePublished(uint256 indexed e3Id, bytes publicKey); + event CommitteePublished(uint256 indexed e3Id, address[] nodes, bytes publicKey); } diff --git a/crates/evm/src/bonding_registry_sol.rs b/crates/evm/src/bonding_registry_sol.rs new file mode 100644 index 0000000000..0cec4ca360 --- /dev/null +++ b/crates/evm/src/bonding_registry_sol.rs @@ -0,0 +1,198 @@ +// 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::{event_reader::EvmEventReaderState, helpers::EthProvider, EvmEventReader}; +use actix::Addr; +use alloy::{ + primitives::{LogData, B256}, + providers::Provider, + sol, + sol_types::SolEvent, +}; +use anyhow::Result; +use e3_data::Repository; +use e3_events::{EnclaveEvent, EventBus}; +use tracing::{error, info, trace}; + +sol!( + #[sol(rpc)] + #[derive(Debug)] + IBondingRegistry, + "../../packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json" +); + +struct TicketBalanceUpdatedWithChainId(pub IBondingRegistry::TicketBalanceUpdated, pub u64); + +impl From for e3_events::TicketBalanceUpdated { + fn from(value: TicketBalanceUpdatedWithChainId) -> Self { + e3_events::TicketBalanceUpdated { + operator: value.0.operator.to_string(), + delta: value.0.delta, + new_balance: value.0.newBalance, + reason: value.0.reason, + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: TicketBalanceUpdatedWithChainId) -> Self { + let payload: e3_events::TicketBalanceUpdated = value.into(); + EnclaveEvent::from(payload) + } +} + +struct ConfigurationUpdatedWithChainId(pub IBondingRegistry::ConfigurationUpdated, pub u64); + +impl From for e3_events::ConfigurationUpdated { + fn from(value: ConfigurationUpdatedWithChainId) -> Self { + let param_bytes = value.0.parameter.as_slice(); + let param_str = String::from_utf8( + param_bytes + .iter() + .copied() + .take_while(|&b| b != 0) + .collect(), + ) + .unwrap_or_else(|_| value.0.parameter.to_string()); + + e3_events::ConfigurationUpdated { + parameter: param_str, + old_value: value.0.oldValue, + new_value: value.0.newValue, + chain_id: value.1, + } + } +} + +struct OperatorActivationChangedWithChainId( + pub IBondingRegistry::OperatorActivationChanged, + pub u64, +); + +impl From for e3_events::OperatorActivationChanged { + fn from(value: OperatorActivationChangedWithChainId) -> Self { + e3_events::OperatorActivationChanged { + operator: value.0.operator.to_string(), + active: value.0.active, + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: OperatorActivationChangedWithChainId) -> Self { + let payload: e3_events::OperatorActivationChanged = value.into(); + EnclaveEvent::from(payload) + } +} + +impl From for EnclaveEvent { + fn from(value: ConfigurationUpdatedWithChainId) -> Self { + let payload: e3_events::ConfigurationUpdated = value.into(); + EnclaveEvent::from(payload) + } +} + +pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { + match topic { + Some(&IBondingRegistry::TicketBalanceUpdated::SIGNATURE_HASH) => { + let Ok(event) = IBondingRegistry::TicketBalanceUpdated::decode_log_data(data) else { + error!("Error parsing event TicketBalanceUpdated after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(TicketBalanceUpdatedWithChainId( + event, chain_id, + ))) + } + Some(&IBondingRegistry::OperatorActivationChanged::SIGNATURE_HASH) => { + let Ok(event) = IBondingRegistry::OperatorActivationChanged::decode_log_data(data) + else { + error!("Error parsing event OperatorActivationChanged after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(OperatorActivationChangedWithChainId( + event, chain_id, + ))) + } + Some(&IBondingRegistry::ConfigurationUpdated::SIGNATURE_HASH) => { + let Ok(event) = IBondingRegistry::ConfigurationUpdated::decode_log_data(data) else { + error!("Error parsing event ConfigurationUpdated after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(ConfigurationUpdatedWithChainId( + event, chain_id, + ))) + } + _topic => { + trace!( + topic=?_topic, + "Unknown event was received by BondingRegistry.sol parser but was ignored" + ); + None + } + } +} + +/// Connects to BondingRegistry.sol converting EVM events to EnclaveEvents +pub struct BondingRegistrySolReader; + +impl BondingRegistrySolReader { + pub async fn attach

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + ) -> Result>> + where + P: Provider + Clone + 'static, + { + let addr = EvmEventReader::attach( + provider, + extractor, + contract_address, + start_block, + &bus.clone().into(), + repository, + rpc_url, + ) + .await?; + + info!(address=%contract_address, "BondingRegistrySolReader is listening to address"); + + Ok(addr) + } +} + +/// Wrapper for a reader +pub struct BondingRegistrySol; + +impl BondingRegistrySol { + pub async fn attach

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + repository: &Repository, + start_block: Option, + rpc_url: String, + ) -> Result<()> + where + P: Provider + Clone + 'static, + { + BondingRegistrySolReader::attach( + bus, + provider, + contract_address, + repository, + start_block, + rpc_url, + ) + .await?; + Ok(()) + } +} diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index b2f1353bb1..9ba569b813 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -5,16 +5,21 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::{event_reader::EvmEventReaderState, helpers::EthProvider, EvmEventReader}; -use actix::Addr; +use actix::prelude::*; use alloy::{ - primitives::{LogData, B256}, - providers::Provider, + primitives::{Address, Bytes, LogData, B256, U256}, + providers::{Provider, WalletProvider}, + rpc::types::TransactionReceipt, sol, sol_types::SolEvent, }; use anyhow::Result; use e3_data::Repository; -use e3_events::{EnclaveEvent, EventBus}; +use e3_events::{ + BusError, CommitteeFinalizeRequested, CommitteeFinalized, E3id, EnclaveErrorType, EnclaveEvent, + EventBus, OrderedSet, PublicKeyAggregated, Seed, Shutdown, Subscribe, TicketGenerated, + TicketId, +}; use tracing::{error, info, trace}; sol!( @@ -81,6 +86,73 @@ impl From for EnclaveEvent { } } +struct CommitteeRequestedWithChainId(pub ICiphernodeRegistry::CommitteeRequested, pub u64); + +impl From for e3_events::CommitteeRequested { + fn from(value: CommitteeRequestedWithChainId) -> Self { + e3_events::CommitteeRequested { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + seed: Seed(value.0.seed.to_be_bytes()), + threshold: [value.0.threshold[0] as usize, value.0.threshold[1] as usize], + request_block: value.0.requestBlock.to(), + submission_deadline: value.0.submissionDeadline.to(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: CommitteeRequestedWithChainId) -> Self { + let payload: e3_events::CommitteeRequested = value.into(); + EnclaveEvent::from(payload) + } +} + +struct CommitteeFinalizedWithChainId(pub ICiphernodeRegistry::CommitteeFinalized, pub u64); + +impl From for CommitteeFinalized { + fn from(value: CommitteeFinalizedWithChainId) -> Self { + e3_events::CommitteeFinalized { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + committee: value + .0 + .committee + .iter() + .map(|addr| addr.to_string()) + .collect(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: CommitteeFinalizedWithChainId) -> Self { + let payload: e3_events::CommitteeFinalized = value.into(); + EnclaveEvent::from(payload) + } +} + +struct TicketSubmittedWithChainId(pub ICiphernodeRegistry::TicketSubmitted, pub u64); + +impl From for e3_events::TicketSubmitted { + fn from(value: TicketSubmittedWithChainId) -> Self { + e3_events::TicketSubmitted { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + node: value.0.node.to_string(), + ticket_id: value.0.ticketId.to(), + score: value.0.score.to_string(), + chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: TicketSubmittedWithChainId) -> Self { + let payload: e3_events::TicketSubmitted = value.into(); + EnclaveEvent::from(payload) + } +} + pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { match topic { Some(&ICiphernodeRegistry::CiphernodeAdded::SIGNATURE_HASH) => { @@ -101,6 +173,33 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< event, chain_id, ))) } + Some(&ICiphernodeRegistry::CommitteeRequested::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CommitteeRequested::decode_log_data(data) else { + error!("Error parsing event CommitteeRequested after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(CommitteeRequestedWithChainId( + event, chain_id, + ))) + } + Some(&ICiphernodeRegistry::CommitteeFinalized::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CommitteeFinalized::decode_log_data(data) else { + error!("Error parsing event CommitteeFinalized after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(CommitteeFinalizedWithChainId( + event, chain_id, + ))) + } + Some(&ICiphernodeRegistry::TicketSubmitted::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::TicketSubmitted::decode_log_data(data) else { + error!("Error parsing event TicketSubmitted after topic was matched!"); + return None; + }; + Some(EnclaveEvent::from(TicketSubmittedWithChainId( + event, chain_id, + ))) + } _topic => { trace!( topic=?_topic, @@ -143,7 +242,268 @@ impl CiphernodeRegistrySolReader { } } -/// Wrapper for a reader and a future writer +/// Writer for publishing committees to CiphernodeRegistry +pub struct CiphernodeRegistrySolWriter

{ + provider: EthProvider

, + contract_address: Address, + bus: Addr>, +} + +impl CiphernodeRegistrySolWriter

{ + pub async fn new( + bus: &Addr>, + provider: EthProvider

, + contract_address: Address, + ) -> Result { + Ok(Self { + provider, + contract_address, + bus: bus.clone(), + }) + } + + pub async fn attach( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + is_aggregator: bool, + ) -> Result>> { + let addr = CiphernodeRegistrySolWriter::new(bus, provider, contract_address.parse()?) + .await? + .start(); + + if is_aggregator { + let _ = bus + .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) + .await; + let _ = bus + .send(Subscribe::new( + "CommitteeFinalizeRequested", + addr.clone().into(), + )) + .await; + } + + // Subscribe to TicketGenerated for ticket submission + let _ = bus + .send(Subscribe::new("TicketGenerated", addr.clone().into())) + .await; + + // Stop gracefully on shutdown + let _ = bus + .send(Subscribe::new("Shutdown", addr.clone().into())) + .await; + + Ok(addr) + } +} + +impl Actor for CiphernodeRegistrySolWriter

{ + type Context = actix::Context; +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::PublicKeyAggregated { data, .. } => { + // Only publish if the src and destination chains match + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } + EnclaveEvent::CommitteeFinalizeRequested { data, .. } => { + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } + EnclaveEvent::TicketGenerated { data, .. } => { + // Submit ticket if chain matches + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } + EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), + _ => (), + } + } +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: TicketGenerated, _: &mut Self::Context) -> Self::Result { + match msg.ticket_id { + TicketId::Score(ticket_id) => { + info!( + "Score sortition ticket generated for E3 {:?}, submitting to contract", + msg.e3_id + ); + + let e3_id = msg.e3_id.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + Box::pin(async move { + info!("Submitting ticket {} for E3 {:?}", ticket_id, e3_id); + + let result = + submit_ticket_to_registry(provider, contract_address, e3_id, ticket_id) + .await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Ticket submitted to registry"); + } + Err(err) => { + error!("Failed to submit ticket: {:?}", err); + bus.err(EnclaveErrorType::Evm, err); + } + } + }) + } + } + } +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: CommitteeFinalizeRequested, _: &mut Self::Context) -> Self::Result { + let e3_id = msg.e3_id.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + Box::pin(async move { + info!("Finalizing committee for E3 {:?}", e3_id); + + let result = finalize_committee_on_registry(provider, contract_address, e3_id).await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Committee finalized on registry"); + } + Err(err) => { + error!("Failed to finalize committee: {:?}", err); + bus.err(EnclaveErrorType::Evm, err); + } + } + }) + } +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: PublicKeyAggregated, _: &mut Self::Context) -> Self::Result { + let e3_id = msg.e3_id.clone(); + let pubkey = msg.pubkey.clone(); + let nodes = msg.nodes.clone(); + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + Box::pin(async move { + let result = + publish_committee_to_registry(provider, contract_address, e3_id, nodes, pubkey) + .await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Committee published to registry"); + } + Err(err) => bus.err(EnclaveErrorType::Evm, err), + } + }) + } +} + +impl Handler + for CiphernodeRegistrySolWriter

+{ + type Result = (); + + fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { + ctx.stop(); + } +} + +pub async fn submit_ticket_to_registry( + provider: EthProvider

, + contract_address: Address, + e3_id: E3id, + ticket_number: u64, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let ticket_number = U256::from(ticket_number); + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + let contract = ICiphernodeRegistry::new(contract_address, provider.provider()); + let builder = contract + .submitTicket(e3_id, ticket_number) + .nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + +pub async fn finalize_committee_on_registry( + provider: EthProvider

, + contract_address: Address, + e3_id: E3id, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + let contract = ICiphernodeRegistry::new(contract_address, provider.provider()); + let builder = contract.finalizeCommittee(e3_id).nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + +pub async fn publish_committee_to_registry( + provider: EthProvider

, + contract_address: Address, + e3_id: E3id, + nodes: OrderedSet, + public_key: Vec, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let public_key = Bytes::from(public_key); + let nodes_vec: Vec

= nodes + .into_iter() + .filter_map(|node| node.parse().ok()) + .collect(); + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + let contract = ICiphernodeRegistry::new(contract_address, provider.provider()); + let builder = contract + .publishCommittee(e3_id, nodes_vec, public_key) + .nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + +/// Wrapper for a reader and writer pub struct CiphernodeRegistrySol; impl CiphernodeRegistrySol { @@ -167,7 +527,21 @@ impl CiphernodeRegistrySol { rpc_url, ) .await?; - // TODO: Writer if needed Ok(()) } + + pub async fn attach_writer

( + bus: &Addr>, + provider: EthProvider

, + contract_address: &str, + is_aggregator: bool, + ) -> Result>> + where + P: Provider + WalletProvider + Clone + 'static, + { + let writer = + CiphernodeRegistrySolWriter::attach(bus, provider, contract_address, is_aggregator) + .await?; + Ok(writer) + } } diff --git a/crates/evm/src/helpers.rs b/crates/evm/src/helpers.rs index 0dc86d3e5d..d4f63eee5d 100644 --- a/crates/evm/src/helpers.rs +++ b/crates/evm/src/helpers.rs @@ -11,7 +11,7 @@ use alloy::{ BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, }, - Identity, Provider, ProviderBuilder, RootProvider, WalletProvider, + Identity, Provider, ProviderBuilder, RootProvider, }, signers::local::PrivateKeySigner, transports::{ @@ -197,6 +197,28 @@ pub async fn load_signer_from_repository( private_key.parse().map_err(Into::into) } +pub async fn get_current_timestamp() -> Result { + let config = e3_config::load_config("_default", None, None)?; + let chain = config + .chains() + .first() + .ok_or_else(|| anyhow::anyhow!("No chains configured"))?; + + let rpc_url = chain.rpc_url()?; + let provider = ProviderConfig::new(rpc_url, chain.rpc_auth.clone()) + .create_readonly_provider() + .await?; + + let block = provider + .provider() + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await + .context("Failed to get latest block")? + .ok_or_else(|| anyhow::anyhow!("Latest block not found"))?; + + Ok(block.header.timestamp) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 2156d2825d..477e20798c 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -4,19 +4,21 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod bonding_registry_sol; mod ciphernode_registry_sol; mod enclave_sol; mod enclave_sol_reader; mod enclave_sol_writer; mod event_reader; pub mod helpers; -mod registry_filter_sol; mod repo; -pub use ciphernode_registry_sol::{CiphernodeRegistrySol, CiphernodeRegistrySolReader}; +pub use bonding_registry_sol::{BondingRegistrySol, BondingRegistrySolReader}; +pub use ciphernode_registry_sol::{ + CiphernodeRegistrySol, CiphernodeRegistrySolReader, CiphernodeRegistrySolWriter, +}; pub use enclave_sol::EnclaveSol; pub use enclave_sol_reader::EnclaveSolReader; pub use enclave_sol_writer::EnclaveSolWriter; pub use event_reader::{EnclaveEvmEvent, EvmEventReader, EvmEventReaderState, ExtractorFn}; -pub use registry_filter_sol::{RegistryFilterSol, RegistryFilterSolWriter}; pub use repo::*; diff --git a/crates/evm/src/registry_filter_sol.rs b/crates/evm/src/registry_filter_sol.rs deleted file mode 100644 index 8c491a3734..0000000000 --- a/crates/evm/src/registry_filter_sol.rs +++ /dev/null @@ -1,170 +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 crate::helpers::EthProvider; -use actix::prelude::*; -use alloy::{ - primitives::{Address, Bytes, U256}, - providers::{Provider, WalletProvider}, - rpc::types::TransactionReceipt, - sol, -}; -use anyhow::Result; -use e3_events::{ - BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, PublicKeyAggregated, - Shutdown, Subscribe, -}; -use tracing::info; - -sol!( - #[sol(rpc)] - NaiveRegistryFilter, - "../../packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json" -); - -pub struct RegistryFilterSolWriter

{ - provider: EthProvider

, - contract_address: Address, - bus: Addr>, -} - -impl RegistryFilterSolWriter

{ - pub async fn new( - bus: &Addr>, - provider: EthProvider

, - contract_address: Address, - ) -> Result { - Ok(Self { - provider, - contract_address, - bus: bus.clone(), - }) - } - - pub async fn attach( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - ) -> Result>> { - let addr = RegistryFilterSolWriter::new(bus, provider, contract_address.parse()?) - .await? - .start(); - - let _ = bus - .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) - .await; - - Ok(addr) - } -} - -impl Actor for RegistryFilterSolWriter

{ - type Context = actix::Context; -} - -impl Handler - for RegistryFilterSolWriter

-{ - type Result = (); - - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { - match msg { - EnclaveEvent::PublicKeyAggregated { data, .. } => { - // Only publish if the src and destination chains match - if self.provider.chain_id() == data.e3_id.chain_id() { - ctx.notify(data); - } - } - EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), - _ => (), - } - } -} - -impl Handler - for RegistryFilterSolWriter

-{ - type Result = ResponseFuture<()>; - - fn handle(&mut self, msg: PublicKeyAggregated, _: &mut Self::Context) -> Self::Result { - Box::pin({ - let e3_id = msg.e3_id.clone(); - let pubkey = msg.pubkey.clone(); - let contract_address = self.contract_address; - let provider = self.provider.clone(); - let bus = self.bus.clone(); - let nodes = msg.nodes.clone(); - - async move { - let result = - publish_committee(provider, contract_address, e3_id, nodes, pubkey).await; - match result { - Ok(receipt) => { - info!(tx=%receipt.transaction_hash, "Transaction published"); - } - Err(err) => bus.err( - EnclaveErrorType::Evm, - anyhow::anyhow!("Error publishing committee output: {:?}", err), - ), - } - } - }) - } -} - -impl Handler - for RegistryFilterSolWriter

-{ - type Result = (); - - fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { - ctx.stop(); - } -} - -pub async fn publish_committee( - provider: EthProvider

, - contract_address: Address, - e3_id: E3id, - nodes: OrderedSet, - public_key: Vec, -) -> Result { - let e3_id: U256 = e3_id.try_into()?; - let public_key = Bytes::from(public_key); - let nodes: Vec

= nodes - .into_iter() - .filter_map(|node| node.parse().ok()) - .collect(); - let from_address = provider.provider().default_signer_address(); - let current_nonce = provider - .provider() - .get_transaction_count(from_address) - .pending() - .await?; - let contract = NaiveRegistryFilter::new(contract_address, provider.provider()); - info!( - "publishCommittee called. e3_id={:?}, nodes={:?}", - e3_id, nodes - ); - let builder = contract - .publishCommittee(e3_id, nodes, public_key) - .nonce(current_nonce); - let receipt = builder.send().await?.get_receipt().await?; - Ok(receipt) -} - -pub struct RegistryFilterSol; - -impl RegistryFilterSol { - pub async fn attach( - bus: &Addr>, - provider: EthProvider

, - contract_address: &str, - ) -> Result<()> { - RegistryFilterSolWriter::attach(bus, provider, contract_address).await?; - Ok(()) - } -} diff --git a/crates/evm/src/repo.rs b/crates/evm/src/repo.rs index 0bcf60c373..0455c09f04 100644 --- a/crates/evm/src/repo.rs +++ b/crates/evm/src/repo.rs @@ -41,3 +41,16 @@ impl CiphernodeRegistryReaderRepositoryFactory for Repositories { ) } } + +pub trait BondingRegistryReaderRepositoryFactory { + fn bonding_registry_reader(&self, chain_id: u64) -> Repository; +} + +impl BondingRegistryReaderRepositoryFactory for Repositories { + fn bonding_registry_reader(&self, chain_id: u64) -> Repository { + Repository::new( + self.store + .scope(StoreKeys::bonding_registry_reader(chain_id)), + ) + } +} diff --git a/crates/request/src/meta.rs b/crates/request/src/meta.rs index 7c60447e91..209ad63da2 100644 --- a/crates/request/src/meta.rs +++ b/crates/request/src/meta.rs @@ -19,6 +19,8 @@ pub struct E3Meta { pub threshold_n: usize, pub seed: Seed, pub params: ArcBytes, + pub esi_per_ct: usize, + pub error_size: ArcBytes, } pub struct E3MetaExtension; @@ -41,6 +43,8 @@ impl E3Extension for E3MetaExtension { seed, e3_id, params, + esi_per_ct, + error_size, .. } = data.clone(); @@ -50,6 +54,8 @@ impl E3Extension for E3MetaExtension { threshold_n, seed, params, + esi_per_ct, + error_size, }; ctx.repositories().meta(&e3_id).write(&meta); let _ = ctx.set_dependency(META_KEY, meta); diff --git a/crates/request/src/router.rs b/crates/request/src/router.rs index 865e21bfae..33246db5fe 100644 --- a/crates/request/src/router.rs +++ b/crates/request/src/router.rs @@ -272,7 +272,7 @@ impl FromSnapshotWithParams for E3Router { extensions: params.extensions.into(), buffer: EventBuffer::default(), bus: params.bus, - store: repositories.router(), + store: params.store, }) } } @@ -291,14 +291,11 @@ impl E3RouterBuilder { } pub async fn build(self) -> Result> { - let repositories = self.store.repositories(); - let router_repo = repositories.router(); - let snapshot: Option = router_repo.read().await?; + let snapshot: Option = self.store.read().await?; let params = E3RouterParams { extensions: self.extensions.into(), bus: self.bus.clone(), - - store: router_repo, + store: self.store.clone(), }; let e3r = match snapshot { diff --git a/crates/sortition/Cargo.toml b/crates/sortition/Cargo.toml index b16f0883f1..096f450ec1 100644 --- a/crates/sortition/Cargo.toml +++ b/crates/sortition/Cargo.toml @@ -16,7 +16,8 @@ async-trait = { workspace = true } e3-config = { workspace = true } e3-data = { workspace = true } e3-events = { workspace = true } -num = { workspace = true } +e3-request = { workspace = true } +num = { workspace = true } rand = { workspace = true } serde = { workspace = true } tracing = { workspace = true } diff --git a/crates/sortition/Readme.md b/crates/sortition/Readme.md new file mode 100644 index 0000000000..508bce70d3 --- /dev/null +++ b/crates/sortition/Readme.md @@ -0,0 +1,377 @@ +# Sortition and E3 Complete Flow + +This document describes the complete flow of the Enclave system, from operator registration through E3 computation request, sortition, committee selection, keyshare generation, public key aggregation, encryption, and decryption. + +## Overview + +The Enclave system uses a score-based sortition mechanism to select a committee of ciphernodes to perform threshold homomorphic encryption operations. The flow involves: + +1. **Operator Setup** - Bonding license tokens and ticket balance +2. **Registration** - Registering as a ciphernode operator +3. **E3 Request** - A computation request triggers sortition +4. **Score Sortition** - Nodes are selected based on ticket balances +5. **Committee Finalization** - Selected nodes form a committee +6. **Keyshare Generation** - Committee nodes generate threshold keyshares +7. **Public Key Aggregation** - Keyshares are aggregated into a public key +8. **Encryption & Decryption** - Data is encrypted and threshold-decrypted + +## Complete System Flow + +```mermaid +sequenceDiagram + participant Operator + participant BondingRegistry + participant CiphernodeRegistry + participant EventBus + participant NodeStateManager + participant Sortition + participant CiphernodeSelector + participant Keyshare + participant PublicKeyAggregator + participant PlaintextAggregator + + Note over Operator,BondingRegistry: Phase 1: Operator Setup & Registration + + Operator->>BondingRegistry: bondLicense(amount) + BondingRegistry->>BondingRegistry: Transfer ENCL tokens + BondingRegistry->>EventBus: LicenseBondUpdated + + Operator->>BondingRegistry: registerOperator() + BondingRegistry->>BondingRegistry: Check isLicensed() + BondingRegistry->>CiphernodeRegistry: addCiphernode(operator) + CiphernodeRegistry->>EventBus: CiphernodeAdded(operator, index, numNodes, chainId) + EventBus->>NodeStateManager: CiphernodeAdded + NodeStateManager->>NodeStateManager: Register operator in nodes HashMap + + Operator->>BondingRegistry: addTicketBalance(amount) + BondingRegistry->>BondingRegistry: Mint ticket tokens + BondingRegistry->>EventBus: TicketBalanceUpdated(operator, delta, newBalance, chainId) + EventBus->>NodeStateManager: TicketBalanceUpdated + NodeStateManager->>NodeStateManager: Update operator ticket balance + + BondingRegistry->>BondingRegistry: Check if balance >= minTicketBalance + BondingRegistry->>EventBus: OperatorActivationChanged(operator, active=true, chainId) + EventBus->>NodeStateManager: OperatorActivationChanged + NodeStateManager->>NodeStateManager: Set operator active status + + Note over Operator,PlaintextAggregator: Phase 2: E3 Request & Sortition + + Operator->>EventBus: E3Requested(e3Id, thresholdM, thresholdN, seed, params, chainId) + EventBus->>Sortition: E3Requested + Sortition->>NodeStateManager: GetNodeState(chainId) + NodeStateManager-->>Sortition: NodeStateStore { nodes, ticketPrice } + Sortition->>Sortition: Build sortition list from active nodes + Sortition->>Sortition: Run score sortition algorithm + Sortition->>Sortition: Generate tickets for selected nodes + + loop For each selected node + Sortition->>EventBus: TicketGenerated(e3Id, node, ticketId, chainId) + end + + Note over CiphernodeRegistry,EventBus: Phase 3: On-Chain Ticket Submission + + EventBus->>CiphernodeRegistry: TicketGenerated (if ticketId != 0) + CiphernodeRegistry->>CiphernodeRegistry: Submit ticket to contract + CiphernodeRegistry->>CiphernodeRegistry: Wait for threshold tickets + CiphernodeRegistry->>CiphernodeRegistry: Call finalizeCommittee() + CiphernodeRegistry->>EventBus: CommitteeFinalized(e3Id, committee[], chainId) + + Note over EventBus,Sortition: Phase 4: Committee Storage + + EventBus->>Sortition: CommitteeFinalized + Sortition->>Sortition: Store committee in finalized_committees HashMap + Sortition->>Sortition: Persist to disk + + Note over EventBus,CiphernodeSelector: Phase 5: Node Selection + + EventBus->>CiphernodeSelector: CommitteeFinalized + CiphernodeSelector->>CiphernodeSelector: Check if node address in committee + alt Node is in committee + CiphernodeSelector->>EventBus: CiphernodeSelected(e3Id, node, chainId) + end + + Note over EventBus,Keyshare: Phase 6: Keyshare Generation + + EventBus->>Keyshare: CiphernodeSelected + Keyshare->>Keyshare: fhe.generate_keyshare() + Keyshare->>Keyshare: Generate secret key (random) + Keyshare->>Keyshare: Generate public key share (sk + CRP) + Keyshare->>Keyshare: Persist secret key + Keyshare->>EventBus: KeyshareCreated(e3Id, node, pubkey, chainId) + + Note over EventBus,PublicKeyAggregator: Phase 7: Public Key Aggregation + + EventBus->>PublicKeyAggregator: KeyshareCreated + PublicKeyAggregator->>Sortition: GetNodesForE3(e3Id, chainId) + Sortition-->>PublicKeyAggregator: committee[] + PublicKeyAggregator->>PublicKeyAggregator: Verify node in committee + PublicKeyAggregator->>PublicKeyAggregator: Add keyshare to OrderedSet + + alt Threshold reached (all keyshares collected) + PublicKeyAggregator->>PublicKeyAggregator: fhe.get_aggregate_public_key(keyshares) + PublicKeyAggregator->>PublicKeyAggregator: Aggregate public key shares + PublicKeyAggregator->>EventBus: PublicKeyAggregated(e3Id, pubkey, nodes, chainId) + PublicKeyAggregator->>CiphernodeRegistry: publishPublicKey(e3Id, pubkey, nodes) + end + + Note over Operator,PlaintextAggregator: Phase 8: Encryption & Computation + + Operator->>Operator: Encrypt input data with aggregated pubkey + Operator->>EventBus: CiphertextOutputPublished(e3Id, ciphertext, chainId) + + Note over EventBus,PlaintextAggregator: Phase 9: Threshold Decryption + + EventBus->>Keyshare: CiphertextOutputPublished + Keyshare->>Keyshare: Load secret key from storage + Keyshare->>Keyshare: fhe.decrypt_ciphertext(secret, ciphertext) + Keyshare->>Keyshare: Generate decryption share + Keyshare->>EventBus: DecryptionshareCreated(e3Id, node, decryptionShare, chainId) + + EventBus->>PlaintextAggregator: DecryptionshareCreated + PlaintextAggregator->>Sortition: GetNodesForE3(e3Id, chainId) + Sortition-->>PlaintextAggregator: committee[] + PlaintextAggregator->>PlaintextAggregator: Verify node in committee + PlaintextAggregator->>PlaintextAggregator: Add decryption share to OrderedSet + + alt Threshold reached (all shares collected) + PlaintextAggregator->>PlaintextAggregator: fhe.get_aggregate_plaintext(shares, ciphertext) + PlaintextAggregator->>PlaintextAggregator: Aggregate decryption shares + PlaintextAggregator->>PlaintextAggregator: Decode plaintext + PlaintextAggregator->>EventBus: PlaintextAggregated(e3Id, plaintext, nodes, chainId) + end +``` + +## State Diagram: Node Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Unbonded + Unbonded --> Licensed: bondLicense(amount >= requiredBond) + Licensed --> Registered: registerOperator() + Registered --> Active: addTicketBalance(balance >= minBalance) + Active --> Inactive: removeTicketBalance() OR unbondLicense() + Inactive --> Active: addTicketBalance() OR bondLicense() + Active --> ExitPending: deregisterOperator() + Inactive --> ExitPending: deregisterOperator() + Registered --> ExitPending: deregisterOperator() + ExitPending --> [*]: claimExits() after exitDelay + ExitPending --> Registered: registerOperator() (cancels exit) +``` + +## Sortition Data Flow + +```mermaid +flowchart TD + A[E3Requested Event] --> B{Chain ID Match?} + B -->|No| Z[Ignore] + B -->|Yes| C[NodeStateManager: Get Active Nodes] + C --> D[Filter: ticket_balance > 0 AND active=true] + D --> E[Score Sortition: Build Weighted List] + E --> F[Calculate Total Ticket Weight] + F --> G{threshold_n nodes available?} + G -->|No| H[Error: Insufficient Nodes] + G -->|Yes| I[Select Top N Nodes by Weight] + I --> J[Generate Ticket IDs] + J --> K[Emit TicketGenerated Events] + K --> L[EVM: Submit Tickets On-Chain] + L --> M{Threshold Tickets Submitted?} + M -->|No| L + M -->|Yes| N[Contract: finalizeCommittee] + N --> O[Emit CommitteeFinalized Event] + O --> P[Sortition: Store Committee] + P --> Q[CiphernodeSelector: Check Membership] + Q --> R[Emit CiphernodeSelected] + R --> S[Keyshare Generation Starts] +``` + +## Committee Finalization Flow + +```mermaid +flowchart LR + A[TicketGenerated] --> D[Score Sortition - Submit to Contract] + D --> E[Contract: Collect Tickets] + E --> F{Threshold Met?} + F -->|No| E + F -->|Yes| G[Contract: finalizeCommittee] + G --> H[Freeze Committee List] + H --> I[Emit CommitteeFinalized Event] + I --> K[All Nodes: Store Committee] + K --> L[CiphernodeSelector: Process] +``` + +## Key Concepts + +### 1. Score Sortition + +- **Purpose**: Select committee based on ticket balance (stake-weighted) +- **Algorithm**: + - Build list of eligible nodes (active + ticket_balance > 0) + - Calculate weight for each node based on ticket balance + - Select top `threshold_n` nodes by weight + - Generate unique ticket IDs for selected nodes +- **On-Chain Integration**: Tickets submitted to contract for verification +- **Committee Finalization**: Contract finalizes committee when threshold tickets received + +### 3. NodeStateManager + +- **Purpose**: Track state of all registered ciphernodes +- **State Per Node**: + - `ticket_balance`: Current ticket balance + - `active`: Whether node is active (has min ticket balance) + - `active_jobs`: Number of active E3 jobs +- **Persistence**: State survives node restarts +- **Events**: + - `CiphernodeAdded` / `CiphernodeRemoved` + - `TicketBalanceUpdated` + - `OperatorActivationChanged` + - `ConfigurationUpdated` (for ticketPrice) + +### 4. Sortition Actor + +- **Purpose**: Manage sortition algorithm and committee state +- **Persistent State**: + - `list`: Current sortition list (backend-specific) + - `finalized_committees`: HashMap of E3id → committee members +- **Messages**: + - `GetNodesForE3`: Query committee members for an E3 + - `GetCommittee`: Query full sortition list + - `GetNodeState`: Get current node state +- **Event Handlers**: + - `E3Requested`: Trigger sortition + - `CommitteeFinalized`: Store committee + - `TicketBalanceUpdated`, `OperatorActivationChanged`, etc. + +### 5. Committee Query Pattern + +- **Old Approach**: Store committee in EVM contract, query from there +- **New Approach**: Query `Sortition` actor via `GetNodesForE3` + - Benefits: Single source of truth, no EVM storage cost + - Used by: `PublicKeyAggregator`, `PlaintextAggregator` + - Validation: Ensures nodes are in finalized committee + +### 6. Event Deduplication + +- **Purpose**: Prevent processing same event multiple times +- **Mechanism**: EventBus with deduplication enabled +- **Hash-based**: Events with same content have same EventId +- **Important**: Allows safe event replay on restart + +### 7. Historical Event Synchronization + +- **Purpose**: Nodes can restart and catch up +- **Mechanism**: Fetch historical events from contracts on startup +- **Events**: + - `CiphernodeAdded` / `CiphernodeRemoved` + - `TicketBalanceUpdated` + - `OperatorActivationChanged` + - `ConfigurationUpdated` + - `CommitteeFinalized` +- **Deduplication**: EventBus ignores already-seen events + +### 8. Threshold Cryptography + +- **Scheme**: BFV Threshold Homomorphic Encryption +- **Parameters**: + - `threshold_m`: Minimum shares needed for decryption + - `threshold_n`: Total committee size +- **Common Random Polynomial (CRP)**: Shared randomness from E3 seed +- **Keyshare Generation**: + - Secret key: Random polynomial + - Public key share: Secret key + CRP +- **Aggregation**: Combining shares into single public key / plaintext + +### 9. Party IDs + +- **Purpose**: Identify position in threshold scheme +- **Assignment**: Based on order in `CommitteeFinalized.committee` array +- **Range**: `0..threshold_n` +- **Critical**: Order must be consistent across all nodes +- **Used In**: Keyshare creation, decryption share verification + +### 10. OrderedSet + +- **Purpose**: Maintain insertion order for aggregation +- **Usage**: + - Keyshares collected by `PublicKeyAggregator` + - Decryption shares collected by `PlaintextAggregator` +- **Critical**: Order affects aggregation result +- **Implementation**: Preserves order in which shares arrive + +## Event Reference + +### Bonding Registry Events + +| Event | Parameters | Purpose | +| --------------------------- | -------------------------------------------- | ---------------------------- | +| `LicenseBondUpdated` | operator, delta, newBalance, reason | Track license token bonding | +| `TicketBalanceUpdated` | operator, delta, newBalance, reason, chainId | Track ticket balance changes | +| `OperatorActivationChanged` | operator, active, chainId | Node activation status | +| `ConfigurationUpdated` | parameter, oldValue, newValue | System parameter changes | + +### Ciphernode Registry Events + +| Event | Parameters | Purpose | +| ------------------- | --------------------------------- | ----------------- | +| `CiphernodeAdded` | address, index, numNodes, chainId | Node registration | +| `CiphernodeRemoved` | address, index, numNodes, chainId | Node removal | + +### Enclave Events + +| Event | Parameters | Purpose | +| --------------------------- | --------------------------------------------------- | --------------------- | +| `E3Requested` | e3Id, thresholdM, thresholdN, seed, params, chainId | Computation request | +| `TicketGenerated` | e3Id, node, ticketId, chainId | Sortition ticket | +| `CommitteeFinalized` | e3Id, committee[], chainId | Committee selected | +| `CiphernodeSelected` | e3Id, node, chainId | Node is in committee | +| `KeyshareCreated` | e3Id, node, pubkey, chainId | Keyshare generated | +| `PublicKeyAggregated` | e3Id, pubkey, nodes, chainId | Public key ready | +| `CiphertextOutputPublished` | e3Id, ciphertext, chainId | Encrypted computation | +| `DecryptionshareCreated` | e3Id, node, partyId, decryptionShare, chainId | Decryption share | +| `PlaintextAggregated` | e3Id, plaintext, nodes, chainId | Decryption complete | + +## Testing Flow + +The integration tests follow this pattern: + +1. **Setup**: Create ciphernodes with shared event bus +2. **Register**: Use `setup_score_sortition_environment` to: + - Set ticket price via `ConfigurationUpdated` + - Add nodes via `CiphernodeAdded` + - Give nodes tickets via `TicketBalanceUpdated` + - Activate nodes via `OperatorActivationChanged` +3. **Request**: Send `E3Requested` event +4. **Finalize**: Send `CommitteeFinalized` event (manual in tests) +5. **Aggregate**: Wait for `PublicKeyAggregated` event +6. **Verify**: Check aggregated pubkey matches expected value + +## Persistence + +### What Gets Persisted? + +- **NodeStateManager**: `nodes` HashMap (ticket balances, activation status) +- **Sortition**: `list` (backend-specific), `finalized_committees` HashMap +- **Keyshare**: Secret keys per E3 + +### Where? + +- Default: In-memory (for tests) +- Production: RocksDB or other repository implementation +- Path: Configured via `RepositoriesFactory` + +### Restart Behavior + +1. Actor starts +2. Loads persisted state from repository +3. Subscribes to events +4. Processes new events +5. Event deduplication prevents re-processing old events + +## Chain ID Handling + +- **Purpose**: Support multiple chains simultaneously +- **Isolation**: Each chain has independent: + - Node state + - Committees + - E3 processes +- **Validation**: All operations validate chain ID matches +- **Critical**: Prevents cross-chain confusion diff --git a/crates/sortition/src/backends.rs b/crates/sortition/src/backends.rs new file mode 100644 index 0000000000..1586964432 --- /dev/null +++ b/crates/sortition/src/backends.rs @@ -0,0 +1,301 @@ +// 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::sortition::NodeStateStore; +use crate::ticket::{RegisteredNode, Ticket}; +use crate::ticket_sortition::ScoreSortition; +use alloy::primitives::Address; +use anyhow::Result; +use e3_events::Seed; +use serde::{Deserialize, Serialize}; +use tracing::info; + +/// Minimal interface that all sortition backends must implement. +/// +/// Backends can store their own shapes (e.g., a `HashSet` of addresses +/// for Score) +pub trait SortitionList { + /// Return `true` if `address` appears in the size-`size` committee under `seed`. + /// + /// Implementations should return `Ok(false)` if the backend has no nodes + /// or if `size == 0`. + fn contains( + &self, + seed: Seed, + size: usize, + address: T, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result; + + /// Return an index if `address` appears in the committee under `seed`. + /// + /// Implementations should return `Ok(None)` if the backend has no nodes + /// or if `size == 0`. + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> Result)>>; + + /// Add a node to the backend. Backends should be idempotent on duplicates. + fn add(&mut self, address: T); + + /// Remove a node from the backend. Removing a non-existent node is a no-op. + fn remove(&mut self, address: T); + + /// Return all registered node addresses as hex strings. + fn nodes(&self) -> Vec; +} + +/// Score-sortition backend. +/// +/// Stores richer `RegisteredNode` entries (address + per-node ticket set). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ScoreBackend { + /// Nodes with their ticket sets (used by score-based committee selection). + registered: Vec, +} + +impl Default for ScoreBackend { + fn default() -> Self { + Self { + registered: Vec::new(), + } + } +} + +impl ScoreBackend { + /// Build a vector of ephemeral nodes from the node state. + /// + /// The nodes are built from the node state and the registered nodes. + fn build_nodes_from_state( + &self, + chain_id: u64, + node_state: &NodeStateStore, + ) -> Vec { + info!( + chain_id = chain_id, + registered_count = self.registered.len(), + node_state_count = node_state.nodes.len(), + "Building nodes from state for score sortition" + ); + + self.registered + .iter() + .filter_map(|n| { + let addr_str = n.address.to_string(); + let Some(ns) = node_state.nodes.get(&addr_str) else { + info!( + address = %addr_str, + chain_id = chain_id, + "Node not found in NodeStateStore" + ); + return None; + }; + if !ns.active { + info!( + address = %addr_str, + "Node is not active" + ); + return None; + } + + let count = node_state.available_tickets(&addr_str) as u64; + let total_tickets = if node_state.ticket_price.is_zero() { + 0u64 + } else { + (ns.ticket_balance / node_state.ticket_price) + .try_into() + .unwrap_or(0u64) + }; + + if count == 0 { + info!( + address = %addr_str, + ticket_balance = ?ns.ticket_balance, + ticket_price = ?node_state.ticket_price, + total_tickets = total_tickets, + active_jobs = ns.active_jobs, + "Node has no available tickets" + ); + return None; + } + + let tickets = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); + Some(RegisteredNode { + address: n.address, + tickets, + }) + }) + .collect() + } +} + +impl SortitionList for ScoreBackend { + /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. + /// + /// Returns `Ok(false)` if there are no nodes or `size == 0`. + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result { + if size == 0 { + return Ok(false); + } + + let nodes = self.build_nodes_from_state(chain_id, node_state); + if nodes.is_empty() { + return Ok(false); + } + + let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; + let want: Address = address.parse()?; + Ok(winners.iter().any(|w| w.address == want)) + } + + /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. + /// + /// Returns `Ok(None)` if there are no nodes or `size == 0`. + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result)>> { + if size == 0 { + return Ok(None); + } + + let nodes: Vec = self.build_nodes_from_state(chain_id, node_state); + + if nodes.is_empty() { + return Ok(None); + } + + let winners = ScoreSortition::new(size).get_committee(seed.into(), &nodes)?; + let want: alloy::primitives::Address = address.parse()?; + + let maybe = winners + .iter() + .enumerate() + .find_map(|(i, w)| (w.address == want).then(|| (i as u64, Some(w.ticket_id)))); + Ok(maybe) + } + + /// Add a node, creating an empty ticket set when first seen. + fn add(&mut self, address: String) { + match address.parse::

() { + Ok(addr) => { + if !self.registered.iter().any(|n| n.address == addr) { + self.registered.push(RegisteredNode { + address: addr, + tickets: Vec::new(), + }); + } + } + Err(e) => { + tracing::warn!("Failed to parse address '{}': {}", address, e); + } + } + } + + /// Remove the node (if present). + /// + /// Note: `used_ticket_ids` is a legacy field and clearing it here has + /// no effect on current per-node ticket ID semantics. + fn remove(&mut self, address: String) { + if let Ok(addr) = address.parse::
() { + if let Some(i) = self.registered.iter().position(|n| n.address == addr) { + self.registered.swap_remove(i); + } + } + } + + /// Return all registered node addresses as hex strings. + fn nodes(&self) -> Vec { + self.registered + .iter() + .map(|n| n.address.to_string()) + .collect() + } +} + +/// Enum wrapper around the supported backends. +/// +/// New chains default to `Score` sortition. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum SortitionBackend { + /// Score-based selection (stores `RegisteredNode`s with tickets). + Score(ScoreBackend), +} + +impl Default for SortitionBackend { + fn default() -> Self { + SortitionBackend::Score(ScoreBackend::default()) + } +} + +impl SortitionBackend { + pub fn score() -> Self { + SortitionBackend::Score(ScoreBackend::default()) + } +} + +impl SortitionList for SortitionBackend { + fn contains( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result { + match self { + SortitionBackend::Score(b) => b.contains(seed, size, address, chain_id, node_state), + } + } + + fn get_index( + &self, + seed: Seed, + size: usize, + address: String, + chain_id: u64, + node_state: &NodeStateStore, + ) -> anyhow::Result)>> { + match self { + SortitionBackend::Score(b) => b.get_index(seed, size, address, chain_id, node_state), + } + } + + fn add(&mut self, address: String) { + match self { + SortitionBackend::Score(backend) => backend.add(address), + } + } + + fn remove(&mut self, address: String) { + match self { + SortitionBackend::Score(backend) => backend.remove(address), + } + } + + fn nodes(&self) -> Vec { + match self { + SortitionBackend::Score(backend) => backend.nodes(), + } + } +} diff --git a/crates/sortition/src/ciphernode_selector.rs b/crates/sortition/src/ciphernode_selector.rs index b4d7a0da11..d89b09cba2 100644 --- a/crates/sortition/src/ciphernode_selector.rs +++ b/crates/sortition/src/ciphernode_selector.rs @@ -4,17 +4,24 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{GetNodeIndex, Sortition}; +use crate::sortition::{GetNodeIndex, Sortition}; /// CiphernodeSelector is an actor that determines if a ciphernode is part of a committee and if so -/// forwards a CiphernodeSelected event to the event bus +/// emits a TicketGenerated event (score sortition) to the event bus use actix::prelude::*; -use e3_events::{CiphernodeSelected, E3Requested, EnclaveEvent, EventBus, Shutdown, Subscribe}; +use e3_config::StoreKeys; +use e3_data::{DataStore, RepositoriesFactory}; +use e3_events::{ + CiphernodeSelected, CommitteeFinalized, E3Requested, EnclaveEvent, EventBus, Shutdown, + Subscribe, TicketGenerated, TicketId, +}; +use e3_request::MetaRepositoryFactory; use tracing::info; pub struct CiphernodeSelector { bus: Addr>, sortition: Addr, address: String, + data_store: DataStore, } impl Actor for CiphernodeSelector { @@ -26,11 +33,13 @@ impl CiphernodeSelector { bus: &Addr>, sortition: &Addr, address: &str, + data_store: &DataStore, ) -> Self { Self { bus: bus.clone(), sortition: sortition.clone(), address: address.to_owned(), + data_store: data_store.clone(), } } @@ -38,10 +47,15 @@ impl CiphernodeSelector { bus: &Addr>, sortition: &Addr, address: &str, + data_store: &DataStore, ) -> Addr { - let addr = CiphernodeSelector::new(bus, sortition, address).start(); + let addr = CiphernodeSelector::new(bus, sortition, address, data_store).start(); bus.do_send(Subscribe::new("E3Requested", addr.clone().recipient())); + bus.do_send(Subscribe::new( + "CommitteeFinalized", + addr.clone().recipient(), + )); bus.do_send(Subscribe::new("Shutdown", addr.clone().recipient())); addr @@ -53,6 +67,7 @@ impl Handler for CiphernodeSelector { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { EnclaveEvent::E3Requested { data, .. } => ctx.notify(data), + EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data), EnclaveEvent::Shutdown { data, .. } => ctx.notify(data), _ => (), } @@ -63,7 +78,6 @@ impl Handler for CiphernodeSelector { type Result = ResponseFuture<()>; fn handle(&mut self, data: E3Requested, _ctx: &mut Self::Context) -> Self::Result { - info!("CiphernodeSelector is handling E3Requested!!!"); let address = self.address.clone(); let sortition = self.sortition.clone(); let bus = self.bus.clone(); @@ -78,7 +92,7 @@ impl Handler for CiphernodeSelector { seed, size ); - if let Ok(found_index) = sortition + if let Ok(found_result) = sortition .send(GetNodeIndex { chain_id, seed, @@ -87,21 +101,23 @@ impl Handler for CiphernodeSelector { }) .await { - let Some(party_id) = found_index else { + let Some((_party_id, ticket_id)) = found_result else { info!(node = address, "Ciphernode was not selected"); return; }; - info!("CIPHERNODE SELECTED: node={} address={}", party_id, address); - bus.do_send(EnclaveEvent::from(CiphernodeSelected { - party_id, - e3_id: data.e3_id, - threshold_m: data.threshold_m, - threshold_n: data.threshold_n, - esi_per_ct: data.esi_per_ct, - error_size: data.error_size, - params: data.params.clone(), - seed: data.seed.clone(), - })); + + if let Some(tid) = ticket_id { + info!( + node = address, + ticket_id = tid, + "Ticket generated for score sortition" + ); + bus.do_send(EnclaveEvent::from(TicketGenerated { + e3_id: data.e3_id.clone(), + ticket_id: TicketId::Score(tid), + node: address.clone(), + })); + } } else { info!("This node is not selected"); } @@ -109,6 +125,63 @@ impl Handler for CiphernodeSelector { } } +impl Handler for CiphernodeSelector { + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { + let address = self.address.clone(); + let bus = self.bus.clone(); + let e3_id = msg.e3_id.clone(); + let repositories = self + .data_store + .scope(StoreKeys::router()) + .scope(StoreKeys::context(&e3_id)) + .repositories(); + + // Check if this node is in the finalized committee + if !msg.committee.contains(&address) { + info!(node = address, "Node not in finalized committee"); + return Box::pin(async {}); + } + + Box::pin(async move { + // Retrieve E3 metadata from repository + let meta_repo = repositories.meta(&e3_id); + let Some(e3_meta) = meta_repo.read().await.ok().flatten() else { + info!( + node = address, + "No stored E3 metadata for {:?}, skipping", e3_id + ); + return; + }; + + let Some(party_id) = msg.committee.iter().position(|addr| addr == &address) else { + info!( + node = address, + "Node address not found in committee list (should not happen)" + ); + return; + }; + + info!( + node = address, + party_id = party_id, + "Node is in finalized committee, emitting CiphernodeSelected" + ); + bus.do_send(EnclaveEvent::from(CiphernodeSelected { + party_id: party_id as u64, + e3_id, + threshold_m: e3_meta.threshold_m, + threshold_n: e3_meta.threshold_n, + esi_per_ct: e3_meta.esi_per_ct, + error_size: e3_meta.error_size, + params: e3_meta.params, + seed: e3_meta.seed, + })); + }) + } +} + impl Handler for CiphernodeSelector { type Result = (); fn handle(&mut self, _msg: Shutdown, ctx: &mut Self::Context) -> Self::Result { diff --git a/crates/sortition/src/distance.rs b/crates/sortition/src/distance.rs deleted file mode 100644 index 8c7250c67e..0000000000 --- a/crates/sortition/src/distance.rs +++ /dev/null @@ -1,45 +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::{keccak256, Address}; -use anyhow::Result; -use num::{BigInt, Num}; - -pub struct DistanceSortition { - pub random_seed: u64, - pub registered_nodes: Vec
, - pub size: usize, -} - -impl DistanceSortition { - pub fn new(random_seed: u64, registered_nodes: Vec
, size: usize) -> Self { - Self { - random_seed, - registered_nodes, - size, - } - } - - pub fn get_committee(&mut self) -> Result> { - let mut scores = self - .registered_nodes - .iter() - .map(|address| { - let concat = address.to_string() + &self.random_seed.to_string(); - let hash = keccak256(concat).to_string(); - let without_prefix = hash.trim_start_matches("0x"); - let z = BigInt::from_str_radix(without_prefix, 16)?; - let score = z - BigInt::from(self.random_seed); - Ok((score, *address)) - }) - .collect::>>()?; - - scores.sort_by(|a, b| a.0.cmp(&b.0)); - let size = std::cmp::min(self.size, scores.len()); - let result = scores[0..size].to_vec(); - Ok(result) - } -} diff --git a/crates/sortition/src/lib.rs b/crates/sortition/src/lib.rs index 6149472bd7..9d172efff2 100644 --- a/crates/sortition/src/lib.rs +++ b/crates/sortition/src/lib.rs @@ -4,13 +4,15 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod backends; mod ciphernode_selector; -mod distance; mod repo; mod sortition; mod ticket; mod ticket_sortition; +pub use backends::*; pub use ciphernode_selector::*; pub use repo::*; pub use sortition::*; +pub use ticket_sortition::*; diff --git a/crates/sortition/src/repo.rs b/crates/sortition/src/repo.rs index 3c97133348..419c8db7d4 100644 --- a/crates/sortition/src/repo.rs +++ b/crates/sortition/src/repo.rs @@ -4,9 +4,11 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::SortitionBackend; +use crate::backends::SortitionBackend; +use crate::sortition::NodeStateStore; use e3_config::StoreKeys; use e3_data::{Repositories, Repository}; +use e3_events::E3id; use std::collections::HashMap; pub trait SortitionRepositoryFactory { @@ -18,3 +20,23 @@ impl SortitionRepositoryFactory for Repositories { Repository::new(self.store.scope(StoreKeys::sortition())) } } + +pub trait NodeStateRepositoryFactory { + fn node_state(&self) -> Repository>; +} + +impl NodeStateRepositoryFactory for Repositories { + fn node_state(&self) -> Repository> { + Repository::new(self.store.scope(StoreKeys::node_state())) + } +} + +pub trait FinalizedCommitteesRepositoryFactory { + fn finalized_committees(&self) -> Repository>>; +} + +impl FinalizedCommitteesRepositoryFactory for Repositories { + fn finalized_committees(&self) -> Repository>> { + Repository::new(self.store.scope(StoreKeys::finalized_committees())) + } +} diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index 1d89cf4038..df67d0650b 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -4,368 +4,141 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::distance::DistanceSortition; -use crate::ticket::{RegisteredNode, Ticket}; -use crate::ticket_sortition::ScoreSortition; +use crate::backends::{SortitionBackend, SortitionList}; use actix::prelude::*; -use alloy::primitives::Address; -use anyhow::{anyhow, Context, Result}; +use alloy::primitives::U256; +use anyhow::Result; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ - BusError, CiphernodeAdded, CiphernodeRemoved, EnclaveErrorType, EnclaveEvent, EventBus, Seed, - Subscribe, + BusError, CiphernodeAdded, CiphernodeRemoved, CommitteeFinalized, CommitteePublished, + ConfigurationUpdated, EnclaveErrorType, EnclaveEvent, EventBus, OperatorActivationChanged, + PlaintextOutputPublished, Seed, Subscribe, TicketBalanceUpdated, }; -use num::BigInt; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use tracing::{info, instrument, trace}; +use std::collections::HashMap; +use tracing::info; +use tracing::instrument; -/// Message: ask the `Sortition` actor whether `address` would be in the -/// committee of size `size` for randomness `seed` on `chain_id`. -/// -/// Membership semantics depend on the backend for that chain: -/// - **Distance backend**: computes a committee using address distance. -/// - **Score backend**: computes each node’s best ticket score and sorts globally. -/// -/// Returns `true` if `address` appears in the resulting top-`size` selection. -#[derive(Message, Clone, Debug, PartialEq, Eq)] -#[rtype(result = "Option")] -pub struct GetNodeIndex { - /// Round seed / randomness used by the sortition algorithm. - pub seed: Seed, - /// Hex-encoded node address (e.g., `"0x..."`). - pub address: String, - /// Committee size (top-N). - pub size: usize, - /// Target chain. - pub chain_id: u64, -} - -/// Message: request the current set of registered node addresses for `chain_id`. -#[derive(Message, Clone, Debug)] -#[rtype(result = "Vec")] -pub struct GetNodes { - /// Target chain. - pub chain_id: u64, -} - -/// Minimal interface that all sortition backends must implement. -/// -/// Backends can store their own shapes (e.g., a `HashSet` of addresses -/// for Distance, or a `Vec` for Score), but they must be able to: -/// - Check committee membership (`contains`) -/// - Add and remove nodes -/// - List all registered node addresses -pub trait SortitionList { - /// Return `true` if `address` appears in the size-`size` committee under `seed`. - /// - /// Implementations should return `Ok(false)` if the backend has no nodes - /// or if `size == 0`. - fn contains(&self, seed: Seed, size: usize, address: T) -> Result; - - /// Return an index if `address` appears in the committee under `seed`. - /// - /// Implementations should return `Ok(None)` if the backend has no nodes - /// or if `size == 0`. - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result>; - - /// Add a node to the backend. Backends should be idempotent on duplicates. - fn add(&mut self, address: T); - - /// Remove a node from the backend. Removing a non-existent node is a no-op. - fn remove(&mut self, address: T); - - /// Return all registered node addresses as hex strings. - fn nodes(&self) -> Vec; -} - -/// Distance-sortition backend. -/// -/// Stores a set of hex-encoded addresses and delegates committee selection -/// to `DistanceSortition`. +/// State for a single ciphernode #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DistanceBackend { - /// Registered node addresses (hex). - nodes: HashSet, +pub struct NodeState { + /// Current ticket balance for this node + pub ticket_balance: U256, + /// Number of active E3 jobs this node is currently participating in + pub active_jobs: u64, + /// Whether this node is active (has met minimum requirements) + pub active: bool, } -impl Default for DistanceBackend { +impl Default for NodeState { fn default() -> Self { Self { - nodes: HashSet::new(), + ticket_balance: U256::ZERO, + active_jobs: 0, + active: false, } } } -impl SortitionList for DistanceBackend { - /// Build the address list, run `DistanceSortition(seed, nodes, size)`, - /// then check whether `address` is in the result. - /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn contains(&self, seed: Seed, size: usize, address: String) -> Result { - if size == 0 { - return Err(anyhow!("Size cannot be 0")); - } - - if self.nodes.len() == 0 { - return Err(anyhow!("No nodes registered!")); - } - - let committee = get_committee(seed, size, self.nodes.clone())?; - - Ok(committee - .iter() - .any(|(_, addr)| addr.to_string() == address)) - } - - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result> { - if size == 0 { - return Err(anyhow!("Size cannot be 0")); - } - - if self.nodes.len() == 0 { - return Err(anyhow!("No nodes registered!")); - } - - let committee = get_committee(seed, size, self.nodes.clone())?; - - let maybe_index = committee.iter().enumerate().find_map(|(index, (_, addr))| { - if addr.to_string() == address { - return Some(index as u64); - } - None - }); - - Ok(maybe_index) - } - - /// Insert a node address (hex). Duplicate inserts are harmless. - fn add(&mut self, address: String) { - self.nodes.insert(address); - } - - /// Remove a node address (hex). Missing entries are ignored. - fn remove(&mut self, address: String) { - self.nodes.remove(&address); - } - - /// Return all node addresses as hex strings. - fn nodes(&self) -> Vec { - self.nodes.iter().cloned().collect() - } -} - -fn get_committee( - seed: Seed, - size: usize, - nodes: HashSet, -) -> Result> { - let registered_nodes: Vec
= nodes - .into_iter() - .map(|b| b.parse().context(format!("Error parsing address {}", b))) - .collect::>()?; - - DistanceSortition::new(seed.into(), registered_nodes, size) - .get_committee() - .context("Could not get committee!") +/// Unified state for all nodes across all chains +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct NodeStateStore { + /// Map of node_address to node state + pub nodes: HashMap, + /// Current ticket price + pub ticket_price: U256, + /// Map of E3 ID to the committee nodes for that E3 + /// This is used to track which nodes are in which E3 jobs + pub e3_committees: HashMap>, } -/// Score-sortition backend. -/// -/// Stores richer `RegisteredNode` entries (address + per-node ticket set). -/// Tickets use **local, per-node** IDs in the range `1..=k`, assigned by -/// [`ScoreBackend::set_ticket_count_addr`]. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ScoreBackend { - /// Nodes with their ticket sets (used by score-based committee selection). - registered: Vec, -} - -impl Default for ScoreBackend { - fn default() -> Self { - Self { - registered: Vec::new(), +impl NodeStateStore { + /// Get available tickets for a node, accounting for active jobs + /// The Process for calculating available tickets is: + /// 1. Get the node state for the node + /// 2. Check if the node is active + /// 3. Check if the node has a ticket price + /// 4. Check if the node has a ticket balance + /// 5. Calculate the available tickets + /// 6. Subtract the active jobs from the available tickets + /// 7. Return the available tickets + pub fn available_tickets(&self, address: &str) -> u64 { + if self.ticket_price.is_zero() { + return 0; } - } -} - -impl ScoreBackend { - /// Set (or replace) a node’s ticket *count* using local IDs `1..=count`. - /// - /// - If the node already exists, its entire ticket vector is replaced. - /// - If the node doesn’t exist, a new `RegisteredNode` is created. - /// - Passing `count == 0` clears the ticket vector for that node. - /// - /// This does **not** attempt to deduplicate across nodes; IDs are local. - pub fn set_ticket_count_addr(&mut self, address: Address, count: u64) { - let tickets: Vec = (1..=count).map(|i| Ticket { ticket_id: i }).collect(); - if let Some(existing) = self.registered.iter_mut().find(|n| n.address == address) { - existing.tickets = tickets; - } else { - self.registered.push(RegisteredNode { address, tickets }); - } - } -} - -impl SortitionList for ScoreBackend { - /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. - /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn contains(&self, seed: Seed, size: usize, address: String) -> Result { - if self.registered.is_empty() || size == 0 { - return Ok(false); - } - let winners = ScoreSortition::new(size).get_committee(seed.into(), &self.registered)?; - let want: Address = address.parse()?; - Ok(winners.iter().any(|w| w.address == want)) - } - - /// Compute score-based winners (`ScoreSortition`) and check if `address` is included. - /// - /// Returns `Ok(false)` if there are no nodes or `size == 0`. - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result> { - if self.registered.is_empty() || size == 0 { - return Ok(None); - } - let winners = ScoreSortition::new(size).get_committee(seed.into(), &self.registered)?; - let want: Address = address.parse()?; - - let maybe_index = winners.iter().enumerate().find_map(|(index, w)| { - if w.address == want { - return Some(index as u64); - } - None - }); - Ok(maybe_index) - } - - /// Add a node, creating an empty ticket set when first seen. - /// - /// To set tickets, call [`ScoreBackend::set_ticket_count_addr`] (or another - /// initialization path) after the node is added. - fn add(&mut self, address: String) { - match address.parse::
() { - Ok(addr) => { - if !self.registered.iter().any(|n| n.address == addr) { - self.registered.push(RegisteredNode { - address: addr, - tickets: Vec::new(), - }); - } - } - Err(e) => { - tracing::warn!("Failed to parse address '{}': {}", address, e); - } - } - } + let node = self.nodes.get(address); - /// Remove the node (if present). - /// - /// Note: `used_ticket_ids` is a legacy field and clearing it here has - /// no effect on current per-node ticket ID semantics. - fn remove(&mut self, address: String) { - if let Ok(addr) = address.parse::
() { - if let Some(i) = self.registered.iter().position(|n| n.address == addr) { - self.registered.swap_remove(i); - } + if let Some(node) = node { + let total_tickets = (node.ticket_balance / self.ticket_price) + .try_into() + .unwrap_or(0u64); + total_tickets.saturating_sub(node.active_jobs) + } else { + 0 } } - /// Return all registered node addresses as hex strings. - fn nodes(&self) -> Vec { - self.registered + /// Get all nodes with their available tickets + /// Only includes active nodes + pub fn get_nodes_with_tickets(&self) -> Vec<(String, u64)> { + self.nodes .iter() - .map(|n| n.address.to_string()) + .filter(|(_, node_state)| node_state.active) + .map(|(addr, _)| (addr.clone(), self.available_tickets(addr))) + .filter(|(_, tickets)| *tickets > 0) .collect() } } -/// Enum wrapper around the two supported backends. -/// -/// New chains should default to `Distance`. If a chain is intended to -/// use score selection, construct it as `SortitionBackend::Score(ScoreBackend::default())` -/// and then populate tickets explicitly. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum SortitionBackend { - /// Distance-based selection (stores a simple set of addresses). - Distance(DistanceBackend), - /// Score-based selection (stores `RegisteredNode`s with tickets). - Score(ScoreBackend), +/// Message: ask the `Sortition` whether `address` would be in the +/// committee of size `size` for randomness `seed` on `chain_id`. +#[derive(Message, Clone, Debug, PartialEq, Eq)] +#[rtype(result = "Option<(u64, Option)>")] +pub struct GetNodeIndex { + /// Round seed / randomness used by the sortition algorithm. + pub seed: Seed, + /// Hex-encoded node address (e.g., `"0x..."`). + pub address: String, + /// Committee size (top-N). + pub size: usize, + /// Target chain. + pub chain_id: u64, } -impl SortitionBackend { - /// Construct a backend preconfigured with a default `DistanceBackend`. - pub fn default() -> Self { - SortitionBackend::Distance(DistanceBackend::default()) - } - - /// Helper for Score backends: assign local ticket IDs `1..=count` for `address`. - /// - /// # Errors - /// Returns an error if called on a `Distance` backend. - pub fn set_ticket_count_addr(&mut self, address: Address, count: u64) -> Result<()> { - match self { - SortitionBackend::Score(b) => { - b.set_ticket_count_addr(address, count); - Ok(()) - } - SortitionBackend::Distance(_) => { - anyhow::bail!("set_ticket_count_addr is only valid for Score backend") - } - } - } +/// Message: request the current set of registered node addresses for `chain_id`. +#[derive(Message, Clone, Debug)] +#[rtype(result = "Vec")] +pub struct GetNodes { + /// Target chain. + pub chain_id: u64, } -impl SortitionList for SortitionBackend { - fn contains(&self, seed: Seed, size: usize, address: String) -> Result { - match self { - SortitionBackend::Distance(backend) => backend.contains(seed, size, address), - SortitionBackend::Score(backend) => backend.contains(seed, size, address), - } - } - - fn get_index(&self, seed: Seed, size: usize, address: String) -> Result> { - match self { - SortitionBackend::Distance(backend) => backend.get_index(seed, size, address), - SortitionBackend::Score(backend) => backend.get_index(seed, size, address), - } - } - fn add(&mut self, address: String) { - match self { - SortitionBackend::Distance(backend) => backend.add(address), - SortitionBackend::Score(backend) => backend.add(address), - } - } - - fn remove(&mut self, address: String) { - match self { - SortitionBackend::Distance(backend) => backend.remove(address), - SortitionBackend::Score(backend) => backend.remove(address), - } - } - - fn nodes(&self) -> Vec { - match self { - SortitionBackend::Distance(backend) => backend.nodes(), - SortitionBackend::Score(backend) => backend.nodes(), - } - } +/// Message to get the finalized committee nodes for a specific E3. +#[derive(Message, Clone, Debug)] +#[rtype(result = "Vec")] +pub struct GetNodesForE3 { + /// E3 ID to get nodes for. + pub e3_id: e3_events::E3id, + /// Chain ID + pub chain_id: u64, } -/// `Sortition` is an Actix actor that owns per-chain backends and exposes -/// message handlers to: -/// - add/remove nodes from a chain, -/// - list nodes for a chain, -/// - check committee membership for a chain. -/// -/// Backends are persisted using `Persistable>` -/// keyed by `chain_id`. +/// Message to get the current node state. +#[derive(Message, Clone, Debug)] +#[rtype(result = "Option>")] +pub struct GetNodeState; + +/// Sortition actor that manages the sortition algorithm and the node state. pub struct Sortition { /// Persistent map of `chain_id -> SortitionBackend`. - list: Persistable>, + backends: Persistable>, + /// Persistent map of `chain_id -> NodeStateStore`. + node_state: Persistable>, /// Event bus for error reporting and enclave event subscription. bus: Addr>, + /// Persistent map of finalized committees per E3 + finalized_committees: Persistable>>, } /// Parameters for constructing a `Sortition` actor. @@ -374,50 +147,76 @@ pub struct SortitionParams { /// Event bus address. pub bus: Addr>, /// Persisted per-chain backend map. - pub list: Persistable>, + pub backends: Persistable>, + /// Node state store per chain + pub node_state: Persistable>, + /// Persistent map of finalized committees per E3 + pub finalized_committees: Persistable>>, } impl Sortition { - /// Construct a new `Sortition` actor with the given bus and repository. pub fn new(params: SortitionParams) -> Self { Self { - list: params.list, + backends: params.backends, + node_state: params.node_state, bus: params.bus, + finalized_committees: params.finalized_committees, } } - /// Load persisted state, start the actor, and subscribe to `CiphernodeAdded/Removed`. - /// - /// The store is initialized with an empty `HashMap` if nothing is present. #[instrument(name = "sortition_attach", skip_all)] pub async fn attach( bus: &Addr>, - store: Repository>, - ) -> Result> { - let list = store.load_or_default(HashMap::new()).await?; + backends_store: Repository>, + node_state_store: Repository>, + committees_store: Repository>>, + default_backend: SortitionBackend, + ) -> Result> { + let mut backends = backends_store.load_or_default(HashMap::new()).await?; + let node_state = node_state_store.load_or_default(HashMap::new()).await?; + let finalized_committees = committees_store.load_or_default(HashMap::new()).await?; + + backends.try_mutate(|mut list| { + list.insert(u64::MAX, default_backend); + Ok(list) + })?; + let addr = Sortition::new(SortitionParams { bus: bus.clone(), - list, + backends, + node_state, + finalized_committees, }) .start(); + + // Subscribe to all relevant events bus.do_send(Subscribe::new("CiphernodeAdded", addr.clone().into())); bus.do_send(Subscribe::new("CiphernodeRemoved", addr.clone().into())); + bus.do_send(Subscribe::new("TicketBalanceUpdated", addr.clone().into())); + bus.do_send(Subscribe::new( + "OperatorActivationChanged", + addr.clone().into(), + )); + bus.do_send(Subscribe::new("ConfigurationUpdated", addr.clone().into())); + bus.do_send(Subscribe::new("CommitteePublished", addr.clone().into())); + bus.do_send(Subscribe::new( + "PlaintextOutputPublished", + addr.clone().into(), + )); + bus.do_send(Subscribe::new("CommitteeFinalized", addr.clone().into())); + + info!("Sortition actor started"); Ok(addr) } - /// Return the current node addresses (hex) for `chain_id`. - /// - /// # Errors - /// - Returns an error if the persisted map cannot be loaded from memory. - /// - Returns an error if the given `chain_id` has no backend entry. pub fn get_nodes(&self, chain_id: u64) -> Result> { let map = self - .list + .backends .get() - .ok_or_else(|| anyhow!("Could not get sortition's list cache"))?; + .ok_or_else(|| anyhow::anyhow!("Could not get backends cache"))?; let backend = map .get(&chain_id) - .ok_or_else(|| anyhow!("No list for chain_id {}", chain_id))?; + .ok_or_else(|| anyhow::anyhow!("No backend for chain_id {}", chain_id))?; Ok(backend.nodes()) } } @@ -428,11 +227,17 @@ impl Actor for Sortition { impl Handler for Sortition { type Result = (); - /// Fan-in enclave events to the corresponding typed handlers. + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg { EnclaveEvent::CiphernodeAdded { data, .. } => ctx.notify(data.clone()), EnclaveEvent::CiphernodeRemoved { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::TicketBalanceUpdated { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::OperatorActivationChanged { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::ConfigurationUpdated { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::CommitteePublished { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::PlaintextOutputPublished { data, .. } => ctx.notify(data.clone()), + EnclaveEvent::CommitteeFinalized { data, .. } => ctx.notify(data.clone()), _ => (), } } @@ -441,42 +246,59 @@ impl Handler for Sortition { impl Handler for Sortition { type Result = (); - /// Add a node to the target chain. - /// - /// If the chain does not exist yet, its backend is initialized to `Distance`. - /// For score-based chains, switch construction time to `SortitionBackend::Score` - /// and call the ticket setters separately (this handler only adds the address). - #[instrument(name = "sortition_add_node", skip_all)] fn handle(&mut self, msg: CiphernodeAdded, _ctx: &mut Self::Context) -> Self::Result { - trace!("Adding node: {}", msg.address); let chain_id = msg.chain_id; let addr = msg.address.clone(); - if let Err(err) = self.list.try_mutate(move |mut list_map| { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_state = state_map + .entry(chain_id) + .or_insert_with(NodeStateStore::default); + chain_state + .nodes + .entry(addr.clone()) + .or_insert_with(NodeState::default); + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + + if let Err(err) = self.backends.try_mutate(move |mut list_map| { + let default_backend = list_map + .get(&u64::MAX) + .cloned() + .unwrap_or_else(|| SortitionBackend::score()); + list_map .entry(chain_id) - .or_insert_with(SortitionBackend::default) + .or_insert_with(|| default_backend) .add(addr); Ok(list_map) }) { self.bus.err(EnclaveErrorType::Sortition, err); } + + info!(address = %msg.address, chain_id = chain_id, "Node added to sortition state"); } } impl Handler for Sortition { type Result = (); - /// Remove a node from the target chain. - /// - /// If the chain entry is missing, nothing is created or removed. - #[instrument(name = "sortition_remove_node", skip_all)] fn handle(&mut self, msg: CiphernodeRemoved, _ctx: &mut Self::Context) -> Self::Result { - info!("Removing node: {}", msg.address); let chain_id = msg.chain_id; let addr = msg.address.clone(); - if let Err(err) = self.list.try_mutate(move |mut list_map| { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + if let Some(chain_state) = state_map.get_mut(&chain_id) { + chain_state.nodes.remove(&addr); + } + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + + if let Err(err) = self.backends.try_mutate(move |mut list_map| { if let Some(backend) = list_map.get_mut(&chain_id) { backend.remove(addr); } @@ -484,38 +306,224 @@ impl Handler for Sortition { }) { self.bus.err(EnclaveErrorType::Sortition, err); } + + info!(address = %msg.address, chain_id = chain_id, "Node removed from sortition state"); + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: TicketBalanceUpdated, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_state = state_map + .entry(msg.chain_id) + .or_insert_with(NodeStateStore::default); + let node = chain_state + .nodes + .entry(msg.operator.clone()) + .or_insert_with(NodeState::default); + node.ticket_balance = msg.new_balance; + + info!( + operator = %msg.operator, + chain_id = msg.chain_id, + new_balance = ?msg.new_balance, + "Updated ticket balance" + ); + + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: OperatorActivationChanged, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + // Update all entries for this operator across all chains + for (_, chain_state) in state_map.iter_mut() { + if let Some(node) = chain_state.nodes.get_mut(&msg.operator) { + node.active = msg.active; + info!( + operator = %msg.operator, + active = msg.active, + "Updated operator active status" + ); + } + } + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: ConfigurationUpdated, _ctx: &mut Self::Context) -> Self::Result { + if msg.parameter == "ticketPrice" { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_state = state_map + .entry(msg.chain_id) + .or_insert_with(NodeStateStore::default); + chain_state.ticket_price = msg.new_value; + info!( + chain_id = msg.chain_id, + old_ticket_price = ?msg.old_value, + new_ticket_price = ?msg.new_value, + "ConfigurationUpdated - ticket price updated" + ); + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: CommitteePublished, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_id = msg.e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); + let chain_state = state_map + .entry(chain_id) + .or_insert_with(NodeStateStore::default); + + chain_state + .e3_committees + .insert(e3_id_str.clone(), msg.nodes.clone()); + + for node_addr in &msg.nodes { + let node = chain_state + .nodes + .entry(node_addr.clone()) + .or_insert_with(NodeState::default); + node.active_jobs += 1; + + info!( + node = %node_addr, + chain_id = chain_id, + e3_id = ?msg.e3_id, + active_jobs = node.active_jobs, + "Incremented active jobs for node in committee" + ); + } + + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} +/// PlaintextOutputPublished is currently used as a signal to decrement the active jobs for the nodes in the committee +/// But in reality, E3 Jobs might not emit that in case there are no votes or the job fails. +/// We need to find a better way to handle the end of an E3, Reduce the jobs in case of of an Error +/// so the tickets do not get locked up. +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: PlaintextOutputPublished, _ctx: &mut Self::Context) -> Self::Result { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_id = msg.e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, msg.e3_id.e3_id()); + + // Get the committee nodes for this E3 + if let Some(chain_state) = state_map.get_mut(&chain_id) { + if let Some(committee_nodes) = chain_state.e3_committees.remove(&e3_id_str) { + // Decrement active jobs for each node in the committee + for node_addr in &committee_nodes { + if let Some(node) = chain_state.nodes.get_mut(node_addr) { + node.active_jobs = node.active_jobs.saturating_sub(1); + + info!( + node = %node_addr, + chain_id = chain_id, + e3_id = ?msg.e3_id, + active_jobs = node.active_jobs, + "Decremented active jobs for node after E3 completion" + ); + } + } + + info!( + e3_id = ?msg.e3_id, + committee_size = committee_nodes.len(), + "PlaintextOutputPublished - job completed, decremented active jobs" + ); + } else { + info!( + e3_id = ?msg.e3_id, + "PlaintextOutputPublished - no committee found (might have been completed already)" + ); + } + } + + Ok(state_map) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } + } +} + +impl Handler for Sortition { + type Result = (); + + fn handle(&mut self, msg: CommitteeFinalized, _ctx: &mut Self::Context) -> Self::Result { + info!( + e3_id = %msg.e3_id, + committee_size = msg.committee.len(), + "Storing finalized committee" + ); + + if let Err(err) = self.finalized_committees.try_mutate(|mut committees| { + committees.insert(msg.e3_id.clone(), msg.committee.clone()); + Ok(committees) + }) { + self.bus.err(EnclaveErrorType::Sortition, err); + } } } impl Handler for Sortition { - type Result = Option; - - /// Return the index of `address` in the size-`size` committee for `seed` - /// on `chain_id`. If the chain has not been initialized, returns `None`. - /// - /// Errors while accessing persisted state or parsing the address are - /// reported on the event bus and surfaced here as `None`. - #[instrument(name = "sortition_contains", skip_all)] + type Result = ResponseFuture)>>; + fn handle(&mut self, msg: GetNodeIndex, _ctx: &mut Self::Context) -> Self::Result { - self.list - .try_with(|map| { - if let Some(backend) = map.get(&msg.chain_id) { - backend.get_index(msg.seed, msg.size, msg.address.clone()) + let backends_snapshot = self.backends.get(); + let node_state_snapshot = self.node_state.get(); + let bus = self.bus.clone(); + + Box::pin(async move { + if let (Some(map), Some(state_map)) = (backends_snapshot, node_state_snapshot) { + if let (Some(backend), Some(state)) = + (map.get(&msg.chain_id), state_map.get(&msg.chain_id)) + { + backend + .get_index(msg.seed, msg.size, msg.address.clone(), msg.chain_id, state) + .unwrap_or_else(|err| { + bus.err(EnclaveErrorType::Sortition, err); + None + }) } else { - Ok(None) + None } - }) - .unwrap_or_else(|err| { - self.bus.err(EnclaveErrorType::Sortition, err); + } else { None - }) + } + }) } } impl Handler for Sortition { type Result = Vec; - /// Return all registered node addresses for a chain, or `[]` on error. fn handle(&mut self, msg: GetNodes, _ctx: &mut Self::Context) -> Self::Result { self.get_nodes(msg.chain_id).unwrap_or_else(|err| { tracing::warn!("Failed to get nodes for chain {}: {}", msg.chain_id, err); @@ -523,3 +531,34 @@ impl Handler for Sortition { }) } } + +impl Handler for Sortition { + type Result = Vec; + + fn handle(&mut self, msg: GetNodesForE3, _ctx: &mut Self::Context) -> Self::Result { + if msg.e3_id.chain_id() != msg.chain_id { + tracing::warn!( + "Chain ID mismatch: e3_id has chain_id {}, but requested chain_id {}", + msg.e3_id.chain_id(), + msg.chain_id + ); + return Vec::new(); + } + + self.finalized_committees + .get() + .and_then(|committees| committees.get(&msg.e3_id).cloned()) + .unwrap_or_else(|| { + tracing::warn!("No finalized committee found for E3 {}", msg.e3_id); + Vec::new() + }) + } +} + +impl Handler for Sortition { + type Result = Option>; + + fn handle(&mut self, _msg: GetNodeState, _: &mut Self::Context) -> Self::Result { + self.node_state.get() + } +} diff --git a/crates/sortition/src/ticket_sortition.rs b/crates/sortition/src/ticket_sortition.rs index 45051eabaa..ea5a655703 100644 --- a/crates/sortition/src/ticket_sortition.rs +++ b/crates/sortition/src/ticket_sortition.rs @@ -66,7 +66,12 @@ impl ScoreSortition { let mut items: Vec = best_map.into_values().collect(); // Sort ascending by (score, ticket_id) - items.sort_unstable_by(|a, b| a.score.cmp(&b.score).then(a.ticket_id.cmp(&b.ticket_id))); + items.sort_unstable_by(|a, b| { + a.score + .cmp(&b.score) + .then(a.ticket_id.cmp(&b.ticket_id)) + .then(a.address.as_slice().cmp(b.address.as_slice())) + }); let k = self.size.min(items.len()); items.truncate(k); diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 69b69317e2..a5e0571781 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -5,12 +5,14 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::Actor; +use alloy::primitives::{FixedBytes, I256, U256}; use anyhow::{bail, Context, Result}; use e3_ciphernode_builder::CiphernodeBuilder; use e3_crypto::Cipher; use e3_events::{ - CiphertextOutputPublished, E3Requested, E3id, EnclaveEvent, EventBus, EventBusConfig, - PlaintextAggregated, + CiphertextOutputPublished, CommitteeFinalized, ConfigurationUpdated, E3Requested, E3id, + EnclaveEvent, EventBus, EventBusConfig, OperatorActivationChanged, PlaintextAggregated, + TicketBalanceUpdated, }; use e3_multithread::Multithread; use e3_sdk::bfv_helpers::{build_bfv_params_arc, encode_bfv_params}; @@ -30,6 +32,43 @@ pub fn save_snapshot(file_name: &str, bytes: &[u8]) { fs::write(format!("tests/{file_name}"), bytes).unwrap(); } +async fn setup_score_sortition_environment( + bus: &actix::Addr>, + eth_addrs: &Vec, + chain_id: u64, +) -> Result<()> { + bus.send(EnclaveEvent::from(ConfigurationUpdated { + parameter: "ticketPrice".to_string(), + old_value: U256::ZERO, + new_value: U256::from(10_000_000u64), + chain_id, + })) + .await?; + + let mut adder = AddToCommittee::new(bus, chain_id); + for addr in eth_addrs { + adder.add(addr).await?; + + bus.send(EnclaveEvent::from(TicketBalanceUpdated { + operator: addr.clone(), + delta: I256::try_from(1_000_000_000u64).unwrap(), + new_balance: U256::from(1_000_000_000u64), + reason: FixedBytes::ZERO, + chain_id, + })) + .await?; + + bus.send(EnclaveEvent::from(OperatorActivationChanged { + operator: addr.clone(), + active: true, + chain_id, + })) + .await?; + } + + Ok(()) +} + /// Test trbfv #[actix::test] #[serial_test::serial] @@ -94,7 +133,6 @@ async fn test_trbfv_actor() -> Result<()> { // Cipher let cipher = Arc::new(Cipher::from_password("I am the music man.").await?); - let mut adder = AddToCommittee::new(&bus, 1); // Actor system setup let multithread = Multithread::attach( @@ -116,6 +154,7 @@ async fn test_trbfv_actor() -> Result<()> { .testmode_with_history() .with_trbfv() .with_pubkey_aggregation() + .with_sortition_score() .with_threshold_plaintext_aggregation() .testmode_with_forked_bus(&bus) .with_logging() @@ -129,6 +168,7 @@ async fn test_trbfv_actor() -> Result<()> { .with_address(&addr) .with_injected_multithread(multithread.clone()) .with_trbfv() + .with_sortition_score() .testmode_with_forked_bus(&bus) .with_logging() .build() @@ -138,9 +178,9 @@ async fn test_trbfv_actor() -> Result<()> { .build() .await?; - for node in nodes.iter() { - adder.add(&node.address()).await?; - } + let chain_id = 1u64; + let eth_addrs: Vec = nodes.iter().map(|n| n.address()).collect(); + setup_score_sortition_environment(&bus, ð_addrs, chain_id).await?; // Flush all events nodes.flush_all_history(100).await?; @@ -173,10 +213,36 @@ async fn test_trbfv_actor() -> Result<()> { bus.do_send(event); - // NOTE: We are using node 0 as the aggregator but it is not selected in this seed which is why - // there is no CiphernodeSelected event + // For score sortition, we need to wait for nodes to process E3Requested and run sortition + // Since TicketGenerated is a local-only event (not shared across network), we can't collect it + // we need to manually construct the committee that sortition would select + + // For seed=123, these 5 nodes get selected by sortition: + // 0x8f32E487328F04927f20c4B14399e4F3123763df (ticket 6) + // 0x95b8a2b9b93aE9e0F13e215A49b8C53172c4f4ba (ticket 68) + // 0x8966a013047aef67Cac52Bc96eB77bC11B5D2572 (ticket 95) + // 0x2B1eD59AC30f668B5b9EcF3D8718A44C15E0E479 (ticket 15) + // 0x83A06c5Ac9E4207526C3eFA79812808428Dd5FaB (ticket 12) + let committee: Vec = vec![ + "0x8f32E487328F04927f20c4B14399e4F3123763df".to_string(), + "0x95b8a2b9b93aE9e0F13e215A49b8C53172c4f4ba".to_string(), + "0x8966a013047aef67Cac52Bc96eB77bC11B5D2572".to_string(), + "0x2B1eD59AC30f668B5b9EcF3D8718A44C15E0E479".to_string(), + "0x83A06c5Ac9E4207526C3eFA79812808428Dd5FaB".to_string(), + ]; + + println!("Emitting CommitteeFinalized with {} nodes", committee.len()); + + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: e3_id.clone(), + committee, + chain_id, + })) + .await?; + let expected = vec![ "E3Requested", + "CommitteeFinalized", "ThresholdShareCreated", "ThresholdShareCreated", "ThresholdShareCreated", @@ -195,7 +261,6 @@ async fn test_trbfv_actor() -> Result<()> { .await?; assert_eq!(h.event_types(), expected); - // Aggregate decryption // First we get the public key diff --git a/crates/tests/tests/integration_legacy.rs b/crates/tests/tests/integration_legacy.rs index 29a9f04cc7..b961682435 100644 --- a/crates/tests/tests/integration_legacy.rs +++ b/crates/tests/tests/integration_legacy.rs @@ -5,6 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use actix::prelude::*; +use actix::Actor; +use alloy::primitives::{FixedBytes, I256, U256}; use anyhow::*; use e3_ciphernode_builder::CiphernodeBuilder; use e3_ciphernode_builder::CiphernodeHandle; @@ -13,9 +15,10 @@ use e3_data::GetDump; use e3_data::InMemStore; use e3_events::GetEvents; use e3_events::{ - CiphernodeSelected, CiphertextOutputPublished, E3Requested, E3id, EnclaveEvent, EventBus, - EventBusConfig, HistoryCollector, OrderedSet, PlaintextAggregated, PublicKeyAggregated, Seed, - Shutdown, Subscribe, TakeEvents, + CiphernodeSelected, CiphertextOutputPublished, CommitteeFinalized, ConfigurationUpdated, + E3Requested, E3id, EnclaveEvent, EventBus, EventBusConfig, HistoryCollector, + OperatorActivationChanged, OrderedSet, PlaintextAggregated, PublicKeyAggregated, Seed, + Shutdown, Subscribe, TakeEvents, TicketBalanceUpdated, }; use e3_net::events::GossipData; use e3_net::{events::NetEvent, NetEventTranslator}; @@ -54,7 +57,8 @@ async fn setup_local_ciphernode( .testmode_with_history() .testmode_with_errors() .with_pubkey_aggregation() - .with_plaintext_aggregation(); + .with_plaintext_aggregation() + .with_sortition_score(); if let Some(data) = data { builder = builder.with_datastore((&data).into()); @@ -111,18 +115,41 @@ async fn create_local_ciphernodes( Ok(result) } -async fn add_ciphernodes( +async fn setup_score_sortition_environment( bus: &Addr>, - addrs: &Vec, + eth_addrs: &Vec, chain_id: u64, -) -> Result> { - let mut committee = AddToCommittee::new(&bus, chain_id); - let mut evts: Vec = vec![]; +) -> Result<()> { + bus.send(EnclaveEvent::from(ConfigurationUpdated { + parameter: "ticketPrice".to_string(), + old_value: U256::ZERO, + new_value: U256::from(10_000_000u64), + chain_id, + })) + .await?; - for addr in addrs { - evts.push(committee.add(addr).await?); + let mut adder = AddToCommittee::new(bus, chain_id); + for addr in eth_addrs { + adder.add(addr).await?; + + bus.send(EnclaveEvent::from(TicketBalanceUpdated { + operator: addr.clone(), + delta: I256::try_from(1_000_000_000u64).unwrap(), + new_balance: U256::from(1_000_000_000u64), + reason: FixedBytes::ZERO, + chain_id, + })) + .await?; + + bus.send(EnclaveEvent::from(OperatorActivationChanged { + operator: addr.clone(), + active: true, + chain_id, + })) + .await?; } - Ok(evts) + + Ok(()) } // Type for our tests to test against @@ -151,7 +178,8 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { .collect::>(); println!("Adding ciphernodes..."); - add_ciphernodes(&bus, ð_addrs, 1).await?; + + setup_score_sortition_environment(&bus, ð_addrs, 1).await?; let e3_request_event = EnclaveEvent::from(E3Requested { e3_id: e3_id.clone(), @@ -169,6 +197,14 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { // Test that we cannot send the same event twice bus.send(e3_request_event.clone()).await?; + // Finalize committee with all available nodes + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: e3_id.clone(), + committee: eth_addrs.clone(), + chain_id: 1, + })) + .await?; + // Generate the test shares and pubkey let rng_test = create_shared_rng_from_u64(42); let test_shares = generate_pk_shares(¶ms, &crpoly, &rng_test, ð_addrs)?; @@ -182,7 +218,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { let history_collector = ciphernodes.get(2).unwrap().history().unwrap(); let history = history_collector - .send(TakeEvents::::new(9)) + .send(TakeEvents::::new(18)) .await?; let aggregated_event: Vec<_> = history @@ -193,7 +229,11 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { }) .collect(); - assert_eq!(aggregated_event, vec![expected_aggregated_event]); + assert!( + !aggregated_event.is_empty(), + "No PublicKeyAggregated event found" + ); + assert_eq!(aggregated_event.last().unwrap(), &expected_aggregated_event); println!("Aggregating decryption..."); // Aggregate decryption @@ -236,12 +276,12 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { async fn test_stopped_keyshares_retain_state() -> Result<()> { let e3_id = E3id::new("1234", 1); let (rng, cn1_address, cn1_data, cn2_address, cn2_data, cipher, history, params, crpoly) = { - let (bus, rng, seed, params, crpoly, ..) = get_common_setup(None)?; + let (bus, rng, seed, params, crpoly, _, _) = get_common_setup(None)?; let cipher = Arc::new(Cipher::from_password("Don't tell anyone my secret").await?); let ciphernodes = create_local_ciphernodes(&bus, &rng, 2, &cipher).await?; let eth_addrs = ciphernodes.iter().map(|n| n.address()).collect::>(); - add_ciphernodes(&bus, ð_addrs, 1).await?; + setup_score_sortition_environment(&bus, ð_addrs, 1).await?; let [cn1, cn2] = &ciphernodes.as_slice() else { panic!("Not enough elements") @@ -260,10 +300,18 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { .clone(), ) .await?; + + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: e3_id.clone(), + committee: eth_addrs.clone(), + chain_id: 1, + })) + .await?; + let history_collector = cn1.history().unwrap(); let error_collector = cn1.errors().unwrap(); let history = history_collector - .send(TakeEvents::::new(7)) + .send(TakeEvents::::new(14)) .await?; let errors = error_collector.send(GetEvents::new()).await?; @@ -347,7 +395,7 @@ async fn test_stopped_keyshares_retain_state() -> Result<()> { .await?; let history = history_collector - .send(TakeEvents::::new(4)) + .send(TakeEvents::::new(5)) .await?; let actual = history.iter().find_map(|evt| match evt { @@ -450,8 +498,9 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { // Setup actual ciphernodes and dispatch add events let ciphernodes = create_local_ciphernodes(&bus, &rng, 3, &cipher).await?; let eth_addrs = ciphernodes.iter().map(|tup| tup.address()).collect(); - add_ciphernodes(&bus, ð_addrs, 1).await?; - add_ciphernodes(&bus, ð_addrs, 2).await?; + + setup_score_sortition_environment(&bus, ð_addrs, 1).await?; + setup_score_sortition_environment(&bus, ð_addrs, 2).await?; // Send the computation requested event bus.send(EnclaveEvent::from(E3Requested { @@ -464,6 +513,12 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { })) .await?; + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: E3id::new("1234", 1), + committee: eth_addrs.clone(), + chain_id: 1, + })) + .await?; // Generate the test shares and pubkey let rng_test = create_shared_rng_from_u64(42); let test_pubkey = aggregate_public_key(&generate_pk_shares( @@ -472,7 +527,7 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { let history_collector = ciphernodes.last().unwrap().history().unwrap(); let history = history_collector - .send(TakeEvents::::new(12)) + .send(TakeEvents::::new(28)) .await?; assert_eq!( @@ -495,12 +550,19 @@ async fn test_duplicate_e3_id_with_different_chain_id() -> Result<()> { })) .await?; + bus.send(EnclaveEvent::from(CommitteeFinalized { + e3_id: E3id::new("1234", 2), + committee: eth_addrs.clone(), + chain_id: 2, + })) + .await?; + let test_pubkey = aggregate_public_key(&generate_pk_shares( ¶ms, &crpoly, &rng_test, ð_addrs, )?)?; let history = history_collector - .send(TakeEvents::::new(6)) + .send(TakeEvents::::new(8)) .await?; assert_eq!( diff --git a/deploy/.env.example b/deploy/.env.example index 14723a665a..5dd7d26b21 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -1,4 +1,4 @@ RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/API_KEY SEPOLIA_ENCLAVE_ADDRESS=0xCe087F31e20E2F76b6544A2E4A74D4557C8fDf77 SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS=0x0952388f6028a9Eda93a5041a3B216Ea331d97Ab -SEPOLIA_FILTER_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 +SEPOLIA_BONDING_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 diff --git a/deploy/agg.yaml b/deploy/agg.yaml index 6635a3b09a..839d7ea8f7 100644 --- a/deploy/agg.yaml +++ b/deploy/agg.yaml @@ -13,4 +13,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" \ No newline at end of file + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/cn1.yaml b/deploy/cn1.yaml index 689649b85f..901e6662eb 100644 --- a/deploy/cn1.yaml +++ b/deploy/cn1.yaml @@ -7,4 +7,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/cn2.yaml b/deploy/cn2.yaml index b7d897435d..b05344e93b 100644 --- a/deploy/cn2.yaml +++ b/deploy/cn2.yaml @@ -11,4 +11,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/cn3.yaml b/deploy/cn3.yaml index c892ab7f27..cc889447f0 100644 --- a/deploy/cn3.yaml +++ b/deploy/cn3.yaml @@ -11,4 +11,4 @@ chains: contracts: enclave: "${SEPOLIA_ENCLAVE_ADDRESS}" ciphernode_registry: "${SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS}" - filter_registry: "${SEPOLIA_FILTER_REGISTRY}" + bonding_registry: "${SEPOLIA_BONDING_REGISTRY}" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 8f64733321..a96bc32c7f 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,6 +1,6 @@ services: cn1: - image: {{IMAGE}} + image: { { IMAGE } } volumes: - ./cn1.yaml:/home/ciphernode/.config/enclave/config.yaml:ro - cn1-data:/home/ciphernode/.local/share/enclave @@ -10,7 +10,7 @@ services: env_file: .env environment: AGGREGATOR: "false" - ADDRESS: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + ADDRESS: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" QUIC_PORT: 9091 deploy: replicas: 1 @@ -19,7 +19,7 @@ services: - global-network cn2: - image: {{IMAGE}} + image: { { IMAGE } } volumes: - ./cn2.yaml:/home/ciphernode/.config/enclave/config.yaml:ro - cn2-data:/home/ciphernode/.local/share/enclave @@ -29,7 +29,7 @@ services: env_file: .env environment: AGGREGATOR: "false" - ADDRESS: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + ADDRESS: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" QUIC_PORT: 9092 deploy: replicas: 1 @@ -38,7 +38,7 @@ services: - global-network cn3: - image: {{IMAGE}} + image: { { IMAGE } } volumes: - ./cn3.yaml:/home/ciphernode/.config/enclave/config.yaml:ro - cn3-data:/home/ciphernode/.local/share/enclave @@ -48,7 +48,7 @@ services: env_file: .env environment: AGGREGATOR: "false" - ADDRESS: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + ADDRESS: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" QUIC_PORT: 9093 deploy: replicas: 1 @@ -57,7 +57,7 @@ services: - global-network aggregator: - image: {{IMAGE}} + image: { { IMAGE } } depends_on: - cn1 volumes: @@ -85,7 +85,7 @@ secrets: secrets_cn3: file: cn3.secrets.json secrets_agg: - file: agg.secrets.json + file: agg.secrets.json volumes: cn1-data: diff --git a/deploy/local/contracts.sh b/deploy/local/contracts.sh index e54e959a5a..8df4747723 100755 --- a/deploy/local/contracts.sh +++ b/deploy/local/contracts.sh @@ -1,26 +1,23 @@ # !/bin/bash # Install the enclave binary -cargo install --locked --path ./crates/cli --bin enclave -f - -# Deploy Contacts -(cd packages/enclave-contracts && rm -rf deployments/localhost && pnpm deploy:mocks --network localhost) +# cargo install --locked --path ./crates/cli --bin enclave -f # Deploy CRISP Contracts -(cd examples/CRISP/packages/crisp-contracts && ETH_WALLET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 FOUNDRY_PROFILE=local forge script --rpc-url http://localhost:8545 --broadcast deploy/Deploy.s.sol) +(cd examples/CRISP/packages/crisp-contracts && USE_MOCK_VERIFIER=true pnpm deploy:contracts:full --network localhost) # Add Ciphernodes to Enclave sleep 2 # wait for enclave to start # Get the addresses of the ciphernodes -CN1=0xbDA5747bFD65F08deb54cb465eB87D40e51B197E -CN2=0xdD2FD4581271e230360230F9337D5c0430Bf44C0 -CN3=0x2546BcD3c84621e976D8185a91A922aE77ECEc30 +CN1=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 +CN2=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC +CN3=0x90F79bf6EB2c4f870365E785982E1f101E93b906 # Add the ciphernodes to the enclave -pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost" -pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost" -pnpm ciphernode:add --ciphernode-address "$CN3" --network "localhost" +(cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost") +(cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost") +(cd examples/CRISP/packages/crisp-contracts && pnpm ciphernode:add --ciphernode-address "$CN3" --network "localhost") # Delete local DB diff --git a/deploy/local/nodes.sh b/deploy/local/nodes.sh index 15033ab2b7..d7deaa9d1a 100755 --- a/deploy/local/nodes.sh +++ b/deploy/local/nodes.sh @@ -1,10 +1,15 @@ # !/bin/bash # Install the enclave binary -cargo install --locked --path ./crates/cli --bin enclave -f +# cargo install --locked --path ./crates/cli --bin enclave -f concurrently \ --names "ANVIL,NODES" \ --prefix-colors "blue,yellow" \ "anvil" \ - "cd examples/CRISP && enclave wallet set --name ag --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" && enclave nodes up -v" \ No newline at end of file + "cd examples/CRISP && \ + enclave wallet set --name ag --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" && + enclave wallet set --name cn1 --private-key "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" && + enclave wallet set --name cn2 --private-key "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" && + enclave wallet set --name cn3 --private-key "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" && + enclave nodes up -v" \ No newline at end of file diff --git a/deploy/local/start.sh b/deploy/local/start.sh index 708268b8f9..20beab7df5 100755 --- a/deploy/local/start.sh +++ b/deploy/local/start.sh @@ -79,9 +79,9 @@ deploy_contracts() { # Add ciphernodes to the registry echo " Adding ciphernodes to registry..." - CN1=0xbDA5747bFD65F08deb54cb465eB87D40e51B197E - CN2=0xdD2FD4581271e230360230F9337D5c0430Bf44C0 - CN3=0x2546BcD3c84621e976D8185a91A922aE77ECEc30 + CN1=0x70997970C51812dc3A010C7d01b50e0d17dc79C8 + CN2=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC + CN3=0x90F79bf6EB2c4f870365E785982E1f101E93b906 pnpm ciphernode:add --ciphernode-address "$CN1" --network "localhost" pnpm ciphernode:add --ciphernode-address "$CN2" --network "localhost" diff --git a/deploy/swarm_deployment.md b/deploy/swarm_deployment.md index 6ea6377932..d850d69c53 100644 --- a/deploy/swarm_deployment.md +++ b/deploy/swarm_deployment.md @@ -13,7 +13,6 @@ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plug sudo docker run hello-world ``` - Initialize swarm ``` @@ -22,7 +21,6 @@ docker swarm init NOTE: If you get an error about not being able to choose between IP addresses choose the more private IP address. - ``` docker swarm init --advertise-addr 10.49.0.5 ``` @@ -86,7 +84,7 @@ Alter the variables to reflect the correct values required for the stack: export RPC_URL=wss://eth-sepolia.g.alchemy.com/v2/ export SEPOLIA_ENCLAVE_ADDRESS=0xCe087F31e20E2F76b6544A2E4A74D4557C8fDf77 export SEPOLIA_CIPHERNODE_REGISTRY_ADDRESS=0x0952388f6028a9Eda93a5041a3B216Ea331d97Ab -export SEPOLIA_FILTER_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 +export SEPOLIA_BONDING_REGISTRY=0xcBaCE7C360b606bb554345b20884A28e41436934 ``` Pay special attention to the `RPC_URL` vars as here we use a standin API key value. @@ -96,20 +94,23 @@ You can peruse the yaml config files for the nodes to see how the vars are used # Secrets Setup Utils Script We have created a secrets setup utility to aid setting up the secrets for each node. - + To deploy with swarm we need to set up the secrets file for our cluster. ## Run + ```bash ./deploy/copy-secrets.sh ``` ## What it does + - Copies `example.secrets.json` to create `cn1/2/3` and `agg.secrets.json` files - Skips existing files - Warns with yellow arrows (==>) if any files are identical to the example ## Example output + ```bash Created cn1.secrets.json Skipping cn2.secrets.json - file already exists @@ -121,7 +122,7 @@ Remember to modify any highlighted files before use with unique secrets. # Deploy a version to the stack -To deploy +To deploy ``` ./deploy/deploy.sh enclave ghcr.io/gnosisguild/ciphernode:latest @@ -153,4 +154,3 @@ enclave_cn2.1.zom4r645ophf@nixos | 2024-12-19T23:47:08.582536Z INFO enclave: ``` This can help you identify which compilation you are looking at. This works by generating a unique ID based on the complication time. - diff --git a/docs/pages/CRISP/running-e3.mdx b/docs/pages/CRISP/running-e3.mdx index aa593a6fc5..64e8001183 100644 --- a/docs/pages/CRISP/running-e3.mdx +++ b/docs/pages/CRISP/running-e3.mdx @@ -7,7 +7,8 @@ import { Steps } from 'nextra/components' # Running an E3 Program -In this section, we will go through all the steps to run an E3 Program using CRISP. We will run a complete voting round of CRISP and do the following: +In this section, we will go through all the steps to run an E3 Program using CRISP. We will run a +complete voting round of CRISP and do the following: - Start the infrastructure (nodes and contracts) - Start the CRISP applications (client, server, program) @@ -24,17 +25,20 @@ Please make sure you have followed the [CRISP Setup](/CRISP/setup) guide before First, ensure you have the infrastructure running. If you haven't already, complete the setup: **Terminal 1: Start Anvil** + ```sh anvil ``` **Terminal 2: Start Ciphernodes** + ```sh cd examples/CRISP enclave nodes up -v ``` -Make sure contracts are deployed and ciphernodes are added to the registry as described in the setup guide. +Make sure contracts are deployed and ciphernodes are added to the registry as described in the setup +guide. ### Start the Client Application @@ -70,7 +74,7 @@ Navigate to the program directory and start the program server: ```sh cd examples/CRISP/ -enclave program start +enclave program start ``` This runs the RISC Zero program server that handles secure computations. @@ -82,7 +86,7 @@ cd examples/CRISP/ enclave program start --dev true ``` -In this case, Risc0 will not be used to generate proofs, and instead these will be mocked. +In this case, Risc0 will not be used to generate proofs, and instead these will be mocked. ### Initialize a New Voting Round @@ -101,6 +105,7 @@ Follow these steps in the CLI: 2. Choose `Initialize new E3 round` to start a new voting round You should see output similar to: + ```sh [2024-10-22 11:56:11] [commands.rs:42] - Starting new CRISP round! [2024-10-22 11:56:11] [commands.rs:46] - Enabling E3 Program... @@ -135,11 +140,13 @@ To interact with the client application, you need to configure MetaMask: You can monitor the entire process through the various terminal outputs: **Server logs will show:** + - Vote submissions being received - Computation starting when the voting period ends - Results being computed and published **Example server output:** + ```sh [2024-10-22 11:59:12] [handlers.rs:95] - Vote Count: 1 [2024-10-22 11:59:12] [handlers.rs:101] - Starting computation for E3: 0 @@ -150,16 +157,18 @@ Prove function execution time: 2 minutes and 37 seconds ``` **Ciphernode logs will show:** + ```sh INFO Extracted log from evm sending now. INFO evt=CiphertextOutputPublished(e3_id: 0) e3_id=0 -INFO evt=DecryptionshareCreated(e3_id: 0, node: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30) e3_id=0 +INFO evt=DecryptionshareCreated(e3_id: 0, node: 0x90F79bf6EB2c4f870365E785982E1f101E93b906) e3_id=0 INFO evt=PlaintextAggregated(e3_id: 0, src_chain_id: 31337) e3_id=0 INFO evt=E3RequestComplete(e3_id: 0) INFO Plaintext published. tx=0x320dd95358cc86c2a709b6fec0c6865b43fa063cb61dfcb8a748005d4886f040 ``` **Final result logs:** + ```sh [2024-10-22 12:01:49] [handlers.rs:171] - Handling PlaintextOutputPublished event... [2024-10-22 12:01:49] [handlers.rs:181] - Vote Count: 1 @@ -183,7 +192,8 @@ The CRISP voting process involves several key steps: ## Troubleshooting - **Ensure all terminals remain open** during the voting process -- **MetaMask connection issues**: Check that you're connected to the correct network (Chain ID: 31337) +- **MetaMask connection issues**: Check that you're connected to the correct network (Chain + ID: 31337) - **Transaction failures**: Verify you have sufficient ETH balance from the Anvil faucet - **Server errors**: Monitor the server logs for detailed error messages - **Ciphernode issues**: Ensure all ciphernode processes are running and connected @@ -198,4 +208,3 @@ Once you've successfully run a voting round, you can: - **Deploy to testnet**: Move beyond local development to public testnets ![Result](/poll-result.png) - diff --git a/docs/pages/building-with-enclave.mdx b/docs/pages/building-with-enclave.mdx index e68fbb532c..c0f2113244 100644 --- a/docs/pages/building-with-enclave.mdx +++ b/docs/pages/building-with-enclave.mdx @@ -52,7 +52,7 @@ function request( IE3Program e3Program, bytes memory e3ProgramParams, bytes memory computeProviderParams -) external payable +) external ``` 2. Contract validates request parameters @@ -238,18 +238,15 @@ the finality of the outcome and to appropriately handle re-orgs. ## Best Practices 1. **Request Management** - - Set appropriate thresholds - Choose realistic time windows 2. **Input Handling** - - Encrypt inputs properly - Include necessary ZKPs - Submit within time windows 3. **Result Processing** - - Listen for all relevant events - Implement timeout handling - Verify result integrity diff --git a/docs/pages/computation-flow.mdx b/docs/pages/computation-flow.mdx index cd4ade37b7..d2ee08a111 100644 --- a/docs/pages/computation-flow.mdx +++ b/docs/pages/computation-flow.mdx @@ -35,7 +35,7 @@ Providers, or other network participant. IE3Program e3Program, bytes memory e3ProgramParams, bytes memory computeProviderParams - ) external payable returns (uint256 e3Id, E3 memory e3) + ) external returns (uint256 e3Id, E3 memory e3) ``` ### Phase 2: Node Selection @@ -57,9 +57,9 @@ During this phase, Data Providers — who may include individual users, applicat ensure they are valid for the requested E3. Some of these proofs are generic (e.g., proof of valid encryption) while others will be specific to your application. 3. **Submit Inputs**: Both encrypted data and ZKPs are submitted to the Enclave contract, which will - call the `validate` function on your E3P InputValidator smart contract. The input hash is then added to a Merkle - tree, the root of which can later be used to anchor proofs of correct execution of your E3 - Program. + call the `validate` function on your E3P InputValidator smart contract. The input hash is then + added to a Merkle tree, the root of which can later be used to anchor proofs of correct execution + of your E3 Program. ```solidity function validate( diff --git a/docs/pages/hello-world-tutorial.mdx b/docs/pages/hello-world-tutorial.mdx index dd6b7c3701..3db59eac73 100644 --- a/docs/pages/hello-world-tutorial.mdx +++ b/docs/pages/hello-world-tutorial.mdx @@ -5,13 +5,16 @@ description: 'Build your first E3 program from scratch with step-by-step explana # Hello World Tutorial -This tutorial walks you through building your first E3 program from scratch. You'll learn how each component works and how they interact to create a secure, encrypted computation. +This tutorial walks you through building your first E3 program from scratch. You'll learn how each +component works and how they interact to create a secure, encrypted computation. -> Make sure to complete the [Quick Start](/quick-start) guide first to get familiar with the basic workflow before diving into this detailed tutorial. +> Make sure to complete the [Quick Start](/quick-start) guide first to get familiar with the basic +> workflow before diving into this detailed tutorial. ## What We're Building We'll create a simple E3 program that: + 1. **Accepts** two encrypted numbers from users 2. **Computes** their sum using Fully Homomorphic Encryption 3. **Returns** the encrypted result without ever decrypting the inputs @@ -19,6 +22,7 @@ We'll create a simple E3 program that: ## Prerequisites Before starting, ensure you have: + - [Enclave CLI installed](/installation) - Basic knowledge of Rust and TypeScript - Rust, Docker, Node.js, and pnpm installed @@ -61,7 +65,7 @@ pub fn fhe_processor(fhe_inputs: &FHEInputs) -> Vec { // Start with zero (encrypted) let mut sum = Ciphertext::zero(¶ms); - + // Add each encrypted input to the sum for ciphertext_bytes in &fhe_inputs.ciphertexts { let ciphertext = Ciphertext::from_bytes(&ciphertext_bytes.0, ¶ms).unwrap(); @@ -116,15 +120,15 @@ The `enclave.config.yaml` file configures your development environment: ```yaml chains: - - name: "hardhat" - rpc_url: "ws://localhost:8545" + - name: 'hardhat' + rpc_url: 'ws://localhost:8545' contracts: - e3_program: "0x9A676e781A523b5d0C0e43731313A708CB607508" + e3_program: '0x9A676e781A523b5d0C0e43731313A708CB607508' # ... other contract addresses nodes: - cn1: # Ciphernode 1 - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + cn1: # Ciphernode 1 + address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' quic_port: 9201 autonetkey: true autopassword: true @@ -189,7 +193,7 @@ Modify your program to accept 3 or more encrypted inputs. Customize the client application in `./client/src/` to match your computation. -Happy building with Enclave! 🚀 +Happy building with Enclave! 🚀 ## Next Steps diff --git a/docs/pages/setting-up-server.mdx b/docs/pages/setting-up-server.mdx index 8f6923419f..cbbd12f783 100644 --- a/docs/pages/setting-up-server.mdx +++ b/docs/pages/setting-up-server.mdx @@ -74,7 +74,6 @@ await sdk.initialize() // Request a new E3 computation const hash = await sdk.requestE3({ - filter: '0x0000000000000000000000000000000000000000', threshold: [2, 3], startWindow: [BigInt(0), BigInt(100)], duration: BigInt(3600), @@ -139,7 +138,6 @@ function E3Dashboard() { const handleRequestE3 = async () => { try { const hash = await requestE3({ - filter: '0x0000000000000000000000000000000000000000', threshold: [2, 3], startWindow: [BigInt(Date.now()), BigInt(Date.now() + 300000)], duration: BigInt(1800), diff --git a/examples/CRISP/Readme.md b/examples/CRISP/Readme.md index 8ef327afe2..c1bf7dad58 100644 --- a/examples/CRISP/Readme.md +++ b/examples/CRISP/Readme.md @@ -200,7 +200,7 @@ After deployment, you will see the addresses for the following contracts: - Enclave - Ciphernode Registry -- Naive Registry Filter +- Bonding Registry Filter - Mock Input Validator - Mock E3 Program - Mock Decryption Verifier @@ -258,11 +258,11 @@ BITQUERY_API_KEY="" # Cron-job API key to trigger new rounds CRON_API_KEY=1234567890 -# Based on Default Hardhat Deployments (Only for testing) -ENCLAVE_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" -CIPHERNODE_REGISTRY_ADDRESS="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -NAIVE_REGISTRY_FILTER_ADDRESS="0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -E3_PROGRAM_ADDRESS="0x0B306BF915C4d645ff596e518fAf3F9669b97016" # CRISPProgram Contract Address +# Based on Default Anvil Deployments (Only for testing) +ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +CIPHERNODE_REGISTRY_ADDRESS="0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" +E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address +FEE_TOKEN_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3" # Mock ERC20 Token Address # E3 Config E3_WINDOW_SIZE=40 @@ -274,6 +274,9 @@ E3_DURATION=160 E3_COMPUTE_PROVIDER_NAME="RISC0" E3_COMPUTE_PROVIDER_PARALLEL=false E3_COMPUTE_PROVIDER_BATCH_SIZE=4 # Must be a power of 2 + +# Bitquery API Key (optional, leave empty if not using) +BITQUERY_API_KEY="" ``` ## Running Ciphernodes diff --git a/examples/CRISP/client/.env.example b/examples/CRISP/client/.env.example index 24be538821..233b53f9d7 100644 --- a/examples/CRISP/client/.env.example +++ b/examples/CRISP/client/.env.example @@ -1,4 +1,4 @@ VITE_ENCLAVE_API=http://127.0.0.1:4000 VITE_TWITTER_SERVERLESS_API= VITE_WALLETCONNECT_PROJECT_ID= -VITE_E3_PROGRAM_ADDRESS=0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1 # Default E3 program address from hardhat +VITE_E3_PROGRAM_ADDRESS=0xc5a5C42992dECbae36851359345FE25997F5C42d # Default E3 program address from hardhat diff --git a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js index 96c965c001..35a7aa5656 100755 --- a/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js +++ b/examples/CRISP/client/libs/wasm/pkg/crisp_worker.js @@ -4,51 +4,46 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { EnclaveSDK, FheProtocol } from '@enclave-e3/sdk'; -import circuit from "../../noir/crisp_circuit.json"; +import { EnclaveSDK, FheProtocol } from '@enclave-e3/sdk' +import circuit from '../../noir/crisp_circuit.json' self.onmessage = async function (event) { - const { type, data } = event.data; - switch (type) { - case 'encrypt_vote': - try { - const { voteId, publicKey } = data; - // use default params for now as they do not matter for what we are doing here, - // which is just encrypting the vote and generating a proof - const sdk = EnclaveSDK.create({ - chainId: 31337, - contracts: { - enclave: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - ciphernodeRegistry: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - }, - // local node - rpcUrl: "http://localhost:8545", - // default Anvil private key - privateKey: - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", - protocol: FheProtocol.BFV, - }); + const { type, data } = event.data + switch (type) { + case 'encrypt_vote': + try { + const { voteId, publicKey } = data + // use default params for now as they do not matter for what we are doing here, + // which is just encrypting the vote and generating a proof + const sdk = EnclaveSDK.create({ + chainId: 31337, + contracts: { + enclave: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + ciphernodeRegistry: '0xc6e7DF5E7b4f2A278906862b61205850344D4e7d', + }, + // local node + rpcUrl: 'http://localhost:8545', + // default Anvil private key + privateKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + protocol: FheProtocol.BFV, + }) - const result = await sdk.encryptNumberAndGenProof( - voteId, - publicKey, - circuit - ); - - self.postMessage({ - type: 'encrypt_vote', - success: true, - encryptedVote: { - vote: result.encryptedVote, - proofData: result.proof, - }, - }); - } catch (error) { - self.postMessage({ type: 'encrypt_vote', success: false, error: error.message }); - } - break; + const result = await sdk.encryptNumberAndGenProof(voteId, publicKey, circuit) - default: - console.error(`Unknown message type: ${type}`); - } -}; + self.postMessage({ + type: 'encrypt_vote', + success: true, + encryptedVote: { + vote: result.encryptedVote, + proofData: result.proof, + }, + }) + } catch (error) { + self.postMessage({ type: 'encrypt_vote', success: false, error: error.message }) + } + break + + default: + console.error(`Unknown message type: ${type}`) + } +} diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index d89f8b058d..6119452800 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -2,9 +2,21 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + e3_program: + address: "0xc5a5C42992dECbae36851359345FE25997F5C42d" + deploy_block: 1 # Set to actual deploy block + enclave: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 1 # Set to actual deploy block + ciphernode_registry: + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + deploy_block: 1 # Set to actual deploy block + bonding_registry: + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 1 # Set to actual deploy block + fee_token: + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + deploy_block: 1 # Set to actual deploy block program: dev: true @@ -15,23 +27,23 @@ program: nodes: cn1: - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" quic_port: 9201 autonetkey: true autopassword: true cn2: - address: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" quic_port: 9202 autonetkey: true autopassword: true cn3: - address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" quic_port: 9203 autonetkey: true autopassword: true ag: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" - quic_port: 9094 + address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" + quic_port: 9204 autonetkey: true autopassword: true role: diff --git a/examples/CRISP/package.json b/examples/CRISP/package.json index aebd61edf2..8cdda65f2b 100644 --- a/examples/CRISP/package.json +++ b/examples/CRISP/package.json @@ -8,6 +8,7 @@ "url": "https://github.com/gnosisguild" }, "scripts": { + "compile": "forge compile", "cli": "bash ./scripts/cli.sh", "dev:setup": "bash ./scripts/setup.sh", "dev:build": "bash ./scripts/build.sh", diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 49abb9532a..22c64fe348 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -82,80 +82,149 @@ } } }, + "undefined": { + "RiscZeroGroth16Verifier": { + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "CRISPInputValidator": { + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + } + }, + "default": { + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + } + }, "localhost": { "PoseidonT3": { - "blockNumber": 21, + "blockNumber": 3, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, - "Enclave": { + "MockUSDC": { "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "registry": "0x0000000000000000000000000000000000000001", - "maxDuration": "2592000", - "params": [ - "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" - ] + "initialSupply": "1000000" }, "blockNumber": 4, "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, - "CiphernodeRegistryOwnable": { + "EnclaveToken": { "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "enclaveAddress": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, "blockNumber": 5, "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" }, - "NaiveRegistryFilter": { + "EnclaveTicketToken": { "constructorArgs": { - "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "ciphernodeRegistryAddress": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 6, - "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, - "MockComputeProvider": { + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, "blockNumber": 8, "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, - "MockDecryptionVerifier": { + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, "blockNumber": 9, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, - "MockInputValidator": { + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "3" + }, "blockNumber": 10, "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, - "MockE3Program": { + "Enclave": { "constructorArgs": { - "mockInputValidator": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] }, "blockNumber": 11, "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, - "MockRISC0Verifier": { - "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + "MockComputeProvider": { + "blockNumber": 20, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + }, + "MockDecryptionVerifier": { + "blockNumber": 21, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, + "MockInputValidator": { + "blockNumber": 22, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "MockE3Program": { + "constructorArgs": { + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "blockNumber": 23, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + }, + "RiscZeroGroth16Verifier": { + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "CRISPInputValidator": { - "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" }, "CRISPInputValidatorFactory": { - "address": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", "constructorArgs": { - "inputValidator": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + "inputValidator": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" } }, "HonkVerifier": { - "address": "0x0B306BF915C4d645ff596e518fAf3F9669b97016" + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" }, "CRISPProgram": { - "address": "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1", + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d", "constructorArgs": { - "enclave": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - "verifierAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", - "inputValidatorAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", - "honkVerifierAddress": "0x0B306BF915C4d645ff596e518fAf3F9669b97016", + "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "verifierAddress": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "inputValidatorAddress": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + "honkVerifierAddress": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" } } diff --git a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts index 25d623394e..bced15cd93 100644 --- a/examples/CRISP/packages/crisp-contracts/hardhat.config.ts +++ b/examples/CRISP/packages/crisp-contracts/hardhat.config.ts @@ -5,8 +5,11 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import type { HardhatUserConfig } from "hardhat/config"; -import { cleanDeploymentsTask} from "@enclave-e3/contracts/tasks/utils"; -import { ciphernodeAdd } from "@enclave-e3/contracts/tasks/ciphernode"; +import { cleanDeploymentsTask } from "@enclave-e3/contracts/tasks/utils"; +import { + ciphernodeAdd, + ciphernodeAdminAdd, +} from "@enclave-e3/contracts/tasks/ciphernode"; import dotenv from "dotenv"; import hardhatEthersChaiMatchers from "@nomicfoundation/hardhat-ethers-chai-matchers"; @@ -71,15 +74,22 @@ const config: HardhatUserConfig = { hardhatToolboxMochaEthersPlugin, hardhatVerify, ], - tasks: [ - cleanDeploymentsTask, - ciphernodeAdd, - ], + tasks: [cleanDeploymentsTask, ciphernodeAdd, ciphernodeAdminAdd], networks: { hardhat: { type: "edr-simulated", chainType: "l1", }, + localhost: { + accounts: { + mnemonic, + }, + chainId: chainIds.hardhat, + url: "http://localhost:8545", + type: "http", + chainType: "l1", + timeout: 60000, + }, ganache: { accounts: { mnemonic, @@ -87,25 +97,26 @@ const config: HardhatUserConfig = { chainId: chainIds.ganache, url: "http://localhost:8545", type: "http", + timeout: 60000, }, arbitrum: getChainConfig( "arbitrum-mainnet", - process.env.ARBISCAN_API_KEY || "", + process.env.ARBISCAN_API_KEY || "" ), avalanche: getChainConfig("avalanche", process.env.SNOWTRACE_API_KEY || ""), bsc: getChainConfig("bsc", process.env.BSCSCAN_API_KEY || ""), mainnet: getChainConfig("mainnet", process.env.ETHERSCAN_API_KEY || ""), optimism: getChainConfig( "optimism-mainnet", - process.env.OPTIMISM_API_KEY || "", + process.env.OPTIMISM_API_KEY || "" ), "polygon-mainnet": getChainConfig( "polygon-mainnet", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), "polygon-mumbai": getChainConfig( "polygon-mumbai", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), sepolia: getChainConfig("sepolia", process.env.ETHERSCAN_API_KEY || ""), goerli: getChainConfig("goerli", process.env.ETHERSCAN_API_KEY || ""), @@ -131,16 +142,20 @@ const config: HardhatUserConfig = { solidity: { version: "0.8.28", npmFilesToBuild: [ - "poseidon-solidity/PoseidonT3.sol", + "poseidon-solidity/PoseidonT3.sol", "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", - "@enclave-e3/contracts/contracts/registry/NaiveRegistryFilter.sol", - "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", + "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", + "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", + "@enclave-e3/contracts/contracts/token/EnclaveToken.sol", + "@enclave-e3/contracts/contracts/token/EnclaveTicketToken.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/test/MockRegistryFilter.sol", + "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", + "@enclave-e3/contracts/contracts/test/MockSlashingVerifier.sol", + "@enclave-e3/contracts/contracts/test/MockStableToken.sol", ], settings: { optimizer: { diff --git a/examples/CRISP/packages/crisp-contracts/package.json b/examples/CRISP/packages/crisp-contracts/package.json index aadc206fa8..f29f703175 100644 --- a/examples/CRISP/packages/crisp-contracts/package.json +++ b/examples/CRISP/packages/crisp-contracts/package.json @@ -11,7 +11,7 @@ }, "scripts": { "compile": "hardhat compile", - "ciphernode:add": "hardhat ciphernode:add", + "ciphernode:add": "hardhat ciphernode:admin-add", "clean:deployments": "hardhat utils:clean-deployments", "deploy:contracts": "hardhat run deploy/deploy.ts", "deploy:contracts:full": "export DEPLOY_ENCLAVE=true && pnpm deploy:contracts", diff --git a/examples/CRISP/playwright.config.ts b/examples/CRISP/playwright.config.ts index 991541d20b..ae724033bc 100644 --- a/examples/CRISP/playwright.config.ts +++ b/examples/CRISP/playwright.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ timeout: 5 * 60 * 10000, use: { baseURL: "http://localhost:3000", - actionTimeout: 60 * 1000, + actionTimeout: 75 * 1000, }, retries: process.env.CI ? 2 : 0, fullyParallel: true, diff --git a/examples/CRISP/scripts/dev_cipher.sh b/examples/CRISP/scripts/dev_cipher.sh index 9f3dc7f5cf..2ca9dbb6c2 100755 --- a/examples/CRISP/scripts/dev_cipher.sh +++ b/examples/CRISP/scripts/dev_cipher.sh @@ -6,9 +6,15 @@ set -euo pipefail rm -rf ./enclave/data rm -rf ./enclave/config -PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -enclave wallet set --name ag --private-key "$PRIVATE_KEY" +PRIVATE_KEY_AG="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" + +enclave wallet set --name ag --private-key "$PRIVATE_KEY_AG" +enclave wallet set --name cn1 --private-key "$PRIVATE_KEY_CN1" +enclave wallet set --name cn2 --private-key "$PRIVATE_KEY_CN2" +enclave wallet set --name cn3 --private-key "$PRIVATE_KEY_CN3" # using & instead of -d so that wait works below enclave nodes up -v & diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index c777948303..75e0932a93 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -12,11 +12,11 @@ BITQUERY_API_KEY="" # Cron-job API key to trigger new rounds CRON_API_KEY=1234567890 -# Based on Default Hardhat Deployments (Only for testing) -ENCLAVE_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" -CIPHERNODE_REGISTRY_ADDRESS="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -NAIVE_REGISTRY_FILTER_ADDRESS="0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -E3_PROGRAM_ADDRESS="0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1" # CRISPProgram Contract Address +# Based on Default Anvil Deployments (Only for testing) +ENCLAVE_ADDRESS="0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" +CIPHERNODE_REGISTRY_ADDRESS="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" +E3_PROGRAM_ADDRESS="0xc5a5C42992dECbae36851359345FE25997F5C42d" # CRISPProgram Contract Address +FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config E3_WINDOW_SIZE=40 diff --git a/examples/CRISP/server/Readme.md b/examples/CRISP/server/Readme.md index ee1b7282bf..ce1e14e8e5 100644 --- a/examples/CRISP/server/Readme.md +++ b/examples/CRISP/server/Readme.md @@ -34,7 +34,7 @@ This is a Rust-based server implementation for CRISP, which is built on top of t ENCLAVE_ADDRESS=your_enclave_contract_address E3_PROGRAM_ADDRESS=your_e3_program_address CIPHERNODE_REGISTRY_ADDRESS=your_ciphernode_registry_address - NAIVE_REGISTRY_FILTER_ADDRESS=your_naive_registry_filter_address + FEE_TOKEN_ADDRESS=free_token_address CHAIN_ID=your_chain_id CRON_API_KEY=your_cron_api_key ``` diff --git a/examples/CRISP/server/src/cli/approve.rs b/examples/CRISP/server/src/cli/approve.rs new file mode 100644 index 0000000000..c248fa3df3 --- /dev/null +++ b/examples/CRISP/server/src/cli/approve.rs @@ -0,0 +1,65 @@ +// 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, U256}; +use alloy::sol; +use eyre::Result; + +sol! { + #[derive(Debug)] + #[sol(rpc)] + contract ERC20 { + function approve(address spender, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + } +} + +pub async fn approve_token( + http_rpc_url: &str, + private_key: &str, + token_address: &str, + spender_address: &str, + amount: U256, +) -> Result<()> { + use alloy::network::EthereumWallet; + use alloy::providers::{Provider, ProviderBuilder}; + use alloy::signers::local::PrivateKeySigner; + + let token_address: Address = token_address.parse()?; + let spender_address: Address = spender_address.parse()?; + let signer: PrivateKeySigner = private_key.parse()?; + let wallet = EthereumWallet::from(signer); + + let provider = ProviderBuilder::new() + .wallet(wallet) + .connect(http_rpc_url) + .await?; + + let contract = ERC20::new(token_address, &provider); + + let owner = provider.get_accounts().await?[0]; + let current_allowance = contract.allowance(owner, spender_address).call().await?; + + log::info!("Current allowance: {}", current_allowance); + + if current_allowance < amount { + log::info!( + "Approving {} tokens for spender {}", + amount, + spender_address + ); + let builder = contract.approve(spender_address, amount); + let receipt = builder.send().await?.get_receipt().await?; + log::info!( + "Approval successful. TxHash: {:?}", + receipt.transaction_hash + ); + } else { + log::info!("Sufficient allowance already exists"); + } + + Ok(()) +} diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index bad8997c51..2ad1116e34 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -11,8 +11,10 @@ use num_bigint::BigUint; use reqwest::Client; use serde::{Deserialize, Serialize}; +use super::approve; use super::CLI_DB; use alloy::primitives::{Address, Bytes, U256}; +use alloy::providers::{Provider, ProviderBuilder}; use crisp::config::CONFIG; use e3_sdk::bfv_helpers::{build_bfv_params_arc, encode_bfv_params, params::SET_2048_1032193_1}; use e3_sdk::evm_helpers::contracts::{EnclaveContract, EnclaveRead, EnclaveWrite}; @@ -56,6 +58,17 @@ struct CTRequest { ct_bytes: Vec, } +pub async fn get_current_timestamp() -> Result> { + let provider = ProviderBuilder::new().connect(&CONFIG.http_rpc_url).await?; + let block = provider + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .ok_or_else(|| anyhow::anyhow!("Latest block not found"))?; + + Ok(block.header.timestamp) +} + pub async fn initialize_crisp_round( token_address: &str, balance_threshold: &str, @@ -73,10 +86,12 @@ pub async fn initialize_crisp_round( .await?; let e3_program: Address = CONFIG.e3_program_address.parse()?; - info!("Enabling E3 Program..."); + info!("Enabling E3 Program with address: {}", e3_program); match contract.is_e3_program_enabled(e3_program).await { Ok(enabled) => { + info!("Debug - E3 Program enabled status: {}", enabled); if !enabled { + info!("E3 Program not enabled, attempting to enable..."); match contract.enable_e3_program(e3_program).await { Ok(res) => info!("E3 Program enabled. TxHash: {:?}", res.transaction_hash), Err(e) => info!("Error enabling E3 Program: {:?}", e), @@ -99,11 +114,11 @@ pub async fn initialize_crisp_round( // Serialize the custom parameters to bytes. let custom_params_bytes = Bytes::from(serde_json::to_vec(&custom_params)?); - let filter: Address = CONFIG.naive_registry_filter_address.parse()?; let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; - let start_window: [U256; 2] = [ - U256::from(Utc::now().timestamp()), - U256::from(Utc::now().timestamp() + CONFIG.e3_window_size as i64), + let mut current_timestamp = get_current_timestamp().await?; + let mut start_window: [U256; 2] = [ + U256::from(current_timestamp), + U256::from(current_timestamp + CONFIG.e3_window_size as u64), ]; let duration: U256 = U256::from(CONFIG.e3_duration); let e3_params = Bytes::from(encode_bfv_params(&generate_bfv_parameters())); @@ -114,9 +129,55 @@ pub async fn initialize_crisp_round( }; let compute_provider_params_bytes = Bytes::from(serde_json::to_vec(&compute_provider_params)?); + info!("Debug Before Fee Quote - start_window: {:?}", start_window); + info!( + "Debug Before Fee Quote - current timestamp: {:?}", + current_timestamp + ); + info!("Getting fee quote..."); + let fee_amount = contract + .get_e3_quote( + threshold, + start_window, + duration, + e3_program, + e3_params.clone(), + compute_provider_params_bytes.clone(), + ) + .await?; + info!("Fee required: {} tokens", fee_amount); + + info!("Approving fee token..."); + approve::approve_token( + &CONFIG.http_rpc_url, + &CONFIG.private_key, + &CONFIG.fee_token_address, + &CONFIG.enclave_address, + fee_amount, + ) + .await?; + + current_timestamp = get_current_timestamp().await?; + start_window = [ + U256::from(current_timestamp), + U256::from(current_timestamp + CONFIG.e3_window_size as u64), + ]; + + info!("Requesting E3 on contract: {}", CONFIG.enclave_address); + + info!("Debug - threshold: {:?}", threshold); + info!("Debug - start_window: {:?}", start_window); + info!("Debug - current timestamp: {:?}", current_timestamp); + info!("Debug - duration: {}", duration); + info!("Debug - e3_program: {}", e3_program); + + info!( + "Debug - Checking ciphernode registry at: {}", + CONFIG.ciphernode_registry_address + ); + let res = contract .request_e3( - filter, threshold, start_window, duration, diff --git a/examples/CRISP/server/src/cli/main.rs b/examples/CRISP/server/src/cli/main.rs index 9eadf6dd73..7a10be5e15 100644 --- a/examples/CRISP/server/src/cli/main.rs +++ b/examples/CRISP/server/src/cli/main.rs @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod approve; mod commands; use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input}; @@ -39,9 +40,13 @@ struct Cli { enum Commands { /// Initialize new E3 round Init { - #[arg(short, long)] + #[arg( + short, + long, + default_value = "0x0000000000000000000000000000000000000000" + )] token_address: String, - #[arg(short, long)] + #[arg(short, long, default_value = "1000000000000000000")] balance_threshold: String, }, } @@ -108,11 +113,13 @@ fn select_action() -> Result> { fn get_token_address() -> Result> { Ok(Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter the token contract address for the voting round") + .default("0x0000000000000000000000000000000000000000".to_string()) .interact_text()?) } fn get_balance_threshold() -> Result> { Ok(Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter the balance threshold for the voting round") + .default("1000000000000000000".to_string()) .interact_text()?) } diff --git a/examples/CRISP/server/src/config.rs b/examples/CRISP/server/src/config.rs index c6945bee60..904fb4abb6 100644 --- a/examples/CRISP/server/src/config.rs +++ b/examples/CRISP/server/src/config.rs @@ -19,7 +19,7 @@ pub struct Config { pub enclave_address: String, pub e3_program_address: String, pub ciphernode_registry_address: String, - pub naive_registry_filter_address: String, + pub fee_token_address: String, pub chain_id: u64, pub cron_api_key: String, // E3 parameters diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index 92822d3b45..558d9c61a9 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -12,8 +12,8 @@ use crate::server::{ token_holders::{build_tree, compute_token_holder_hashes}, CONFIG, }; +use alloy::providers::{Provider, ProviderBuilder}; use alloy::sol_types::{sol_data, SolType}; - use alloy_primitives::Address; use e3_sdk::{ evm_helpers::{ @@ -32,8 +32,8 @@ use eyre::Context; use log::info; use num_bigint::BigUint; use std::error::Error; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::time::{sleep_until, Instant}; +use std::time::Duration; +use tokio::time::sleep; type Result = std::result::Result>; @@ -154,13 +154,18 @@ pub async fn register_e3_activated( .await?; // Calculate expiration time to sleep until - let expiration = Instant::now() - + (UNIX_EPOCH + Duration::from_secs(expiration)) - .duration_since(SystemTime::now()) - .unwrap_or_else(|_| Duration::ZERO); - - sleep_until(expiration).await; - + let now = get_current_timestamp_rpc().await?; + let wait_duration = if expiration > now { + let secs = expiration - now; + info!("Need to wait {} seconds until expiration", secs); + Duration::from_secs(secs) + } else { + info!("Expired E3"); + Duration::ZERO + }; + if !wait_duration.is_zero() { + sleep(wait_duration).await; + } let e3: e3_sdk::indexer::models::E3 = repo.get_e3().await?; repo.update_status("Expired").await?; @@ -291,27 +296,29 @@ pub async fn register_committee_published( return Ok(()); } - // Convert milliseconds to seconds for comparison with block.timestamp - let start_time = UNIX_EPOCH + Duration::from_secs(e3.startWindow[0].to::()); + // Read Start time in Seconds + let start_time = e3.startWindow[0].to::(); + info!("Start time: {}", start_time); // Get current time - let now = SystemTime::now(); + let now = get_current_timestamp_rpc().await?; + info!("Current time: {}", now); // Calculate wait duration - let wait_duration = match start_time.duration_since(now) { - Ok(duration) => { - info!("Need to wait {:?} ({}s) until activation", duration, duration.as_secs()); - duration - } - Err(_) => { - info!("Activating E3"); - Duration::ZERO - } + let wait_duration = if start_time > now { + let secs = start_time - now; + info!("Need to wait {} seconds until activation", secs); + Duration::from_secs(secs) + } else { + info!("Activating E3"); + Duration::ZERO }; + info!("Wait duration: {:?}", wait_duration); // Sleep until start time - let start_instant = Instant::now() + wait_duration; - sleep_until(start_instant).await; + if !wait_duration.is_zero() { + sleep(wait_duration).await; + } // If not activated activate let tx = contract.activate(event.e3Id, event.publicKey).await?; @@ -323,10 +330,20 @@ pub async fn register_committee_published( Ok(listener) } +pub async fn get_current_timestamp_rpc() -> eyre::Result { + let provider = ProviderBuilder::new().connect(&CONFIG.http_rpc_url).await?; + let block = provider + .get_block_by_number(alloy::eips::BlockNumberOrTag::Latest) + .await? + .ok_or_else(|| eyre::eyre!("Latest block not found"))?; + + Ok(block.header.timestamp) +} + pub async fn start_indexer( ws_url: &str, contract_address: &str, - registry_filter_address: &str, + registry_address: &str, store: impl DataStore, private_key: &str, ) -> Result<()> { @@ -349,7 +366,7 @@ pub async fn start_indexer( // Registry Listener let registry_contract_listener = - EventListener::create_contract_listener(&ws_url, registry_filter_address).await?; + EventListener::create_contract_listener(&ws_url, registry_address).await?; let registry_listener = register_committee_published(registry_contract_listener, readwrite_contract).await?; registry_listener.start(); diff --git a/examples/CRISP/server/src/server/routes/rounds.rs b/examples/CRISP/server/src/server/routes/rounds.rs index 2a74cc56b7..ccebc09d91 100644 --- a/examples/CRISP/server/src/server/routes/rounds.rs +++ b/examples/CRISP/server/src/server/routes/rounds.rs @@ -195,7 +195,6 @@ pub async fn initialize_crisp_round( let custom_params_bytes = Bytes::from(serde_json::to_vec(&custom_params)?); info!("Requesting E3..."); - let filter: Address = CONFIG.naive_registry_filter_address.parse()?; let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; let start_window: [U256; 2] = [ U256::from(Utc::now().timestamp()), @@ -211,7 +210,6 @@ pub async fn initialize_crisp_round( let compute_provider_params = Bytes::from(bincode::serialize(&compute_provider_params)?); let res = contract .request_e3( - filter, threshold, start_window, duration, diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 69f4e7bda5..e12bccd1bc 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -15,7 +15,7 @@ async function runCliInit() { // Execute the command and wait for it to complete const output = execSync( "pnpm cli init --token-address 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 --balance-threshold 1000", - { encoding: "utf-8" }, + { encoding: "utf-8" } ); console.log("Command output:", output); return output; @@ -30,7 +30,7 @@ const { expect } = test; async function ensureHomePageLoaded(page: Page) { return await expect(page.locator("h4")).toHaveText( - "Coercion-Resistant Impartial Selection Protocol", + "Coercion-Resistant Impartial Selection Protocol" ); } @@ -44,10 +44,12 @@ test("CRISP smoke test", async ({ context, metamaskPage, basicSetup.walletPassword, - extensionId, + extensionId ); await runCliInit(); + // Wait 4 seconds for committee to be published + await page.waitForTimeout(4_000); await page.goto("/"); await ensureHomePageLoaded(page); await page.locator('button:has-text("Connect Wallet")').click(); @@ -62,9 +64,9 @@ test("CRISP smoke test", async ({ await page.locator('a:has-text("Historic polls")').click(); await expect(page.locator("h1")).toHaveText("Historic polls"); await expect( - page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-0'] h3"), + page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-0'] h3") ).toHaveText("100%"); await expect( - page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-1'] h3"), + page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-1'] h3") ).toHaveText("0%"); }); diff --git a/package.json b/package.json index fda8c2e0ee..a6fddf7637 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "prepare": "husky", "enclave": "cd crates && ./scripts/launch.sh", "ciphernode:lint": "cargo fmt -- --check", - "ciphernode:add": "cd packages/enclave-contracts && pnpm ciphernode:add", + "ciphernode:add": "cd packages/enclave-contracts && pnpm ciphernode:admin-add", "ciphernode:remove": "cd packages/enclave-contracts && pnpm ciphernode:remove", "ciphernode:test": "cd crates && ./scripts/test.sh", "ciphernode:build": "cargo build --locked --release", diff --git a/packages/enclave-contracts/.gitignore b/packages/enclave-contracts/.gitignore index 1671543cfc..d778ae5d7a 100644 --- a/packages/enclave-contracts/.gitignore +++ b/packages/enclave-contracts/.gitignore @@ -10,10 +10,10 @@ !/artifacts/contracts/registry/ !/artifacts/contracts/interfaces/IEnclave.sol/ !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ -!/artifacts/contracts/registry/NaiveRegistryFilter.sol/ +!/artifacts/contracts/interfaces/IBondingRegistry.sol/ !/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json -!/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +!/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json build cache coverage diff --git a/packages/enclave-contracts/README.md b/packages/enclave-contracts/README.md index 0963347a37..40b5c9b5ec 100644 --- a/packages/enclave-contracts/README.md +++ b/packages/enclave-contracts/README.md @@ -47,12 +47,56 @@ pnpm deploy:mocks --network localhost This will ensure that you are a local node running, as well as that there are no conflicting deployments stored in localhost. +## Configuration + +### Using Environment Variables (Development) + +For development, you can set your private key in a `.env` file: + +```sh +# .env +PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### Using Hardhat Configuration Variables (Production) + +For production, it's recommended to use Hardhat's configuration variables +system: + +```sh +# Set your configuration variable (securely stored) +npx hardhat vars set PRIVATE_KEY + +``` + +Then update `hardhat.config.ts` to use configuration variables: + +```typescript +import { vars } from "hardhat/config"; + +const privateKey = vars.get("PRIVATE_KEY", ""); +``` + ## Registering a Ciphernode -To add a ciphernode to the registry, run +The tasks use the first signer configured in your Hardhat network configuration. + +To add a ciphernode to the registry: + +```sh +pnpm ciphernode:add --network [network] +``` + +Options: + +- `--license-bond-amount`: Amount of ENCL to bond (default: 1000 ENCL) +- `--ticket-amount`: Amount of USDC for tickets (default: 1000 USDC) + +For testing/development, you can also use the admin task to register any +ciphernode address: ```sh -pnpm ciphernode:add --network [network] --ciphernode-address [address] +pnpm ciphernode:admin-add --network localhost --ciphernode-address [address] ``` To request a new committee, run diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json new file mode 100644 index 0000000000..da0ed3208f --- /dev/null +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -0,0 +1,855 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "IBondingRegistry", + "sourceName": "contracts/interfaces/IBondingRegistry.sol", + "abi": [ + { + "inputs": [], + "name": "AlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "CiphernodeBanned", + "type": "error" + }, + { + "inputs": [], + "name": "ExitInProgress", + "type": "error" + }, + { + "inputs": [], + "name": "ExitNotReady", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidConfiguration", + "type": "error" + }, + { + "inputs": [], + "name": "NoPendingDeregistration", + "type": "error" + }, + { + "inputs": [], + "name": "NotLicensed", + "type": "error" + }, + { + "inputs": [], + "name": "NotRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyRewardDistributor", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "unlockAt", + "type": "uint64" + } + ], + "name": "CiphernodeDeregistrationRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "parameter", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldValue", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newValue", + "type": "uint256" + } + ], + "name": "ConfigurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "delta", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBond", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "LicenseBondUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "name": "OperatorActivationChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + } + ], + "name": "SlashedFundsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "delta", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "TicketBalanceUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "addTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "availableTickets", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "bondLicense", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxTicketAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLicenseAmount", + "type": "uint256" + } + ], + "name": "claimExits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "siblingNodes", + "type": "uint256[]" + } + ], + "name": "deregisterOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "rewardToken", + "type": "address" + }, + { + "internalType": "address[]", + "name": "operators", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "distributeRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "exitDelay", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "getLicenseBond", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "getTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getTicketBalanceAtBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "hasExitInProgress", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isLicensed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isRegistered", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "licenseRequiredBond", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "pendingExits", + "outputs": [ + { + "internalType": "uint256", + "name": "ticket", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "license", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "previewClaimable", + "outputs": [ + { + "internalType": "uint256", + "name": "ticket", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "license", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registerOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "removeTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "newExitDelay", + "type": "uint64" + } + ], + "name": "setExitDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newBps", + "type": "uint256" + } + ], + "name": "setLicenseActiveBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newLicenseRequiredBond", + "type": "uint256" + } + ], + "name": "setLicenseRequiredBond", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "newLicenseToken", + "type": "address" + } + ], + "name": "setLicenseToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newMinTicketBalance", + "type": "uint256" + } + ], + "name": "setMinTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ICiphernodeRegistry", + "name": "newRegistry", + "type": "address" + } + ], + "name": "setRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newRewardDistributor", + "type": "address" + } + ], + "name": "setRewardDistributor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newSlashedFundsTreasury", + "type": "address" + } + ], + "name": "setSlashedFundsTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newSlashingManager", + "type": "address" + } + ], + "name": "setSlashingManager", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newTicketPrice", + "type": "uint256" + } + ], + "name": "setTicketPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract EnclaveTicketToken", + "name": "newTicketToken", + "type": "address" + } + ], + "name": "setTicketToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "slashLicenseBond", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "slashTicketBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "slashedFundsTreasury", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "slashedLicenseBond", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "slashedTicketBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ticketPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "unbondLicense", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + } + ], + "name": "withdrawSlashedFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", + "buildInfoId": "solc-0_8_28-a2f64967aeae699bd499cc90bbcf76e2314a0651" +} \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 56656bba77..b4749a16db 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -74,6 +74,50 @@ "name": "e3Id", "type": "uint256" }, + { + "indexed": false, + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "name": "CommitteeActivationChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "committee", + "type": "address[]" + } + ], + "name": "CommitteeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + }, { "indexed": false, "internalType": "bytes", @@ -95,15 +139,27 @@ }, { "indexed": false, - "internalType": "address", - "name": "filter", - "type": "address" + "internalType": "uint256", + "name": "seed", + "type": "uint256" }, { "indexed": false, "internalType": "uint32[2]", "name": "threshold", "type": "uint32[2]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "requestBlock", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "submissionDeadline", + "type": "uint256" } ], "name": "CommitteeRequested", @@ -122,6 +178,63 @@ "name": "EnclaveSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "sortitionSubmissionWindow", + "type": "uint256" + } + ], + "name": "SortitionSubmissionWindowSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "score", + "type": "uint256" + } + ], + "name": "TicketSubmitted", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "addCiphernode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -141,6 +254,51 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "finalizeCommittee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getBondingRegistry", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getCommitteeNodes", + "outputs": [ + { + "internalType": "address[]", + "name": "committeeNodes", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -160,6 +318,44 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "isOpen", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -168,9 +364,9 @@ "type": "uint256" }, { - "internalType": "bytes", - "name": "proof", - "type": "bytes" + "internalType": "address[]", + "name": "nodes", + "type": "address[]" }, { "internalType": "bytes", @@ -183,6 +379,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "siblingNodes", + "type": "uint256[]" + } + ], + "name": "removeCiphernode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -191,9 +405,9 @@ "type": "uint256" }, { - "internalType": "address", - "name": "filter", - "type": "address" + "internalType": "uint256", + "name": "seed", + "type": "uint256" }, { "internalType": "uint32[2]", @@ -211,6 +425,108 @@ ], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [], + "name": "root", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "rootAt", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_bondingRegistry", + "type": "address" + } + ], + "name": "setBondingRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_enclave", + "type": "address" + } + ], + "name": "setEnclave", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_sortitionSubmissionWindow", + "type": "uint256" + } + ], + "name": "setSortitionSubmissionWindow", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "ticketNumber", + "type": "uint256" + } + ], + "name": "submitTicket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "treeSize", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" } ], "bytecode": "0x", @@ -219,5 +535,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-5c8e4a4cdd9ec90fb8a4640bf873c02f6e1ad388" -} + "buildInfoId": "solc-0_8_28-a2f64967aeae699bd499cc90bbcf76e2314a0651" +} \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index c1583db71b..b7d1d76a68 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -16,6 +16,19 @@ "name": "AllowedE3ProgramsParamsSet", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "bondingRegistry", + "type": "address" + } + ], + "name": "BondingRegistrySet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -191,12 +204,6 @@ "name": "e3", "type": "tuple" }, - { - "indexed": false, - "internalType": "address", - "name": "filter", - "type": "address" - }, { "indexed": true, "internalType": "contract IE3Program", @@ -233,6 +240,19 @@ "name": "EncryptionSchemeEnabled", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "feeToken", + "type": "address" + } + ], + "name": "FeeTokenSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -296,6 +316,31 @@ "name": "PlaintextOutputPublished", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + } + ], + "name": "RewardsDistributed", + "type": "event" + }, { "inputs": [ { @@ -339,6 +384,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "encryptionSchemeId", + "type": "bytes32" + } + ], + "name": "disableEncryptionScheme", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -358,6 +422,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "encryptionSchemeId", + "type": "bytes32" + } + ], + "name": "getDecryptionVerifier", + "outputs": [ + { + "internalType": "contract IDecryptionVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -454,6 +537,62 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint32[2]", + "name": "threshold", + "type": "uint32[2]" + }, + { + "internalType": "uint256[2]", + "name": "startWindow", + "type": "uint256[2]" + }, + { + "internalType": "uint256", + "name": "duration", + "type": "uint256" + }, + { + "internalType": "contract IE3Program", + "name": "e3Program", + "type": "address" + }, + { + "internalType": "bytes", + "name": "e3ProgramParams", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "computeProviderParams", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "customParams", + "type": "bytes" + } + ], + "internalType": "struct IEnclave.E3RequestParams", + "name": "e3Params", + "type": "tuple" + } + ], + "name": "getE3Quote", + "outputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -578,11 +717,6 @@ "inputs": [ { "components": [ - { - "internalType": "address", - "name": "filter", - "type": "address" - }, { "internalType": "uint32[2]", "name": "threshold", @@ -714,7 +848,107 @@ "type": "tuple" } ], - "stateMutability": "payable", + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IBondingRegistry", + "name": "_bondingRegistry", + "type": "address" + } + ], + "name": "setBondingRegistry", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ICiphernodeRegistry", + "name": "_ciphernodeRegistry", + "type": "address" + } + ], + "name": "setCiphernodeRegistry", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "encryptionSchemeId", + "type": "bytes32" + }, + { + "internalType": "contract IDecryptionVerifier", + "name": "decryptionVerifier", + "type": "address" + } + ], + "name": "setDecryptionVerifier", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "_e3ProgramsParams", + "type": "bytes[]" + } + ], + "name": "setE3ProgramsParams", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "_feeToken", + "type": "address" + } + ], + "name": "setFeeToken", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", "type": "function" }, { @@ -743,5 +977,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-7cfe8fb65ebb805121d5f67e8fe6de2aae01d9ec" + "buildInfoId": "solc-0_8_28-c5db5579375d04ced938f4f4a02b4414441687bc" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json b/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json deleted file mode 100644 index 6943540a75..0000000000 --- a/packages/enclave-contracts/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json +++ /dev/null @@ -1,309 +0,0 @@ -{ - "_format": "hh3-artifact-1", - "contractName": "NaiveRegistryFilter", - "sourceName": "contracts/registry/NaiveRegistryFilter.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "address", - "name": "_registry", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "ciphernode", - "type": "address" - } - ], - "name": "CiphernodeNotEnabled", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeAlreadyExists", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeAlreadyPublished", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeDoesNotExist", - "type": "error" - }, - { - "inputs": [], - "name": "CommitteeNotPublished", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidInitialization", - "type": "error" - }, - { - "inputs": [], - "name": "NotInitializing", - "type": "error" - }, - { - "inputs": [], - "name": "OnlyRegistry", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - } - ], - "name": "OwnableInvalidOwner", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "OwnableUnauthorizedAccount", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint64", - "name": "version", - "type": "uint64" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "previousOwner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3", - "type": "uint256" - } - ], - "name": "committees", - "outputs": [ - { - "internalType": "bytes32", - "name": "publicKey", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - } - ], - "name": "getCommittee", - "outputs": [ - { - "components": [ - { - "internalType": "address[]", - "name": "nodes", - "type": "address[]" - }, - { - "internalType": "uint32[2]", - "name": "threshold", - "type": "uint32[2]" - }, - { - "internalType": "bytes32", - "name": "publicKey", - "type": "bytes32" - } - ], - "internalType": "struct NaiveRegistryFilter.Committee", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_owner", - "type": "address" - }, - { - "internalType": "address", - "name": "_registry", - "type": "address" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "address[]", - "name": "nodes", - "type": "address[]" - }, - { - "internalType": "bytes", - "name": "publicKey", - "type": "bytes" - } - ], - "name": "publishCommittee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "registry", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "renounceOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "e3Id", - "type": "uint256" - }, - { - "internalType": "uint32[2]", - "name": "threshold", - "type": "uint32[2]" - } - ], - "name": "requestCommittee", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_registry", - "type": "address" - } - ], - "name": "setRegistry", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newOwner", - "type": "address" - } - ], - "name": "transferOwnership", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x608060405234801561000f575f5ffd5b50604051610f68380380610f6883398101604081905261002e916102f6565b610038828261003f565b5050610327565b5f516020610f485f395f51905f52805468010000000000000000810460ff1615906001600160401b03165f811580156100755750825b90505f826001600160401b031660011480156100905750303b155b90508115801561009e575080155b156100bc5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156100ea57845460ff60401b1916680100000000000000001785555b6100f333610175565b6100fc86610189565b5f516020610f285f395f51905f52546001600160a01b0388811691161461012657610126876101b2565b831561016c57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b61017d6101f1565b6101868161022e565b50565b610191610236565b5f80546001600160a01b0319166001600160a01b0392909216919091179055565b6101ba610236565b6001600160a01b0381166101e857604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101868161027e565b5f516020610f485f395f51905f525468010000000000000000900460ff1661022c57604051631afcd79f60e31b815260040160405180910390fd5b565b6101ba6101f1565b336102555f516020610f285f395f51905f52546001600160a01b031690565b6001600160a01b03161461022c5760405163118cdaa760e01b81523360048201526024016101df565b5f516020610f285f395f51905f5280546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80516001600160a01b03811681146102f1575f5ffd5b919050565b5f5f60408385031215610307575f5ffd5b610310836102db565b915061031e602084016102db565b90509250929050565b610bf4806103345f395ff3fe608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca3660046108e3565b6101e8565b6040516100dc91906108fa565b60405180910390f35b6100f86100f33660046109dc565b6102d3565b005b61010d610108366004610a87565b6103be565b60405190151581526020016100dc565b6100f861012b366004610ad2565b61044e565b6100f86105b7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b03565b6105ca565b6100f86101b3366004610b03565b610600565b6101da6101c63660046108e3565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f0610781565b5f828152600160209081526040918290208251815460809381028201840190945260608101848152909391928492849184018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d579050505050505081526020016002820154815250509050919050565b6102db610642565b5f85815260016020526040902060028101541561030b5760405163632a22bb60e01b815260040160405180910390fd5b6103168186866107a7565b508282604051610327929190610b23565b60405190819003812060028301555f546001600160a01b03169063d9bbec959088906103599089908990602001610b32565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103899493929190610b7c565b5f604051808303815f87803b1580156103a0575f5ffd5b505af11580156103b2573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b031633146103e9576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610427576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104449101836002610815565b5060019392505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff165f811580156104985750825b90505f8267ffffffffffffffff1660011480156104b45750303b155b9050811580156104c2575080155b156104e05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051457845468ff00000000000000001916680100000000000000001785555b61051d3361069d565b610526866105ca565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105635761056387610600565b83156105ae57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105bf610642565b6105c85f6106ae565b565b6105d2610642565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610608610642565b6001600160a01b03811661063657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61063f816106ae565b50565b336106747f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105c85760405163118cdaa760e01b815233600482015260240161062d565b6106a561072b565b61063f81610779565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005468010000000000000000900460ff166105c857604051631afcd79f60e31b815260040160405180910390fd5b61060861072b565b60405180606001604052806060815260200161079b6108b1565b81526020015f81525090565b828054828255905f5260205f20908101928215610805579160200282015b8281111561080557815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107c5565b506108119291506108cf565b5090565b600183019183908215610805579160200282015f5b8382111561087457833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261082a565b80156108a45782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610874565b50506108119291506108cf565b60405180604001604052806002906020820280368337509192915050565b5b80821115610811575f81556001016108d0565b5f602082840312156108f3575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b8083101561094b576001600160a01b03845116825260208201915060208401935060018301925061091f565b50602086015192506040850191505f5b600281101561098057835163ffffffff1683526020938401939092019160010161095b565b506040860151608086015280935050505092915050565b5f5f83601f8401126109a7575f5ffd5b50813567ffffffffffffffff8111156109be575f5ffd5b6020830191508360208285010111156109d5575f5ffd5b9250929050565b5f5f5f5f5f606086880312156109f0575f5ffd5b85359450602086013567ffffffffffffffff811115610a0d575f5ffd5b8601601f81018813610a1d575f5ffd5b803567ffffffffffffffff811115610a33575f5ffd5b8860208260051b8401011115610a47575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a6a575f5ffd5b610a7688828901610997565b969995985093965092949392505050565b5f5f60608385031215610a98575f5ffd5b8235915060608301841015610aab575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610acd575f5ffd5b919050565b5f5f60408385031215610ae3575f5ffd5b610aec83610ab7565b9150610afa60208401610ab7565b90509250929050565b5f60208284031215610b13575f5ffd5b610b1c82610ab7565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b72576001600160a01b03610b5d84610ab7565b16825260209283019290910190600101610b44565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea164736f6c634300081c000a9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106100b8575f3560e01c80637b10399911610072578063a91ee0dc11610058578063a91ee0dc14610192578063f2fde38b146101a5578063f5e820fd146101b8575f5ffd5b80637b103999146101385780638da5cb5b14610162575f5ffd5b80632b20a4f6116100a25780632b20a4f6146100fa578063485cc9551461011d578063715018a614610130575f5ffd5b806218449a146100bc57806329f73b9c146100e5575b5f5ffd5b6100cf6100ca3660046108e3565b6101e8565b6040516100dc91906108fa565b60405180910390f35b6100f86100f33660046109dc565b6102d3565b005b61010d610108366004610a87565b6103be565b60405190151581526020016100dc565b6100f861012b366004610ad2565b61044e565b6100f86105b7565b5f5461014a906001600160a01b031681565b6040516001600160a01b0390911681526020016100dc565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031661014a565b6100f86101a0366004610b03565b6105ca565b6100f86101b3366004610b03565b610600565b6101da6101c63660046108e3565b60016020525f908152604090206002015481565b6040519081526020016100dc565b6101f0610781565b5f828152600160209081526040918290208251815460809381028201840190945260608101848152909391928492849184018282801561025757602002820191905f5260205f20905b81546001600160a01b03168152600190910190602001808311610239575b50505091835250506040805180820191829052602090920191906001840190600290825f855b82829054906101000a900463ffffffff1663ffffffff168152602001906004019060208260030104928301926001038202915080841161027d579050505050505081526020016002820154815250509050919050565b6102db610642565b5f85815260016020526040902060028101541561030b5760405163632a22bb60e01b815260040160405180910390fd5b6103168186866107a7565b508282604051610327929190610b23565b60405190819003812060028301555f546001600160a01b03169063d9bbec959088906103599089908990602001610b32565b60405160208183030381529060405286866040518563ffffffff1660e01b81526004016103899493929190610b7c565b5f604051808303815f87803b1580156103a0575f5ffd5b505af11580156103b2573d5f5f3e3d5ffd5b50505050505050505050565b5f80546001600160a01b031633146103e9576040516310f5403960e31b815260040160405180910390fd5b5f8381526001602081905260409091200154640100000000900463ffffffff1615610427576040516334c2a65d60e11b815260040160405180910390fd5b5f8381526001602081905260409091206104449101836002610815565b5060019392505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff165f811580156104985750825b90505f8267ffffffffffffffff1660011480156104b45750303b155b9050811580156104c2575080155b156104e05760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561051457845468ff00000000000000001916680100000000000000001785555b61051d3361069d565b610526866105ca565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b038881169116146105635761056387610600565b83156105ae57845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6105bf610642565b6105c85f6106ae565b565b6105d2610642565b5f805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b610608610642565b6001600160a01b03811661063657604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b61063f816106ae565b50565b336106747f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146105c85760405163118cdaa760e01b815233600482015260240161062d565b6106a561072b565b61063f81610779565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005468010000000000000000900460ff166105c857604051631afcd79f60e31b815260040160405180910390fd5b61060861072b565b60405180606001604052806060815260200161079b6108b1565b81526020015f81525090565b828054828255905f5260205f20908101928215610805579160200282015b8281111561080557815473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b038435161782556020909201916001909101906107c5565b506108119291506108cf565b5090565b600183019183908215610805579160200282015f5b8382111561087457833563ffffffff1683826101000a81548163ffffffff021916908363ffffffff160217905550926020019260040160208160030104928301926001030261082a565b80156108a45782816101000a81549063ffffffff0219169055600401602081600301049283019260010302610874565b50506108119291506108cf565b60405180604001604052806002906020820280368337509192915050565b5b80821115610811575f81556001016108d0565b5f602082840312156108f3575f5ffd5b5035919050565b60208082528251608083830152805160a084018190525f929190910190829060c08501905b8083101561094b576001600160a01b03845116825260208201915060208401935060018301925061091f565b50602086015192506040850191505f5b600281101561098057835163ffffffff1683526020938401939092019160010161095b565b506040860151608086015280935050505092915050565b5f5f83601f8401126109a7575f5ffd5b50813567ffffffffffffffff8111156109be575f5ffd5b6020830191508360208285010111156109d5575f5ffd5b9250929050565b5f5f5f5f5f606086880312156109f0575f5ffd5b85359450602086013567ffffffffffffffff811115610a0d575f5ffd5b8601601f81018813610a1d575f5ffd5b803567ffffffffffffffff811115610a33575f5ffd5b8860208260051b8401011115610a47575f5ffd5b60209190910194509250604086013567ffffffffffffffff811115610a6a575f5ffd5b610a7688828901610997565b969995985093965092949392505050565b5f5f60608385031215610a98575f5ffd5b8235915060608301841015610aab575f5ffd5b50926020919091019150565b80356001600160a01b0381168114610acd575f5ffd5b919050565b5f5f60408385031215610ae3575f5ffd5b610aec83610ab7565b9150610afa60208401610ab7565b90509250929050565b5f60208284031215610b13575f5ffd5b610b1c82610ab7565b9392505050565b818382375f9101908152919050565b602080825281018290525f8360408301825b85811015610b72576001600160a01b03610b5d84610ab7565b16825260209283019290910190600101610b44565b5095945050505050565b848152606060208201525f84518060608401528060208701608085015e5f60808285010152601f19601f820116830190506080838203016040840152836080820152838560a08301375f60a0828601810191909152601f909401601f1916019092019594505050505056fea164736f6c634300081c000a", - "linkReferences": {}, - "deployedLinkReferences": {}, - "immutableReferences": {}, - "inputSourceName": "project/contracts/registry/NaiveRegistryFilter.sol", - "buildInfoId": "solc-0_8_28-5c8e4a4cdd9ec90fb8a4640bf873c02f6e1ad388" -} diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 55c18dd02c..7753b6190d 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -8,18 +8,29 @@ pragma solidity >=0.8.27; import { IEnclave, E3, IE3Program } from "./interfaces/IEnclave.sol"; import { IInputValidator } from "./interfaces/IInputValidator.sol"; import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; +import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; import { IDecryptionVerifier } from "./interfaces/IDecryptionVerifier.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { InternalLeanIMT, LeanIMTData, PoseidonT3 } from "@zk-kit/lean-imt.sol/InternalLeanIMT.sol"; +/** + * @title Enclave + * @notice Main contract for managing Encrypted Execution Environments (E3) + * @dev Coordinates E3 lifecycle including request, activation, input publishing, and output verification + */ contract Enclave is IEnclave, OwnableUpgradeable { using InternalLeanIMT for LeanIMTData; + using SafeERC20 for IERC20; //////////////////////////////////////////////////////////// // // @@ -27,95 +38,211 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// - ICiphernodeRegistry public ciphernodeRegistry; // address of the Ciphernode registry. - uint256 public maxDuration; // maximum duration of a computation in seconds. - uint256 public nexte3Id; // ID of the next E3. + /// @notice Address of the Ciphernode Registry contract. + /// @dev Manages the pool of ciphernodes and committee selection. + ICiphernodeRegistry public ciphernodeRegistry; + + /// @notice Address of the Bonding Registry contract. + /// @dev Handles staking and reward distribution for ciphernodes. + IBondingRegistry public bondingRegistry; + + /// @notice Address of the ERC20 token used for E3 fees. + /// @dev All E3 request fees must be paid in this token. + IERC20 public feeToken; + + /// @notice Maximum allowed duration for an E3 computation in seconds. + /// @dev Requests with duration exceeding this value will be rejected. + uint256 public maxDuration; + + /// @notice ID counter for the next E3 to be created. + /// @dev Incremented after each successful E3 request. + uint256 public nexte3Id; - // Mapping of allowed E3 Programs. + /// @notice Mapping of allowed E3 Programs. + /// @dev Only enabled E3 Programs can be used in computation requests. mapping(IE3Program e3Program => bool allowed) public e3Programs; - // Mapping of E3s. + /// @notice Mapping storing all E3 instances by their ID. + /// @dev Contains the full state and configuration of each E3. mapping(uint256 e3Id => E3 e3) public e3s; - // Mapping of input merkle trees. + /// @notice Mapping of input merkle trees for each E3. + /// @dev Uses Lean IMT for efficient incremental merkle tree operations. mapping(uint256 e3Id => LeanIMTData imt) public inputs; - // Mapping counting the number of inputs for each E3. + /// @notice Counter tracking the number of inputs published for each E3. + /// @dev Used as the index when inserting new inputs into the merkle tree. mapping(uint256 e3Id => uint256 inputCount) public inputCounts; - // Mapping of enabled encryption schemes. + /// @notice Mapping of enabled encryption schemes to their decryption verifiers. + /// @dev Each encryption scheme ID maps to a contract that can verify decrypted outputs. mapping(bytes32 encryptionSchemeId => IDecryptionVerifier decryptionVerifier) public decryptionVerifiers; - /// Mapping that stores the valid E3 program ABI encoded parameter sets (e.g., BFV). + /// @notice Mapping storing valid E3 program ABI encoded parameter sets. + /// @dev Stores allowed encryption scheme parameters (e.g., BFV parameters). mapping(bytes e3ProgramParams => bool allowed) public e3ProgramsParams; + /// @notice Mapping tracking fee payments for each E3. + /// @dev Stores the amount paid for an E3, distributed to committee upon completion. + mapping(uint256 e3Id => uint256 e3Payment) public e3Payments; + //////////////////////////////////////////////////////////// // // // Errors // // // //////////////////////////////////////////////////////////// + /// @notice Thrown when committee selection fails during E3 request or activation. error CommitteeSelectionFailed(); + + /// @notice Thrown when an E3 request uses a program that is not enabled. + /// @param e3Program The E3 program address that is not allowed. error E3ProgramNotAllowed(IE3Program e3Program); + + /// @notice Thrown when attempting to activate an E3 that is already activated. + /// @param e3Id The ID of the E3 that is already activated. error E3AlreadyActivated(uint256 e3Id); + + /// @notice Thrown when the E3 start window or computation period has expired. error E3Expired(); + + /// @notice Thrown when attempting operations on an E3 that has not been activated yet. + /// @param e3Id The ID of the E3 that is not activated. error E3NotActivated(uint256 e3Id); + + /// @notice Thrown when attempting to activate an E3 before its start window begins. error E3NotReady(); + + /// @notice Thrown when attempting to access an E3 that does not exist. + /// @param e3Id The ID of the non-existent E3. error E3DoesNotExist(uint256 e3Id); + + /// @notice Thrown when attempting to enable a module or program that is already enabled. + /// @param module The address of the module that is already enabled. error ModuleAlreadyEnabled(address module); + + /// @notice Thrown when attempting to disable a module or program that is not enabled. + /// @param module The address of the module that is not enabled. error ModuleNotEnabled(address module); + + /// @notice Thrown when an invalid or disabled encryption scheme is used. + /// @param encryptionSchemeId The ID of the invalid encryption scheme. error InvalidEncryptionScheme(bytes32 encryptionSchemeId); + + /// @notice Thrown when attempting to publish input after the computation deadline has passed. + /// @param e3Id The ID of the E3. + /// @param expiration The expiration timestamp that has passed. error InputDeadlinePassed(uint256 e3Id, uint256 expiration); + + /// @notice Thrown when attempting to publish output before the input deadline has passed. + /// @param e3Id The ID of the E3. + /// @param expiration The expiration timestamp that has not yet passed. error InputDeadlineNotPassed(uint256 e3Id, uint256 expiration); + + /// @notice Thrown when the input validator in the computation request is invalid. + /// @param inputValidator The address of the invalid input validator. error InvalidComputationRequest(IInputValidator inputValidator); + + /// @notice Thrown when attempting to set an invalid ciphernode registry address. + /// @param ciphernodeRegistry The invalid ciphernode registry address. error InvalidCiphernodeRegistry(ICiphernodeRegistry ciphernodeRegistry); + + /// @notice Thrown when the requested duration exceeds maxDuration or is zero. + /// @param duration The invalid duration value. error InvalidDuration(uint256 duration); + + /// @notice Thrown when output verification fails. + /// @param output The invalid output data. error InvalidOutput(bytes output); + + /// @notice Thrown when input data is invalid. error InvalidInput(); + + /// @notice Thrown when the start window parameters are invalid. error InvalidStartWindow(); + + /// @notice Thrown when the threshold parameters are invalid (e.g., M > N or M = 0). + /// @param threshold The invalid threshold array [M, N]. error InvalidThreshold(uint32[2] threshold); + + /// @notice Thrown when attempting to publish ciphertext output that has already been published. + /// @param e3Id The ID of the E3. error CiphertextOutputAlreadyPublished(uint256 e3Id); + + /// @notice Thrown when attempting to publish plaintext output before ciphertext output. + /// @param e3Id The ID of the E3. error CiphertextOutputNotPublished(uint256 e3Id); + + /// @notice Thrown when payment is required but not provided or insufficient. + /// @param value The required payment amount. error PaymentRequired(uint256 value); + + /// @notice Thrown when attempting to publish plaintext output that has already been published. + /// @param e3Id The ID of the E3. error PlaintextOutputAlreadyPublished(uint256 e3Id); + /// @notice Thrown when attempting to set an invalid bonding registry address. + /// @param bondingRegistry The invalid bonding registry address. + error InvalidBondingRegistry(IBondingRegistry bondingRegistry); + + /// @notice Thrown when attempting to set an invalid fee token address. + /// @param feeToken The invalid fee token address. + error InvalidFeeToken(IERC20 feeToken); + //////////////////////////////////////////////////////////// // // // Initialization // // // //////////////////////////////////////////////////////////// - /// @param _owner The owner of this contract - /// @param _maxDuration The maximum duration of a computation in seconds - /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV) + /// @notice Constructs the Enclave contract. + /// @dev Calls initialize() to set up the contract state. Can be used for non-proxy deployments. + /// @param _owner The owner address of this contract. + /// @param _ciphernodeRegistry The address of the Ciphernode Registry contract. + /// @param _bondingRegistry The address of the Bonding Registry contract. + /// @param _feeToken The address of the ERC20 token used for E3 fees. + /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). constructor( address _owner, ICiphernodeRegistry _ciphernodeRegistry, + IBondingRegistry _bondingRegistry, + IERC20 _feeToken, uint256 _maxDuration, bytes[] memory _e3ProgramsParams ) { initialize( _owner, _ciphernodeRegistry, + _bondingRegistry, + _feeToken, _maxDuration, _e3ProgramsParams ); } - /// @param _owner The owner of this contract - /// @param _ciphernodeRegistry The address of the ciphernode registry - /// @param _maxDuration The maximum duration of a computation in seconds - /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV) + /// @notice Initializes the Enclave contract with initial configuration. + /// @dev This function can only be called once due to the initializer modifier. Sets up core dependencies. + /// @param _owner The owner address of this contract. + /// @param _ciphernodeRegistry The address of the Ciphernode Registry contract. + /// @param _bondingRegistry The address of the Bonding Registry contract. + /// @param _feeToken The address of the ERC20 token used for E3 fees. + /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). function initialize( address _owner, ICiphernodeRegistry _ciphernodeRegistry, + IBondingRegistry _bondingRegistry, + IERC20 _feeToken, uint256 _maxDuration, bytes[] memory _e3ProgramsParams ) public initializer { __Ownable_init(msg.sender); setMaxDuration(_maxDuration); setCiphernodeRegistry(_ciphernodeRegistry); + setBondingRegistry(_bondingRegistry); + setFeeToken(_feeToken); setE3ProgramsParams(_e3ProgramsParams); if (_owner != owner()) transferOwnership(_owner); } @@ -129,10 +256,8 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @inheritdoc IEnclave function request( E3RequestParams calldata requestParams - ) external payable returns (uint256 e3Id, E3 memory e3) { - // TODO: allow for other payment methods or only native tokens? - // TODO: should payment checks be somewhere else? Perhaps in the E3 Program or ciphernode registry? - require(msg.value > 0, PaymentRequired(msg.value)); + ) external returns (uint256 e3Id, E3 memory e3) { + uint256 e3Fee = getE3Quote(requestParams); require( requestParams.threshold[1] >= requestParams.threshold[0] && requestParams.threshold[0] > 0, @@ -199,22 +324,20 @@ contract Enclave is IEnclave, OwnableUpgradeable { e3.decryptionVerifier = decryptionVerifier; e3s[e3Id] = e3; + e3Payments[e3Id] = e3Fee; + + feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); require( ciphernodeRegistry.requestCommittee( e3Id, - requestParams.filter, + seed, requestParams.threshold ), CommitteeSelectionFailed() ); - emit E3Requested( - e3Id, - e3, - requestParams.filter, - requestParams.e3Program - ); + emit E3Requested(e3Id, e3, requestParams.e3Program); } /// @inheritdoc IEnclave @@ -330,15 +453,55 @@ contract Enclave is IEnclave, OwnableUpgradeable { ); require(success, InvalidOutput(plaintextOutput)); + _distributeRewards(e3Id); + emit PlaintextOutputPublished(e3Id, plaintextOutput); } + //////////////////////////////////////////////////////////// + // // + // Internal Functions // + // // + //////////////////////////////////////////////////////////// + + /// @notice Distributes rewards to committee members after successful E3 completion. + /// @dev Divides the E3 payment equally among all committee members and transfers via bonding registry. + /// @dev Emits RewardsDistributed event upon successful distribution. + /// @param e3Id The ID of the E3 for which to distribute rewards. + function _distributeRewards(uint256 e3Id) internal { + address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( + e3Id + ); + uint256 committeeLength = committeeNodes.length; + uint256[] memory amounts = new uint256[](committeeLength); + + // TODO: do we need to pay different amounts to different nodes? + // For now, we'll pay the same amount to all nodes. + uint256 amount = e3Payments[e3Id] / committeeLength; + for (uint256 i = 0; i < committeeLength; i++) { + amounts[i] = amount; + } + + uint256 totalAmount = e3Payments[e3Id]; + e3Payments[e3Id] = 0; + + feeToken.approve(address(bondingRegistry), totalAmount); + + bondingRegistry.distributeRewards(feeToken, committeeNodes, amounts); + + // TODO: decide where does dust go? Treasury maybe? + feeToken.approve(address(bondingRegistry), 0); + + emit RewardsDistributed(e3Id, committeeNodes, amounts); + } + //////////////////////////////////////////////////////////// // // // Set Functions // // // //////////////////////////////////////////////////////////// + /// @inheritdoc IEnclave function setMaxDuration( uint256 _maxDuration ) public onlyOwner returns (bool success) { @@ -347,6 +510,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit MaxDurationSet(_maxDuration); } + /// @inheritdoc IEnclave function setCiphernodeRegistry( ICiphernodeRegistry _ciphernodeRegistry ) public onlyOwner returns (bool success) { @@ -360,6 +524,34 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit CiphernodeRegistrySet(address(_ciphernodeRegistry)); } + /// @inheritdoc IEnclave + function setBondingRegistry( + IBondingRegistry _bondingRegistry + ) public onlyOwner returns (bool success) { + require( + address(_bondingRegistry) != address(0) && + _bondingRegistry != bondingRegistry, + InvalidBondingRegistry(_bondingRegistry) + ); + bondingRegistry = _bondingRegistry; + success = true; + emit BondingRegistrySet(address(_bondingRegistry)); + } + + /// @inheritdoc IEnclave + function setFeeToken( + IERC20 _feeToken + ) public onlyOwner returns (bool success) { + require( + address(_feeToken) != address(0) && _feeToken != feeToken, + InvalidFeeToken(_feeToken) + ); + feeToken = _feeToken; + success = true; + emit FeeTokenSet(address(_feeToken)); + } + + /// @inheritdoc IEnclave function enableE3Program( IE3Program e3Program ) public onlyOwner returns (bool success) { @@ -372,6 +564,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3ProgramEnabled(e3Program); } + /// @inheritdoc IEnclave function disableE3Program( IE3Program e3Program ) public onlyOwner returns (bool success) { @@ -381,6 +574,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3ProgramDisabled(e3Program); } + /// @inheritdoc IEnclave function setDecryptionVerifier( bytes32 encryptionSchemeId, IDecryptionVerifier decryptionVerifier @@ -395,6 +589,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit EncryptionSchemeEnabled(encryptionSchemeId); } + /// @inheritdoc IEnclave function disableEncryptionScheme( bytes32 encryptionSchemeId ) public onlyOwner returns (bool success) { @@ -410,6 +605,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit EncryptionSchemeDisabled(encryptionSchemeId); } + /// @inheritdoc IEnclave function setE3ProgramsParams( bytes[] memory _e3ProgramsParams ) public onlyOwner returns (bool success) { @@ -445,6 +641,15 @@ contract Enclave is IEnclave, OwnableUpgradeable { return InternalLeanIMT._root(inputs[e3Id]); } + /// @inheritdoc IEnclave + function getE3Quote( + E3RequestParams calldata + ) public pure returns (uint256 fee) { + fee = 1 * 10 ** 6; + require(fee > 0, PaymentRequired(fee)); + } + + /// @inheritdoc IEnclave function getDecryptionVerifier( bytes32 encryptionSchemeId ) public view returns (IDecryptionVerifier) { diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol new file mode 100644 index 0000000000..5c805ebc24 --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -0,0 +1,434 @@ +// 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. + +pragma solidity >=0.8.27; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { ICiphernodeRegistry } from "./ICiphernodeRegistry.sol"; +import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; + +/** + * @title IBondingRegistry + * @notice Interface for the main bonding registry that holds operator balance and license bonds + */ +interface IBondingRegistry { + // ====================== + // Custom Errors + // ====================== + + // General + error ZeroAddress(); + error ZeroAmount(); + error CiphernodeBanned(); + error Unauthorized(); + error InsufficientBalance(); + error NotLicensed(); + error AlreadyRegistered(); + error NotRegistered(); + error ExitInProgress(); + error ExitNotReady(); + error InvalidAmount(); + error InvalidConfiguration(); + error NoPendingDeregistration(); + error OnlyRewardDistributor(); + error ArrayLengthMismatch(); + + // ====================== + // Events (Protocol-Named) + // ====================== + + /** + * @notice Emitted when operator's ticket balance changes + * @param operator Address of the operator + * @param delta Change in balance (positive for increase, negative for decrease) + * @param newBalance New total balance + * @param reason Reason for the change (e.g., "DEPOSIT", "WITHDRAW", slash reason) + */ + event TicketBalanceUpdated( + address indexed operator, + int256 delta, + uint256 newBalance, + bytes32 indexed reason + ); + + /** + * @notice Emitted when operator's license bond changes + * @param operator Address of the operator + * @param delta Change in bond (positive for increase, negative for decrease) + * @param newBond New total license bond + * @param reason Reason for the change (e.g., "BOND", "UNBOND", slash reason) + */ + event LicenseBondUpdated( + address indexed operator, + int256 delta, + uint256 newBond, + bytes32 indexed reason + ); + + /** + * @notice Emitted when operator requests deregistration from the protocol + * @param operator Address of the operator + * @param unlockAt Timestamp when deregistration can be finalized + */ + event CiphernodeDeregistrationRequested( + address indexed operator, + uint64 unlockAt + ); + + /** + * @notice Emitted when operator active status changes + * @param operator Address of the operator + * @param active True if active, false if inactive + */ + event OperatorActivationChanged(address indexed operator, bool active); + + /** + * @notice Emitted when configuration is updated + * @param parameter Name of the parameter + * @param oldValue Previous value + * @param newValue New value + */ + event ConfigurationUpdated( + bytes32 indexed parameter, + uint256 oldValue, + uint256 newValue + ); + + /** + * @notice Emitted when treasury withdraws slashed funds + * @param to Treasury address + * @param ticketAmount Amount of slashed ticket balance withdrawn + * @param licenseAmount Amount of slashed license bond withdrawn + */ + event SlashedFundsWithdrawn( + address indexed to, + uint256 ticketAmount, + uint256 licenseAmount + ); + + // ====================== + // View Functions + // ====================== + + /** + * @notice Get operator's current ticket balance + * @param operator Address of the operator + * @return Current collateral balance + */ + function getTicketBalance(address operator) external view returns (uint256); + + /** + * @notice Get operator's current license bond + * @param operator Address of the operator + * @return Current license bond + */ + function getLicenseBond(address operator) external view returns (uint256); + + /** + * @notice Get current ticket price + * @return Price per ticket in collateral token units + */ + function ticketPrice() external view returns (uint256); + + /** + * @notice Calculate available tickets for an operator + * @param operator Address of the operator + * @return Number of tickets available (floor(balance / ticketPrice)) + */ + function availableTickets(address operator) external view returns (uint256); + + /** + * @notice Check if operator is licensed + * @param operator Address of the operator + * @return True if operator has sufficient license bond + */ + function isLicensed(address operator) external view returns (bool); + + /** + * @notice Check if operator is registered + * @param operator Address of the operator + * @return True if operator is registered + */ + function isRegistered(address operator) external view returns (bool); + + /** + * @notice Check if operator is active + * @param operator Address of the operator + * @return True if operator is active (licensed, registered, and has min tickets) + */ + function isActive(address operator) external view returns (bool); + + /** + * @notice Check if operator has deregistration in progress + * @param operator Address of the operator + * @return True if exit requested but not finalized + */ + function hasExitInProgress(address operator) external view returns (bool); + + /** + * @notice Get license bond price required + * @return License bond price amount + */ + function licenseRequiredBond() external view returns (uint256); + + /** + * @notice Get minimum ticket balance required for activation + * @return Minimum number of tickets required + */ + function minTicketBalance() external view returns (uint256); + + /** + * @notice Get exit delay period + * @return Number of seconds operators must wait after requesting exit + */ + function exitDelay() external view returns (uint64); + + /** + * @notice Get operator's ticket balance at a specific block + * @param operator Address of the operator + * @param blockNumber Block number to query + * @return Ticket balance at the specified block + */ + function getTicketBalanceAtBlock( + address operator, + uint256 blockNumber + ) external view returns (uint256); + + /** + * @notice Get operator's total pending exit amounts + * @param operator Address of the operator + * @return ticket Total pending ticket balance in exit queue + * @return license Total pending license bond in exit queue + */ + function pendingExits( + address operator + ) external view returns (uint256 ticket, uint256 license); + + /** + * @notice Preview how much an operator can currently claim + * @param operator Address of the operator + * @return ticket Claimable ticket balance + * @return license Claimable license bond + */ + function previewClaimable( + address operator + ) external view returns (uint256 ticket, uint256 license); + + /** + * @notice Get slashed funds treasury address + * @return Address where slashed funds are sent + */ + function slashedFundsTreasury() external view returns (address); + + /** + * @notice Get total slashed ticket balance + * @return Amount of ticket balance slashed and available for treasury withdrawal + */ + function slashedTicketBalance() external view returns (uint256); + + /** + * @notice Get total slashed license bond + * @return Amount of license bond slashed and available for treasury withdrawal + */ + function slashedLicenseBond() external view returns (uint256); + + // ====================== + // Operator Functions + // ====================== + + /** + * @notice Register as an operator (callable by licensed operators) + * @dev Requires sufficient license bond and calls registry + */ + function registerOperator() external; + + /** + * @notice Deregister as an operator and remove from IMT + * @param siblingNodes Sibling node proofs for IMT removal + * @dev Requires operator to provide sibling nodes for immediate IMT removal + */ + function deregisterOperator(uint256[] calldata siblingNodes) external; + + /** + * @notice Increase operator's ticket balance by depositing tokens + * @param amount Amount of ticket tokens to deposit + * @dev Requires approval for ticket token transfer + */ + function addTicketBalance(uint256 amount) external; + + /** + * @notice Decrease operator's ticket balance by withdrawing tokens + * @param amount Amount of ticket tokens to withdraw + * @dev Reverts if operator is in any active committee + */ + function removeTicketBalance(uint256 amount) external; + + /** + * @notice Bond license tokens to become eligible for registration + * @param amount Amount of license tokens to bond + * @dev Requires approval for license token transfer + */ + function bondLicense(uint256 amount) external; + + /** + * @notice Unbond license tokens + * @param amount Amount of license tokens to unbond + * @dev Reverts if operator is in any active committee or still registered + */ + function unbondLicense(uint256 amount) external; + + // ====================== + // Claim Functions + // ====================== + + /** + * @notice Claim operator's ticket balance and license bond + * @param maxTicketAmount Maximum amount of ticket tokens to claim + * @param maxLicenseAmount Maximum amount of license tokens to claim + */ + function claimExits( + uint256 maxTicketAmount, + uint256 maxLicenseAmount + ) external; + + // ====================== + // Slashing Functions + // ====================== + + /** + * @notice Slash operator's ticket balance by absolute amount + * @param operator Address of the operator to slash + * @param amount Amount to slash + * @param reason Reason for slashing (stored in event) + * @dev Only callable by authorized slashing manager + */ + function slashTicketBalance( + address operator, + uint256 amount, + bytes32 reason + ) external; + + /** + * @notice Slash operator's license bond by absolute amount + * @param operator Address of the operator to slash + * @param amount Amount to slash + * @param reason Reason for slashing (stored in event) + * @dev Only callable by authorized slashing manager + */ + function slashLicenseBond( + address operator, + uint256 amount, + bytes32 reason + ) external; + + // ====================== + // Reward Distribution Functions + // ====================== + /** + * @notice Distribute rewards to operators + * @param rewardToken Reward token contract + * @param operators Addresses of the operators to distribute rewards to + * @param amounts Amounts of rewards to distribute to each operator + * @dev Only callable by contract owner + */ + function distributeRewards( + IERC20 rewardToken, + address[] calldata operators, + uint256[] calldata amounts + ) external; + + // ====================== + // Admin Functions + // ====================== + + /** + * @notice Set ticket price + * @param newTicketPrice New price per ticket + * @dev Only callable by contract owner + */ + function setTicketPrice(uint256 newTicketPrice) external; + + /** + * @notice Set license bond price required + * @param newLicenseRequiredBond New license bond price + * @dev Only callable by contract owner + */ + function setLicenseRequiredBond(uint256 newLicenseRequiredBond) external; + + /** + * @notice Set license active BPS + * @param newBps New license active BPS + * @dev Only callable by contract owner + */ + function setLicenseActiveBps(uint256 newBps) external; + + /** + * @notice Set minimum ticket balance required for activation + * @param newMinTicketBalance New minimum ticket balance + * @dev Only callable by contract owner + */ + function setMinTicketBalance(uint256 newMinTicketBalance) external; + + /** + * @notice Set exit delay period + * @param newExitDelay New exit delay in seconds + * @dev Only callable by contract owner + */ + function setExitDelay(uint64 newExitDelay) external; + + /** + * @notice Set ticket token + * @param newTicketToken New ticket token + * @dev Only callable by contract owner + */ + function setTicketToken(EnclaveTicketToken newTicketToken) external; + + /** + * @notice Set license token + * @param newLicenseToken New license token + * @dev Only callable by contract owner + */ + function setLicenseToken(IERC20 newLicenseToken) external; + + /** + * @notice Set slashed funds treasury address + * @param newSlashedFundsTreasury New slashed funds treasury address + * @dev Only callable by contract owner + */ + function setSlashedFundsTreasury(address newSlashedFundsTreasury) external; + + /** + * @notice Set registry address + * @param newRegistry New registry contract address + * @dev Only callable by contract owner + */ + function setRegistry(ICiphernodeRegistry newRegistry) external; + + /** + * @notice Set slashing manager address + * @param newSlashingManager New slashing manager contract address + * @dev Only callable by contract owner + */ + function setSlashingManager(address newSlashingManager) external; + + /** + * @notice Set reward distributor address + * @param newRewardDistributor New reward distributor address + * @dev Only callable by contract owner + */ + function setRewardDistributor(address newRewardDistributor) external; + + /** + * @notice Withdraw slashed funds to treasury + * @param ticketAmount Amount of slashed ticket balance to withdraw + * @param licenseAmount Amount of slashed license bond to withdraw + * @dev Only callable by contract owner, sends to treasury address + */ + function withdrawSlashedFunds( + uint256 ticketAmount, + uint256 licenseAmount + ) external; +} diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index afa2c2bec2..f294ff4ec3 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -5,21 +5,83 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title ICiphernodeRegistry + * @notice Interface for managing ciphernode registration and committee selection + * @dev This registry maintains an Incremental Merkle Tree (IMT) of registered ciphernodes + * and coordinates committee selection for E3 computations + */ interface ICiphernodeRegistry { + /// @notice Struct representing the sortition state for an E3 round. + /// @param initialized Whether the round has been initialized. + /// @param finalized Whether the round has been finalized. + /// @param requestBlock The block number when the committee was requested. + /// @param submissionDeadline The deadline for submitting tickets. + /// @param threshold The M/N threshold for the committee ([M, N]). + /// @param publicKey Hash of the committee's public key. + /// @param seed The seed for the round. + /// @param topNodes The top nodes in the round. + /// @param committee The committee for the round. + /// @param submitted Mapping of nodes to their submission status. + /// @param scoreOf Mapping of nodes to their scores. + struct Committee { + bool initialized; + bool finalized; + uint256 seed; + uint256 requestBlock; + uint256 submissionDeadline; + bytes32 publicKey; + uint32[2] threshold; + address[] topNodes; + address[] committee; + mapping(address node => bool submitted) submitted; + mapping(address node => uint256 score) scoreOf; + } + /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. - /// @param filter Address of the contract that will coordinate committee selection. + /// @param seed Random seed for score computation. /// @param threshold The M/N threshold for the committee. + /// @param requestBlock Block number for snapshot validation. + /// @param submissionDeadline Deadline for submitting tickets. event CommitteeRequested( uint256 indexed e3Id, - address filter, - uint32[2] threshold + uint256 seed, + uint32[2] threshold, + uint256 requestBlock, + uint256 submissionDeadline ); + /// @notice This event MUST be emitted when a ticket is submitted for sortition + /// @param e3Id ID of the E3 computation + /// @param node Address of the ciphernode submitting the ticket + /// @param ticketId The ticket number being submitted + /// @param score The computed score for the ticket + event TicketSubmitted( + uint256 indexed e3Id, + address indexed node, + uint256 ticketId, + uint256 score + ); + + /// @notice This event MUST be emitted when a committee is finalized + /// @param e3Id ID of the E3 computation + /// @param committee Array of selected ciphernode addresses + event CommitteeFinalized(uint256 indexed e3Id, address[] committee); + /// @notice This event MUST be emitted when a committee is selected for an E3. /// @param e3Id ID of the E3 for which the committee was selected. /// @param publicKey Public key of the committee. - event CommitteePublished(uint256 indexed e3Id, bytes publicKey); + event CommitteePublished( + uint256 indexed e3Id, + address[] nodes, + bytes publicKey + ); + + /// @notice This event MUST be emitted when a committee's active status changes. + /// @param e3Id ID of the E3 for which the committee status changed. + /// @param active True if committee is now active, false if completed. + event CommitteeActivationChanged(uint256 indexed e3Id, bool active); /// @notice This event MUST be emitted when `enclave` is set. /// @param enclave Address of the enclave contract. @@ -49,27 +111,53 @@ interface ICiphernodeRegistry { uint256 size ); + /// @notice This event MUST be emitted any time the `sortitionSubmissionWindow` is set. + /// @param sortitionSubmissionWindow The submission window for the E3 sortition in seconds. + event SortitionSubmissionWindowSet(uint256 sortitionSubmissionWindow); + + /// @notice Check if a ciphernode is eligible for committee selection + /// @dev A ciphernode is eligible if it is enabled in the registry and meets bonding requirements + /// @param ciphernode Address of the ciphernode to check + /// @return eligible Whether the ciphernode is eligible for committee selection function isCiphernodeEligible(address ciphernode) external returns (bool); + /// @notice Check if a ciphernode is enabled in the registry + /// @param node Address of the ciphernode + /// @return enabled Whether the ciphernode is enabled + function isEnabled(address node) external view returns (bool enabled); + + /// @notice Add a ciphernode to the registry + /// @param node Address of the ciphernode to add + function addCiphernode(address node) external; + + /// @notice Remove a ciphernode from the registry + /// @param node Address of the ciphernode to remove + /// @param siblingNodes Array of sibling node indices for tree operations + function removeCiphernode( + address node, + uint256[] calldata siblingNodes + ) external; + /// @notice Initiates the committee selection process for a specified E3. /// @dev This function MUST revert when not called by the Enclave contract. /// @param e3Id ID of the E3 for which to select the committee. - /// @param filter The address of the filter responsible for the committee selection process. + /// @param seed Random seed for score computation. /// @param threshold The M/N threshold for the committee. /// @return success True if committee selection was successfully initiated. function requestCommittee( uint256 e3Id, - address filter, + uint256 seed, uint32[2] calldata threshold ) external returns (bool success); /// @notice Publishes the public key resulting from the committee selection process. - /// @dev This function MUST revert if not called by the previously selected filter. + /// @dev This function MUST revert if not called by the owner. /// @param e3Id ID of the E3 for which to select the committee. - /// @param publicKey The hash of the public key generated by the given committee. + /// @param nodes Array of ciphernode addresses selected for the committee. + /// @param publicKey The public key generated by the given committee. function publishCommittee( uint256 e3Id, - bytes calldata proof, + address[] calldata nodes, bytes calldata publicKey ) external; @@ -81,4 +169,60 @@ interface ICiphernodeRegistry { function committeePublicKey( uint256 e3Id ) external view returns (bytes32 publicKeyHash); + + /// @notice This function should be called by the Enclave contract to get the committee for a given E3. + /// @dev This function MUST revert if no committee has been requested for the given E3. + /// @param e3Id ID of the E3 for which to get the committee. + /// @return committeeNodes The nodes in the committee for the given E3. + function getCommitteeNodes( + uint256 e3Id + ) external view returns (address[] memory committeeNodes); + + /// @notice Returns the current root of the ciphernode IMT + /// @return Current IMT root + function root() external view returns (uint256); + + /// @notice Returns the IMT root at the time a committee was requested + /// @param e3Id ID of the E3 + /// @return IMT root at time of committee request + function rootAt(uint256 e3Id) external view returns (uint256); + + /// @notice Returns the current size of the ciphernode IMT + /// @return Size of the IMT + function treeSize() external view returns (uint256); + + /// @notice Returns the address of the bonding registry + /// @return Address of the bonding registry contract + function getBondingRegistry() external view returns (address); + + /// @notice Sets the Enclave contract address + /// @dev Only callable by owner + /// @param _enclave Address of the Enclave contract + function setEnclave(address _enclave) external; + + /// @notice Sets the bonding registry contract address + /// @dev Only callable by owner + /// @param _bondingRegistry Address of the bonding registry contract + function setBondingRegistry(address _bondingRegistry) external; + + /// @notice This function should be called to set the submission window for the E3 sortition. + /// @param _sortitionSubmissionWindow The submission window for the E3 sortition in seconds. + function setSortitionSubmissionWindow( + uint256 _sortitionSubmissionWindow + ) external; + + /// @notice Submit a ticket for sortition + /// @dev Validates ticket against node's balance at request block + /// @param e3Id ID of the E3 computation + /// @param ticketNumber The ticket number to submit + function submitTicket(uint256 e3Id, uint256 ticketNumber) external; + + /// @notice Finalize the committee after submission window closes + /// @param e3Id ID of the E3 computation + function finalizeCommittee(uint256 e3Id) external; + + /// @notice Check if submission window is still open for an E3 + /// @param e3Id ID of the E3 computation + /// @return Whether the submission window is open + function isOpen(uint256 e3Id) external view returns (bool); } diff --git a/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol b/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol index 74f8d0f231..2f2a625600 100644 --- a/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol +++ b/packages/enclave-contracts/contracts/interfaces/IComputeProvider.sol @@ -7,9 +7,19 @@ pragma solidity >=0.8.27; import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; +/** + * @title IComputeProvider + * @notice Interface for compute provider validation and configuration + * @dev Compute providers define how computations are executed and verified in the E3 system + */ interface IComputeProvider { - /// @notice This function should be called by the Enclave contract to validate the compute provider parameters. - /// @param params ABI encoded compute provider parameters. + /// @notice Validate compute provider parameters and return the appropriate decryption verifier + /// @dev This function is called by the Enclave contract during E3 request to validate + /// compute provider configuration + /// @param e3Id ID of the E3 computation + /// @param seed Random seed for the computation + /// @param params ABI encoded compute provider parameters + /// @return decryptionVerifier The decryption verifier contract to use for this computation function validate( uint256 e3Id, uint256 seed, diff --git a/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol b/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol index bb3025c775..e7b727914e 100644 --- a/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol +++ b/packages/enclave-contracts/contracts/interfaces/IDecryptionVerifier.sol @@ -5,13 +5,18 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title IDecryptionVerifier + * @notice Interface for verifying decrypted computation outputs + * @dev Implements cryptographic verification of plaintext outputs from encrypted computations + */ interface IDecryptionVerifier { - /// @notice This function should be called by the Enclave contract to verify the - /// decryption of output of a computation. - /// @param e3Id ID of the E3. - /// @param plaintextOutputHash The keccak256 hash of the plaintext output to be verified. - /// @param proof ABI encoded proof of the given output hash. - /// @return success Whether or not the plaintextOutputHash was successfully verified. + /// @notice Verify the decryption of a computation output + /// @dev This function is called by the Enclave contract when plaintext output is published + /// @param e3Id ID of the E3 computation + /// @param plaintextOutputHash The keccak256 hash of the plaintext output to be verified + /// @param proof ABI encoded proof of the decryption validity + /// @return success Whether the plaintextOutputHash was successfully verified function verify( uint256 e3Id, bytes32 plaintextOutputHash, diff --git a/packages/enclave-contracts/contracts/interfaces/IE3.sol b/packages/enclave-contracts/contracts/interfaces/IE3.sol index 5bfa5deb40..dc36d39c05 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3.sol @@ -9,22 +9,27 @@ import { IInputValidator } from "./IInputValidator.sol"; import { IE3Program } from "./IE3Program.sol"; import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; -/// @title E3 struct -/// @notice This struct represents an E3 computation. -/// @param threshold M/N threshold for the committee. -/// @param requestBlock Block number when the E3 was requested. -/// @param startWindow Start window for the computation: index zero is minimum, index 1 is the maxium. -/// @param duration Duration of the E3. -/// @param expiration Timestamp when committee duties expire. -/// @param e3Program Address of the E3 Program contract. -/// @param e3ProgramParams ABI encoded computation parameters. -/// @param customParams Arbitrary ABI-encoded application-defined parameters. -/// @param computeProvider Address of the compute provider contract. -/// @param inputValidator Address of the input validator contract. -/// @param decryptionVerifier Address of the output verifier contract. -/// @param committeeId ID of the selected committee. -/// @param ciphertextOutput Encrypted output data. -/// @param plaintextOutput Decrypted output data. +/** + * @title E3 + * @notice Represents a complete E3 (Encrypted Execution Environment) computation request and its lifecycle + * @dev This struct tracks all parameters, state, and results of an encrypted computation + * from request through completion + * @param seed Random seed for committee selection and computation initialization + * @param threshold M/N threshold for the committee (M required out of N total members) + * @param requestBlock Block number when the E3 computation was requested + * @param startWindow Start window for the computation: index 0 is minimum block, index 1 is the maximum block + * @param duration Duration of the E3 computation in blocks or time units + * @param expiration Timestamp when committee duties expire and computation is considered failed + * @param encryptionSchemeId Identifier for the encryption scheme used in this computation + * @param e3Program Address of the E3 Program contract that validates and verifies the computation + * @param e3ProgramParams ABI encoded computation parameters specific to the E3 program + * @param customParams Arbitrary ABI-encoded application-defined parameters. + * @param inputValidator Address of the input validator contract for input verification + * @param decryptionVerifier Address of the output verifier contract for decryption verification + * @param committeePublicKey The public key of the selected committee for this computation + * @param ciphertextOutput Hash of the encrypted output data produced by the computation + * @param plaintextOutput Decrypted output data after committee decryption + */ struct E3 { uint256 seed; uint32[2] threshold; diff --git a/packages/enclave-contracts/contracts/interfaces/IE3Program.sol b/packages/enclave-contracts/contracts/interfaces/IE3Program.sol index 1ac8df976a..0dd1f642c1 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3Program.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3Program.sol @@ -7,14 +7,20 @@ pragma solidity >=0.8.27; import { IInputValidator } from "./IInputValidator.sol"; +/** + * @title IE3Program + * @notice Interface for E3 program validation and verification + * @dev E3 programs define the computation logic and validation rules for encrypted execution environments + */ interface IE3Program { - /// @notice This function should be called by the Enclave contract to validate the computation parameters. - /// @param e3Id ID of the E3. - /// @param seed Seed for the computation. - /// @param e3ProgramParams ABI encoded computation parameters. - /// @param computeProviderParams ABI encoded compute provider parameters. - /// @return encryptionSchemeId ID of the encryption scheme to be used for the computation. - /// @return inputValidator The input validator to be used for the computation. + /// @notice Validate E3 computation parameters and return encryption scheme and input validator + /// @dev This function is called by the Enclave contract during E3 request to configure the computation + /// @param e3Id ID of the E3 computation + /// @param seed Random seed for the computation + /// @param e3ProgramParams ABI encoded E3 program parameters + /// @param computeProviderParams ABI encoded compute provider parameters + /// @return encryptionSchemeId ID of the encryption scheme to be used for the computation + /// @return inputValidator The input validator to be used for the computation function validate( uint256 e3Id, uint256 seed, @@ -24,11 +30,12 @@ interface IE3Program { external returns (bytes32 encryptionSchemeId, IInputValidator inputValidator); - /// @notice This function should be called by the Enclave contract to verify the decrypted output of an E3. - /// @param e3Id ID of the E3. - /// @param ciphertextOutputHash The keccak256 hash of output data to be verified. - /// @param proof ABI encoded data to verify the ciphertextOutputHash. - /// @return success Whether the output data is valid. + /// @notice Verify the ciphertext output of an E3 computation + /// @dev This function is called by the Enclave contract when ciphertext output is published + /// @param e3Id ID of the E3 computation + /// @param ciphertextOutputHash The keccak256 hash of output data to be verified + /// @param proof ABI encoded data to verify the ciphertextOutputHash + /// @return success Whether the output data is valid function verify( uint256 e3Id, bytes32 ciphertextOutputHash, diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index e87012d30c..f8b926a4d9 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -6,6 +6,10 @@ pragma solidity >=0.8.27; import { E3, IE3Program } from "./IE3.sol"; +import { ICiphernodeRegistry } from "./ICiphernodeRegistry.sol"; +import { IBondingRegistry } from "./IBondingRegistry.sol"; +import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IEnclave { //////////////////////////////////////////////////////////// @@ -17,14 +21,8 @@ interface IEnclave { /// @notice This event MUST be emitted when an Encrypted Execution Environment (E3) is successfully requested. /// @param e3Id ID of the E3. /// @param e3 Details of the E3. - /// @param filter Address of the pool of nodes from which the Cipher Node committee was selected. /// @param e3Program Address of the Computation module selected. - event E3Requested( - uint256 e3Id, - E3 e3, - address filter, - IE3Program indexed e3Program - ); + event E3Requested(uint256 e3Id, E3 e3, IE3Program indexed e3Program); /// @notice This event MUST be emitted when an Encrypted Execution Environment (E3) is successfully activated. /// @param e3Id ID of the E3. @@ -70,6 +68,24 @@ interface IEnclave { /// @param ciphernodeRegistry The address of the CiphernodeRegistry contract. event CiphernodeRegistrySet(address ciphernodeRegistry); + /// @notice This event MUST be emitted any time the BondingRegistry is set. + /// @param bondingRegistry The address of the BondingRegistry contract. + event BondingRegistrySet(address bondingRegistry); + + /// @notice This event MUST be emitted any time the fee token is set. + /// @param feeToken The address of the fee token. + event FeeTokenSet(address feeToken); + + /// @notice This event MUST be emitted when rewards are distributed to committee members. + /// @param e3Id The ID of the E3 computation. + /// @param nodes The addresses of the committee members receiving rewards. + /// @param amounts The reward amounts for each committee member. + event RewardsDistributed( + uint256 indexed e3Id, + address[] nodes, + uint256[] amounts + ); + /// @notice The event MUST be emitted any time an encryption scheme is enabled. /// @param encryptionSchemeId The ID of the encryption scheme that was enabled. event EncryptionSchemeEnabled(bytes32 encryptionSchemeId); @@ -97,7 +113,6 @@ interface IEnclave { //////////////////////////////////////////////////////////// /// @notice This struct contains the parameters to submit a request to Enclave. - /// @param filter The address of the pool of nodes from which to select the committee. /// @param threshold The M/N threshold for the committee. /// @param startWindow The start window for the computation. /// @param duration The duration of the computation in seconds. @@ -106,7 +121,6 @@ interface IEnclave { /// @param computeProviderParams The ABI encoded compute provider parameters. /// @param customParams Arbitrary ABI-encoded application-defined parameters. struct E3RequestParams { - address filter; uint32[2] threshold; uint256[2] startWindow; uint256 duration; @@ -128,8 +142,8 @@ interface IEnclave { /// @return e3Id ID of the E3. /// @return e3 The E3 struct. function request( - E3RequestParams memory requestParams - ) external payable returns (uint256 e3Id, E3 memory e3); + E3RequestParams calldata requestParams + ) external returns (uint256 e3Id, E3 memory e3); /// @notice This function should be called to activate an Encrypted Execution Environment (E3) once it has been /// initialized and is ready for input. @@ -192,6 +206,28 @@ interface IEnclave { uint256 _maxDuration ) external returns (bool success); + /// @notice Sets the Ciphernode Registry contract address. + /// @dev This function MUST revert if the address is zero or the same as the current registry. + /// @param _ciphernodeRegistry The address of the new Ciphernode Registry contract. + /// @return success True if the registry was successfully set. + function setCiphernodeRegistry( + ICiphernodeRegistry _ciphernodeRegistry + ) external returns (bool success); + + /// @notice Sets the Bonding Registry contract address. + /// @dev This function MUST revert if the address is zero or the same as the current registry. + /// @param _bondingRegistry The address of the new Bonding Registry contract. + /// @return success True if the registry was successfully set. + function setBondingRegistry( + IBondingRegistry _bondingRegistry + ) external returns (bool success); + + /// @notice Sets the fee token used for E3 payments. + /// @dev This function MUST revert if the address is zero or the same as the current fee token. + /// @param _feeToken The address of the new fee token. + /// @return success True if the fee token was successfully set. + function setFeeToken(IERC20 _feeToken) external returns (bool success); + /// @notice This function should be called to enable an E3 Program. /// @param e3Program The address of the E3 Program. /// @return success True if the E3 Program was successfully enabled. @@ -206,6 +242,32 @@ interface IEnclave { IE3Program e3Program ) external returns (bool success); + /// @notice Sets or enables a decryption verifier for a specific encryption scheme. + /// @dev This function MUST revert if the verifier address is zero or already set to the same value. + /// @param encryptionSchemeId The unique identifier for the encryption scheme. + /// @param decryptionVerifier The address of the decryption verifier contract. + /// @return success True if the verifier was successfully set. + function setDecryptionVerifier( + bytes32 encryptionSchemeId, + IDecryptionVerifier decryptionVerifier + ) external returns (bool success); + + /// @notice Disables a previously enabled encryption scheme. + /// @dev This function MUST revert if the encryption scheme is not currently enabled. + /// @param encryptionSchemeId The unique identifier for the encryption scheme to disable. + /// @return success True if the encryption scheme was successfully disabled. + function disableEncryptionScheme( + bytes32 encryptionSchemeId + ) external returns (bool success); + + /// @notice Sets the allowed E3 program parameters. + /// @dev This function enables specific parameter sets for E3 programs (e.g., BFV encryption parameters). + /// @param _e3ProgramsParams Array of ABI encoded parameter sets to allow. + /// @return success True if the parameters were successfully set. + function setE3ProgramsParams( + bytes[] memory _e3ProgramsParams + ) external returns (bool success); + //////////////////////////////////////////////////////////// // // // Get Functions // @@ -231,4 +293,19 @@ interface IEnclave { function getInputsLength( uint256 e3Id ) external view returns (uint256 length); + + /// @notice This function returns the fee of an E3 + /// @dev This function MUST revert if the E3 parameters are invalid. + /// @param e3Params the struct representing the E3 request parameters + /// @return fee the fee of the E3 + function getE3Quote( + E3RequestParams calldata e3Params + ) external view returns (uint256 fee); + + /// @notice Returns the decryption verifier for a given encryption scheme. + /// @param encryptionSchemeId The unique identifier for the encryption scheme. + /// @return The decryption verifier contract for the specified encryption scheme. + function getDecryptionVerifier( + bytes32 encryptionSchemeId + ) external view returns (IDecryptionVerifier); } diff --git a/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol b/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol index cc1b9c7336..2c87a2ba05 100644 --- a/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol +++ b/packages/enclave-contracts/contracts/interfaces/IInputValidator.sol @@ -5,12 +5,17 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +/** + * @title IInputValidator + * @notice Interface for validating computation inputs + * @dev Input validators enforce access control and validation rules for E3 computation inputs + */ interface IInputValidator { - /// @notice This function should be called by the Enclave contract to validate the - /// input of a computation. - /// @param sender The account that is submitting the input. - /// @param data The input to be verified. - /// @return input The decoded, policy-approved application payload. + /// @notice Validate and process input data for a computation + /// @dev This function is called by the Enclave contract when input is published + /// @param sender The account that is submitting the input + /// @param data The input data to be validated + /// @return input The decoded, policy-approved application payload function validate( address sender, bytes memory data diff --git a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol b/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol deleted file mode 100644 index b7aeb0c63d..0000000000 --- a/packages/enclave-contracts/contracts/interfaces/IRegistryFilter.sol +++ /dev/null @@ -1,13 +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. -pragma solidity >=0.8.27; - -interface IRegistryFilter { - function requestCommittee( - uint256 e3Id, - uint32[2] calldata threshold - ) external returns (bool success); -} diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol new file mode 100644 index 0000000000..a2ac4b860f --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol @@ -0,0 +1,23 @@ +// 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. +pragma solidity >=0.8.27; + +/** + * @title ISlashVerifier + * @notice Interface for verifying slash proofs + * @dev Slash verifiers implement cryptographic or logical verification of slash proposals + */ +interface ISlashVerifier { + /// @notice Verify a slash proof + /// @dev This function is called by the SlashingManager contract during slash proposal to verify proof validity + /// @param proposalId ID of the slash proposal + /// @param proof ABI encoded proof data supporting the slash + /// @return success Whether the proof was successfully verified + function verify( + uint256 proposalId, + bytes memory proof + ) external view returns (bool success); +} diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol new file mode 100644 index 0000000000..d5711fc77b --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -0,0 +1,426 @@ +// 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. + +pragma solidity >=0.8.27; + +import { IBondingRegistry } from "./IBondingRegistry.sol"; + +/** + * @title ISlashingManager + * @notice Interface for managing slashing proposals, appeals, and execution + * @dev Maintains policy table and handles slash workflows with appeals + */ +interface ISlashingManager { + // ====================== + // Structs + // ====================== + + /** + * @notice Slashing policy configuration for different slash reasons + * @dev Defines penalties, proof requirements, and appeal mechanisms for each slash type + * @param ticketPenalty Amount of ticket collateral to slash (in wei) + * @param licensePenalty Amount of license bond to slash (in wei) + * @param requiresProof Whether this slash type requires cryptographic proof verification + * @param proofVerifier Address of the ISlashVerifier contract for proof validation + * @param banNode Whether executing this slash will permanently ban the node + * @param appealWindow Time window in seconds for operators to appeal (0 = immediate execution, no appeals) + * @param enabled Whether this slash type is currently active and can be proposed + */ + struct SlashPolicy { + uint256 ticketPenalty; + uint256 licensePenalty; + bool requiresProof; + address proofVerifier; + bool banNode; + uint256 appealWindow; + bool enabled; + } + + /** + * @notice Slash proposal details tracking the full lifecycle of a slash + * @dev Stores all state needed for proposal, appeal, and execution workflows + * @param operator Address of the ciphernode operator being slashed + * @param reason Hash of the slash reason (maps to SlashPolicy configuration) + * @param ticketAmount Amount of ticket collateral to slash (copied from policy at proposal time) + * @param licenseAmount Amount of license bond to slash (copied from policy at proposal time) + * @param executed Whether the slashing penalties have been executed + * @param appealed Whether the operator has filed an appeal + * @param resolved Whether the appeal has been resolved by governance + * @param appealUpheld Whether the appeal was approved (true = cancel slash, false = slash proceeds) + * @param proposedAt Block timestamp when the slash was proposed + * @param executableAt Block timestamp when execution becomes possible (proposedAt + appealWindow) + * @param proposer Address that created this slash proposal + * @param proofHash Keccak256 hash of the proof data submitted with the proposal + * @param proofVerified Whether the proof was successfully verified by the proof verifier contract + */ + struct SlashProposal { + address operator; + bytes32 reason; + uint256 ticketAmount; + uint256 licenseAmount; + bool executed; + bool appealed; + bool resolved; + bool appealUpheld; + uint256 proposedAt; + uint256 executableAt; + address proposer; + bytes32 proofHash; + bool proofVerified; + } + + // ====================== + // Errors + // ====================== + + /// @notice Thrown when a zero address is provided where a valid address is required + error ZeroAddress(); + + /// @notice Thrown when caller lacks required role permissions for the operation + error Unauthorized(); + + /// @notice Thrown when a slash policy configuration is invalid + error InvalidPolicy(); + + /// @notice Thrown when referencing a proposal ID that doesn't exist or is in invalid state + error InvalidProposal(); + + /// @notice Thrown when proof is required by policy but not provided + error ProofRequired(); + + /// @notice Thrown when provided proof fails verification + error InvalidProof(); + + /// @notice Thrown when attempting to execute a slash whose appeal was upheld + error AppealUpheld(); + + /// @notice Thrown when attempting to execute a slash with an unresolved appeal + error AppealPending(); + + /// @notice Thrown when attempting to file an appeal after the appeal window has closed + error AppealWindowExpired(); + + /// @notice Thrown when attempting to execute a slash before the appeal window has closed + error AppealWindowActive(); + + /// @notice Thrown when attempting to file a second appeal for the same proposal + error AlreadyAppealed(); + + /// @notice Thrown when attempting to execute a slash that has already been executed + error AlreadyExecuted(); + + /// @notice Thrown when attempting to resolve an appeal that has already been resolved + error AlreadyResolved(); + + /// @notice Thrown when referencing a slash reason that doesn't exist + error SlashReasonNotFound(); + + /// @notice Thrown when attempting to propose a slash for a disabled reason + error SlashReasonDisabled(); + + /// @notice Thrown when a banned ciphernode attempts a restricted operation + error CiphernodeBanned(); + + /// @notice Thrown when a policy requires proof but no verifier contract is configured + error VerifierNotSet(); + + // ====================== + // Events + // ====================== + + /** + * @notice Emitted when a slash policy is created or updated + * @param reason Hash of the slash reason being configured + * @param policy The complete policy configuration including penalties and appeal settings + */ + event SlashPolicyUpdated(bytes32 indexed reason, SlashPolicy policy); + + /** + * @notice Emitted when a new slash proposal is created + * @param proposalId Unique ID of the created proposal + * @param operator Address of the ciphernode operator being slashed + * @param reason Hash of the slash reason + * @param ticketAmount Amount of ticket collateral to be slashed + * @param licenseAmount Amount of license bond to be slashed + * @param executableAt Timestamp when the slash can be executed (after appeal window) + * @param proposer Address that created the proposal + */ + event SlashProposed( + uint256 indexed proposalId, + address indexed operator, + bytes32 indexed reason, + uint256 ticketAmount, + uint256 licenseAmount, + uint256 executableAt, + address proposer + ); + + /** + * @notice Emitted when a slash proposal is executed and penalties are applied + * @param proposalId ID of the executed proposal + * @param operator Address of the slashed operator + * @param reason Hash of the slash reason + * @param ticketAmount Amount of ticket collateral slashed + * @param licenseAmount Amount of license bond slashed + * @param executed Execution status (should always be true) + */ + event SlashExecuted( + uint256 indexed proposalId, + address indexed operator, + bytes32 indexed reason, + uint256 ticketAmount, + uint256 licenseAmount, + bool executed + ); + + /** + * @notice Emitted when an operator files an appeal against a slash proposal + * @param proposalId ID of the proposal being appealed + * @param operator Address of the operator filing the appeal + * @param reason Hash of the slash reason being appealed + * @param evidence Evidence string provided by the operator supporting their appeal + */ + event AppealFiled( + uint256 indexed proposalId, + address indexed operator, + bytes32 indexed reason, + string evidence + ); + + /** + * @notice Emitted when governance resolves an appeal + * @param proposalId ID of the proposal with the resolved appeal + * @param operator Address of the operator who appealed + * @param appealUpheld Whether the appeal was approved (true = slash cancelled, false = slash proceeds) + * @param resolver Address of the governance account that resolved the appeal + * @param resolution Explanation string for the resolution decision + */ + event AppealResolved( + uint256 indexed proposalId, + address indexed operator, + bool appealUpheld, + address resolver, + string resolution + ); + + /** + * @notice Emitted when a node is banned or unbanned from the network + * @param node Address of the node + * @param status Whether the node is banned + * @param reason Hash of the reason for banning or unbanning + * @param updater Address that executed the ban (governance or contract) + */ + event NodeBanUpdated( + address indexed node, + bool status, + bytes32 indexed reason, + address updater + ); + + // ====================== + // View Functions + // ====================== + + /** + * @notice Retrieves the slash policy configuration for a given reason + * @param reason Hash of the slash reason to query + * @return policy The complete SlashPolicy struct (returns default empty struct if not configured) + */ + function getSlashPolicy( + bytes32 reason + ) external view returns (SlashPolicy memory policy); + + /** + * @notice Retrieves the details of a slash proposal + * @param proposalId ID of the proposal to query + * @return proposal The complete SlashProposal struct + * @dev Reverts with InvalidProposal if proposalId >= totalProposals + */ + function getSlashProposal( + uint256 proposalId + ) external view returns (SlashProposal memory proposal); + + /** + * @notice Returns the total number of slash proposals ever created + * @return count The total count of proposals (next proposalId will be this value) + */ + function totalProposals() external view returns (uint256 count); + + /** + * @notice Checks whether a node is currently banned + * @param node Address of the node to check + * @return isBanned True if the node is banned, false otherwise + */ + function isBanned(address node) external view returns (bool isBanned); + + /** + * @notice Returns the bonding registry contract used for executing slashes + * @return registry The IBondingRegistry contract instance + */ + function bondingRegistry() + external + view + returns (IBondingRegistry registry); + + // ====================== + // Admin Functions + // ====================== + + /** + * @notice Creates or updates the slash policy for a specific reason + * @dev Only callable by GOVERNANCE_ROLE. Validates policy constraints before setting + * @param reason Hash of the slash reason to configure (must be non-zero) + * @param policy Complete policy configuration including penalties, proof requirements, and appeal settings + * Requirements: + * - reason must not be bytes32(0) + * - policy.enabled must be true + * - At least one of ticketPenalty or licensePenalty must be non-zero + * - If requiresProof is true, proofVerifier must be set and appealWindow must be 0 + * - If requiresProof is false, appealWindow must be greater than 0 + */ + function setSlashPolicy( + bytes32 reason, + SlashPolicy calldata policy + ) external; + + /** + * @notice Updates the bonding registry contract address + * @dev Only callable by DEFAULT_ADMIN_ROLE. Used to execute actual slashing of funds + * @param newBondingRegistry Address of the new IBondingRegistry contract (must be non-zero) + */ + function setBondingRegistry(address newBondingRegistry) external; + + /** + * @notice Grants SLASHER_ROLE to an address + * @dev Only callable by DEFAULT_ADMIN_ROLE. Slashers can propose and execute slashes + * @param slasher Address to grant slashing permissions (must be non-zero) + */ + function addSlasher(address slasher) external; + + /** + * @notice Revokes SLASHER_ROLE from an address + * @dev Only callable by DEFAULT_ADMIN_ROLE + * @param slasher Address to revoke slashing permissions from + */ + function removeSlasher(address slasher) external; + + /** + * @notice Grants VERIFIER_ROLE to an address + * @dev Only callable by DEFAULT_ADMIN_ROLE. Verifiers can validate proof-based slashes + * @param verifier Address to grant verification permissions (must be non-zero) + */ + function addVerifier(address verifier) external; + + /** + * @notice Revokes VERIFIER_ROLE from an address + * @dev Only callable by DEFAULT_ADMIN_ROLE + * @param verifier Address to revoke verification permissions from + */ + function removeVerifier(address verifier) external; + + // ====================== + // Slashing Functions + // ====================== + + /** + * @notice Creates a new slash proposal against an operator + * @dev Only callable by SLASHER_ROLE. Validates policy and proof if required + * @param operator Address of the ciphernode operator to slash (must be non-zero) + * @param reason Hash of the slash reason (must have an enabled policy configured) + * @param proof Proof data to be verified (required if policy.requiresProof is true, can be empty otherwise) + * @return proposalId Sequential ID of the created proposal + * Requirements: + * - operator must not be zero address + * - reason must have an enabled policy configured + * - If policy requires proof, proof must be non-empty and pass verification + * - Caller must have SLASHER_ROLE + */ + function proposeSlash( + address operator, + bytes32 reason, + bytes calldata proof + ) external returns (uint256 proposalId); + + /** + * @notice Executes a slash proposal and applies penalties to the operator + * @dev Only callable by SLASHER_ROLE. Validates execution conditions and applies slashing + * @param proposalId ID of the proposal to execute (must exist and not be already executed) + * Requirements: + * - Proposal must exist and not be already executed + * - For proof-required slashes: proof must be verified + * - For evidence-based slashes: appeal window must have expired + * - If appeal was filed and resolved, appeal must not have been upheld + * - Caller must have SLASHER_ROLE + * Effects: + * - Marks proposal as executed + * - Slashes ticket balance if ticketAmount > 0 + * - Slashes license bond if licenseAmount > 0 + * - Bans node if policy.banNode is true + */ + function executeSlash(uint256 proposalId) external; + + // ====================== + // Appeal Functions + // ====================== + + /** + * @notice Allows an operator to file an appeal against a slash proposal + * @dev Only the operator being slashed can file an appeal, and only within the appeal window + * @param proposalId ID of the proposal to appeal (must exist) + * @param evidence String containing evidence and arguments supporting the appeal + * Requirements: + * - Proposal must exist + * - Caller must be the operator being slashed + * - Current timestamp must be before proposal.executableAt (within appeal window) + * - Proposal must not already have an appeal filed + */ + function fileAppeal(uint256 proposalId, string calldata evidence) external; + + /** + * @notice Resolves an appeal by accepting or rejecting it + * @dev Only callable by GOVERNANCE_ROLE. If appeal is upheld, the slash cannot be executed + * @param proposalId ID of the proposal with the appeal to resolve (must exist and have an appeal) + * @param appealUpheld True to uphold the appeal (cancel the slash), false to deny the appeal + * (allow slash to proceed) + * @param resolution String explaining the governance decision + * Requirements: + * - Proposal must exist and have an appeal filed + * - Appeal must not already be resolved + * - Caller must have GOVERNANCE_ROLE + * Effects: + * - Marks appeal as resolved + * - Sets appealUpheld flag (true = slash cancelled, false = slash can proceed) + */ + function resolveAppeal( + uint256 proposalId, + bool appealUpheld, + string calldata resolution + ) external; + + // ====================== + // Ban Management + // ====================== + + /** + * @notice Bans or unbans a node from the network + * @dev Only callable by GOVERNANCE_ROLE. Bans can also occur automatically via executeSlash + * @param node Address of the node to ban (must be non-zero) + * @param status Whether to ban the node + * @param reason Hash of the reason for banning + * Requirements: + * - node must not be zero address + * - Caller must have GOVERNANCE_ROLE + * Effects: + * - Sets banned[node] to status + * - Emits NodeBanned event if status is true + * - Emits NodeUnbanned event if status is false + */ + function updateBanStatus( + address node, + bool status, + bytes32 reason + ) external; +} diff --git a/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol new file mode 100644 index 0000000000..12d250dcc1 --- /dev/null +++ b/packages/enclave-contracts/contracts/lib/ExitQueueLib.sol @@ -0,0 +1,485 @@ +// 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. + +pragma solidity >=0.8.27; + +/** + * @title ExitQueueLib + * @notice Library for managing time-locked exit queues for tickets and licenses + * @dev Implements a queue system where assets are locked for a delay period before they can be claimed or slashed. + * Assets are organized into tranches based on unlock timestamps, allowing efficient batch operations. + */ +library ExitQueueLib { + /** + * @notice Represents a single tranche of assets with a specific unlock timestamp + * @dev Multiple assets queued at the same time are merged into the same tranche for efficiency + * @param unlockTimestamp The timestamp when assets in this tranche become claimable + * @param ticketAmount The amount of tickets in this tranche + * @param licenseAmount The amount of licenses in this tranche + */ + struct ExitTranche { + uint64 unlockTimestamp; + uint256 ticketAmount; + uint256 licenseAmount; + } + + /** + * @notice Tracks total pending amounts for an operator across all tranches + * @param ticketAmount Total pending tickets waiting in the exit queue + * @param licenseAmount Total pending licenses waiting in the exit queue + */ + struct PendingAmounts { + uint256 ticketAmount; + uint256 licenseAmount; + } + + /** + * @notice Main state structure for the exit queue system + * @dev Contains all per-operator queue data and pending totals + * @param operatorQueues Maps operator addresses to their arrays of exit tranches + * @param queueHeadIndex Maps operator addresses to the current head index (for efficient cleanup) + * @param pendingTotals Maps operator addresses to their total pending amounts + */ + struct ExitQueueState { + mapping(address operator => ExitTranche[] operatorQueues) operatorQueues; + mapping(address operator => uint256 queueHeadIndex) queueHeadIndex; + mapping(address operator => PendingAmounts operatorPendings) pendingTotals; + } + + /** + * @notice Types of assets that can be queued for exit + * @dev Used internally to differentiate between ticket and license operations + */ + enum AssetType { + Ticket, + License + } + + /** + * @notice Emitted when assets are queued for exit + * @param operator The operator whose assets were queued + * @param ticketAmount The amount of tickets queued + * @param licenseAmount The amount of licenses queued + * @param unlockTimestamp The timestamp when these assets will become claimable + */ + event AssetsQueuedForExit( + address indexed operator, + uint256 ticketAmount, + uint256 licenseAmount, + uint64 unlockTimestamp + ); + + /** + * @notice Emitted when assets are claimed from the exit queue + * @param operator The operator who claimed the assets + * @param ticketAmount The amount of tickets claimed + * @param licenseAmount The amount of licenses claimed + */ + event AssetsClaimed( + address indexed operator, + uint256 ticketAmount, + uint256 licenseAmount + ); + + /** + * @notice Emitted when pending assets are slashed + * @param operator The operator whose assets were slashed + * @param ticketAmount The amount of tickets slashed + * @param licenseAmount The amount of licenses slashed + * @param includedLockedAssets Whether locked (not yet unlocked) assets were included in the slash + */ + event PendingAssetsSlashed( + address indexed operator, + uint256 ticketAmount, + uint256 licenseAmount, + bool includedLockedAssets + ); + + /// @notice Thrown when attempting to queue zero amount of both asset types + error ZeroAmountNotAllowed(); + + /// @notice Thrown when timestamp calculation would overflow uint64 + error TimestampOverflow(); + + /// @notice Thrown when accessing an invalid queue index + error IndexOutOfBounds(); + + /** + * @notice Queues both tickets and licenses for exit with a time delay + * @dev Assets are added to the operator's queue and will be claimable after exitDelaySeconds. + * If a tranche with the same unlock timestamp already exists, amounts are merged into it. + * @param state The exit queue state storage + * @param operator The operator whose assets are being queued + * @param exitDelaySeconds The number of seconds until assets become claimable + * @param ticketAmount The amount of tickets to queue (can be 0) + * @param licenseAmount The amount of licenses to queue (can be 0) + */ + function queueAssetsForExit( + ExitQueueState storage state, + address operator, + uint64 exitDelaySeconds, + uint256 ticketAmount, + uint256 licenseAmount + ) internal { + if (ticketAmount == 0 && licenseAmount == 0) { + return; + } + + uint64 currentTimestamp = uint64(block.timestamp); + require( + currentTimestamp <= (type(uint64).max - exitDelaySeconds), + TimestampOverflow() + ); + uint64 unlockTimestamp = currentTimestamp + exitDelaySeconds; + + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + + uint256 len = operatorQueue.length; + bool merged; + if (len != 0) { + ExitTranche storage lastTranche = operatorQueue[len - 1]; + if (lastTranche.unlockTimestamp == unlockTimestamp) { + if (ticketAmount != 0) lastTranche.ticketAmount += ticketAmount; + if (licenseAmount != 0) + lastTranche.licenseAmount += licenseAmount; + merged = true; + } + } + + if (!merged) { + ExitTranche storage t = operatorQueue.push(); + t.unlockTimestamp = unlockTimestamp; + t.ticketAmount = ticketAmount; + t.licenseAmount = licenseAmount; + } + + _updatePendingTotals( + state, + operator, + ticketAmount, + licenseAmount, + true + ); + + emit AssetsQueuedForExit( + operator, + ticketAmount, + licenseAmount, + unlockTimestamp + ); + } + + /** + * @notice Queues only tickets for exit with a time delay + * @dev Convenience function that calls queueAssetsForExit with licenseAmount = 0 + * @param state The exit queue state storage + * @param operator The operator whose tickets are being queued + * @param exitDelaySeconds The number of seconds until tickets become claimable + * @param ticketAmount The amount of tickets to queue + */ + function queueTicketsForExit( + ExitQueueState storage state, + address operator, + uint64 exitDelaySeconds, + uint256 ticketAmount + ) internal { + queueAssetsForExit(state, operator, exitDelaySeconds, ticketAmount, 0); + } + + /** + * @notice Queues only licenses for exit with a time delay + * @dev Convenience function that calls queueAssetsForExit with ticketAmount = 0 + * @param state The exit queue state storage + * @param operator The operator whose licenses are being queued + * @param exitDelaySeconds The number of seconds until licenses become claimable + * @param licenseAmount The amount of licenses to queue + */ + function queueLicensesForExit( + ExitQueueState storage state, + address operator, + uint64 exitDelaySeconds, + uint256 licenseAmount + ) internal { + queueAssetsForExit(state, operator, exitDelaySeconds, 0, licenseAmount); + } + + /** + * @notice Gets the total pending amounts for an operator across all tranches + * @dev Returns both locked (not yet claimable) and unlocked (claimable) amounts + * @param state The exit queue state storage + * @param operator The operator to query + * @return ticketAmount Total pending tickets in the exit queue + * @return licenseAmount Total pending licenses in the exit queue + */ + function getPendingAmounts( + ExitQueueState storage state, + address operator + ) internal view returns (uint256 ticketAmount, uint256 licenseAmount) { + PendingAmounts storage pending = state.pendingTotals[operator]; + return (pending.ticketAmount, pending.licenseAmount); + } + + /** + * @notice Previews the amounts that can be claimed at the current block timestamp + * @dev Iterates through tranches and sums up amounts where unlock timestamp has passed + * @param state The exit queue state storage + * @param operator The operator to query + * @return ticketAmount Total claimable tickets at current timestamp + * @return licenseAmount Total claimable licenses at current timestamp + */ + function previewClaimableAmounts( + ExitQueueState storage state, + address operator + ) internal view returns (uint256 ticketAmount, uint256 licenseAmount) { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 currentIndex = state.queueHeadIndex[operator]; + + for (uint256 i = currentIndex; i < operatorQueue.length; i++) { + ExitTranche storage tranche = operatorQueue[i]; + + if (block.timestamp < tranche.unlockTimestamp) { + break; + } + + ticketAmount += tranche.ticketAmount; + licenseAmount += tranche.licenseAmount; + } + } + + /** + * @notice Claims unlocked assets from the exit queue + * @dev Only processes tranches where unlock timestamp has passed. Updates pending totals + * and cleans up empty tranches. + * @param state The exit queue state storage + * @param operator The operator claiming assets + * @param maxTicketAmount Maximum tickets to claim (actual claimed may be less if queue has fewer) + * @param maxLicenseAmount Maximum licenses to claim (actual claimed may be less if queue has fewer) + * @return ticketsClaimed Actual amount of tickets claimed + * @return licensesClaimed Actual amount of licenses claimed + */ + function claimAssets( + ExitQueueState storage state, + address operator, + uint256 maxTicketAmount, + uint256 maxLicenseAmount + ) internal returns (uint256 ticketsClaimed, uint256 licensesClaimed) { + if (maxTicketAmount > 0) { + ticketsClaimed = _takeAssetsFromQueue( + state, + operator, + maxTicketAmount, + AssetType.Ticket, + false + ); + if (ticketsClaimed > 0) { + state.pendingTotals[operator].ticketAmount -= ticketsClaimed; + } + } + + if (maxLicenseAmount > 0) { + licensesClaimed = _takeAssetsFromQueue( + state, + operator, + maxLicenseAmount, + AssetType.License, + false + ); + if (licensesClaimed > 0) { + state.pendingTotals[operator].licenseAmount -= licensesClaimed; + } + } + + if (ticketsClaimed > 0 || licensesClaimed > 0) { + _cleanupEmptyTranches(state, operator); + emit AssetsClaimed(operator, ticketsClaimed, licensesClaimed); + } + } + + /** + * @notice Slashes pending assets from the exit queue + * @dev Can optionally include locked (not yet unlocked) assets. Updates pending totals + * and cleans up empty tranches. + * @param state The exit queue state storage + * @param operator The operator whose assets are being slashed + * @param ticketAmountToSlash Maximum tickets to slash + * @param licenseAmountToSlash Maximum licenses to slash + * @param includeLockedAssets If true, slashes locked assets; if false, only slashes unlocked assets + * @return ticketsSlashed Actual amount of tickets slashed + * @return licensesSlashed Actual amount of licenses slashed + */ + function slashPendingAssets( + ExitQueueState storage state, + address operator, + uint256 ticketAmountToSlash, + uint256 licenseAmountToSlash, + bool includeLockedAssets + ) internal returns (uint256 ticketsSlashed, uint256 licensesSlashed) { + if (ticketAmountToSlash > 0) { + ticketsSlashed = _takeAssetsFromQueue( + state, + operator, + ticketAmountToSlash, + AssetType.Ticket, + includeLockedAssets + ); + if (ticketsSlashed > 0) { + state.pendingTotals[operator].ticketAmount -= ticketsSlashed; + } + } + + if (licenseAmountToSlash > 0) { + licensesSlashed = _takeAssetsFromQueue( + state, + operator, + licenseAmountToSlash, + AssetType.License, + includeLockedAssets + ); + if (licensesSlashed > 0) { + state.pendingTotals[operator].licenseAmount -= licensesSlashed; + } + } + + if (ticketsSlashed > 0 || licensesSlashed > 0) { + _cleanupEmptyTranches(state, operator); + emit PendingAssetsSlashed( + operator, + ticketsSlashed, + licensesSlashed, + includeLockedAssets + ); + } + } + + /** + * @notice Updates the pending totals for an operator + * @dev Internal helper to increase or decrease pending amounts. Uses bitwise OR for efficient zero check. + * @param state The exit queue state storage + * @param operator The operator whose pending totals are being updated + * @param ticketAmountDelta The change in ticket amount + * @param licenseAmountDelta The change in license amount + * @param isIncrease If true, increases totals; if false, decreases totals + */ + function _updatePendingTotals( + ExitQueueState storage state, + address operator, + uint256 ticketAmountDelta, + uint256 licenseAmountDelta, + bool isIncrease + ) private { + if ((ticketAmountDelta | licenseAmountDelta) == 0) return; + + PendingAmounts storage pending = state.pendingTotals[operator]; + + if (isIncrease) { + if (ticketAmountDelta != 0) + pending.ticketAmount += ticketAmountDelta; + if (licenseAmountDelta != 0) + pending.licenseAmount += licenseAmountDelta; + } else { + if (ticketAmountDelta != 0) + pending.ticketAmount -= ticketAmountDelta; + if (licenseAmountDelta != 0) + pending.licenseAmount -= licenseAmountDelta; + } + } + + /** + * @notice Cleans up empty tranches from the head of the queue + * @dev Advances the queue head index past all tranches with zero tickets and licenses. + * This prevents the queue from growing unbounded and reduces gas costs for future operations. + * @param state The exit queue state storage + * @param operator The operator whose queue is being cleaned up + */ + function _cleanupEmptyTranches( + ExitQueueState storage state, + address operator + ) private { + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 currentIndex = state.queueHeadIndex[operator]; + + while (currentIndex < operatorQueue.length) { + ExitTranche storage tranche = operatorQueue[currentIndex]; + if (tranche.ticketAmount == 0 && tranche.licenseAmount == 0) { + currentIndex++; + } else { + break; + } + } + + state.queueHeadIndex[operator] = currentIndex; + } + + /** + * @notice Takes assets from the queue, either for claiming or slashing + * @dev Iterates through tranches from head to tail, taking assets up to wantedAmount. + * Respects unlock timestamps unless includeLockedAssets is true. + * @param state The exit queue state storage + * @param operator The operator whose assets are being taken + * @param wantedAmount The maximum amount to take + * @param assetType Whether to take tickets or licenses + * @param includeLockedAssets If true, takes locked assets; if false, only takes unlocked assets + * @return takenAmount The actual amount taken (may be less than wantedAmount if queue has fewer assets) + */ + function _takeAssetsFromQueue( + ExitQueueState storage state, + address operator, + uint256 wantedAmount, + AssetType assetType, + bool includeLockedAssets + ) private returns (uint256 takenAmount) { + if (wantedAmount == 0) { + return 0; + } + + ExitTranche[] storage operatorQueue = state.operatorQueues[operator]; + uint256 currentIndex = state.queueHeadIndex[operator]; + uint256 queueLength = operatorQueue.length; + uint256 remainingWanted = wantedAmount; + + while (remainingWanted > 0 && currentIndex < queueLength) { + ExitTranche storage tranche = operatorQueue[currentIndex]; + + if ( + !includeLockedAssets && + block.timestamp < tranche.unlockTimestamp + ) { + break; + } + + uint256 availableAmount; + if (assetType == AssetType.Ticket) { + availableAmount = tranche.ticketAmount; + } else { + availableAmount = tranche.licenseAmount; + } + + if (availableAmount == 0) { + currentIndex++; + continue; + } + + uint256 amountToTake = remainingWanted < availableAmount + ? remainingWanted + : availableAmount; + + if (assetType == AssetType.Ticket) { + tranche.ticketAmount -= amountToTake; + } else { + tranche.licenseAmount -= amountToTake; + } + + remainingWanted -= amountToTake; + takenAmount += amountToTake; + + if (tranche.ticketAmount == 0 && tranche.licenseAmount == 0) { + currentIndex++; + } + } + + state.queueHeadIndex[operator] = currentIndex; + } +} diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol new file mode 100644 index 0000000000..8dee1952ea --- /dev/null +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -0,0 +1,751 @@ +// 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. + +pragma solidity >=0.8.27; + +import { + OwnableUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { ExitQueueLib } from "../lib/ExitQueueLib.sol"; + +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; +import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; +import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; + +/** + * @title BondingRegistry + * @notice Implementation of the bonding registry managing operator ticket balances and license bonds + * @dev Handles deposits, withdrawals, slashing, exits, and integrates with registry and slashing manager + */ +contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { + using SafeERC20 for IERC20; + using ExitQueueLib for ExitQueueLib.ExitQueueState; + + // ====================== + // Constants + // ====================== + + /// @dev Reason code for ticket balance deposits + bytes32 private constant REASON_DEPOSIT = bytes32("DEPOSIT"); + + /// @dev Reason code for ticket balance withdrawals + bytes32 private constant REASON_WITHDRAW = bytes32("WITHDRAW"); + + /// @dev Reason code for license bond operations + bytes32 private constant REASON_BOND = bytes32("BOND"); + + /// @dev Reason code for license unbond operations + bytes32 private constant REASON_UNBOND = bytes32("UNBOND"); + + // ====================== + // Storage + // ====================== + + /// @notice Ticket token (ETK with underlying USDC) used for collateral + EnclaveTicketToken public ticketToken; + + /// @notice License token (ENCL) required for operator registration + IERC20 public licenseToken; + + /// @notice Registry contract for managing committee membership + ICiphernodeRegistry public registry; + + /// @notice Address authorized to perform slashing operations + address public slashingManager; + + /// @notice Address authorized to distribute rewards to operators + address public rewardDistributor; + + /// @notice Treasury address that receives slashed funds + address public slashedFundsTreasury; + + /// @notice Price per ticket in ticket token units + uint256 public ticketPrice; + + /// @notice Minimum license bond required for initial registration + uint256 public licenseRequiredBond; + + /// @notice Minimum number of tickets required to maintain active status + uint256 public minTicketBalance; + + /// @notice Time delay in seconds before exits can be claimed + uint64 public exitDelay; + + /// @notice Percentage (in basis points) of license bond that must remain bonded to stay active + /// @dev Default 8000 = 80%. Allows operators to unbond up to 20% while remaining active + uint256 public licenseActiveBps = 8_000; + + /// @notice Operator state data structure + /// @param licenseBond Amount of license tokens currently bonded + /// @param exitUnlocksAt Timestamp when pending exit can be claimed + /// @param registered Whether operator is registered in the protocol + /// @param exitRequested Whether operator has requested to exit + /// @param active Whether operator meets all requirements for active status + struct Operator { + uint256 licenseBond; + uint64 exitUnlocksAt; + bool registered; + bool exitRequested; + bool active; + } + + /// @notice Maps operator address to their state data + mapping(address operator => Operator data) internal operators; + + /// @notice Total slashed ticket balance available for treasury withdrawal + uint256 public slashedTicketBalance; + + /// @notice Total slashed license bond available for treasury withdrawal + uint256 public slashedLicenseBond; + + // ====================== + // Exit Queue library state + // ====================== + + /// @dev Internal state for managing exit queue of tickets and licenses + ExitQueueLib.ExitQueueState private _exits; + + // ====================== + // Modifiers + // ====================== + + /// @dev Restricts function access to only the slashing manager + modifier onlySlashingManager() { + if (msg.sender != slashingManager) revert Unauthorized(); + _; + } + + /// @dev Reverts if operator has an exit in progress that hasn't unlocked yet + /// @param operator Address of the operator to check + modifier noExitInProgress(address operator) { + Operator memory op = operators[operator]; + if (op.exitRequested && block.timestamp < op.exitUnlocksAt) + revert ExitInProgress(); + _; + } + + //////////////////////////////////////////////////////////// + // // + // Initialization // + // // + //////////////////////////////////////////////////////////// + + /// @notice Constructor that initializes the bonding registry + /// @param _owner Address that will own the contract + /// @param _ticketToken Ticket token contract for collateral + /// @param _licenseToken License token contract for bonding + /// @param _registry Ciphernode registry contract + /// @param _slashedFundsTreasury Address to receive slashed funds + /// @param _ticketPrice Initial price per ticket + /// @param _licenseRequiredBond Initial required license bond for registration + /// @param _minTicketBalance Initial minimum ticket balance for activation + /// @param _exitDelay Initial exit delay period in seconds + constructor( + address _owner, + EnclaveTicketToken _ticketToken, + IERC20 _licenseToken, + ICiphernodeRegistry _registry, + address _slashedFundsTreasury, + uint256 _ticketPrice, + uint256 _licenseRequiredBond, + uint256 _minTicketBalance, + uint64 _exitDelay + ) { + initialize( + _owner, + _ticketToken, + _licenseToken, + _registry, + _slashedFundsTreasury, + _ticketPrice, + _licenseRequiredBond, + _minTicketBalance, + _exitDelay + ); + } + + /// @notice Initializes the bonding registry contract + /// @dev Can only be called once due to initializer modifier + /// @param _owner Address that will own the contract + /// @param _ticketToken Ticket token contract for collateral + /// @param _licenseToken License token contract for bonding + /// @param _registry Ciphernode registry contract + /// @param _slashedFundsTreasury Address to receive slashed funds + /// @param _ticketPrice Initial price per ticket + /// @param _licenseRequiredBond Initial required license bond for registration + /// @param _minTicketBalance Initial minimum ticket balance for activation + /// @param _exitDelay Initial exit delay period in seconds + function initialize( + address _owner, + EnclaveTicketToken _ticketToken, + IERC20 _licenseToken, + ICiphernodeRegistry _registry, + address _slashedFundsTreasury, + uint256 _ticketPrice, + uint256 _licenseRequiredBond, + uint256 _minTicketBalance, + uint64 _exitDelay + ) public initializer { + __Ownable_init(msg.sender); + setTicketToken(_ticketToken); + setLicenseToken(_licenseToken); + setRegistry(_registry); + setSlashedFundsTreasury(_slashedFundsTreasury); + setTicketPrice(_ticketPrice); + setLicenseRequiredBond(_licenseRequiredBond); + setMinTicketBalance(_minTicketBalance); + setExitDelay(_exitDelay); + if (_owner != owner()) transferOwnership(_owner); + } + + // ====================== + // View Functions + // ====================== + + /// @inheritdoc IBondingRegistry + function getTicketBalance( + address operator + ) external view returns (uint256) { + return ticketToken.balanceOf(operator); + } + + /// @inheritdoc IBondingRegistry + function getLicenseBond(address operator) external view returns (uint256) { + return operators[operator].licenseBond; + } + + /// @inheritdoc IBondingRegistry + function availableTickets( + address operator + ) external view returns (uint256) { + return ticketToken.balanceOf(operator) / ticketPrice; + } + + /// @notice Get operator's ticket balance at a specific block + /// @dev Uses checkpoint mechanism from ticket token + /// @param operator Address of the operator + /// @param blockNumber Block number to query + /// @return Ticket balance at the specified block + function getTicketBalanceAtBlock( + address operator, + uint256 blockNumber + ) external view returns (uint256) { + return ticketToken.getPastVotes(operator, blockNumber); + } + + /// @notice Get operator's total pending exit amounts + /// @param operator Address of the operator + /// @return ticket Total pending ticket balance in exit queue + /// @return license Total pending license bond in exit queue + function pendingExits( + address operator + ) external view returns (uint256 ticket, uint256 license) { + return _exits.getPendingAmounts(operator); + } + + /// @notice Preview how much an operator can currently claim + /// @param operator Address of the operator + /// @return ticket Claimable ticket balance + /// @return license Claimable license bond + function previewClaimable( + address operator + ) external view returns (uint256 ticket, uint256 license) { + return _exits.previewClaimableAmounts(operator); + } + + /// @inheritdoc IBondingRegistry + function isLicensed(address operator) external view returns (bool) { + return operators[operator].licenseBond >= _minLicenseBond(); + } + + /// @inheritdoc IBondingRegistry + function isRegistered(address operator) external view returns (bool) { + return operators[operator].registered; + } + + /// @inheritdoc IBondingRegistry + function isActive(address operator) external view returns (bool) { + return operators[operator].active; + } + + /// @inheritdoc IBondingRegistry + function hasExitInProgress(address operator) external view returns (bool) { + Operator memory op = operators[operator]; + return op.exitRequested && block.timestamp < op.exitUnlocksAt; + } + + // ====================== + // Operator Functions + // ====================== + + /// @inheritdoc IBondingRegistry + function registerOperator() external noExitInProgress(msg.sender) { + // Clear previous exit request + if (operators[msg.sender].exitRequested) { + operators[msg.sender].exitRequested = false; + operators[msg.sender].exitUnlocksAt = 0; + } + + require( + !ISlashingManager(slashingManager).isBanned(msg.sender), + CiphernodeBanned() + ); + require(!operators[msg.sender].registered, AlreadyRegistered()); + require( + operators[msg.sender].licenseBond >= licenseRequiredBond, + NotLicensed() + ); + + operators[msg.sender].registered = true; + + // CiphernodeRegistry already emits an event when a ciphernode is added + registry.addCiphernode(msg.sender); + + _updateOperatorStatus(msg.sender); + } + + /// @inheritdoc IBondingRegistry + function deregisterOperator( + uint256[] calldata siblingNodes + ) external noExitInProgress(msg.sender) { + Operator storage op = operators[msg.sender]; + require(op.registered, NotRegistered()); + + op.registered = false; + op.exitRequested = true; + op.exitUnlocksAt = uint64(block.timestamp) + exitDelay; + + uint256 ticketOut = ticketToken.balanceOf(msg.sender); + uint256 licenseOut = op.licenseBond; + if (ticketOut != 0) { + ticketToken.burnTickets(msg.sender, ticketOut); + emit TicketBalanceUpdated( + msg.sender, + -int256(ticketOut), + 0, + REASON_WITHDRAW + ); + } + if (licenseOut != 0) { + op.licenseBond = 0; + emit LicenseBondUpdated( + msg.sender, + -int256(licenseOut), + 0, + REASON_UNBOND + ); + } + + if (ticketOut != 0 || licenseOut != 0) { + _exits.queueAssetsForExit( + msg.sender, + exitDelay, + ticketOut, + licenseOut + ); + } + + // CiphernodeRegistry already emits an event when a ciphernode is removed + registry.removeCiphernode(msg.sender, siblingNodes); + + emit CiphernodeDeregistrationRequested(msg.sender, op.exitUnlocksAt); + _updateOperatorStatus(msg.sender); + } + + /// @inheritdoc IBondingRegistry + function addTicketBalance( + uint256 amount + ) external noExitInProgress(msg.sender) { + require(amount != 0, ZeroAmount()); + require(operators[msg.sender].registered, NotRegistered()); + + ticketToken.depositFrom(msg.sender, msg.sender, amount); + + emit TicketBalanceUpdated( + msg.sender, + int256(amount), + ticketToken.balanceOf(msg.sender), + REASON_DEPOSIT + ); + + _updateOperatorStatus(msg.sender); + } + + /// @inheritdoc IBondingRegistry + function removeTicketBalance( + uint256 amount + ) external noExitInProgress(msg.sender) { + require(amount != 0, ZeroAmount()); + require(operators[msg.sender].registered, NotRegistered()); + require( + ticketToken.balanceOf(msg.sender) >= amount, + InsufficientBalance() + ); + + ticketToken.burnTickets(msg.sender, amount); + _exits.queueTicketsForExit(msg.sender, exitDelay, amount); + + emit TicketBalanceUpdated( + msg.sender, + -int256(amount), + ticketToken.balanceOf(msg.sender), + REASON_WITHDRAW + ); + + _updateOperatorStatus(msg.sender); + } + + /// @inheritdoc IBondingRegistry + function bondLicense(uint256 amount) external noExitInProgress(msg.sender) { + require(amount != 0, ZeroAmount()); + + uint256 balanceBefore = licenseToken.balanceOf(address(this)); + licenseToken.safeTransferFrom(msg.sender, address(this), amount); + uint256 actualReceived = licenseToken.balanceOf(address(this)) - + balanceBefore; + + operators[msg.sender].licenseBond += actualReceived; + + emit LicenseBondUpdated( + msg.sender, + int256(actualReceived), + operators[msg.sender].licenseBond, + REASON_BOND + ); + + _updateOperatorStatus(msg.sender); + } + + /// @inheritdoc IBondingRegistry + function unbondLicense( + uint256 amount + ) external noExitInProgress(msg.sender) { + require(amount != 0, ZeroAmount()); + require( + operators[msg.sender].licenseBond >= amount, + InsufficientBalance() + ); + + operators[msg.sender].licenseBond -= amount; + _exits.queueLicensesForExit(msg.sender, exitDelay, amount); + + emit LicenseBondUpdated( + msg.sender, + -int256(amount), + operators[msg.sender].licenseBond, + REASON_UNBOND + ); + + _updateOperatorStatus(msg.sender); + } + + // ====================== + // Claim Functions + // ====================== + + /// @inheritdoc IBondingRegistry + function claimExits( + uint256 maxTicketAmount, + uint256 maxLicenseAmount + ) external { + (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( + msg.sender, + maxTicketAmount, + maxLicenseAmount + ); + require(ticketClaim > 0 || licenseClaim > 0, ExitNotReady()); + + if (ticketClaim > 0) ticketToken.payout(msg.sender, ticketClaim); + if (licenseClaim > 0) + licenseToken.safeTransfer(msg.sender, licenseClaim); + } + + // ====================== + // Slashing Functions + // ====================== + + /// @inheritdoc IBondingRegistry + function slashTicketBalance( + address operator, + uint256 requestedSlashAmount, + bytes32 slashReason + ) external onlySlashingManager { + require(requestedSlashAmount != 0, ZeroAmount()); + + (uint256 pendingTicketBalance, ) = _exits.getPendingAmounts(operator); + uint256 activeBalance = ticketToken.balanceOf(operator); + uint256 totalAvailableBalance = activeBalance + pendingTicketBalance; + + uint256 actualSlashAmount = Math.min( + requestedSlashAmount, + totalAvailableBalance + ); + + if (actualSlashAmount == 0) { + return; + } + + // Slash from active balance first + uint256 slashedFromActiveBalance = Math.min( + actualSlashAmount, + activeBalance + ); + if (slashedFromActiveBalance > 0) { + ticketToken.burnTickets(operator, slashedFromActiveBalance); + } + + // Slash remaining amount from pending queue + uint256 remainingToSlash = actualSlashAmount - slashedFromActiveBalance; + if (remainingToSlash > 0) { + _exits.slashPendingAssets( + operator, + remainingToSlash, + 0, // licenseAmount + true + ); + } + + slashedTicketBalance += actualSlashAmount; + emit TicketBalanceUpdated( + operator, + -int256(actualSlashAmount), + ticketToken.balanceOf(operator), + slashReason + ); + + _updateOperatorStatus(operator); + } + + /// @inheritdoc IBondingRegistry + function slashLicenseBond( + address operator, + uint256 requestedSlashAmount, + bytes32 slashReason + ) external onlySlashingManager { + require(requestedSlashAmount != 0, ZeroAmount()); + + Operator storage operatorData = operators[operator]; + (, uint256 pendingLicenseBalance) = _exits.getPendingAmounts(operator); + uint256 totalAvailableBalance = operatorData.licenseBond + + pendingLicenseBalance; + uint256 actualSlashAmount = Math.min( + requestedSlashAmount, + totalAvailableBalance + ); + + if (actualSlashAmount == 0) return; + + // Slash from active balance first + uint256 slashedFromActiveBalance = Math.min( + actualSlashAmount, + operatorData.licenseBond + ); + if (slashedFromActiveBalance > 0) { + operatorData.licenseBond -= slashedFromActiveBalance; + } + + // Slash remaining amount from pending queue + uint256 remainingToSlash = actualSlashAmount - slashedFromActiveBalance; + if (remainingToSlash > 0) { + _exits.slashPendingAssets( + operator, + 0, // ticketAmount + remainingToSlash, + true + ); + } + + slashedLicenseBond += actualSlashAmount; + emit LicenseBondUpdated( + operator, + -int256(actualSlashAmount), + operatorData.licenseBond, + slashReason + ); + + _updateOperatorStatus(operator); + } + + // ====================== + // Reward Distribution Functions + // ====================== + + /// @inheritdoc IBondingRegistry + function distributeRewards( + IERC20 rewardToken, + address[] calldata recipients, + uint256[] calldata amounts + ) external { + require(msg.sender == rewardDistributor, OnlyRewardDistributor()); + require(recipients.length == amounts.length, ArrayLengthMismatch()); + + uint256 len = recipients.length; + for (uint256 i = 0; i < len; i++) { + if (amounts[i] > 0 && operators[recipients[i]].registered) { + rewardToken.safeTransferFrom( + rewardDistributor, + recipients[i], + amounts[i] + ); + } + } + } + + // ====================== + // Admin Functions + // ====================== + + /// @inheritdoc IBondingRegistry + function setTicketPrice(uint256 newTicketPrice) public onlyOwner { + require(newTicketPrice != 0, InvalidConfiguration()); + + uint256 oldValue = ticketPrice; + ticketPrice = newTicketPrice; + + emit ConfigurationUpdated("ticketPrice", oldValue, newTicketPrice); + } + + /// @inheritdoc IBondingRegistry + function setLicenseRequiredBond( + uint256 newLicenseRequiredBond + ) public onlyOwner { + require(newLicenseRequiredBond != 0, InvalidConfiguration()); + + uint256 oldValue = licenseRequiredBond; + licenseRequiredBond = newLicenseRequiredBond; + + emit ConfigurationUpdated( + "licenseRequiredBond", + oldValue, + newLicenseRequiredBond + ); + } + + /// @inheritdoc IBondingRegistry + function setLicenseActiveBps(uint256 newBps) public onlyOwner { + require(newBps > 0 && newBps <= 10_000, InvalidConfiguration()); + + uint256 oldValue = licenseActiveBps; + licenseActiveBps = newBps; + + emit ConfigurationUpdated("licenseActiveBps", oldValue, newBps); + } + + /// @inheritdoc IBondingRegistry + function setMinTicketBalance(uint256 newMinTicketBalance) public onlyOwner { + uint256 oldValue = minTicketBalance; + minTicketBalance = newMinTicketBalance; + + emit ConfigurationUpdated( + "minTicketBalance", + oldValue, + newMinTicketBalance + ); + } + + /// @inheritdoc IBondingRegistry + function setExitDelay(uint64 newExitDelay) public onlyOwner { + uint256 oldValue = uint256(exitDelay); + exitDelay = newExitDelay; + + emit ConfigurationUpdated("exitDelay", oldValue, uint256(newExitDelay)); + } + + /// @inheritdoc IBondingRegistry + function setSlashedFundsTreasury( + address newSlashedFundsTreasury + ) public onlyOwner { + require(newSlashedFundsTreasury != address(0), ZeroAddress()); + slashedFundsTreasury = newSlashedFundsTreasury; + } + + /// @inheritdoc IBondingRegistry + function setTicketToken( + EnclaveTicketToken newTicketToken + ) public onlyOwner { + ticketToken = newTicketToken; + } + + /// @inheritdoc IBondingRegistry + function setLicenseToken(IERC20 newLicenseToken) public onlyOwner { + licenseToken = newLicenseToken; + } + + /// @inheritdoc IBondingRegistry + function setRegistry(ICiphernodeRegistry newRegistry) public onlyOwner { + registry = newRegistry; + } + + /// @inheritdoc IBondingRegistry + function setSlashingManager(address newSlashingManager) public onlyOwner { + slashingManager = newSlashingManager; + } + + /// @notice Sets the reward distributor address + /// @dev Only callable by owner + /// @param newRewardDistributor Address of the reward distributor + function setRewardDistributor( + address newRewardDistributor + ) public onlyOwner { + rewardDistributor = newRewardDistributor; + } + + /// @inheritdoc IBondingRegistry + function withdrawSlashedFunds( + uint256 ticketAmount, + uint256 licenseAmount + ) public onlyOwner { + require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); + require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); + + if (ticketAmount > 0) { + slashedTicketBalance -= ticketAmount; + ticketToken.payout(slashedFundsTreasury, ticketAmount); + } + + if (licenseAmount > 0) { + slashedLicenseBond -= licenseAmount; + licenseToken.safeTransfer(slashedFundsTreasury, licenseAmount); + } + + emit SlashedFundsWithdrawn( + slashedFundsTreasury, + ticketAmount, + licenseAmount + ); + } + + // ====================== + // Internal Functions + // ====================== + + /// @dev Updates operator's active status based on current conditions + /// @dev Operator is active if: registered, has minimum license bond, and has minimum tickets + /// @param operator Address of the operator to update + function _updateOperatorStatus(address operator) internal { + Operator storage op = operators[operator]; + bool newActiveStatus = op.registered && + op.licenseBond >= _minLicenseBond() && + (ticketToken.balanceOf(operator) / ticketPrice >= minTicketBalance); + + if (op.active != newActiveStatus) { + op.active = newActiveStatus; + emit OperatorActivationChanged(operator, newActiveStatus); + } + } + + /// @dev Calculates the minimum license bond required to maintain active status + /// @return Minimum license bond (licenseRequiredBond * licenseActiveBps / 10000) + function _minLicenseBond() internal view returns (uint256) { + return (licenseRequiredBond * licenseActiveBps) / 10_000; + } +} diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 59724bf828..3102538bfe 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -6,7 +6,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -15,60 +15,190 @@ import { LeanIMTData } from "@zk-kit/lean-imt.sol/InternalLeanIMT.sol"; +/** + * @title CiphernodeRegistryOwnable + * @notice Ownable implementation of the ciphernode registry with IMT-based membership tracking + * @dev Manages ciphernode registration, committee selection, and integrates with bonding registry + */ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { using InternalLeanIMT for LeanIMTData; + //////////////////////////////////////////////////////////// + // // + // Events // + // // + //////////////////////////////////////////////////////////// + + /// @notice Emitted when the bonding registry address is set + /// @param bondingRegistry Address of the bonding registry contract + event BondingRegistrySet(address indexed bondingRegistry); + //////////////////////////////////////////////////////////// // // // Storage Variables // // // //////////////////////////////////////////////////////////// + /// @notice Address of the Enclave contract authorized to request committees address public enclave; + + /// @notice Address of the bonding registry for checking node eligibility + address public bondingRegistry; + + /// @notice Current number of registered ciphernodes uint256 public numCiphernodes; + + /// @notice Submission Window for an E3 Sortition. + /// @dev The submission window is the time period during which the ciphernodes can submit + /// their tickets to be a part of the committee. + uint256 public sortitionSubmissionWindow; + + /// @notice Incremental Merkle Tree (IMT) containing all registered ciphernodes LeanIMTData public ciphernodes; - mapping(uint256 e3Id => IRegistryFilter filter) public registryFilters; + /// @notice Maps E3 ID to the IMT root at the time of committee request mapping(uint256 e3Id => uint256 root) public roots; + + /// @notice Maps E3 ID to the hash of the committee's public key mapping(uint256 e3Id => bytes32 publicKeyHash) public publicKeyHashes; + /// @notice Maps E3 ID to its committee data + mapping(uint256 e3Id => Committee committee) internal committees; + //////////////////////////////////////////////////////////// // // // Errors // // // //////////////////////////////////////////////////////////// + /// @notice Committee has already been requested for this E3 error CommitteeAlreadyRequested(); + + /// @notice Committee has already been published for this E3 error CommitteeAlreadyPublished(); - error OnlyFilter(); + + /// @notice Committee has not been published yet for this E3 error CommitteeNotPublished(); + + /// @notice Committee has not been requested yet for this E3 + error CommitteeNotRequested(); + + /// @notice Committee Not Initialized or Finalized + error CommitteeNotInitializedOrFinalized(); + + /// @notice Submission Window has been closed for this E3 + error SubmissionWindowClosed(); + + /// @notice Submission deadline has been reached for this E3 + error SubmissionDeadlineReached(); + + /// @notice Committee has already been finalized for this E3 + error CommitteeAlreadyFinalized(); + + /// @notice Committee has not been finalized yet for this E3 + error CommitteeNotFinalized(); + + /// @notice Node has already submitted a ticket for this E3 + error NodeAlreadySubmitted(); + + /// @notice Node has not submitted a ticket for this E3 + error NodeNotSubmitted(); + + /// @notice Node is not eligible for this E3 + error NodeNotEligible(); + + /// @notice Ciphernode is not enabled in the registry + /// @param node Address of the ciphernode error CiphernodeNotEnabled(address node); + + /// @notice Caller is not the Enclave contract error OnlyEnclave(); + /// @notice Caller is not the bonding registry + error OnlyBondingRegistry(); + + /// @notice Caller is neither owner nor bonding registry + error NotOwnerOrBondingRegistry(); + + /// @notice Node is not bonded + /// @param node Address of the node + error NodeNotBonded(address node); + + /// @notice Address cannot be zero + error ZeroAddress(); + + /// @notice Bonding registry has not been set + error BondingRegistryNotSet(); + + /// @notice Invalid ticket number + error InvalidTicketNumber(); + + /// @notice Submission window not closed yet + error SubmissionWindowNotClosed(); + + /// @notice Threshold not met for this E3 + error ThresholdNotMet(); + + /// @notice Caller is not authorized + error Unauthorized(); + //////////////////////////////////////////////////////////// // // // Modifiers // // // //////////////////////////////////////////////////////////// + /// @dev Restricts function access to only the Enclave contract modifier onlyEnclave() { require(msg.sender == enclave, OnlyEnclave()); _; } + /// @dev Restricts function access to only the bonding registry + modifier onlyBondingRegistry() { + require(msg.sender == bondingRegistry, OnlyBondingRegistry()); + _; + } + + /// @dev Restricts function access to owner or bonding registry + modifier onlyOwnerOrBondingVault() { + require( + msg.sender == owner() || msg.sender == bondingRegistry, + NotOwnerOrBondingRegistry() + ); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // // // //////////////////////////////////////////////////////////// - constructor(address _owner, address _enclave) { - initialize(_owner, _enclave); + /// @notice Constructor that initializes the registry with owner and enclave + /// @param _owner Address that will own the contract + /// @param _enclave Address of the Enclave contract + /// @param _submissionWindow The submission window for the E3 sortition in seconds + constructor(address _owner, address _enclave, uint256 _submissionWindow) { + initialize(_owner, _enclave, _submissionWindow); } - function initialize(address _owner, address _enclave) public initializer { + /// @notice Initializes the registry contract + /// @dev Can only be called once due to initializer modifier + /// @param _owner Address that will own the contract + /// @param _enclave Address of the Enclave contract + /// @param _submissionWindow The submission window for the E3 sortition in seconds + function initialize( + address _owner, + address _enclave, + uint256 _submissionWindow + ) public initializer { + require(_owner != address(0), ZeroAddress()); + require(_enclave != address(0), ZeroAddress()); + __Ownable_init(msg.sender); setEnclave(_enclave); + setSortitionSubmissionWindow(_submissionWindow); if (_owner != owner()) transferOwnership(_owner); } @@ -78,36 +208,64 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @inheritdoc ICiphernodeRegistry function requestCommittee( uint256 e3Id, - address filter, + uint256 seed, uint32[2] calldata threshold ) external onlyEnclave returns (bool success) { - require( - registryFilters[e3Id] == IRegistryFilter(address(0)), - CommitteeAlreadyRequested() - ); - registryFilters[e3Id] = IRegistryFilter(filter); + Committee storage c = committees[e3Id]; + require(!c.initialized, CommitteeAlreadyRequested()); + + c.initialized = true; + c.finalized = false; + c.seed = seed; + c.requestBlock = block.number; + c.submissionDeadline = block.timestamp + sortitionSubmissionWindow; + c.threshold = threshold; roots[e3Id] = root(); - IRegistryFilter(filter).requestCommittee(e3Id, threshold); - emit CommitteeRequested(e3Id, filter, threshold); + emit CommitteeRequested( + e3Id, + seed, + threshold, + c.requestBlock, + c.submissionDeadline + ); success = true; } + /// @notice Publishes a committee for an E3 computation + /// @dev Only callable by owner. Verifies committee is finalized and matches provided nodes. + /// @param e3Id ID of the E3 computation + /// @param nodes Array of ciphernode addresses selected for the committee + /// @param publicKey Aggregated public key of the committee function publishCommittee( uint256 e3Id, - bytes calldata, + address[] calldata nodes, bytes calldata publicKey - ) external { - // only to be published by the filter - require(address(registryFilters[e3Id]) == msg.sender, OnlyFilter()); + ) external onlyOwner { + Committee storage c = committees[e3Id]; + + require(c.initialized, CommitteeNotRequested()); + require(c.finalized, CommitteeNotFinalized()); + require(c.publicKey == bytes32(0), CommitteeAlreadyPublished()); + require(nodes.length == c.committee.length, "Node count mismatch"); - publicKeyHashes[e3Id] = keccak256(publicKey); - emit CommitteePublished(e3Id, publicKey); + // TODO: Currently we trust the owner to publish the correct committee. + // TODO: Need a Proof that the public key is generated from the committee + bytes32 publicKeyHash = keccak256(publicKey); + c.publicKey = publicKeyHash; + publicKeyHashes[e3Id] = publicKeyHash; + emit CommitteePublished(e3Id, nodes, publicKey); } - function addCiphernode(address node) external onlyOwner { + /// @inheritdoc ICiphernodeRegistry + function addCiphernode(address node) external onlyOwnerOrBondingVault { + if (isEnabled(node)) { + return; + } + uint160 ciphernode = uint160(node); ciphernodes._insert(ciphernode); numCiphernodes++; @@ -119,10 +277,13 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { ); } + /// @inheritdoc ICiphernodeRegistry function removeCiphernode( address node, uint256[] calldata siblingNodes - ) external onlyOwner { + ) external onlyOwnerOrBondingVault { + require(isEnabled(node), CiphernodeNotEnabled(node)); + uint160 ciphernode = uint160(node); uint256 index = ciphernodes._indexOf(ciphernode); ciphernodes._remove(ciphernode, siblingNodes); @@ -130,23 +291,116 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit CiphernodeRemoved(node, index, numCiphernodes, ciphernodes.size); } + //////////////////////////////////////////////////////////// + // // + // Sortition Functions // + // // + //////////////////////////////////////////////////////////// + + /// @notice Submit a ticket for sortition + /// @dev Validates ticket against node's balance at request block and inserts into top-N + /// @param e3Id ID of the E3 computation + /// @param ticketNumber The ticket number to submit (1 to available tickets at snapshot) + function submitTicket(uint256 e3Id, uint256 ticketNumber) external { + Committee storage c = committees[e3Id]; + require(c.initialized, CommitteeNotRequested()); + require(!c.finalized, CommitteeAlreadyFinalized()); + require( + block.timestamp <= c.submissionDeadline, + SubmissionDeadlineReached() + ); + require(!c.submitted[msg.sender], NodeAlreadySubmitted()); + require(isCiphernodeEligible(msg.sender), NodeNotEligible()); + + // Validate node eligibility and ticket number + _validateNodeEligibility(msg.sender, ticketNumber, e3Id); + + // Compute score + uint256 score = _computeTicketScore( + msg.sender, + ticketNumber, + e3Id, + c.seed + ); + + // Store submission + c.submitted[msg.sender] = true; + c.scoreOf[msg.sender] = score; + + // Insert into top-N (ascending score) + _insertTopN(c, msg.sender, score); + + emit TicketSubmitted(e3Id, msg.sender, ticketNumber, score); + } + + /// @notice Finalize the committee after submission window closes + /// @dev Can be called by anyone after the deadline. Reverts if not enough nodes submitted. + /// @param e3Id ID of the E3 computation + function finalizeCommittee(uint256 e3Id) external { + Committee storage c = committees[e3Id]; + require(c.initialized, CommitteeNotRequested()); + require(!c.finalized, CommitteeAlreadyFinalized()); + require( + block.timestamp >= c.submissionDeadline, + SubmissionWindowNotClosed() + ); + // TODO: Handle what happens if the threshold is not met. + require(c.topNodes.length >= c.threshold[0], ThresholdNotMet()); + + c.finalized = true; + c.committee = c.topNodes; + + emit CommitteeFinalized(e3Id, c.topNodes); + } + + /// @notice Check if submission window is still open for an E3 + /// @param e3Id ID of the E3 computation + /// @return Whether the submission window is open + function isOpen(uint256 e3Id) public view returns (bool) { + Committee storage c = committees[e3Id]; + if (!c.initialized || c.finalized) return false; + return block.timestamp <= c.submissionDeadline; + } + //////////////////////////////////////////////////////////// // // // Set Functions // // // //////////////////////////////////////////////////////////// + /// @notice Sets the Enclave contract address + /// @dev Only callable by owner + /// @param _enclave Address of the Enclave contract function setEnclave(address _enclave) public onlyOwner { + require(_enclave != address(0), ZeroAddress()); enclave = _enclave; emit EnclaveSet(_enclave); } + /// @notice Sets the bonding registry contract address + /// @dev Only callable by owner + /// @param _bondingRegistry Address of the bonding registry contract + function setBondingRegistry(address _bondingRegistry) public onlyOwner { + require(_bondingRegistry != address(0), ZeroAddress()); + bondingRegistry = _bondingRegistry; + emit BondingRegistrySet(_bondingRegistry); + } + + /// @inheritdoc ICiphernodeRegistry + function setSortitionSubmissionWindow( + uint256 _sortitionSubmissionWindow + ) public onlyOwner { + sortitionSubmissionWindow = _sortitionSubmissionWindow; + emit SortitionSubmissionWindowSet(_sortitionSubmissionWindow); + } + //////////////////////////////////////////////////////////// // // // Get Functions // // // //////////////////////////////////////////////////////////// + /// @inheritdoc ICiphernodeRegistry function committeePublicKey( uint256 e3Id ) external view returns (bytes32 publicKeyHash) { @@ -154,27 +408,155 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(publicKeyHash != bytes32(0), CommitteeNotPublished()); } - function isCiphernodeEligible(address node) external view returns (bool) { - return isEnabled(node); + /// @inheritdoc ICiphernodeRegistry + function isCiphernodeEligible(address node) public view returns (bool) { + if (!isEnabled(node)) return false; + + require(bondingRegistry != address(0), BondingRegistryNotSet()); + return IBondingRegistry(bondingRegistry).isActive(node); } + /// @inheritdoc ICiphernodeRegistry function isEnabled(address node) public view returns (bool) { return ciphernodes._has(uint160(node)); } + /// @notice Returns the current root of the ciphernode IMT + /// @return Current IMT root function root() public view returns (uint256) { return (ciphernodes._root()); } + /// @notice Returns the IMT root at the time a committee was requested + /// @param e3Id ID of the E3 + /// @return IMT root at time of committee request function rootAt(uint256 e3Id) public view returns (uint256) { return roots[e3Id]; } - function getFilter(uint256 e3Id) public view returns (IRegistryFilter) { - return registryFilters[e3Id]; + /// @inheritdoc ICiphernodeRegistry + function getCommitteeNodes( + uint256 e3Id + ) public view returns (address[] memory nodes) { + Committee storage c = committees[e3Id]; + require(c.publicKey != bytes32(0), CommitteeNotPublished()); + nodes = c.committee; } + /// @notice Returns the current size of the ciphernode IMT + /// @return Size of the IMT function treeSize() public view returns (uint256) { return ciphernodes.size; } + + /// @notice Returns the address of the bonding registry + /// @return Address of the bonding registry contract + function getBondingRegistry() external view returns (address) { + return bondingRegistry; + } + + //////////////////////////////////////////////////////////// + // // + // Internal Functions // + // // + //////////////////////////////////////////////////////////// + + /// @notice Computes ticket score as keccak256(node || ticketNumber || e3Id || seed) + /// @param node Address of the ciphernode + /// @param ticketNumber The ticket number + /// @param e3Id ID of the E3 computation + /// @param seed Random seed for the E3 + /// @return score The computed score + function _computeTicketScore( + address node, + uint256 ticketNumber, + uint256 e3Id, + uint256 seed + ) internal pure returns (uint256) { + bytes32 hash = keccak256( + abi.encodePacked(node, ticketNumber, e3Id, seed) + ); + return uint256(hash); + } + + /// @notice Validates that a node is eligible to submit a ticket + /// @dev Uses snapshot of ticket balance at E3 request block for deterministic validation + /// @param node Address of the ciphernode + /// @param ticketNumber The ticket number being submitted + /// @param e3Id ID of the E3 computation + function _validateNodeEligibility( + address node, + uint256 ticketNumber, + uint256 e3Id + ) internal view { + require(ticketNumber > 0, InvalidTicketNumber()); + require(bondingRegistry != address(0), BondingRegistryNotSet()); + + Committee storage c = committees[e3Id]; + + uint256 ticketBalance = IBondingRegistry(bondingRegistry) + .getTicketBalanceAtBlock(node, c.requestBlock); + uint256 ticketPrice = IBondingRegistry(bondingRegistry).ticketPrice(); + + require(ticketPrice > 0, InvalidTicketNumber()); + uint256 availableTickets = ticketBalance / ticketPrice; + + require(availableTickets > 0, NodeNotEligible()); + require(ticketNumber <= availableTickets, InvalidTicketNumber()); + } + + /// @notice Inserts a node into the top-N sorted list by score + /// @dev Maintains sorted order (ascending by score) + /// @param c Committee storage reference + /// @param node Address of the ciphernode + /// @param score The computed score + function _insertTopN( + Committee storage c, + address node, + uint256 score + ) internal { + address[] storage topNodes = c.topNodes; + + // If list not full, insert in sorted order + if (topNodes.length < c.threshold[1]) { + _insertSorted(c, node, score); + return; + } + + // If list is full, only add if score is better than worst + uint256 worstScore = c.scoreOf[topNodes[topNodes.length - 1]]; + if (score < worstScore) { + topNodes.pop(); + _insertSorted(c, node, score); + } + } + + /// @notice Inserts a node at the correct sorted position (ascending by score) + /// @param c Committee storage reference + /// @param node Address of the ciphernode + /// @param score The computed score + function _insertSorted( + Committee storage c, + address node, + uint256 score + ) internal { + address[] storage topNodes = c.topNodes; + + // Find insertion position + uint256 insertPos = topNodes.length; + for (uint256 i = 0; i < topNodes.length; i++) { + uint256 existingScore = c.scoreOf[topNodes[i]]; + if (score < existingScore) { + insertPos = i; + break; + } + } + + // Insert at position + topNodes.push(address(0)); + for (uint256 i = topNodes.length - 1; i > insertPos; i--) { + topNodes[i] = topNodes[i - 1]; + } + topNodes[insertPos] = node; + } } diff --git a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol b/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol deleted file mode 100644 index 0c7d4d4c04..0000000000 --- a/packages/enclave-contracts/contracts/registry/NaiveRegistryFilter.sol +++ /dev/null @@ -1,124 +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. -pragma solidity >=0.8.27; - -import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract NaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { - struct Committee { - address[] nodes; - uint32[2] threshold; - bytes32 publicKey; - } - - //////////////////////////////////////////////////////////// - // // - // Storage Variables // - // // - //////////////////////////////////////////////////////////// - - address public registry; - - mapping(uint256 e3 => Committee committee) public committees; - - //////////////////////////////////////////////////////////// - // // - // Errors // - // // - //////////////////////////////////////////////////////////// - - error CommitteeAlreadyExists(); - error CommitteeAlreadyPublished(); - error CommitteeDoesNotExist(); - error CommitteeNotPublished(); - error CiphernodeNotEnabled(address ciphernode); - error OnlyRegistry(); - - //////////////////////////////////////////////////////////// - // // - // Modifiers // - // // - //////////////////////////////////////////////////////////// - - modifier onlyRegistry() { - require(msg.sender == registry, OnlyRegistry()); - _; - } - - //////////////////////////////////////////////////////////// - // // - // Initialization // - // // - //////////////////////////////////////////////////////////// - - constructor(address _owner, address _registry) { - initialize(_owner, _registry); - } - - function initialize(address _owner, address _registry) public initializer { - __Ownable_init(msg.sender); - setRegistry(_registry); - if (_owner != owner()) transferOwnership(_owner); - } - - //////////////////////////////////////////////////////////// - // // - // Core Entrypoints // - // // - //////////////////////////////////////////////////////////// - - function requestCommittee( - uint256 e3Id, - uint32[2] calldata threshold - ) external onlyRegistry returns (bool success) { - require(committees[e3Id].threshold[1] == 0, CommitteeAlreadyExists()); - committees[e3Id].threshold = threshold; - success = true; - } - - function publishCommittee( - uint256 e3Id, - address[] calldata nodes, - bytes calldata publicKey - ) external onlyOwner { - Committee storage committee = committees[e3Id]; - require(committee.publicKey == bytes32(0), CommitteeAlreadyPublished()); - committee.nodes = nodes; - committee.publicKey = keccak256(publicKey); - - ICiphernodeRegistry(registry).publishCommittee( - e3Id, - abi.encode(nodes), - publicKey - ); - } - - //////////////////////////////////////////////////////////// - // // - // Set Functions // - // // - //////////////////////////////////////////////////////////// - - function setRegistry(address _registry) public onlyOwner { - registry = _registry; - } - - //////////////////////////////////////////////////////////// - // // - // Get Functions // - // // - //////////////////////////////////////////////////////////// - - function getCommittee( - uint256 e3Id - ) external view returns (Committee memory) { - return committees[e3Id]; - } -} diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol new file mode 100644 index 0000000000..7001ef127d --- /dev/null +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -0,0 +1,367 @@ +// 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. + +pragma solidity >=0.8.27; + +import { + AccessControl +} from "@openzeppelin/contracts/access/AccessControl.sol"; +import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; +import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; + +/** + * @title SlashingManager + * @notice Implementation of slashing management with proposal, appeal, and execution workflows + * @dev Role-based access control for slashers, verifiers, and governance with configurable slash policies + */ +contract SlashingManager is ISlashingManager, AccessControl { + // ====================== + // Constants & Roles + // ====================== + + /// @notice Role identifier for accounts authorized to propose and execute slashes + bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); + + /// @notice Role identifier for accounts authorized to verify cryptographic proofs in slash proposals + bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE"); + + /// @notice Role identifier for governance accounts that can configure policies, resolve appeals, and manage bans + bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); + + // ====================== + // Storage + // ====================== + + /// @notice Reference to the bonding registry contract where slash penalties are executed + /// @dev Used to call slashTicketBalance() and slashLicenseBond() when executing slashes + IBondingRegistry public bondingRegistry; + + /// @notice Mapping from slash reason hash to its configured policy + /// @dev Stores penalty amounts, proof requirements, and appeal settings for each slash type + mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; + + /// @notice Internal storage for all slash proposals indexed by proposal ID + /// @dev Sequentially indexed starting from 0, accessed via getSlashProposal() + mapping(uint256 proposalId => SlashProposal proposal) internal _proposals; + + /// @notice Counter for total number of slash proposals ever created + /// @dev Also serves as the next proposal ID to be assigned + uint256 public totalProposals; + + /// @notice Mapping tracking which nodes are currently banned from the network + /// @dev Set to true when a node is banned (either via executeSlash or banNode), false when unbanned + mapping(address node => bool banned) public banned; + + // ====================== + // Modifiers + // ====================== + + /// @notice Restricts function access to accounts with SLASHER_ROLE + /// @dev Reverts with Unauthorized() if caller lacks the role + modifier onlySlasher() { + if (!hasRole(SLASHER_ROLE, msg.sender)) revert Unauthorized(); + _; + } + + /// @notice Restricts function access to accounts with VERIFIER_ROLE + /// @dev Reverts with Unauthorized() if caller lacks the role + modifier onlyVerifier() { + if (!hasRole(VERIFIER_ROLE, msg.sender)) revert Unauthorized(); + _; + } + + /// @notice Restricts function access to accounts with GOVERNANCE_ROLE + /// @dev Reverts with Unauthorized() if caller lacks the role + modifier onlyGovernance() { + if (!hasRole(GOVERNANCE_ROLE, msg.sender)) revert Unauthorized(); + _; + } + + // ====================== + // Constructor + // ====================== + + /** + * @notice Initializes the SlashingManager contract with admin and bonding registry + * @dev Sets up initial role assignments and bonding registry reference + * @param admin Address to receive DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE + * @param _bondingRegistry Address of the bonding registry contract for executing slashes + * Requirements: + * - admin must not be zero address + * - _bondingRegistry must not be zero address + */ + constructor(address admin, address _bondingRegistry) { + require(admin != address(0), ZeroAddress()); + require(_bondingRegistry != address(0), ZeroAddress()); + + bondingRegistry = IBondingRegistry(_bondingRegistry); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(GOVERNANCE_ROLE, admin); + } + + // ====================== + // View Functions + // ====================== + + /// @inheritdoc ISlashingManager + function getSlashPolicy( + bytes32 reason + ) external view returns (SlashPolicy memory) { + return slashPolicies[reason]; + } + + /// @inheritdoc ISlashingManager + function getSlashProposal( + uint256 proposalId + ) external view returns (SlashProposal memory) { + require(proposalId < totalProposals, InvalidProposal()); + return _proposals[proposalId]; + } + + /// @inheritdoc ISlashingManager + function isBanned(address node) external view returns (bool) { + return banned[node]; + } + + // ====================== + // Admin Functions + // ====================== + + /// @inheritdoc ISlashingManager + function setSlashPolicy( + bytes32 reason, + SlashPolicy calldata policy + ) external onlyRole(GOVERNANCE_ROLE) { + require(reason != bytes32(0), InvalidPolicy()); + require(policy.enabled, InvalidPolicy()); + require( + policy.ticketPenalty > 0 || policy.licensePenalty > 0, + InvalidPolicy() + ); + + if (policy.requiresProof) { + require(policy.proofVerifier != address(0), VerifierNotSet()); + // TODO: Should we allow appeal window for proof required? + require(policy.appealWindow == 0, InvalidPolicy()); + } else { + require(policy.appealWindow > 0, InvalidPolicy()); + } + + slashPolicies[reason] = policy; + emit SlashPolicyUpdated(reason, policy); + } + + /// @inheritdoc ISlashingManager + function setBondingRegistry( + address newBondingRegistry + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(newBondingRegistry != address(0), ZeroAddress()); + bondingRegistry = IBondingRegistry(newBondingRegistry); + } + + /// @inheritdoc ISlashingManager + function addSlasher(address slasher) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(slasher != address(0), ZeroAddress()); + _grantRole(SLASHER_ROLE, slasher); + } + + /// @inheritdoc ISlashingManager + function removeSlasher( + address slasher + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + _revokeRole(SLASHER_ROLE, slasher); + } + + /// @inheritdoc ISlashingManager + function addVerifier( + address verifier + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(verifier != address(0), ZeroAddress()); + _grantRole(VERIFIER_ROLE, verifier); + } + + /// @inheritdoc ISlashingManager + function removeVerifier( + address verifier + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + _revokeRole(VERIFIER_ROLE, verifier); + } + + // ====================== + // Slashing Functions + // ====================== + + /// @inheritdoc ISlashingManager + function proposeSlash( + address operator, + bytes32 reason, + bytes calldata proof + ) + external + // TODO: Do we need an onlySlasher modifier? + // Can anyone propose a slash? + onlySlasher + returns (uint256 proposalId) + { + require(operator != address(0), ZeroAddress()); + + SlashPolicy memory policy = slashPolicies[reason]; + require(policy.enabled, SlashReasonDisabled()); + + proposalId = totalProposals; + totalProposals = proposalId + 1; + + uint256 executableAt = block.timestamp + policy.appealWindow; + SlashProposal storage p = _proposals[proposalId]; + + p.operator = operator; + p.reason = reason; + p.ticketAmount = policy.ticketPenalty; + p.licenseAmount = policy.licensePenalty; + p.proposedAt = block.timestamp; + p.executableAt = executableAt; + p.proposer = msg.sender; + p.proofHash = keccak256(proof); + + if (policy.requiresProof) { + require(proof.length != 0, ProofRequired()); + bool ok = ISlashVerifier(policy.proofVerifier).verify( + proposalId, + proof + ); + require(ok, InvalidProof()); + p.proofVerified = true; + } + + emit SlashProposed( + proposalId, + operator, + reason, + policy.ticketPenalty, + policy.licensePenalty, + executableAt, + msg.sender + ); + } + + /// @inheritdoc ISlashingManager + function executeSlash(uint256 proposalId) external { + require(proposalId < totalProposals, InvalidProposal()); + SlashProposal storage p = _proposals[proposalId]; + + // Has already been executed? + require(!p.executed, AlreadyExecuted()); + p.executed = true; + + SlashPolicy memory policy = slashPolicies[p.reason]; + + if (policy.requiresProof) { + // Appeal window is 0 by policy validation, so we dont check for appeal gating + require(p.proofVerified, InvalidProof()); + } else { + // Evidence mode with appeals + require(block.timestamp >= p.executableAt, AppealWindowActive()); + if (p.appealed) { + require(p.resolved, AppealPending()); + require(!p.appealUpheld, AppealUpheld()); // approved = appeal upheld => cancel slash, return? + } + } + + if (p.ticketAmount > 0) { + bondingRegistry.slashTicketBalance( + p.operator, + p.ticketAmount, + p.reason + ); + } + + if (p.licenseAmount > 0) { + bondingRegistry.slashLicenseBond( + p.operator, + p.licenseAmount, + p.reason + ); + } + + if (policy.banNode) { + banned[p.operator] = true; + emit NodeBanUpdated(p.operator, true, p.reason, msg.sender); + } + + emit SlashExecuted( + proposalId, + p.operator, + p.reason, + p.ticketAmount, + p.licenseAmount, + p.executed + ); + } + + // ====================== + // Appeal Functions + // ====================== + + /// @inheritdoc ISlashingManager + function fileAppeal(uint256 proposalId, string calldata evidence) external { + require(proposalId < totalProposals, InvalidProposal()); + // TODO: Should we reject the appeal if the proposal has a cryptographic proof? + SlashProposal storage p = _proposals[proposalId]; + + // Only the accused can appeal + require(msg.sender == p.operator, Unauthorized()); + // Only in the window + require(block.timestamp < p.executableAt, AppealWindowExpired()); + // Only once + require(!p.appealed, AlreadyAppealed()); + + p.appealed = true; + + emit AppealFiled(proposalId, p.operator, p.reason, evidence); + } + + /// @inheritdoc ISlashingManager + function resolveAppeal( + uint256 proposalId, + bool appealUpheld, + string calldata resolution + ) external onlyGovernance { + require(proposalId < totalProposals, InvalidProposal()); + SlashProposal storage p = _proposals[proposalId]; + + require(p.appealed, InvalidProposal()); + require(!p.resolved, AlreadyResolved()); + + p.resolved = true; + p.appealUpheld = appealUpheld; // true => cancel slash, false => slash stands + + emit AppealResolved( + proposalId, + p.operator, + appealUpheld, + msg.sender, + resolution + ); + } + + // ====================== + // Ban Management + // ====================== + + /// @inheritdoc ISlashingManager + function updateBanStatus( + address node, + bool status, + bytes32 reason + ) external onlyGovernance { + require(node != address(0), ZeroAddress()); + + banned[node] = status; + emit NodeBanUpdated(node, status, reason, msg.sender); + } +} diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index a04f31d896..a3b7695b4c 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -10,22 +10,15 @@ import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { function requestCommittee( uint256, - address filter, + uint256, uint32[2] calldata ) external pure returns (bool success) { - if (filter == address(2)) { - success = false; - } else { - success = true; - } + success = true; } - // solhint-disable no-empty-blocks - function publishCommittee( - uint256, - bytes calldata, - bytes calldata - ) external {} // solhint-disable-line no-empty-blocks + function isEnabled(address) external pure returns (bool) { + return true; + } function committeePublicKey(uint256 e3Id) external pure returns (bytes32) { if (e3Id == type(uint256).max) { @@ -38,27 +31,74 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { function isCiphernodeEligible(address) external pure returns (bool) { return false; } + + // solhint-disable-next-line no-empty-blocks + function addCiphernode(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function removeCiphernode(address, uint256[] calldata) external pure {} + + function publishCommittee( + uint256, + address[] calldata, + bytes calldata + ) external pure {} // solhint-disable-line no-empty-blocks + + function getCommitteeNodes( + uint256 + ) external pure returns (address[] memory) { + address[] memory nodes = new address[](0); + return nodes; + } + + function root() external pure returns (uint256) { + return 0; + } + + function rootAt(uint256) external pure returns (uint256) { + return 0; + } + + function treeSize() external pure returns (uint256) { + return 0; + } + + function getBondingRegistry() external pure returns (address) { + return address(0); + } + + // solhint-disable-next-line no-empty-blocks + function setEnclave(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setBondingRegistry(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function submitTicket(uint256, uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function finalizeCommittee(uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setSortitionSubmissionWindow(uint256) external pure {} + + function isOpen(uint256) external pure returns (bool) { + return false; + } } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function requestCommittee( uint256, - address filter, + uint256, uint32[2] calldata ) external pure returns (bool success) { - if (filter == address(2)) { - success = false; - } else { - success = true; - } + success = true; } - // solhint-disable no-empty-blocks - function publishCommittee( - uint256, - bytes calldata, - bytes calldata - ) external {} // solhint-disable-line no-empty-blocks + function isEnabled(address) external pure returns (bool) { + return true; + } function committeePublicKey(uint256) external pure returns (bytes32) { return bytes32(0); @@ -67,4 +107,58 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function isCiphernodeEligible(address) external pure returns (bool) { return false; } + + // solhint-disable-next-line no-empty-blocks + function addCiphernode(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function removeCiphernode(address, uint256[] calldata) external pure {} + + function publishCommittee( + uint256, + address[] calldata, + bytes calldata + ) external pure {} // solhint-disable-line no-empty-blocks + + function getCommitteeNodes( + uint256 + ) external pure returns (address[] memory) { + address[] memory nodes = new address[](0); + return nodes; + } + + function root() external pure returns (uint256) { + return 0; + } + + function rootAt(uint256) external pure returns (uint256) { + return 0; + } + + function treeSize() external pure returns (uint256) { + return 0; + } + + function getBondingRegistry() external pure returns (address) { + return address(0); + } + + // solhint-disable-next-line no-empty-blocks + function setEnclave(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setBondingRegistry(address) external pure {} + + // solhint-disable-next-line no-empty-blocks + function setSortitionSubmissionWindow(uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function submitTicket(uint256, uint256) external pure {} + + // solhint-disable-next-line no-empty-blocks + function finalizeCommittee(uint256) external pure {} + + function isOpen(uint256) external pure returns (bool) { + return false; + } } diff --git a/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol b/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol deleted file mode 100644 index a8a6d390eb..0000000000 --- a/packages/enclave-contracts/contracts/test/MockRegistryFilter.sol +++ /dev/null @@ -1,117 +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. -pragma solidity >=0.8.27; - -import { IRegistryFilter } from "../interfaces/IRegistryFilter.sol"; -import { - OwnableUpgradeable -} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -interface IRegistry { - function publishCommittee( - uint256 e3Id, - address[] calldata ciphernodes, - bytes calldata publicKey - ) external; -} - -contract MockNaiveRegistryFilter is IRegistryFilter, OwnableUpgradeable { - struct Committee { - address[] nodes; - uint32[2] threshold; - bytes publicKey; - } - - //////////////////////////////////////////////////////////// - // // - // Storage Variables // - // // - //////////////////////////////////////////////////////////// - - address public registry; - - mapping(uint256 e3 => Committee committee) public committees; - - //////////////////////////////////////////////////////////// - // // - // Errors // - // // - //////////////////////////////////////////////////////////// - - error CommitteeAlreadyExists(); - error CommitteeAlreadyPublished(); - error CommitteeDoesNotExist(); - error CommitteeNotPublished(); - error OnlyRegistry(); - - //////////////////////////////////////////////////////////// - // // - // Modifiers // - // // - //////////////////////////////////////////////////////////// - - modifier onlyRegistry() { - require(msg.sender == registry, OnlyRegistry()); - _; - } - - //////////////////////////////////////////////////////////// - // // - // Initialization // - // // - //////////////////////////////////////////////////////////// - - constructor(address _owner, address _enclave) { - initialize(_owner, _enclave); - } - - function initialize(address _owner, address _registry) public initializer { - __Ownable_init(msg.sender); - setRegistry(_registry); - if (_owner != owner()) transferOwnership(_owner); - } - - //////////////////////////////////////////////////////////// - // // - // Core Entrypoints // - // // - //////////////////////////////////////////////////////////// - - function requestCommittee( - uint256 e3Id, - uint32[2] calldata threshold - ) external onlyRegistry returns (bool success) { - Committee storage committee = committees[e3Id]; - require(committee.threshold.length == 0, CommitteeAlreadyExists()); - committee.threshold = threshold; - success = true; - } - - function publishCommittee( - uint256 e3Id, - address[] memory nodes, - bytes memory publicKey - ) external onlyOwner { - Committee storage committee = committees[e3Id]; - require( - keccak256(committee.publicKey) == keccak256(hex""), - CommitteeAlreadyPublished() - ); - committee.nodes = nodes; - committee.publicKey = publicKey; - IRegistry(registry).publishCommittee(e3Id, nodes, publicKey); - } - - //////////////////////////////////////////////////////////// - // // - // Set Functions // - // // - //////////////////////////////////////////////////////////// - - function setRegistry(address _registry) public onlyOwner { - registry = _registry; - } -} diff --git a/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol new file mode 100644 index 0000000000..9a134afbea --- /dev/null +++ b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol @@ -0,0 +1,19 @@ +// 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. +pragma solidity >=0.8.27; + +import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; + +contract MockSlashingVerifier is ISlashVerifier { + function verify( + uint256, + bytes memory data + ) external pure returns (bool success) { + data; + + if (data.length > 0) success = true; + } +} diff --git a/packages/enclave-contracts/contracts/test/MockStableToken.sol b/packages/enclave-contracts/contracts/test/MockStableToken.sol new file mode 100644 index 0000000000..dcfc978518 --- /dev/null +++ b/packages/enclave-contracts/contracts/test/MockStableToken.sol @@ -0,0 +1,28 @@ +// 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. +pragma solidity >=0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockUSDC is ERC20, Ownable { + uint8 private _decimals; + + constructor( + uint256 initialSupply + ) ERC20("USD Coin", "USDC") Ownable(msg.sender) { + _decimals = 6; + _mint(msg.sender, initialSupply * 10 ** _decimals); + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } +} diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol new file mode 100644 index 0000000000..9d932d6a66 --- /dev/null +++ b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol @@ -0,0 +1,243 @@ +// 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. +pragma solidity ^0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + ERC20Wrapper +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; +import { + ERC20Permit +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { + ERC20Votes +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; + +/** + * @title EnclaveTicketToken (ETK) + * @notice Non-transferable, non-delegatable ERC20Votes wrapper over a stablecoin for operator staking + * @dev ERC20 wrapper token that represents staked stablecoins (e.g., USDC, DAI) used for operator + * bonding in the Enclave protocol. Implements voting power tracking through ERC20Votes but + * prevents transfers between users and manual delegation. Deposits automatically delegate to + * self to enable voting power tracking. Only the designated registry contract can mint + * (deposit) and burn (withdraw) tokens. + */ +contract EnclaveTicketToken is + ERC20, + ERC20Permit, + ERC20Votes, + Ownable, + ERC20Wrapper +{ + using SafeERC20 for IERC20; + // Custom errors + + /// @notice Thrown when a function is called by an address other than the registry + error NotRegistry(); + + /// @notice Thrown when attempting to transfer tokens between non-zero addresses + error TransferNotAllowed(); + + /// @notice Thrown when a zero address is provided where a valid address is required + error ZeroAddress(); + + /// @notice Thrown when attempting to manually delegate voting power + error DelegationLocked(); + + /// @notice Address of the registry contract authorized to mint, burn, and manage ticket tokens + /// @dev Only this contract can call restricted functions like depositFor, withdrawTo, burnTickets, and payout + address public registry; + + /// @notice Restricts function access to only the registry contract + /// @dev Reverts with NotRegistry if caller is not the registry address + modifier onlyRegistry() { + if (msg.sender != registry) revert NotRegistry(); + _; + } + + /** + * @notice Initializes the Enclave Ticket Token with name "Enclave Ticket Token" and symbol "ETK" + * @dev Sets up the token as an ERC20 wrapper around the provided base token (stablecoin). + * Initializes voting and permit functionality. The decimals will match the base token. + * @param baseToken The underlying ERC20 stablecoin to wrap (e.g., USDC, DAI) + * @param registry_ Address of the registry contract that will manage deposits and withdrawals + * @param initialOwner_ Address that will own the contract and can update the registry + */ + constructor( + IERC20 baseToken, + address registry_, + address initialOwner_ + ) + ERC20("Enclave Ticket Token", "ETK") + ERC20Permit("Enclave Ticket Token") + ERC20Wrapper(baseToken) + Ownable(initialOwner_) + { + setRegistry(registry_); + } + + /** + * @notice Updates the registry contract address + * @dev Only callable by the contract owner. The new registry address cannot be zero. + * This function grants the new registry exclusive rights to mint, burn, and manage tokens. + * @param newRegistry Address of the new registry contract (must not be zero address) + */ + function setRegistry(address newRegistry) public onlyOwner { + require(newRegistry != address(0), ZeroAddress()); + registry = newRegistry; + } + + /** + * @notice Deposits underlying tokens from the registry and mints ticket tokens to an operator + * @dev Only callable by the registry contract. Transfers underlying tokens from the registry to + * this contract and mints an equivalent amount of ticket tokens. Automatically delegates + * voting power to the operator on their first deposit to enable voting power tracking. + * @param operator Address to receive the minted ticket tokens + * @param amount Number of underlying tokens to deposit and ticket tokens to mint + * @return success True if the deposit and minting succeeded + */ + function depositFor( + address operator, + uint256 amount + ) public override onlyRegistry returns (bool success) { + success = super.depositFor(operator, amount); + + // Auto-delegate on first deposit to track voting power + if (delegates(operator) == address(0)) { + _delegate(operator, operator); + } + } + + /** + * @notice Deposits underlying tokens from a specified account and mints ticket tokens to another account + * @dev Only callable by the registry contract. Transfers underlying tokens from the 'from' address + * to this contract and mints ticket tokens to the 'to' address. Useful for scenarios where + * the source and destination differ. Automatically delegates voting power to recipient on + * their first deposit. + * @param from Address to transfer underlying tokens from (must have approved this contract) + * @param to Address to receive the minted ticket tokens + * @param amount Number of underlying tokens to deposit and ticket tokens to mint + * @return bool True if the deposit and minting succeeded + */ + function depositFrom( + address from, + address to, + uint256 amount + ) external onlyRegistry returns (bool) { + SafeERC20.safeTransferFrom( + IERC20(address(underlying())), + from, + address(this), + amount + ); + _mint(to, amount); + if (delegates(to) == address(0)) _delegate(to, to); + return true; + } + + /** + * @notice Burns ticket tokens from the registry and transfers underlying tokens to a receiver + * @dev Only callable by the registry contract. Burns ticket tokens from the registry's balance + * and transfers an equivalent amount of underlying tokens to the receiver address. Used + * when operators unstake their tokens. + * @param receiver Address to receive the underlying tokens + * @param amount Number of ticket tokens to burn and underlying tokens to transfer + * @return success True if the burn and transfer succeeded + */ + function withdrawTo( + address receiver, + uint256 amount + ) public override onlyRegistry returns (bool success) { + return super.withdrawTo(receiver, amount); + } + + /** + * @notice Burns ticket tokens from an operator's balance without transferring underlying tokens + * @dev Only callable by the registry contract. Used for slashing or penalizing operators where + * the underlying tokens should remain in the contract or be handled separately. Does not + * return underlying tokens to the operator. + * @param operator Address whose ticket tokens will be burned + * @param amount Number of ticket tokens to burn from the operator's balance + */ + function burnTickets( + address operator, + uint256 amount + ) external onlyRegistry { + _burn(operator, amount); + } + + /** + * @notice Transfer underlying tokens to an address without burning ticket tokens. + * @dev Only callable by the registry contract. + * @param to Address to payout to. + * @param amount Amount of ticket tokens to payout. + */ + function payout(address to, uint256 amount) external onlyRegistry { + SafeERC20.safeTransfer(IERC20(address(underlying())), to, amount); + } + + /** + * @dev Override ERC20Votes update hook to prevent transfers between users. + */ + function _update( + address from, + address to, + uint256 value + ) internal override(ERC20, ERC20Votes) { + if (from != address(0) && to != address(0)) { + revert TransferNotAllowed(); + } + super._update(from, to, value); + } + + /** + * @dev Prevent delegation of voting power. + */ + function delegate(address) public pure override { + revert DelegationLocked(); + } + + /** + * @dev Prevent delegation of voting power via signature. + */ + function delegateBySig( + address, + uint256, + uint256, + uint8, + bytes32, + bytes32 + ) public pure override { + revert DelegationLocked(); + } + + /** + * @dev Expose decimals from the underlying token. + */ + function decimals() + public + view + override(ERC20, ERC20Wrapper) + returns (uint8) + { + return super.decimals(); + } + + /** + * @dev Expose permit nonces via both ERC20Permit and OpenZeppelin Nonces. + */ + function nonces( + address owner + ) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol new file mode 100644 index 0000000000..70699d23ac --- /dev/null +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -0,0 +1,265 @@ +// 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. + +pragma solidity >=0.8.27; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { + ERC20Permit, + Nonces +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import { + ERC20Votes +} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { + AccessControl +} from "@openzeppelin/contracts/access/AccessControl.sol"; + +/** + * @title EnclaveToken + * @notice The governance and utility token for the Enclave protocol + * @dev ERC20 token with voting capabilities, permit functionality, and controlled minting. + * Implements transfer restrictions that can be toggled by the owner to control token + * transferability during early phases. Supports a maximum supply cap and role-based + * minting through the MINTER_ROLE. + */ +contract EnclaveToken is + ERC20, + ERC20Permit, + ERC20Votes, + Ownable, + AccessControl +{ + // Custom errors + + /// @notice Thrown when a zero address is provided where a valid address is required + error ZeroAddress(); + + /// @notice Thrown when attempting to mint zero tokens + error ZeroAmount(); + + /// @notice Thrown when minting would exceed the maximum token supply + error ExceedsTotalSupply(); + + /// @notice Thrown when array parameters have mismatched lengths + error ArrayLengthMismatch(); + + /// @notice Thrown when a transfer is attempted while restrictions are active and neither party is whitelisted + error TransferNotAllowed(); + + /// @notice Maximum supply of the token: 1.2 billion tokens with 18 decimals + /// @dev Hard cap on total token supply that cannot be exceeded through minting + uint256 public constant MAX_SUPPLY = 1_200_000_000e18; + + /// @notice Role identifier for accounts authorized to mint new tokens + /// @dev Keccak256 hash of "MINTER_ROLE" used in AccessControl + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @notice Tracks the cumulative amount of tokens minted since deployment + /// @dev Incremented with each mint operation to enforce MAX_SUPPLY cap + uint256 public totalMinted; + + /// @notice Mapping of addresses permitted to transfer tokens when restrictions are active + /// @dev When transfersRestricted is true, only whitelisted addresses can send or receive tokens + mapping(address account => bool allowed) public transferWhitelisted; + + /// @notice Indicates whether token transfers are currently restricted + /// @dev When true, only whitelisted addresses can transfer tokens + bool public transfersRestricted; + + /// @notice Emitted when tokens are minted as part of a named allocation + /// @param recipient Address receiving the minted tokens + /// @param amount Number of tokens minted (18 decimals) + /// @param allocation Description of the allocation for tracking purposes + event AllocationMinted( + address indexed recipient, + uint256 amount, + string allocation + ); + + /// @notice Emitted when the transfer restriction setting is changed + /// @param restricted New state of transfer restrictions (true = restricted, false = unrestricted) + event TransferRestrictionUpdated(bool restricted); + + /// @notice Emitted when an address is added to or removed from the transfer whitelist + /// @param account Address whose whitelist status changed + /// @param whitelisted New whitelist status (true = whitelisted, false = not whitelisted) + event TransferWhitelistUpdated(address indexed account, bool whitelisted); + + /** + * @notice Initializes the Enclave token with name "Enclave" and symbol "ENCL" + * @dev Sets up the token with voting and permit functionality. Grants admin and minter + * roles to the owner, enables transfer restrictions, and whitelists the owner. + * @param _owner Address that will own the contract and receive DEFAULT_ADMIN_ROLE and MINTER_ROLE + */ + constructor( + address _owner + ) ERC20("Enclave", "ENCL") ERC20Permit("Enclave") Ownable(_owner) { + // Grant the deployer all admin roles. + _grantRole(DEFAULT_ADMIN_ROLE, _owner); + _grantRole(MINTER_ROLE, _owner); + + // Initialise state variables. + transfersRestricted = true; + transferWhitelisted[_owner] = true; + + emit TransferRestrictionUpdated(true); + emit TransferWhitelistUpdated(_owner, true); + } + + /** + * @notice Mints a named allocation of tokens to a specified recipient + * @dev Only callable by accounts with MINTER_ROLE. Reverts if recipient is zero address, + * amount is zero, or minting would exceed MAX_SUPPLY. Updates totalMinted tracker. + * @param recipient Address to receive the minted tokens (cannot be zero address) + * @param amount Number of tokens to mint in wei (18 decimals, must be greater than zero) + * @param allocation Human-readable description of this allocation for tracking and auditing purposes + */ + function mintAllocation( + address recipient, + uint256 amount, + string memory allocation + ) external onlyRole(MINTER_ROLE) { + if (recipient == address(0)) revert ZeroAddress(); + if (amount == 0) revert ZeroAmount(); + // Ensure we do not exceed the total supply. + if (totalMinted + amount > MAX_SUPPLY) revert ExceedsTotalSupply(); + + _mint(recipient, amount); + totalMinted += amount; + emit AllocationMinted(recipient, amount, allocation); + } + + /** + * @notice Mints multiple named allocations to different recipients in a single transaction + * @dev Only callable by accounts with MINTER_ROLE. All arrays must have the same length. + * Reverts if any amount is zero or if the cumulative minting would exceed MAX_SUPPLY. + * Gas-efficient for distributing tokens to multiple addresses. + * @param recipients Array of addresses to receive minted tokens + * @param amounts Array of token amounts to mint (18 decimals, must match recipients length) + * @param allocations Array of allocation descriptions (must match recipients length) + */ + function batchMintAllocations( + address[] calldata recipients, + uint256[] calldata amounts, + string[] calldata allocations + ) external onlyRole(MINTER_ROLE) { + uint256 len = recipients.length; + if (amounts.length != len || allocations.length != len) + revert ArrayLengthMismatch(); + + uint256 minted = totalMinted; + + for (uint256 i = 0; i < len; i++) { + address recipient = recipients[i]; + uint256 amount = amounts[i]; + if (amount == 0) revert ZeroAmount(); + + if (amount > MAX_SUPPLY - minted) revert ExceedsTotalSupply(); + minted += amount; + + _mint(recipient, amount); + emit AllocationMinted(recipient, amount, allocations[i]); + } + + totalMinted = minted; + } + + /** + * @notice Enables or disables transfer restrictions for the token + * @dev Only callable by the contract owner. When restrictions are enabled, only whitelisted + * addresses can send or receive tokens. Useful for controlling token circulation during + * early phases before public trading. + * @param restricted True to enable restrictions, false to allow unrestricted transfers + */ + function setTransferRestriction(bool restricted) external onlyOwner { + transfersRestricted = restricted; + emit TransferRestrictionUpdated(restricted); + } + + /** + * @notice Toggles an account's transfer whitelist status between enabled and disabled + * @dev Only callable by the contract owner. Flips the current whitelist state for the given + * account. Whitelisted accounts can send and receive tokens even when transfer restrictions + * are active. + * @param account Address whose whitelist status will be toggled + */ + function toggleTransferWhitelist(address account) external onlyOwner { + bool newStatus = !transferWhitelisted[account]; + transferWhitelisted[account] = newStatus; + emit TransferWhitelistUpdated(account, newStatus); + } + + /** + * @notice Whitelists key protocol contracts to allow them to transfer tokens during restricted periods + * @dev Only callable by the contract owner. Convenience function for whitelisting multiple protocol + * contracts in a single transaction. Zero addresses are safely ignored. Typically used to whitelist + * contracts like bonding managers and vesting escrows that need to handle tokens on behalf of users. + * @param bondingManager Address of the BondingManager contract (zero address skipped) + * @param vestingEscrow Address of the VestingEscrow contract (zero address skipped) + */ + function whitelistContracts( + address bondingManager, + address vestingEscrow + ) external onlyOwner { + if (bondingManager != address(0)) { + transferWhitelisted[bondingManager] = true; + emit TransferWhitelistUpdated(bondingManager, true); + } + if (vestingEscrow != address(0)) { + transferWhitelisted[vestingEscrow] = true; + emit TransferWhitelistUpdated(vestingEscrow, true); + } + } + + /** + * @notice Internal hook that enforces transfer restrictions and updates voting power + * @dev Overrides ERC20 and ERC20Votes to add transfer restriction logic. Reverts if transfers + * are restricted and neither sender nor receiver is whitelisted. Minting (from == 0) and + * burning (to == 0) are always allowed regardless of restrictions. + * @param from Address sending tokens (zero address for minting) + * @param to Address receiving tokens (zero address for burning) + * @param value Amount of tokens being transferred + */ + function _update( + address from, + address to, + uint256 value + ) internal override(ERC20, ERC20Votes) { + // When transfers are restricted, only whitelisted addresses can send or receive. + if (from != address(0) && to != address(0) && transfersRestricted) { + if (!transferWhitelisted[from] && !transferWhitelisted[to]) + revert TransferNotAllowed(); + } + super._update(from, to, value); + } + + /** + * @notice Checks if this contract implements a given interface + * @dev Implements ERC165 interface detection via AccessControl + * @param interfaceId The interface identifier to check, as specified in ERC-165 + * @return bool True if the contract implements the interface, false otherwise + */ + function supportsInterface( + bytes4 interfaceId + ) public view override(AccessControl) returns (bool) { + return super.supportsInterface(interfaceId); + } + + /** + * @notice Returns the current nonce for an address, used for permit signatures + * @dev Resolves the override conflict between ERC20Permit and Nonces by calling the parent + * implementation. Nonces are incremented with each permit to prevent replay attacks. + * @param owner Address to query the nonce for + * @return uint256 The current nonce value for the given address + */ + function nonces( + address owner + ) public view override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 3a532226f8..cf2ffe77d8 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -1,55 +1,169 @@ { "sepolia": { "PoseidonT3": { - "blockNumber": 9473394, + "blockNumber": 9479393, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "Enclave": { "constructorArgs": { "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", - "registry": "0x0000000000000000000000000000000000000001", + "registry": "0xEC98074C1F64f820f897842d266e1091A0f47Ad8", + "bondingRegistry": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61", + "feeToken": "0xB58B762748c64f1a36B34012d1C52503617f4De0", "maxDuration": "2592000", "params": [ "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" ] }, - "blockNumber": 9461442, - "address": "0x966eC3eC1b46158f5e310B5a28aFc967C68De90a" + "blockNumber": 9479401, + "address": "0x787e6e63EF62ad4a40ea19D117bD7343ee8F1eC0" }, "CiphernodeRegistryOwnable": { "constructorArgs": { "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", - "enclaveAddress": "0x966eC3eC1b46158f5e310B5a28aFc967C68De90a" + "enclaveAddress": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 9461443, - "address": "0xC458d854cbC93D389CDaCB0FAAC8B25A59c3b74E" - }, - "NaiveRegistryFilter": { - "constructorArgs": { - "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", - "ciphernodeRegistryAddress": "0xC458d854cbC93D389CDaCB0FAAC8B25A59c3b74E" - }, - "blockNumber": 9461444, - "address": "0x0DC777566d1255B871dda65dD770cDb0E0280223" + "blockNumber": 9479399, + "address": "0xEC98074C1F64f820f897842d266e1091A0f47Ad8" }, "MockComputeProvider": { - "blockNumber": 9473395, - "address": "0x3528F39143034D0c84ebBAEc58407AD3775F26e1" + "blockNumber": 9479402, + "address": "0xf428dc63Ef8df4AdB3C202983Ea3b00B6985a03b" }, "MockDecryptionVerifier": { - "blockNumber": 9473396, - "address": "0x1F48f9939b9bb53635264c88E3F956Ac4F56bB58" + "blockNumber": 9479403, + "address": "0x09E79Fbcc3A5d9dc9f45920F7A8D15c7208Dd568" }, "MockInputValidator": { - "blockNumber": 9473397, - "address": "0x645da3bFA7699d2006a34b1B223ecc712FD5d600" + "blockNumber": 9479404, + "address": "0x885F5D69D2aA3ec34bb2e2598529213D308B12CF" }, "MockE3Program": { "constructorArgs": { - "mockInputValidator": "0x645da3bFA7699d2006a34b1B223ecc712FD5d600" + "mockInputValidator": "0x885F5D69D2aA3ec34bb2e2598529213D308B12CF" + }, + "blockNumber": 9479405, + "address": "0x5a196784e60A6A18b86Af7a9e564A969F6d2bC76" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 9479394, + "address": "0xB58B762748c64f1a36B34012d1C52503617f4De0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676" + }, + "blockNumber": 9479395, + "address": "0x9B3D470a2937c500632d382574EbD1D8FfCE3D63" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0xB58B762748c64f1a36B34012d1C52503617f4De0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676" + }, + "blockNumber": 9479396, + "address": "0xceb48a1bc9a3F160Ca687Da0a2A624F7a09a6158" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 9479397, + "address": "0x4752200Fc26747672d6CD3abe2e9072152D46f04" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", + "ticketToken": "0xceb48a1bc9a3F160Ca687Da0a2A624F7a09a6158", + "licenseToken": "0x9B3D470a2937c500632d382574EbD1D8FfCE3D63", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "blockNumber": 9479398, + "address": "0xD461aeA2c84D3fD7D4B0E83E0035446f5A741d61" + } + }, + "undefined": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + } + }, + "default": { + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 1, + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3" + } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" }, - "blockNumber": 9473398, - "address": "0x40fF23DE7F572524CE9125E8EfDAd2aF1faFd929" + "blockNumber": 9, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" } } -} +} \ No newline at end of file diff --git a/packages/enclave-contracts/hardhat.config.ts b/packages/enclave-contracts/hardhat.config.ts index b2310c79a2..186f504277 100644 --- a/packages/enclave-contracts/hardhat.config.ts +++ b/packages/enclave-contracts/hardhat.config.ts @@ -14,6 +14,8 @@ import type { HardhatUserConfig } from "hardhat/config"; import { ciphernodeAdd, + ciphernodeAdminAdd, + ciphernodeMintTokens, ciphernodeRemove, ciphernodeSiblings, } from "./tasks/ciphernode"; @@ -87,6 +89,8 @@ const config: HardhatUserConfig = { ], tasks: [ ciphernodeAdd, + ciphernodeAdminAdd, + ciphernodeMintTokens, ciphernodeRemove, ciphernodeSiblings, requestCommittee, @@ -104,6 +108,16 @@ const config: HardhatUserConfig = { type: "edr-simulated", chainType: "l1", }, + localhost: { + accounts: { + mnemonic, + }, + chainId: chainIds.hardhat, + url: "http://localhost:8545", + type: "http", + chainType: "l1", + timeout: 60000, + }, ganache: { accounts: { mnemonic, @@ -111,6 +125,7 @@ const config: HardhatUserConfig = { chainId: chainIds.ganache, url: "http://localhost:8545", type: "http", + timeout: 60000, }, arbitrum: getChainConfig( "arbitrum-mainnet", diff --git a/packages/enclave-contracts/ignition/modules/bondingRegistry.ts b/packages/enclave-contracts/ignition/modules/bondingRegistry.ts new file mode 100644 index 0000000000..de79a9ad2d --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/bondingRegistry.ts @@ -0,0 +1,34 @@ +// 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. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("BondingRegistry", (m) => { + const ticketToken = m.getParameter("ticketToken"); + const licenseToken = m.getParameter("licenseToken"); + const registry = m.getParameter("registry"); + const slashedFundsTreasury = m.getParameter("slashedFundsTreasury"); + const ticketPrice = m.getParameter("ticketPrice"); + const licenseRequiredBond = m.getParameter("licenseRequiredBond"); + const minTicketBalance = m.getParameter("minTicketBalance"); + const exitDelay = m.getParameter("exitDelay"); + const owner = m.getParameter("owner"); + + const bondingRegistry = m.contract("BondingRegistry", [ + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + ]); + + return { bondingRegistry }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts b/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts index e374e8f928..4819aa85d2 100644 --- a/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts +++ b/packages/enclave-contracts/ignition/modules/ciphernodeRegistry.ts @@ -10,12 +10,13 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("CiphernodeRegistry", (m) => { const enclaveAddress = m.getParameter("enclaveAddress"); const owner = m.getParameter("owner"); + const submissionWindow = m.getParameter("submissionWindow"); const poseidonT3 = m.library("PoseidonT3"); const cipherNodeRegistry = m.contract( "CiphernodeRegistryOwnable", - [owner, enclaveAddress], + [owner, enclaveAddress, submissionWindow], { libraries: { PoseidonT3: poseidonT3, diff --git a/packages/enclave-contracts/ignition/modules/enclave.ts b/packages/enclave-contracts/ignition/modules/enclave.ts index eec6c0da2f..03aa4f9777 100644 --- a/packages/enclave-contracts/ignition/modules/enclave.ts +++ b/packages/enclave-contracts/ignition/modules/enclave.ts @@ -12,12 +12,14 @@ export default buildModule("Enclave", (m) => { const owner = m.getParameter("owner"); const maxDuration = m.getParameter("maxDuration"); const registry = m.getParameter("registry"); + const bondingRegistry = m.getParameter("bondingRegistry"); + const feeToken = m.getParameter("feeToken"); const poseidonT3 = m.library("PoseidonT3"); const enclave = m.contract( "Enclave", - [owner, registry, maxDuration, [params]], + [owner, registry, bondingRegistry, feeToken, maxDuration, [params]], { libraries: { PoseidonT3: poseidonT3, diff --git a/packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts b/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts similarity index 58% rename from packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts rename to packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts index c1a9a8913c..de98a2a1d8 100644 --- a/packages/enclave-contracts/ignition/modules/naiveRegistryFilter.ts +++ b/packages/enclave-contracts/ignition/modules/enclaveTicketToken.ts @@ -7,14 +7,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; -export default buildModule("NaiveRegistryFilter", (m) => { - const ciphernodeRegistryAddress = m.getParameter("ciphernodeRegistryAddress"); +export default buildModule("EnclaveTicketToken", (m) => { + const baseToken = m.getParameter("baseToken"); + const registry = m.getParameter("registry"); const owner = m.getParameter("owner"); - const naiveRegistryFilter = m.contract("NaiveRegistryFilter", [ + const enclaveTicketToken = m.contract("EnclaveTicketToken", [ + baseToken, + registry, owner, - ciphernodeRegistryAddress, ]); - return { naiveRegistryFilter }; + return { enclaveTicketToken }; }) as any; diff --git a/packages/enclave-contracts/ignition/modules/enclaveToken.ts b/packages/enclave-contracts/ignition/modules/enclaveToken.ts new file mode 100644 index 0000000000..b8baaf9cfb --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/enclaveToken.ts @@ -0,0 +1,16 @@ +// 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. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("EnclaveToken", (m) => { + const owner = m.getParameter("owner"); + + const enclaveToken = m.contract("EnclaveToken", [owner]); + + return { enclaveToken }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts new file mode 100644 index 0000000000..5e9d0e1431 --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts @@ -0,0 +1,14 @@ +// 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. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("MockSlashingVerifier", (m) => { + const mockSlashingVerifier = m.contract("MockSlashingVerifier"); + + return { mockSlashingVerifier }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/mockStableToken.ts b/packages/enclave-contracts/ignition/modules/mockStableToken.ts new file mode 100644 index 0000000000..480389be16 --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/mockStableToken.ts @@ -0,0 +1,16 @@ +// 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. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("MockUSDC", (m) => { + const initialSupply = m.getParameter("initialSupply"); + + const mockUSDC = m.contract("MockUSDC", [initialSupply]); + + return { mockUSDC }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/slashingManager.ts b/packages/enclave-contracts/ignition/modules/slashingManager.ts new file mode 100644 index 0000000000..0d5919900e --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/slashingManager.ts @@ -0,0 +1,20 @@ +// 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. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("SlashingManager", (m) => { + const bondingRegistry = m.getParameter("bondingRegistry"); + const admin = m.getParameter("admin"); + + const slashingManager = m.contract("SlashingManager", [ + admin, + bondingRegistry, + ]); + + return { slashingManager }; +}) as any; diff --git a/packages/enclave-contracts/package.json b/packages/enclave-contracts/package.json index fe864789be..84c4a48406 100644 --- a/packages/enclave-contracts/package.json +++ b/packages/enclave-contracts/package.json @@ -149,6 +149,8 @@ "e3:activate": "hardhat e3:activate", "e3:enable": "hardhat e3:enable", "ciphernode:add": "hardhat ciphernode:add", + "ciphernode:admin-add": "hardhat ciphernode:admin-add", + "ciphernode:mint-tokens": "hardhat ciphernode:mint-tokens", "ciphernode:remove": "hardhat ciphernode:remove", "ciphernode:siblings": "hardhat ciphernode:siblings", "committee:new": "hardhat committee:new", @@ -159,7 +161,6 @@ "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", "test": "hardhat test mocha", "test:report-gas": "REPORT_GAS=true hardhat test mocha", - "test:registryFilter": "pnpm run test test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts", "test:enclave": "pnpm run test test/Enclave.spec.ts", "test:ciphernodeRegistry": "pnpm run test test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts", "prerelease": "pnpm clean && pnpm compile && pnpm typechain", diff --git a/packages/enclave-contracts/scripts/cleanIgnitionState.ts b/packages/enclave-contracts/scripts/cleanIgnitionState.ts new file mode 100644 index 0000000000..e9995ab99e --- /dev/null +++ b/packages/enclave-contracts/scripts/cleanIgnitionState.ts @@ -0,0 +1,51 @@ +// 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. +import fs from "fs"; +import path from "path"; + +/** + * Cleans deployment records for a specific network from deployed_contracts.json + * + * @param networkName - The network name (e.g., "localhost", "hardhat") + */ +export const cleanDeploymentRecords = (networkName: string): void => { + const deploymentsFile = path.join(process.cwd(), "deployed_contracts.json"); + + if (!fs.existsSync(deploymentsFile)) { + return; + } + + try { + const deployments = JSON.parse(fs.readFileSync(deploymentsFile, "utf8")); + + if (deployments[networkName]) { + console.log( + `Cleaning deployment records for network '${networkName}'...`, + ); + delete deployments[networkName]; + fs.writeFileSync(deploymentsFile, JSON.stringify(deployments, null, 2)); + console.log(`Cleaned deployment records for '${networkName}'`); + } + } catch (error) { + console.warn("Failed to clean deployment records:", error); + } +}; + +/** + * Automatically clean Ignition state and deployment records for localhost/hardhat networks before deployment. + * This prevents stale state issues when Anvil is restarted. + */ +export const autoCleanForLocalhost = async ( + networkName: string, +): Promise => { + const localNetworks = ["localhost", "hardhat", "anvil", "ganache"]; + if (localNetworks.includes(networkName)) { + console.log( + `Detected local network '${networkName}', auto-cleaning stale deployment state...`, + ); + cleanDeploymentRecords(networkName); + } +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts b/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts new file mode 100644 index 0000000000..0a44f35ce6 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/bondingRegistry.ts @@ -0,0 +1,137 @@ +// 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. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import { + BondingRegistry, + BondingRegistry__factory as BondingRegistryFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveBondingRegistry function + */ +export interface BondingRegistryArgs { + owner?: string; + ticketToken?: string; + licenseToken?: string; + registry?: string; + slashedFundsTreasury?: string; + ticketPrice?: string; + licenseRequiredBond?: string; + minTicketBalance?: number; + exitDelay?: number; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the BondingRegistry contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed BondingRegistry contract + */ +export const deployAndSaveBondingRegistry = async ({ + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + hre, +}: BondingRegistryArgs): Promise<{ + bondingRegistry: BondingRegistry; +}> => { + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("BondingRegistry", chain); + + if ( + !owner || + !ticketToken || + !licenseToken || + !registry || + !slashedFundsTreasury || + !ticketPrice || + !licenseRequiredBond || + minTicketBalance === undefined || + exitDelay === undefined || + (preDeployedArgs?.constructorArgs?.owner === owner && + preDeployedArgs?.constructorArgs?.ticketToken === ticketToken && + preDeployedArgs?.constructorArgs?.licenseToken === licenseToken && + preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.slashedFundsTreasury === + slashedFundsTreasury && + preDeployedArgs?.constructorArgs?.ticketPrice === ticketPrice && + preDeployedArgs?.constructorArgs?.licenseRequiredBond === + licenseRequiredBond && + preDeployedArgs?.constructorArgs?.minTicketBalance === + minTicketBalance.toString() && + preDeployedArgs?.constructorArgs?.exitDelay === exitDelay.toString()) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "BondingRegistry address not found, it must be deployed first", + ); + } + const bondingRegistryContract = BondingRegistryFactory.connect( + preDeployedArgs.address, + signer, + ); + return { bondingRegistry: bondingRegistryContract }; + } + + const bondingRegistryFactory = + await ethers.getContractFactory("BondingRegistry"); + + const bondingRegistry = await bondingRegistryFactory.deploy( + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance, + exitDelay, + ); + + await bondingRegistry.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const bondingRegistryAddress = await bondingRegistry.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + owner, + ticketToken, + licenseToken, + registry, + slashedFundsTreasury, + ticketPrice, + licenseRequiredBond, + minTicketBalance: minTicketBalance.toString(), + exitDelay: exitDelay.toString(), + }, + blockNumber, + address: bondingRegistryAddress, + }, + "BondingRegistry", + chain, + ); + + const bondingRegistryContract = BondingRegistryFactory.connect( + bondingRegistryAddress, + signer, + ); + + return { bondingRegistry: bondingRegistryContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts index 698c2a48ee..eaebb43a7d 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/ciphernodeRegistryOwnable.ts @@ -17,6 +17,7 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; export interface CiphernodeRegistryOwnableArgs { enclaveAddress?: string; owner?: string; + submissionWindow?: number; poseidonT3Address: string; hre: HardhatRuntimeEnvironment; } @@ -29,6 +30,7 @@ export interface CiphernodeRegistryOwnableArgs { export const deployAndSaveCiphernodeRegistryOwnable = async ({ enclaveAddress, owner, + submissionWindow, poseidonT3Address, hre, }: CiphernodeRegistryOwnableArgs): Promise<{ @@ -46,8 +48,11 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ if ( !enclaveAddress || !owner || + !submissionWindow || (preDeployedArgs?.constructorArgs?.enclaveAddress === enclaveAddress && - preDeployedArgs?.constructorArgs?.owner === owner) + preDeployedArgs?.constructorArgs?.owner === owner && + preDeployedArgs?.constructorArgs?.submissionWindow === + submissionWindow.toString()) ) { if (!preDeployedArgs?.address) { throw new Error( @@ -73,6 +78,7 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ const ciphernodeRegistry = await ciphernodeRegistryFactory.deploy( owner, enclaveAddress, + submissionWindow, ); await ciphernodeRegistry.waitForDeployment(); @@ -83,7 +89,11 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ storeDeploymentArgs( { - constructorArgs: { owner, enclaveAddress: enclaveAddress }, + constructorArgs: { + owner, + enclaveAddress: enclaveAddress, + submissionWindow: submissionWindow.toString(), + }, blockNumber, address: ciphernodeRegistryAddress, }, @@ -93,6 +103,7 @@ export const deployAndSaveCiphernodeRegistryOwnable = async ({ const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, + signer, ); return { ciphernodeRegistry: ciphernodeRegistryContract }; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index 24b7e7fbad..1b814b9e93 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -20,6 +20,8 @@ export interface EnclaveArgs { owner?: string; maxDuration?: string; registry?: string; + bondingRegistry?: string; + feeToken?: string; poseidonT3Address: string; hre: HardhatRuntimeEnvironment; } @@ -34,6 +36,8 @@ export const deployAndSaveEnclave = async ({ owner, maxDuration, registry, + bondingRegistry, + feeToken, poseidonT3Address, hre, }: EnclaveArgs): Promise<{ enclave: Enclave }> => { @@ -49,9 +53,13 @@ export const deployAndSaveEnclave = async ({ !owner || !maxDuration || !registry || + !bondingRegistry || + !feeToken || (preDeployedArgs?.constructorArgs?.owner === owner && preDeployedArgs?.constructorArgs?.maxDuration === maxDuration && preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.feeToken === feeToken && areArraysEqual( preDeployedArgs?.constructorArgs?.params as string[], params, @@ -79,6 +87,8 @@ export const deployAndSaveEnclave = async ({ const enclave = await enclaveFactory.deploy( owner, registry, + bondingRegistry, + feeToken, maxDuration, params, ); @@ -90,7 +100,14 @@ export const deployAndSaveEnclave = async ({ storeDeploymentArgs( { - constructorArgs: { owner, registry, maxDuration, params }, + constructorArgs: { + owner, + registry, + bondingRegistry, + feeToken, + maxDuration, + params, + }, blockNumber, address: enclaveAddress, }, diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts new file mode 100644 index 0000000000..36123181b8 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveTicketToken.ts @@ -0,0 +1,97 @@ +// 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. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import { + EnclaveTicketToken, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveEnclaveTicketToken function + */ +export interface EnclaveTicketTokenArgs { + baseToken?: string; + registry?: string; + owner?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the EnclaveTicketToken contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed EnclaveTicketToken contract + */ +export const deployAndSaveEnclaveTicketToken = async ({ + baseToken, + registry, + owner, + hre, +}: EnclaveTicketTokenArgs): Promise<{ + enclaveTicketToken: EnclaveTicketToken; +}> => { + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("EnclaveTicketToken", chain); + + if ( + !baseToken || + !registry || + !owner || + (preDeployedArgs?.constructorArgs?.baseToken === baseToken && + preDeployedArgs?.constructorArgs?.registry === registry && + preDeployedArgs?.constructorArgs?.owner === owner) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "EnclaveTicketToken address not found, it must be deployed first", + ); + } + const enclaveTicketTokenContract = EnclaveTicketTokenFactory.connect( + preDeployedArgs.address, + signer, + ); + return { enclaveTicketToken: enclaveTicketTokenContract }; + } + + const enclaveTicketTokenFactory = + await ethers.getContractFactory("EnclaveTicketToken"); + const enclaveTicketToken = await enclaveTicketTokenFactory.deploy( + baseToken, + registry, + owner, + ); + + await enclaveTicketToken.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const enclaveTicketTokenAddress = await enclaveTicketToken.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + baseToken, + registry, + owner, + }, + blockNumber, + address: enclaveTicketTokenAddress, + }, + "EnclaveTicketToken", + chain, + ); + + const enclaveTicketTokenContract = EnclaveTicketTokenFactory.connect( + enclaveTicketTokenAddress, + signer, + ); + + return { enclaveTicketToken: enclaveTicketTokenContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts new file mode 100644 index 0000000000..d3ef2919d6 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/enclaveToken.ts @@ -0,0 +1,109 @@ +// 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. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import { + EnclaveToken, + EnclaveToken__factory as EnclaveTokenFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveEnclaveToken function + */ +export interface EnclaveTokenArgs { + owner?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Disables transfer restrictions for local development + */ +async function disableTransferRestrictionsForLocal( + contract: EnclaveToken, + chain: string, +): Promise { + if (chain !== "localhost" && chain !== "hardhat") { + return; + } + console.log("Disabling transfer restrictions for chain", chain); + console.log("Contract address", await contract.getAddress()); + + try { + const isRestricted = await contract.transfersRestricted(); + if (isRestricted) { + const tx = await contract.setTransferRestriction(false); + await tx.wait(); + console.log("Transfer restrictions disabled for local development"); + } + } catch (error) { + console.warn("Failed to disable transfer restrictions:", error); + } +} + +/** + * Deploys the EnclaveToken contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed EnclaveToken contract + */ +export const deployAndSaveEnclaveToken = async ({ + owner, + hre, +}: EnclaveTokenArgs): Promise<{ + enclaveToken: EnclaveToken; +}> => { + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("EnclaveToken", chain); + + if (!owner || preDeployedArgs?.constructorArgs?.owner === owner) { + if (!preDeployedArgs?.address) { + throw new Error( + "EnclaveToken address not found, it must be deployed first", + ); + } + const enclaveTokenContract = EnclaveTokenFactory.connect( + preDeployedArgs.address, + signer, + ); + + await disableTransferRestrictionsForLocal(enclaveTokenContract, chain); + + return { enclaveToken: enclaveTokenContract }; + } + + const enclaveTokenFactory = await ethers.getContractFactory("EnclaveToken"); + const enclaveToken = await enclaveTokenFactory.deploy(owner); + + await enclaveToken.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const enclaveTokenAddress = await enclaveToken.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + owner, + }, + blockNumber, + address: enclaveTokenAddress, + }, + "EnclaveToken", + chain, + ); + + const enclaveTokenContract = EnclaveTokenFactory.connect( + enclaveTokenAddress, + signer, + ); + + await disableTransferRestrictionsForLocal(enclaveTokenContract, chain); + + return { enclaveToken: enclaveTokenContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts b/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts new file mode 100644 index 0000000000..539599a433 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/mockStableToken.ts @@ -0,0 +1,78 @@ +// 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. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import { MockUSDC, MockUSDC__factory as MockUSDCFactory } from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveMockStableToken function + */ +export interface MockStableTokenArgs { + initialSupply?: number; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the MockStableToken contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed MockStableToken contract + */ +export const deployAndSaveMockStableToken = async ({ + initialSupply, + hre, +}: MockStableTokenArgs): Promise<{ + mockStableToken: MockUSDC; +}> => { + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("MockUSDC", chain); + + if ( + initialSupply === undefined || + preDeployedArgs?.constructorArgs?.initialSupply === + initialSupply?.toString() + ) { + if (!preDeployedArgs?.address) { + throw new Error("MockUSDC address not found, it must be deployed first"); + } + const mockStableTokenContract = MockUSDCFactory.connect( + preDeployedArgs.address, + signer, + ); + return { mockStableToken: mockStableTokenContract }; + } + + const mockStableTokenFactory = await ethers.getContractFactory("MockUSDC"); + const mockStableToken = await mockStableTokenFactory.deploy(initialSupply); + + await mockStableToken.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const mockStableTokenAddress = await mockStableToken.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + initialSupply: initialSupply?.toString(), + }, + blockNumber, + address: mockStableTokenAddress, + }, + "MockUSDC", + chain, + ); + + const mockStableTokenContract = MockUSDCFactory.connect( + mockStableTokenAddress, + signer, + ); + + return { mockStableToken: mockStableTokenContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts b/packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts deleted file mode 100644 index d68ac71857..0000000000 --- a/packages/enclave-contracts/scripts/deployAndSave/naiveRegistryFilter.ts +++ /dev/null @@ -1,85 +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. -import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; - -import { - NaiveRegistryFilter, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, -} from "../../types"; -import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; - -export interface NaiveRegistryFilterArgs { - ciphernodeRegistryAddress?: string; - owner?: string; - hre: HardhatRuntimeEnvironment; -} - -export const deployAndSaveNaiveRegistryFilter = async ({ - ciphernodeRegistryAddress, - owner, - hre, -}: NaiveRegistryFilterArgs): Promise<{ - naiveRegistryFilter: NaiveRegistryFilter; -}> => { - const { ethers } = await hre.network.connect(); - const [signer] = await ethers.getSigners(); - const chain = hre.globalOptions.network; - - const preDeployedArgs = readDeploymentArgs("NaiveRegistryFilter", chain); - if ( - !ciphernodeRegistryAddress || - !owner || - (preDeployedArgs?.constructorArgs?.ciphernodeRegistryAddress === - ciphernodeRegistryAddress && - preDeployedArgs?.constructorArgs?.owner === owner) - ) { - if (!preDeployedArgs?.address) { - throw new Error( - "NaiveRegistryFilter address not found, it must be deployed first", - ); - } - const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( - preDeployedArgs.address, - signer, - ); - return { naiveRegistryFilter: naiveRegistryFilterContract }; - } - - const naiveRegistryFilterFactory = await ethers.getContractFactory( - "NaiveRegistryFilter", - ); - - const naiveRegistryFilter = await naiveRegistryFilterFactory.deploy( - owner, - ciphernodeRegistryAddress, - ); - - await naiveRegistryFilter.waitForDeployment(); - - const naiveRegistryFilterAddress = await naiveRegistryFilter.getAddress(); - - const blockNumber = await ethers.provider.getBlockNumber(); - - storeDeploymentArgs( - { - constructorArgs: { - owner, - ciphernodeRegistryAddress: ciphernodeRegistryAddress, - }, - blockNumber, - address: naiveRegistryFilterAddress, - }, - "NaiveRegistryFilter", - chain, - ); - - const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( - naiveRegistryFilterAddress, - signer, - ); - - return { naiveRegistryFilter: naiveRegistryFilterContract }; -}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts new file mode 100644 index 0000000000..47b7e179e6 --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts @@ -0,0 +1,91 @@ +// 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. +import type { HardhatRuntimeEnvironment } from "hardhat/types/hre"; + +import { + SlashingManager, + SlashingManager__factory as SlashingManagerFactory, +} from "../../types"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveSlashingManager function + */ +export interface SlashingManagerArgs { + admin?: string; + bondingRegistry?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the SlashingManager contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed SlashingManager contract + */ +export const deployAndSaveSlashingManager = async ({ + admin, + bondingRegistry, + hre, +}: SlashingManagerArgs): Promise<{ + slashingManager: SlashingManager; +}> => { + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = (await signer.provider?.getNetwork())?.name ?? "localhost"; + + const preDeployedArgs = readDeploymentArgs("SlashingManager", chain); + + if ( + !admin || + !bondingRegistry || + (preDeployedArgs?.constructorArgs?.admin === admin && + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "SlashingManager address not found, it must be deployed first", + ); + } + const slashingManagerContract = SlashingManagerFactory.connect( + preDeployedArgs.address, + signer, + ); + return { slashingManager: slashingManagerContract }; + } + + const slashingManagerFactory = + await ethers.getContractFactory("SlashingManager"); + const slashingManager = await slashingManagerFactory.deploy( + admin, + bondingRegistry, + ); + + await slashingManager.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + + const slashingManagerAddress = await slashingManager.getAddress(); + + storeDeploymentArgs( + { + constructorArgs: { + admin, + bondingRegistry, + }, + blockNumber, + address: slashingManagerAddress, + }, + "SlashingManager", + chain, + ); + + const slashingManagerContract = SlashingManagerFactory.connect( + slashingManagerAddress, + signer, + ); + + return { slashingManager: slashingManagerContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index a192c95699..d9461ae78a 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -5,10 +5,15 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import hre from "hardhat"; +import { autoCleanForLocalhost } from "./cleanIgnitionState"; +import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; import { deployAndSaveEnclave } from "./deployAndSave/enclave"; -import { deployAndSaveNaiveRegistryFilter } from "./deployAndSave/naiveRegistryFilter"; +import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; +import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; +import { deployAndSaveMockStableToken } from "./deployAndSave/mockStableToken"; import { deployAndSavePoseidonT3 } from "./deployAndSave/poseidonT3"; +import { deployAndSaveSlashingManager } from "./deployAndSave/slashingManager"; import { deployMocks } from "./deployMocks"; /** @@ -17,6 +22,10 @@ import { deployMocks } from "./deployMocks"; export const deployEnclave = async (withMocks?: boolean) => { const { ethers } = await hre.network.connect(); + // Auto-clean state for local networks to prevent stale state issues + const networkName = hre.globalOptions.network ?? "localhost"; + await autoCleanForLocalhost(networkName); + const [owner] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); @@ -31,66 +40,130 @@ export const deployEnclave = async (withMocks?: boolean) => { ); const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; + const SORTITION_SUBMISSION_WINDOW = 3; const addressOne = "0x0000000000000000000000000000000000000001"; const poseidonT3 = await deployAndSavePoseidonT3({ hre }); - console.log("Deploying Enclave"); - const { enclave } = await deployAndSaveEnclave({ - params: [encoded], + const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; + let feeTokenAddress: string; + + if (shouldDeployMocks) { + console.log("Deploying mock Fee token..."); + const { mockStableToken } = await deployAndSaveMockStableToken({ + initialSupply: 1000000, + hre, + }); + feeTokenAddress = await mockStableToken.getAddress(); + console.log("MockFeeToken deployed to:", feeTokenAddress); + } else { + throw new Error( + "Fee token address must be provided for production deployment", + ); + } + + console.log("Deploying ENCL token..."); + const { enclaveToken } = await deployAndSaveEnclaveToken({ owner: ownerAddress, - maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), + hre, + }); + const enclaveTokenAddress = await enclaveToken.getAddress(); + console.log("EnclaveToken deployed to:", enclaveTokenAddress); + + console.log("Deploying EnclaveTicketToken..."); + const { enclaveTicketToken } = await deployAndSaveEnclaveTicketToken({ + baseToken: feeTokenAddress, registry: addressOne, - poseidonT3Address: poseidonT3, + owner: ownerAddress, hre, }); + const enclaveTicketTokenAddress = await enclaveTicketToken.getAddress(); + console.log("EnclaveTicketToken deployed to:", enclaveTicketTokenAddress); - const enclaveAddress = await enclave.getAddress(); + console.log("Deploying SlashingManager..."); + const { slashingManager } = await deployAndSaveSlashingManager({ + admin: ownerAddress, + bondingRegistry: addressOne, + hre, + }); + const slashingManagerAddress = await slashingManager.getAddress(); + console.log("SlashingManager deployed to:", slashingManagerAddress); - console.log("Deploying CiphernodeRegistry"); - const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ - enclaveAddress: enclaveAddress, + console.log("Deploying BondingRegistry..."); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ owner: ownerAddress, - poseidonT3Address: poseidonT3, + ticketToken: enclaveTicketTokenAddress, + licenseToken: enclaveTokenAddress, + registry: addressOne, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseUnits("10", 6).toString(), + licenseRequiredBond: ethers.parseEther("100").toString(), + minTicketBalance: 1, + exitDelay: 7 * 24 * 60 * 60, hre, }); + const bondingRegistryAddress = await bondingRegistry.getAddress(); + console.log("BondingRegistry deployed to:", bondingRegistryAddress); + console.log("Deploying CiphernodeRegistry..."); + const { ciphernodeRegistry } = await deployAndSaveCiphernodeRegistryOwnable({ + poseidonT3Address: poseidonT3, + enclaveAddress: addressOne, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + hre, + }); const ciphernodeRegistryAddress = await ciphernodeRegistry.getAddress(); + console.log("CiphernodeRegistry deployed to:", ciphernodeRegistryAddress); - console.log("Deploying NaiveRegistryFilter"); - const { naiveRegistryFilter } = await deployAndSaveNaiveRegistryFilter({ - ciphernodeRegistryAddress: ciphernodeRegistryAddress, + console.log("Deploying Enclave..."); + const { enclave } = await deployAndSaveEnclave({ + params: [encoded], owner: ownerAddress, + maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), + registry: ciphernodeRegistryAddress, + bondingRegistry: bondingRegistryAddress, + feeToken: feeTokenAddress, + poseidonT3Address: poseidonT3, hre, }); + const enclaveAddress = await enclave.getAddress(); + console.log("Enclave deployed to:", enclaveAddress); - const naiveRegistryFilterAddress = await naiveRegistryFilter.getAddress(); + /////////////////////////////////////////// + // Configure cross-contract dependencies + /////////////////////////////////////////// - const registryAddress = await enclave.ciphernodeRegistry(); + console.log("Configuring cross-contract dependencies..."); - console.log("Setting CiphernodeRegistry in Enclave"); - if (registryAddress === ciphernodeRegistryAddress) { - console.log(`Enclave contract already has registry`); - } else { - const tx = await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); - await tx.wait(); + console.log("Setting Enclave address in CiphernodeRegistry..."); + await ciphernodeRegistry.setEnclave(enclaveAddress); - console.log(`Enclave contract updated with registry`); - } + console.log("Setting BondingRegistry address in CiphernodeRegistry..."); + await ciphernodeRegistry.setBondingRegistry(bondingRegistryAddress); - console.log(` - Deployments: - ---------------------------------------------------------------------- - Enclave: ${enclaveAddress} - CiphernodeRegistry: ${ciphernodeRegistryAddress} - NaiveRegistryFilter: ${naiveRegistryFilterAddress} - `); - - // Deploy mocks only if specified - const shouldDeployMocks = process.env.DEPLOY_MOCKS === "true" || withMocks; + console.log("Setting Submission Window in CiphernodeRegistry..."); + console.log("SORTITION_SUBMISSION_WINDOW:", SORTITION_SUBMISSION_WINDOW); + await ciphernodeRegistry.setSortitionSubmissionWindow( + SORTITION_SUBMISSION_WINDOW, + ); + + console.log("Setting BondingRegistry address in EnclaveTicketToken..."); + await enclaveTicketToken.setRegistry(bondingRegistryAddress); + + console.log("Setting CiphernodeRegistry address in BondingRegistry..."); + await bondingRegistry.setRegistry(ciphernodeRegistryAddress); + + console.log("Setting BondingRegistry address in SlashingManager..."); + await slashingManager.setBondingRegistry(bondingRegistryAddress); + + console.log("Setting SlashingManager address in BondingRegistry..."); + await bondingRegistry.setSlashingManager(slashingManagerAddress); + + console.log("Setting Enclave as reward distributor in BondingRegistry..."); + await bondingRegistry.setRewardDistributor(enclaveAddress); if (shouldDeployMocks) { - console.log("Deploying Mocks"); const { decryptionVerifierAddress, e3ProgramAddress } = await deployMocks(); const encryptionSchemeId = ethers.keccak256( @@ -118,4 +191,18 @@ export const deployEnclave = async (withMocks?: boolean) => { await tx.wait(); console.log(`Successfully enabled E3 Program in Enclave contract`); } + + console.log(` + ============================================ + Deployment Complete! + ============================================ + MockFeeToken: ${feeTokenAddress} + EnclaveToken (ENCL): ${enclaveTokenAddress} + EnclaveTicketToken: ${enclaveTicketTokenAddress} + SlashingManager: ${slashingManagerAddress} + BondingRegistry: ${bondingRegistryAddress} + CiphernodeRegistry: ${ciphernodeRegistryAddress} + Enclave: ${enclaveAddress} + ============================================ + `); }; diff --git a/packages/enclave-contracts/scripts/index.ts b/packages/enclave-contracts/scripts/index.ts index b76aa75654..c74699be1d 100644 --- a/packages/enclave-contracts/scripts/index.ts +++ b/packages/enclave-contracts/scripts/index.ts @@ -7,9 +7,14 @@ export * from "./deployEnclave"; export * from "./deployMocks"; export * from "./utils"; +export * from "./cleanIgnitionState"; +export * from "./deployAndSave/bondingRegistry"; export * from "./deployAndSave/ciphernodeRegistryOwnable"; export * from "./deployAndSave/enclave"; -export * from "./deployAndSave/naiveRegistryFilter"; +export * from "./deployAndSave/enclaveTicketToken"; +export * from "./deployAndSave/enclaveToken"; +export * from "./deployAndSave/mockStableToken"; +export * from "./deployAndSave/slashingManager"; export * from "./deployAndSave/mockComputeProvider"; export * from "./deployAndSave/mockDecryptionVerifier"; export * from "./deployAndSave/mockInputValidator"; diff --git a/packages/enclave-contracts/tasks/ciphernode.ts b/packages/enclave-contracts/tasks/ciphernode.ts index d740af2aa0..8c85500b76 100644 --- a/packages/enclave-contracts/tasks/ciphernode.ts +++ b/packages/enclave-contracts/tasks/ciphernode.ts @@ -10,75 +10,477 @@ import { poseidon2 } from "poseidon-lite"; export const ciphernodeAdd = task( "ciphernode:add", - "Register a ciphernode to the registry", + "Register a ciphernode to the bonding registry and ciphernode registry", ) .addOption({ - name: "ciphernodeAddress", - description: "address of ciphernode to register", - defaultValue: ZeroAddress, + name: "licenseBondAmount", + description: + "amount of ENCL to bond (in wei, e.g., 1000000000000000000000 for 1000 ENCL)", + defaultValue: "1000000000000000000000", + }) + .addOption({ + name: "ticketAmount", + description: + "amount of USDC to deposit for tickets (in wei, e.g., 1,000,000,000 for 1000 USDC)", + defaultValue: "1000000000", }) .setAction(async () => ({ - default: async ({ ciphernodeAddress }, hre) => { - const { deployAndSaveCiphernodeRegistryOwnable } = await import( - "../scripts/deployAndSave/ciphernodeRegistryOwnable" + default: async ({ licenseBondAmount, ticketAmount }, hre) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + const [signer] = await ethers.getSigners(); + console.log(`Registering ciphernode: ${signer.address}`); + + const { deployAndSaveBondingRegistry } = await import( + "../scripts/deployAndSave/bondingRegistry" + ); + const { deployAndSaveEnclaveTicketToken } = await import( + "../scripts/deployAndSave/enclaveTicketToken" ); - const { deployAndSavePoseidonT3 } = await import( - "../scripts/deployAndSave/poseidonT3" + const { deployAndSaveEnclaveToken } = await import( + "../scripts/deployAndSave/enclaveToken" ); - const poseidonT3 = await deployAndSavePoseidonT3({ hre }); + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" + ); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + const { enclaveToken } = await deployAndSaveEnclaveToken({ hre }); + const { enclaveTicketToken } = await deployAndSaveEnclaveTicketToken({ + hre, + }); + const { mockStableToken } = await deployAndSaveMockStableToken({ hre }); - const { ciphernodeRegistry } = - await deployAndSaveCiphernodeRegistryOwnable({ - hre, - poseidonT3Address: poseidonT3, - }); + const licenseToken = enclaveToken.connect(signer); + const ticketToken = enclaveTicketToken.connect(signer); + const usdcToken = mockStableToken.connect(signer); + const bondingRegistryConnected = bondingRegistry.connect(signer); + + try { + console.log("Step 1: Checking balances..."); + const enclBalance = await licenseToken.balanceOf(signer.address); + const usdcBalance = await usdcToken.balanceOf(signer.address); + + console.log(`ENCL balance: ${ethers.formatEther(enclBalance)}`); + console.log(`USDC balance: ${ethers.formatUnits(usdcBalance, 6)}`); + + const licenseBondAmountBigInt = BigInt(licenseBondAmount); + const ticketAmountBigInt = BigInt(ticketAmount); + + if (enclBalance < licenseBondAmountBigInt) { + throw new Error( + `Insufficient ENCL balance. Need: ${ethers.formatEther(licenseBondAmountBigInt)}, Have: ${ethers.formatEther(enclBalance)}`, + ); + } - const tx = await ciphernodeRegistry.addCiphernode(ciphernodeAddress); - await tx.wait(); - console.log(`Ciphernode ${ciphernodeAddress} registered`); + if (usdcBalance < ticketAmountBigInt) { + throw new Error( + `Insufficient USDC balance. Need: ${ethers.formatUnits(ticketAmountBigInt, 6)}, Have: ${ethers.formatUnits(usdcBalance, 6)}`, + ); + } + + console.log("Step 2: Approving ENCL for license bond..."); + const approveTx = await licenseToken.approve( + await bondingRegistry.getAddress(), + licenseBondAmountBigInt, + ); + await approveTx.wait(); + console.log("ENCL approved"); + + console.log("Step 3: Bonding license..."); + const bondTx = await bondingRegistryConnected.bondLicense( + licenseBondAmountBigInt, + ); + await bondTx.wait(); + console.log( + `Licensed bonded: ${ethers.formatEther(licenseBondAmountBigInt)} ENCL`, + ); + + console.log("Step 4: Registering as operator..."); + const registerTx = await bondingRegistryConnected.registerOperator(); + await registerTx.wait(); + console.log( + "Operator registered (automatically added to CiphernodeRegistry)", + ); + + console.log("Step 5: Approving USDC for ticket purchase..."); + const approveUsdcTx = await usdcToken.approve( + ticketToken.getAddress(), + ticketAmountBigInt, + ); + await approveUsdcTx.wait(); + console.log("USDC approved"); + + console.log("Step 6: Adding ticket balance..."); + const ticketTx = + await bondingRegistryConnected.addTicketBalance(ticketAmountBigInt); + await ticketTx.wait(); + console.log( + `Ticket balance added: ${ethers.formatUnits(ticketAmountBigInt, 6)} USDC worth`, + ); + + const isRegistered = await bondingRegistry.isRegistered(signer.address); + const isActive = await bondingRegistry.isActive(signer.address); + const licenseBond = await bondingRegistry.getLicenseBond( + signer.address, + ); + const ticketBalance = await bondingRegistry.getTicketBalance( + signer.address, + ); + + console.log("\n=== Registration Complete ==="); + console.log(`Ciphernode: ${signer.address}`); + console.log(`Registered: ${isRegistered}`); + console.log(`Active: ${isActive}`); + console.log(`License Bond: ${ethers.formatEther(licenseBond)} ENCL`); + console.log( + `Ticket Balance: ${ethers.formatUnits(ticketBalance, 6)} USDC worth`, + ); + } catch (error) { + console.error("Registration failed:", error); + throw error; + } }, })) .build(); export const ciphernodeRemove = task( "ciphernode:remove", - "Remove a ciphernode from the registry", + "Deregister a ciphernode from the bonding registry", +) + .addOption({ + name: "siblings", + description: "comma separated siblings from tree proof", + defaultValue: "", + }) + .setAction(async () => ({ + default: async ({ siblings }, hre) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + const [signer] = await ethers.getSigners(); + console.log(`Deregistering ciphernode: ${signer.address}`); + + const { deployAndSaveBondingRegistry } = await import( + "../scripts/deployAndSave/bondingRegistry" + ); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + + const bondingRegistryConnected = bondingRegistry.connect(signer); + + const siblingsArray = siblings.split(",").map((s: string) => BigInt(s)); + + try { + console.log( + "Deregistering operator (will also remove from CiphernodeRegistry)...", + ); + const tx = + await bondingRegistryConnected.deregisterOperator(siblingsArray); + await tx.wait(); + + console.log(`Ciphernode ${signer.address} deregistered`); + console.log( + "Note: Funds are now in exit queue. Use claimExits() after the exit delay period.", + ); + } catch (error) { + console.error("Deregistration failed:", error); + throw error; + } + }, + })) + .build(); + +export const ciphernodeMintTokens = task( + "ciphernode:mint-tokens", + "Mint ENCL and USDC tokens for a ciphernode (for testing)", ) .addOption({ name: "ciphernodeAddress", - description: "address of ciphernode to remove", + description: "address of ciphernode to mint tokens for", defaultValue: ZeroAddress, }) .addOption({ - name: "siblings", - description: "comma separated siblings from tree proof", + name: "enclAmount", + description: + "amount of ENCL to mint (in ether units, e.g., 2000 for 2000 ENCL)", + defaultValue: "2000", + }) + .addOption({ + name: "usdcAmount", + description: + "amount of USDC to mint (in USDC units, e.g., 1000 for 1000 USDC)", + defaultValue: "1000", + }) + .setAction(async () => ({ + default: async ({ ciphernodeAddress, enclAmount, usdcAmount }, hre) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + if (ciphernodeAddress === ZeroAddress) { + throw new Error( + "Ciphernode address is required. Use --ciphernode-address option.", + ); + } + + const { deployAndSaveEnclaveToken } = await import( + "../scripts/deployAndSave/enclaveToken" + ); + const { enclaveToken } = await deployAndSaveEnclaveToken({ hre }); + + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" + ); + const { mockStableToken } = await deployAndSaveMockStableToken({ + hre, + }); + + const [signer] = await ethers.getSigners(); + const enclaveTokenContract = enclaveToken.connect(signer); + const mockUSDCContract = mockStableToken.connect(signer); + + try { + console.log(`Minting tokens for: ${ciphernodeAddress}`); + + console.log(`Minting ${enclAmount} ENCL...`); + const enclTx = await enclaveTokenContract.mintAllocation( + ciphernodeAddress, + ethers.parseEther(enclAmount), + "Ciphernode allocation", + ); + await enclTx.wait(); + console.log(`${enclAmount} ENCL minted`); + + console.log(`Minting ${usdcAmount} USDC...`); + const usdcTx = await mockUSDCContract.mint( + ciphernodeAddress, + ethers.parseUnits(usdcAmount, 6), + ); + await usdcTx.wait(); + console.log(`${usdcAmount} USDC minted`); + + const enclBalance = + await enclaveTokenContract.balanceOf(ciphernodeAddress); + const usdcBalance = await mockUSDCContract.balanceOf(ciphernodeAddress); + + console.log("\n=== Token Balances ==="); + console.log(`ENCL: ${ethers.formatEther(enclBalance)}`); + console.log(`USDC: ${ethers.formatUnits(usdcBalance, 6)}`); + } catch (error) { + console.error("Token minting failed:", error); + throw error; + } + }, + })) + .build(); + +export const ciphernodeAdminAdd = task( + "ciphernode:admin-add", + "Register a ciphernode using admin privileges (for testing/setup)", +) + .addOption({ + name: "ciphernodeAddress", + description: "address of ciphernode to register", + defaultValue: ZeroAddress, + }) + .addOption({ + name: "adminPrivateKey", + description: + "private key of admin wallet (optional, uses anvil first key if not provided)", defaultValue: "", }) + .addOption({ + name: "licenseBondAmount", + description: + "amount of ENCL to bond (in ether units, e.g., 1000 for 1000 ENCL)", + defaultValue: "1000", + }) + .addOption({ + name: "ticketAmount", + description: + "amount of USDC for tickets (in USDC units, e.g., 1000 for 1000 USDC)", + defaultValue: "1000", + }) .setAction(async () => ({ - default: async ({ ciphernodeAddress, siblings }, hre) => { - const { deployAndSaveCiphernodeRegistryOwnable } = await import( - "../scripts/deployAndSave/ciphernodeRegistryOwnable" + default: async ( + { ciphernodeAddress, adminPrivateKey, licenseBondAmount, ticketAmount }, + hre, + ) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + + if (ciphernodeAddress === ZeroAddress) { + throw new Error( + "Ciphernode address is required. Use --ciphernode-address option.", + ); + } + + let adminWallet; + if (adminPrivateKey) { + adminWallet = new ethers.Wallet(adminPrivateKey, ethers.provider); + } else { + const anvilFirstKey = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + adminWallet = new ethers.Wallet(anvilFirstKey, ethers.provider); + } + + console.log(`Admin wallet: ${adminWallet.address}`); + console.log(`Registering ciphernode: ${ciphernodeAddress}`); + + const { deployAndSaveBondingRegistry } = await import( + "../scripts/deployAndSave/bondingRegistry" + ); + const { bondingRegistry } = await deployAndSaveBondingRegistry({ hre }); + + const { deployAndSaveEnclaveToken } = await import( + "../scripts/deployAndSave/enclaveToken" ); - const { deployAndSavePoseidonT3 } = await import( - "../scripts/deployAndSave/poseidonT3" + const { enclaveToken } = await deployAndSaveEnclaveToken({ hre }); + + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" ); - const poseidonT3 = await deployAndSavePoseidonT3({ hre }); + const { mockStableToken: mockUSDC } = await deployAndSaveMockStableToken({ + hre, + }); + + const enclaveTokenConnected = enclaveToken.connect(adminWallet); + const mockUSDCConnected = mockUSDC.connect(adminWallet); + + const ticketTokenAddress = await bondingRegistry.ticketToken(); + + try { + const licenseBondWei = ethers.parseEther(licenseBondAmount); + const ticketAmountWei = ethers.parseUnits(ticketAmount, 6); + + console.log("Step 1: Minting and transferring ENCL to ciphernode..."); + + const enclTx = await enclaveTokenConnected.mintAllocation( + adminWallet.address, + licenseBondWei, + "Admin allocation for ciphernode registration", + ); + await enclTx.wait(); + + const transferTx = await enclaveTokenConnected.transfer( + ciphernodeAddress, + licenseBondWei, + ); + await transferTx.wait(); + console.log(`${licenseBondAmount} ENCL transferred to ciphernode`); + + console.log("Step 2: Minting USDC to admin..."); + const usdcTx = await mockUSDCConnected.mint( + adminWallet.address, + ticketAmountWei, + ); + await usdcTx.wait(); + console.log(`${ticketAmount} USDC minted to admin`); - const { ciphernodeRegistry } = - await deployAndSaveCiphernodeRegistryOwnable({ - hre, - poseidonT3Address: poseidonT3, + console.log( + "Step 3: Impersonating ciphernode for license operations...", + ); + await connection.provider.request({ + method: "hardhat_impersonateAccount", + params: [ciphernodeAddress], }); - const siblingsArray = siblings.split(",").map((s: string) => BigInt(s)); + await connection.provider.request({ + method: "hardhat_setBalance", + params: [ciphernodeAddress, "0x1000000000000000000000"], + }); - const tx = await ciphernodeRegistry.removeCiphernode( - ciphernodeAddress, - siblingsArray, - ); - await tx.wait(); + const ciphernodeSigner = await ethers.getSigner(ciphernodeAddress); + const enclaveTokenAsCiphernode = enclaveToken.connect(ciphernodeSigner); + const bondingRegistryAsCiphernode = + bondingRegistry.connect(ciphernodeSigner); + + const approveTx = await enclaveTokenAsCiphernode.approve( + await bondingRegistry.getAddress(), + licenseBondWei, + ); + await approveTx.wait(); + + const bondTx = + await bondingRegistryAsCiphernode.bondLicense(licenseBondWei); + await bondTx.wait(); + console.log(`License bonded: ${licenseBondAmount} ENCL`); + + const registerTx = await bondingRegistryAsCiphernode.registerOperator(); + await registerTx.wait(); + console.log( + "Operator registered (automatically added to CiphernodeRegistry)", + ); + + await connection.provider.request({ + method: "hardhat_stopImpersonatingAccount", + params: [ciphernodeAddress], + }); + + console.log("Step 4: Adding ticket balance via admin..."); + + const approveUsdcTx = await mockUSDCConnected.approve( + ticketTokenAddress, + ticketAmountWei, + ); + await approveUsdcTx.wait(); + + await connection.provider.request({ + method: "hardhat_impersonateAccount", + params: [ciphernodeAddress], + }); + + await connection.provider.request({ + method: "hardhat_setBalance", + params: [ciphernodeAddress, "0x1000000000000000000000"], + }); + + const ciphernodeSigner2 = await ethers.getSigner(ciphernodeAddress); + const bondingRegistryAsCiphernode2 = + bondingRegistry.connect(ciphernodeSigner2); - console.log(`Ciphernode ${ciphernodeAddress} removed`); + const usdcTransferTx = await mockUSDCConnected.transfer( + ciphernodeAddress, + ticketAmountWei, + ); + await usdcTransferTx.wait(); + + const mockUSDCAsCiphernode = mockUSDC.connect(ciphernodeSigner2); + const approveUsdcAsCiphernodeTx = await mockUSDCAsCiphernode.approve( + ticketTokenAddress, + ticketAmountWei, + ); + await approveUsdcAsCiphernodeTx.wait(); + + const addTicketTx = + await bondingRegistryAsCiphernode2.addTicketBalance(ticketAmountWei); + await addTicketTx.wait(); + console.log(`Ticket balance added: ${ticketAmount} USDC worth`); + + await connection.provider.request({ + method: "hardhat_stopImpersonatingAccount", + params: [ciphernodeAddress], + }); + + const isRegistered = + await bondingRegistry.isRegistered(ciphernodeAddress); + const isActive = await bondingRegistry.isActive(ciphernodeAddress); + const licenseBond = + await bondingRegistry.getLicenseBond(ciphernodeAddress); + const ticketBalance = + await bondingRegistry.getTicketBalance(ciphernodeAddress); + + console.log("\n=== Registration Complete ==="); + console.log(`Ciphernode: ${ciphernodeAddress}`); + console.log(`Registered: ${isRegistered}`); + console.log(`Active: ${isActive}`); + console.log(`License Bond: ${ethers.formatEther(licenseBond)} ENCL`); + console.log( + `Ticket Balance: ${ethers.formatUnits(ticketBalance, 6)} USDC worth`, + ); + } catch (error) { + console.error("Admin registration failed:", error); + throw error; + } }, })) .build(); diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index 5c755b1862..6cd029d075 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -77,7 +77,6 @@ export const requestCommittee = task( .setAction(async () => ({ default: async ( { - filter, thresholdQuorum, thresholdTotal, windowStart, @@ -90,9 +89,15 @@ export const requestCommittee = task( }, hre, ) => { + const connection = await hre.network.connect(); + const { ethers } = connection; + const { deployAndSaveEnclave } = await import( "../scripts/deployAndSave/enclave" ); + const { deployAndSaveMockStableToken } = await import( + "../scripts/deployAndSave/mockStableToken" + ); const { deployAndSavePoseidonT3 } = await import( "../scripts/deployAndSave/poseidonT3" @@ -104,6 +109,14 @@ export const requestCommittee = task( poseidonT3Address: poseidonT3, }); + const { mockStableToken: mockUSDC } = await deployAndSaveMockStableToken({ + hre, + }); + + const [signer] = await ethers.getSigners(); + const enclaveContract = enclave.connect(signer); + const mockUSDCContract = mockUSDC.connect(signer); + const enclaveArgs = readDeploymentArgs( "Enclave", hre.globalOptions.network, @@ -122,15 +135,6 @@ export const requestCommittee = task( throw new Error("CiphernodeRegistry deployment arguments not found"); } - const filterArgs = readDeploymentArgs( - "NaiveRegistryFilter", - hre.globalOptions.network, - ); - - if (!filterArgs) { - throw new Error("NaiveRegistryFilter deployment arguments not found"); - } - const mockE3ProgramArgs = readDeploymentArgs( "MockE3Program", hre.globalOptions.network, @@ -166,31 +170,42 @@ export const requestCommittee = task( ); } - console.log({ - filter: filter === ZeroAddress ? filterArgs.address : filter, - threshold: [thresholdQuorum, thresholdTotal], - startWindow: [windowStart, windowEnd], + const requestParams = { + threshold: [thresholdQuorum, thresholdTotal] as [number, number], + startWindow: [windowStart, windowEnd] as [number, number], duration: duration, e3Program: e3Address === ZeroAddress ? mockE3ProgramArgs!.address : e3Address, e3ProgramParams, computeProviderParams, customParams, - }); - const tx = await enclave.request( - { - filter: filter === ZeroAddress ? filterArgs.address : filter, - threshold: [thresholdQuorum, thresholdTotal], - startWindow: [windowStart, windowEnd], - duration: duration, - e3Program: - e3Address === ZeroAddress ? mockE3ProgramArgs!.address : e3Address, - e3ProgramParams, - computeProviderParams, - customParams, - }, - { value: "1000000000000000000" }, + }; + + console.log("Request parameters:", requestParams); + + const fee = await enclaveContract.getE3Quote(requestParams); + console.log(`E3 fee: ${ethers.formatUnits(fee, 6)} USDC`); + + const usdcBalance = await mockUSDCContract.balanceOf(signer.address); + console.log(`USDC balance: ${ethers.formatUnits(usdcBalance, 6)} USDC`); + + if (usdcBalance < fee) { + const mintAmount = fee - usdcBalance + ethers.parseUnits("1000", 6); + console.log(`Minting ${ethers.formatUnits(mintAmount, 6)} USDC...`); + const mintTx = await mockUSDCContract.mint(signer.address, mintAmount); + await mintTx.wait(); + console.log("USDC minted"); + } + + console.log("Approving USDC spending..."); + const approveTx = await mockUSDCContract.approve( + await enclaveContract.getAddress(), + fee, ); + await approveTx.wait(); + console.log("USDC approved"); + + const tx = await enclaveContract.request(requestParams); console.log("Requesting committee... ", tx.hash); await tx.wait(); @@ -256,13 +271,20 @@ export const publishCommittee = task( }) .setAction(async () => ({ default: async ({ e3Id, nodes, publicKey }, hre) => { - const { deployAndSaveNaiveRegistryFilter } = await import( - "../scripts/deployAndSave/naiveRegistryFilter" + const { deployAndSaveCiphernodeRegistryOwnable } = await import( + "../scripts/deployAndSave/ciphernodeRegistryOwnable" ); - const { naiveRegistryFilter } = await deployAndSaveNaiveRegistryFilter({ - hre, - }); + const { deployAndSavePoseidonT3 } = await import( + "../scripts/deployAndSave/poseidonT3" + ); + const poseidonT3 = await deployAndSavePoseidonT3({ hre }); + + const { ciphernodeRegistry } = + await deployAndSaveCiphernodeRegistryOwnable({ + hre, + poseidonT3Address: poseidonT3, + }); const nodesToSend = nodes .split(",") @@ -273,7 +295,7 @@ export const publishCommittee = task( throw new Error("Invalid nodes format: no valid addresses found"); } - const tx = await naiveRegistryFilter.publishCommittee( + const tx = await ciphernodeRegistry.publishCommittee( e3Id, nodesToSend, publicKey, diff --git a/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts deleted file mode 100644 index 0f6548e4f3..0000000000 --- a/packages/enclave-contracts/test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts +++ /dev/null @@ -1,425 +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. -import { LeanIMT } from "@zk-kit/lean-imt"; -import { expect } from "chai"; -import { network } from "hardhat"; -import { poseidon2 } from "poseidon-lite"; - -import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; -import NaiveRegistryFilterModule from "../../ignition/modules/naiveRegistryFilter"; -import { - CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, -} from "../../types"; - -const AddressOne = "0x0000000000000000000000000000000000000001"; -const AddressTwo = "0x0000000000000000000000000000000000000002"; -const AddressThree = "0x0000000000000000000000000000000000000003"; - -const { ethers, networkHelpers, ignition } = await network.connect(); -const { loadFixture } = networkHelpers; - -const data = "0xda7a"; -const dataHash = ethers.keccak256(data); - -// Hash function used to compute the tree nodes. -const hash = (a: bigint, b: bigint) => poseidon2([a, b]); - -describe("CiphernodeRegistryOwnable", function () { - async function setup() { - const [owner, notTheOwner] = await ethers.getSigners(); - - const registryContract = await ignition.deploy(CiphernodeRegistryModule, { - parameters: { - CiphernodeRegistry: { - enclaveAddress: await owner.getAddress(), - owner: await owner.getAddress(), - }, - }, - }); - - const filterContract = await ignition.deploy(NaiveRegistryFilterModule, { - parameters: { - NaiveRegistryFilter: { - owner: await owner.getAddress(), - ciphernodeRegistryAddress: - await registryContract.cipherNodeRegistry.getAddress(), - }, - }, - }); - - const registry = CiphernodeRegistryFactory.connect( - await registryContract.cipherNodeRegistry.getAddress(), - owner, - ); - const filter = NaiveRegistryFilterFactory.connect( - await filterContract.naiveRegistryFilter.getAddress(), - owner, - ); - - const tree = new LeanIMT(hash); - await registry.addCiphernode(AddressOne); - tree.insert(BigInt(AddressOne)); - await registry.addCiphernode(AddressTwo); - tree.insert(BigInt(AddressTwo)); - - return { - owner, - notTheOwner, - registry, - filter, - tree, - request: { - e3Id: 1, - filter: await filter.getAddress(), - threshold: [2, 2] as [number, number], - }, - }; - } - - describe("constructor / initialize()", function () { - it("correctly sets `_owner` and `enclave` ", async function () { - const poseidonFactory = await ethers.getContractFactory("PoseidonT3"); - const poseidonDeployment = await poseidonFactory.deploy(); - const [deployer] = await ethers.getSigners(); - if (!deployer) throw new Error("Bad getSigners() output"); - const ciphernodeRegistryFactory = await ethers.getContractFactory( - "CiphernodeRegistryOwnable", - { - libraries: { - PoseidonT3: await poseidonDeployment.getAddress(), - }, - }, - ); - const ciphernodeRegistry = await ciphernodeRegistryFactory.deploy( - deployer.address, - AddressTwo, - ); - expect(await ciphernodeRegistry.owner()).to.equal(deployer.address); - expect(await ciphernodeRegistry.enclave()).to.equal(AddressTwo); - }); - }); - - describe("requestCommittee()", function () { - it("reverts if committee has already been requested for given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await expect( - registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ).to.be.revertedWithCustomError(registry, "CommitteeAlreadyRequested"); - }); - it("stores the registry filter for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.getFilter(request.e3Id)).to.equal(request.filter); - }); - it("stores the root of the ciphernode registry at the time of the request", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.rootAt(request.e3Id)).to.equal( - await registry.root(), - ); - }); - it("requests a committee from the given filter", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.getFilter(request.e3Id)).to.equal(request.filter); - }); - it("emits a CommitteeRequested event", async function () { - const { registry, request } = await loadFixture(setup); - await expect( - registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ) - .to.emit(registry, "CommitteeRequested") - .withArgs(request.e3Id, request.filter, request.threshold); - }); - it("reverts if filter.requestCommittee() fails", async function () { - const { owner, registry, filter, request } = await loadFixture(setup); - - await filter.setRegistry(await owner.getAddress()); - await filter.requestCommittee(request.e3Id, request.threshold); - await filter.setRegistry(await registry.getAddress()); - - await expect( - registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyExists"); - }); - it("returns true if the request is successful", async function () { - const { registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee.staticCall( - request.e3Id, - request.filter, - request.threshold, - ), - ).to.be.true; - }); - }); - - describe("publishCommittee()", function () { - it("reverts if the caller is not the filter for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await expect( - registry.publishCommittee(request.e3Id, "0xc0de", data), - ).to.be.revertedWithCustomError(registry, "OnlyFilter"); - }); - it("stores the public key of the committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - data, - ); - expect(await registry.committeePublicKey(request.e3Id)).to.equal( - dataHash, - ); - }); - it("emits a CommitteePublished event", async function () { - const { filter, registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await expect( - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - data, - ), - ) - .to.emit(registry, "CommitteePublished") - .withArgs(request.e3Id, data); - }); - }); - - describe("addCiphernode()", function () { - it("reverts if the caller is not the owner", async function () { - const { registry, notTheOwner } = await loadFixture(setup); - await expect(registry.connect(notTheOwner).addCiphernode(AddressThree)) - .to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount") - .withArgs(await notTheOwner.getAddress()); - }); - it("adds the ciphernode to the registry", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.addCiphernode(AddressThree)); - expect(await registry.isCiphernodeEligible(AddressThree)).to.be.true; - }); - it("increments numCiphernodes", async function () { - const { registry } = await loadFixture(setup); - const numCiphernodes = await registry.numCiphernodes(); - expect(await registry.addCiphernode(AddressThree)); - expect(await registry.numCiphernodes()).to.equal( - numCiphernodes + BigInt(1), - ); - }); - it("emits a CiphernodeAdded event", async function () { - const { registry } = await loadFixture(setup); - const treeSize = await registry.treeSize(); - const numCiphernodes = await registry.numCiphernodes(); - await expect(await registry.addCiphernode(AddressThree)) - .to.emit(registry, "CiphernodeAdded") - .withArgs( - AddressThree, - treeSize, - numCiphernodes + BigInt(1), - treeSize + BigInt(1), - ); - }); - }); - - describe("removeCiphernode()", function () { - it("reverts if the caller is not the owner", async function () { - const { registry, notTheOwner } = await loadFixture(setup); - await expect( - registry.connect(notTheOwner).removeCiphernode(AddressOne, []), - ) - .to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount") - .withArgs(await notTheOwner.getAddress()); - }); - it("removes the ciphernode from the registry", async function () { - const { registry } = await loadFixture(setup); - const tree = new LeanIMT(hash); - tree.insert(BigInt(AddressOne)); - tree.insert(BigInt(AddressTwo)); - const index = tree.indexOf(BigInt(AddressOne)); - const proof = tree.generateProof(index); - tree.update(index, BigInt(0)); - expect(await registry.isEnabled(AddressOne)).to.be.true; - expect(await registry.removeCiphernode(AddressOne, proof.siblings)); - expect(await registry.isEnabled(AddressOne)).to.be.false; - expect(await registry.root()).to.equal(tree.root); - }); - it("decrements numCiphernodes", async function () { - const { registry, tree } = await loadFixture(setup); - const numCiphernodes = await registry.numCiphernodes(); - const index = tree.indexOf(BigInt(AddressOne)); - const proof = tree.generateProof(index); - expect(await registry.removeCiphernode(AddressOne, proof.siblings)); - expect(await registry.numCiphernodes()).to.equal( - numCiphernodes - BigInt(1), - ); - }); - it("emits a CiphernodeRemoved event", async function () { - const { registry, tree } = await loadFixture(setup); - const numCiphernodes = await registry.numCiphernodes(); - const size = await registry.treeSize(); - const index = tree.indexOf(BigInt(AddressOne)); - const proof = tree.generateProof(index); - await expect(registry.removeCiphernode(AddressOne, proof.siblings)) - .to.emit(registry, "CiphernodeRemoved") - .withArgs(AddressOne, index, numCiphernodes - BigInt(1), size); - }); - }); - - describe("setEnclave()", function () { - it("reverts if the caller is not the owner", async function () { - const { registry, notTheOwner } = await loadFixture(setup); - await expect( - registry.connect(notTheOwner).setEnclave(AddressThree), - ).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount"); - }); - it("sets the enclave address", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.setEnclave(AddressThree)); - expect(await registry.enclave()).to.equal(AddressThree); - }); - it("emits an EnclaveSet event", async function () { - const { registry } = await loadFixture(setup); - await expect(await registry.setEnclave(AddressThree)) - .to.emit(registry, "EnclaveSet") - .withArgs(AddressThree); - }); - }); - - describe("committeePublicKey()", function () { - it("returns the public key of the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - data, - ); - expect(await registry.committeePublicKey(request.e3Id)).to.equal( - dataHash, - ); - }); - it("reverts if the committee has not been published", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - await expect( - registry.committeePublicKey(request.e3Id), - ).to.be.revertedWithCustomError(registry, "CommitteeNotPublished"); - }); - }); - - describe("isCiphernodeEligible()", function () { - it("returns true if the ciphernode is in the registry", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.isCiphernodeEligible(AddressOne)).to.be.true; - }); - it("returns false if the ciphernode is not in the registry", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.isCiphernodeEligible(AddressThree)).to.be.false; - }); - }); - - describe("isEnabled()", function () { - it("returns true if the ciphernode is currently enabled", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.isEnabled(AddressOne)).to.be.true; - }); - it("returns false if the ciphernode is not currently enabled", async function () { - const { registry } = await loadFixture(setup); - expect(await registry.isEnabled(AddressThree)).to.be.false; - }); - }); - - describe("root()", function () { - it("returns the root of the ciphernode registry merkle tree", async function () { - const { registry, tree } = await loadFixture(setup); - expect(await registry.root()).to.equal(tree.root); - }); - }); - - describe("rootAt()", function () { - it("returns the root of the ciphernode registry merkle tree at the given e3Id", async function () { - const { registry, tree, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.rootAt(request.e3Id)).to.equal(tree.root); - }); - }); - - describe("getFilter()", function () { - it("returns the registry filter for the given e3Id", async function () { - const { registry, request } = await loadFixture(setup); - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ); - expect(await registry.getFilter(request.e3Id)).to.equal(request.filter); - }); - }); - - describe("treeSize()", function () { - it("returns the size of the ciphernode registry merkle tree", async function () { - const { registry, tree } = await loadFixture(setup); - expect(await registry.treeSize()).to.equal(tree.size); - }); - }); -}); diff --git a/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts b/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts deleted file mode 100644 index af32a57bea..0000000000 --- a/packages/enclave-contracts/test/CiphernodeRegistry/NaiveRegistryFilter.spec.ts +++ /dev/null @@ -1,268 +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. -import { LeanIMT } from "@zk-kit/lean-imt"; -import { expect } from "chai"; -import { network } from "hardhat"; -import { poseidon2 } from "poseidon-lite"; - -import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; -import NaiveRegistryFilterModule from "../../ignition/modules/naiveRegistryFilter"; -import { - CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, -} from "../../types"; - -const AddressOne = "0x0000000000000000000000000000000000000001"; -const AddressTwo = "0x0000000000000000000000000000000000000002"; -const AddressThree = "0x0000000000000000000000000000000000000003"; - -const { ethers, networkHelpers, ignition } = await network.connect(); -const { loadFixture } = networkHelpers; - -// Hash function used to compute the tree nodes. -const hash = (a: bigint, b: bigint) => poseidon2([a, b]); - -describe("NaiveRegistryFilter", function () { - async function setup() { - const [owner, notTheOwner] = await ethers.getSigners(); - - const registryContract = await ignition.deploy(CiphernodeRegistryModule, { - parameters: { - CiphernodeRegistry: { - enclaveAddress: await owner.getAddress(), - owner: await owner.getAddress(), - }, - }, - }); - - const filterContract = await ignition.deploy(NaiveRegistryFilterModule, { - parameters: { - NaiveRegistryFilter: { - owner: await owner.getAddress(), - ciphernodeRegistryAddress: - await registryContract.cipherNodeRegistry.getAddress(), - }, - }, - }); - - const registry = CiphernodeRegistryFactory.connect( - await registryContract.cipherNodeRegistry.getAddress(), - owner, - ); - const filter = NaiveRegistryFilterFactory.connect( - await filterContract.naiveRegistryFilter.getAddress(), - owner, - ); - - const tree = new LeanIMT(hash); - await registry.addCiphernode(AddressOne); - tree.insert(BigInt(AddressOne)); - await registry.addCiphernode(AddressTwo); - tree.insert(BigInt(AddressTwo)); - - return { - owner, - notTheOwner, - registry, - filter, - tree, - request: { - e3Id: 1, - filter: await filter.getAddress(), - threshold: [2, 2] as [number, number], - }, - }; - } - - describe("constructor / initialize()", function () { - it("should set the owner", async function () { - const { owner, filter } = await loadFixture(setup); - expect(await filter.owner()).to.equal(await owner.getAddress()); - }); - it("should set the registry", async function () { - const { registry, filter } = await loadFixture(setup); - expect(await filter.registry()).to.equal(await registry.getAddress()); - }); - }); - - describe("requestCommittee()", function () { - it("should revert if the caller is not the registry", async function () { - const { filter, request } = await loadFixture(setup); - await expect( - filter.requestCommittee(request.e3Id, request.threshold), - ).to.be.revertedWithCustomError(filter, "OnlyRegistry"); - }); - it("should revert if a committee has already been requested for the given e3Id", async function () { - const { filter, request, owner } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - await filter.requestCommittee(request.e3Id, request.threshold); - await expect( - filter.requestCommittee(request.e3Id, request.threshold), - ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyExists"); - }); - it("should set the threshold for the requested committee", async function () { - const { filter, owner, request } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - await filter.requestCommittee(request.e3Id, request.threshold); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.threshold).to.deep.equal(request.threshold); - }); - it("should return true when a committee is requested", async function () { - const { filter, owner, request } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - const result = await filter.requestCommittee.staticCall( - request.e3Id, - request.threshold, - ); - expect(result).to.equal(true); - }); - }); - - describe("publishCommittee()", function () { - it("should revert if the caller is not owner", async function () { - const { filter, notTheOwner, request } = await loadFixture(setup); - await expect( - filter - .connect(notTheOwner) - .publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ), - ).to.be.revertedWithCustomError(filter, "OwnableUnauthorizedAccount"); - }); - it("should revert if committee already published", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - await expect( - filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ), - ).to.be.revertedWithCustomError(filter, "CommitteeAlreadyPublished"); - }); - it("should store the node addresses of the committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.nodes).to.deep.equal([AddressOne, AddressTwo]); - }); - it("should store the public key of the committee", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.publicKey).to.equal(ethers.keccak256(AddressThree)); - }); - it("should publish the correct node addresses of the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.nodes).to.deep.equal([AddressOne, AddressTwo]); - }); - it("should publish the public key of the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.publicKey).to.equal(ethers.keccak256(AddressThree)); - }); - }); - - describe("setRegistry()", function () { - it("should revert if the caller is not the owner", async function () { - const { filter, notTheOwner } = await loadFixture(setup); - await expect( - filter.connect(notTheOwner).setRegistry(await notTheOwner.getAddress()), - ) - .to.be.revertedWithCustomError(filter, "OwnableUnauthorizedAccount") - .withArgs(await notTheOwner.getAddress()); - }); - it("should set the registry", async function () { - const { filter, owner } = await loadFixture(setup); - await filter.setRegistry(await owner.getAddress()); - expect(await filter.registry()).to.equal(await owner.getAddress()); - }); - }); - - describe("getCommittee()", function () { - it("should return the committee for the given e3Id", async function () { - const { filter, registry, request } = await loadFixture(setup); - expect( - await registry.requestCommittee( - request.e3Id, - request.filter, - request.threshold, - ), - ); - expect( - await filter.publishCommittee( - request.e3Id, - [AddressOne, AddressTwo], - AddressThree, - ), - ); - const committee = await filter.getCommittee(request.e3Id); - expect(committee.threshold).to.deep.equal(request.threshold); - expect(committee.nodes).to.deep.equal([AddressOne, AddressTwo]); - expect(committee.publicKey).to.equal(ethers.keccak256(AddressThree)); - }); - }); -}); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 9579382789..35613cfee1 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -5,28 +5,36 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { LeanIMT } from "@zk-kit/lean-imt"; import { expect } from "chai"; +import type { Signer } from "ethers"; import { network } from "hardhat"; import { poseidon2 } from "poseidon-lite"; +import BondingRegistryModule from "../ignition/modules/bondingRegistry"; +import CiphernodeRegistryModule from "../ignition/modules/ciphernodeRegistry"; import EnclaveModule from "../ignition/modules/enclave"; -import MockCiphernodeRegistryModule from "../ignition/modules/mockCiphernodeRegistry"; +import EnclaveTicketTokenModule from "../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../ignition/modules/enclaveToken"; import MockCiphernodeRegistryEmptyKeyModule from "../ignition/modules/mockCiphernodeRegistryEmptyKey"; import mockComputeProviderModule from "../ignition/modules/mockComputeProvider"; import MockDecryptionVerifierModule from "../ignition/modules/mockDecryptionVerifier"; import MockE3ProgramModule from "../ignition/modules/mockE3Program"; import MockInputValidatorModule from "../ignition/modules/mockInputValidator"; -import NaiveRegistryFilterModule from "../ignition/modules/naiveRegistryFilter"; +import MockStableTokenModule from "../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../ignition/modules/slashingManager"; import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, Enclave__factory as EnclaveFactory, - NaiveRegistryFilter__factory as NaiveRegistryFilterFactory, + MockUSDC__factory as MockUSDCFactory, } from "../types"; +import type { Enclave } from "../types/contracts/Enclave"; +import type { MockUSDC } from "../types/contracts/test/MockStableToken.sol/MockUSDC"; const { ethers, ignition, networkHelpers } = await network.connect(); const { loadFixture, time, mine } = networkHelpers; describe("Enclave", function () { const THIRTY_DAYS_IN_SECONDS = 60 * 60 * 24 * 30; + const SORTITION_SUBMISSION_WINDOW = 10; const addressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; @@ -53,61 +61,238 @@ describe("Enclave", function () { // Hash function used to compute the tree nodes. const hash = (a: bigint, b: bigint) => poseidon2([a, b]); + const setupAndPublishCommittee = async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registry: any, + e3Id: number, + nodes: string[], + publicKey: string, + operator1: Signer, + operator2: Signer, + ): Promise => { + await registry.connect(operator1).submitTicket(e3Id, 1); + await registry.connect(operator2).submitTicket(e3Id, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(e3Id); + await registry.publishCommittee(e3Id, nodes, publicKey); + }; + + // Helper function to approve USDC and make request + const makeRequest = async ( + enclave: Enclave, + usdcToken: MockUSDC, + requestParams: Parameters[0], + signer?: Signer, + ) => { + const fee = await enclave.getE3Quote(requestParams); + const tokenContract = signer ? usdcToken.connect(signer) : usdcToken; + const enclaveContract = signer ? enclave.connect(signer) : enclave; + + await tokenContract.approve(await enclave.getAddress(), fee); + return enclaveContract.request(requestParams); + }; + + async function setupOperatorForSortition( + operator: Signer, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bondingRegistry: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + licenseToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + usdcToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ticketToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registry: any, + ): Promise { + const operatorAddress = await operator.getAddress(); + + await licenseToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await licenseToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + + await registry.addCiphernode(operatorAddress); + } + const setup = async () => { - const [owner, notTheOwner] = await ethers.getSigners(); + const [owner, notTheOwner, operator1, operator2] = + await ethers.getSigners(); const ownerAddress = await owner.getAddress(); - const enclaveContract = await ignition.deploy(EnclaveModule, { + const usdcContract = await ignition.deploy(MockStableTokenModule, { parameters: { - Enclave: { - params: encodedE3ProgramParams, - owner: ownerAddress, - maxDuration: THIRTY_DAYS_IN_SECONDS, - registry: addressOne, + MockUSDC: { + initialSupply: 1000000, }, }, }); - const enclaveAddress = await enclaveContract.enclave.getAddress(); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); - const ciphernodeRegistry = await ignition.deploy( - MockCiphernodeRegistryModule, + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcToken.getAddress(), + registry: addressOne, + owner: ownerAddress, + }, + }, + }, ); - const ciphernodeRegistryAddress = - await ciphernodeRegistry.mockCiphernodeRegistry.getAddress(); + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: addressOne, + }, + }, + }, + ); - const naiveRegistryFilter = await ignition.deploy( - NaiveRegistryFilterModule, + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, { parameters: { - NaiveRegistryFilter: { - ciphernodeRegistryAddress, + BondingRegistry: { owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: addressOne, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: 7 * 24 * 60 * 60, }, }, }, ); - const naiveRegistryFilterAddress = - await naiveRegistryFilter.naiveRegistryFilter.getAddress(); + const enclaveContract = await ignition.deploy(EnclaveModule, { + parameters: { + Enclave: { + params: encodedE3ProgramParams, + owner: ownerAddress, + maxDuration: THIRTY_DAYS_IN_SECONDS, + registry: addressOne, + bondingRegistry: + await bondingRegistryContract.bondingRegistry.getAddress(), + feeToken: await usdcToken.getAddress(), + }, + }, + }); + + const enclaveAddress = await enclaveContract.enclave.getAddress(); + + const ciphernodeRegistry = await ignition.deploy(CiphernodeRegistryModule, { + parameters: { + CiphernodeRegistry: { + enclaveAddress: enclaveAddress, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + }, + }, + }); + + const ciphernodeRegistryAddress = + await ciphernodeRegistry.cipherNodeRegistry.getAddress(); const enclave = EnclaveFactory.connect(enclaveAddress, owner); const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, owner, ); - const naiveRegistryFilterContract = NaiveRegistryFilterFactory.connect( - naiveRegistryFilterAddress, - owner, - ); const registryAddress = await enclave.ciphernodeRegistry(); if (registryAddress !== ciphernodeRegistryAddress) { await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); } + await ciphernodeRegistryContract.setBondingRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + await bondingRegistryContract.bondingRegistry.setRegistry( + ciphernodeRegistryAddress, + ); + await bondingRegistryContract.bondingRegistry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); + await slashingManagerContract.slashingManager.setBondingRegistry( + await bondingRegistryContract.bondingRegistry.getAddress(), + ); + + await bondingRegistryContract.bondingRegistry.setRewardDistributor( + enclaveAddress, + ); + + const tree = new LeanIMT(hash); + + const licenseToken = enclTokenContract.enclaveToken; + const ticketToken = ticketTokenContract.enclaveTicketToken; + + await licenseToken.setTransferRestriction(false); + + await setupOperatorForSortition( + operator1, + bondingRegistryContract.bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + ciphernodeRegistryContract, + ); + tree.insert(BigInt(await operator1.getAddress())); + + await setupOperatorForSortition( + operator2, + bondingRegistryContract.bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + ciphernodeRegistryContract, + ); + tree.insert(BigInt(await operator2.getAddress())); + + await mine(1); + const mockComputeProvider = await ignition.deploy( mockComputeProviderModule, ); @@ -135,9 +320,8 @@ describe("Enclave", function () { ); const request = { - filter: await naiveRegistryFilterContract.getAddress(), threshold: [2, 2] as [number, number], - startTime: [await time.latest(), (await time.latest()) + 100] as [ + startWindow: [await time.latest(), (await time.latest()) + 100] as [ number, number, ], @@ -154,10 +338,21 @@ describe("Enclave", function () { ), }; + await usdcToken.mint(ownerAddress, ethers.parseUnits("1000000", 6)); + await usdcToken.mint( + await notTheOwner.getAddress(), + ethers.parseUnits("1000000", 6), + ); + return { enclave, ciphernodeRegistryContract, - naiveRegistryFilterContract, + bondingRegistry: bondingRegistryContract.bondingRegistry, + ticketToken: ticketTokenContract.enclaveTicketToken, + licenseToken: licenseToken, + usdcToken, + slashingManager: slashingManagerContract.slashingManager, + tree, mocks: { decryptionVerifier: decryptionVerifier.mockDecryptionVerifier, inputValidator: inputValidator.mockInputValidator, @@ -167,6 +362,8 @@ describe("Enclave", function () { request, owner, notTheOwner, + operator1, + operator2, }; }; @@ -336,22 +533,17 @@ describe("Enclave", function () { }); it("returns correct E3 details", async function () { - const { enclave, request, mocks, naiveRegistryFilterContract } = - await loadFixture(setup); - - await enclave.request( - { - filter: await naiveRegistryFilterContract.getAddress(), - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + const { enclave, request, mocks, usdcToken } = await loadFixture(setup); + + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3 = await enclave.getE3(0); @@ -588,169 +780,143 @@ describe("Enclave", function () { }); describe("request()", function () { - it("reverts if msg.value is 0", async function () { - const { enclave, request } = await loadFixture(setup); + it("reverts if USDC allowance is insufficient", async function () { + const { enclave, request, usdcToken } = await loadFixture(setup); await expect( enclave.request({ - filter: request.filter, threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, customParams: request.customParams, }), - ).to.be.revertedWithCustomError(enclave, "PaymentRequired"); + ).to.be.revertedWithCustomError(usdcToken, "ERC20InsufficientAllowance"); }); it("reverts if threshold is 0", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); + const fee = await enclave.getE3Quote({ + threshold: [0, 2], + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + await usdcToken.approve(await enclave.getAddress(), fee); await expect( - enclave.request( - { - filter: request.filter, - threshold: [0, 2], - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidThreshold"); + enclave.request({ + threshold: [0, 2], + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidThreshold") + .withArgs([0, 2]); }); it("reverts if threshold is greater than number", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: [3, 2], - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidThreshold"); + makeRequest(enclave, usdcToken, { + threshold: [3, 2], + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidThreshold") + .withArgs([3, 2]); }); it("reverts if duration is 0", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: 0, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidDuration"); + makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: 0, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidDuration") + .withArgs(0); }); it("reverts if duration is greater than maxDuration", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: time.duration.days(31), - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "InvalidDuration"); + makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: time.duration.days(31), + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidDuration") + .withArgs(time.duration.days(31)); }); it("reverts if E3 Program is not enabled", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); + await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: ethers.ZeroAddress, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), + makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: ethers.ZeroAddress, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }), ) .to.be.revertedWithCustomError(enclave, "E3ProgramNotAllowed") .withArgs(ethers.ZeroAddress); }); it("reverts if given encryption scheme is not enabled", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); await enclave.disableEncryptionScheme(encryptionSchemeId); await expect( - enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), - ) - .to.be.revertedWithCustomError(enclave, "InvalidEncryptionScheme") - .withArgs(encryptionSchemeId); - }); - - it("reverts if committee selection fails", async function () { - const { enclave, request } = await loadFixture(setup); - await expect( - enclave.request( - { - filter: AddressTwo, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ), - ).to.be.revertedWithCustomError(enclave, "CommitteeSelectionFailed"); - }); - it("instantiates a new E3", async function () { - const { enclave, request, mocks } = await loadFixture(setup); - - await enclave.request( - { - filter: request.filter, + makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: request.startTime, + startWindow: request.startWindow, duration: request.duration, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, customParams: request.customParams, - }, - { value: 10 }, - ); + }), + ) + .to.be.revertedWithCustomError(enclave, "InvalidEncryptionScheme") + .withArgs(encryptionSchemeId); + }); + it("instantiates a new E3", async function () { + const { enclave, request, mocks, usdcToken } = await loadFixture(setup); + + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3 = await enclave.getE3(0); const block = await ethers.provider.getBlock("latest").catch((e) => e); @@ -770,25 +936,21 @@ describe("Enclave", function () { expect(e3.plaintextOutput).to.equal("0x"); }); it("emits E3Requested event", async function () { - const { enclave, request } = await loadFixture(setup); - const tx = await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + const { enclave, request, usdcToken } = await loadFixture(setup); + const tx = await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3 = await enclave.getE3(0); await expect(tx) .to.emit(enclave, "E3Requested") - .withArgs(0, e3, request.filter, request.e3Program); + .withArgs(0, e3, request.e3Program); }); }); @@ -801,151 +963,164 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if E3 has already been activated", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); await expect(enclave.getE3(0)).to.not.be.revert(ethers); - await expect(enclave.activate(0, ethers.ZeroHash)).to.not.be.revert( - ethers, - ); - await expect(enclave.activate(0, ethers.ZeroHash)) + await expect(enclave.activate(0, data)).to.not.be.revert(ethers); + await expect(enclave.activate(0, data)) .to.be.revertedWithCustomError(enclave, "E3AlreadyActivated") .withArgs(0); }); it("reverts if E3 is not yet ready to start", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const startTime = [ (await time.latest()) + 1000, (await time.latest()) + 2000, ] as [number, number]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: startTime, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); await expect( enclave.activate(0, ethers.ZeroHash), ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request } = await loadFixture(setup); - const startTime = [ - (await time.latest()) + 1, - (await time.latest()) + 1000, - ] as [number, number]; + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); + const e3Id = 0; + const currentTime = await time.latest(); + const startTime = [currentTime + 10, currentTime + 100] as [ + number, + number, + ]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: startTime, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); await mine(2, { interval: 2000 }); - await expect( - enclave.activate(0, ethers.ZeroHash), - ).to.be.revertedWithCustomError(enclave, "E3Expired"); + await expect(enclave.activate(e3Id, data)).to.be.revertedWithCustomError( + enclave, + "E3Expired", + ); }); it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const startTime = [ (await time.latest()) + 1000, (await time.latest()) + 2000, ] as [number, number]; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: startTime, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); await expect( enclave.activate(0, ethers.ZeroHash), ).to.be.revertedWithCustomError(enclave, "E3NotReady"); }); it("reverts if E3 start has expired", async function () { - const { enclave, request } = await loadFixture(setup); - const startTime = [await time.latest(), (await time.latest()) + 1] as [ - number, - number, - ]; - - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); + const e3Id = 0; + const currentTime = await time.latest(); + const startTime = [currentTime + 5, currentTime + 50] as [number, number]; + + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: startTime, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await mine(1, { interval: 1000 }); + await time.increaseTo(currentTime + request.duration + 100); - await expect( - enclave.activate(0, ethers.ZeroHash), - ).to.be.revertedWithCustomError(enclave, "E3Expired"); + await expect(enclave.activate(e3Id, data)).to.be.revertedWithCustomError( + enclave, + "E3Expired", + ); }); it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request, naiveRegistryFilterContract } = - await loadFixture(setup); + const { + enclave, + request, + ciphernodeRegistryContract, + usdcToken, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, request); const prevRegistry = await enclave.ciphernodeRegistry(); @@ -954,91 +1129,132 @@ describe("Enclave", function () { await reg.mockCiphernodeRegistryEmptyKey.getAddress(); await enclave.setCiphernodeRegistry(nextRegistry); - await naiveRegistryFilterContract.setRegistry(nextRegistry); await expect( enclave.activate(0, ethers.ZeroHash), ).to.be.revertedWithCustomError(enclave, "CommitteeSelectionFailed"); await enclave.setCiphernodeRegistry(prevRegistry); - await expect(enclave.activate(0, ethers.ZeroHash)).not.to.be.revert( - ethers, + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); + + await expect(enclave.activate(0, data)).not.to.be.revert(ethers); }); it("sets committeePublicKey correctly", async () => { - const { enclave, request, ciphernodeRegistryContract } = - await loadFixture(setup); + const { + enclave, + request, + ciphernodeRegistryContract, + usdcToken, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3Id = 0; + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + const publicKey = await ciphernodeRegistryContract.committeePublicKey(e3Id); let e3 = await enclave.getE3(e3Id); - expect(e3.committeePublicKey).to.not.equal(ethers.keccak256(publicKey)); + expect(e3.committeePublicKey).to.not.equal(publicKey); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); e3 = await enclave.getE3(e3Id); expect(e3.committeePublicKey).to.equal(publicKey); }); it("returns true if E3 is activated successfully", async () => { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3Id = 0; - expect( - await enclave.activate.staticCall(e3Id, ethers.ZeroHash), - ).to.be.equal(true); + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + + expect(await enclave.activate.staticCall(e3Id, data)).to.be.equal(true); }); it("emits E3Activated event", async () => { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3Id = 0; - await expect(enclave.activate(e3Id, ethers.ZeroHash)).to.emit( + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + + await expect(enclave.activate(e3Id, data)).to.emit( enclave, "E3Activated", ); @@ -1055,21 +1271,17 @@ describe("Enclave", function () { }); it("reverts if E3 has not been activated", async function () { - const { enclave, request } = await loadFixture(setup); - - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + const { enclave, request, usdcToken } = await loadFixture(setup); + + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const inputData = abiCoder.encode(["bytes32"], [ethers.ZeroHash]); @@ -1077,51 +1289,71 @@ describe("Enclave", function () { await expect(enclave.publishInput(0, inputData)) .to.be.revertedWithCustomError(enclave, "E3NotActivated") .withArgs(0); - - await enclave.activate(0, ethers.ZeroHash); }); it("reverts if input is not valid", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - - await enclave.activate(0, ethers.ZeroHash); + await enclave.activate(0, data); await expect( enclave.publishInput(0, "0xaabbcc"), ).to.be.revertedWithCustomError(enclave, "InvalidInput"); }); it("reverts if outside of input window", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - - await enclave.activate(0, ethers.ZeroHash); + await enclave.activate(0, data); await mine(2, { interval: request.duration }); @@ -1133,65 +1365,88 @@ describe("Enclave", function () { it("it allows publishing input to different requests", async function () { const fixtureSetup = () => setup(); - const { enclave, request } = await loadFixture(fixtureSetup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(fixtureSetup); const inputData = "0x12345678"; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - - await enclave.activate(0, ethers.ZeroHash); + await enclave.activate(0, data); await enclave.publishInput(0, inputData); - // Make a new request, activate and call - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); - await enclave.activate( + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, 1, - "0x0000000000000000000000000000000000000000000000000000000000000001", + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); + await enclave.activate(1, data); await enclave.publishInput(1, inputData); }); it("returns true if input is published successfully", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const inputData = "0x12345678"; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - - await enclave.activate(0, ethers.ZeroHash); + await enclave.activate(0, data); expect(await enclave.publishInput.staticCall(0, inputData)).to.equal( true, @@ -1199,29 +1454,39 @@ describe("Enclave", function () { }); it("adds inputHash to merkle tree", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); - // To create an instance of a LeanIMT, you must provide the hash function. const tree = new LeanIMT(hash); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3Id = 0; - await enclave.activate(e3Id, ethers.ZeroHash); + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + await enclave.activate(e3Id, data); tree.insert(hash(BigInt(ethers.keccak256(inputData)), BigInt(0))); @@ -1234,26 +1499,37 @@ describe("Enclave", function () { expect(await enclave.getInputRoot(e3Id)).to.equal(tree.root); }); it("emits InputPublished event", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); const e3Id = 0; const inputData = abiCoder.encode(["bytes"], ["0xaabbccddeeff"]); - await enclave.activate(e3Id, ethers.ZeroHash); + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + await enclave.activate(e3Id, data); const expectedHash = hash(BigInt(ethers.keccak256(inputData)), BigInt(0)); await expect(enclave.publishInput(e3Id, inputData)) @@ -1261,24 +1537,35 @@ describe("Enclave", function () { .withArgs(e3Id, inputData, expectedHash, 0); }); it("increases the input count", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const inputData = "0x12345678"; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + 0, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - - await enclave.activate(0, ethers.ZeroHash); + await enclave.activate(0, data); await enclave.publishInput(0, inputData); expect(await enclave.getInputsLength(0)).to.equal(1n); @@ -1294,70 +1581,82 @@ describe("Enclave", function () { .withArgs(0); }); it("reverts if E3 has not been activated", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: request.startTime, - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: request.startWindow, + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); await expect(enclave.publishCiphertextOutput(e3Id, "0x", "0x")) .to.be.revertedWithCustomError(enclave, "E3NotActivated") .withArgs(e3Id); }); it("reverts if input deadline has not passed", async function () { - const { enclave, request } = await loadFixture(setup); - const tx = await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); - const block = await tx.getBlock(); - const timestamp = block ? block.timestamp : await time.latest(); - const expectedExpiration = timestamp + request.duration + 1; + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); + const currentTime = await time.latest(); + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [currentTime, currentTime + 100], + }); const e3Id = 0; - await enclave.activate(e3Id, ethers.ZeroHash); + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, + ); + await enclave.activate(e3Id, data); - await expect(enclave.publishCiphertextOutput(e3Id, "0x", "0x")) - .to.be.revertedWithCustomError(enclave, "InputDeadlineNotPassed") - .withArgs(e3Id, expectedExpiration); + await expect( + enclave.publishCiphertextOutput(e3Id, "0x", "0x"), + ).to.be.revertedWithCustomError(enclave, "InputDeadlineNotPassed"); }); it("reverts if output has already been published", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: [await time.latest(), (await time.latest()) + 100], + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); expect(await enclave.publishCiphertextOutput(e3Id, data, proof)); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) @@ -1368,92 +1667,125 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + threshold: request.threshold, + startWindow: [await time.latest(), (await time.latest()) + 100], + duration: request.duration, + e3Program: request.e3Program, + e3ProgramParams: request.e3ProgramParams, + computeProviderParams: request.computeProviderParams, + customParams: request.customParams, + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await expect( enclave.publishCiphertextOutput(e3Id, "0x", "0x"), ).to.be.revertedWithCustomError(enclave, "InvalidOutput"); }); it("sets ciphertextOutput correctly", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); expect(await enclave.publishCiphertextOutput(e3Id, data, proof)); const e3 = await enclave.getE3(e3Id); expect(e3.ciphertextOutput).to.equal(dataHash); }); it("returns true if output is published successfully", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); expect( await enclave.publishCiphertextOutput.staticCall(e3Id, data, proof), ).to.equal(true); }); it("emits CiphertextOutputPublished event", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) .to.emit(enclave, "CiphertextOutputPublished") @@ -1472,66 +1804,71 @@ describe("Enclave", function () { }); it("reverts if E3 has not been activated", async function () { - const { enclave, request } = await loadFixture(setup); + const { enclave, request, usdcToken } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, - ); + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) .to.be.revertedWithCustomError(enclave, "E3NotActivated") .withArgs(e3Id); }); it("reverts if ciphertextOutput has not been published", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) .to.be.revertedWithCustomError(enclave, "CiphertextOutputNotPublished") .withArgs(e3Id); }); it("reverts if plaintextOutput has already been published", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await enclave.publishPlaintextOutput(e3Id, data, proof); @@ -1543,23 +1880,30 @@ describe("Enclave", function () { .withArgs(e3Id); }); it("reverts if output is not valid", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) @@ -1567,23 +1911,30 @@ describe("Enclave", function () { .withArgs(data); }); it("sets plaintextOutput correctly", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); expect(await enclave.publishPlaintextOutput(e3Id, data, proof)); @@ -1592,23 +1943,30 @@ describe("Enclave", function () { expect(e3.plaintextOutput).to.equal(data); }); it("returns true if output is published successfully", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); expect( @@ -1616,23 +1974,30 @@ describe("Enclave", function () { ).to.equal(true); }); it("emits PlaintextOutputPublished event", async function () { - const { enclave, request } = await loadFixture(setup); + const { + enclave, + request, + usdcToken, + ciphernodeRegistryContract, + operator1, + operator2, + } = await loadFixture(setup); const e3Id = 0; - await enclave.request( - { - filter: request.filter, - threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, - e3Program: request.e3Program, - e3ProgramParams: request.e3ProgramParams, - computeProviderParams: request.computeProviderParams, - customParams: request.customParams, - }, - { value: 10 }, + await makeRequest(enclave, usdcToken, { + ...request, + startWindow: [await time.latest(), (await time.latest()) + 100], + }); + + await setupAndPublishCommittee( + ciphernodeRegistryContract, + e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + operator1, + operator2, ); - await enclave.activate(e3Id, ethers.ZeroHash); + await enclave.activate(e3Id, data); await mine(2, { interval: request.duration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(await enclave.publishPlaintextOutput(e3Id, data, proof)) diff --git a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts new file mode 100644 index 0000000000..b53d785db1 --- /dev/null +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -0,0 +1,1159 @@ +// 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. +import { LeanIMT } from "@zk-kit/lean-imt"; +import { expect } from "chai"; +import { network } from "hardhat"; +import { poseidon2 } from "poseidon-lite"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockCiphernodeRegistryModule from "../../ignition/modules/mockCiphernodeRegistry"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, +} from "../../types"; + +const AddressOne = "0x0000000000000000000000000000000000000001"; + +const { ethers, networkHelpers, ignition } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +const hash = (a: bigint, b: bigint) => poseidon2([a, b]); + +const REASON_DEPOSIT = ethers.encodeBytes32String("DEPOSIT"); +const REASON_WITHDRAW = ethers.encodeBytes32String("WITHDRAW"); +const REASON_BOND = ethers.encodeBytes32String("BOND"); +const REASON_UNBOND = ethers.encodeBytes32String("UNBOND"); + +describe("BondingRegistry", function () { + const SEVEN_DAYS_IN_SECONDS = 7 * 24 * 60 * 60; + const TICKET_PRICE = ethers.parseUnits("10", 6); + const LICENSE_REQUIRED_BOND = ethers.parseEther("1000"); + const MIN_TICKET_BALANCE = 5; + async function setup() { + const [owner, operator1, operator2, treasury, notTheOwner] = + await ethers.getSigners(); + + const ownerAddress = await owner.getAddress(); + const operator1Address = await operator1.getAddress(); + const operator2Address = await operator2.getAddress(); + const treasuryAddress = await treasury.getAddress(); + + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, + }, + }, + }); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + const ciphernodeRegistryContract = await ignition.deploy( + MockCiphernodeRegistryModule, + { + parameters: { + CiphernodeRegistry: { + enclaveAddress: ownerAddress, + owner: ownerAddress, + }, + }, + }, + ); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcContract.mockUSDC.getAddress(), + registry: AddressOne, + owner: ownerAddress, + }, + }, + }, + ); + + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: AddressOne, + }, + }, + }, + ); + + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: + await ciphernodeRegistryContract.mockCiphernodeRegistry.getAddress(), + slashedFundsTreasury: treasuryAddress, + ticketPrice: TICKET_PRICE, + licenseRequiredBond: LICENSE_REQUIRED_BOND, + minTicketBalance: MIN_TICKET_BALANCE, + exitDelay: SEVEN_DAYS_IN_SECONDS, + }, + }, + }, + ); + + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + const ticketToken = EnclaveTicketTokenFactory.connect( + await ticketTokenContract.enclaveTicketToken.getAddress(), + owner, + ); + const licenseToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + const slashingManager = SlashingManagerFactory.connect( + await slashingManagerContract.slashingManager.getAddress(), + owner, + ); + const ciphernodeRegistry = CiphernodeRegistryOwnableFactory.connect( + await ciphernodeRegistryContract.mockCiphernodeRegistry.getAddress(), + owner, + ); + + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), + ); + + await usdcToken.mint(ownerAddress, ethers.parseUnits("100000", 6)); + await usdcToken.mint(operator1Address, ethers.parseUnits("100000", 6)); + await usdcToken.mint(operator2Address, ethers.parseUnits("100000", 6)); + await licenseToken.mintAllocation( + ownerAddress, + ethers.parseEther("100000"), + "Test allocation", + ); + await licenseToken.mintAllocation( + operator1Address, + ethers.parseEther("100000"), + "Test allocation", + ); + await licenseToken.mintAllocation( + operator2Address, + ethers.parseEther("100000"), + "Test allocation", + ); + + await licenseToken.setTransferRestriction(false); + + const tree = new LeanIMT(hash); + + return { + bondingRegistry, + ticketToken, + licenseToken, + usdcToken, + slashingManager, + ciphernodeRegistry, + tree, + owner, + operator1, + operator2, + treasury, + notTheOwner, + ownerAddress, + operator1Address, + operator2Address, + treasuryAddress, + }; + } + + describe("constructor / initialize()", function () { + it("correctly sets initial parameters", async function () { + const { bondingRegistry, ticketToken, licenseToken, treasuryAddress } = + await loadFixture(setup); + + expect(await bondingRegistry.ticketToken()).to.equal( + await ticketToken.getAddress(), + ); + expect(await bondingRegistry.licenseToken()).to.equal( + await licenseToken.getAddress(), + ); + expect(await bondingRegistry.slashedFundsTreasury()).to.equal( + treasuryAddress, + ); + expect(await bondingRegistry.ticketPrice()).to.equal(TICKET_PRICE); + expect(await bondingRegistry.licenseRequiredBond()).to.equal( + LICENSE_REQUIRED_BOND, + ); + expect(await bondingRegistry.minTicketBalance()).to.equal( + MIN_TICKET_BALANCE, + ); + expect(await bondingRegistry.exitDelay()).to.equal(SEVEN_DAYS_IN_SECONDS); + expect(await bondingRegistry.licenseActiveBps()).to.equal(8000); + }); + }); + + describe("bondLicense()", function () { + it("allows operators to bond license tokens", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + + await expect(bondingRegistry.connect(operator1).bondLicense(bondAmount)) + .to.emit(bondingRegistry, "LicenseBondUpdated") + .withArgs( + await operator1.getAddress(), + bondAmount, + bondAmount, + REASON_BOND, + ); + + expect( + await bondingRegistry.getLicenseBond(await operator1.getAddress()), + ).to.equal(bondAmount); + }); + + it("reverts if amount is zero", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).bondLicense(0), + ).to.be.revertedWithCustomError(bondingRegistry, "ZeroAmount"); + }); + + it("reverts if exit is in progress", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + await bondingRegistry.connect(operator1).registerOperator(); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await expect( + bondingRegistry.connect(operator1).bondLicense(bondAmount), + ).to.be.revertedWithCustomError(bondingRegistry, "ExitInProgress"); + }); + + it("accumulates multiple bond amounts", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount1 = ethers.parseEther("500"); + const bondAmount2 = ethers.parseEther("300"); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount1); + await bondingRegistry.connect(operator1).bondLicense(bondAmount1); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount2); + await bondingRegistry.connect(operator1).bondLicense(bondAmount2); + + expect( + await bondingRegistry.getLicenseBond(await operator1.getAddress()), + ).to.equal(bondAmount1 + bondAmount2); + }); + }); + + describe("unbondLicense()", function () { + it("allows operators to unbond license tokens", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + const unbondAmount = ethers.parseEther("200"); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + await expect( + bondingRegistry.connect(operator1).unbondLicense(unbondAmount), + ) + .to.emit(bondingRegistry, "LicenseBondUpdated") + .withArgs( + await operator1.getAddress(), + -unbondAmount, + bondAmount - unbondAmount, + REASON_UNBOND, + ); + + expect( + await bondingRegistry.getLicenseBond(await operator1.getAddress()), + ).to.equal(bondAmount - unbondAmount); + }); + + it("reverts if amount is zero", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).unbondLicense(0), + ).to.be.revertedWithCustomError(bondingRegistry, "ZeroAmount"); + }); + + it("reverts if insufficient balance", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry + .connect(operator1) + .unbondLicense(ethers.parseEther("100")), + ).to.be.revertedWithCustomError(bondingRegistry, "InsufficientBalance"); + }); + + it("queues license tokens for exit", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = ethers.parseEther("1000"); + const unbondAmount = ethers.parseEther("200"); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + await bondingRegistry.connect(operator1).unbondLicense(unbondAmount); + + const [, licensePending] = await bondingRegistry.pendingExits( + await operator1.getAddress(), + ); + expect(licensePending).to.equal(unbondAmount); + }); + }); + + describe("registerOperator()", function () { + it("allows properly licensed operators to register", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + + await bondingRegistry.connect(operator1).registerOperator(); + + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + }); + + it("reverts if not properly licensed", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).registerOperator(), + ).to.be.revertedWithCustomError(bondingRegistry, "NotLicensed"); + }); + + it("reverts if already registered", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await expect( + bondingRegistry.connect(operator1).registerOperator(), + ).to.be.revertedWithCustomError(bondingRegistry, "AlreadyRegistered"); + }); + + it("clears previous exit request when re-registering", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + expect( + await bondingRegistry.hasExitInProgress(await operator1.getAddress()), + ).to.be.false; + }); + }); + + describe("deregisterOperator()", function () { + it("allows registered operators to deregister", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const latestTime = await time.latest(); + await expect(bondingRegistry.connect(operator1).deregisterOperator([])) + .to.emit(bondingRegistry, "CiphernodeDeregistrationRequested") + .withArgs( + await operator1.getAddress(), + latestTime + SEVEN_DAYS_IN_SECONDS + 1, + ); + + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.false; + expect( + await bondingRegistry.hasExitInProgress(await operator1.getAddress()), + ).to.be.true; + }); + + it("reverts if not registered", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(operator1).deregisterOperator([]), + ).to.be.revertedWithCustomError(bondingRegistry, "NotRegistered"); + }); + + it("queues assets for exit when deregistering", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + const [ticketPending, licensePending] = + await bondingRegistry.pendingExits(await operator1.getAddress()); + expect(ticketPending).to.equal(ticketAmount); + expect(licensePending).to.equal(bondAmount); + }); + }); + + describe("addTicketBalance()", function () { + it("allows registered operators to add ticket balance", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + + await expect( + bondingRegistry.connect(operator1).addTicketBalance(ticketAmount), + ) + .to.emit(bondingRegistry, "TicketBalanceUpdated") + .withArgs( + await operator1.getAddress(), + ticketAmount, + ticketAmount, + REASON_DEPOSIT, + ); + + expect( + await bondingRegistry.getTicketBalance(await operator1.getAddress()), + ).to.equal(ticketAmount); + }); + + it("activates operator when minimum balance is reached", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("50", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + + await expect( + bondingRegistry.connect(operator1).addTicketBalance(ticketAmount), + ) + .to.emit(bondingRegistry, "OperatorActivationChanged") + .withArgs(await operator1.getAddress(), true); + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .true; + }); + + it("reverts if not registered", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + await expect( + bondingRegistry + .connect(operator1) + .addTicketBalance(ethers.parseUnits("100", 6)), + ).to.be.revertedWithCustomError(bondingRegistry, "NotRegistered"); + }); + + it("reverts if amount is zero", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await expect( + bondingRegistry.connect(operator1).addTicketBalance(0), + ).to.be.revertedWithCustomError(bondingRegistry, "ZeroAmount"); + }); + }); + + describe("removeTicketBalance()", function () { + it("allows operators to remove ticket balance", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + const removeAmount = ethers.parseUnits("30", 6); + await expect( + bondingRegistry.connect(operator1).removeTicketBalance(removeAmount), + ) + .to.emit(bondingRegistry, "TicketBalanceUpdated") + .withArgs( + await operator1.getAddress(), + -removeAmount, + ticketAmount - removeAmount, + REASON_WITHDRAW, + ); + + expect( + await bondingRegistry.getTicketBalance(await operator1.getAddress()), + ).to.equal(ticketAmount - removeAmount); + }); + + it("queues removed tickets for exit", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + const removeAmount = ethers.parseUnits("30", 6); + await bondingRegistry + .connect(operator1) + .removeTicketBalance(removeAmount); + + const [ticketPending] = await bondingRegistry.pendingExits( + await operator1.getAddress(), + ); + expect(ticketPending).to.equal(removeAmount); + }); + + it("deactivates operator if balance falls below minimum", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + const removeAmount = ethers.parseUnits("20", 6); + await expect( + bondingRegistry.connect(operator1).removeTicketBalance(removeAmount), + ) + .to.emit(bondingRegistry, "OperatorActivationChanged") + .withArgs(await operator1.getAddress(), false); + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + }); + + it("reverts if insufficient balance", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await expect( + bondingRegistry + .connect(operator1) + .removeTicketBalance(ethers.parseUnits("100", 6)), + ).to.be.revertedWithCustomError(bondingRegistry, "InsufficientBalance"); + }); + }); + + describe("claimExits()", function () { + it("allows claiming after exit delay", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + const initialUSDCBalance = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const initialENCLBalance = await licenseToken.balanceOf( + await operator1.getAddress(), + ); + + await bondingRegistry + .connect(operator1) + .claimExits(ticketAmount, bondAmount); + + expect(await usdcToken.balanceOf(await operator1.getAddress())).to.equal( + initialUSDCBalance + ticketAmount, + ); + expect( + await licenseToken.balanceOf(await operator1.getAddress()), + ).to.equal(initialENCLBalance + bondAmount); + }); + + it("reverts if exit not ready", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + await expect( + bondingRegistry.connect(operator1).claimExits(0, bondAmount), + ).to.be.revertedWithCustomError(bondingRegistry, "ExitNotReady"); + }); + + it("allows partial claims", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + await bondingRegistry.connect(operator1).deregisterOperator([]); + + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + const partialTickets = ethers.parseUnits("50", 6); + const partialLicense = ethers.parseEther("500"); + + const initialUSDCBalance = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const initialENCLBalance = await licenseToken.balanceOf( + await operator1.getAddress(), + ); + + await bondingRegistry + .connect(operator1) + .claimExits(partialTickets, partialLicense); + + expect(await usdcToken.balanceOf(await operator1.getAddress())).to.equal( + initialUSDCBalance + partialTickets, + ); + expect( + await licenseToken.balanceOf(await operator1.getAddress()), + ).to.equal(initialENCLBalance + partialLicense); + + const [remainingTickets, remainingLicense] = + await bondingRegistry.pendingExits(await operator1.getAddress()); + expect(remainingTickets).to.equal(ticketAmount - partialTickets); + expect(remainingLicense).to.equal(bondAmount - partialLicense); + }); + }); + + describe("isLicensed()", function () { + it("returns true when operator has minimum license bond", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const minBond = (LICENSE_REQUIRED_BOND * 8000n) / 10000n; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), minBond); + await bondingRegistry.connect(operator1).bondLicense(minBond); + + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.true; + }); + + it("returns false when operator has insufficient license bond", async function () { + const { bondingRegistry, licenseToken, operator1 } = + await loadFixture(setup); + + const insufficientBond = (LICENSE_REQUIRED_BOND * 7999n) / 10000n; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), insufficientBond); + await bondingRegistry.connect(operator1).bondLicense(insufficientBond); + + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.false; + }); + }); + + describe("availableTickets()", function () { + it("calculates available tickets correctly", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + expect( + await bondingRegistry.availableTickets(await operator1.getAddress()), + ).to.equal(10); + }); + + it("returns 0 when operator has zero ticket balance", async function () { + const { bondingRegistry, operator1 } = await loadFixture(setup); + + expect( + await bondingRegistry.availableTickets(await operator1.getAddress()), + ).to.equal(0); + }); + }); + + describe("Admin Functions", function () { + describe("setTicketPrice()", function () { + it("allows owner to set ticket price", async function () { + const { bondingRegistry } = await loadFixture(setup); + + const newPrice = ethers.parseUnits("15", 6); + await expect(bondingRegistry.setTicketPrice(newPrice)) + .to.emit(bondingRegistry, "ConfigurationUpdated") + .withArgs( + ethers.encodeBytes32String("ticketPrice"), + TICKET_PRICE, + newPrice, + ); + + expect(await bondingRegistry.ticketPrice()).to.equal(newPrice); + }); + + it("reverts if price is zero", async function () { + const { bondingRegistry } = await loadFixture(setup); + + await expect( + bondingRegistry.setTicketPrice(0), + ).to.be.revertedWithCustomError( + bondingRegistry, + "InvalidConfiguration", + ); + }); + + it("reverts if not owner", async function () { + const { bondingRegistry, notTheOwner } = await loadFixture(setup); + + await expect( + bondingRegistry + .connect(notTheOwner) + .setTicketPrice(ethers.parseEther("15")), + ).to.be.revertedWithCustomError( + bondingRegistry, + "OwnableUnauthorizedAccount", + ); + }); + }); + + describe("setLicenseActiveBps()", function () { + it("allows owner to set license active basis points", async function () { + const { bondingRegistry } = await loadFixture(setup); + + const newBps = 9000; + await expect(bondingRegistry.setLicenseActiveBps(newBps)) + .to.emit(bondingRegistry, "ConfigurationUpdated") + .withArgs( + ethers.encodeBytes32String("licenseActiveBps"), + 8000, + newBps, + ); + + expect(await bondingRegistry.licenseActiveBps()).to.equal(newBps); + }); + + it("reverts if bps is 0", async function () { + const { bondingRegistry } = await loadFixture(setup); + + await expect( + bondingRegistry.setLicenseActiveBps(0), + ).to.be.revertedWithCustomError( + bondingRegistry, + "InvalidConfiguration", + ); + }); + + it("reverts if bps is greater than 10000", async function () { + const { bondingRegistry } = await loadFixture(setup); + + await expect( + bondingRegistry.setLicenseActiveBps(10001), + ).to.be.revertedWithCustomError( + bondingRegistry, + "InvalidConfiguration", + ); + }); + }); + + describe("withdrawSlashedFunds()", function () { + it("allows owner to withdraw slashed funds", async function () { + const { bondingRegistry, treasury } = await loadFixture(setup); + + await expect(bondingRegistry.withdrawSlashedFunds(0, 0)) + .to.emit(bondingRegistry, "SlashedFundsWithdrawn") + .withArgs(await treasury.getAddress(), 0, 0); + }); + + it("reverts if not owner", async function () { + const { bondingRegistry, notTheOwner } = await loadFixture(setup); + + await expect( + bondingRegistry.connect(notTheOwner).withdrawSlashedFunds(0, 0), + ).to.be.revertedWithCustomError( + bondingRegistry, + "OwnableUnauthorizedAccount", + ); + }); + }); + }); + + describe("Edge Cases and Complex Scenarios", function () { + it("handles operator becoming inactive due to license reduction", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .true; + + const unbondAmount = LICENSE_REQUIRED_BOND / 5n; + await bondingRegistry.connect(operator1).unbondLicense(unbondAmount + 1n); + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.false; + }); + + it("handles multiple operators with different states", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + operator2, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + + await licenseToken + .connect(operator2) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator2).bondLicense(bondAmount); + await bondingRegistry.connect(operator2).registerOperator(); + + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator2) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator2).addTicketBalance(ticketAmount); + + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + + expect(await bondingRegistry.isRegistered(await operator2.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator2.getAddress())).to.be + .true; + }); + + it("handles the complete operator lifecycle", async function () { + const { + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + operator1, + } = await loadFixture(setup); + + const bondAmount = LICENSE_REQUIRED_BOND; + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + expect(await bondingRegistry.isLicensed(await operator1.getAddress())).to + .be.true; + + await bondingRegistry.connect(operator1).registerOperator(); + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .false; + + const ticketAmount = ethers.parseUnits("60", 6); + await usdcToken + .connect(operator1) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator1).addTicketBalance(ticketAmount); + expect(await bondingRegistry.isActive(await operator1.getAddress())).to.be + .true; + + await bondingRegistry.connect(operator1).deregisterOperator([]); + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.false; + expect( + await bondingRegistry.hasExitInProgress(await operator1.getAddress()), + ).to.be.true; + + await time.increase(SEVEN_DAYS_IN_SECONDS + 1); + + const initialUSDCBalance = await usdcToken.balanceOf( + await operator1.getAddress(), + ); + const initialENCLBalance = await licenseToken.balanceOf( + await operator1.getAddress(), + ); + + await bondingRegistry + .connect(operator1) + .claimExits(ticketAmount, bondAmount); + + expect(await usdcToken.balanceOf(await operator1.getAddress())).to.equal( + initialUSDCBalance + ticketAmount, + ); + expect( + await licenseToken.balanceOf(await operator1.getAddress()), + ).to.equal(initialENCLBalance + bondAmount); + + await licenseToken + .connect(operator1) + .approve(await bondingRegistry.getAddress(), bondAmount); + await bondingRegistry.connect(operator1).bondLicense(bondAmount); + await bondingRegistry.connect(operator1).registerOperator(); + expect(await bondingRegistry.isRegistered(await operator1.getAddress())) + .to.be.true; + }); + }); +}); diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts new file mode 100644 index 0000000000..c5f09606a7 --- /dev/null +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -0,0 +1,543 @@ +// 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. +import { LeanIMT } from "@zk-kit/lean-imt"; +import { expect } from "chai"; +import type { Signer } from "ethers"; +import { network } from "hardhat"; +import { poseidon2 } from "poseidon-lite"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory } from "../../types"; + +const AddressOne = "0x0000000000000000000000000000000000000001"; +const AddressTwo = "0x0000000000000000000000000000000000000002"; +const AddressThree = "0x0000000000000000000000000000000000000003"; + +const { ethers, networkHelpers, ignition } = await network.connect(); +const { loadFixture } = networkHelpers; + +const data = "0xda7a"; +const dataHash = ethers.keccak256(data); +const SORTITION_SUBMISSION_WINDOW = 3; + +// Hash function used to compute the tree nodes. +const hash = (a: bigint, b: bigint) => poseidon2([a, b]); + +describe("CiphernodeRegistryOwnable", function () { + async function finalizeCommitteeAfterWindow( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registry: any, + e3Id: number, + ): Promise { + await networkHelpers.time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(e3Id); + } + + async function setupOperatorForSortition( + operator: Signer, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bondingRegistry: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + licenseToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + usdcToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ticketToken: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + registry: any, + ): Promise { + const operatorAddress = await operator.getAddress(); + + await licenseToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await licenseToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(await ticketToken.getAddress(), ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + + await registry.addCiphernode(operatorAddress); + } + + async function setup() { + const [owner, notTheOwner, operator1, operator2] = + await ethers.getSigners(); + const ownerAddress = await owner.getAddress(); + + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, + }, + }, + }); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcContract.mockUSDC.getAddress(), + registry: AddressOne, + owner: ownerAddress, + }, + }, + }, + ); + + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: AddressOne, + }, + }, + }, + ); + + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: AddressOne, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: 7 * 24 * 60 * 60, + }, + }, + }, + ); + + const registryContract = await ignition.deploy(CiphernodeRegistryModule, { + parameters: { + CiphernodeRegistry: { + enclaveAddress: ownerAddress, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + }, + }, + }); + + const registryAddress = + await registryContract.cipherNodeRegistry.getAddress(); + + const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); + const bondingRegistry = bondingRegistryContract.bondingRegistry; + + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistry.getAddress(), + ); + await bondingRegistry.setRegistry(registryAddress); + await bondingRegistry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); + await slashingManagerContract.slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + + await registry.setBondingRegistry(await bondingRegistry.getAddress()); + + const tree = new LeanIMT(hash); + const licenseToken = enclTokenContract.enclaveToken; + const ticketToken = ticketTokenContract.enclaveTicketToken; + const usdcToken = usdcContract.mockUSDC; + + await licenseToken.setTransferRestriction(false); + await setupOperatorForSortition( + operator1, + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + registry, + ); + tree.insert(BigInt(await operator1.getAddress())); + + await setupOperatorForSortition( + operator2, + bondingRegistry, + licenseToken, + usdcToken, + ticketToken, + registry, + ); + tree.insert(BigInt(await operator2.getAddress())); + await networkHelpers.mine(1); + + return { + owner, + notTheOwner, + operator1, + operator2, + registry, + bondingRegistry, + licenseToken, + ticketToken, + usdcToken, + tree, + request: { + e3Id: 1, + threshold: [2, 2] as [number, number], + }, + }; + } + + describe("constructor / initialize()", function () { + it("correctly sets `_owner` and `enclave` ", async function () { + const poseidonFactory = await ethers.getContractFactory("PoseidonT3"); + const poseidonDeployment = await poseidonFactory.deploy(); + const [deployer] = await ethers.getSigners(); + if (!deployer) throw new Error("Bad getSigners() output"); + const ciphernodeRegistryFactory = await ethers.getContractFactory( + "CiphernodeRegistryOwnable", + { + libraries: { + PoseidonT3: await poseidonDeployment.getAddress(), + }, + }, + ); + const ciphernodeRegistry = await ciphernodeRegistryFactory.deploy( + deployer.address, + AddressTwo, + SORTITION_SUBMISSION_WINDOW, + ); + expect(await ciphernodeRegistry.owner()).to.equal(deployer.address); + expect(await ciphernodeRegistry.enclave()).to.equal(AddressTwo); + expect(await ciphernodeRegistry.sortitionSubmissionWindow()).to.equal( + SORTITION_SUBMISSION_WINDOW, + ); + }); + }); + + describe("requestCommittee()", function () { + it("reverts if committee has already been requested for given e3Id", async function () { + const { registry, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + await expect( + registry.requestCommittee(request.e3Id, 0, request.threshold), + ).to.be.revertedWithCustomError(registry, "CommitteeAlreadyRequested"); + }); + it("stores the root of the ciphernode registry at the time of the request", async function () { + const { registry, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + expect(await registry.rootAt(request.e3Id)).to.equal( + await registry.root(), + ); + }); + it("emits a CommitteeRequested event", async function () { + const { registry, request } = await loadFixture(setup); + + const tx = await registry.requestCommittee( + request.e3Id, + 0n, + request.threshold, + ); + const receipt = await tx.wait(); + if (!receipt) throw new Error("Transaction failed"); + + const sWindow = await registry.sortitionSubmissionWindow(); + const block = await ethers.provider.getBlock(receipt.blockNumber); + if (!block) throw new Error("Block not found"); + + const expectedBlockNumber = BigInt(receipt.blockNumber); + const expectedDeadline = BigInt(block.timestamp) + sWindow; + + await expect(tx) + .to.emit(registry, "CommitteeRequested") + .withArgs( + request.e3Id, + 0n, + request.threshold, + expectedBlockNumber, + expectedDeadline, + ); + }); + it("returns true if the request is successful", async function () { + const { registry, request } = await loadFixture(setup); + expect( + await registry.requestCommittee.staticCall( + request.e3Id, + 0, + request.threshold, + ), + ).to.be.true; + }); + }); + + describe("publishCommittee()", function () { + it("reverts if the caller is not the owner", async function () { + const { registry, request, notTheOwner, operator1, operator2 } = + await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + + await expect( + registry + .connect(notTheOwner) + .publishCommittee( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ), + ).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount"); + }); + it("stores the public key of the committee", async function () { + const { registry, request, operator1, operator2 } = + await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + + await networkHelpers.mine(1); + + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + + await registry.publishCommittee( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ); + expect(await registry.committeePublicKey(request.e3Id)).to.equal( + dataHash, + ); + }); + it("emits a CommitteePublished event", async function () { + const { registry, request, operator1, operator2 } = + await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + + // Submit tickets from both operators and finalize + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + + await expect( + await registry.publishCommittee( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ), + ) + .to.emit(registry, "CommitteePublished") + .withArgs( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ); + }); + }); + + describe("addCiphernode()", function () { + it("reverts if the caller is not the owner", async function () { + const { registry, notTheOwner } = await loadFixture(setup); + await expect( + registry.connect(notTheOwner).addCiphernode(AddressThree), + ).to.be.revertedWithCustomError(registry, "NotOwnerOrBondingRegistry"); + }); + it("adds the ciphernode to the registry", async function () { + const { registry } = await loadFixture(setup); + expect(await registry.addCiphernode(AddressThree)); + expect(await registry.isEnabled(AddressThree)).to.be.true; + }); + it("increments numCiphernodes", async function () { + const { registry } = await loadFixture(setup); + const numCiphernodes = await registry.numCiphernodes(); + expect(await registry.addCiphernode(AddressThree)); + expect(await registry.numCiphernodes()).to.equal( + numCiphernodes + BigInt(1), + ); + }); + it("emits a CiphernodeAdded event", async function () { + const { registry } = await loadFixture(setup); + const treeSize = await registry.treeSize(); + const numCiphernodes = await registry.numCiphernodes(); + await expect(await registry.addCiphernode(AddressThree)) + .to.emit(registry, "CiphernodeAdded") + .withArgs( + AddressThree, + treeSize, + numCiphernodes + BigInt(1), + treeSize + BigInt(1), + ); + }); + }); + + describe("removeCiphernode()", function () { + it("reverts if the caller is not the owner", async function () { + const { registry, notTheOwner } = await loadFixture(setup); + await expect( + registry.connect(notTheOwner).removeCiphernode(AddressOne, []), + ).to.be.revertedWithCustomError(registry, "NotOwnerOrBondingRegistry"); + }); + it("removes the ciphernode from the registry", async function () { + const { registry, operator1, tree } = await loadFixture(setup); + const operator1Address = await operator1.getAddress(); + const localTree = new LeanIMT(hash); + for (let i = 0; i < tree.size; i++) { + localTree.insert(tree.leaves[i]); + } + const index = localTree.indexOf(BigInt(operator1Address)); + const proof = localTree.generateProof(index); + localTree.update(index, BigInt(0)); + expect(await registry.isEnabled(operator1Address)).to.be.true; + expect(await registry.removeCiphernode(operator1Address, proof.siblings)); + expect(await registry.isEnabled(operator1Address)).to.be.false; + expect(await registry.root()).to.equal(localTree.root); + }); + it("decrements numCiphernodes", async function () { + const { registry, operator1, tree } = await loadFixture(setup); + const operator1Address = await operator1.getAddress(); + const numCiphernodes = await registry.numCiphernodes(); + const index = tree.indexOf(BigInt(operator1Address)); + const proof = tree.generateProof(index); + expect(await registry.removeCiphernode(operator1Address, proof.siblings)); + expect(await registry.numCiphernodes()).to.equal( + numCiphernodes - BigInt(1), + ); + }); + it("emits a CiphernodeRemoved event", async function () { + const { registry, operator1, tree } = await loadFixture(setup); + const operator1Address = await operator1.getAddress(); + const numCiphernodes = await registry.numCiphernodes(); + const size = await registry.treeSize(); + const index = tree.indexOf(BigInt(operator1Address)); + const proof = tree.generateProof(index); + await expect(registry.removeCiphernode(operator1Address, proof.siblings)) + .to.emit(registry, "CiphernodeRemoved") + .withArgs(operator1Address, index, numCiphernodes - BigInt(1), size); + }); + }); + + describe("setEnclave()", function () { + it("reverts if the caller is not the owner", async function () { + const { registry, notTheOwner } = await loadFixture(setup); + await expect( + registry.connect(notTheOwner).setEnclave(AddressThree), + ).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount"); + }); + it("sets the enclave address", async function () { + const { registry } = await loadFixture(setup); + expect(await registry.setEnclave(AddressThree)); + expect(await registry.enclave()).to.equal(AddressThree); + }); + it("emits an EnclaveSet event", async function () { + const { registry } = await loadFixture(setup); + await expect(await registry.setEnclave(AddressThree)) + .to.emit(registry, "EnclaveSet") + .withArgs(AddressThree); + }); + }); + + describe("committeePublicKey()", function () { + it("returns the public key of the committee for the given e3Id", async function () { + const { registry, request, operator1, operator2 } = + await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + + await registry.connect(operator1).submitTicket(request.e3Id, 1); + await registry.connect(operator2).submitTicket(request.e3Id, 1); + await finalizeCommitteeAfterWindow(registry, request.e3Id); + + await registry.publishCommittee( + request.e3Id, + [await operator1.getAddress(), await operator2.getAddress()], + data, + ); + expect(await registry.committeePublicKey(request.e3Id)).to.equal( + dataHash, + ); + }); + it("reverts if the committee has not been published", async function () { + const { registry, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + await expect( + registry.committeePublicKey(request.e3Id), + ).to.be.revertedWithCustomError(registry, "CommitteeNotPublished"); + }); + }); + + describe("isCiphernodeEligible()", function () { + it("returns true if the ciphernode is in the registry", async function () { + const { registry, operator1 } = await loadFixture(setup); + expect(await registry.isEnabled(await operator1.getAddress())).to.be.true; + }); + it("returns false if the ciphernode is not in the registry", async function () { + const { registry } = await loadFixture(setup); + expect(await registry.isCiphernodeEligible(AddressThree)).to.be.false; + }); + }); + + describe("isEnabled()", function () { + it("returns true if the ciphernode is currently enabled", async function () { + const { registry, operator1 } = await loadFixture(setup); + expect(await registry.isEnabled(await operator1.getAddress())).to.be.true; + }); + it("returns false if the ciphernode is not currently enabled", async function () { + const { registry } = await loadFixture(setup); + expect(await registry.isEnabled(AddressThree)).to.be.false; + }); + }); + + describe("root()", function () { + it("returns the root of the ciphernode registry merkle tree", async function () { + const { registry, tree } = await loadFixture(setup); + expect(await registry.root()).to.equal(tree.root); + }); + }); + + describe("rootAt()", function () { + it("returns the root of the ciphernode registry merkle tree at the given e3Id", async function () { + const { registry, tree, request } = await loadFixture(setup); + await registry.requestCommittee(request.e3Id, 0, request.threshold); + expect(await registry.rootAt(request.e3Id)).to.equal(tree.root); + }); + }); + + describe("treeSize()", function () { + it("returns the size of the ciphernode registry merkle tree", async function () { + const { registry, tree } = await loadFixture(setup); + expect(await registry.treeSize()).to.equal(tree.size); + }); + }); +}); diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts new file mode 100644 index 0000000000..20b29055e1 --- /dev/null +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -0,0 +1,1174 @@ +// 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. +import { expect } from "chai"; +import { network } from "hardhat"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockSlashingVerifierModule from "../../ignition/modules/mockSlashingVerifier"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + EnclaveTicketToken__factory as EnclaveTicketTokenFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockSlashingVerifier__factory as MockSlashingVerifierFactory, + MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, +} from "../../types"; +import type { SlashingManager } from "../../types/contracts/slashing/SlashingManager"; +import type { MockSlashingVerifier } from "../../types/contracts/test/MockSlashingVerifier"; + +const { ethers, networkHelpers, ignition } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +describe("SlashingManager", function () { + const REASON_MISBEHAVIOR = ethers.encodeBytes32String("misbehavior"); + const REASON_INACTIVITY = ethers.encodeBytes32String("inactivity"); + const REASON_DOUBLE_SIGN = ethers.encodeBytes32String("doubleSign"); + + const SLASHER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("SLASHER_ROLE")); + const VERIFIER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("VERIFIER_ROLE")); + const GOVERNANCE_ROLE = ethers.keccak256( + ethers.toUtf8Bytes("GOVERNANCE_ROLE"), + ); + const DEFAULT_ADMIN_ROLE = ethers.ZeroHash; + + const APPEAL_WINDOW = 7 * 24 * 60 * 60; + + async function setupPolicies( + slashingManager: SlashingManager, + mockVerifier: MockSlashingVerifier, + ) { + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + + const evidencePolicy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + + const banPolicy = { + ticketPenalty: ethers.parseUnits("100", 6), + licensePenalty: ethers.parseEther("500"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: true, + appealWindow: 0, + enabled: true, + }; + + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + await slashingManager.setSlashPolicy(REASON_DOUBLE_SIGN, banPolicy); + } + + async function setup() { + const [owner, slasher, verifier, operator, notTheOwner] = + await ethers.getSigners(); + const ownerAddress = await owner.getAddress(); + const operatorAddress = await operator.getAddress(); + + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 1000000, + }, + }, + }); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcContract.mockUSDC.getAddress(), + registry: ownerAddress, + owner: ownerAddress, + }, + }, + }, + ); + + const mockVerifierContract = await ignition.deploy( + MockSlashingVerifierModule, + ); + + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: ownerAddress, + }, + }, + }, + ); + + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclTokenContract.enclaveToken.getAddress(), + registry: ethers.ZeroAddress, + slashedFundsTreasury: ownerAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: APPEAL_WINDOW, + }, + }, + }, + ); + + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + const enclaveToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + const ticketToken = EnclaveTicketTokenFactory.connect( + await ticketTokenContract.enclaveTicketToken.getAddress(), + owner, + ); + const mockVerifier = MockSlashingVerifierFactory.connect( + await mockVerifierContract.mockSlashingVerifier.getAddress(), + owner, + ); + const slashingManager = SlashingManagerFactory.connect( + await slashingManagerContract.slashingManager.getAddress(), + owner, + ); + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + + await ticketToken.setRegistry(await bondingRegistry.getAddress()); + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), + ); + + await enclaveToken.setTransferRestriction(false); + + await enclaveToken.mintAllocation( + operatorAddress, + ethers.parseEther("2000"), + "Test allocation", + ); + + await slashingManager.addSlasher(await slasher.getAddress()); + await slashingManager.addVerifier(await verifier.getAddress()); + + return { + owner, + slasher, + verifier, + operator, + operatorAddress, + notTheOwner, + slashingManager, + bondingRegistry, + enclaveToken, + ticketToken, + usdcToken, + mockVerifier, + }; + } + + describe("constructor / initialization", function () { + it("should set the admin role correctly", async function () { + const { slashingManager, owner } = await loadFixture(setup); + + expect( + await slashingManager.hasRole( + DEFAULT_ADMIN_ROLE, + await owner.getAddress(), + ), + ).to.be.true; + expect( + await slashingManager.hasRole( + GOVERNANCE_ROLE, + await owner.getAddress(), + ), + ).to.be.true; + }); + + it("should set the bonding registry correctly", async function () { + const { slashingManager, bondingRegistry } = await loadFixture(setup); + + expect(await slashingManager.bondingRegistry()).to.equal( + await bondingRegistry.getAddress(), + ); + }); + + it("should revert if admin is zero address", async function () { + await expect( + ignition.deploy(SlashingManagerModule, { + parameters: { + SlashingManager: { + admin: ethers.ZeroAddress, + bondingRegistry: ethers.ZeroAddress, + }, + }, + }), + ).to.be.rejected; + }); + }); + + describe("setSlashPolicy()", function () { + it("should set a valid slash policy", async function () { + const { slashingManager, mockVerifier } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + + await expect(slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy)) + .to.emit(slashingManager, "SlashPolicyUpdated") + .withArgs(REASON_MISBEHAVIOR, Object.values(policy)); + + const storedPolicy = + await slashingManager.getSlashPolicy(REASON_MISBEHAVIOR); + expect(storedPolicy.ticketPenalty).to.equal(policy.ticketPenalty); + expect(storedPolicy.licensePenalty).to.equal(policy.licensePenalty); + expect(storedPolicy.requiresProof).to.equal(policy.requiresProof); + expect(storedPolicy.enabled).to.equal(policy.enabled); + }); + + it("should set a policy without proof requirement", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + + await expect(slashingManager.setSlashPolicy(REASON_INACTIVITY, policy)) + .to.emit(slashingManager, "SlashPolicyUpdated") + .withArgs(REASON_INACTIVITY, Object.values(policy)); + }); + + it("should revert if caller is not governance", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + + await expect( + slashingManager + .connect(notTheOwner) + .setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError( + slashingManager, + "AccessControlUnauthorizedAccount", + ); + }); + + it("should revert if reason is zero", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(ethers.ZeroHash, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if policy is disabled", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: false, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if no penalties are set", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: 0, + licensePenalty: 0, + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if proof required but no verifier set", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "VerifierNotSet"); + }); + + it("should revert if proof required but appeal window set", async function () { + const { slashingManager, mockVerifier } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + + it("should revert if no proof required but no appeal window", async function () { + const { slashingManager } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: 0, + enabled: true, + }; + + await expect( + slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy), + ).to.be.revertedWithCustomError(slashingManager, "InvalidPolicy"); + }); + }); + + describe("role management", function () { + it("should add and remove slasher role", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + await slashingManager.addSlasher(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + SLASHER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.true; + + await slashingManager.removeSlasher(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + SLASHER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.false; + }); + + it("should add and remove verifier role", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + await slashingManager.addVerifier(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + VERIFIER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.true; + + await slashingManager.removeVerifier(await notTheOwner.getAddress()); + expect( + await slashingManager.hasRole( + VERIFIER_ROLE, + await notTheOwner.getAddress(), + ), + ).to.be.false; + }); + + it("should revert if non-admin tries to add slasher", async function () { + const { slashingManager, notTheOwner } = await loadFixture(setup); + + await expect( + slashingManager + .connect(notTheOwner) + .addSlasher(await notTheOwner.getAddress()), + ).to.be.revert(ethers); + }); + + it("should revert if zero address is added as slasher", async function () { + const { slashingManager } = await loadFixture(setup); + + await expect( + slashingManager.addSlasher(ethers.ZeroAddress), + ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); + }); + }); + + describe("proposeSlash()", function () { + it("should propose slash with proof", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + const proof = ethers.toUtf8Bytes("Valid proof data"); + const currentTime = await time.latest(); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), + ) + .to.emit(slashingManager, "SlashProposed") + .withArgs( + 0, + operatorAddress, + REASON_MISBEHAVIOR, + ethers.parseUnits("50", 6), + ethers.parseEther("100"), + currentTime + 1, + await slasher.getAddress(), + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.operator).to.equal(operatorAddress); + expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); + expect(proposal.proofVerified).to.be.true; + expect(proposal.proposer).to.equal(await slasher.getAddress()); + }); + + it("should propose slash without proof (evidence-based)", async function () { + const { slashingManager, slasher, operatorAddress } = + await loadFixture(setup); + + const evidencePolicy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + + const proof = ethers.toUtf8Bytes(""); + const currentTime = await time.latest(); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_INACTIVITY, proof), + ) + .to.emit(slashingManager, "SlashProposed") + .withArgs( + 0, + operatorAddress, + REASON_INACTIVITY, + ethers.parseUnits("20", 6), + ethers.parseEther("50"), + currentTime + APPEAL_WINDOW + 1, + await slasher.getAddress(), + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.proofVerified).to.be.false; + expect(proposal.executableAt).to.be.greaterThan( + currentTime + APPEAL_WINDOW, + ); + }); + + it("should revert if caller is not slasher", async function () { + const { slashingManager, notTheOwner, operatorAddress } = + await loadFixture(setup); + + const proof = ethers.toUtf8Bytes("Some proof"); + + await expect( + slashingManager + .connect(notTheOwner) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + + it("should revert if operator is zero address", async function () { + const { slashingManager, slasher } = await loadFixture(setup); + + const proof = ethers.toUtf8Bytes("Some proof"); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(ethers.ZeroAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); + }); + + it("should revert if slash reason is disabled", async function () { + const { slashingManager, slasher, operatorAddress } = + await loadFixture(setup); + + const proof = ethers.toUtf8Bytes("Some proof"); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof), + ).to.be.revertedWithCustomError(slashingManager, "SlashReasonDisabled"); + }); + + it("should revert if proof required but not provided", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + const emptyProof = ethers.toUtf8Bytes(""); + + await expect( + slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, emptyProof), + ).to.be.revertedWithCustomError(slashingManager, "ProofRequired"); + }); + + it("should increment totalProposals", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + const evidencePolicy = { + ticketPenalty: ethers.parseUnits("20", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: false, + proofVerifier: ethers.ZeroAddress, + banNode: false, + appealWindow: APPEAL_WINDOW, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + + expect(await slashingManager.totalProposals()).to.equal(0); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + expect(await slashingManager.totalProposals()).to.equal(1); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + expect(await slashingManager.totalProposals()).to.equal(2); + }); + }); + + describe("executeSlash()", function () { + it("should execute slash with proof immediately", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + await expect(slashingManager.connect(slasher).executeSlash(0)) + .to.emit(slashingManager, "SlashExecuted") + .withArgs( + 0, + operatorAddress, + REASON_MISBEHAVIOR, + ethers.parseUnits("50", 6), + ethers.parseEther("100"), + true, + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.executed).to.be.true; + }); + + it("should execute slash after appeal window expires", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealWindowActive"); + + await time.increase(APPEAL_WINDOW + 1); + + await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + slashingManager, + "SlashExecuted", + ); + }); + + it("should ban node when policy requires it", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("Serious violation proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + + await expect(slashingManager.connect(slasher).executeSlash(0)) + .to.emit(slashingManager, "NodeBanUpdated") + .withArgs(operatorAddress, true, REASON_DOUBLE_SIGN, slasher); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + }); + + it("should revert if proposal doesn't exist", async function () { + const { slashingManager, slasher } = await loadFixture(setup); + + await expect( + slashingManager.connect(slasher).executeSlash(999), + ).to.be.revertedWithCustomError(slashingManager, "InvalidProposal"); + }); + + it("should revert if already executed", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("Valid proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + await slashingManager.connect(slasher).executeSlash(0); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); + }); + }); + + describe("appeal system", function () { + it("should allow operator to file appeal", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + const evidence = "I was not inactive, here's the proof..."; + + await expect(slashingManager.connect(operator).fileAppeal(0, evidence)) + .to.emit(slashingManager, "AppealFiled") + .withArgs(0, operatorAddress, REASON_INACTIVITY, evidence); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.appealed).to.be.true; + }); + + it("should revert if non-operator tries to appeal", async function () { + const { + slashingManager, + slasher, + notTheOwner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await expect( + slashingManager.connect(notTheOwner).fileAppeal(0, "Not my appeal"), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + + it("should revert if appeal window expired", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await time.increase(APPEAL_WINDOW + 1); + + await expect( + slashingManager.connect(operator).fileAppeal(0, "Too late"), + ).to.be.revertedWithCustomError(slashingManager, "AppealWindowExpired"); + }); + + it("should revert if already appealed", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + + await slashingManager.connect(operator).fileAppeal(0, "First appeal"); + + await expect( + slashingManager.connect(operator).fileAppeal(0, "Second appeal"), + ).to.be.revertedWithCustomError(slashingManager, "AlreadyAppealed"); + }); + + it("should allow governance to resolve appeal (approve)", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + + const resolution = "Appeal approved after review"; + + await expect( + slashingManager.connect(owner).resolveAppeal(0, true, resolution), + ) + .to.emit(slashingManager, "AppealResolved") + .withArgs( + 0, + operatorAddress, + true, + await owner.getAddress(), + resolution, + ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.resolved).to.be.true; + expect(proposal.appealUpheld).to.be.true; + }); + + it("should allow governance to resolve appeal (deny)", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + + await slashingManager + .connect(owner) + .resolveAppeal(0, false, "Appeal denied"); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.resolved).to.be.true; + expect(proposal.appealUpheld).to.be.false; + }); + + it("should block execution if appeal is pending", async function () { + const { + slashingManager, + slasher, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + + await time.increase(APPEAL_WINDOW + 1); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealPending"); + }); + + it("should block execution if appeal was approved", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + await slashingManager.connect(owner).resolveAppeal(0, true, "Approved"); + + await time.increase(APPEAL_WINDOW + 1); + + await expect( + slashingManager.connect(slasher).executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AppealUpheld"); + }); + + it("should allow execution if appeal was denied", async function () { + const { + slashingManager, + slasher, + operator, + owner, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await slashingManager + .connect(slasher) + .proposeSlash( + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ); + await slashingManager.connect(operator).fileAppeal(0, "Evidence"); + await slashingManager.connect(owner).resolveAppeal(0, false, "Denied"); + + await time.increase(APPEAL_WINDOW + 1); + + await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + slashingManager, + "SlashExecuted", + ); + }); + }); + + describe("ban management", function () { + it("should allow governance to ban node", async function () { + const { slashingManager, owner, operatorAddress } = + await loadFixture(setup); + + const reason = ethers.encodeBytes32String("manual_ban"); + + await expect( + slashingManager + .connect(owner) + .updateBanStatus(operatorAddress, true, reason), + ) + .to.emit(slashingManager, "NodeBanUpdated") + .withArgs(operatorAddress, true, reason, await owner.getAddress()); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + }); + + it("should allow governance to unban node", async function () { + const { slashingManager, owner, operatorAddress } = + await loadFixture(setup); + + await slashingManager + .connect(owner) + .updateBanStatus( + operatorAddress, + true, + ethers.encodeBytes32String("test"), + ); + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + + await expect( + slashingManager + .connect(owner) + .updateBanStatus( + operatorAddress, + false, + ethers.encodeBytes32String("test"), + ), + ) + .to.emit(slashingManager, "NodeBanUpdated") + .withArgs( + operatorAddress, + false, + ethers.encodeBytes32String("test"), + await owner.getAddress(), + ); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + }); + + it("should revert if non-governance tries to ban", async function () { + const { slashingManager, notTheOwner, operatorAddress } = + await loadFixture(setup); + + await expect( + slashingManager + .connect(notTheOwner) + .updateBanStatus( + operatorAddress, + false, + ethers.encodeBytes32String("test"), + ), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + }); + }); + + describe("view functions", function () { + it("should return correct slash policy", async function () { + const { slashingManager, mockVerifier } = await loadFixture(setup); + + const policy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: true, + appealWindow: 0, + enabled: true, + }; + + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy); + + const retrieved = + await slashingManager.getSlashPolicy(REASON_MISBEHAVIOR); + expect(retrieved.ticketPenalty).to.equal(policy.ticketPenalty); + expect(retrieved.licensePenalty).to.equal(policy.licensePenalty); + expect(retrieved.requiresProof).to.equal(policy.requiresProof); + expect(retrieved.proofVerifier).to.equal(policy.proofVerifier); + expect(retrieved.banNode).to.equal(policy.banNode); + expect(retrieved.appealWindow).to.equal(policy.appealWindow); + expect(retrieved.enabled).to.equal(policy.enabled); + }); + + it("should return correct slash proposal", async function () { + const { slashingManager, slasher, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const proof = ethers.toUtf8Bytes("test proof"); + await slashingManager + .connect(slasher) + .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.operator).to.equal(operatorAddress); + expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); + expect(proposal.ticketAmount).to.equal(ethers.parseUnits("50", 6)); + expect(proposal.licenseAmount).to.equal(ethers.parseEther("100")); + expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.proofHash).to.equal(ethers.keccak256(proof)); + expect(proposal.proofVerified).to.be.true; + }); + + it("should revert for invalid proposal ID", async function () { + const { slashingManager } = await loadFixture(setup); + + await expect( + slashingManager.getSlashProposal(999), + ).to.be.revertedWithCustomError(slashingManager, "InvalidProposal"); + }); + }); +}); diff --git a/packages/enclave-react/README.md b/packages/enclave-react/README.md index fad084004e..8343a11207 100644 --- a/packages/enclave-react/README.md +++ b/packages/enclave-react/README.md @@ -61,7 +61,6 @@ function MyComponent() { const handleRequest = async () => { try { const hash = await requestE3({ - filter: "0x...", threshold: [2, 3], startWindow: [BigInt(Date.now()), BigInt(Date.now() + 300000)], duration: BigInt(1800), @@ -69,7 +68,6 @@ function MyComponent() { e3ProgramParams: "0x...", computeProviderParams: "0x...", customParams: "0x...", - value: BigInt("1000000000000000"), // 0.001 ETH }); console.log("E3 requested with hash:", hash); } catch (error) { diff --git a/packages/enclave-react/src/useEnclaveSDK.ts b/packages/enclave-react/src/useEnclaveSDK.ts index 172875cfce..fadd197fa4 100644 --- a/packages/enclave-react/src/useEnclaveSDK.ts +++ b/packages/enclave-react/src/useEnclaveSDK.ts @@ -22,6 +22,7 @@ export interface UseEnclaveSDKConfig { contracts?: { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; + feeToken: `0x${string}`; }; chainId?: number; autoConnect?: boolean; @@ -40,11 +41,11 @@ export interface UseEnclaveSDKReturn { // Event handling onEnclaveEvent: ( eventType: T, - callback: EventCallback, + callback: EventCallback ) => void; off: ( eventType: T, - callback: EventCallback, + callback: EventCallback ) => void; // Event types for convenience EnclaveEventType: typeof EnclaveEventType; @@ -87,7 +88,7 @@ export interface UseEnclaveSDKReturn { * ``` */ export const useEnclaveSDK = ( - config: UseEnclaveSDKConfig, + config: UseEnclaveSDKConfig ): UseEnclaveSDKReturn => { const [sdk, setSdk] = useState(null); const [isInitialized, setIsInitialized] = useState(false); @@ -115,6 +116,7 @@ export const useEnclaveSDK = ( contracts: config.contracts || { enclave: "0x0000000000000000000000000000000000000000", ciphernodeRegistry: "0x0000000000000000000000000000000000000000", + feeToken: "0x0000000000000000000000000000000000000000", }, chainId: config.chainId, protocol: config.protocol, @@ -165,7 +167,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.requestE3(...args); }, - [sdk], + [sdk] ); const activateE3 = useCallback( @@ -173,7 +175,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.activateE3(...args); }, - [sdk], + [sdk] ); const publishInput = useCallback( @@ -181,7 +183,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.publishInput(...args); }, - [sdk], + [sdk] ); // Event handling methods @@ -190,7 +192,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.onEnclaveEvent(eventType, callback); }, - [sdk], + [sdk] ); const off = useCallback( @@ -198,7 +200,7 @@ export const useEnclaveSDK = ( if (!sdk) throw new Error("SDK not initialized"); return sdk.off(eventType, callback); }, - [sdk], + [sdk] ); return { diff --git a/packages/enclave-sdk/README.md b/packages/enclave-sdk/README.md index e288b6d33b..f947d7e7b0 100644 --- a/packages/enclave-sdk/README.md +++ b/packages/enclave-sdk/README.md @@ -66,7 +66,6 @@ sdk.onEnclaveEvent(RegistryEventType.CIPHERNODE_ADDED, (event) => { // Interact with contracts const hash = await sdk.requestE3({ - filter: "0x...", threshold: [1, 3], startWindow: [BigInt(0), BigInt(100)], duration: BigInt(3600), @@ -136,6 +135,7 @@ enum RegistryEventType { CIPHERNODE_REMOVED = "CiphernodeRemoved", COMMITTEE_REQUESTED = "CommitteeRequested", COMMITTEE_PUBLISHED = "CommitteePublished", + COMMITTEE_FINALIZED = "CommitteeFinalized", ENCLAVE_SET = "EnclaveSet", // ... more events } @@ -212,7 +212,6 @@ function MyComponent() { ```typescript // Request a new E3 computation await sdk.requestE3({ - filter: `0x${string}`, threshold: [number, number], startWindow: [bigint, bigint], duration: bigint, @@ -220,7 +219,6 @@ await sdk.requestE3({ e3ProgramParams: `0x${string}`, computeProviderParams: `0x${string}`, customParams?: `0x${string}`, - value?: bigint, gasLimit?: bigint }); diff --git a/packages/enclave-sdk/src/contract-client.ts b/packages/enclave-sdk/src/contract-client.ts index 7246825323..b6cb03e190 100644 --- a/packages/enclave-sdk/src/contract-client.ts +++ b/packages/enclave-sdk/src/contract-client.ts @@ -15,6 +15,7 @@ import { import { CiphernodeRegistryOwnable__factory, Enclave__factory, + EnclaveToken__factory, } from "@enclave-e3/contracts/types"; import { type E3 } from "./types"; import { SDKError, isValidAddress } from "./utils"; @@ -23,6 +24,7 @@ export class ContractClient { private contractInfo: { enclave: { address: `0x${string}`; abi: Abi }; ciphernodeRegistry: { address: `0x${string}`; abi: Abi }; + feeToken: { address: `0x${string}`; abi: Abi }; } | null = null; constructor( @@ -31,9 +33,11 @@ export class ContractClient { private addresses: { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; + feeToken: `0x${string}`; } = { enclave: "0x0000000000000000000000000000000000000000", ciphernodeRegistry: "0x0000000000000000000000000000000000000000", + feeToken: "0x0000000000000000000000000000000000000000", } ) { if (!isValidAddress(addresses.enclave)) { @@ -45,6 +49,12 @@ export class ContractClient { "INVALID_ADDRESS" ); } + if (!isValidAddress(addresses.feeToken)) { + throw new SDKError( + "Invalid FeeToken contract address", + "INVALID_ADDRESS" + ); + } } /** @@ -61,6 +71,10 @@ export class ContractClient { address: this.addresses.ciphernodeRegistry, abi: CiphernodeRegistryOwnable__factory.abi, }, + feeToken: { + address: this.addresses.feeToken, + abi: EnclaveToken__factory.abi, + }, }; } catch (error) { throw new SDKError( @@ -70,12 +84,52 @@ export class ContractClient { } } + /** + * Approve the fee token for the Enclave + * approve(address spender, uint256 amount) + */ + public async approveFeeToken(amount: bigint): Promise { + if (!this.walletClient) { + throw new SDKError( + "Wallet client required for write operations", + "NO_WALLET" + ); + } + + if (!this.contractInfo) { + await this.initialize(); + } + + try { + const account = this.walletClient.account; + if (!account) { + throw new SDKError("No account connected", "NO_ACCOUNT"); + } + + const { request } = await this.publicClient.simulateContract({ + address: this.addresses.feeToken, + abi: EnclaveToken__factory.abi, + functionName: "approve", + args: [this.addresses.enclave, amount], + account, + }); + + const hash = await this.walletClient.writeContract(request); + + return hash; + } catch (error) { + throw new SDKError( + `Failed to approve fee token: ${error}`, + "APPROVE_FEE_TOKEN_FAILED" + ); + } + } + /** * Request a new E3 computation * request(address filter, uint32[2] threshold, uint256[2] startWindow, uint256 duration, IE3Program e3Program, bytes e3ProgramParams, bytes computeProviderParams, bytes customParams) */ public async requestE3( - filter: `0x${string}`, threshold: [number, number], startWindow: [bigint, bigint], duration: bigint, @@ -83,7 +137,6 @@ export class ContractClient { e3ProgramParams: `0x${string}`, computeProviderParams: `0x${string}`, customParams?: `0x${string}`, - value?: bigint, gasLimit?: bigint ): Promise { if (!this.walletClient) { @@ -110,7 +163,6 @@ export class ContractClient { functionName: "request", args: [ { - filter, threshold, startWindow, duration, @@ -121,7 +173,6 @@ export class ContractClient { }, ], account, - value: value || BigInt(0), gas: gasLimit, }); @@ -301,7 +352,7 @@ export class ContractClient { /** * Get the public key for an E3 computation * Based on the contract: committeePublicKey(uint256 e3Id) returns (bytes32 publicKeyHash) - * @param e3Id + * @param e3Id * @returns The public key */ public async getE3PublicKey(e3Id: bigint): Promise<`0x${string}`> { @@ -319,7 +370,10 @@ export class ContractClient { return result; } catch (error) { - throw new SDKError(`Failed to get E3 public key: ${error}`, "GET_E3_PUBLIC_KEY_FAILED"); + throw new SDKError( + `Failed to get E3 public key: ${error}`, + "GET_E3_PUBLIC_KEY_FAILED" + ); } } diff --git a/packages/enclave-sdk/src/enclave-sdk.ts b/packages/enclave-sdk/src/enclave-sdk.ts index 2e28e77626..9401a8ebb3 100644 --- a/packages/enclave-sdk/src/enclave-sdk.ts +++ b/packages/enclave-sdk/src/enclave-sdk.ts @@ -75,6 +75,13 @@ export class EnclaveSDK { ); } + if (!isValidAddress(config.contracts.feeToken)) { + throw new SDKError( + "Invalid FeeToken contract address", + "INVALID_ADDRESS" + ); + } + this.eventListener = new EventListener(config.publicClient); this.contractClient = new ContractClient( config.publicClient, @@ -166,7 +173,7 @@ export class EnclaveSDK { } /** - * This function encrypts a number using the configured FHE protocol + * This function encrypts a number using the configured FHE protocol * and generates the necessary public inputs for a zk-SNARK proof. * @param data The number to encrypt * @param publicKey The public key to use for encryption @@ -174,7 +181,7 @@ export class EnclaveSDK { */ public async encryptNumberAndGenInputs( data: bigint, - publicKey: Uint8Array, + publicKey: Uint8Array ): Promise { await initializeWasm(); switch (this.protocol) { @@ -190,7 +197,7 @@ export class EnclaveSDK { const publicInputs = JSON.parse(circuitInputs); return { encryptedData, - publicInputs + publicInputs, }; default: throw new Error("Protocol not supported"); @@ -209,13 +216,14 @@ export class EnclaveSDK { publicKey: Uint8Array, circuit: CompiledCircuit ): Promise { - const { publicInputs, encryptedData } = await this.encryptNumberAndGenInputs(data, publicKey); - const proof = await generateProof(publicInputs, circuit); + const { publicInputs, encryptedData } = + await this.encryptNumberAndGenInputs(data, publicKey); + const proof = await generateProof(publicInputs, circuit); - return { - encryptedData, - proof, - }; + return { + encryptedData, + proof, + }; } /** @@ -226,7 +234,7 @@ export class EnclaveSDK { */ public async encryptVectorAndGenInputs( data: BigUint64Array, - publicKey: Uint8Array, + publicKey: Uint8Array ): Promise { await initializeWasm(); switch (this.protocol) { @@ -242,7 +250,7 @@ export class EnclaveSDK { const publicInputs = JSON.parse(circuitInputs); return { encryptedData, - publicInputs + publicInputs, }; default: throw new Error("Protocol not supported"); @@ -261,7 +269,8 @@ export class EnclaveSDK { publicKey: Uint8Array, circuit: CompiledCircuit ): Promise { - const { publicInputs, encryptedData } = await this.encryptVectorAndGenInputs(data, publicKey); + const { publicInputs, encryptedData } = + await this.encryptVectorAndGenInputs(data, publicKey); const proof = await generateProof(publicInputs, circuit); @@ -271,11 +280,25 @@ export class EnclaveSDK { }; } + /** + * Approve the fee token for the Enclave + * @param amount - The amount to approve + * @returns The approval transaction hash + */ + public async approveFeeToken(amount: bigint): Promise { + console.log(">>> APPROVE FEE TOKEN"); + + if (!this.initialized) { + await this.initialize(); + } + + return this.contractClient.approveFeeToken(amount); + } + /** * Request a new E3 computation */ public async requestE3(params: { - filter: `0x${string}`; threshold: [number, number]; startWindow: [bigint, bigint]; duration: bigint; @@ -283,7 +306,6 @@ export class EnclaveSDK { e3ProgramParams: `0x${string}`; computeProviderParams: `0x${string}`; customParams?: `0x${string}`; - value?: bigint; gasLimit?: bigint; }): Promise { console.log(">>> REQUEST"); @@ -293,7 +315,6 @@ export class EnclaveSDK { } return this.contractClient.requestE3( - params.filter, params.threshold, params.startWindow, params.duration, @@ -301,7 +322,6 @@ export class EnclaveSDK { params.e3ProgramParams, params.computeProviderParams, params.customParams, - params.value, params.gasLimit ); } @@ -551,6 +571,7 @@ export class EnclaveSDK { contracts: { enclave: `0x${string}`; ciphernodeRegistry: `0x${string}`; + feeToken: `0x${string}`; }; privateKey?: `0x${string}`; chainId: keyof typeof EnclaveSDK.chains; diff --git a/packages/enclave-sdk/src/index.ts b/packages/enclave-sdk/src/index.ts index dc63dd2c54..31017513a5 100644 --- a/packages/enclave-sdk/src/index.ts +++ b/packages/enclave-sdk/src/index.ts @@ -32,6 +32,7 @@ export type { CiphernodeRemovedData, CommitteeRequestedData, CommitteePublishedData, + CommitteeFinalizedData, EnclaveEventData, RegistryEventData, ProtocolParams, @@ -71,4 +72,9 @@ export { type ComputeProviderParams, } from "./utils"; -export { generateProof, type Polynomial, convertToPolynomial, convertToPolynomialArray } from "./greco"; +export { + generateProof, + type Polynomial, + convertToPolynomial, + convertToPolynomialArray, +} from "./greco"; diff --git a/packages/enclave-sdk/src/types.ts b/packages/enclave-sdk/src/types.ts index 01b7475f41..0f21717cef 100644 --- a/packages/enclave-sdk/src/types.ts +++ b/packages/enclave-sdk/src/types.ts @@ -10,6 +10,8 @@ import type { CiphernodeRegistryOwnable, Enclave, MockCiphernodeRegistry, + MockUSDC, + EnclaveToken, } from "@enclave-e3/contracts/types"; import type { CircuitInputs } from "./greco"; @@ -29,7 +31,7 @@ export interface SDKConfig { walletClient?: WalletClient; /** - * The Enclave contracts + * The Enclave contracts */ contracts: { /** @@ -41,6 +43,11 @@ export interface SDKConfig { * The CiphernodeRegistry contract address */ ciphernodeRegistry: `0x${string}`; + + /** + * The FeeToken contract address + */ + feeToken: `0x${string}`; }; /** @@ -69,6 +76,7 @@ export interface EventListenerConfig { export interface ContractInstances { enclave: Enclave; ciphernodeRegistry: CiphernodeRegistryOwnable | MockCiphernodeRegistry; + feeToken: EnclaveToken | MockUSDC; } // Unified Event System @@ -102,6 +110,7 @@ export enum RegistryEventType { // Committee Management COMMITTEE_REQUESTED = "CommitteeRequested", COMMITTEE_PUBLISHED = "CommitteePublished", + COMMITTEE_FINALIZED = "CommitteeFinalized", // Configuration ENCLAVE_SET = "EnclaveSet", @@ -178,8 +187,10 @@ export interface CiphernodeRemovedData { export interface CommitteeRequestedData { e3Id: bigint; - filter: string; + seed: bigint; threshold: [bigint, bigint]; + requestBlock: bigint; + submissionDeadline: bigint; } export interface CommitteePublishedData { @@ -187,6 +198,11 @@ export interface CommitteePublishedData { publicKey: string; } +export interface CommitteeFinalizedData { + e3Id: bigint; + nodes: string[]; +} + // Event data mapping export interface EnclaveEventData { [EnclaveEventType.E3_REQUESTED]: E3RequestedData; @@ -213,6 +229,7 @@ export interface EnclaveEventData { export interface RegistryEventData { [RegistryEventType.COMMITTEE_REQUESTED]: CommitteeRequestedData; [RegistryEventType.COMMITTEE_PUBLISHED]: CommitteePublishedData; + [RegistryEventType.COMMITTEE_FINALIZED]: CommitteeFinalizedData; [RegistryEventType.ENCLAVE_SET]: { enclave: string }; [RegistryEventType.OWNERSHIP_TRANSFERRED]: { previousOwner: string; @@ -225,10 +242,10 @@ export interface RegistryEventData { export interface EnclaveEvent { type: T; data: T extends EnclaveEventType - ? EnclaveEventData[T] - : T extends RegistryEventType - ? RegistryEventData[T] - : unknown; + ? EnclaveEventData[T] + : T extends RegistryEventType + ? RegistryEventData[T] + : unknown; log: Log; timestamp: Date; blockNumber: bigint; @@ -236,7 +253,7 @@ export interface EnclaveEvent { } export type EventCallback = ( - event: EnclaveEvent, + event: EnclaveEvent ) => void | Promise; export interface EventFilter { @@ -287,7 +304,7 @@ export interface ProtocolParams { /** * The degree of the polynomial */ - degree: number; + degree: number; /** * The plaintext modulus */ @@ -299,21 +316,21 @@ export interface ProtocolParams { } /** - * Parameters for the BFV protocol + * Parameters for the BFV protocol */ export const BfvProtocolParams = { /** - * Recommended parameters for BFV protocol - * - Degree: 2048 - * - Plaintext modulus: 1032193 - * - Moduli:0x3FFFFFFF000001 - */ + * Recommended parameters for BFV protocol + * - Degree: 2048 + * - Plaintext modulus: 1032193 + * - Moduli:0x3FFFFFFF000001 + */ BFV_NORMAL: { degree: 2048, plaintextModulus: 1032193n, - moduli: 0x3FFFFFFF000001n, + moduli: 0x3fffffff000001n, } as const satisfies ProtocolParams, -} +}; /** * The result of encrypting a value and generating a proof diff --git a/packages/enclave-sdk/src/utils.ts b/packages/enclave-sdk/src/utils.ts index 1c0040a71b..a0c6ae416e 100644 --- a/packages/enclave-sdk/src/utils.ts +++ b/packages/enclave-sdk/src/utils.ts @@ -81,8 +81,8 @@ export const DEFAULT_COMPUTE_PROVIDER_PARAMS: ComputeProviderParams = { // Default E3 configuration export const DEFAULT_E3_CONFIG = { - threshold_min: 2, - threshold_max: 3, + threshold_min: 1, + threshold_max: 2, window_size: 120, // 2 minutes in seconds duration: 1800, // 30 minutes in seconds payment_amount: "0", // 0 ETH in wei @@ -120,11 +120,17 @@ export function encodeBfvParams( } /** - * Encode compute provider parameters for the smart contract + * Encode compute provider parameters for the smart contract' + * If mock is true, the compute provider parameters will return 32 bytes of 0x00 */ export function encodeComputeProviderParams( - params: ComputeProviderParams + params: ComputeProviderParams, + mock: boolean = false ): `0x${string}` { + if (mock) { + return `0x${"0".repeat(32)}` as `0x${string}`; + } + const jsonString = JSON.stringify(params); const encoder = new TextEncoder(); const bytes = encoder.encode(jsonString); diff --git a/packages/enclave-sdk/tests/sdk.test.ts b/packages/enclave-sdk/tests/sdk.test.ts index af7f614125..56cca7d7d3 100644 --- a/packages/enclave-sdk/tests/sdk.test.ts +++ b/packages/enclave-sdk/tests/sdk.test.ts @@ -22,6 +22,7 @@ describe("encryptNumber", () => { contracts: { enclave: zeroAddress, ciphernodeRegistry: zeroAddress, + feeToken: zeroAddress, }, rpcUrl: "", privateKey: @@ -31,7 +32,7 @@ describe("encryptNumber", () => { it("should encrypt a number without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") ); const value = await sdk.encryptNumber(10n, Uint8Array.from(buffer)); expect(value).to.be.an.instanceof(Uint8Array); @@ -40,35 +41,46 @@ describe("encryptNumber", () => { }); it("should encrypt a number and generate a proof without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") + ); + + const value = await sdk.encryptNumberAndGenProof( + 1n, + Uint8Array.from(buffer), + demoCircuit as unknown as CompiledCircuit ); - const value = await sdk.encryptNumberAndGenProof(1n, Uint8Array.from(buffer), demoCircuit as unknown as CompiledCircuit); - expect(value).to.be.an.instanceof(Object); expect(value.encryptedData).to.be.an.instanceof(Uint8Array); - expect(value.proof).to.be.an.instanceOf(Object) + expect(value.proof).to.be.an.instanceOf(Object); }, 9999999); it("should encrypt a vecor of numbers without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") + ); + const value = await sdk.encryptVector( + new BigUint64Array([1n, 2n]), + Uint8Array.from(buffer) ); - const value = await sdk.encryptVector(new BigUint64Array([1n, 2n]), Uint8Array.from(buffer)); expect(value).to.be.an.instanceof(Uint8Array); expect(value.length).to.equal(27_674); }); it("should encrypt a vector and generate a proof without crashing in a node environent", async () => { const buffer = await fs.readFile( - path.resolve(__dirname, "./fixtures/pubkey.bin"), + path.resolve(__dirname, "./fixtures/pubkey.bin") + ); + + const value = await sdk.encryptVectorAndGenProof( + new BigUint64Array([1n, 2n]), + Uint8Array.from(buffer), + demoCircuit as unknown as CompiledCircuit ); - const value = await sdk.encryptVectorAndGenProof(new BigUint64Array([1n, 2n]), Uint8Array.from(buffer), demoCircuit as unknown as CompiledCircuit); - expect(value).to.be.an.instanceof(Object); expect(value.encryptedData).to.be.an.instanceof(Uint8Array); - expect(value.proof).to.be.an.instanceOf(Object) + expect(value.proof).to.be.an.instanceOf(Object); }, 9999999); }); }); diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx index 0916fe0bd1..e24314469e 100644 --- a/templates/default/client/src/pages/steps/RequestComputation.tsx +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -111,14 +111,12 @@ const RequestComputation: React.FC = () => { console.log('requestE3') const hash = await requestE3({ - filter: contracts.filterRegistry, threshold, startWindow, duration, e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, - value: BigInt('1000000000000000'), // 0.001 ETH }) setLocalTransactionHash(hash) diff --git a/templates/default/client/src/utils/env-config.ts b/templates/default/client/src/utils/env-config.ts index a2cf0408fc..ec681fa2b5 100644 --- a/templates/default/client/src/utils/env-config.ts +++ b/templates/default/client/src/utils/env-config.ts @@ -7,28 +7,46 @@ export const ENCLAVE_ADDRESS = import.meta.env.VITE_ENCLAVE_ADDRESS export const E3_PROGRAM_ADDRESS = import.meta.env.VITE_E3_PROGRAM_ADDRESS export const REGISTRY_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS -export const FILTER_REGISTRY_ADDRESS = import.meta.env.VITE_FILTER_REGISTRY_ADDRESS +export const BONDING_REGISTRY_ADDRESS = import.meta.env.VITE_BONDING_REGISTRY_ADDRESS +export const FEE_TOKEN_ADDRESS = import.meta.env.VITE_FEE_TOKEN_ADDRESS export const RPC_URL = import.meta.env.VITE_RPC_URL || 'http://localhost:8545' -// Get the missing environment variables. -// This is used to check if the environment variables are set. -export const MISSING_ENV_VARS = Object.entries({ +const requiredEnvVars = { VITE_ENCLAVE_ADDRESS: ENCLAVE_ADDRESS, VITE_E3_PROGRAM_ADDRESS: E3_PROGRAM_ADDRESS, VITE_REGISTRY_ADDRESS: REGISTRY_ADDRESS, - VITE_FILTER_REGISTRY_ADDRESS: FILTER_REGISTRY_ADDRESS, -}) + VITE_BONDING_REGISTRY_ADDRESS: BONDING_REGISTRY_ADDRESS, + VITE_FEE_TOKEN_ADDRESS: FEE_TOKEN_ADDRESS, +} + +export const MISSING_ENV_VARS = Object.entries(requiredEnvVars) .filter(([, value]) => !value) .map(([key]) => key) +export const HAS_MISSING_ENV_VARS = MISSING_ENV_VARS.length > 0 + +/** + * Validate environment variables and throw an error if any are missing + */ +export function validateEnvVars(): void { + if (HAS_MISSING_ENV_VARS) { + throw new Error( + `Missing required environment variables: ${MISSING_ENV_VARS.join(', ')}\n` + + 'Please check your .env file and ensure all required variables are set.', + ) + } +} + /** - * Get validated contract addresses. + * Get validated contract addresses */ export function getContractAddresses() { + validateEnvVars() return { enclave: ENCLAVE_ADDRESS as `0x${string}`, ciphernodeRegistry: REGISTRY_ADDRESS as `0x${string}`, - filterRegistry: FILTER_REGISTRY_ADDRESS as `0x${string}`, + bondingRegistry: BONDING_REGISTRY_ADDRESS as `0x${string}`, e3Program: E3_PROGRAM_ADDRESS as `0x${string}`, + feeToken: FEE_TOKEN_ADDRESS as `0x${string}`, } } diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index 0fe03e4dbe..67404b228a 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -17,14 +17,118 @@ }, "blockNumber": 9181748, "address": "0x5ABDfCbA0366ABF2893D3f2465F4C97908488A6d" + } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, - "NaiveRegistryFilter": { + "MockUSDC": { "constructorArgs": { - "ciphernodeRegistryAddress": "0x5ABDfCbA0366ABF2893D3f2465F4C97908488A6d", - "owner": "0x4f1f3a157073A35515C4fC4A8af2F1Af088f0676" + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "blockNumber": 9, + "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "3" }, - "blockNumber": 9181753, - "address": "0x58708A1bf1AEdf8e75755FDa1882F8dc46985009" + "blockNumber": 10, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "bondingRegistry": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] + }, + "blockNumber": 11, + "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "MockComputeProvider": { + "blockNumber": 20, + "address": "0x68B1D87F95878fE05B998F19b66F4baba5De1aed" + }, + "MockDecryptionVerifier": { + "blockNumber": 21, + "address": "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c" + }, + "MockInputValidator": { + "blockNumber": 22, + "address": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "MockE3Program": { + "constructorArgs": { + "mockInputValidator": "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d" + }, + "blockNumber": 23, + "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + }, + "MockRISC0Verifier": { + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + }, + "ImageID": { + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + }, + "InputValidator": { + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + }, + "MyProgram": { + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + "constructorArgs": { + "enclave": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + "verifier": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + "programId": "0xaf928ebf39fec4696c3f41f473a1a9473b67d723c6373149c6ab99ba4c1a76ef", + "inputValidator": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + } } } } \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 6416578df8..8df8049404 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,10 +2,21 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - e3_program: "0x0B306BF915C4d645ff596e518fAf3F9669b97016" - enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + e3_program: + address: "0x09635f643e140090a9a8dcd712ed6285858cebef" + deploy_block: 1 # Set to actual deploy block + enclave: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 1 # Set to actual deploy block + ciphernode_registry: + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + deploy_block: 1 # Set to actual deploy block + bonding_registry: + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 1 # Set to actual deploy block + fee_token: + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + deploy_block: 1 # Set to actual deploy block program: dev: true @@ -16,23 +27,23 @@ program: nodes: cn1: - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" quic_port: 9201 autonetkey: true autopassword: true cn2: - address: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" quic_port: 9202 autonetkey: true autopassword: true cn3: - address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" quic_port: 9203 autonetkey: true autopassword: true ag: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" - quic_port: 9094 + address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" + quic_port: 9204 autonetkey: true autopassword: true role: diff --git a/templates/default/hardhat.config.ts b/templates/default/hardhat.config.ts index a1c8cc9086..739e7f671f 100644 --- a/templates/default/hardhat.config.ts +++ b/templates/default/hardhat.config.ts @@ -4,7 +4,10 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { ciphernodeAdd } from "@enclave-e3/contracts/tasks/ciphernode"; +import { + ciphernodeAdd, + ciphernodeAdminAdd, +} from "@enclave-e3/contracts/tasks/ciphernode"; import { cleanDeploymentsTask } from "@enclave-e3/contracts/tasks/utils"; import dotenv from "dotenv"; @@ -65,10 +68,7 @@ function getChainConfig(chain: keyof typeof chainIds, apiUrl: string) { } const config: HardhatUserConfig = { - tasks: [ - ciphernodeAdd, - cleanDeploymentsTask, - ], + tasks: [ciphernodeAdd, ciphernodeAdminAdd, cleanDeploymentsTask], plugins: [ hardhatTypechainPlugin, hardhatEthersChaiMatchers, @@ -95,38 +95,42 @@ const config: HardhatUserConfig = { }, arbitrum: getChainConfig( "arbitrum-mainnet", - process.env.ARBISCAN_API_KEY || "", + process.env.ARBISCAN_API_KEY || "" ), avalanche: getChainConfig("avalanche", process.env.SNOWTRACE_API_KEY || ""), bsc: getChainConfig("bsc", process.env.BSCSCAN_API_KEY || ""), mainnet: getChainConfig("mainnet", process.env.ETHERSCAN_API_KEY || ""), optimism: getChainConfig( "optimism-mainnet", - process.env.OPTIMISM_API_KEY || "", + process.env.OPTIMISM_API_KEY || "" ), "polygon-mainnet": getChainConfig( "polygon-mainnet", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), "polygon-mumbai": getChainConfig( "polygon-mumbai", - process.env.POLYGONSCAN_API_KEY || "", + process.env.POLYGONSCAN_API_KEY || "" ), sepolia: getChainConfig("sepolia", process.env.ETHERSCAN_API_KEY || ""), goerli: getChainConfig("goerli", process.env.ETHERSCAN_API_KEY || ""), }, solidity: { npmFilesToBuild: [ - "poseidon-solidity/PoseidonT3.sol", + "poseidon-solidity/PoseidonT3.sol", "@enclave-e3/contracts/contracts/Enclave.sol", "@enclave-e3/contracts/contracts/registry/CiphernodeRegistryOwnable.sol", - "@enclave-e3/contracts/contracts/registry/NaiveRegistryFilter.sol", - "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", + "@enclave-e3/contracts/contracts/registry/BondingRegistry.sol", + "@enclave-e3/contracts/contracts/slashing/SlashingManager.sol", + "@enclave-e3/contracts/contracts/token/EnclaveToken.sol", + "@enclave-e3/contracts/contracts/token/EnclaveTicketToken.sol", "@enclave-e3/contracts/contracts/test/MockCiphernodeRegistry.sol", "@enclave-e3/contracts/contracts/test/MockComputeProvider.sol", "@enclave-e3/contracts/contracts/test/MockDecryptionVerifier.sol", "@enclave-e3/contracts/contracts/test/MockE3Program.sol", - "@enclave-e3/contracts/contracts/test/MockRegistryFilter.sol", + "@enclave-e3/contracts/contracts/test/MockInputValidator.sol", + "@enclave-e3/contracts/contracts/test/MockSlashingVerifier.sol", + "@enclave-e3/contracts/contracts/test/MockStableToken.sol", ], compilers: [ { diff --git a/templates/default/package.json b/templates/default/package.json index 57dc05ef89..d94bce430b 100644 --- a/templates/default/package.json +++ b/templates/default/package.json @@ -4,7 +4,6 @@ "private": true, "type": "module", "scripts": { - "ciphernode:add": "hardhat run scripts/ciphernode-add.ts -- ", "compile": "hardhat compile", "clean:deployments": "hardhat utils:clean-deployments", "deploy": "pnpm clean:deployments && hardhat run scripts/deploy-local.ts --network localhost", diff --git a/templates/default/scripts/dev_ciphernodes.sh b/templates/default/scripts/dev_ciphernodes.sh index 3f0f406adb..e6f982b859 100755 --- a/templates/default/scripts/dev_ciphernodes.sh +++ b/templates/default/scripts/dev_ciphernodes.sh @@ -24,9 +24,15 @@ pnpm wait-on http://localhost:8545 rm -rf .enclave/data rm -rf .enclave/config -PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_AG="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" -enclave wallet set --name ag --private-key "$PRIVATE_KEY" +enclave wallet set --name ag --private-key "$PRIVATE_KEY_AG" +enclave wallet set --name cn1 --private-key "$PRIVATE_KEY_CN1" +enclave wallet set --name cn2 --private-key "$PRIVATE_KEY_CN2" +enclave wallet set --name cn3 --private-key "$PRIVATE_KEY_CN3" # using & instead of -d so that wait works below enclave nodes up -v & @@ -39,9 +45,9 @@ CN3=$(grep -A 1 'cn3:' enclave.config.yaml | grep 'address:' | sed 's/.*address: # Add ciphernodes using variables from config.sh pnpm run deploy && sleep 2 -pnpm hardhat ciphernode:add --ciphernode-address $CN1 --network localhost -pnpm hardhat ciphernode:add --ciphernode-address $CN2 --network localhost -pnpm hardhat ciphernode:add --ciphernode-address $CN3 --network localhost +pnpm hardhat ciphernode:admin-add --ciphernode-address $CN1 --network localhost +pnpm hardhat ciphernode:admin-add --ciphernode-address $CN2 --network localhost +pnpm hardhat ciphernode:admin-add --ciphernode-address $CN3 --network localhost # Function to send RPC request. send_rpc() { diff --git a/templates/default/server/index.ts b/templates/default/server/index.ts index 6a4a2aefdb..c681cfdf1d 100644 --- a/templates/default/server/index.ts +++ b/templates/default/server/index.ts @@ -33,6 +33,7 @@ async function createPrivateSDK() { PRIVATE_KEY, CIPHERNODE_REGISTRY_CONTRACT, ENCLAVE_CONTRACT, + FEE_TOKEN_CONTRACT, RPC_URL, } = getCheckedEnvVars(); @@ -46,6 +47,7 @@ async function createPrivateSDK() { contracts: { enclave: ENCLAVE_CONTRACT as `0x${string}`, ciphernodeRegistry: CIPHERNODE_REGISTRY_CONTRACT as `0x${string}`, + feeToken: FEE_TOKEN_CONTRACT as `0x${string}`, }, chainId: CHAIN_ID, protocol: FheProtocol.BFV, diff --git a/templates/default/server/utils.ts b/templates/default/server/utils.ts index dba3efbbd3..b216e76bec 100644 --- a/templates/default/server/utils.ts +++ b/templates/default/server/utils.ts @@ -17,6 +17,7 @@ export function getCheckedEnvVars() { RPC_URL: ensureEnv("RPC_URL"), ENCLAVE_CONTRACT: ensureEnv("ENCLAVE_ADDRESS"), CIPHERNODE_REGISTRY_CONTRACT: ensureEnv("REGISTRY_ADDRESS"), + FEE_TOKEN_CONTRACT: ensureEnv("FEE_TOKEN_ADDRESS"), PRIVATE_KEY: ensureEnv("PRIVATE_KEY"), CHAIN_ID: parseInt(ensureEnv("CHAIN_ID")), PROGRAM_RUNNER_URL: diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index bfecfd3b55..6d18d74be1 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -27,8 +27,9 @@ export function getContractAddresses() { return { enclave: process.env.ENCLAVE_ADDRESS as `0x${string}`, ciphernodeRegistry: process.env.REGISTRY_ADDRESS as `0x${string}`, - filterRegistry: process.env.FILTER_REGISTRY_ADDRESS as `0x${string}`, + bondingRegistry: process.env.BONDING_REGISTRY_ADDRESS as `0x${string}`, e3Program: process.env.E3_PROGRAM_ADDRESS as `0x${string}`, + feeToken: process.env.FEE_TOKEN_ADDRESS as `0x${string}`, }; } @@ -42,7 +43,6 @@ type E3Shared = { e3Id: bigint; e3Program: string; e3: E3; - filter: string; }; type E3StateRequested = E3Shared & { @@ -171,13 +171,14 @@ describe("Integration", () => { contracts: { enclave: contracts.enclave, ciphernodeRegistry: contracts.ciphernodeRegistry, + feeToken: contracts.feeToken, }, rpcUrl: "ws://localhost:8545", privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", protocol: FheProtocol.BFV, }); - + it("should run an integration test", async () => { const { waitForEvent } = await setupEventListeners(sdk, store); @@ -185,82 +186,91 @@ describe("Integration", () => { DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max, ]; - const startWindow = calculateStartWindow(60); - const duration = BigInt(10); + const startWindow = calculateStartWindow(100); + const duration = BigInt(15); const e3ProgramParams = encodeBfvParams(); const computeProviderParams = encodeComputeProviderParams( - DEFAULT_COMPUTE_PROVIDER_PARAMS + DEFAULT_COMPUTE_PROVIDER_PARAMS, + true // Mock the compute provider parameters, return 32 bytes of 0x00 ); - + let state; let event; - + + // Approve fee token + console.log("Approving fee token..."); + const hash = await sdk.approveFeeToken(100000000000n); + console.log("Fee token approved:", hash); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + // REQUEST phase await waitForEvent(EnclaveEventType.E3_REQUESTED, async () => { console.log("Requested E3..."); await sdk.requestE3({ - filter: contracts.filterRegistry, threshold, startWindow, duration, e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, - value: BigInt("1000000000000000"), // 0.001 ETH }); }); - + state = store.get(0n); assert(state); assert.strictEqual(state.e3Id, 0n); - assert.strictEqual(state.filter, contracts.filterRegistry); assert.strictEqual(state.type, "requested"); - + // Ciphernodes will publish a public key within the COMMITTEE_PUBLISHED event event = await waitForEvent(RegistryEventType.COMMITTEE_PUBLISHED); - + state = store.get(0n); assert(state); assert.strictEqual(state.type, "committee_published"); assert.strictEqual(state.publicKey, event.data.publicKey); - + let { e3Id, publicKey } = state; - + // ACTIVATION phase event = await waitForEvent(EnclaveEventType.E3_ACTIVATED, async () => { await sdk.activateE3(e3Id, publicKey); }); - + state = store.get(0n); assert(state); assert.strictEqual(state.type, "activated"); - + // INPUT PUBLISHING phase const num1 = 12n; const num2 = 21n; const publicKeyBytes = hexToBytes(state.publicKey); const enc1 = await sdk.encryptNumber(num1, publicKeyBytes); const enc2 = await sdk.encryptNumber(num2, publicKeyBytes); - + await waitForEvent(EnclaveEventType.INPUT_PUBLISHED, async () => { await sdk.publishInput( e3Id, - `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`, + `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, "0")).join( + "" + )}` as `0x${string}` ); }); await waitForEvent(EnclaveEventType.INPUT_PUBLISHED, async () => { await sdk.publishInput( e3Id, - `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, "0")).join("")}` as `0x${string}`, + `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, "0")).join( + "" + )}` as `0x${string}` ); }); - + const plaintextEvent = await waitForEvent( EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED ); - + const parsed = hexToUint8Array(plaintextEvent.data.plaintextOutput); - + expect(BigInt(parsed[0])).toBe(num1 + num2); - }, 9999999) -}) + }, 9999999); +}); diff --git a/tests/integration/base.sh b/tests/integration/base.sh index c112954358..7bc1e979c8 100755 --- a/tests/integration/base.sh +++ b/tests/integration/base.sh @@ -20,7 +20,10 @@ pnpm evm:clean pnpm evm:deploy --network localhost # set wallet to ag specifically -enclave_wallet_set ag "$PRIVATE_KEY" +enclave_wallet_set ag "$PRIVATE_KEY_AG" +enclave_wallet_set cn1 "$PRIVATE_KEY_CN1" +enclave_wallet_set cn2 "$PRIVATE_KEY_CN2" +enclave_wallet_set cn3 "$PRIVATE_KEY_CN3" # start swarm enclave_nodes_up @@ -40,9 +43,6 @@ pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_2 --network localho heading "Add ciphernode $CIPHERNODE_ADDRESS_3" pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_3 --network localhost -heading "Add ciphernode $CIPHERNODE_ADDRESS_4" -pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_4 --network localhost - heading "Request Committee" ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh --moduli 0x3FFFFFFF000001 --degree 2048 --plaintext-modulus 1032193) diff --git a/tests/integration/enclave.config.yaml b/tests/integration/enclave.config.yaml index 321ed18c3a..fcb16eae42 100644 --- a/tests/integration/enclave.config.yaml +++ b/tests/integration/enclave.config.yaml @@ -2,34 +2,48 @@ chains: - name: "hardhat" rpc_url: "ws://localhost:8545" contracts: - enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" - ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + e3_program: + address: "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + deploy_block: 1 # Set to actual deploy block + enclave: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 1 # Set to actual deploy block + ciphernode_registry: + address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + deploy_block: 1 # Set to actual deploy block + bonding_registry: + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 1 # Set to actual deploy block + fee_token: + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + deploy_block: 1 # Set to actual deploy block + +program: + dev: true + # risc0: + # risc0_dev_mode: 1 # 0 = real groth16 proofs, 1 = fake proofs (dev mode) + # bonsai_api_key: xxxxxxxxxxxxxxxx + # bonsai_api_url: xxxxxxxxxxxxxxxx nodes: cn1: - address: "0x2546BcD3c84621e976D8185a91A922aE77ECEc30" - quic_port: 9091 + address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + quic_port: 9201 autonetkey: true autopassword: true cn2: - address: "0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" - quic_port: 9092 + address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + quic_port: 9202 autonetkey: true autopassword: true cn3: - address: "0xdD2FD4581271e230360230F9337D5c0430Bf44C0" - quic_port: 9093 - autonetkey: true - autopassword: true - cn4: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" - quic_port: 9094 + address: "0x90F79bf6EB2c4f870365E785982E1f101E93b906" + quic_port: 9203 autonetkey: true autopassword: true ag: - address: "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" - quic_port: 9095 + address: "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65" + quic_port: 9204 autonetkey: true autopassword: true role: diff --git a/tests/integration/fns.sh b/tests/integration/fns.sh index 865bf7296a..2251c9f200 100644 --- a/tests/integration/fns.sh +++ b/tests/integration/fns.sh @@ -15,28 +15,22 @@ fi # Environment variables RPC_URL="ws://localhost:8545" -PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_AG="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +PRIVATE_KEY_CN1="0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +PRIVATE_KEY_CN2="0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" +PRIVATE_KEY_CN3="0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" NETWORK_PRIVATE_KEY_AG="0x51a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" CIPHERNODE_SECRET="We are the music makers and we are the dreamers of the dreams." -# These contracts are based on the deterministic order of hardhat deploy -# We _may_ wish to get these off the hardhat environment somehow? -ENCLAVE_CONTRACT="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -REGISTRY_CONTRACT="0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" -REGISTRY_FILTER_CONTRACT="0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -INPUT_VALIDATOR_CONTRACT="0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" - # These are random addresses for now -CIPHERNODE_ADDRESS_1="0x2546BcD3c84621e976D8185a91A922aE77ECEc30" -CIPHERNODE_ADDRESS_2="0xbDA5747bFD65F08deb54cb465eB87D40e51B197E" -CIPHERNODE_ADDRESS_3="0xdD2FD4581271e230360230F9337D5c0430Bf44C0" -CIPHERNODE_ADDRESS_4="0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199" +CIPHERNODE_ADDRESS_1="0x90F79bf6EB2c4f870365E785982E1f101E93b906" +CIPHERNODE_ADDRESS_2="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +CIPHERNODE_ADDRESS_3="0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" # These are the network private keys for the ciphernodes NETWORK_PRIVATE_KEY_1="0x11a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" NETWORK_PRIVATE_KEY_2="0x21a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" NETWORK_PRIVATE_KEY_3="0x31a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" -NETWORK_PRIVATE_KEY_4="0x41a1e500a548b70d88184a1e042900c0ed6c57f8710bcc35dc8c85fa33d3f580" if command -v enclave >/dev/null 2>&1; then ENCLAVE_BIN="enclave" diff --git a/tests/integration/persist.sh b/tests/integration/persist.sh index c29f6386c1..480805f3f8 100755 --- a/tests/integration/persist.sh +++ b/tests/integration/persist.sh @@ -20,7 +20,10 @@ pnpm evm:clean pnpm evm:deploy --network localhost # set wallet to ag specifically -enclave_wallet_set ag "$PRIVATE_KEY" +enclave_wallet_set ag "$PRIVATE_KEY_AG" +enclave_wallet_set cn1 "$PRIVATE_KEY_CN1" +enclave_wallet_set cn2 "$PRIVATE_KEY_CN2" +enclave_wallet_set cn3 "$PRIVATE_KEY_CN3" # start swarm enclave_nodes_up @@ -36,9 +39,6 @@ pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_2 --network localho heading "Add ciphernode $CIPHERNODE_ADDRESS_3" pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_3 --network localhost -heading "Add ciphernode $CIPHERNODE_ADDRESS_4" -pnpm ciphernode:add --ciphernode-address $CIPHERNODE_ADDRESS_4 --network localhost - heading "Request Committee" ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh --moduli 0x3FFFFFFF000001 --degree 2048 --plaintext-modulus 1032193)