diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index de319534d8..6ecda11931 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -61,7 +61,7 @@ impl Handler for CommitteeFinalizer { 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; + let committee_deadline = msg.committee_deadline; const FINALIZATION_BUFFER_SECONDS: u64 = 1; @@ -85,12 +85,12 @@ impl Handler for CommitteeFinalizer { 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 + let seconds_until_deadline = if committee_deadline > current_timestamp { + (committee_deadline - current_timestamp) + FINALIZATION_BUFFER_SECONDS } else { info!( e3_id = %e3_id_for_async, - submission_deadline = submission_deadline, + committee_deadline = committee_deadline, current_timestamp = current_timestamp, "Submission deadline already passed, finalizing with buffer" ); @@ -99,7 +99,7 @@ impl Handler for CommitteeFinalizer { info!( e3_id = %e3_id_for_async, - submission_deadline = submission_deadline, + committee_deadline = committee_deadline, current_timestamp = current_timestamp, seconds_to_wait = seconds_until_deadline, "Scheduling committee finalization" diff --git a/crates/events/src/enclave_event/committee_requested.rs b/crates/events/src/enclave_event/committee_requested.rs index ebe8b4a08c..2c7d620710 100644 --- a/crates/events/src/enclave_event/committee_requested.rs +++ b/crates/events/src/enclave_event/committee_requested.rs @@ -16,7 +16,7 @@ pub struct CommitteeRequested { pub seed: Seed, pub threshold: [usize; 2], pub request_block: u64, - pub submission_deadline: u64, + pub committee_deadline: u64, pub chain_id: u64, } @@ -24,8 +24,8 @@ 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 + "e3_id: {}, seed: {:?}, threshold: [{}, {}], request_block: {}, committee_deadline: {}, chain_id: {}", + self.e3_id, self.seed, self.threshold[0], self.threshold[1], self.request_block, self.committee_deadline, self.chain_id ) } } diff --git a/crates/events/src/enclave_event/e3_failed.rs b/crates/events/src/enclave_event/e3_failed.rs new file mode 100644 index 0000000000..ebce4b531a --- /dev/null +++ b/crates/events/src/enclave_event/e3_failed.rs @@ -0,0 +1,58 @@ +// 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}; + +/// Reason why an E3 failed +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum FailureReason { + None, + CommitteeFormationTimeout, + InsufficientCommitteeMembers, + DKGTimeout, + DKGInvalidShares, + NoInputsReceived, + ComputeTimeout, + ComputeProviderExpired, + ComputeProviderFailed, + RequesterCancelled, + DecryptionTimeout, + DecryptionInvalidShares, + VerificationFailed, +} + +/// E3 lifecycle stage +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum E3Stage { + None, + Requested, + CommitteeFinalized, + KeyPublished, + CiphertextReady, + Complete, + Failed, +} + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct E3Failed { + pub e3_id: E3id, + pub failed_at_stage: E3Stage, + pub reason: FailureReason, +} + +impl Display for E3Failed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "E3Failed {{ e3_id: {}, stage: {:?}, reason: {:?} }}", + self.e3_id, self.failed_at_stage, self.reason + ) + } +} diff --git a/crates/events/src/enclave_event/e3_stage_changed.rs b/crates/events/src/enclave_event/e3_stage_changed.rs new file mode 100644 index 0000000000..d7c9d1dba0 --- /dev/null +++ b/crates/events/src/enclave_event/e3_stage_changed.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; +use actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +// Re-export E3Stage from e3_failed to avoid duplication +pub use super::e3_failed::E3Stage; + +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct E3StageChanged { + pub e3_id: E3id, + pub previous_stage: E3Stage, + pub new_stage: E3Stage, +} + +impl Display for E3StageChanged { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "E3StageChanged {{ e3_id: {}, {:?} -> {:?} }}", + self.e3_id, self.previous_stage, self.new_stage + ) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 1d4f7f8f48..2256a8e979 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -16,8 +16,10 @@ mod compute_request; mod configuration_updated; mod decryptionshare_created; mod die; +mod e3_failed; mod e3_request_complete; mod e3_requested; +mod e3_stage_changed; mod enclave_error; mod encryption_key_collection_failed; mod encryption_key_created; @@ -57,8 +59,10 @@ pub use compute_request::*; pub use configuration_updated::*; pub use decryptionshare_created::*; pub use die::*; +pub use e3_failed::*; pub use e3_request_complete::*; pub use e3_requested::*; +pub use e3_stage_changed::*; use e3_utils::{colorize, Color}; pub use enclave_error::*; pub use encryption_key_collection_failed::*; @@ -206,6 +210,8 @@ pub enum EnclaveEventData { PlaintextOutputPublished(PlaintextOutputPublished), EnclaveError(EnclaveError), E3RequestComplete(E3RequestComplete), + E3Failed(E3Failed), + E3StageChanged(E3StageChanged), Shutdown(Shutdown), DocumentReceived(DocumentReceived), ThresholdShareCreated(ThresholdShareCreated), @@ -432,6 +438,8 @@ impl EnclaveEventData { EnclaveEventData::TicketSubmitted(ref data) => Some(data.e3_id.clone()), EnclaveEventData::EncryptionKeyCreated(ref data) => Some(data.e3_id.clone()), EnclaveEventData::ComputeResponse(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::E3Failed(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::E3StageChanged(ref data) => Some(data.e3_id.clone()), _ => None, } } @@ -469,6 +477,8 @@ impl_event_types!( PlaintextAggregated, PublishDocumentRequested, E3RequestComplete, + E3Failed, + E3StageChanged, CiphernodeSelected, CiphernodeAdded, CiphernodeRemoved, diff --git a/crates/evm-helpers/src/contracts.rs b/crates/evm-helpers/src/contracts.rs index b3eb8db57e..a85b07ed0c 100644 --- a/crates/evm-helpers/src/contracts.rs +++ b/crates/evm-helpers/src/contracts.rs @@ -43,9 +43,7 @@ sol! { uint256 seed; uint32[2] threshold; uint256 requestBlock; - uint256[2] startWindow; - uint256 duration; - uint256 expiration; + uint256[2] inputWindow; bytes32 encryptionSchemeId; address e3Program; bytes e3ProgramParams; @@ -60,14 +58,56 @@ sol! { #[derive(Debug)] struct E3RequestParams { uint32[2] threshold; - uint256[2] startWindow; - uint256 duration; + uint256[2] inputWindow; address e3Program; bytes e3ProgramParams; bytes computeProviderParams; bytes customParams; } + #[derive(Debug, PartialEq)] + enum E3Stage { + None, + Requested, + CommitteeFinalized, + KeyPublished, + CiphertextReady, + Complete, + Failed + } + + #[derive(Debug, PartialEq)] + enum FailureReason { + None, + CommitteeFormationTimeout, + InsufficientCommitteeMembers, + DKGTimeout, + DKGInvalidShares, + NoInputsReceived, + ComputeTimeout, + ComputeProviderExpired, + ComputeProviderFailed, + RequesterCancelled, + DecryptionTimeout, + DecryptionInvalidShares, + VerificationFailed + } + + #[derive(Debug)] + struct E3TimeoutConfig { + uint256 dkgWindow; + uint256 computeWindow; + uint256 decryptionWindow; + uint256 gracePeriod; + } + + #[derive(Debug)] + struct E3Deadlines { + uint256 dkgDeadline; + uint256 computeDeadline; + uint256 decryptionDeadline; + } + #[derive(Debug)] #[sol(rpc)] contract Enclave { @@ -76,14 +116,17 @@ sol! { mapping(uint256 e3Id => bytes params) public e3Params; mapping(address e3Program => bool allowed) public e3Programs; function request(E3RequestParams calldata requestParams) external returns (uint256 e3Id, E3 memory e3); - function activate(uint256 e3Id) external returns (bool success); function enableE3Program(address e3Program) public onlyOwner returns (bool success); - function publishInput(uint256 e3Id, bytes calldata data) external returns (bool success); function publishCiphertextOutput(uint256 e3Id, bytes calldata ciphertextOutput, bytes calldata proof) external returns (bool success); 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); + function getE3Stage(uint256 e3Id) external view returns (E3Stage stage); + function getFailureReason(uint256 e3Id) external view returns (FailureReason reason); + function getRequester(uint256 e3Id) external view returns (address requester); + function getDeadlines(uint256 e3Id) external view returns (E3Deadlines memory deadlines); + function getTimeoutConfig() external view returns (E3TimeoutConfig memory config); } } @@ -115,12 +158,21 @@ pub trait EnclaveRead { async fn get_e3_quote( &self, threshold: [u32; 2], - start_window: [U256; 2], - duration: U256, + input_window: [U256; 2], e3_program: Address, e3_params: Bytes, compute_provider_params: Bytes, ) -> Result; + + async fn get_e3_stage(&self, e3_id: U256) -> Result; + + async fn get_failure_reason(&self, e3_id: U256) -> Result; + + async fn get_requester(&self, e3_id: U256) -> Result
; + + async fn get_deadlines(&self, e3_id: U256) -> Result; + + async fn get_timeout_config(&self) -> Result; } /// Trait for write operations on the Enclave contract @@ -130,23 +182,16 @@ pub trait EnclaveWrite { async fn request_e3( &self, threshold: [u32; 2], - start_window: [U256; 2], - duration: U256, + input_window: [U256; 2], e3_program: Address, e3_params: Bytes, compute_provider_params: Bytes, custom_params: Bytes, ) -> Result<(TransactionReceipt, U256)>; - /// Activate an E3 - async fn activate(&self, e3_id: U256) -> Result; - /// Enable an E3 program async fn enable_e3_program(&self, e3_program: Address) -> Result; - /// Publish input data for an E3 - async fn publish_input(&self, e3_id: U256, data: Bytes) -> Result; - /// Publish ciphertext output with proof async fn publish_ciphertext_output( &self, @@ -341,16 +386,14 @@ where async fn get_e3_quote( &self, threshold: [u32; 2], - start_window: [U256; 2], - duration: U256, + input_window: [U256; 2], e3_program: Address, e3_params: Bytes, compute_provider_params: Bytes, ) -> Result { let e3_request = E3RequestParams { threshold, - startWindow: start_window, - duration, + inputWindow: input_window, e3Program: e3_program, e3ProgramParams: e3_params, computeProviderParams: compute_provider_params, @@ -361,6 +404,36 @@ where let fee = contract.getE3Quote(e3_request).call().await?; Ok(fee) } + + async fn get_e3_stage(&self, e3_id: U256) -> Result { + let contract = Enclave::new(self.contract_address, &self.provider); + let stage = contract.getE3Stage(e3_id).call().await?; + Ok(stage) + } + + async fn get_failure_reason(&self, e3_id: U256) -> Result { + let contract = Enclave::new(self.contract_address, &self.provider); + let reason = contract.getFailureReason(e3_id).call().await?; + Ok(reason) + } + + async fn get_requester(&self, e3_id: U256) -> Result
{ + let contract = Enclave::new(self.contract_address, &self.provider); + let requester = contract.getRequester(e3_id).call().await?; + Ok(requester) + } + + async fn get_deadlines(&self, e3_id: U256) -> Result { + let contract = Enclave::new(self.contract_address, &self.provider); + let deadlines = contract.getDeadlines(e3_id).call().await?; + Ok(deadlines) + } + + async fn get_timeout_config(&self) -> Result { + let contract = Enclave::new(self.contract_address, &self.provider); + let config = contract.getTimeoutConfig().call().await?; + Ok(config) + } } // Implement EnclaveWrite only for contracts with ReadWrite marker @@ -369,8 +442,7 @@ impl EnclaveWrite for EnclaveContract { async fn request_e3( &self, threshold: [u32; 2], - start_window: [U256; 2], - duration: U256, + input_window: [U256; 2], e3_program: Address, e3_params: Bytes, compute_provider_params: Bytes, @@ -387,8 +459,7 @@ impl EnclaveWrite for EnclaveContract { let e3_request = E3RequestParams { threshold, - startWindow: start_window, - duration, + inputWindow: input_window, e3Program: e3_program, e3ProgramParams: e3_params.clone(), computeProviderParams: compute_provider_params.clone(), @@ -401,20 +472,6 @@ impl EnclaveWrite for EnclaveContract { Ok((receipt, e3_id)) } - async fn activate(&self, e3_id: U256) -> Result { - let _guard = NONCE_LOCK.lock().await; - let wallet_addr = self - .wallet_address - .ok_or_else(|| eyre::eyre!("No wallet address configured"))?; - let nonce = get_next_nonce(&*self.provider, wallet_addr).await?; - - let contract = Enclave::new(self.contract_address, &self.provider); - let builder = contract.activate(e3_id).nonce(nonce); - let receipt = builder.send().await?.get_receipt().await?; - - Ok(receipt) - } - async fn enable_e3_program(&self, e3_program: Address) -> Result { let _guard = NONCE_LOCK.lock().await; let wallet_addr = self @@ -429,20 +486,6 @@ impl EnclaveWrite for EnclaveContract { Ok(receipt) } - async fn publish_input(&self, e3_id: U256, data: Bytes) -> Result { - let _guard = NONCE_LOCK.lock().await; - let wallet_addr = self - .wallet_address - .ok_or_else(|| eyre::eyre!("No wallet address configured"))?; - let nonce = get_next_nonce(&*self.provider, wallet_addr).await?; - - let contract = Enclave::new(self.contract_address, &self.provider); - let builder = contract.publishInput(e3_id, data).nonce(nonce); - let receipt = builder.send().await?.get_receipt().await?; - - Ok(receipt) - } - async fn publish_ciphertext_output( &self, e3_id: U256, diff --git a/crates/evm-helpers/src/events.rs b/crates/evm-helpers/src/events.rs index b2197fbb14..8d1d2b6896 100644 --- a/crates/evm-helpers/src/events.rs +++ b/crates/evm-helpers/src/events.rs @@ -9,9 +9,6 @@ use alloy::sol; // TODO: extract these from that actual contract sol! { - #[derive(Debug)] - event E3Activated(uint256 e3Id, uint256 expiration, bytes32 committeePublicKey); - #[derive(Debug)] event E3Requested(uint256 e3Id, E3 e3, IE3Program indexed e3Program); @@ -30,9 +27,7 @@ sol! { uint256 seed; uint32[2] threshold; uint256 requestBlock; - uint256[2] startWindow; - uint256 duration; - uint256 expiration; + uint256[2] inputWindow; bytes32 encryptionSchemeId; IE3Program e3Program; bytes e3ProgramParams; @@ -52,4 +47,41 @@ sol! { #[derive(Debug)] event CommitteePublished(uint256 indexed e3Id, address[] nodes, bytes publicKey); + + #[derive(Debug)] + enum E3Stage { + None, + Requested, + CommitteeFinalized, + KeyPublished, + CiphertextReady, + Complete, + Failed + } + + #[derive(Debug)] + enum FailureReason { + None, + CommitteeFormationTimeout, + InsufficientCommitteeMembers, + DKGTimeout, + DKGInvalidShares, + NoInputsReceived, + ComputeTimeout, + ComputeProviderExpired, + ComputeProviderFailed, + RequesterCancelled, + DecryptionTimeout, + DecryptionInvalidShares, + VerificationFailed + } + + #[derive(Debug)] + event CommitteeFinalized(uint256 indexed e3Id); + + #[derive(Debug)] + event E3StageChanged(uint256 indexed e3Id, E3Stage previousStage, E3Stage newStage); + + #[derive(Debug)] + event E3Failed(uint256 indexed e3Id, E3Stage failedAtStage, FailureReason reason); } diff --git a/crates/evm-helpers/tests/fixtures/fake_enclave.sol b/crates/evm-helpers/tests/fixtures/fake_enclave.sol index 151afd4046..87c4004cf1 100644 --- a/crates/evm-helpers/tests/fixtures/fake_enclave.sol +++ b/crates/evm-helpers/tests/fixtures/fake_enclave.sol @@ -7,16 +7,10 @@ pragma solidity >=0.4.24; contract FakeEnclave { - event E3Activated(uint256 e3Id, uint256 expiration, bytes32 committeePublicKey); event InputPublished(uint256 indexed e3Id, bytes data, uint256 inputHash, uint256 index); event CiphertextOutputPublished(uint256 indexed e3Id, bytes ciphertextOutput); event PlaintextOutputPublished(uint256 indexed e3Id, bytes plaintextOutput); - event CommitteePublished(uint256 indexed e3Id, bytes publicKey); - - // Emit E3Activated event with passed test data - function emitE3Activated(uint256 e3Id, uint256 expiration, bytes32 committeePublicKey) public { - emit E3Activated(e3Id, expiration, committeePublicKey); - } + event CommitteePublished(uint256 indexed e3Id, address[] nodes, bytes publicKey); // Emit InputPublished event with passed test data function emitInputPublished(uint256 e3Id, bytes memory data, uint256 inputHash, uint256 index) public { @@ -35,7 +29,8 @@ contract FakeEnclave { // Emit CommitteePublished event with passed test data function emitCommitteePublished(uint256 e3Id, bytes memory publicKey) public { - emit CommitteePublished(e3Id, publicKey); + address[] memory nodes = new address[](1); + emit CommitteePublished(e3Id, nodes, publicKey); } function getE3(uint256 _e3Id) external view returns (E3 memory e3) { @@ -43,9 +38,7 @@ contract FakeEnclave { seed: 123456789012, threshold: [uint32(2), uint32(3)], requestBlock: 18750000, - startWindow: [uint256(18750100), uint256(18750200)], - duration: 100, - expiration: block.timestamp + 1 days, + inputWindow: [uint256(18750100), uint256(18750200)], encryptionSchemeId: bytes32(keccak256("AES-256-GCM")), e3Program: 0x7F3E4df648B8Cb96C1D343be976b91B97CaD5c21, decryptionVerifier: 0x4B0D8c2E5f7a6c832f8b16d3aB0e7F5d9E9B24b1, @@ -62,9 +55,7 @@ struct E3 { uint256 seed; uint32[2] threshold; uint256 requestBlock; - uint256[2] startWindow; - uint256 duration; - uint256 expiration; + uint256[2] inputWindow; bytes32 encryptionSchemeId; address e3Program; bytes e3ProgramParams; diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index a841371cff..456b63fd96 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -99,7 +99,7 @@ impl From for e3_events::CommitteeRequested { 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(), + committee_deadline: value.0.committeeDeadline.to(), chain_id: value.1, } } diff --git a/crates/evm/src/enclave_sol_reader.rs b/crates/evm/src/enclave_sol_reader.rs index 8d89f9c8bb..aa5d6b0c29 100644 --- a/crates/evm/src/enclave_sol_reader.rs +++ b/crates/evm/src/enclave_sol_reader.rs @@ -11,6 +11,7 @@ use alloy::primitives::{LogData, B256}; use alloy::{sol, sol_types::SolEvent}; use e3_events::E3id; use e3_events::EnclaveEventData; +use e3_events::{E3Failed, E3Stage, E3StageChanged, FailureReason}; use e3_fhe_params::decode_bfv_params_arc; use e3_trbfv::helpers::calculate_error_size; use e3_utils::ArcBytes; @@ -104,6 +105,77 @@ impl From for EnclaveEventData { } } +struct E3FailedWithChainId(pub IEnclave::E3Failed, pub u64); + +fn convert_u8_to_e3_stage(stage_u8: u8) -> E3Stage { + match stage_u8 { + 0 => E3Stage::None, + 1 => E3Stage::Requested, + 2 => E3Stage::CommitteeFinalized, + 3 => E3Stage::KeyPublished, + 4 => E3Stage::CiphertextReady, + 5 => E3Stage::Complete, + 6 => E3Stage::Failed, + _ => E3Stage::None, + } +} + +// Helper function to convert u8 to Rust FailureReason +fn convert_u8_to_failure_reason(reason_u8: u8) -> FailureReason { + match reason_u8 { + 0 => FailureReason::None, + 1 => FailureReason::CommitteeFormationTimeout, + 2 => FailureReason::InsufficientCommitteeMembers, + 3 => FailureReason::DKGTimeout, + 4 => FailureReason::DKGInvalidShares, + 5 => FailureReason::NoInputsReceived, + 6 => FailureReason::ComputeTimeout, + 7 => FailureReason::ComputeProviderExpired, + 8 => FailureReason::ComputeProviderFailed, + 9 => FailureReason::RequesterCancelled, + 10 => FailureReason::DecryptionTimeout, + 11 => FailureReason::DecryptionInvalidShares, + 12 => FailureReason::VerificationFailed, + _ => FailureReason::None, + } +} + +impl From for E3Failed { + fn from(value: E3FailedWithChainId) -> Self { + E3Failed { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + failed_at_stage: convert_u8_to_e3_stage(value.0.failedAtStage), + reason: convert_u8_to_failure_reason(value.0.reason), + } + } +} + +impl From for EnclaveEventData { + fn from(value: E3FailedWithChainId) -> Self { + let payload: E3Failed = value.into(); + payload.into() + } +} + +struct E3StageChangedWithChainId(pub IEnclave::E3StageChanged, pub u64); + +impl From for E3StageChanged { + fn from(value: E3StageChangedWithChainId) -> Self { + E3StageChanged { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + previous_stage: convert_u8_to_e3_stage(value.0.previousStage), + new_stage: convert_u8_to_e3_stage(value.0.newStage), + } + } +} + +impl From for EnclaveEventData { + fn from(value: E3StageChangedWithChainId) -> Self { + let payload: E3StageChanged = value.into(); + payload.into() + } +} + pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { match topic { Some(&IEnclave::E3Requested::SIGNATURE_HASH) => { @@ -124,6 +196,32 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< CiphertextOutputPublishedWithChainId(event, chain_id), )) } + Some(&IEnclave::E3Failed::SIGNATURE_HASH) => { + let Ok(event) = IEnclave::E3Failed::decode_log_data(data) else { + error!("Error parsing event E3Failed after topic matched!"); + return None; + }; + info!( + "E3Failed event received: e3_id={}, stage={:?}, reason={:?}", + event.e3Id, event.failedAtStage, event.reason + ); + Some(EnclaveEventData::from(E3FailedWithChainId(event, chain_id))) + } + Some(&IEnclave::E3StageChanged::SIGNATURE_HASH) => { + let Ok(event) = IEnclave::E3StageChanged::decode_log_data(data) else { + error!("Error parsing event E3StageChanged after topic matched!"); + return None; + }; + trace!( + "E3StageChanged event received: e3_id={}, {:?} -> {:?}", + event.e3Id, + event.previousStage, + event.newStage + ); + Some(EnclaveEventData::from(E3StageChangedWithChainId( + event, chain_id, + ))) + } _topic => { trace!( topic=?_topic, diff --git a/crates/indexer/src/indexer.rs b/crates/indexer/src/indexer.rs index e185af91e0..7bdf58ff4e 100644 --- a/crates/indexer/src/indexer.rs +++ b/crates/indexer/src/indexer.rs @@ -19,9 +19,7 @@ use e3_evm_helpers::{ EnclaveContract, EnclaveContractFactory, EnclaveRead, ProviderType, ReadOnly, ReadWrite, }, event_listener::EventListener, - events::{ - CiphertextOutputPublished, CommitteePublished, E3Activated, PlaintextOutputPublished, - }, + events::{CiphertextOutputPublished, CommitteePublished, PlaintextOutputPublished}, }; use eyre::eyre; use eyre::Result; @@ -332,101 +330,50 @@ impl EnclaveIndexer { } async fn register_committee_published(&mut self) -> Result<()> { - self.add_event_handler(move |e: CommitteePublished, ctx| { - async move { - let db = ctx.store(); - let e3_id = u64_try_from(e.e3Id)?; - info!( - "CommitteePublished: id={}, public_key_len={}", - e.e3Id, - e.publicKey.len() - ); - - // Store the public key temporarily to use when E3Activated happens - let temp_key = format!("_committee_pubkey:{e3_id}"); - let mut db_clone = db.clone(); - db_clone - .insert(&temp_key, &e.publicKey.to_vec()) - .await - .map_err(|e| eyre::eyre!("Failed to store committee public key: {}", e))?; - info!("Stored committee_public_key temporarily for E3 {}", e3_id); - Ok(()) - } - }) - .await; - Ok(()) - } + self.add_event_handler(move |e: CommitteePublished, ctx| async move { + let contract = ctx.contract(); + let db = ctx.store(); + let enclave_address = ctx.enclave_address(); + let e3_id = u64_try_from(e.e3Id)?; - async fn register_e3_activated(&mut self) -> Result<()> { - self.add_event_handler(move |e: E3Activated, ctx| { - async move { - let contract = ctx.contract(); - let db = ctx.store(); - let enclave_address = ctx.enclave_address(); - let e3_id = u64_try_from(e.e3Id)?; - - // Get the actual public key from CommitteePublished event - // CommitteePublished always happens before E3Activated, so it should always be in temporary storage - let temp_key = format!("_committee_pubkey:{e3_id}"); - let committee_public_key = db - .get::>(&temp_key) - .await - .map_err(|e| eyre::eyre!("Failed to get committee public key: {}", e))? - .ok_or_else(|| { - eyre::eyre!( - "CommitteePublished event not found for E3 {} - this should not happen", - e3_id - ) - })?; - - info!( - "E3Activated: id={}, expiration={}, using actual public_key (len={})", - e.e3Id, - e.expiration, - committee_public_key.len() - ); - - // Remove the temporary storage - let mut db_clone = db.clone(); - let _ = db_clone.modify::, _>(&temp_key, |_| None).await; - - let e3 = contract.get_e3(e.e3Id).await?; - let duration = u64_try_from(e3.duration)?; - let expiration = u64_try_from(e.expiration)?; - let seed = e3.seed.to_be_bytes(); - let request_block = u64_try_from(e3.requestBlock)?; - let start_window = [ - u64_try_from(e3.startWindow[0])?, - u64_try_from(e3.startWindow[1])?, - ]; - - // NOTE: we are only saving protocol specific info - // here and not CRISP specific info so E3 corresponds to the solidity E3 - let e3_obj = E3 { - chain_id: ctx.chain_id(), - ciphertext_inputs: vec![], - ciphertext_output: vec![], - committee_public_key, - duration, - custom_params: e3.customParams.to_vec(), - e3_params: e3.e3ProgramParams.to_vec(), - enclave_address, - encryption_scheme_id: e3.encryptionSchemeId.to_vec(), - expiration, - id: e3_id, - plaintext_output: vec![], - request_block, - seed, - start_window, - threshold: e3.threshold, - requester: e3.requester.to_string(), - }; - - let mut repo = E3Repository::new(db, e3_id); - - repo.set_e3(e3_obj).await?; - Ok(()) - } + info!( + "CommitteePublished: id={}, public_key_len={}", + e.e3Id, + e.publicKey.len() + ); + + let e3 = contract.get_e3(e.e3Id).await?; + let seed = e3.seed.to_be_bytes(); + let request_block = u64_try_from(e3.requestBlock)?; + let input_window = [ + u64_try_from(e3.inputWindow[0])?, + u64_try_from(e3.inputWindow[1])?, + ]; + + let e3_obj = E3 { + chain_id: ctx.chain_id(), + ciphertext_inputs: vec![], + ciphertext_output: vec![], + committee_public_key: e.publicKey.to_vec(), + custom_params: e3.customParams.to_vec(), + e3_params: e3.e3ProgramParams.to_vec(), + enclave_address, + encryption_scheme_id: e3.encryptionSchemeId.to_vec(), + id: e3_id, + plaintext_output: vec![], + request_block, + seed, + input_window, + threshold: e3.threshold, + requester: e3.requester.to_string(), + }; + + let mut repo = E3Repository::new(db, e3_id); + repo.set_e3(e3_obj).await?; + + info!("E3 {} created and stored", e3_id); + + Ok(()) }) .await; Ok(()) @@ -492,7 +439,6 @@ impl EnclaveIndexer { async fn setup_listeners(&mut self) -> Result<()> { info!("Setting up listeners for EnclaveIndexer..."); self.register_committee_published().await?; - self.register_e3_activated().await?; self.register_ciphertext_output_published().await?; self.register_plaintext_output_published().await?; self.register_blocktime_callback_handler().await?; diff --git a/crates/indexer/src/models.rs b/crates/indexer/src/models.rs index ce19072bd6..b3c8a0fec7 100644 --- a/crates/indexer/src/models.rs +++ b/crates/indexer/src/models.rs @@ -14,17 +14,15 @@ pub struct E3 { pub ciphertext_inputs: Vec<(Vec, u64)>, pub ciphertext_output: Vec, pub committee_public_key: Vec, - pub duration: u64, pub e3_params: Vec, pub custom_params: Vec, pub enclave_address: String, pub encryption_scheme_id: Vec, - pub expiration: u64, pub id: u64, pub plaintext_output: Vec, pub request_block: u64, pub seed: [u8; 32], - pub start_window: [u64; 2], + pub input_window: [u64; 2], pub threshold: [u32; 2], pub requester: String, } diff --git a/crates/indexer/tests/fixtures/fake_enclave.sol b/crates/indexer/tests/fixtures/fake_enclave.sol index fa830792f6..87c4004cf1 100644 --- a/crates/indexer/tests/fixtures/fake_enclave.sol +++ b/crates/indexer/tests/fixtures/fake_enclave.sol @@ -7,17 +7,11 @@ pragma solidity >=0.4.24; contract FakeEnclave { - event E3Activated(uint256 e3Id, uint256 expiration, bytes32 committeePublicKey); event InputPublished(uint256 indexed e3Id, bytes data, uint256 inputHash, uint256 index); event CiphertextOutputPublished(uint256 indexed e3Id, bytes ciphertextOutput); event PlaintextOutputPublished(uint256 indexed e3Id, bytes plaintextOutput); event CommitteePublished(uint256 indexed e3Id, address[] nodes, bytes publicKey); - // Emit E3Activated event with passed test data - function emitE3Activated(uint256 e3Id, uint256 expiration, bytes32 committeePublicKey) public { - emit E3Activated(e3Id, expiration, committeePublicKey); - } - // Emit InputPublished event with passed test data function emitInputPublished(uint256 e3Id, bytes memory data, uint256 inputHash, uint256 index) public { emit InputPublished(e3Id, data, inputHash, index); @@ -44,9 +38,7 @@ contract FakeEnclave { seed: 123456789012, threshold: [uint32(2), uint32(3)], requestBlock: 18750000, - startWindow: [uint256(18750100), uint256(18750200)], - duration: 100, - expiration: block.timestamp + 1 days, + inputWindow: [uint256(18750100), uint256(18750200)], encryptionSchemeId: bytes32(keccak256("AES-256-GCM")), e3Program: 0x7F3E4df648B8Cb96C1D343be976b91B97CaD5c21, decryptionVerifier: 0x4B0D8c2E5f7a6c832f8b16d3aB0e7F5d9E9B24b1, @@ -63,9 +55,7 @@ struct E3 { uint256 seed; uint32[2] threshold; uint256 requestBlock; - uint256[2] startWindow; - uint256 duration; - uint256 expiration; + uint256[2] inputWindow; bytes32 encryptionSchemeId; address e3Program; bytes e3ProgramParams; diff --git a/crates/indexer/tests/integration.rs b/crates/indexer/tests/integration.rs index 4b26ec4efe..014bf2f582 100644 --- a/crates/indexer/tests/integration.rs +++ b/crates/indexer/tests/integration.rs @@ -100,7 +100,7 @@ async fn test_indexer() -> Result<()> { let sk = SecretKey::random(¶ms, &mut rng); let pk = PublicKey::new(&sk, &mut rng); - let public_key_commitment = compute_pk_commitment( + _ = compute_pk_commitment( pk.to_bytes(), params.degree(), params.plaintext(), @@ -119,17 +119,6 @@ async fn test_indexer() -> Result<()> { .watch() .await?; - enclave_contract - .emitE3Activated( - Uint::from(E3_ID), - Uint::from(THRESHOLD), - FixedBytes::from(public_key_commitment), - ) - .send() - .await? - .watch() - .await?; - enclave_contract .emitInputPublished( Uint::from(E3_ID), diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 84b29045b0..44819d10c4 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -39,7 +39,7 @@ use std::{ mem, sync::{Arc, Mutex}, }; -use tracing::{error, info, warn}; +use tracing::{error, info, trace, warn}; use crate::encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; use crate::threshold_share_collector::ThresholdShareCollector; @@ -949,6 +949,29 @@ impl Handler for ThresholdKeyshare { let _ = self.handle_encryption_key_created(data, ctx.address()); } EnclaveEventData::E3RequestComplete(data) => self.notify_sync(ctx, data), + EnclaveEventData::E3Failed(data) => { + warn!( + "E3 failed: {:?}. Shutting down ThresholdKeyshare for e3_id={}", + data.reason, data.e3_id + ); + self.notify_sync(ctx, E3RequestComplete { e3_id: data.e3_id }); + } + EnclaveEventData::E3StageChanged(data) => { + use e3_events::E3Stage; + match &data.new_stage { + E3Stage::Complete | E3Stage::Failed => { + info!("E3 reached terminal stage {:?}. Shutting down ThresholdKeyshare for e3_id={}", data.new_stage, data.e3_id); + self.notify_sync(ctx, E3RequestComplete { e3_id: data.e3_id }); + } + _ => { + trace!( + "E3 stage changed to {:?} for e3_id={}", + data.new_stage, + data.e3_id + ); + } + } + } EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, msg.to_typed_event(data)) } diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index b5e9e37b4c..adfab6c8ac 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -13,8 +13,8 @@ use anyhow::Result; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ prelude::*, CiphernodeAdded, CiphernodeRemoved, CommitteeFinalized, CommitteePublished, - ConfigurationUpdated, E3Requested, EType, EnclaveEvent, EventType, OperatorActivationChanged, - PlaintextOutputPublished, Seed, TicketBalanceUpdated, + ConfigurationUpdated, E3Failed, E3Requested, E3Stage, E3StageChanged, EType, EnclaveEvent, + EventType, OperatorActivationChanged, PlaintextOutputPublished, Seed, TicketBalanceUpdated, }; use e3_events::{BusHandle, E3id, EnclaveEventData}; use e3_utils::NotifySync; @@ -246,6 +246,8 @@ impl Sortition { EventType::CommitteePublished, EventType::PlaintextOutputPublished, EventType::CommitteeFinalized, + EventType::E3Failed, + EventType::E3StageChanged, ], addr.clone().into(), ); @@ -285,6 +287,51 @@ impl Sortition { None }) } + + /// Helper method to decrement active jobs for an E3's committee + fn decrement_jobs_for_e3(&mut self, e3_id: &E3id, reason: &str) { + if let Err(err) = self.node_state.try_mutate(|mut state_map| { + let chain_id = e3_id.chain_id(); + let e3_id_str = format!("{}:{}", chain_id, e3_id.e3_id()); + + 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 = ?e3_id, + active_jobs = node.active_jobs, + reason = reason, + "Decremented active jobs for node" + ); + } + } + + info!( + e3_id = ?e3_id, + committee_size = committee_nodes.len(), + reason = reason, + "E3 completed/failed - decremented active jobs for committee" + ); + } else { + info!( + e3_id = ?e3_id, + reason = reason, + "No committee found (might have been completed already)" + ); + } + } + + Ok(state_map) + }) { + self.bus.err(EType::Sortition, err); + } + } } impl Actor for Sortition { @@ -526,52 +573,36 @@ impl Handler for Sortition { } } } -/// 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()); + self.decrement_jobs_for_e3(&msg.e3_id, "PlaintextOutputPublished"); + } +} - // 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); +impl Handler for Sortition { + type Result = (); - 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" - ); - } - } + fn handle(&mut self, msg: E3Failed, _ctx: &mut Self::Context) -> Self::Result { + let reason = format!("E3Failed: {:?}", msg.reason); + self.decrement_jobs_for_e3(&msg.e3_id, &reason); + } +} - 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)" - ); - } - } +impl Handler for Sortition { + type Result = (); - Ok(state_map) - }) { - self.bus.err(EType::Sortition, err); + fn handle(&mut self, msg: E3StageChanged, _ctx: &mut Self::Context) -> Self::Result { + match msg.new_stage { + E3Stage::Complete | E3Stage::Failed => { + let reason = format!("E3StageChanged to {:?}", msg.new_stage); + self.decrement_jobs_for_e3(&msg.e3_id, &reason); + } + _ => { + // Non-terminal stages, no action needed + } } } } diff --git a/docs/pages/building-with-enclave.mdx b/docs/pages/building-with-enclave.mdx index 3ed67e64bc..dd51597852 100644 --- a/docs/pages/building-with-enclave.mdx +++ b/docs/pages/building-with-enclave.mdx @@ -84,7 +84,7 @@ Activating an E3 will allow valid Data Providers to submit inputs to the computa ## Input Publication -Inputs are published directly to the Enclave contract's `publishInput()` function. +Inputs are published to Program contracts' `publishInput()` function. ```solidity function publishInput( @@ -117,15 +117,11 @@ As much as possible, you should aim to validate inputs via proofs generated by D rather than in your Secure Process. This pushed computation to the edges and allows you to reduce the complexity of your FHE computation. -Your E3 Program must include logic that validates user inputs. When publishing an input, the Enclave -contracts will call the `validateInput()` function on your Program contract. +Your E3 Program must include logic that validates user inputs. For simplicity, this can be included +inside the `publishInput` function of the E3 Program contract. -```solidity -function validateInput(address sender, bytes memory params) external returns (bytes memory input); -``` - -At a minimum, this function should validate a proof that the given ciphertext is a valid encryption -to the E3's public key. It is also recommended to bundle in proofs to validate: +At a minimum, this logic should validate a proof that the given ciphertext is a valid encryption to +the E3's public key. It is also recommended to bundle in proofs to validate: - The legitimacy of the Data Provider (e.g., ensuring they are listed in a registry of approved data providers). @@ -194,8 +190,10 @@ enclaveContract.on('PlaintextOutputPublished', (e3Id, plaintext) => { ### Submitting Inputs +You can submit inputs by calling the `publishInput` function on your E3 Program contract: + ```javascript -const tx = await enclaveContract.publishInput(e3Id, encryptedInput) +const tx = await programContract.publishInput(e3Id, encryptedInput) await tx.wait() ``` diff --git a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx index 24404fd6e8..1299e8d105 100644 --- a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx +++ b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx @@ -147,7 +147,7 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { setRoundState({ ...fetchedRoundState, start_block: startBlockNumber }) setVotingRound({ round_id: fetchedRoundState.id, pk_bytes: fetchedRoundState.committee_public_key }) setPollOptions(generatePoll({ round_id: fetchedRoundState.id, emojis: fetchedRoundState.emojis })) - setRoundEndDate(convertTimestampToDate(fetchedRoundState.start_time, fetchedRoundState.duration)) + setRoundEndDate(convertTimestampToDate(fetchedRoundState.end_time)) setCurrentRoundId(fetchedRoundState.id) } } diff --git a/examples/CRISP/client/src/model/vote.model.ts b/examples/CRISP/client/src/model/vote.model.ts index ff9dc61587..edb04a6c65 100644 --- a/examples/CRISP/client/src/model/vote.model.ts +++ b/examples/CRISP/client/src/model/vote.model.ts @@ -50,8 +50,7 @@ export interface VoteStateLite { vote_count: number start_time: number - duration: number - expiration: number + end_time: number start_block: number committee_public_key: number[] diff --git a/examples/CRISP/client/src/pages/DailyPoll/DailyPoll.tsx b/examples/CRISP/client/src/pages/DailyPoll/DailyPoll.tsx index 8555285da0..bd3db01666 100644 --- a/examples/CRISP/client/src/pages/DailyPoll/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/DailyPoll/DailyPoll.tsx @@ -11,7 +11,7 @@ import { convertTimestampToDate } from '@/utils/methods' const DailyPoll: React.FC = () => { const { roundState, isLoading } = useVoteManagementContext() - const endTime = useMemo(() => (roundState ? convertTimestampToDate(roundState.start_time, roundState.duration) : null), [roundState]) + const endTime = useMemo(() => (roundState ? convertTimestampToDate(roundState.end_time) : null), [roundState]) const loading = isLoading || !roundState diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx index aeac48d9de..a233c00750 100644 --- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx @@ -39,7 +39,7 @@ const DailyPollSection: React.FC = ({ loading, endTime, t const block = await client.getBlock() - if (block.timestamp > roundState.expiration) { + if (block.timestamp > roundState.end_time) { setIsEnded(true) } })() diff --git a/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx b/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx index c4b9d13f54..39d232948a 100644 --- a/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx +++ b/examples/CRISP/client/src/pages/RoundPoll/RoundPoll.tsx @@ -39,7 +39,7 @@ const RoundPoll: React.FC = () => { loadRound() }, [isValidRoundId, parsedRoundId, getRoundStateLite]) - const endTime = useMemo(() => (roundState ? convertTimestampToDate(roundState.start_time, roundState.duration) : null), [roundState]) + const endTime = useMemo(() => (roundState ? convertTimestampToDate(roundState.end_time) : null), [roundState]) const title = `Round #${roundId}` diff --git a/examples/CRISP/client/src/utils/methods.ts b/examples/CRISP/client/src/utils/methods.ts index 0c0c6ae31b..873c3b23ac 100644 --- a/examples/CRISP/client/src/utils/methods.ts +++ b/examples/CRISP/client/src/utils/methods.ts @@ -80,7 +80,7 @@ export const convertPollData = (request: PollRequestResult[]): PollResult[] => { } export const convertVoteStateLite = (voteState: VoteStateLite): PollResult => { - const endTime = voteState.expiration + const endTime = voteState.end_time const date = new Date(endTime * 1000).toISOString() const options: PollOption[] = [ diff --git a/examples/CRISP/crates/evm_helpers/src/lib.rs b/examples/CRISP/crates/evm_helpers/src/lib.rs index 8955ce4386..d53dacffae 100644 --- a/examples/CRISP/crates/evm_helpers/src/lib.rs +++ b/examples/CRISP/crates/evm_helpers/src/lib.rs @@ -5,18 +5,12 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use alloy::{ - network::{Ethereum, EthereumWallet}, - primitives::{Address, U256}, - providers::{ - fillers::{ + network::{Ethereum, EthereumWallet}, primitives::{Address, Bytes, U256}, providers::{ + Identity, ProviderBuilder, RootProvider, fillers::{ BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller, - }, - Identity, ProviderBuilder, RootProvider, - }, - rpc::types::TransactionReceipt, - signers::local::PrivateKeySigner, - sol, + } + }, rpc::types::TransactionReceipt, signers::local::PrivateKeySigner, sol }; use eyre::Result; use std::sync::Arc; @@ -28,6 +22,7 @@ sol! { function setMerkleRoot(uint256 e3_id, uint256 _root) external; function getSlotIndex(uint256 e3_id, address slot_address) external view returns (uint256); function isSlotEmptyByAddress(uint256 e3_id, address slot_address) external view returns (bool); + function publishInput(uint256 e3_id, bytes data) external; } } @@ -102,6 +97,23 @@ impl CRISPContract { Ok(receipt) } + + // publish an input to the CRISPProgram contract + pub async fn publish_input( + &self, + e3_id: U256, + data: Bytes, + ) -> Result { + let contract = CRISPProgram::new(self.contract_address, self.provider.as_ref()); + let receipt = contract + .publishInput(e3_id, data.into()) + .send() + .await? + .get_receipt() + .await?; + + Ok(receipt) + } } impl CRISPContract { diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index b08f24d19d..976928cd43 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,20 +3,20 @@ chains: rpc_url: ws://localhost:8545 contracts: e3_program: - address: '0x67d269191c92Caf3cD7723F116c85e6E9bf55933' - deploy_block: 1 + address: '0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB' + deploy_block: 37 enclave: address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' - deploy_block: 13 + deploy_block: 15 ciphernode_registry: address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' - deploy_block: 11 + deploy_block: 13 bonding_registry: address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' - deploy_block: 8 + deploy_block: 10 fee_token: address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' - deploy_block: 4 + deploy_block: 5 program: dev: true nodes: @@ -52,3 +52,4 @@ nodes: autopassword: true role: type: aggregator + diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol index fcc758cc91..acb5b60f45 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPProgram.sol @@ -49,13 +49,11 @@ contract CRISPProgram is IE3Program, Ownable { HonkVerifier private immutable honkVerifier; // Mappings - mapping(address => bool) public authorizedContracts; mapping(uint256 e3Id => RoundData) e3Data; // Errors error CallerNotAuthorized(); error E3AlreadyInitialized(); - error E3DoesNotExist(); error EnclaveAddressZero(); error Risc0VerifierAddressZero(); error InvalidHonkVerifier(); @@ -67,6 +65,9 @@ contract CRISPProgram is IE3Program, Ownable { error SlotIsEmpty(); error MerkleRootNotSet(); error InvalidNumOptions(); + error InputDeadlinePassed(uint256 e3Id, uint256 deadline); + error KeyNotPublished(uint256 e3Id); + error E3NotAcceptingInputs(uint256 e3Id); // Events event InputPublished(uint256 indexed e3Id, bytes encryptedVote, uint256 index); @@ -84,7 +85,6 @@ contract CRISPProgram is IE3Program, Ownable { enclave = _enclave; risc0Verifier = _risc0Verifier; honkVerifier = _honkVerifier; - authorizedContracts[address(_enclave)] = true; imageId = _imageId; } @@ -126,7 +126,7 @@ contract CRISPProgram is IE3Program, Ownable { bytes calldata, bytes calldata customParams ) external returns (bytes32) { - if (!authorizedContracts[msg.sender] && msg.sender != owner()) revert CallerNotAuthorized(); + if (msg.sender != address(enclave) && msg.sender != owner()) revert CallerNotAuthorized(); if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized(); // decode custom params to get the number of options @@ -147,9 +147,24 @@ contract CRISPProgram is IE3Program, Ownable { } /// @inheritdoc IE3Program - function validateInput(uint256 e3Id, address, bytes memory data) external { - // it should only be called via Enclave for now - if (!authorizedContracts[msg.sender] && msg.sender != owner()) revert CallerNotAuthorized(); + function publishInput(uint256 e3Id, bytes memory data) external { + E3 memory e3 = enclave.getE3(e3Id); + + // check that we are in the correct stage + IEnclave.E3Stage stage = enclave.getE3Stage(e3Id); + if (stage != IEnclave.E3Stage.KeyPublished) { + revert KeyNotPublished(e3Id); + } + + // check that we are not past the input deadline + if (block.timestamp > e3.inputWindow[1]) { + revert InputDeadlinePassed(e3Id, e3.inputWindow[1]); + } + + // check that we are within the input window + if (block.timestamp < e3.inputWindow[0]) { + revert E3NotAcceptingInputs(e3Id); + } // We need to ensure that the CRISP admin set the merkle root of the census. if (e3Data[e3Id].merkleRoot == 0) revert MerkleRootNotSet(); @@ -163,9 +178,6 @@ contract CRISPProgram is IE3Program, Ownable { (uint40 voteIndex, bytes32 previousEncryptedVoteCommitment) = _processVote(e3Id, slotAddress, encryptedVoteCommitment); - // Fetch E3 to get committee public key - E3 memory e3 = enclave.getE3(e3Id); - // Set the public inputs for the proof. Order must match Noir circuit. bytes32[] memory noirPublicInputs = new bytes32[](7); noirPublicInputs[0] = previousEncryptedVoteCommitment; @@ -244,12 +256,13 @@ contract CRISPProgram is IE3Program, Ownable { /// @inheritdoc IE3Program function verify(uint256 e3Id, bytes32 ciphertextOutputHash, bytes memory proof) external view override returns (bool) { - if (e3Data[e3Id].paramsHash == bytes32(0)) revert E3DoesNotExist(); + bytes32 paramsHash = getParamsHash(e3Id); + bytes32 inputRoot = bytes32(e3Data[e3Id].votes._root(TREE_DEPTH)); bytes memory journal = new bytes(396); // (32 + 1) * 4 * 3 _encodeLengthPrefixAndHash(journal, 0, ciphertextOutputHash); - _encodeLengthPrefixAndHash(journal, 132, e3Data[e3Id].paramsHash); + _encodeLengthPrefixAndHash(journal, 132, paramsHash); _encodeLengthPrefixAndHash(journal, 264, inputRoot); risc0Verifier.verify(proof, imageId, sha256(journal)); diff --git a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol index d483d6d8a4..a9433e3998 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/Mocks/MockEnclave.sol @@ -6,6 +6,7 @@ pragma solidity >=0.8.27; import { E3 } from "@enclave-e3/contracts/contracts/interfaces/IE3.sol"; +import { IEnclave } from "@enclave-e3/contracts/contracts/interfaces/IEnclave.sol"; import { IE3Program } from "@enclave-e3/contracts/contracts/interfaces/IE3Program.sol"; import { IDecryptionVerifier } from "@enclave-e3/contracts/contracts/interfaces/IDecryptionVerifier.sol"; @@ -22,11 +23,9 @@ contract MockEnclave { seed: 0, threshold: [uint32(1), uint32(2)], requestBlock: 0, - startWindow: [uint256(0), uint256(0)], - duration: 0, - expiration: 0, + inputWindow: [uint256(0), uint256(0)], encryptionSchemeId: bytes32(0), - e3Program: IE3Program(program), + e3Program: IE3Program(address(0)), e3ProgramParams: bytes(""), customParams: abi.encode(address(0), nextE3Id, 2, 0, 0), decryptionVerifier: IDecryptionVerifier(address(0)), @@ -49,15 +48,17 @@ contract MockEnclave { committeePublicKey = publicKeyHash; } + function getE3Stage(uint256) external view returns (IEnclave.E3Stage) { + return IEnclave.E3Stage.KeyPublished; + } + function getE3(uint256) external view returns (E3 memory) { return E3({ seed: 0, threshold: [uint32(1), uint32(2)], requestBlock: 0, - startWindow: [uint256(0), uint256(0)], - duration: 0, - expiration: 0, + inputWindow: [uint256(0), block.timestamp + 100], encryptionSchemeId: bytes32(0), e3Program: IE3Program(address(0)), e3ProgramParams: bytes(""), diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 9cce2a7d76..8e1b5f31d5 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -132,5 +132,179 @@ "address": "0x77da6521A1A22e81Df08E98b4Af41D71413EA354", "blockNumber": 10073653 } + }, + "undefined": { + "RiscZeroGroth16Verifier": { + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "blockNumber": 1 + }, + "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": 4, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 5, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 6, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 8, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 10, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "proxyRecords": { + "initData": "0x7333fa82000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707000000000000000000000000cf7ed3acca5a467e9e704c703e8d87f634fb0fc90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000093a80", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "proxyAdminAddress": "0x94099942864EA81cCF197E9D71ac53310b1468D8", + "implementationAddress": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "blockNumber": 10, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "10" + }, + "proxyRecords": { + "initData": "0x1794bb3c000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "proxyAdminAddress": "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321", + "implementationAddress": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "blockNumber": 13, + "address": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "bondingRegistry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "e3RefundManager": "0x0000000000000000000000000000000000000001", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "timeoutConfig": "{\"committeeFormationWindow\":3600,\"dkgWindow\":7200,\"computeWindow\":86400,\"decryptionWindow\":3600,\"gracePeriod\":600}", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001" + ] + }, + "proxyRecords": { + "initData": "0x8d158aa7000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d000000000000000000000000000000000000000000000000000000000000000e100000000000000000000000000000000000000000000000000000000000001c2000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e10000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "proxyAdminAddress": "0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D", + "implementationAddress": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + }, + "blockNumber": 15, + "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + }, + "E3RefundManager": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "treasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "proxyRecords": { + "initData": "0xc0c53b8b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "proxyAdminAddress": "0x8e80FFe6Dc044F4A766Afd6e5a8732Fe0977A493", + "implementationAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 17, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, + "MockComputeProvider": { + "blockNumber": 29, + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" + }, + "MockDecryptionVerifier": { + "blockNumber": 30, + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" + }, + "MockE3Program": { + "blockNumber": 31, + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" + }, + "MockRISC0Verifier": { + "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", + "blockNumber": 34 + }, + "HonkVerifier": { + "address": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", + "blockNumber": 36 + }, + "CRISPProgram": { + "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "blockNumber": 37, + "constructorArgs": { + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "verifierAddress": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", + "honkVerifierAddress": "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690", + "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" + } + }, + "MockCRISPToken": { + "address": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", + "blockNumber": 39 + } } } \ No newline at end of file diff --git a/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts b/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts index a73d5530fb..b13b4038f7 100644 --- a/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts +++ b/examples/CRISP/packages/crisp-contracts/tests/crisp.contracts.test.ts @@ -4,7 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { zeroAddress } from 'viem' import { hashLeaf, generatePublicKey, @@ -106,6 +105,8 @@ describe('CRISP Contracts', function () { const e3Id = 0n + await mockEnclave.request(await crispProgram.getAddress()) + const vote = [10n, 0n] const balance = 100n const signature = (await signer.signMessage(SIGNATURE_MESSAGE)) as `0x${string}` @@ -131,7 +132,7 @@ describe('CRISP Contracts', function () { await crispProgram.setMerkleRoot(e3Id, merkleTree.root) // If it doesn't throw, the test is successful. - await crispProgram.validateInput(e3Id, zeroAddress, encodedProof) + await crispProgram.publishInput(e3Id, encodedProof) }) }) }) diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 04fc28623a..572f71fa70 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -15,14 +15,14 @@ CRON_API_KEY=1234567890 # Based on Default Hardhat Deployments (Only for testing) ENCLAVE_ADDRESS="0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" CIPHERNODE_REGISTRY_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" -E3_PROGRAM_ADDRESS="0x67d269191c92Caf3cD7723F116c85e6E9bf55933" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config -# Defines the time window during which an e3 can be activated -E3_WINDOW_SIZE=30 # Defines the time interval during which users can submit their inputs -# After this interval, the computation phase starts automatically +# After this interval, the computation phase starts automatically +# After activation + this interval, ciphernodes are then not responsing to +# any more decryption requests E3_DURATION=70 E3_THRESHOLD_MIN=2 E3_THRESHOLD_MAX=5 diff --git a/examples/CRISP/server/src/cli/commands.rs b/examples/CRISP/server/src/cli/commands.rs index 1211c2a26c..d01758d542 100644 --- a/examples/CRISP/server/src/cli/commands.rs +++ b/examples/CRISP/server/src/cli/commands.rs @@ -6,13 +6,15 @@ use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input}; use e3_fhe_params::default_param_set; +use e3_sdk::evm_helpers::contracts::E3Stage; +use evm_helpers::CRISPContract; use log::info; use reqwest::Client; use serde::{Deserialize, Serialize}; use super::approve; use super::CLI_DB; -use alloy::primitives::{Address, Bytes, FixedBytes, U256}; +use alloy::primitives::{Address, Bytes, U256}; use alloy::providers::{Provider, ProviderBuilder}; use alloy::sol_types::SolValue; use crisp::config::CONFIG; @@ -63,6 +65,16 @@ pub async fn get_current_timestamp() -> Result Result> { + let contract = + EnclaveContract::read_only(&CONFIG.http_rpc_url, &CONFIG.enclave_address).await?; + let e3_stage: E3Stage = contract.get_e3_stage(U256::from(e3_id)).await?; + + Ok(e3_stage == E3Stage::KeyPublished) +} + pub async fn initialize_crisp_round( token_address: &str, balance_threshold: &str, @@ -120,11 +132,11 @@ pub async fn initialize_crisp_round( let threshold: [u32; 2] = [CONFIG.e3_threshold_min, CONFIG.e3_threshold_max]; 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 input_window: [U256; 2] = [ + // give a little buffer + U256::from(current_timestamp) + U256::from(20), + U256::from(current_timestamp + CONFIG.e3_duration), ]; - let duration: U256 = U256::from(CONFIG.e3_duration); let e3_params = Bytes::from(encode_bfv_params(&generate_bfv_parameters())); let compute_provider_params = ComputeProviderParams { name: CONFIG.e3_compute_provider_name.to_string(), @@ -133,7 +145,6 @@ 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 @@ -142,8 +153,7 @@ pub async fn initialize_crisp_round( let fee_amount = contract .get_e3_quote( threshold, - start_window, - duration, + input_window, e3_program, e3_params.clone(), compute_provider_params_bytes.clone(), @@ -162,17 +172,12 @@ pub async fn initialize_crisp_round( .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 - input_window: {:?}", input_window); info!("Debug - current timestamp: {:?}", current_timestamp); - info!("Debug - duration: {}", duration); info!("Debug - e3_program: {}", e3_program); info!( @@ -183,8 +188,7 @@ pub async fn initialize_crisp_round( let (res, e3_id) = contract .request_e3( threshold, - start_window, - duration, + input_window, e3_program, e3_params, compute_provider_params_bytes, @@ -198,48 +202,6 @@ pub async fn initialize_crisp_round( Ok(e3_id_u64) } -pub async fn check_e3_activated( - e3_id: u64, -) -> Result> { - let contract = - EnclaveContract::read_only(&CONFIG.http_rpc_url, &CONFIG.enclave_address).await?; - let e3: E3 = contract.get_e3(U256::from(e3_id)).await?; - Ok(u64::try_from(e3.expiration)? > 0) -} - -pub async fn activate_e3_round() -> Result<(), Box> { - let input_e3_id: u64 = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter CRISP round ID.") - .interact_text()?; - - let params = generate_bfv_parameters(); - let (sk, pk) = generate_keys(¶ms); - let contract = EnclaveContract::new( - &CONFIG.http_rpc_url, - &CONFIG.private_key, - &CONFIG.enclave_address, - ) - .await?; - let e3_id = U256::from(input_e3_id); - - let res = contract.activate(e3_id).await?; - info!("E3 activated. TxHash: {:?}", res.transaction_hash); - - let e3_params = FHEParams { - params: encode_bfv_params(¶ms), - pk: pk.to_bytes(), - sk: sk.coeffs.into_vec(), - }; - - let db = CLI_DB.write().await; - let key = format!("_e3:{}", input_e3_id); - db.insert(key, serde_json::to_vec(&e3_params)?)?; - db.flush()?; - info!("E3 parameters stored in database."); - - Ok(()) -} - pub async fn participate_in_existing_round( client: &Client, ) -> Result<(), Box> { @@ -264,7 +226,7 @@ pub async fn participate_in_existing_round( let vote_choice = get_user_vote()?; if let Some(vote) = vote_choice { let ct = encrypt_vote(vote, &pk_deserialized, ¶ms)?; - let contract = EnclaveContract::new( + let contract = CRISPContract::new( &CONFIG.http_rpc_url, &CONFIG.private_key, &CONFIG.enclave_address, diff --git a/examples/CRISP/server/src/cli/main.rs b/examples/CRISP/server/src/cli/main.rs index 2f68e62293..01ce0958b1 100644 --- a/examples/CRISP/server/src/cli/main.rs +++ b/examples/CRISP/server/src/cli/main.rs @@ -10,7 +10,7 @@ mod commands; use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input}; use reqwest::Client; -use commands::{check_e3_activated, initialize_crisp_round}; +use commands::initialize_crisp_round; use crisp::logger::init_logger; use log::info; @@ -20,6 +20,8 @@ use sled::Db; use std::sync::Arc; use tokio::sync::RwLock; +use crate::commands::check_committee_key_published; + pub static CLI_DB: Lazy>> = Lazy::new(|| { let pathdb = std::env::current_dir().unwrap().join("database/cli"); Arc::new(RwLock::new(sled::open(pathdb).unwrap())) @@ -49,10 +51,10 @@ enum Commands { #[arg(short, long, default_value = "1000000000000000000")] balance_threshold: String, }, - CheckActivate { + CheckE3Ready { #[arg(short, long)] e3id: u64, - }, + } } #[tokio::main] @@ -75,9 +77,9 @@ pub async fn main() -> Result<(), Box> { let e3_id = initialize_crisp_round(&token_address, &balance_threshold).await?; println!("{}", e3_id); } - Some(Commands::CheckActivate { e3id }) => { - let is_activated = check_e3_activated(e3id).await?; - println!("{}", is_activated); + Some(Commands::CheckE3Ready { e3id }) => { + let is_ready = check_committee_key_published(e3id).await?; + println!("{}", is_ready); } None => { // Fall back to interactive mode if no command was specified diff --git a/examples/CRISP/server/src/config.rs b/examples/CRISP/server/src/config.rs index 446cba6d51..40d76f4767 100644 --- a/examples/CRISP/server/src/config.rs +++ b/examples/CRISP/server/src/config.rs @@ -25,7 +25,6 @@ pub struct Config { // E3 parameters pub e3_threshold_min: u32, pub e3_threshold_max: u32, - pub e3_window_size: u64, pub e3_duration: u64, pub e3_compute_provider_name: String, pub e3_compute_provider_parallel: bool, diff --git a/examples/CRISP/server/src/server/indexer.rs b/examples/CRISP/server/src/server/indexer.rs index bf1aed90f7..1691605729 100644 --- a/examples/CRISP/server/src/server/indexer.rs +++ b/examples/CRISP/server/src/server/indexer.rs @@ -16,12 +16,11 @@ use alloy::providers::{Provider, ProviderBuilder}; use alloy::sol_types::{sol_data, SolType}; use alloy_primitives::{Address, U256}; use crisp_utils::decode_tally; -use e3_sdk::indexer::IndexerContext; use e3_sdk::{ evm_helpers::{ - contracts::{EnclaveRead, EnclaveWrite, ReadWrite}, + contracts::{EnclaveRead, ReadWrite}, events::{ - CiphertextOutputPublished, CommitteePublished, E3Activated, E3Requested, + CiphertextOutputPublished, CommitteePublished, E3Requested, PlaintextOutputPublished, }, retry::call_with_retry, @@ -33,7 +32,6 @@ use eyre::Context; use log::info; use num_bigint::BigUint; use std::error::Error; -use std::sync::Arc; type Result = std::result::Result>; @@ -66,7 +64,7 @@ pub async fn register_e3_requested( .await .map_err(|e| eyre::eyre!("{}", e))?; - // Convert custom params bytes back to token address and balance threshold. + let input_window = [e3.inputWindow[0].to::(), e3.inputWindow[1].to::()]; // Use sol_data types instead of primitives type CustomParamsTuple = (sol_data::Address, sol_data::Uint<256>, sol_data::Uint<256>, sol_data::Uint<256>, sol_data::Uint<256>); @@ -104,6 +102,8 @@ pub async fn register_e3_requested( .parse() .with_context(|| "Invalid token address")?; + let input_window = [e3.inputWindow[0].to::(), e3.inputWindow[1].to::()]; + // Get token holders from Etherscan API or mocked data. let token_holders = if matches!(CONFIG.chain_id, 31337 | 1337) { info!( @@ -170,7 +170,7 @@ pub async fn register_e3_requested( } // save the e3 details - repo.initialize_round(custom_params, e3.requester.to_string()) + repo.initialize_round(custom_params, e3.requester.to_string(), input_window[1]) .await?; // Store eligible addresses in the repository. @@ -234,37 +234,6 @@ pub async fn register_e3_requested( Ok(indexer) } -pub async fn register_e3_activated( - indexer: EnclaveIndexer, -) -> Result> { - // E3Activated - indexer - .add_event_handler(move |event: E3Activated, ctx| { - let store = ctx.store(); - let e3_id = event.e3Id.to::(); - let mut repo = CrispE3Repository::new(store.clone(), e3_id); - let mut current_round_repo = CurrentRoundRepository::new(store); - let expiration = event.expiration.to::(); - - info!("[e3_id={}] Handling E3 request", e3_id); - async move { - repo.start_round().await?; - - current_round_repo - .set_current_round(CurrentRound { id: e3_id }) - .await?; - - info!("[e3_id={}] Registering hook for {}", e3_id, expiration); - ctx.do_later(expiration, move |_, ctx| { - handle_e3_input_deadline_expiration(e3_id, ctx.store()) - }); - Ok(()) - } - }) - .await; - Ok(indexer) -} - async fn handle_e3_input_deadline_expiration( e3_id: u64, store: SharedStore, @@ -374,40 +343,26 @@ pub async fn register_committee_published( indexer .add_event_handler(move |event: CommitteePublished, ctx| { async move { - let contract = ctx.contract(); - // We need to do this to ensure this is idempotent. - // TODO: conserve bandwidth and check for E3AlreadyActivated error instead of - // making two calls to contract - // 0xcd6f4a4f = E3DoesNotExist() - let e3 = call_with_retry("get_e3", &["0xcd6f4a4f"], || { - let contract = contract.clone(); - let event_e3_id = event.e3Id; - async move { - contract - .get_e3(event_e3_id) - .await - .map_err(|e| anyhow::anyhow!("{}", e)) - } - }) - .await - .map_err(|e| eyre::eyre!("{}", e))?; - if u64::try_from(e3.expiration)? > 0 { - info!("[e3_id={}] E3 already activated", event.e3Id); - return Ok(()); - } - - // Read Start time in Seconds - let start_time = e3.startWindow[0].to::(); - info!("[e3_id={}] Start time: {}", event.e3Id, start_time); - + let store = ctx.store(); + let e3_id = event.e3Id.to::(); + let mut repo = CrispE3Repository::new(store.clone(), e3_id); + let mut current_round_repo = CurrentRoundRepository::new(store); + info!("[e3_id={}] Handling CommitteePublished", e3_id); // Get current time let now = get_current_timestamp_rpc().await?; info!("[e3_id={}] Current time: {}", event.e3Id, now); - let later_event = event.clone(); - ctx.do_later(start_time, move |_, ctx| { - let event = later_event.clone(); - handle_committee_time_expired(event, ctx) + repo.start_round().await?; + + current_round_repo + .set_current_round(CurrentRound { id: e3_id }) + .await?; + + let expiration = repo.get_input_deadline().await?; + + info!("[e3_id={}] Registering hook for {}", e3_id, expiration); + ctx.do_later(expiration, move |_, ctx| { + handle_e3_input_deadline_expiration(e3_id, ctx.store()) }); Ok(()) @@ -417,33 +372,6 @@ pub async fn register_committee_published( Ok(indexer) } -async fn handle_committee_time_expired( - event: CommitteePublished, - ctx: Arc>, -) -> eyre::Result<()> { - // If not activated activate - let tx = call_with_retry("activate", &["0x45ccf3c6"], || { - let value = ctx.clone(); - async move { - info!("[e3_id={}] Calling Enclave.Activate", event.e3Id); - let receipt = value - .contract() - .activate(event.e3Id) - .await - .map_err(|e| anyhow::anyhow!("{:?}", e))?; - anyhow::Ok(receipt) - } - }) - .await - .map_err(|e| eyre::eyre!("{:?}", e))?; - - info!( - "[e3_id={}] E3 activated with tx: {:?}", - event.e3Id, tx.transaction_hash - ); - Ok(()) -} - pub async fn get_current_timestamp_rpc() -> eyre::Result { let provider = ProviderBuilder::new().connect(&CONFIG.http_rpc_url).await?; let block = provider @@ -498,7 +426,6 @@ pub async fn start_indexer( info!("CRISP: Indexer registering handlers..."); let crisp_indexer = register_e3_requested(crisp_indexer).await?; - let crisp_indexer = register_e3_activated(crisp_indexer).await?; let crisp_indexer = register_ciphertext_output_published(crisp_indexer).await?; let crisp_indexer = register_plaintext_output_published(crisp_indexer).await?; let crisp_indexer = register_committee_published(crisp_indexer).await?; diff --git a/examples/CRISP/server/src/server/models.rs b/examples/CRISP/server/src/server/models.rs index fd1034c150..26cc897420 100644 --- a/examples/CRISP/server/src/server/models.rs +++ b/examples/CRISP/server/src/server/models.rs @@ -177,8 +177,7 @@ pub struct E3StateLite { pub vote_count: u64, pub start_time: u64, - pub duration: u64, - pub expiration: u64, + pub end_time: u64, pub start_block: u64, pub committee_public_key: Vec, @@ -210,8 +209,7 @@ pub struct E3 { // Timing-related pub start_time: u64, pub block_start: u64, - pub duration: u64, - pub expiration: u64, + pub end_time: u64, // Parameters pub e3_params: Vec, @@ -236,6 +234,7 @@ pub struct E3Crisp { pub emojis: [String; 2], pub has_voted: Vec, pub start_time: u64, + pub end_time: u64, pub status: String, pub tally: Vec, pub token_holder_hashes: Vec, @@ -257,7 +256,7 @@ impl From for WebResultRequest { option_1_emoji: e3.emojis[0].clone(), option_2_emoji: e3.emojis[1].clone(), total_votes: e3.vote_count, - end_time: e3.expiration, + end_time: e3.end_time, requester: e3.requester, } } diff --git a/examples/CRISP/server/src/server/repo.rs b/examples/CRISP/server/src/server/repo.rs index d4e650b458..df1d031a66 100644 --- a/examples/CRISP/server/src/server/repo.rs +++ b/examples/CRISP/server/src/server/repo.rs @@ -169,6 +169,7 @@ impl CrispE3Repository { &mut self, custom_params: CustomParams, requester: String, + end_time: u64, ) -> Result<()> { self.set_crisp(E3Crisp { has_voted: vec![], @@ -185,6 +186,7 @@ impl CrispE3Repository { num_options: custom_params.num_options, credit_mode: custom_params.credit_mode, credits: custom_params.credits, + end_time, }) .await } @@ -263,7 +265,7 @@ impl CrispE3Repository { tally: e3_crisp.tally, option_1_emoji: e3_crisp.emojis[0].clone(), option_2_emoji: e3_crisp.emojis[1].clone(), - end_time: e3.expiration, + end_time: e3.input_window[1], total_votes: self.get_vote_count().await?, requester: e3_crisp.requester, }) @@ -274,13 +276,12 @@ impl CrispE3Repository { let e3_crisp = self.get_crisp().await?; Ok(E3StateLite { emojis: e3_crisp.emojis, - expiration: e3.expiration, id: self.e3_id, status: e3_crisp.status, chain_id: e3.chain_id, - duration: e3.duration, + start_time: e3.input_window[0], + end_time: e3.input_window[1], vote_count: u64::try_from(e3_crisp.has_voted.len())?, - start_time: e3_crisp.start_time, start_block: e3.request_block, enclave_address: e3.enclave_address, committee_public_key: e3.committee_public_key, @@ -293,6 +294,12 @@ impl CrispE3Repository { }) } + /// Get the input deadline for the current round + pub async fn get_input_deadline(&self) -> Result { + let e3_crisp = self.get_crisp().await?; + Ok(e3_crisp.end_time) + } + pub async fn get_ciphertext_inputs(&self) -> Result, u64)>> { let e3_crisp = self.get_crisp().await?; Ok(e3_crisp.ciphertext_inputs) diff --git a/examples/CRISP/server/src/server/routes/rounds.rs b/examples/CRISP/server/src/server/routes/rounds.rs index 91ed6f13b5..e26dc1f6d7 100644 --- a/examples/CRISP/server/src/server/routes/rounds.rs +++ b/examples/CRISP/server/src/server/routes/rounds.rs @@ -209,11 +209,8 @@ pub async fn initialize_crisp_round( info!("Requesting E3..."); 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 duration: U256 = U256::from(CONFIG.e3_duration); + + let input_window = [U256::from(Utc::now().timestamp()), U256::from(Utc::now().timestamp()) + U256::from(CONFIG.e3_duration)]; let e3_params = Bytes::from(params); let compute_provider_params = ComputeProviderParams { name: CONFIG.e3_compute_provider_name.clone(), @@ -224,8 +221,7 @@ pub async fn initialize_crisp_round( let (receipt, e3_id) = contract .request_e3( threshold, - start_window, - duration, + input_window, e3_program, e3_params, compute_provider_params, diff --git a/examples/CRISP/server/src/server/routes/voting.rs b/examples/CRISP/server/src/server/routes/voting.rs index a46cd6b378..4321bbb01b 100644 --- a/examples/CRISP/server/src/server/routes/voting.rs +++ b/examples/CRISP/server/src/server/routes/voting.rs @@ -15,7 +15,7 @@ use crate::server::{ }; use actix_web::{web, HttpResponse, Responder}; use alloy::primitives::{Bytes, U256}; -use e3_sdk::evm_helpers::contracts::{EnclaveContract, EnclaveWrite}; +use evm_helpers::CRISPContract; use eyre::Error; use log::{error, info}; @@ -156,10 +156,10 @@ async fn broadcast_encrypted_vote( }; // Broadcast vote to blockchain - let contract = match EnclaveContract::new( + let contract = match CRISPContract::new( &CONFIG.http_rpc_url, &CONFIG.private_key, - &CONFIG.enclave_address, + &CONFIG.e3_program_address, ) .await { diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 2aef522011..f5e288b520 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -30,9 +30,9 @@ async function runCliInit(): Promise { } } -async function checkE3Activated(e3id: number): Promise { +async function checkE3Ready(e3id: number): Promise { try { - const output = execSync(`pnpm cli check-activate --e3id ${e3id}`, { + const output = execSync(`pnpm cli check-e3-ready --e3id ${e3id}`, { encoding: 'utf-8', }) const lines = output.trim().split('\n') @@ -44,17 +44,17 @@ async function checkE3Activated(e3id: number): Promise { } } -async function waitForE3Activation(e3id: number, maxWaitMs: number = 30000): Promise { +async function waitForE3Ready(e3id: number, maxWaitMs: number = 30000): Promise { const startTime = Date.now() while (Date.now() - startTime < maxWaitMs) { - const isActivated = await checkE3Activated(e3id) + const isActivated = await checkE3Ready(e3id) if (isActivated) { - console.log(`E3 ${e3id} is activated`) + console.log(`E3 ${e3id} is ready`) return } await new Promise((resolve) => setTimeout(resolve, 2000)) } - throw new Error(`E3 ${e3id} was not activated within ${maxWaitMs}ms`) + throw new Error(`E3 ${e3id} was not ready within ${maxWaitMs}ms`) } const test = testWithSynpress(metaMaskFixtures(basicSetup)) @@ -99,8 +99,8 @@ test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => log(`clicking try demo...`) await page.locator('button:has-text("Try Demo")').click() - log(`waiting for E3 activation...`) - await waitForE3Activation(e3id) + log(`waiting for E3 Committee being published...`) + await waitForE3Ready(e3id) log(`forcing page reload...`) await page.reload() diff --git a/package.json b/package.json index ce6f526a6e..0e02836f9b 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "committee:new": "cd packages/enclave-contracts && pnpm committee:new", "committee:publish": "cd packages/enclave-contracts && pnpm hardhat committee:publish", "e3:activate": "cd packages/enclave-contracts && pnpm e3:activate", - "e3:publishInput": "cd packages/enclave-contracts && pnpm hardhat e3:publishInput", + "e3-program:publishInput": "cd packages/enclave-contracts && pnpm hardhat e3-program:publishInput", "e3:publishCiphertext": "cd packages/enclave-contracts && pnpm hardhat e3:publishCiphertext", "evm:install": "cd packages/enclave-contracts && pnpm install", "evm:node": "cd packages/enclave-contracts && pnpm hardhat node", diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index ef641a20af..d2234a92a8 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -530,6 +530,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "numActiveOperators", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -877,5 +890,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-e1018ade42270e3293a7c3b47ab6b91dfdeea5fe" + "buildInfoId": "solc-0_8_28-1274269bf8b5435b6fb6eba99e4eb3854e5d9864" } \ 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 2ce871f7a3..08a2088fe2 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -103,6 +103,31 @@ "name": "CommitteeFinalized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "nodesSubmitted", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thresholdRequired", + "type": "uint256" + } + ], + "name": "CommitteeFormationFailed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -158,7 +183,7 @@ { "indexed": false, "internalType": "uint256", - "name": "submissionDeadline", + "name": "committeeDeadline", "type": "uint256" } ], @@ -263,7 +288,13 @@ } ], "name": "finalizeCommittee", - "outputs": [], + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -280,6 +311,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getCommitteeDeadline", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -466,7 +516,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IBondingRegistry", "name": "_bondingRegistry", "type": "address" } @@ -479,7 +529,7 @@ { "inputs": [ { - "internalType": "address", + "internalType": "contract IEnclave", "name": "_enclave", "type": "address" } @@ -540,5 +590,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-e1018ade42270e3293a7c3b47ab6b91dfdeea5fe" + "buildInfoId": "solc-0_8_28-1274269bf8b5435b6fb6eba99e4eb3854e5d9864" } \ 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 88eceab0ae..a9876f1f92 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -64,8 +64,59 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "CommitteeFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "CommitteeFormed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, { "indexed": false, + "internalType": "enum IEnclave.E3Stage", + "name": "failedAtStage", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "enum IEnclave.FailureReason", + "name": "reason", + "type": "uint8" + } + ], + "name": "E3Failed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, "internalType": "uint256", "name": "e3Id", "type": "uint256" @@ -73,17 +124,17 @@ { "indexed": false, "internalType": "uint256", - "name": "expiration", + "name": "paymentAmount", "type": "uint256" }, { "indexed": false, - "internalType": "bytes32", - "name": "committeePublicKey", - "type": "bytes32" + "internalType": "uint256", + "name": "honestNodeCount", + "type": "uint256" } ], - "name": "E3Activated", + "name": "E3FailureProcessed", "type": "event" }, { @@ -112,6 +163,19 @@ "name": "E3ProgramEnabled", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "e3RefundManager", + "type": "address" + } + ], + "name": "E3RefundManagerSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -140,19 +204,9 @@ }, { "internalType": "uint256[2]", - "name": "startWindow", + "name": "inputWindow", "type": "uint256[2]" }, - { - "internalType": "uint256", - "name": "duration", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "expiration", - "type": "uint256" - }, { "internalType": "bytes32", "name": "encryptionSchemeId", @@ -214,6 +268,31 @@ "name": "E3Requested", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum IEnclave.E3Stage", + "name": "previousStage", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "enum IEnclave.E3Stage", + "name": "newStage", + "type": "uint8" + } + ], + "name": "E3StageChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -341,6 +420,54 @@ "name": "RewardsDistributed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "dkgWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "computeWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decryptionWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct IEnclave.E3TimeoutConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "TimeoutConfigUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "bondingRegistry", + "outputs": [ + { + "internalType": "contract IBondingRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -349,15 +476,20 @@ "type": "uint256" } ], - "name": "activate", + "name": "checkFailureCondition", "outputs": [ { "internalType": "bool", - "name": "success", + "name": "canFail", "type": "bool" + }, + { + "internalType": "enum IEnclave.FailureReason", + "name": "reason", + "type": "uint8" } ], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -369,13 +501,7 @@ } ], "name": "disableE3Program", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -388,13 +514,7 @@ } ], "name": "disableEncryptionScheme", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -407,13 +527,7 @@ } ], "name": "enableE3Program", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -430,6 +544,42 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getDeadlines", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "dkgDeadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "computeDeadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decryptionDeadline", + "type": "uint256" + } + ], + "internalType": "struct IEnclave.E3Deadlines", + "name": "deadlines", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -478,19 +628,9 @@ }, { "internalType": "uint256[2]", - "name": "startWindow", + "name": "inputWindow", "type": "uint256[2]" }, - { - "internalType": "uint256", - "name": "duration", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "expiration", - "type": "uint256" - }, { "internalType": "bytes32", "name": "encryptionSchemeId", @@ -556,14 +696,9 @@ }, { "internalType": "uint256[2]", - "name": "startWindow", + "name": "inputWindow", "type": "uint256[2]" }, - { - "internalType": "uint256", - "name": "duration", - "type": "uint256" - }, { "internalType": "contract IE3Program", "name": "e3Program", @@ -607,26 +742,152 @@ "internalType": "uint256", "name": "e3Id", "type": "uint256" - }, + } + ], + "name": "getE3Stage", + "outputs": [ { - "internalType": "bytes", - "name": "ciphertextOutput", - "type": "bytes" - }, + "internalType": "enum IEnclave.E3Stage", + "name": "stage", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { - "internalType": "bytes", - "name": "proof", - "type": "bytes" + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" } ], - "name": "publishCiphertextOutput", + "name": "getFailureReason", "outputs": [ { - "internalType": "bool", - "name": "success", - "type": "bool" + "internalType": "enum IEnclave.FailureReason", + "name": "reason", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" } ], + "name": "getRequester", + "outputs": [ + { + "internalType": "address", + "name": "requester", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTimeoutConfig", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "dkgWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "computeWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decryptionWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + } + ], + "internalType": "struct IEnclave.E3TimeoutConfig", + "name": "config", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "markE3Failed", + "outputs": [ + { + "internalType": "enum IEnclave.FailureReason", + "name": "reason", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "onCommitteeFinalized", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "onCommitteePublished", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "reason", + "type": "uint8" + } + ], + "name": "onE3Failed", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -639,11 +900,16 @@ }, { "internalType": "bytes", - "name": "data", + "name": "ciphertextOutput", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "proof", "type": "bytes" } ], - "name": "publishInput", + "name": "publishCiphertextOutput", "outputs": [ { "internalType": "bool", @@ -694,14 +960,9 @@ }, { "internalType": "uint256[2]", - "name": "startWindow", + "name": "inputWindow", "type": "uint256[2]" }, - { - "internalType": "uint256", - "name": "duration", - "type": "uint256" - }, { "internalType": "contract IE3Program", "name": "e3Program", @@ -754,19 +1015,9 @@ }, { "internalType": "uint256[2]", - "name": "startWindow", + "name": "inputWindow", "type": "uint256[2]" }, - { - "internalType": "uint256", - "name": "duration", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "expiration", - "type": "uint256" - }, { "internalType": "bytes32", "name": "encryptionSchemeId", @@ -830,13 +1081,7 @@ } ], "name": "setBondingRegistry", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -849,13 +1094,7 @@ } ], "name": "setCiphernodeRegistry", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -873,13 +1112,7 @@ } ], "name": "setDecryptionVerifier", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -892,13 +1125,7 @@ } ], "name": "setE3ProgramsParams", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -911,13 +1138,7 @@ } ], "name": "setFeeToken", - "outputs": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - } - ], + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -930,13 +1151,42 @@ } ], "name": "setMaxDuration", - "outputs": [ + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { - "internalType": "bool", - "name": "success", - "type": "bool" + "components": [ + { + "internalType": "uint256", + "name": "dkgWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "computeWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "decryptionWindow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + } + ], + "internalType": "struct IEnclave.E3TimeoutConfig", + "name": "config", + "type": "tuple" } ], + "name": "setTimeoutConfig", + "outputs": [], "stateMutability": "nonpayable", "type": "function" } @@ -947,5 +1197,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-b28d74bd7e343f56ce9ffdaec2e49273276b6ab9" + "buildInfoId": "solc-0_8_28-1274269bf8b5435b6fb6eba99e4eb3854e5d9864" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json index 32e4ef8771..1cfee6a53d 100644 --- a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +++ b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json @@ -1148,7 +1148,7 @@ "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": { - "1944": [ + "3415": [ { "length": 32, "start": 872 @@ -1174,43 +1174,43 @@ "start": 3711 } ], - "4931": [ + "6684": [ { "length": 32, "start": 3942 } ], - "4933": [ + "6686": [ { "length": 32, "start": 3900 } ], - "4935": [ + "6688": [ { "length": 32, "start": 3858 } ], - "4937": [ + "6690": [ { "length": 32, "start": 4023 } ], - "4939": [ + "6692": [ { "length": 32, "start": 4063 } ], - "4942": [ + "6695": [ { "length": 32, "start": 4727 } ], - "4945": [ + "6698": [ { "length": 32, "start": 4772 @@ -1218,5 +1218,5 @@ ] }, "inputSourceName": "project/contracts/token/EnclaveTicketToken.sol", - "buildInfoId": "solc-0_8_28-bef1dec98cb6cc37e15bd06abee5a353862d85f4" + "buildInfoId": "solc-0_8_28-15112a5a277d7846347c8e3a91ba0ec7bb3521bd" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol new file mode 100644 index 0000000000..5e9001b403 --- /dev/null +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -0,0 +1,365 @@ +// 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 { + SafeERC20 +} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IE3RefundManager } from "./interfaces/IE3RefundManager.sol"; +import { IEnclave } from "./interfaces/IEnclave.sol"; +import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; + +/** + * @title E3RefundManager + * @notice Manages refund distribution for failed E3 computations + * @dev Implements fault-attribution based refund system + * + */ +contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { + using SafeERC20 for IERC20; + //////////////////////////////////////////////////////////// + // // + // Storage Variables // + // // + //////////////////////////////////////////////////////////// + /// @notice The Enclave contract (contains lifecycle functionality) + IEnclave public enclave; + /// @notice The fee token used for payments + IERC20 public feeToken; + /// @notice The bonding registry for node rewards + IBondingRegistry public bondingRegistry; + /// @notice Protocol treasury for protocol fee collection + address public treasury; + /// @notice Work value allocation configuration + WorkValueAllocation internal _workAllocation; + /// @notice Maps E3 ID to refund distribution + mapping(uint256 e3Id => RefundDistribution) internal _distributions; + /// @notice Tracks claims per E3 per address + mapping(uint256 e3Id => mapping(address => bool)) internal _claimed; + /// @notice Maps E3 ID to honest node addresses + mapping(uint256 e3Id => address[]) internal _honestNodes; + //////////////////////////////////////////////////////////// + // // + // Modifiers // + // // + //////////////////////////////////////////////////////////// + /// @notice Restricts function to Enclave contract only + modifier onlyEnclave() { + if (msg.sender != address(enclave)) revert Unauthorized(); + _; + } + + //////////////////////////////////////////////////////////// + // // + // Initialization // + // // + //////////////////////////////////////////////////////////// + /// @notice Constructor that disables initializers + constructor() { + _disableInitializers(); + } + + /// @notice Initializes the E3RefundManager contract + /// @param _owner The owner address + /// @param _enclave The Enclave contract address + /// @param _treasury The protocol treasury address + function initialize( + address _owner, + address _enclave, + address _treasury + ) public initializer { + __Ownable_init(msg.sender); + + require(_enclave != address(0), "Invalid enclave"); + require(_treasury != address(0), "Invalid treasury"); + + enclave = IEnclave(_enclave); + feeToken = enclave.feeToken(); + bondingRegistry = enclave.bondingRegistry(); + treasury = _treasury; + + _workAllocation = WorkValueAllocation({ + committeeFormationBps: 1000, + dkgBps: 3000, + decryptionBps: 5500, + protocolBps: 500 + }); + + if (_owner != owner()) transferOwnership(_owner); + } + + //////////////////////////////////////////////////////////// + // // + // Refund Calculation // + // // + //////////////////////////////////////////////////////////// + /// @inheritdoc IE3RefundManager + function calculateRefund( + uint256 e3Id, + uint256 originalPayment, + address[] calldata honestNodes + ) external onlyEnclave { + require(!_distributions[e3Id].calculated, "Already calculated"); + require(originalPayment > 0, "No payment"); + + // Calculate work value based on stage + IEnclave.E3Stage failedAt = _getFailedAtStage(e3Id); + (uint16 workCompletedBps, uint16 workRemainingBps) = calculateWorkValue( + failedAt + ); + + // Calculate base distribution + uint256 honestNodeAmount = (originalPayment * workCompletedBps) / 10000; + uint256 requesterAmount = (originalPayment * workRemainingBps) / 10000; + uint256 protocolAmount = originalPayment - + honestNodeAmount - + requesterAmount; + + // Store distribution + _distributions[e3Id] = RefundDistribution({ + requesterAmount: requesterAmount, + honestNodeAmount: honestNodeAmount, + protocolAmount: protocolAmount, + totalSlashed: 0, + honestNodeCount: honestNodes.length, + calculated: true + }); + + // Store honest nodes + for (uint256 i = 0; i < honestNodes.length; i++) { + _honestNodes[e3Id].push(honestNodes[i]); + } + + // Transfer protocol fee to treasury immediately + if (protocolAmount > 0) { + feeToken.safeTransfer(treasury, protocolAmount); + } + + emit RefundDistributionCalculated( + e3Id, + requesterAmount, + honestNodeAmount, + protocolAmount, + 0 + ); + } + + /// @notice Get the stage at which E3 failed (for work calculation) + function _getFailedAtStage( + uint256 e3Id + ) internal view returns (IEnclave.E3Stage) { + IEnclave.FailureReason reason = enclave.getFailureReason(e3Id); + + // Map failure reason to stage + if ( + reason == IEnclave.FailureReason.CommitteeFormationTimeout || + reason == IEnclave.FailureReason.InsufficientCommitteeMembers + ) { + return IEnclave.E3Stage.Requested; + } + if ( + reason == IEnclave.FailureReason.DKGTimeout || + reason == IEnclave.FailureReason.DKGInvalidShares + ) { + return IEnclave.E3Stage.CommitteeFinalized; + } + if (reason == IEnclave.FailureReason.NoInputsReceived) { + return IEnclave.E3Stage.KeyPublished; + } + if ( + reason == IEnclave.FailureReason.ComputeTimeout || + reason == IEnclave.FailureReason.ComputeProviderExpired || + reason == IEnclave.FailureReason.ComputeProviderFailed || + reason == IEnclave.FailureReason.RequesterCancelled + ) { + return IEnclave.E3Stage.KeyPublished; + } + if ( + reason == IEnclave.FailureReason.DecryptionTimeout || + reason == IEnclave.FailureReason.DecryptionInvalidShares || + reason == IEnclave.FailureReason.VerificationFailed + ) { + return IEnclave.E3Stage.CiphertextReady; + } + + return IEnclave.E3Stage.None; + } + + /// @inheritdoc IE3RefundManager + function calculateWorkValue( + IEnclave.E3Stage stage + ) public view returns (uint16 workCompletedBps, uint16 workRemainingBps) { + WorkValueAllocation memory alloc = _workAllocation; + + if ( + stage == IEnclave.E3Stage.Requested || + stage == IEnclave.E3Stage.None + ) { + // Failed at Requested = no work done + workCompletedBps = 0; + } else if (stage == IEnclave.E3Stage.CommitteeFinalized) { + // Failed during DKG = sortition work done + workCompletedBps = alloc.committeeFormationBps; + } else if (stage == IEnclave.E3Stage.KeyPublished) { + // Failed during input phase = sortition + DKG done (no additional work) + workCompletedBps = alloc.committeeFormationBps + alloc.dkgBps; + } else if (stage == IEnclave.E3Stage.CiphertextReady) { + // Failed during decryption = sortition + DKG done (awaiting decryption shares) + workCompletedBps = alloc.committeeFormationBps + alloc.dkgBps; + } + + workRemainingBps = 10000 - workCompletedBps - alloc.protocolBps; + } + + //////////////////////////////////////////////////////////// + // // + // Claiming Functions // + // // + //////////////////////////////////////////////////////////// + /// @inheritdoc IE3RefundManager + function claimRequesterRefund( + uint256 e3Id + ) external returns (uint256 amount) { + RefundDistribution storage dist = _distributions[e3Id]; + if (!dist.calculated) revert RefundNotCalculated(e3Id); + + address requester = enclave.getRequester(e3Id); + if (msg.sender != requester) revert NotRequester(e3Id, msg.sender); + + if (_claimed[e3Id][msg.sender]) revert AlreadyClaimed(e3Id, msg.sender); + + amount = dist.requesterAmount; + if (amount == 0) revert NoRefundAvailable(e3Id); + + _claimed[e3Id][msg.sender] = true; + + feeToken.safeTransfer(msg.sender, amount); + + emit RefundClaimed(e3Id, msg.sender, amount, "REQUESTER"); + } + + /// @inheritdoc IE3RefundManager + function claimHonestNodeReward( + uint256 e3Id + ) external returns (uint256 amount) { + RefundDistribution storage dist = _distributions[e3Id]; + require(dist.calculated, RefundNotCalculated(e3Id)); + require(!_claimed[e3Id][msg.sender], AlreadyClaimed(e3Id, msg.sender)); + + // Check if caller is honest node + address[] memory nodes = _honestNodes[e3Id]; + bool isHonest = false; + for (uint256 i = 0; i < nodes.length && !isHonest; i++) { + isHonest = (nodes[i] == msg.sender); + } + require(isHonest, NotHonestNode(e3Id, msg.sender)); + + require(dist.honestNodeCount > 0, NoRefundAvailable(e3Id)); + amount = dist.honestNodeAmount / dist.honestNodeCount; + require(amount > 0, NoRefundAvailable(e3Id)); + + _claimed[e3Id][msg.sender] = true; + + // Distribute reward through bonding registry + feeToken.approve(address(bondingRegistry), amount); + + address[] memory nodeArray = new address[](1); + nodeArray[0] = msg.sender; + uint256[] memory amountArray = new uint256[](1); + amountArray[0] = amount; + + bondingRegistry.distributeRewards(feeToken, nodeArray, amountArray); + + emit RefundClaimed(e3Id, msg.sender, amount, "HONEST_NODE"); + } + + /// @inheritdoc IE3RefundManager + function routeSlashedFunds( + uint256 e3Id, + uint256 amount + ) external onlyEnclave { + RefundDistribution storage dist = _distributions[e3Id]; + require(dist.calculated, "Not calculated"); + + // Add slashed funds to distribution + // Note: slashing should be finalized before claims are made. + // 50% to requester, 50% to honest nodes for non-participation + uint256 toRequester = amount / 2; + uint256 toHonestNodes = amount - toRequester; + + dist.requesterAmount += toRequester; + dist.honestNodeAmount += toHonestNodes; + dist.totalSlashed += amount; + + emit SlashedFundsRouted(e3Id, amount); + } + + //////////////////////////////////////////////////////////// + // // + // View Functions // + // // + //////////////////////////////////////////////////////////// + /// @inheritdoc IE3RefundManager + function getRefundDistribution( + uint256 e3Id + ) external view returns (RefundDistribution memory) { + return _distributions[e3Id]; + } + + /// @inheritdoc IE3RefundManager + function hasClaimed( + uint256 e3Id, + address claimant + ) external view returns (bool) { + return _claimed[e3Id][claimant]; + } + + /// @inheritdoc IE3RefundManager + function getWorkAllocation() + external + view + returns (WorkValueAllocation memory) + { + return _workAllocation; + } + + //////////////////////////////////////////////////////////// + // // + // Admin Functions // + // // + //////////////////////////////////////////////////////////// + /// @inheritdoc IE3RefundManager + function setWorkAllocation( + WorkValueAllocation calldata allocation + ) external onlyOwner { + uint256 total = uint256(allocation.committeeFormationBps) + + uint256(allocation.dkgBps) + + uint256(allocation.decryptionBps) + + uint256(allocation.protocolBps); + require(total == 10000, "Must sum to 10000"); + + _workAllocation = allocation; + + emit WorkAllocationUpdated(allocation); + } + + /// @notice Set the Enclave contract address + /// @param _enclave New Enclave address + function setEnclave(address _enclave) external onlyOwner { + require(_enclave != address(0), "Invalid enclave"); + enclave = IEnclave(_enclave); + } + + /// @notice Set the treasury address + /// @param _treasury New treasury address + function setTreasury(address _treasury) external onlyOwner { + require(_treasury != address(0), "Invalid treasury"); + treasury = _treasury; + } +} diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 372f416dee..afe1493979 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -8,6 +8,7 @@ pragma solidity >=0.8.27; import { IEnclave, E3, IE3Program } from "./interfaces/IEnclave.sol"; import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; +import { IE3RefundManager } from "./interfaces/IE3RefundManager.sol"; import { IDecryptionVerifier } from "./interfaces/IDecryptionVerifier.sol"; import { OwnableUpgradeable @@ -39,6 +40,10 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Handles staking and reward distribution for ciphernodes. IBondingRegistry public bondingRegistry; + /// @notice E3 Refund Manager contract for handling failed E3 refunds. + /// @dev Manages refund calculation and claiming for failed E3s. + IE3RefundManager public e3RefundManager; + /// @notice Address of the ERC20 token used for E3 fees. /// @dev All E3 request fees must be paid in this token. IERC20 public feeToken; @@ -72,6 +77,21 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Stores the amount paid for an E3, distributed to committee upon completion. mapping(uint256 e3Id => uint256 e3Payment) public e3Payments; + /// @notice Maps E3 ID to its current stage + mapping(uint256 e3Id => E3Stage) internal _e3Stages; + + /// @notice Maps E3 ID to its deadlines + mapping(uint256 e3Id => E3Deadlines) internal _e3Deadlines; + + /// @notice Maps E3 ID to failure reason (if failed) + mapping(uint256 e3Id => FailureReason) internal _e3FailureReasons; + + /// @notice Maps E3 ID to requester address + mapping(uint256 e3Id => address) internal _e3Requesters; + + /// @notice Global timeout configuration + E3TimeoutConfig internal _timeoutConfig; + //////////////////////////////////////////////////////////// // // // Errors // @@ -85,20 +105,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @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); @@ -115,16 +121,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @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 attempting to set an invalid ciphernode registry address. /// @param ciphernodeRegistry The invalid ciphernode registry address. error InvalidCiphernodeRegistry(ICiphernodeRegistry ciphernodeRegistry); @@ -137,12 +133,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @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); @@ -171,12 +161,56 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @param feeToken The invalid fee token address. error InvalidFeeToken(IERC20 feeToken); + /// @notice E3 is not in expected stage + error InvalidStage(uint256 e3Id, E3Stage expected, E3Stage actual); + + /// @notice E3 has already been marked as failed + error E3AlreadyFailed(uint256 e3Id); + + /// @notice E3 has already completed + error E3AlreadyComplete(uint256 e3Id); + + /// @notice Failure condition not yet met + error FailureConditionNotMet(uint256 e3Id); + + /// @notice The Input deadline is invalid + error InvalidInputDeadline(uint256 deadline); + + /// @notice The input deadline start is in the past + error InvalidInputDeadlineStart(uint256 start); + /// @notice The input deadline end is before the start + error InvalidInputDeadlineEnd(uint256 end); + + /// @notice The duties are completed, and ciphernodes are not required to act anymore for this E3 + /// @param e3Id The ID of the E3 + /// @param expiration The expiration timestamp of the E3 + error CommitteeDutiesCompleted(uint256 e3Id, uint256 expiration); + + /// @notice The input deadline has not yet been reached + /// @param e3Id The ID of the E3 + /// @param inputDeadline The input deadline timestamp of the E3 + error InputDeadlineNotReached(uint256 e3Id, uint256 inputDeadline); + //////////////////////////////////////////////////////////// // // - // Initialization // + // Modifiers // // // //////////////////////////////////////////////////////////// + /// @notice Restricts function to CiphernodeRegistry contract only + modifier onlyCiphernodeRegistry() { + require( + msg.sender == address(ciphernodeRegistry), + "Only CiphernodeRegistry" + ); + _; + } + + //////////////////////////////////////////////////////////// + // // + // Initialization // + //////////////////////////////////////////////////////////// + /// @notice Constructor that disables initializers. /// @dev Prevents the implementation contract from being initialized. Initialization is performed /// via the initialize() function when deployed behind a proxy. @@ -189,22 +223,28 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @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 _e3RefundManager The address of the E3 Refund Manager contract. /// @param _feeToken The address of the ERC20 token used for E3 fees. /// @param _maxDuration The maximum duration of a computation in seconds. + /// @param config Initial timeout configuration for E3 lifecycle stages. /// @param _e3ProgramsParams Array of ABI encoded E3 encryption scheme parameters sets (e.g., for BFV). function initialize( address _owner, ICiphernodeRegistry _ciphernodeRegistry, IBondingRegistry _bondingRegistry, + IE3RefundManager _e3RefundManager, IERC20 _feeToken, uint256 _maxDuration, + E3TimeoutConfig calldata config, bytes[] memory _e3ProgramsParams ) public initializer { __Ownable_init(msg.sender); setMaxDuration(_maxDuration); setCiphernodeRegistry(_ciphernodeRegistry); setBondingRegistry(_bondingRegistry); + setE3RefundManager(_e3RefundManager); setFeeToken(_feeToken); + _setTimeoutConfig(config); setE3ProgramsParams(_e3ProgramsParams); if (_owner != owner()) transferOwnership(_owner); } @@ -219,28 +259,42 @@ contract Enclave is IEnclave, OwnableUpgradeable { function request( E3RequestParams calldata requestParams ) external returns (uint256 e3Id, E3 memory e3) { - uint256 e3Fee = getE3Quote(requestParams); + // check whether the threshold config is valid require( requestParams.threshold[1] >= requestParams.threshold[0] && requestParams.threshold[0] > 0, InvalidThreshold(requestParams.threshold) ); + + // input start date should be in the future require( - // TODO: do we need a minimum start window to allow time for committee selection? - requestParams.startWindow[1] >= requestParams.startWindow[0] && - requestParams.startWindow[1] >= block.timestamp, - InvalidStartWindow() + requestParams.inputWindow[0] >= block.timestamp, + // && + // requestParams.inputWindow[0] >= block.timestamp + + // _timeoutConfig.dkgWindow, + InvalidInputDeadlineStart(requestParams.inputWindow[0]) ); + // the end of the input window should be after the start require( - requestParams.duration > 0 && requestParams.duration <= maxDuration, - InvalidDuration(requestParams.duration) + requestParams.inputWindow[1] >= requestParams.inputWindow[0], + InvalidInputDeadlineEnd(requestParams.inputWindow[1]) ); + + // The total duration cannot be > maxDuration + uint256 totalDuration = requestParams.inputWindow[1] - + block.timestamp + + _timeoutConfig.computeWindow + + _timeoutConfig.decryptionWindow; + // TODO do we actually need a max duration? + require(totalDuration < maxDuration, InvalidDuration(totalDuration)); + require( e3Programs[requestParams.e3Program], E3ProgramNotAllowed(requestParams.e3Program) ); - // TODO: should IDs be incremental or produced deterministically? + uint256 e3Fee = getE3Quote(requestParams); + e3Id = nexte3Id; nexte3Id++; uint256 seed = uint256(keccak256(abi.encode(block.prevrandao, e3Id))); @@ -248,9 +302,7 @@ contract Enclave is IEnclave, OwnableUpgradeable { e3.seed = seed; e3.threshold = requestParams.threshold; e3.requestBlock = block.number; - e3.startWindow = requestParams.startWindow; - e3.duration = requestParams.duration; - e3.expiration = 0; + e3.inputWindow = requestParams.inputWindow; e3.e3Program = requestParams.e3Program; e3.e3ProgramParams = requestParams.e3ProgramParams; e3.customParams = requestParams.customParams; @@ -293,47 +345,17 @@ contract Enclave is IEnclave, OwnableUpgradeable { CommitteeSelectionFailed() ); - emit E3Requested(e3Id, e3, requestParams.e3Program); - } - - /// @inheritdoc IEnclave - function activate(uint256 e3Id) external returns (bool success) { - E3 memory e3 = getE3(e3Id); - - require(e3.expiration == 0, E3AlreadyActivated(e3Id)); - require(e3.startWindow[0] <= block.timestamp, E3NotReady()); - // TODO: handle what happens to the payment if the start window has passed. - require(e3.startWindow[1] >= block.timestamp, E3Expired()); - - bytes32 publicKeyHash = ciphernodeRegistry.committeePublicKey(e3Id); - - uint256 expiresAt = block.timestamp + e3.duration; - e3s[e3Id].expiration = expiresAt; - e3s[e3Id].committeePublicKey = publicKeyHash; - - emit E3Activated(e3Id, expiresAt, publicKeyHash); - - return true; - } - - /// @inheritdoc IEnclave - function publishInput( - uint256 e3Id, - bytes calldata data - ) external returns (bool success) { - E3 memory e3 = getE3(e3Id); - - // Note: if we make 0 a no expiration, this has to be refactored - require(e3.expiration > 0, E3NotActivated(e3Id)); - // TODO: should we have an input window, including both a start and end timestamp? - require( - e3.expiration > block.timestamp, - InputDeadlinePassed(e3Id, e3.expiration) - ); + // Initialize E3 lifecycle + _e3Stages[e3Id] = E3Stage.Requested; + _e3Requesters[e3Id] = msg.sender; - e3.e3Program.validateInput(e3Id, msg.sender, data); + // the compute deadline is end of input window + compute window + _e3Deadlines[e3Id].computeDeadline = + e3.inputWindow[1] + + _timeoutConfig.computeWindow; - success = true; + emit E3Requested(e3Id, e3, requestParams.e3Program); + emit E3StageChanged(e3Id, E3Stage.None, E3Stage.Requested); } /// @inheritdoc IEnclave @@ -344,14 +366,21 @@ contract Enclave is IEnclave, OwnableUpgradeable { ) external returns (bool success) { E3 memory e3 = getE3(e3Id); - // Note: if we make 0 a no expiration, this has to be refactored - require(e3.expiration > 0, E3NotActivated(e3Id)); + E3Deadlines memory deadlines = _e3Deadlines[e3Id]; + + // You cannot post outputs after the compute deadline + require( + deadlines.computeDeadline >= block.timestamp, + CommitteeDutiesCompleted(e3Id, deadlines.computeDeadline) + ); + + // The program need to have stopped accepting inputs require( - e3.expiration <= block.timestamp, - InputDeadlineNotPassed(e3Id, e3.expiration) + block.timestamp >= e3.inputWindow[1], + InputDeadlineNotReached(e3Id, e3.inputWindow[1]) ); - // TODO: should the output verifier be able to change its mind? - //i.e. should we be able to call this multiple times? + + // For now we only accept one output require( e3.ciphertextOutput == bytes32(0), CiphertextOutputAlreadyPublished(e3Id) @@ -363,7 +392,18 @@ contract Enclave is IEnclave, OwnableUpgradeable { (success) = e3.e3Program.verify(e3Id, ciphertextOutputHash, proof); require(success, InvalidOutput(ciphertextOutput)); + // Update lifecycle stage + _e3Stages[e3Id] = E3Stage.CiphertextReady; + _e3Deadlines[e3Id].decryptionDeadline = + block.timestamp + + _timeoutConfig.decryptionWindow; + emit CiphertextOutputPublished(e3Id, ciphertextOutput); + emit E3StageChanged( + e3Id, + E3Stage.KeyPublished, + E3Stage.CiphertextReady + ); } /// @inheritdoc IEnclave @@ -374,15 +414,20 @@ contract Enclave is IEnclave, OwnableUpgradeable { ) external returns (bool success) { E3 memory e3 = getE3(e3Id); - // Note: if we make 0 a no expiration, this has to be refactored - require(e3.expiration > 0, E3NotActivated(e3Id)); + // Check we are in the right stage + // no need to check if there's a ciphertext as we would not + // be in this stage otherwise + E3Stage current = _e3Stages[e3Id]; require( - e3.ciphertextOutput != bytes32(0), - CiphertextOutputNotPublished(e3Id) + current == E3Stage.CiphertextReady, + InvalidStage(e3Id, E3Stage.CiphertextReady, current) ); + + // you cannot post a decryption after the decryption deadline + E3Deadlines memory deadlines = _e3Deadlines[e3Id]; require( - e3.plaintextOutput.length == 0, - PlaintextOutputAlreadyPublished(e3Id) + deadlines.decryptionDeadline >= block.timestamp, + CommitteeDutiesCompleted(e3Id, deadlines.decryptionDeadline) ); e3s[e3Id].plaintextOutput = plaintextOutput; @@ -394,9 +439,13 @@ contract Enclave is IEnclave, OwnableUpgradeable { ); require(success, InvalidOutput(plaintextOutput)); + // Update lifecycle stage to Complete + _e3Stages[e3Id] = E3Stage.Complete; + _distributeRewards(e3Id); emit PlaintextOutputPublished(e3Id, plaintextOutput); + emit E3StageChanged(e3Id, E3Stage.CiphertextReady, E3Stage.Complete); } //////////////////////////////////////////////////////////// @@ -436,6 +485,34 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit RewardsDistributed(e3Id, committeeNodes, amounts); } + /// @notice Retrieves the honest committee nodes for a given E3. + /// @dev Determines honest nodes based on failure reason and committee publication status. + /// @param e3Id The ID of the E3. + /// @return honestNodes An array of addresses of honest committee nodes. + function _getHonestNodes( + uint256 e3Id + ) private view returns (address[] memory) { + FailureReason reason = _e3FailureReasons[e3Id]; + + // Early failures have no committee + if ( + reason == FailureReason.CommitteeFormationTimeout || + reason == FailureReason.InsufficientCommitteeMembers + ) { + return new address[](0); + } + + // Try to get published committee nodes + try ciphernodeRegistry.getCommitteeNodes(e3Id) returns ( + address[] memory nodes + ) { + // TODO: Implement fault attribution to filter honest from faulting nodes + return nodes; // Assume all are honest for now + } catch { + return new address[](0); // Committee not published (DKG failed) + } + } + //////////////////////////////////////////////////////////// // // // Set Functions // @@ -443,75 +520,61 @@ contract Enclave is IEnclave, OwnableUpgradeable { //////////////////////////////////////////////////////////// /// @inheritdoc IEnclave - function setMaxDuration( - uint256 _maxDuration - ) public onlyOwner returns (bool success) { + function setMaxDuration(uint256 _maxDuration) public onlyOwner { maxDuration = _maxDuration; - success = true; emit MaxDurationSet(_maxDuration); } /// @inheritdoc IEnclave function setCiphernodeRegistry( ICiphernodeRegistry _ciphernodeRegistry - ) public onlyOwner returns (bool success) { + ) public onlyOwner { require( address(_ciphernodeRegistry) != address(0) && _ciphernodeRegistry != ciphernodeRegistry, InvalidCiphernodeRegistry(_ciphernodeRegistry) ); ciphernodeRegistry = _ciphernodeRegistry; - success = true; emit CiphernodeRegistrySet(address(_ciphernodeRegistry)); } /// @inheritdoc IEnclave function setBondingRegistry( IBondingRegistry _bondingRegistry - ) public onlyOwner returns (bool success) { + ) public onlyOwner { 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) { + function setFeeToken(IERC20 _feeToken) public onlyOwner { 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) { + function enableE3Program(IE3Program e3Program) public onlyOwner { require( !e3Programs[e3Program], ModuleAlreadyEnabled(address(e3Program)) ); e3Programs[e3Program] = true; - success = true; emit E3ProgramEnabled(e3Program); } /// @inheritdoc IEnclave - function disableE3Program( - IE3Program e3Program - ) public onlyOwner returns (bool success) { + function disableE3Program(IE3Program e3Program) public onlyOwner { require(e3Programs[e3Program], ModuleNotEnabled(address(e3Program))); delete e3Programs[e3Program]; - success = true; emit E3ProgramDisabled(e3Program); } @@ -519,21 +582,20 @@ contract Enclave is IEnclave, OwnableUpgradeable { function setDecryptionVerifier( bytes32 encryptionSchemeId, IDecryptionVerifier decryptionVerifier - ) public onlyOwner returns (bool success) { + ) public onlyOwner { require( decryptionVerifier != IDecryptionVerifier(address(0)) && decryptionVerifiers[encryptionSchemeId] != decryptionVerifier, InvalidEncryptionScheme(encryptionSchemeId) ); decryptionVerifiers[encryptionSchemeId] = decryptionVerifier; - success = true; emit EncryptionSchemeEnabled(encryptionSchemeId); } /// @inheritdoc IEnclave function disableEncryptionScheme( bytes32 encryptionSchemeId - ) public onlyOwner returns (bool success) { + ) public onlyOwner { require( decryptionVerifiers[encryptionSchemeId] != IDecryptionVerifier(address(0)), @@ -542,14 +604,13 @@ contract Enclave is IEnclave, OwnableUpgradeable { decryptionVerifiers[encryptionSchemeId] = IDecryptionVerifier( address(0) ); - success = true; emit EncryptionSchemeDisabled(encryptionSchemeId); } /// @inheritdoc IEnclave function setE3ProgramsParams( bytes[] memory _e3ProgramsParams - ) public onlyOwner returns (bool success) { + ) public onlyOwner { uint256 length = _e3ProgramsParams.length; for (uint256 i; i < length; ) { e3ProgramsParams[_e3ProgramsParams[i]] = true; @@ -557,10 +618,253 @@ contract Enclave is IEnclave, OwnableUpgradeable { ++i; } } - success = true; emit AllowedE3ProgramsParamsSet(_e3ProgramsParams); } + /// @notice Sets the E3 Refund Manager contract address + /// @param _e3RefundManager The new E3 Refund Manager contract address + function setE3RefundManager( + IE3RefundManager _e3RefundManager + ) public onlyOwner { + require( + address(_e3RefundManager) != address(0), + "Invalid E3RefundManager address" + ); + e3RefundManager = _e3RefundManager; + emit E3RefundManagerSet(address(_e3RefundManager)); + } + + /// @notice Process a failed E3 and calculate refunds + /// @dev Can be called by anyone once E3 is in failed state + /// @param e3Id The ID of the failed E3 + function processE3Failure(uint256 e3Id) external { + E3Stage stage = _e3Stages[e3Id]; + require(stage == E3Stage.Failed, "E3 not failed"); + + uint256 payment = e3Payments[e3Id]; + require(payment > 0, "No payment to refund"); + e3Payments[e3Id] = 0; // Prevent double processing + + address[] memory honestNodes = _getHonestNodes(e3Id); + + feeToken.safeTransfer(address(e3RefundManager), payment); + e3RefundManager.calculateRefund(e3Id, payment, honestNodes); + + emit E3FailureProcessed(e3Id, payment, honestNodes.length); + } + + /// @inheritdoc IEnclave + function onCommitteeFinalized( + uint256 e3Id + ) external onlyCiphernodeRegistry { + // Update E3 lifecycle stage - committee finalized, DKG starting + E3Stage current = _e3Stages[e3Id]; + if (current != E3Stage.Requested) { + revert InvalidStage(e3Id, E3Stage.Requested, current); + } + _e3Stages[e3Id] = E3Stage.CommitteeFinalized; + _e3Deadlines[e3Id].dkgDeadline = + block.timestamp + + _timeoutConfig.dkgWindow; + + emit CommitteeFinalized(e3Id); + emit E3StageChanged( + e3Id, + E3Stage.Requested, + E3Stage.CommitteeFinalized + ); + } + + /// @inheritdoc IEnclave + function onCommitteePublished( + uint256 e3Id + ) external onlyCiphernodeRegistry { + // DKG complete, key published + E3Stage current = _e3Stages[e3Id]; + if (current != E3Stage.CommitteeFinalized) { + revert InvalidStage(e3Id, E3Stage.CommitteeFinalized, current); + } + _e3Stages[e3Id] = E3Stage.KeyPublished; + + emit CommitteeFormed(e3Id); + emit E3StageChanged( + e3Id, + E3Stage.CommitteeFinalized, + E3Stage.KeyPublished + ); + } + + /// @inheritdoc IEnclave + function onE3Failed( + uint256 e3Id, + uint8 reason + ) external onlyCiphernodeRegistry { + require(reason > 0 && reason <= 12, "Invalid failure reason"); + // Mark E3 as failed with the given reason + _markE3FailedWithReason(e3Id, FailureReason(reason)); + } + + //////////////////////////////////////////////////////////// + // // + // Lifecycle Functions // + // // + //////////////////////////////////////////////////////////// + + /// @notice Anyone can mark an E3 as failed if timeout passed + /// @param e3Id The E3 ID + /// @return reason The failure reason + function markE3Failed( + uint256 e3Id + ) external returns (FailureReason reason) { + E3Stage current = _e3Stages[e3Id]; + + if (current == E3Stage.None) + revert InvalidStage(e3Id, E3Stage.Requested, current); + if (current == E3Stage.Complete) revert E3AlreadyComplete(e3Id); + if (current == E3Stage.Failed) revert E3AlreadyFailed(e3Id); + + bool canFail; + (canFail, reason) = _checkFailureCondition(e3Id, current); + if (!canFail) revert FailureConditionNotMet(e3Id); + + _e3Stages[e3Id] = E3Stage.Failed; + _e3FailureReasons[e3Id] = reason; + + emit E3StageChanged(e3Id, current, E3Stage.Failed); + emit E3Failed(e3Id, current, reason); + } + + /// @notice Internal function to mark E3 as failed with specific reason + /// @param e3Id The E3 ID + /// @param reason The failure reason + function _markE3FailedWithReason( + uint256 e3Id, + FailureReason reason + ) internal { + E3Stage current = _e3Stages[e3Id]; + + if (current == E3Stage.None) + revert InvalidStage(e3Id, E3Stage.Requested, current); + if (current == E3Stage.Complete) revert E3AlreadyComplete(e3Id); + if (current == E3Stage.Failed) revert E3AlreadyFailed(e3Id); + + _e3Stages[e3Id] = E3Stage.Failed; + _e3FailureReasons[e3Id] = reason; + + emit E3StageChanged(e3Id, current, E3Stage.Failed); + emit E3Failed(e3Id, current, reason); + } + + /// @notice Check if E3 can be marked as failed + /// @param e3Id The E3 ID + /// @return canFail Whether failure condition is met + /// @return reason The failure reason if applicable + function checkFailureCondition( + uint256 e3Id + ) external view returns (bool canFail, FailureReason reason) { + E3Stage current = _e3Stages[e3Id]; + return _checkFailureCondition(e3Id, current); + } + + /// @notice Internal function to check failure conditions + function _checkFailureCondition( + uint256 e3Id, + E3Stage stage + ) internal view returns (bool canFail, FailureReason reason) { + E3Deadlines memory d = _e3Deadlines[e3Id]; + + uint256 committeeDeadline = ciphernodeRegistry.getCommitteeDeadline( + e3Id + ); + + if (stage == E3Stage.Requested && block.timestamp > committeeDeadline) { + return (true, FailureReason.CommitteeFormationTimeout); + } + if ( + stage == E3Stage.CommitteeFinalized && + block.timestamp > d.dkgDeadline + ) { + return (true, FailureReason.DKGTimeout); + } + if ( + stage == E3Stage.KeyPublished && block.timestamp > d.computeDeadline + ) { + return (true, FailureReason.ComputeTimeout); + } + if ( + stage == E3Stage.CiphertextReady && + block.timestamp > d.decryptionDeadline + ) { + return (true, FailureReason.DecryptionTimeout); + } + + return (false, FailureReason.None); + } + + /// @notice Get current stage of an E3 + /// @param e3Id The E3 ID + /// @return stage The current stage + function getE3Stage(uint256 e3Id) external view returns (E3Stage stage) { + return _e3Stages[e3Id]; + } + + /// @notice Get failure reason for an E3 + /// @param e3Id The E3 ID + /// @return reason The failure reason + function getFailureReason( + uint256 e3Id + ) external view returns (FailureReason reason) { + return _e3FailureReasons[e3Id]; + } + + /// @notice Get requester address for an E3 + /// @param e3Id The E3 ID + /// @return requester The requester address + function getRequester( + uint256 e3Id + ) external view returns (address requester) { + return _e3Requesters[e3Id]; + } + + /// @notice Get deadlines for an E3 + /// @param e3Id The E3 ID + /// @return deadlines The E3 deadlines + function getDeadlines( + uint256 e3Id + ) external view returns (E3Deadlines memory deadlines) { + return _e3Deadlines[e3Id]; + } + + /// @notice Get timeout configuration + /// @return config The current timeout config + function getTimeoutConfig() + external + view + returns (E3TimeoutConfig memory config) + { + return _timeoutConfig; + } + + /// @notice Set timeout configuration + /// @param config The new timeout config + function setTimeoutConfig( + E3TimeoutConfig calldata config + ) external onlyOwner { + _setTimeoutConfig(config); + } + + /// @notice Internal function to set timeout config + function _setTimeoutConfig(E3TimeoutConfig calldata config) internal { + require(config.dkgWindow > 0, "Invalid DKG window"); + require(config.computeWindow > 0, "Invalid compute window"); + require(config.decryptionWindow > 0, "Invalid decryption window"); + require(config.gracePeriod > 0, "Invalid grace period"); + + _timeoutConfig = config; + + emit TimeoutConfigUpdated(config); + } + //////////////////////////////////////////////////////////// // // // Get Functions // diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index 6d300ea68e..a343b2f747 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -173,6 +173,12 @@ interface IBondingRegistry { */ function isActive(address operator) external view returns (bool); + /** + * @notice Get the number of currently active operators + * @return Number of active operators + */ + function numActiveOperators() external view returns (uint256); + /** * @notice Check if operator has deregistration in progress * @param operator Address of the operator diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 4b8d11937f..4cfa7b1c8f 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -5,6 +5,9 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; +import { IEnclave } from "./IEnclave.sol"; +import { IBondingRegistry } from "./IBondingRegistry.sol"; + /** * @title ICiphernodeRegistry * @notice Interface for managing ciphernode registration and committee selection @@ -16,7 +19,7 @@ interface ICiphernodeRegistry { /// @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 committeeDeadline The deadline for committee formation (ticket submission). /// @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. @@ -24,12 +27,14 @@ interface ICiphernodeRegistry { /// @param committee The committee for the round. /// @param submitted Mapping of nodes to their submission status. /// @param scoreOf Mapping of nodes to their scores. + /// @param failed True if committee formation failed (threshold not met). struct Committee { bool initialized; bool finalized; + bool failed; uint256 seed; uint256 requestBlock; - uint256 submissionDeadline; + uint256 committeeDeadline; bytes32 publicKey; uint32[2] threshold; address[] topNodes; @@ -43,13 +48,13 @@ interface ICiphernodeRegistry { /// @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. + /// @param committeeDeadline Deadline for committee formation (ticket submission). event CommitteeRequested( uint256 indexed e3Id, uint256 seed, uint32[2] threshold, uint256 requestBlock, - uint256 submissionDeadline + uint256 committeeDeadline ); /// @notice This event MUST be emitted when a ticket is submitted for sortition @@ -69,6 +74,16 @@ interface ICiphernodeRegistry { /// @param committee Array of selected ciphernode addresses event CommitteeFinalized(uint256 indexed e3Id, address[] committee); + /// @notice This event MUST be emitted when committee formation fails (threshold not met) + /// @param e3Id ID of the E3 computation + /// @param nodesSubmitted Number of nodes that submitted tickets + /// @param thresholdRequired Minimum number of nodes required + event CommitteeFormationFailed( + uint256 indexed e3Id, + uint256 nodesSubmitted, + uint256 thresholdRequired + ); + /// @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. @@ -200,12 +215,12 @@ interface ICiphernodeRegistry { /// @notice Sets the Enclave contract address /// @dev Only callable by owner /// @param _enclave Address of the Enclave contract - function setEnclave(address _enclave) external; + function setEnclave(IEnclave _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; + function setBondingRegistry(IBondingRegistry _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. @@ -220,11 +235,18 @@ interface ICiphernodeRegistry { function submitTicket(uint256 e3Id, uint256 ticketNumber) external; /// @notice Finalize the committee after submission window closes + /// @dev If threshold not met, marks E3 as failed and returns false /// @param e3Id ID of the E3 computation - function finalizeCommittee(uint256 e3Id) external; + /// @return success True if committee formed successfully, false if threshold not met + function finalizeCommittee(uint256 e3Id) external returns (bool success); /// @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); + + /// @notice Get the committee deadline for an E3 + /// @param e3Id ID of the E3 computation + /// @return committeeDeadline The committee deadline timestamp + function getCommitteeDeadline(uint256 e3Id) external view returns (uint256); } diff --git a/packages/enclave-contracts/contracts/interfaces/IE3.sol b/packages/enclave-contracts/contracts/interfaces/IE3.sol index 545ec7c1c7..ed83c285ef 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3.sol @@ -16,9 +16,7 @@ import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; * @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 inputWindow When to start and stop accepting inputs from data providers * @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 @@ -33,9 +31,7 @@ struct E3 { uint256 seed; uint32[2] threshold; uint256 requestBlock; - uint256[2] startWindow; - uint256 duration; - uint256 expiration; + uint256[2] inputWindow; bytes32 encryptionSchemeId; IE3Program e3Program; bytes e3ProgramParams; diff --git a/packages/enclave-contracts/contracts/interfaces/IE3Program.sol b/packages/enclave-contracts/contracts/interfaces/IE3Program.sol index 742eadd730..189f8a9f65 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3Program.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3Program.sol @@ -40,13 +40,9 @@ interface IE3Program { ) external returns (bool success); /// @notice Validate and process input data for a computation - /// @dev This function is called by the Enclave contract when input is published + /// @dev This function is called by data providers when they want to submit their + /// encrypted data /// @param e3Id ID of the E3 computation - /// @param sender The account that is submitting the input /// @param data The input data to be validated - function validateInput( - uint256 e3Id, - address sender, - bytes memory data - ) external; + function publishInput(uint256 e3Id, bytes memory data) external; } diff --git a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol new file mode 100644 index 0000000000..aa2062cb47 --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol @@ -0,0 +1,150 @@ +// 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 { IEnclave } from "./IEnclave.sol"; + +/** + * @title IE3RefundManager + * @notice Interface for E3 refund distribution mechanism + * @dev Handles refund calculation and claiming for failed E3s + */ +interface IE3RefundManager { + //////////////////////////////////////////////////////////// + // // + // Structs // + // // + //////////////////////////////////////////////////////////// + /// @notice Work value allocation in basis points (10000 = 100%) + struct WorkValueAllocation { + uint16 committeeFormationBps; + uint16 dkgBps; + uint16 decryptionBps; + uint16 protocolBps; + } + /// @notice Refund distribution for a failed E3 + struct RefundDistribution { + uint256 requesterAmount; // Amount for requester + uint256 honestNodeAmount; // Total amount for honest nodes + uint256 protocolAmount; // Amount for protocol treasury + uint256 totalSlashed; // Slashed funds added + uint256 honestNodeCount; // Number of honest nodes + bool calculated; // Whether distribution is calculated + } + //////////////////////////////////////////////////////////// + // // + // Events // + // // + //////////////////////////////////////////////////////////// + /// @notice Emitted when refund distribution is calculated + event RefundDistributionCalculated( + uint256 indexed e3Id, + uint256 requesterAmount, + uint256 honestNodeAmount, + uint256 protocolAmount, + uint256 totalSlashed + ); + /// @notice Emitted when a refund is claimed + event RefundClaimed( + uint256 indexed e3Id, + address indexed claimant, + uint256 amount, + bytes32 claimType + ); + /// @notice Emitted when slashed funds are routed to E3 + event SlashedFundsRouted(uint256 indexed e3Id, uint256 amount); + /// @notice Emitted when work allocation is updated + event WorkAllocationUpdated(WorkValueAllocation allocation); + //////////////////////////////////////////////////////////// + // // + // Errors // + // // + //////////////////////////////////////////////////////////// + /// @notice E3 is not in failed state + error E3NotFailed(uint256 e3Id); + /// @notice Refund already claimed + error AlreadyClaimed(uint256 e3Id, address claimant); + /// @notice Not the requester + error NotRequester(uint256 e3Id, address caller); + /// @notice Not an honest node + error NotHonestNode(uint256 e3Id, address caller); + /// @notice Refund not calculated yet + error RefundNotCalculated(uint256 e3Id); + /// @notice No refund available + error NoRefundAvailable(uint256 e3Id); + /// @notice Caller not authorized + error Unauthorized(); + + //////////////////////////////////////////////////////////// + // // + // Functions // + // // + //////////////////////////////////////////////////////////// + /// @notice Calculate refund distribution for a failed E3 + /// @param e3Id The failed E3 ID + /// @param originalPayment The original payment amount + /// @param honestNodes Array of honest node addresses + function calculateRefund( + uint256 e3Id, + uint256 originalPayment, + address[] calldata honestNodes + ) external; + + /// @notice Requester claims their refund + /// @param e3Id The failed E3 ID + /// @return amount The amount claimed + function claimRequesterRefund( + uint256 e3Id + ) external returns (uint256 amount); + + /// @notice Honest node claims their reward + /// @param e3Id The failed E3 ID + /// @return amount The amount claimed + function claimHonestNodeReward( + uint256 e3Id + ) external returns (uint256 amount); + + /// @notice Route slashed funds to E3 refund pool + /// @param e3Id The E3 ID + /// @param amount The slashed amount + function routeSlashedFunds(uint256 e3Id, uint256 amount) external; + + /// @notice Get refund distribution for an E3 + /// @param e3Id The E3 ID + /// @return distribution The refund distribution + function getRefundDistribution( + uint256 e3Id + ) external view returns (RefundDistribution memory distribution); + + /// @notice Check if address has claimed refund + /// @param e3Id The E3 ID + /// @param claimant The address to check + /// @return claimed Whether the address has claimed + function hasClaimed( + uint256 e3Id, + address claimant + ) external view returns (bool claimed); + + /// @notice Calculate work value for a given stage + /// @param stage The stage when E3 failed + /// @return workCompletedBps Work completed in basis points + /// @return workRemainingBps Work remaining in basis points + function calculateWorkValue( + IEnclave.E3Stage stage + ) external view returns (uint16 workCompletedBps, uint16 workRemainingBps); + + /// @notice Set work value allocation + /// @param allocation The new work allocation + function setWorkAllocation( + WorkValueAllocation calldata allocation + ) external; + + /// @notice Get current work allocation + /// @return allocation The current work allocation + function getWorkAllocation() + external + view + returns (WorkValueAllocation memory allocation); +} diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index ba95e0da84..af45f5dfb9 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -12,6 +12,63 @@ import { IDecryptionVerifier } from "./IDecryptionVerifier.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IEnclave { + //////////////////////////////////////////////////////////// + // // + // Enums // + // // + //////////////////////////////////////////////////////////// + + /// @notice Lifecycle stages of an E3 computation + enum E3Stage { + None, + Requested, + CommitteeFinalized, + // Once a key is published, it is possible to then accept inputs + // as long as we are within the input deadline (start and end) + KeyPublished, + CiphertextReady, + Complete, + Failed + } + + /// @notice Reasons why an E3 failed + enum FailureReason { + None, + CommitteeFormationTimeout, + InsufficientCommitteeMembers, + DKGTimeout, + DKGInvalidShares, + NoInputsReceived, + ComputeTimeout, + ComputeProviderExpired, + ComputeProviderFailed, + RequesterCancelled, + DecryptionTimeout, + DecryptionInvalidShares, + VerificationFailed + } + + //////////////////////////////////////////////////////////// + // // + // Structs // + // // + //////////////////////////////////////////////////////////// + + /// @notice Timeout configuration for E3 stages + struct E3TimeoutConfig { + uint256 dkgWindow; + uint256 computeWindow; + uint256 decryptionWindow; + uint256 gracePeriod; + } + + /// @notice Deadlines for each E3 + struct E3Deadlines { + uint256 dkgDeadline; + uint256 computeDeadline; + uint256 decryptionDeadline; + } + //////////////////////////////////////////////////////////// // // // Events // @@ -24,16 +81,6 @@ interface IEnclave { /// @param e3Program Address of the Computation module selected. 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. - /// @param expiration Timestamp when committee duties expire. - /// @param committeePublicKey Hash of the public key of the committee. - event E3Activated( - uint256 e3Id, - uint256 expiration, - bytes32 committeePublicKey - ); - /// @notice This event MUST be emitted when an input to an Encrypted Execution Environment (E3) is /// successfully published. /// @param e3Id ID of the E3. @@ -106,6 +153,45 @@ interface IEnclave { /// @param e3ProgramParams Array of encoded encryption scheme parameters (e.g, for BFV) event AllowedE3ProgramsParamsSet(bytes[] e3ProgramParams); + /// @notice Emitted when E3RefundManager contract is set. + /// @param e3RefundManager The address of the E3RefundManager contract. + event E3RefundManagerSet(address indexed e3RefundManager); + + /// @notice Emitted when a failed E3 is processed for refunds. + /// @param e3Id The ID of the failed E3. + /// @param paymentAmount The original payment amount being refunded. + /// @param honestNodeCount The number of honest nodes in the refund distribution. + event E3FailureProcessed( + uint256 indexed e3Id, + uint256 paymentAmount, + uint256 honestNodeCount + ); + + /// @notice Emitted when a committee is published and E3 lifecycle is updated. + /// @param e3Id The ID of the E3. + event CommitteeFormed(uint256 indexed e3Id); + + /// @notice Emitted when a committee is finalized (sortition complete, DKG starting). + /// @param e3Id The ID of the E3. + event CommitteeFinalized(uint256 indexed e3Id); + + /// @notice Emitted when E3 stage changes + event E3StageChanged( + uint256 indexed e3Id, + E3Stage previousStage, + E3Stage newStage + ); + + /// @notice Emitted when an E3 is marked as failed + event E3Failed( + uint256 indexed e3Id, + E3Stage failedAtStage, + FailureReason reason + ); + + /// @notice Emitted when timeout config is updated + event TimeoutConfigUpdated(E3TimeoutConfig config); + //////////////////////////////////////////////////////////// // // // Structs // @@ -114,16 +200,14 @@ interface IEnclave { /// @notice This struct contains the parameters to submit a request to Enclave. /// @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. + /// @param inputWindow When the program will start and stop accepting inputs. /// @param e3Program The address of the E3 Program. /// @param e3ProgramParams The ABI encoded computation parameters. /// @param computeProviderParams The ABI encoded compute provider parameters. /// @param customParams Arbitrary ABI-encoded application-defined parameters. struct E3RequestParams { uint32[2] threshold; - uint256[2] startWindow; - uint256 duration; + uint256[2] inputWindow; IE3Program e3Program; bytes e3ProgramParams; bytes computeProviderParams; @@ -145,26 +229,6 @@ interface IEnclave { 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. - /// @dev This function MUST emit the E3Activated event. - /// @dev This function MUST revert if the given E3 has not yet been requested. - /// @dev This function MUST revert if the selected node committee has not yet published a public key. - /// @param e3Id ID of the E3. - /// @return success True if the E3 was successfully activated. - function activate(uint256 e3Id) external returns (bool success); - - /// @notice This function should be called to publish input data for Encrypted Execution Environment (E3). - /// @dev This function MUST revert if the E3 is not yet activated. - /// @dev This function MUST emit the InputPublished event. - /// @param e3Id ID of the E3. - /// @param data ABI encoded input data to publish. - /// @return success True if the input was successfully published. - function publishInput( - uint256 e3Id, - bytes calldata data - ) external returns (bool success); - /// @notice This function should be called to publish output data for an Encrypted Execution Environment (E3). /// @dev This function MUST emit the CiphertextOutputPublished event. /// @param e3Id ID of the E3. @@ -197,72 +261,51 @@ interface IEnclave { /// @notice This function should be called to set the maximum duration of requested computations. /// @param _maxDuration The maximum duration of a computation in seconds. - /// @return success True if the max duration was successfully set. - function setMaxDuration( - uint256 _maxDuration - ) external returns (bool success); + function setMaxDuration(uint256 _maxDuration) external; /// @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); + ) external; /// @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); + function setBondingRegistry(IBondingRegistry _bondingRegistry) external; /// @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); + function setFeeToken(IERC20 _feeToken) external; /// @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. - function enableE3Program( - IE3Program e3Program - ) external returns (bool success); + function enableE3Program(IE3Program e3Program) external; /// @notice This function should be called to disable an E3 Program. /// @param e3Program The address of the E3 Program. - /// @return success True if the E3 Program was successfully disabled. - function disableE3Program( - IE3Program e3Program - ) external returns (bool success); + function disableE3Program(IE3Program e3Program) external; /// @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); + ) external; /// @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); + function disableEncryptionScheme(bytes32 encryptionSchemeId) external; /// @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); + function setE3ProgramsParams(bytes[] memory _e3ProgramsParams) external; //////////////////////////////////////////////////////////// // // @@ -293,4 +336,79 @@ interface IEnclave { /// @notice Returns the ERC20 token used to pay for E3 fees. function feeToken() external view returns (IERC20); + + /// @notice Returns the BondingRegistry contract. + function bondingRegistry() external view returns (IBondingRegistry); + + /// @notice Called by CiphernodeRegistry when committee is finalized (sortition complete). + /// @dev Updates E3 lifecycle to CommitteeFinalized stage, starts DKG deadline. + /// @param e3Id ID of the E3. + function onCommitteeFinalized(uint256 e3Id) external; + + /// @notice Called by CiphernodeRegistry when committee public key is published (DKG complete). + /// @dev Updates E3 lifecycle to KeyPublished stage. + /// @param e3Id ID of the E3. + function onCommitteePublished(uint256 e3Id) external; + + /// @notice Called by authorized contracts to mark an E3 as failed with a specific reason. + /// @dev Updates E3 lifecycle to Failed stage with the given reason. + /// @param e3Id ID of the E3. + /// @param reason The failure reason from FailureReason enum. + function onE3Failed(uint256 e3Id, uint8 reason) external; + + //////////////////////////////////////////////////////////// + // // + // Lifecycle Functions // + // // + //////////////////////////////////////////////////////////// + + /// @notice Anyone can mark an E3 as failed if timeout passed + /// @param e3Id The E3 ID + /// @return reason The failure reason + function markE3Failed(uint256 e3Id) external returns (FailureReason reason); + + /// @notice Check if E3 can be marked as failed + /// @param e3Id The E3 ID + /// @return canFail Whether failure condition is met + /// @return reason The failure reason if applicable + function checkFailureCondition( + uint256 e3Id + ) external view returns (bool canFail, FailureReason reason); + + /// @notice Get current stage of an E3 + /// @param e3Id The E3 ID + /// @return stage The current stage + function getE3Stage(uint256 e3Id) external view returns (E3Stage stage); + + /// @notice Get failure reason for an E3 + /// @param e3Id The E3 ID + /// @return reason The failure reason + function getFailureReason( + uint256 e3Id + ) external view returns (FailureReason reason); + + /// @notice Get requester address for an E3 + /// @param e3Id The E3 ID + /// @return requester The requester address + function getRequester( + uint256 e3Id + ) external view returns (address requester); + + /// @notice Get deadlines for an E3 + /// @param e3Id The E3 ID + /// @return deadlines The E3 deadlines + function getDeadlines( + uint256 e3Id + ) external view returns (E3Deadlines memory deadlines); + + /// @notice Get timeout configuration + /// @return config The current timeout config + function getTimeoutConfig() + external + view + returns (E3TimeoutConfig memory config); + + /// @notice Set timeout configuration + /// @param config The new timeout config + function setTimeoutConfig(E3TimeoutConfig calldata config) external; } diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 85345826ea..41006ce38b 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -84,6 +84,9 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @dev Default 8000 = 80%. Allows operators to unbond up to 20% while remaining active uint256 public licenseActiveBps; + /// @notice Number of currently active operators + uint256 public numActiveOperators; + /// @notice Operator state data structure /// @param licenseBond Amount of license tokens currently bonded /// @param exitUnlocksAt Timestamp when pending exit can be claimed @@ -725,6 +728,12 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { if (op.active != newActiveStatus) { op.active = newActiveStatus; + if (newActiveStatus) { + numActiveOperators++; + } else { + numActiveOperators--; + } + emit OperatorActivationChanged(operator, newActiveStatus); } } diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 70dc6dc408..3d37836ab3 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -7,6 +7,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; +import { IEnclave } from "../interfaces/IEnclave.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -40,10 +41,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { //////////////////////////////////////////////////////////// /// @notice Address of the Enclave contract authorized to request committees - address public enclave; + IEnclave public enclave; /// @notice Address of the bonding registry for checking node eligibility - address public bondingRegistry; + IBondingRegistry public bondingRegistry; /// @notice Current number of registered ciphernodes uint256 public numCiphernodes; @@ -89,8 +90,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Submission Window has been closed for this E3 error SubmissionWindowClosed(); - /// @notice Submission deadline has been reached for this E3 - error SubmissionDeadlineReached(); + /// @notice Committee deadline has been reached for this E3 + error CommitteeDeadlineReached(); /// @notice Committee has already been finalized for this E3 error CommitteeAlreadyFinalized(); @@ -142,6 +143,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Caller is not authorized error Unauthorized(); + /// @notice Not enough registered ciphernodes to meet threshold + /// @param requested The requested committee size (N) + /// @param available The number of registered ciphernodes + error InsufficientCiphernodes(uint256 requested, uint256 available); + //////////////////////////////////////////////////////////// // // // Modifiers // @@ -150,20 +156,20 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @dev Restricts function access to only the Enclave contract modifier onlyEnclave() { - require(msg.sender == enclave, OnlyEnclave()); + require(msg.sender == address(enclave), OnlyEnclave()); _; } /// @dev Restricts function access to only the bonding registry modifier onlyBondingRegistry() { - require(msg.sender == bondingRegistry, OnlyBondingRegistry()); + require(msg.sender == address(bondingRegistry), OnlyBondingRegistry()); _; } /// @dev Restricts function access to owner or bonding registry modifier onlyOwnerOrBondingVault() { require( - msg.sender == owner() || msg.sender == bondingRegistry, + msg.sender == owner() || msg.sender == address(bondingRegistry), NotOwnerOrBondingRegistry() ); _; @@ -189,11 +195,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @param _submissionWindow The submission window for the E3 sortition in seconds function initialize( address _owner, - address _enclave, + IEnclave _enclave, uint256 _submissionWindow ) public initializer { require(_owner != address(0), ZeroAddress()); - require(_enclave != address(0), ZeroAddress()); __Ownable_init(msg.sender); setEnclave(_enclave); @@ -216,11 +221,17 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { Committee storage c = committees[e3Id]; require(!c.initialized, CommitteeAlreadyRequested()); + uint256 activeCount = bondingRegistry.numActiveOperators(); + require( + threshold[1] <= activeCount, + InsufficientCiphernodes(threshold[1], activeCount) + ); + c.initialized = true; c.finalized = false; c.seed = seed; c.requestBlock = block.number; - c.submissionDeadline = block.timestamp + sortitionSubmissionWindow; + c.committeeDeadline = block.timestamp + sortitionSubmissionWindow; c.threshold = threshold; roots[e3Id] = root(); @@ -229,7 +240,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { seed, threshold, c.requestBlock, - c.submissionDeadline + c.committeeDeadline ); success = true; } @@ -257,6 +268,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // TODO: Need a Proof that the public key is generated from the committee c.publicKey = publicKeyHash; publicKeyHashes[e3Id] = publicKeyHash; + // Progress E3 to KeyPublished stage + enclave.onCommitteePublished(e3Id); emit CommitteePublished(e3Id, nodes, publicKey); } @@ -306,8 +319,8 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(c.initialized, CommitteeNotRequested()); require(!c.finalized, CommitteeAlreadyFinalized()); require( - block.timestamp <= c.submissionDeadline, - SubmissionDeadlineReached() + block.timestamp <= c.committeeDeadline, + CommitteeDeadlineReached() ); require(!c.submitted[msg.sender], NodeAlreadySubmitted()); require(isCiphernodeEligible(msg.sender), NodeNotEligible()); @@ -333,32 +346,38 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @notice Finalize the committee after submission window closes - /// @dev Can be called by anyone after the deadline. Reverts if not enough nodes submitted. + /// @dev Can be called by anyone after the deadline. If threshold not met, marks E3 as failed. /// @param e3Id ID of the E3 computation - function finalizeCommittee(uint256 e3Id) external { + /// @return success True if committee formed successfully, false if threshold not met + function finalizeCommittee(uint256 e3Id) external returns (bool success) { Committee storage c = committees[e3Id]; require(c.initialized, CommitteeNotRequested()); require(!c.finalized, CommitteeAlreadyFinalized()); require( - block.timestamp >= c.submissionDeadline, + block.timestamp >= c.committeeDeadline, SubmissionWindowNotClosed() ); - // TODO: Handle what happens if the threshold is not met. - require(c.topNodes.length >= c.threshold[1], ThresholdNotMet()); - c.finalized = true; - c.committee = c.topNodes; + bool thresholdMet = c.topNodes.length >= c.threshold[1]; + + if (!thresholdMet) { + c.failed = true; + emit CommitteeFormationFailed( + e3Id, + c.topNodes.length, + c.threshold[1] + ); + enclave.onE3Failed( + e3Id, + uint8(IEnclave.FailureReason.InsufficientCommitteeMembers) + ); + return false; + } + c.committee = c.topNodes; + enclave.onCommitteeFinalized(e3Id); 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; + return true; } //////////////////////////////////////////////////////////// @@ -370,19 +389,21 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @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()); + function setEnclave(IEnclave _enclave) public onlyOwner { + require(address(_enclave) != address(0), ZeroAddress()); enclave = _enclave; - emit EnclaveSet(_enclave); + emit EnclaveSet(address(_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()); + function setBondingRegistry( + IBondingRegistry _bondingRegistry + ) public onlyOwner { + require(address(_bondingRegistry) != address(0), ZeroAddress()); bondingRegistry = _bondingRegistry; - emit BondingRegistrySet(_bondingRegistry); + emit BondingRegistrySet(address(_bondingRegistry)); } /// @inheritdoc ICiphernodeRegistry @@ -399,6 +420,15 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// + /// @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.committeeDeadline; + } + /// @inheritdoc ICiphernodeRegistry function committeePublicKey( uint256 e3Id @@ -411,8 +441,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { function isCiphernodeEligible(address node) public view returns (bool) { if (!isEnabled(node)) return false; - require(bondingRegistry != address(0), BondingRegistryNotSet()); - return IBondingRegistry(bondingRegistry).isActive(node); + require( + address(bondingRegistry) != address(0), + BondingRegistryNotSet() + ); + return bondingRegistry.isActive(node); } /// @inheritdoc ICiphernodeRegistry @@ -451,7 +484,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Returns the address of the bonding registry /// @return Address of the bonding registry contract function getBondingRegistry() external view returns (address) { - return bondingRegistry; + return address(bondingRegistry); + } + + /// @inheritdoc ICiphernodeRegistry + function getCommitteeDeadline( + uint256 e3Id + ) external view returns (uint256) { + Committee storage c = committees[e3Id]; + require(c.initialized, CommitteeNotRequested()); + return c.committeeDeadline; } //////////////////////////////////////////////////////////// @@ -489,15 +531,20 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { uint256 e3Id ) internal view { require(ticketNumber > 0, InvalidTicketNumber()); - require(bondingRegistry != address(0), BondingRegistryNotSet()); + require( + address(bondingRegistry) != address(0), + BondingRegistryNotSet() + ); Committee storage c = committees[e3Id]; // @todo Ensure we check everywhere that we use the block before the request block // to ensure cases where everything is done in the same block are handled correctly. - uint256 ticketBalance = IBondingRegistry(bondingRegistry) - .getTicketBalanceAtBlock(node, c.requestBlock - 1); - uint256 ticketPrice = IBondingRegistry(bondingRegistry).ticketPrice(); + uint256 ticketBalance = bondingRegistry.getTicketBalanceAtBlock( + node, + c.requestBlock - 1 + ); + uint256 ticketPrice = bondingRegistry.ticketPrice(); require(ticketPrice > 0, InvalidTicketNumber()); uint256 availableTickets = ticketBalance / ticketPrice; diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index 8d2a19ce53..db92a079df 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -6,6 +6,8 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { IEnclave } from "../interfaces/IEnclave.sol"; +import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { function requestCommittee( @@ -16,6 +18,10 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { success = true; } + function getCommitteeDeadline(uint256) external view returns (uint256) { + return block.timestamp + 10; + } + function isEnabled(address) external pure returns (bool) { return true; } @@ -69,16 +75,18 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { } // solhint-disable-next-line no-empty-blocks - function setEnclave(address) external pure {} + function setEnclave(IEnclave) external pure {} // solhint-disable-next-line no-empty-blocks - function setBondingRegistry(address) external pure {} + function setBondingRegistry(IBondingRegistry) 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 finalizeCommittee(uint256) external pure returns (bool) { + return true; + } // solhint-disable-next-line no-empty-blocks function setSortitionSubmissionWindow(uint256) external pure {} @@ -99,6 +107,10 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { success = true; } + function getCommitteeDeadline(uint256) external view returns (uint256) { + return block.timestamp + 10; + } + function isEnabled(address) external pure returns (bool) { return true; } @@ -148,10 +160,10 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { } // solhint-disable-next-line no-empty-blocks - function setEnclave(address) external pure {} + function setEnclave(IEnclave) external pure {} // solhint-disable-next-line no-empty-blocks - function setBondingRegistry(address) external pure {} + function setBondingRegistry(IBondingRegistry) external pure {} // solhint-disable-next-line no-empty-blocks function setSortitionSubmissionWindow(uint256) external pure {} @@ -160,7 +172,9 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function submitTicket(uint256, uint256) external pure {} // solhint-disable-next-line no-empty-blocks - function finalizeCommittee(uint256) external pure {} + function finalizeCommittee(uint256) external pure returns (bool) { + return true; + } function isOpen(uint256) external pure returns (bool) { return false; diff --git a/packages/enclave-contracts/contracts/test/MockE3Program.sol b/packages/enclave-contracts/contracts/test/MockE3Program.sol index b372801586..f356281d11 100644 --- a/packages/enclave-contracts/contracts/test/MockE3Program.sol +++ b/packages/enclave-contracts/contracts/test/MockE3Program.sol @@ -10,13 +10,12 @@ import { IE3Program } from "../interfaces/IE3Program.sol"; contract MockE3Program is IE3Program { error InvalidParams(bytes e3ProgramParams, bytes computeProviderParams); error E3AlreadyInitialized(); + error InvalidInput(); bytes32 public constant ENCRYPTION_SCHEME_ID = keccak256("fhe.rs:BFV"); mapping(uint256 e3Id => bytes32 paramsHash) public paramsHashes; - error InvalidInput(); - function validate( uint256 e3Id, uint256, @@ -34,12 +33,8 @@ contract MockE3Program is IE3Program { return ENCRYPTION_SCHEME_ID; } - function validateInput( - uint256, - address sender, - bytes memory data - ) external pure { - if (data.length == 3 || sender == address(0)) { + function publishInput(uint256, bytes memory data) external pure { + if (data.length == 3) { revert InvalidInput(); } } diff --git a/packages/enclave-contracts/hardhat.config.ts b/packages/enclave-contracts/hardhat.config.ts index fda3e1daf7..f375888ed1 100644 --- a/packages/enclave-contracts/hardhat.config.ts +++ b/packages/enclave-contracts/hardhat.config.ts @@ -21,14 +21,13 @@ import { updateSubmissionWindow, } from "./tasks/ciphernode"; import { - activateE3, enableE3, publishCiphertext, publishCommittee, - publishInput, publishPlaintext, requestCommittee, } from "./tasks/enclave"; +import { publishInput } from "./tasks/program"; import { cleanDeploymentsTask } from "./tasks/utils"; dotenv.config(); @@ -97,9 +96,8 @@ const config: HardhatUserConfig = { requestCommittee, publishPlaintext, publishCiphertext, - publishInput, - activateE3, publishCommittee, + publishInput, enableE3, cleanDeploymentsTask, updateSubmissionWindow, diff --git a/packages/enclave-contracts/ignition/modules/e3RefundManager.ts b/packages/enclave-contracts/ignition/modules/e3RefundManager.ts new file mode 100644 index 0000000000..e391ae4478 --- /dev/null +++ b/packages/enclave-contracts/ignition/modules/e3RefundManager.ts @@ -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. +import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; + +export default buildModule("E3RefundManager", (m) => { + const owner = m.getParameter("owner"); + const enclave = m.getParameter("enclave"); + const treasury = m.getParameter("treasury"); + + const e3RefundManagerImpl = m.contract("E3RefundManager", []); + + const initData = m.encodeFunctionCall(e3RefundManagerImpl, "initialize", [ + owner, + enclave, + treasury, + ]); + + const e3RefundManager = m.contract("TransparentUpgradeableProxy", [ + e3RefundManagerImpl, + owner, + initData, + ]); + + return { e3RefundManager }; +}) as any; diff --git a/packages/enclave-contracts/ignition/modules/enclave.ts b/packages/enclave-contracts/ignition/modules/enclave.ts index 330c790dfc..9f216e7544 100644 --- a/packages/enclave-contracts/ignition/modules/enclave.ts +++ b/packages/enclave-contracts/ignition/modules/enclave.ts @@ -11,7 +11,15 @@ export default buildModule("Enclave", (m) => { const maxDuration = m.getParameter("maxDuration"); const registry = m.getParameter("registry"); const bondingRegistry = m.getParameter("bondingRegistry"); + const e3RefundManager = m.getParameter("e3RefundManager"); const feeToken = m.getParameter("feeToken"); + const timeoutConfig = m.getParameter("timeoutConfig", { + committeeFormationWindow: 3600, + dkgWindow: 7200, + computeWindow: 86400, + decryptionWindow: 3600, + gracePeriod: 600, + }); const enclaveImpl = m.contract("Enclave", []); @@ -19,8 +27,10 @@ export default buildModule("Enclave", (m) => { owner, registry, bondingRegistry, + e3RefundManager, feeToken, maxDuration, + timeoutConfig, [params], ]); diff --git a/packages/enclave-contracts/package.json b/packages/enclave-contracts/package.json index 1c2f1d81f9..26c3dc2209 100644 --- a/packages/enclave-contracts/package.json +++ b/packages/enclave-contracts/package.json @@ -24,6 +24,16 @@ "default": "./dist/tasks/ciphernode.js" } }, + "./tasks/program": { + "import": { + "types": "./dist/tasks/program.d.ts", + "default": "./dist/tasks/program.js" + }, + "require": { + "types": "./dist/tasks/program.d.ts", + "default": "./dist/tasks/program.js" + } + }, "./tasks/enclave": { "import": { "types": "./dist/tasks/enclave.d.ts", @@ -166,7 +176,10 @@ "test": "hardhat test mocha", "test:report-gas": "REPORT_GAS=true hardhat test mocha", "test:enclave": "pnpm run test test/Enclave.spec.ts", - "test:ciphernodeRegistry": "pnpm run test test/CiphernodeRegistry/CiphernodeRegistryOwnable.spec.ts", + "test:ciphernodeRegistry": "pnpm run test test/Registry/CiphernodeRegistryOwnable.spec.ts", + "test:bondingRegistry": "pnpm run test test/Registry/BondingRegistry.spec.ts", + "test:integration": "pnpm run test test/E3Lifecycle/E3Integration.spec.ts", + "test:slashing": "pnpm run test test/Slashing/SlashingManager.spec.ts", "prerelease": "pnpm clean && pnpm compile && pnpm typechain", "release": "pnpm publish", "verify:contracts": "hardhat run scripts/runVerification.ts", diff --git a/packages/enclave-contracts/scripts/deployAndSave/e3RefundManager.ts b/packages/enclave-contracts/scripts/deployAndSave/e3RefundManager.ts new file mode 100644 index 0000000000..34c5d03a1e --- /dev/null +++ b/packages/enclave-contracts/scripts/deployAndSave/e3RefundManager.ts @@ -0,0 +1,114 @@ +// 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 { + E3RefundManager, + E3RefundManager__factory as E3RefundManagerFactory, +} from "../../types"; +import { getProxyAdmin } from "../proxy"; +import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; + +/** + * The arguments for the deployAndSaveE3RefundManager function + */ +export interface E3RefundManagerArgs { + owner?: string; + enclave?: string; + treasury?: string; + hre: HardhatRuntimeEnvironment; +} + +/** + * Deploys the E3RefundManager contract and saves the deployment arguments + * @param param0 - The deployment arguments + * @returns The deployed E3RefundManager contract + */ +export const deployAndSaveE3RefundManager = async ({ + owner, + enclave, + treasury, + hre, +}: E3RefundManagerArgs): Promise<{ e3RefundManager: E3RefundManager }> => { + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + const chain = hre.globalOptions.network; + + const preDeployedArgs = readDeploymentArgs("E3RefundManager", chain); + + if ( + !owner || + !enclave || + !treasury || + (preDeployedArgs?.constructorArgs?.owner === owner && + preDeployedArgs?.constructorArgs?.enclave === enclave && + preDeployedArgs?.constructorArgs?.treasury === treasury) + ) { + if (!preDeployedArgs?.address) { + throw new Error( + "E3RefundManager address not found, it must be deployed first", + ); + } + const e3RefundManagerContract = E3RefundManagerFactory.connect( + preDeployedArgs.address, + signer, + ); + return { e3RefundManager: e3RefundManagerContract }; + } + + const e3RefundManagerFactory = await ethers.getContractFactory( + E3RefundManagerFactory.abi, + E3RefundManagerFactory.bytecode, + signer, + ); + const e3RefundManager = await e3RefundManagerFactory.deploy(); + await e3RefundManager.waitForDeployment(); + + const blockNumber = await ethers.provider.getBlockNumber(); + const e3RefundManagerAddress = await e3RefundManager.getAddress(); + + const initData = e3RefundManagerFactory.interface.encodeFunctionData( + "initialize", + [owner, enclave, treasury], + ); + + const ProxyCF = await ethers.getContractFactory( + "TransparentUpgradeableProxy", + ); + const proxy = await ProxyCF.deploy(e3RefundManagerAddress, owner, initData); + await proxy.waitForDeployment(); + const proxyAddress = await proxy.getAddress(); + + const proxyAdminAddress = await getProxyAdmin(ethers.provider, proxyAddress); + + storeDeploymentArgs( + { + constructorArgs: { + owner, + enclave, + treasury, + }, + proxyRecords: { + initData, + initialOwner: owner, + proxyAddress, + proxyAdminAddress, + implementationAddress: e3RefundManagerAddress, + }, + blockNumber, + address: proxyAddress, + }, + "E3RefundManager", + chain, + ); + + const e3RefundManagerContract = E3RefundManagerFactory.connect( + proxyAddress, + signer, + ); + + return { e3RefundManager: e3RefundManagerContract }; +}; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index 139c79e303..ff60f4217c 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -13,6 +13,17 @@ import { storeDeploymentArgs, } from "../utils"; +/** + * Timeout configuration for E3 stages + */ +export interface E3TimeoutConfig { + committeeFormationWindow: number; + dkgWindow: number; + computeWindow: number; + decryptionWindow: number; + gracePeriod: number; +} + /** * The arguments for the deployAndSaveEnclave function */ @@ -22,7 +33,9 @@ export interface EnclaveArgs { maxDuration?: string; registry?: string; bondingRegistry?: string; + e3RefundManager?: string; feeToken?: string; + timeoutConfig?: E3TimeoutConfig; hre: HardhatRuntimeEnvironment; } @@ -37,7 +50,9 @@ export const deployAndSaveEnclave = async ({ maxDuration, registry, bondingRegistry, + e3RefundManager, feeToken, + timeoutConfig, hre, }: EnclaveArgs): Promise<{ enclave: Enclave }> => { const { ethers } = await hre.network.connect(); @@ -53,11 +68,14 @@ export const deployAndSaveEnclave = async ({ !maxDuration || !registry || !bondingRegistry || + !e3RefundManager || !feeToken || + !timeoutConfig || (preDeployedArgs?.constructorArgs?.owner === owner && preDeployedArgs?.constructorArgs?.maxDuration === maxDuration && preDeployedArgs?.constructorArgs?.registry === registry && preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.e3RefundManager === e3RefundManager && preDeployedArgs?.constructorArgs?.feeToken === feeToken && areArraysEqual( preDeployedArgs?.constructorArgs?.params as string[], @@ -85,8 +103,10 @@ export const deployAndSaveEnclave = async ({ owner, registry, bondingRegistry, + e3RefundManager, feeToken, maxDuration, + timeoutConfig, params, ]); @@ -105,8 +125,10 @@ export const deployAndSaveEnclave = async ({ owner, registry, bondingRegistry, + e3RefundManager, feeToken, maxDuration, + timeoutConfig: JSON.stringify(timeoutConfig), params, }, proxyRecords: { diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 70cce8721d..8ab8141586 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -8,6 +8,7 @@ import hre from "hardhat"; import { autoCleanForLocalhost } from "./cleanIgnitionState"; import { deployAndSaveBondingRegistry } from "./deployAndSave/bondingRegistry"; import { deployAndSaveCiphernodeRegistryOwnable } from "./deployAndSave/ciphernodeRegistryOwnable"; +import { deployAndSaveE3RefundManager } from "./deployAndSave/e3RefundManager"; import { deployAndSaveEnclave } from "./deployAndSave/enclave"; import { deployAndSaveEnclaveTicketToken } from "./deployAndSave/enclaveTicketToken"; import { deployAndSaveEnclaveToken } from "./deployAndSave/enclaveToken"; @@ -16,6 +17,17 @@ import { deployAndSavePoseidonT3 } from "./deployAndSave/poseidonT3"; import { deployAndSaveSlashingManager } from "./deployAndSave/slashingManager"; import { deployMocks } from "./deployMocks"; +/** + * Default timeout configuration (in seconds) + */ +const DEFAULT_TIMEOUT_CONFIG = { + committeeFormationWindow: 3600, + dkgWindow: 7200, + computeWindow: 86400, + decryptionWindow: 3600, + gracePeriod: 600, +}; + /** * Deploys the Enclave contracts */ @@ -128,12 +140,27 @@ export const deployEnclave = async (withMocks?: boolean) => { maxDuration: THIRTY_DAYS_IN_SECONDS.toString(), registry: ciphernodeRegistryAddress, bondingRegistry: bondingRegistryAddress, + e3RefundManager: addressOne, // placeholder, will be updated feeToken: feeTokenAddress, + timeoutConfig: DEFAULT_TIMEOUT_CONFIG, hre, }); const enclaveAddress = await enclave.getAddress(); console.log("Enclave deployed to:", enclaveAddress); + console.log("Deploying E3RefundManager..."); + const { e3RefundManager } = await deployAndSaveE3RefundManager({ + owner: ownerAddress, + enclave: enclaveAddress, + treasury: ownerAddress, // Protocol treasury + hre, + }); + const e3RefundManagerAddress = await e3RefundManager.getAddress(); + console.log("E3RefundManager deployed to:", e3RefundManagerAddress); + + console.log("Setting E3RefundManager in Enclave..."); + await enclave.setE3RefundManager(e3RefundManagerAddress); + /////////////////////////////////////////// // Configure cross-contract dependencies /////////////////////////////////////////// @@ -167,6 +194,8 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting Enclave as reward distributor in BondingRegistry..."); await bondingRegistry.setRewardDistributor(enclaveAddress); + // E3RefundManager already has correct enclave from deployment + if (shouldDeployMocks) { const { decryptionVerifierAddress, e3ProgramAddress } = await deployMocks(); @@ -206,6 +235,7 @@ export const deployEnclave = async (withMocks?: boolean) => { SlashingManager: ${slashingManagerAddress} BondingRegistry: ${bondingRegistryAddress} CiphernodeRegistry: ${ciphernodeRegistryAddress} + E3RefundManager: ${e3RefundManagerAddress} Enclave: ${enclaveAddress} ============================================ `); diff --git a/packages/enclave-contracts/tasks/enclave.ts b/packages/enclave-contracts/tasks/enclave.ts index 6a2717049f..7be1108a2c 100644 --- a/packages/enclave-contracts/tasks/enclave.ts +++ b/packages/enclave-contracts/tasks/enclave.ts @@ -3,7 +3,7 @@ // This file is provided WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { ZeroAddress, zeroPadValue } from "ethers"; +import { BigNumberish, ZeroAddress, zeroPadValue } from "ethers"; import fs from "fs"; import { task } from "hardhat/config"; import { ArgumentType } from "hardhat/types/arguments"; @@ -33,21 +33,15 @@ export const requestCommittee = task( type: ArgumentType.INT, }) .addOption({ - name: "windowStart", - description: "timestamp start of window for the E3 (default: now)", - defaultValue: Math.floor(Date.now() / 1000), + name: "inputWindowStart", + description: "start of input submission window (default: now + 300)", + defaultValue: Math.floor(Date.now() / 1000) + 300, type: ArgumentType.INT, }) .addOption({ - name: "windowEnd", - description: "timestamp end of window for the E3 (default: now + 1 day)", - defaultValue: Math.floor(Date.now() / 1000) + 86400, - type: ArgumentType.INT, - }) - .addOption({ - name: "duration", - description: "duration in seconds of the E3 (default: 1 day)", - defaultValue: 86400, + name: "inputWindowEnd", + description: "deadline for input submission (default: now + 2 days)", + defaultValue: Math.floor(Date.now() / 1000) + 86400 * 2, type: ArgumentType.INT, }) .addOption({ @@ -79,9 +73,8 @@ export const requestCommittee = task( { thresholdQuorum, thresholdTotal, - windowStart, - windowEnd, - duration, + inputWindowStart, + inputWindowEnd, e3Address, e3Params, computeParams, @@ -163,8 +156,10 @@ export const requestCommittee = task( const requestParams = { threshold: [thresholdQuorum, thresholdTotal] as [number, number], - startWindow: [windowStart, windowEnd] as [number, number], - duration: duration, + inputWindow: [inputWindowStart, inputWindowEnd] as [ + BigNumberish, + BigNumberish, + ], e3Program: e3Address === ZeroAddress ? mockE3ProgramArgs!.address : e3Address, e3ProgramParams, @@ -305,82 +300,6 @@ export const publishCommittee = task( })) .build(); -export const activateE3 = task("e3:activate", "Activate an E3 program") - .addOption({ - name: "e3Id", - description: "Id of the E3 program", - defaultValue: 0, - type: ArgumentType.INT, - }) - .setAction(async () => ({ - default: async ({ e3Id }, hre) => { - const { deployAndSaveEnclave } = await import( - "../scripts/deployAndSave/enclave" - ); - - const { enclave } = await deployAndSaveEnclave({ - hre, - }); - - const tx = await enclave.activate(e3Id); - - console.log("Activating E3 program... ", tx.hash); - await tx.wait(); - - console.log(`E3 program activated`); - }, - })) - .build(); - -export const publishInput = task( - "e3:publishInput", - "Publish input for an E3 program", -) - .addOption({ - name: "e3Id", - description: "Id of the E3 program", - defaultValue: 0, - type: ArgumentType.INT, - }) - .addOption({ - name: "data", - description: "data to publish", - defaultValue: "", - type: ArgumentType.STRING, - }) - .addOption({ - name: "dataFile", - description: "file containing data to publish", - defaultValue: "", - type: ArgumentType.STRING, - }) - .setAction(async () => ({ - default: async ({ e3Id, data, dataFile }, hre) => { - const { deployAndSaveEnclave } = await import( - "../scripts/deployAndSave/enclave" - ); - - const { enclave } = await deployAndSaveEnclave({ - hre, - }); - - let dataToSend = data; - - if (dataFile) { - const file = fs.readFileSync(dataFile); - dataToSend = file.toString(); - } - - const tx = await enclave.publishInput(e3Id, dataToSend); - - console.log("Publishing input... ", tx.hash); - await tx.wait(); - - console.log(`Input published`); - }, - })) - .build(); - export const publishCiphertext = task( "e3:publishCiphertext", "Publish ciphertext output for an E3 program", diff --git a/packages/enclave-contracts/tasks/program.ts b/packages/enclave-contracts/tasks/program.ts new file mode 100644 index 0000000000..b9104227ce --- /dev/null +++ b/packages/enclave-contracts/tasks/program.ts @@ -0,0 +1,73 @@ +// 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 { task } from "hardhat/config"; +import { ArgumentType } from "hardhat/types/arguments"; + +export const publishInput = task( + "e3-program:publishInput", + "Publish input for an E3 program", +) + .addOption({ + name: "e3Id", + description: "Id of the E3 program", + defaultValue: 0, + type: ArgumentType.INT, + }) + .addOption({ + name: "data", + description: "data to publish", + defaultValue: "", + type: ArgumentType.STRING, + }) + .addOption({ + name: "dataFile", + description: "file containing data to publish", + defaultValue: "", + type: ArgumentType.STRING, + }) + // MockProgram + .addOption({ + name: "programAddress", + description: "Address of the E3 program", + defaultValue: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + type: ArgumentType.STRING, + }) + .setAction(async () => ({ + default: async ({ e3Id, data, dataFile, programAddress }, hre) => { + const { deployAndSaveMockProgram } = await import( + "../scripts/deployAndSave/mockProgram" + ); + const { MockE3Program__factory } = await import("../types"); + + const { ethers } = await hre.network.connect(); + const [signer] = await ethers.getSigners(); + + let actualProgramAddress = programAddress; + if (programAddress === "") { + actualProgramAddress = await deployAndSaveMockProgram({ hre }).then( + ({ e3Program }) => e3Program.getAddress(), + ); + } + + const program = MockE3Program__factory.connect( + actualProgramAddress, + signer, + ); + + let dataToSend = data; + + if (dataFile) { + const file = fs.readFileSync(dataFile); + dataToSend = file.toString(); + } + + await program.publishInput(e3Id, dataToSend); + + console.log(`Input published`); + }, + })) + .build(); diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts new file mode 100644 index 0000000000..6b44dc185f --- /dev/null +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -0,0 +1,1250 @@ +// 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 type { Signer } from "ethers"; +import { network } from "hardhat"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; +import E3RefundManagerModule from "../../ignition/modules/e3RefundManager"; +import EnclaveModule from "../../ignition/modules/enclave"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockDecryptionVerifierModule from "../../ignition/modules/mockDecryptionVerifier"; +import MockE3ProgramModule from "../../ignition/modules/mockE3Program"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, + E3RefundManager__factory as E3RefundManagerFactory, + Enclave__factory as EnclaveFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockDecryptionVerifier__factory as MockDecryptionVerifierFactory, + MockE3Program__factory as MockE3ProgramFactory, + MockUSDC__factory as MockUSDCFactory, +} from "../../types"; + +const { ethers, ignition, networkHelpers } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +/** + * Integration tests for E3 Refund/Timeout Mechanism + * + * These tests verify the full integration between: + * - Enclave.sol (main coordinator with integrated lifecycle management) + * - E3RefundManager.sol (refund calculation and claiming) + * - CiphernodeRegistryOwnable.sol (committee management) + */ +describe("E3 Integration - Refund/Timeout Mechanism", function () { + // Time constants + const ONE_HOUR = 60 * 60; + const ONE_DAY = 24 * ONE_HOUR; + const THREE_DAYS = 3 * ONE_DAY; + const SEVEN_DAYS = 7 * ONE_DAY; + const THIRTY_DAYS = 30 * ONE_DAY; + const SORTITION_SUBMISSION_WINDOW = 10; + + const addressOne = "0x0000000000000000000000000000000000000001"; + + // Default timeout configuration + const defaultTimeoutConfig = { + committeeFormationWindow: ONE_DAY, + dkgWindow: ONE_DAY, + computeWindow: THREE_DAYS, + decryptionWindow: ONE_DAY, + gracePeriod: ONE_HOUR, + }; + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const polynomial_degree = ethers.toBigInt(2048); + const plaintext_modulus = ethers.toBigInt(1032193); + const moduli = [ethers.toBigInt("18014398492704769")]; + + const encodedE3ProgramParams = abiCoder.encode( + ["uint256", "uint256", "uint256[]"], + [polynomial_degree, plaintext_modulus, moduli], + ); + + const encryptionSchemeId = + "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; + + const setup = async () => { + const [owner, requester, treasury, operator1, operator2, computeProvider] = + await ethers.getSigners(); + + const ownerAddress = await owner.getAddress(); + const treasuryAddress = await treasury.getAddress(); + const requesterAddress = await requester.getAddress(); + + // Deploy USDC mock + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { + MockUSDC: { + initialSupply: 10000000, + }, + }, + }); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + + // Deploy ENCL token + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { + EnclaveToken: { + owner: ownerAddress, + }, + }, + }); + const enclToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + + // Deploy ticket token + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcToken.getAddress(), + registry: addressOne, + owner: ownerAddress, + }, + }, + }, + ); + + // Deploy slashing manager + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: addressOne, // Will be updated + }, + }, + }, + ); + + // Deploy bonding registry + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclToken.getAddress(), + registry: addressOne, // Will be updated + slashedFundsTreasury: treasuryAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 5, + exitDelay: SEVEN_DAYS, + }, + }, + }, + ); + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + + // Deploy Enclave (with addressOne as temp registry) + const enclaveContract = await ignition.deploy(EnclaveModule, { + parameters: { + Enclave: { + params: encodedE3ProgramParams, + owner: ownerAddress, + maxDuration: THIRTY_DAYS, + registry: addressOne, + e3RefundManager: addressOne, + bondingRegistry: await bondingRegistry.getAddress(), + feeToken: await usdcToken.getAddress(), + timeoutConfig: defaultTimeoutConfig, + }, + }, + }); + const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclave = EnclaveFactory.connect(enclaveAddress, owner); + + // Deploy CiphernodeRegistry + const ciphernodeRegistry = await ignition.deploy(CiphernodeRegistryModule, { + parameters: { + CiphernodeRegistry: { + enclaveAddress: enclaveAddress, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + }, + }, + }); + const ciphernodeRegistryAddress = + await ciphernodeRegistry.cipherNodeRegistry.getAddress(); + const registry = CiphernodeRegistryOwnableFactory.connect( + ciphernodeRegistryAddress, + owner, + ); + + // Deploy E3RefundManager + const e3RefundManagerContract = await ignition.deploy( + E3RefundManagerModule, + { + parameters: { + E3RefundManager: { + owner: ownerAddress, + enclave: enclaveAddress, + treasury: treasuryAddress, + }, + }, + }, + ); + const e3RefundManagerAddress = + await e3RefundManagerContract.e3RefundManager.getAddress(); + const e3RefundManager = E3RefundManagerFactory.connect( + e3RefundManagerAddress, + owner, + ); + + // Deploy mock E3 Program + const e3ProgramContract = await ignition.deploy(MockE3ProgramModule, { + parameters: { + MockE3Program: { + encryptionSchemeId: encryptionSchemeId, + }, + }, + }); + const e3Program = MockE3ProgramFactory.connect( + await e3ProgramContract.mockE3Program.getAddress(), + owner, + ); + + // Deploy mock decryption verifier + const decryptionVerifierContract = await ignition.deploy( + MockDecryptionVerifierModule, + ); + const decryptionVerifier = MockDecryptionVerifierFactory.connect( + await decryptionVerifierContract.mockDecryptionVerifier.getAddress(), + owner, + ); + + // Wire up all the contracts + await enclave.setCiphernodeRegistry(ciphernodeRegistryAddress); + await enclave.setE3RefundManager(e3RefundManagerAddress); + await enclave.enableE3Program(await e3Program.getAddress()); + await enclave.setDecryptionVerifier( + encryptionSchemeId, + await decryptionVerifier.getAddress(), + ); + + // Setup bonding registry connections + await bondingRegistry.setRewardDistributor(enclaveAddress); + await bondingRegistry.setRegistry(ciphernodeRegistryAddress); + await bondingRegistry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); + await slashingManagerContract.slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await registry.setBondingRegistry(await bondingRegistry.getAddress()); + + // Update ticket token registry + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistry.getAddress(), + ); + + // Mint tokens to requester + await usdcToken.mint(requesterAddress, ethers.parseUnits("10000", 6)); + // Mint tokens to refund manager for distribution tests + await usdcToken.mint(e3RefundManagerAddress, ethers.parseUnits("10000", 6)); + + // Helper to make E3 request + const makeRequest = async ( + signer: Signer = requester, + ): Promise<{ e3Id: number }> => { + const startTime = (await time.latest()) + 100; + + const requestParams = { + threshold: [2, 2] as [number, number], + inputWindow: [startTime + 100, startTime + ONE_DAY] as [number, number], + e3Program: await e3Program.getAddress(), + e3ProgramParams: encodedE3ProgramParams, + // computeProviderParams must be exactly 32 bytes for MockE3Program.validate + computeProviderParams: abiCoder.encode( + ["address"], + [await decryptionVerifier.getAddress()], + ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], + ), + }; + + const fee = await enclave.getE3Quote(requestParams); + await usdcToken.connect(signer).approve(enclaveAddress, fee); + + await enclave.connect(signer).request(requestParams); + + // Get e3Id from event (it's 0 for first request) + return { e3Id: 0 }; + }; + + async function setupOperator(operator: Signer) { + const operatorAddress = await operator.getAddress(); + + await enclToken.setTransferRestriction(false); + await enclToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await enclToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketTokenAddress = await bondingRegistry.ticketToken(); + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(ticketTokenAddress, ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + } + + return { + enclave, + e3RefundManager, + bondingRegistry, + registry, + usdcToken, + enclToken, + e3Program, + decryptionVerifier, + owner, + requester, + treasury, + operator1, + operator2, + computeProvider, + makeRequest, + setupOperator, + }; + }; + + describe("E3 Request with Lifecycle Integration", function () { + it("initializes E3 lifecycle when request is made", async function () { + const { + enclave, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Check that E3 lifecycle was initialized + const stage = await enclave.getE3Stage(0); + expect(stage).to.equal(1); // E3Stage.Requested + + // Check requester is tracked + const storedRequester = await enclave.getRequester(0); + expect(storedRequester).to.equal(await requester.getAddress()); + }); + }); + + describe("Committee Formed Integration", function () { + it("transitions to CommitteeFormed when publishCommittee is called", async function () { + const { + enclave, + registry, + makeRequest, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // Make a request first + await makeRequest(); + + // Verify stage is Requested + let stage = await enclave.getE3Stage(0); + expect(stage).to.equal(1); // E3Stage.Requested + + // Submit tickets for sortition + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + + // Fast forward past submission window + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + + // Finalize committee + await registry.finalizeCommittee(0); + + // Publish committee (this triggers onCommitteePublished -> onCommitteeFormed) + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + // Verify stage transitioned to KeyPublished (after publishCommittee which calls onKeyPublished) + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(3); // E3Stage.KeyPublished + + // Verify deadlines were set + const deadlines = await enclave.getDeadlines(0); + expect(deadlines.dkgDeadline).to.be.gt(0); + }); + + it("emits CommitteeFormed event when committee is published", async function () { + const { + enclave, + registry, + makeRequest, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // Make a request + await makeRequest(); + + // Complete sortition process + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + // Publish committee and expect CommitteeFormed event + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + + await expect( + registry.publishCommittee(0, nodes, publicKey, publicKeyHash), + ) + .to.emit(enclave, "CommitteeFormed") + .withArgs(0); + }); + }); + + describe("processE3Failure()", function () { + it("reverts if lifecycle is not a valid contract", async function () { + const { + enclave, + owner, + makeRequest, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Create a new enclave with addressOne as lifecycle placeholder (not a real contract) + const newEnclaveContract = await ignition.deploy(EnclaveModule, { + parameters: { + Enclave: { + params: encodedE3ProgramParams, + owner: await owner.getAddress(), + maxDuration: THIRTY_DAYS, + registry: await enclave.ciphernodeRegistry(), + bondingRegistry: await enclave.bondingRegistry(), + e3RefundManager: addressOne, + feeToken: await enclave.feeToken(), + }, + }, + }); + const newEnclave = EnclaveFactory.connect( + await newEnclaveContract.enclave.getAddress(), + owner, + ); + + // Calling processE3Failure with a placeholder lifecycle should revert + // (it will try to call getE3Stage on an EOA which will fail) + await expect(newEnclave.processE3Failure(0)).to.be.revert(ethers); + }); + + it("reverts if E3 not in failed state", async function () { + const { enclave, makeRequest, operator1, operator2, setupOperator } = + await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // E3 is in Requested state, not Failed + await expect(enclave.processE3Failure(0)).to.be.revertedWith( + "E3 not failed", + ); + }); + + it("processes failure and calculates refund for committee formation timeout", async function () { + const { + enclave, + e3RefundManager, + makeRequest, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Fast forward past committee formation deadline + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + + // Mark E3 as failed + await enclave.markE3Failed(0); + + const stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // E3Stage.Failed + + // Process the failure + await expect(enclave.processE3Failure(0)).to.emit( + enclave, + "E3FailureProcessed", + ); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(distribution.calculated).to.be.true; + expect(distribution.requesterAmount).to.be.gt(0); + }); + + it("allows requester to claim refund after failure processing", async function () { + const { + enclave, + e3RefundManager, + makeRequest, + requester, + usdcToken, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Get initial balance + const balanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + // Fast forward and fail E3 + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + await enclave.processE3Failure(0); + + // Claim refund + await e3RefundManager.connect(requester).claimRequesterRefund(0); + + const balanceAfter = await usdcToken.balanceOf( + await requester.getAddress(), + ); + expect(balanceAfter).to.be.gt(balanceBefore); + }); + + it("reverts if trying to process failure twice", async function () { + const { enclave, makeRequest, operator1, operator2, setupOperator } = + await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + await enclave.processE3Failure(0); + + // Second call should fail - payment already cleared + await expect(enclave.processE3Failure(0)).to.be.revertedWith( + "No payment to refund", + ); + }); + + it("reverts if requester tries to claim refund twice", async function () { + const { + enclave, + e3RefundManager, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + await enclave.processE3Failure(0); + + // First claim succeeds + await e3RefundManager.connect(requester).claimRequesterRefund(0); + + // Second claim should fail + await expect( + e3RefundManager.connect(requester).claimRequesterRefund(0), + ).to.be.revertedWithCustomError(e3RefundManager, "AlreadyClaimed"); + }); + + it("reverts if refund not yet calculated", async function () { + const { + e3RefundManager, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Try to claim before failure is processed + await expect( + e3RefundManager.connect(requester).claimRequesterRefund(0), + ).to.be.revertedWithCustomError(e3RefundManager, "RefundNotCalculated"); + }); + }); + + describe("E3RefundManager Initialization", function () { + it("correctly sets enclave address", async function () { + const { enclave, e3RefundManager } = await loadFixture(setup); + + expect(await e3RefundManager.enclave()).to.equal( + await enclave.getAddress(), + ); + }); + }); + + describe("Full Failure Flow - Committee Formation Timeout", function () { + it("complete flow: request -> timeout -> fail -> process -> claim", async function () { + const { + enclave, + e3RefundManager, + makeRequest, + requester, + usdcToken, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Make request + await makeRequest(); + + // Verify stage + let stage = await enclave.getE3Stage(0); + expect(stage).to.equal(1); // Requested + + // 2. Fast forward past deadline + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + + // 3. Anyone can mark as failed + const [canFail, reason] = await enclave.checkFailureCondition(0); + expect(canFail).to.be.true; + expect(reason).to.equal(1); // CommitteeFormationTimeout + + await enclave.markE3Failed(0); + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // Failed + + // 4. Process failure + await enclave.processE3Failure(0); + + // 5. Requester claims refund + const balanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + await e3RefundManager.connect(requester).claimRequesterRefund(0); + + const balanceAfter = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(balanceAfter - balanceBefore).to.equal( + distribution.requesterAmount, + ); + }); + }); + + describe("Slashed Funds Routing", function () { + it("routes slashed funds 50/50 to requester and honest nodes", async function () { + const { + enclave, + e3RefundManager, + makeRequest, + owner, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest(); + + // Fail the E3 + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + await enclave.processE3Failure(0); + + const distributionBefore = await e3RefundManager.getRefundDistribution(0); + const slashedAmount = ethers.parseUnits("100", 6); + + // Route slashed funds (normally called by SlashingManager through Enclave) + // For testing, temporarily set enclave to owner to call this permissioned function + const originalEnclave = await e3RefundManager.enclave(); + await e3RefundManager.setEnclave(await owner.getAddress()); + await e3RefundManager.connect(owner).routeSlashedFunds(0, slashedAmount); + await e3RefundManager.setEnclave(originalEnclave); + + const distributionAfter = await e3RefundManager.getRefundDistribution(0); + + // Verify slashed funds are split 50/50 between requester and honest nodes + expect(distributionAfter.requesterAmount).to.equal( + distributionBefore.requesterAmount + slashedAmount / 2n, + ); + expect(distributionAfter.honestNodeAmount).to.equal( + distributionBefore.honestNodeAmount + slashedAmount / 2n, + ); + expect(distributionAfter.totalSlashed).to.equal(slashedAmount); + }); + }); + + describe("Full Failure Flow - DKG Timeout", function () { + it("complete flow: request -> committee formed -> DKG timeout -> fail -> process -> claim", async function () { + const { + enclave, + e3RefundManager, + registry, + usdcToken, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Make request + await makeRequest(); + let stage = await enclave.getE3Stage(0); + expect(stage).to.equal(1); // Requested + + // 2. Complete sortition (committee finalized, DKG starts) + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(2); // CommitteeFinalized + + // 3. Fast forward past DKG deadline (key never published - simulating DKG failure) + await time.increase(defaultTimeoutConfig.dkgWindow + 1); + + // 4. Check failure condition and mark as failed + const [canFail, reason] = await enclave.checkFailureCondition(0); + expect(canFail).to.be.true; + expect(reason).to.equal(3); // DKGTimeout + + await enclave.markE3Failed(0); + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // Failed + + const failureReason = await enclave.getFailureReason(0); + expect(failureReason).to.equal(3); // DKGTimeout + + // 5. Process failure and claim refund + await enclave.processE3Failure(0); + + const balanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + await e3RefundManager.connect(requester).claimRequesterRefund(0); + const balanceAfter = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(balanceAfter - balanceBefore).to.equal( + distribution.requesterAmount, + ); + }); + }); + + describe("Full Failure Flow - Compute Timeout", function () { + it("complete flow: request -> activated -> compute timeout -> fail -> process -> claim", async function () { + const { + enclave, + e3RefundManager, + registry, + usdcToken, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Make request + await makeRequest(); + let stage = await enclave.getE3Stage(0); + expect(stage).to.equal(1); // Requested + + // 2. Complete sortition and DKG + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(3); // KeyPublished + + // 3. Wait past compute deadline (ciphertext never published) + const e3 = await enclave.getE3(0); + const computeDeadline = + Number(e3.inputWindow[1]) + defaultTimeoutConfig.computeWindow; + await time.increaseTo(computeDeadline + 1); + + // 4. Check failure condition and mark as failed + const [canFail, reason] = await enclave.checkFailureCondition(0); + expect(canFail).to.be.true; + expect(reason).to.equal(6); // ComputeTimeout + + await enclave.markE3Failed(0); + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // Failed + + const failureReason = await enclave.getFailureReason(0); + expect(failureReason).to.equal(6); // ComputeTimeout + + // 5. Process and claim + await enclave.processE3Failure(0); + + const balanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + await e3RefundManager.connect(requester).claimRequesterRefund(0); + const balanceAfter = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(balanceAfter - balanceBefore).to.equal( + distribution.requesterAmount, + ); + }); + }); + + describe("Full Failure Flow - Decryption Timeout", function () { + it("complete flow: request -> ciphertext published -> decryption timeout -> fail -> process -> claim", async function () { + const { + enclave, + e3RefundManager, + registry, + usdcToken, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Make request + await makeRequest(); + let stage = await enclave.getE3Stage(0); + expect(stage).to.equal(1); // Requested + + // 2. Complete sortition and DKG + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(3); // KeyPublished + + // 3. Publish ciphertext output + const e3 = await enclave.getE3(0); + await time.increaseTo(Number(e3.inputWindow[1])); + + const ciphertextOutput = "0x" + "ab".repeat(100); + const proof = "0x1337"; + await enclave.publishCiphertextOutput(0, ciphertextOutput, proof); + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(4); // CiphertextReady + + // 4. Wait past decryption deadline (plaintext never published) + await time.increase(defaultTimeoutConfig.decryptionWindow + 1); + + // 5. Check failure condition and mark as failed + const [canFail, reason] = await enclave.checkFailureCondition(0); + expect(canFail).to.be.true; + expect(reason).to.equal(10); // DecryptionTimeout + + await enclave.markE3Failed(0); + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // Failed + + const failureReason = await enclave.getFailureReason(0); + expect(failureReason).to.equal(10); // DecryptionTimeout + + // 6. Process failure and claim refund + await enclave.processE3Failure(0); + + const balanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + await e3RefundManager.connect(requester).claimRequesterRefund(0); + const balanceAfter = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + const distribution = await e3RefundManager.getRefundDistribution(0); + expect(balanceAfter - balanceBefore).to.equal( + distribution.requesterAmount, + ); + expect(distribution.requesterAmount).to.be.gt(0); + }); + }); + + describe("Multiple E3 Requests Isolation", function () { + it("tracks multiple E3s independently", async function () { + const { + enclave, + usdcToken, + requester, + e3Program, + decryptionVerifier, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + const enclaveAddress = await enclave.getAddress(); + + // Helper to make requests + const makeRequestN = async (n: number) => { + const startTime = (await time.latest()) + 100; + const requestParams = { + threshold: [2, 2] as [number, number], + inputWindow: [startTime, startTime + ONE_DAY] as [number, number], + e3Program: await e3Program.getAddress(), + e3ProgramParams: encodedE3ProgramParams, + computeProviderParams: abiCoder.encode( + ["address"], + [await decryptionVerifier.getAddress()], + ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], + ), + }; + const fee = await enclave.getE3Quote(requestParams); + await usdcToken.connect(requester).approve(enclaveAddress, fee); + await enclave.connect(requester).request(requestParams); + return n; + }; + + // Make 3 requests + await makeRequestN(0); + await makeRequestN(1); + await makeRequestN(2); + + // Verify all are in Requested stage + expect(await enclave.getE3Stage(0)).to.equal(1); + expect(await enclave.getE3Stage(1)).to.equal(1); + expect(await enclave.getE3Stage(2)).to.equal(1); + + // Fail E3 #0 by waiting past its deadline + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + + // E3 #0 is failed, but E3 #1 and #2 are still active + expect(await enclave.getE3Stage(0)).to.equal(6); // Failed + expect(await enclave.getE3Stage(1)).to.equal(1); // Still Requested + expect(await enclave.getE3Stage(2)).to.equal(1); // Still Requested + + // E3 #1 and #2 also can be failed now (their deadlines have also passed) + const [canFail1] = await enclave.checkFailureCondition(1); + const [canFail2] = await enclave.checkFailureCondition(2); + expect(canFail1).to.be.true; + expect(canFail2).to.be.true; + + // But they haven't auto-failed - must be explicitly marked + expect(await enclave.getE3Stage(1)).to.equal(1); + expect(await enclave.getE3Stage(2)).to.equal(1); + + // Now mark E3 #2 as failed (but not #1) + await enclave.markE3Failed(2); + expect(await enclave.getE3Stage(2)).to.equal(6); // Now Failed + expect(await enclave.getE3Stage(1)).to.equal(1); // Still Requested + + // Verify each E3 has independent failure reasons + expect(await enclave.getFailureReason(0)).to.equal(1); // CommitteeFormationTimeout + expect(await enclave.getFailureReason(2)).to.equal(1); // CommitteeFormationTimeout + }); + + it("allows claiming refunds for each failed E3 independently", async function () { + const { + enclave, + e3RefundManager, + usdcToken, + requester, + e3Program, + decryptionVerifier, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + const enclaveAddress = await enclave.getAddress(); + + // Make 2 requests + for (let i = 0; i < 2; i++) { + const startTime = (await time.latest()) + 100; + const requestParams = { + threshold: [2, 2] as [number, number], + inputWindow: [startTime, startTime + ONE_DAY] as [number, number], + e3Program: await e3Program.getAddress(), + e3ProgramParams: encodedE3ProgramParams, + computeProviderParams: abiCoder.encode( + ["address"], + [await decryptionVerifier.getAddress()], + ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], + ), + }; + const fee = await enclave.getE3Quote(requestParams); + await usdcToken.connect(requester).approve(enclaveAddress, fee); + await enclave.connect(requester).request(requestParams); + } + + // Fail both + await time.increase(defaultTimeoutConfig.committeeFormationWindow + 1); + await enclave.markE3Failed(0); + await enclave.markE3Failed(1); + + // Process both + await enclave.processE3Failure(0); + await enclave.processE3Failure(1); + + // Claim both refunds independently + const balanceBefore = await usdcToken.balanceOf( + await requester.getAddress(), + ); + + await e3RefundManager.connect(requester).claimRequesterRefund(0); + const balanceAfterFirst = await usdcToken.balanceOf( + await requester.getAddress(), + ); + expect(balanceAfterFirst).to.be.gt(balanceBefore); + + await e3RefundManager.connect(requester).claimRequesterRefund(1); + const balanceAfterSecond = await usdcToken.balanceOf( + await requester.getAddress(), + ); + expect(balanceAfterSecond).to.be.gt(balanceAfterFirst); + + // Verify can't claim twice + await expect( + e3RefundManager.connect(requester).claimRequesterRefund(0), + ).to.be.revertedWithCustomError(e3RefundManager, "AlreadyClaimed"); + }); + }); + + describe("Success Path (Complete E3)", function () { + it("transitions through all stages to completion", async function () { + const { + enclave, + registry, + makeRequest, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // 1. Make request + await makeRequest(); + expect(await enclave.getE3Stage(0)).to.equal(1); // Requested + + // 2. Complete sortition and publish committee (CommitteeFinalized -> KeyPublished) + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + expect(await enclave.getE3Stage(0)).to.equal(2); // CommitteeFinalized + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + expect(await enclave.getE3Stage(0)).to.equal(3); // KeyPublished + + // 3. Publish ciphertext output (after input deadline) + const e3 = await enclave.getE3(0); + await time.increaseTo(Number(e3.inputWindow[1])); + + const ciphertextOutput = "0x" + "ab".repeat(100); + const proof = "0x1337"; + await enclave.publishCiphertextOutput(0, ciphertextOutput, proof); + expect(await enclave.getE3Stage(0)).to.equal(4); // CiphertextReady + + // 4. Publish plaintext output + const plaintextOutput = "0x" + "cd".repeat(100); + await enclave.publishPlaintextOutput(0, plaintextOutput, proof); + expect(await enclave.getE3Stage(0)).to.equal(5); // Complete + + // Cannot mark completed E3 as failed + await expect(enclave.markE3Failed(0)).to.be.revertedWithCustomError( + enclave, + "E3AlreadyComplete", + ); + }); + + it("prevents refund claims for completed E3", async function () { + const { + enclave, + e3RefundManager, + registry, + makeRequest, + requester, + operator1, + operator2, + setupOperator, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + // Complete full E3 flow + await makeRequest(); + + // Complete sortition + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(0); + + const nodes = [ + await operator1.getAddress(), + await operator2.getAddress(), + ]; + const publicKey = "0x1234567890abcdef1234567890abcdef"; + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(0, nodes, publicKey, publicKeyHash); + + // Publish outputs + const e3 = await enclave.getE3(0); + await time.increaseTo(Number(e3.inputWindow[1])); + + const ciphertextOutput = "0x" + "ab".repeat(100); + const proof = "0x1337"; + await enclave.publishCiphertextOutput(0, ciphertextOutput, proof); + + const plaintextOutput = "0x" + "cd".repeat(100); + await enclave.publishPlaintextOutput(0, plaintextOutput, proof); + + // Verify E3 is complete + expect(await enclave.getE3Stage(0)).to.equal(5); // Complete + + await expect( + e3RefundManager.connect(requester).claimRequesterRefund(0), + ).to.be.revertedWithCustomError(e3RefundManager, "RefundNotCalculated"); + }); + }); +}); diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index a7c5e53fc4..4b6ac074e5 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -11,10 +11,10 @@ import { poseidon2 } from "poseidon-lite"; import BondingRegistryModule from "../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../ignition/modules/ciphernodeRegistry"; +import E3RefundManagerModule from "../ignition/modules/e3RefundManager"; import EnclaveModule from "../ignition/modules/enclave"; 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"; @@ -38,6 +38,16 @@ describe("Enclave", function () { const addressOne = "0x0000000000000000000000000000000000000001"; const AddressTwo = "0x0000000000000000000000000000000000000002"; + const timeoutConfig = { + committeeFormationWindow: 3600, // 1 hour + dkgWindow: 3600, // 1 hour + computeWindow: 3600, // 1 hour + decryptionWindow: 3600, // 1 hour + gracePeriod: 300, // 5 minutes + }; + + const inputWindowDuration = 300; + const encryptionSchemeId = "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; const newEncryptionSchemeId = @@ -209,13 +219,34 @@ describe("Enclave", function () { registry: addressOne, bondingRegistry: await bondingRegistryContract.bondingRegistry.getAddress(), + e3RefundManager: addressOne, // placeholder, will be updated feeToken: await usdcToken.getAddress(), + timeoutConfig, }, }, }); const enclaveAddress = await enclaveContract.enclave.getAddress(); + const e3RefundManagerContract = await ignition.deploy( + E3RefundManagerModule, + { + parameters: { + E3RefundManager: { + owner: ownerAddress, + enclave: enclaveAddress, + treasury: ownerAddress, + }, + }, + }, + ); + + const e3RefundManagerAddress = + await e3RefundManagerContract.e3RefundManager.getAddress(); + + const enclave = EnclaveFactory.connect(enclaveAddress, owner); + await enclave.setE3RefundManager(e3RefundManagerAddress); + const ciphernodeRegistry = await ignition.deploy(CiphernodeRegistryModule, { parameters: { CiphernodeRegistry: { @@ -229,7 +260,6 @@ describe("Enclave", function () { const ciphernodeRegistryAddress = await ciphernodeRegistry.cipherNodeRegistry.getAddress(); - const enclave = EnclaveFactory.connect(enclaveAddress, owner); const ciphernodeRegistryContract = CiphernodeRegistryOwnableFactory.connect( ciphernodeRegistryAddress, owner, @@ -311,11 +341,10 @@ describe("Enclave", function () { const request = { threshold: [2, 2] as [number, number], - startWindow: [await time.latest(), (await time.latest()) + 100] as [ - number, - number, - ], - duration: time.duration.days(30), + inputWindow: [ + (await time.latest()) + 10, + (await time.latest()) + inputWindowDuration, + ] as [number, number], e3Program: await e3Program.mockE3Program.getAddress(), e3ProgramParams: encodedE3ProgramParams, computeProviderParams: abiCoder.encode( @@ -392,11 +421,6 @@ describe("Enclave", function () { await enclave.setMaxDuration(1); expect(await enclave.maxDuration()).to.equal(1); }); - it("returns true if max duration is set successfully", async function () { - const { enclave } = await loadFixture(setup); - const result = await enclave.setMaxDuration.staticCall(1); - expect(result).to.be.true; - }); it("emits MaxDurationSet event", async function () { const { enclave } = await loadFixture(setup); await expect(enclave.setMaxDuration(1)) @@ -442,13 +466,6 @@ describe("Enclave", function () { expect(await enclave.ciphernodeRegistry()).to.equal(AddressTwo); }); - it("returns true if ciphernodeRegistry is set successfully", async function () { - const { enclave } = await loadFixture(setup); - - const result = await enclave.setCiphernodeRegistry.staticCall(AddressTwo); - expect(result).to.be.true; - }); - it("emits CiphernodeRegistrySet event", async function () { const { enclave } = await loadFixture(setup); @@ -481,15 +498,6 @@ describe("Enclave", function () { .true; }); - it("returns true if parameters are set successfully", async function () { - const { enclave } = await loadFixture(setup); - - const result = await enclave.setE3ProgramsParams.staticCall( - encodedE3ProgramsParams, - ); - expect(result).to.be.true; - }); - it("emits AllowedE3ProgramsParamsSet event", async function () { const { enclave } = await loadFixture(setup); @@ -526,8 +534,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -537,7 +544,8 @@ describe("Enclave", function () { const e3 = await enclave.getE3(0); expect(e3.threshold).to.deep.equal(request.threshold); - expect(e3.expiration).to.equal(0n); + expect(e3.inputWindow[0]).to.equal(request.inputWindow[0]); + expect(e3.inputWindow[1]).to.equal(request.inputWindow[1]); expect(e3.e3Program).to.equal(request.e3Program); expect(e3.e3ProgramParams).to.equal(request.e3ProgramParams); expect(e3.decryptionVerifier).to.equal( @@ -608,16 +616,6 @@ describe("Enclave", function () { ).to.equal(await mocks.decryptionVerifier.getAddress()); }); - it("returns true if decryption verifier is enabled successfully", async function () { - const { enclave, mocks } = await loadFixture(setup); - - const result = await enclave.setDecryptionVerifier.staticCall( - newEncryptionSchemeId, - await mocks.decryptionVerifier.getAddress(), - ); - expect(result).to.be.true; - }); - it("emits EncryptionSchemeEnabled", async function () { const { enclave, mocks } = await loadFixture(setup); @@ -659,13 +657,6 @@ describe("Enclave", function () { ethers.ZeroAddress, ); }); - it("returns true if encryption scheme is disabled successfully", async function () { - const { enclave } = await loadFixture(setup); - - const result = - await enclave.disableEncryptionScheme.staticCall(encryptionSchemeId); - expect(result).to.be.true; - }); it("emits EncryptionSchemeDisabled", async function () { const { enclave } = await loadFixture(setup); @@ -705,11 +696,6 @@ describe("Enclave", function () { const enabled = await enclave.e3Programs(e3Program); expect(enabled).to.be.true; }); - it("returns true if E3 Program is enabled successfully", async function () { - const { enclave } = await loadFixture(setup); - const result = await enclave.enableE3Program.staticCall(AddressTwo); - expect(result).to.be.true; - }); it("emits E3ProgramEnabled event", async function () { const { enclave } = await loadFixture(setup); await expect(enclave.enableE3Program(AddressTwo)) @@ -745,15 +731,6 @@ describe("Enclave", function () { const enabled = await enclave.e3Programs(e3Program); expect(enabled).to.be.false; }); - it("returns true if E3 Program is disabled successfully", async function () { - const { - enclave, - mocks: { e3Program }, - } = await loadFixture(setup); - const result = await enclave.disableE3Program.staticCall(e3Program); - - expect(result).to.be.true; - }); it("emits E3ProgramDisabled event", async function () { const { enclave, @@ -771,8 +748,7 @@ describe("Enclave", function () { await expect( enclave.request({ threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -784,8 +760,7 @@ describe("Enclave", function () { const { enclave, request, usdcToken } = await loadFixture(setup); const fee = await enclave.getE3Quote({ threshold: [0, 2], - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -795,8 +770,7 @@ describe("Enclave", function () { await expect( enclave.request({ threshold: [0, 2], - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -812,8 +786,7 @@ describe("Enclave", function () { await expect( makeRequest(enclave, usdcToken, { threshold: [3, 2], - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -823,48 +796,30 @@ describe("Enclave", function () { .to.be.revertedWithCustomError(enclave, "InvalidThreshold") .withArgs([3, 2]); }); - it("reverts if duration is 0", async function () { + it("reverts if total duration is greater than maxDuration", async function () { const { enclave, request, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { - threshold: request.threshold, - startWindow: request.startWindow, - duration: 0, + threshold: [2, 3], + inputWindow: [ + request.inputWindow[0], + request.inputWindow[1] + time.duration.days(31), + ], 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, usdcToken } = await loadFixture(setup); - - await expect( - 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)); + ).to.be.revertedWithCustomError(enclave, "InvalidDuration"); }); it("reverts if E3 Program is not enabled", async function () { const { enclave, request, usdcToken } = await loadFixture(setup); await expect( makeRequest(enclave, usdcToken, { - threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + threshold: [2, 3], + inputWindow: request.inputWindow, e3Program: ethers.ZeroAddress, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -880,8 +835,7 @@ describe("Enclave", function () { await expect( makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -896,8 +850,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -908,7 +861,8 @@ describe("Enclave", function () { const block = await ethers.provider.getBlock("latest").catch((e) => e); expect(e3.threshold).to.deep.equal(request.threshold); - expect(e3.expiration).to.equal(0n); + expect(e3.inputWindow[0]).to.equal(request.inputWindow[0]); + expect(e3.inputWindow[1]).to.equal(request.inputWindow[1]); expect(e3.e3Program).to.equal(request.e3Program); expect(e3.requestBlock).to.equal(block.number); expect(e3.decryptionVerifier).to.equal( @@ -922,8 +876,7 @@ describe("Enclave", function () { const { enclave, request, usdcToken } = await loadFixture(setup); const tx = await makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -937,228 +890,16 @@ describe("Enclave", function () { }); }); - describe("activate()", function () { + describe("publishCiphertextOutput()", function () { it("reverts if E3 does not exist", async function () { const { enclave } = await loadFixture(setup); - await expect(enclave.activate(0)) + await expect(enclave.publishCiphertextOutput(0, "0x", "0x")) .to.be.revertedWithCustomError(enclave, "E3DoesNotExist") .withArgs(0); }); - it("reverts if E3 has already been activated", async function () { - const { - enclave, - request, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = 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, - }); - - 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)).to.not.be.revert(ethers); - await expect(enclave.activate(0)) - .to.be.revertedWithCustomError(enclave, "E3AlreadyActivated") - .withArgs(0); - }); - it("reverts if E3 is not yet ready to start", async function () { - const { enclave, request, usdcToken } = await loadFixture(setup); - const startTime = [ - (await time.latest()) + 1000, - (await time.latest()) + 2000, - ] as [number, number]; - 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)).to.be.revertedWithCustomError( - enclave, - "E3NotReady", - ); - }); - it("reverts if E3 start has expired", async function () { - 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 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(e3Id)).to.be.revertedWithCustomError( - enclave, - "E3Expired", - ); - }); - it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request, usdcToken } = await loadFixture(setup); - const startTime = [ - (await time.latest()) + 1000, - (await time.latest()) + 2000, - ] as [number, number]; - - 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)).to.be.revertedWithCustomError( - enclave, - "E3NotReady", - ); - }); - it("reverts if E3 start has expired", async function () { - 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 time.increaseTo(currentTime + request.duration + 100); - - await expect(enclave.activate(e3Id)).to.be.revertedWithCustomError( - enclave, - "E3Expired", - ); - }); - it("reverts if ciphernodeRegistry does not return a public key", async function () { - const { enclave, request, usdcToken } = await loadFixture(setup); - - await makeRequest(enclave, usdcToken, request); - - const prevRegistry = await enclave.ciphernodeRegistry(); - - const reg = await ignition.deploy(MockCiphernodeRegistryEmptyKeyModule); - const nextRegistry = - await reg.mockCiphernodeRegistryEmptyKey.getAddress(); - - await enclave.setCiphernodeRegistry(nextRegistry); - - await expect(enclave.activate(0)).to.be.revertedWithCustomError( - reg.mockCiphernodeRegistryEmptyKey, - "CommitteeNotPublished", - ); - - await enclave.setCiphernodeRegistry(prevRegistry); - }); - - it("sets committeePublicKey correctly", async () => { - const { - enclave, - request, - ciphernodeRegistryContract, - usdcToken, - operator1, - operator2, - } = 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 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(publicKey); - - await enclave.activate(e3Id); - - e3 = await enclave.getE3(e3Id); - expect(e3.committeePublicKey).to.equal(publicKey); - }); - it("returns true if E3 is activated successfully", async () => { + it("reverts if output has already been published", async function () { const { enclave, request, @@ -1167,52 +908,17 @@ describe("Enclave", function () { operator1, operator2, } = 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 e3Id = 0; - await setupAndPublishCommittee( - ciphernodeRegistryContract, - e3Id, - [await operator1.getAddress(), await operator2.getAddress()], - data, - operator1, - operator2, - ); - - expect(await enclave.activate.staticCall(e3Id)).to.be.equal(true); - }); - it("emits E3Activated event", async () => { - const { - enclave, - request, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = await loadFixture(setup); - await makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: request.startWindow, - duration: request.duration, + inputWindow: request.inputWindow, e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, customParams: request.customParams, }); - const e3Id = 0; - await setupAndPublishCommittee( ciphernodeRegistryContract, e3Id, @@ -1221,168 +927,17 @@ describe("Enclave", function () { operator1, operator2, ); + await mine(2, { interval: inputWindowDuration }); - await expect(enclave.activate(e3Id)).to.emit(enclave, "E3Activated"); - }); - }); - - describe("publishInput()", function () { - it("reverts if E3 does not exist", async function () { - const { enclave } = await loadFixture(setup); - - await expect(enclave.publishInput(0, "0x")) - .to.be.revertedWithCustomError(enclave, "E3DoesNotExist") - .withArgs(0); - }); - - it("reverts if E3 has not been activated", async function () { - 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]); - - await expect(enclave.getE3(0)).to.not.be.revert(ethers); - await expect(enclave.publishInput(0, inputData)) - .to.be.revertedWithCustomError(enclave, "E3NotActivated") - .withArgs(0); - }); - - it("reverts if input is not valid", async function () { - const { - enclave, - request, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = 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, - }); - - await setupAndPublishCommittee( - ciphernodeRegistryContract, - 0, - [await operator1.getAddress(), await operator2.getAddress()], - data, - operator1, - operator2, - ); - await enclave.activate(0); - await expect( - enclave.publishInput(0, "0xaabbcc"), - ).to.be.revertedWithCustomError(enclave, "InvalidInput"); - }); - - it("reverts if outside of input window", async function () { - const { - enclave, - request, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = 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, - }); - - await setupAndPublishCommittee( - ciphernodeRegistryContract, - 0, - [await operator1.getAddress(), await operator2.getAddress()], - data, - operator1, - operator2, - ); - await enclave.activate(0); - - await mine(2, { interval: request.duration }); - - await expect( - enclave.publishInput(0, ethers.ZeroHash), - ).to.be.revertedWithCustomError(enclave, "InputDeadlinePassed"); - }); - - it("it allows publishing input to different requests", async function () { - const fixtureSetup = () => setup(); - - const { - enclave, - request, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = await loadFixture(fixtureSetup); - const inputData = "0x12345678"; - - 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); - await enclave.publishInput(0, inputData); - - 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, - [await operator1.getAddress(), await operator2.getAddress()], - data, - operator1, - operator2, - ); - await enclave.activate(1); - await enclave.publishInput(1, inputData); + await enclave.publishCiphertextOutput(e3Id, data, proof); + await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) + .to.be.revertedWithCustomError( + enclave, + "CiphertextOutputAlreadyPublished", + ) + .withArgs(e3Id); }); - it("returns true if input is published successfully", async function () { + it("reverts if committee duties are over", async function () { const { enclave, request, @@ -1391,74 +946,12 @@ describe("Enclave", function () { operator1, operator2, } = await loadFixture(setup); - const inputData = "0x12345678"; - - 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); - - expect(await enclave.publishInput.staticCall(0, inputData)).to.equal( - true, - ); - }); - }); - - describe("publishCiphertextOutput()", function () { - it("reverts if E3 does not exist", async function () { - const { enclave } = await loadFixture(setup); - - await expect(enclave.publishCiphertextOutput(0, "0x", "0x")) - .to.be.revertedWithCustomError(enclave, "E3DoesNotExist") - .withArgs(0); - }); - it("reverts if E3 has not been activated", async function () { - const { enclave, request, usdcToken } = await loadFixture(setup); const e3Id = 0; - 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, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = await loadFixture(setup); - const currentTime = await time.latest(); await makeRequest(enclave, usdcToken, { ...request, - startWindow: [currentTime, currentTime + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); - const e3Id = 0; await setupAndPublishCommittee( ciphernodeRegistryContract, @@ -1468,50 +961,12 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - - 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, - usdcToken, - ciphernodeRegistryContract, - operator1, - operator2, - } = await loadFixture(setup); - const e3Id = 0; - - 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 mine(2, { + interval: inputWindowDuration + timeoutConfig.computeWindow, }); - - await setupAndPublishCommittee( - ciphernodeRegistryContract, - e3Id, - [await operator1.getAddress(), await operator2.getAddress()], - data, - operator1, - operator2, - ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); - expect(await enclave.publishCiphertextOutput(e3Id, data, proof)); - await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) - .to.be.revertedWithCustomError( - enclave, - "CiphertextOutputAlreadyPublished", - ) - .withArgs(e3Id); + await expect( + enclave.publishCiphertextOutput(e3Id, data, proof), + ).to.be.revertedWithCustomError(enclave, "CommitteeDutiesCompleted"); }); it("reverts if output is not valid", async function () { const { @@ -1526,8 +981,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { threshold: request.threshold, - startWindow: [await time.latest(), (await time.latest()) + 100], - duration: request.duration, + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], e3Program: request.e3Program, e3ProgramParams: request.e3ProgramParams, computeProviderParams: request.computeProviderParams, @@ -1542,8 +996,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await expect( enclave.publishCiphertextOutput(e3Id, "0x", "0x"), ).to.be.revertedWithCustomError(enclave, "InvalidOutput"); @@ -1561,7 +1014,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1572,8 +1025,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); expect(await enclave.publishCiphertextOutput(e3Id, data, proof)); const e3 = await enclave.getE3(e3Id); expect(e3.ciphertextOutput).to.equal(ethers.keccak256(data)); @@ -1591,7 +1043,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1602,8 +1054,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); expect( await enclave.publishCiphertextOutput.staticCall(e3Id, data, proof), ).to.equal(true); @@ -1621,7 +1072,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1632,8 +1083,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) .to.emit(enclave, "CiphertextOutputPublished") .withArgs(e3Id, data); @@ -1650,18 +1100,6 @@ describe("Enclave", function () { .withArgs(e3Id); }); - it("reverts if E3 has not been activated", async function () { - const { enclave, request, usdcToken } = await loadFixture(setup); - const e3Id = 0; - - 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, @@ -1675,7 +1113,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1686,10 +1124,9 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) - .to.be.revertedWithCustomError(enclave, "CiphertextOutputNotPublished") - .withArgs(e3Id); + await expect( + enclave.publishPlaintextOutput(e3Id, data, "0x"), + ).to.be.revertedWithCustomError(enclave, "InvalidStage"); }); it("reverts if plaintextOutput has already been published", async function () { const { @@ -1704,7 +1141,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1715,16 +1152,12 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await enclave.publishPlaintextOutput(e3Id, data, proof); - await expect(enclave.publishPlaintextOutput(e3Id, data, proof)) - .to.be.revertedWithCustomError( - enclave, - "PlaintextOutputAlreadyPublished", - ) - .withArgs(e3Id); + await expect( + enclave.publishPlaintextOutput(e3Id, data, proof), + ).to.be.revertedWithCustomError(enclave, "InvalidStage"); }); it("reverts if output is not valid", async function () { const { @@ -1739,7 +1172,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1750,8 +1183,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(enclave.publishPlaintextOutput(e3Id, data, "0x")) .to.be.revertedWithCustomError(enclave, "InvalidOutput") @@ -1770,7 +1202,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1781,8 +1213,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await enclave.publishCiphertextOutput(e3Id, data, proof); expect(await enclave.publishPlaintextOutput(e3Id, data, proof)); @@ -1802,7 +1233,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1813,8 +1244,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await enclave.publishCiphertextOutput(e3Id, data, proof); expect( await enclave.publishPlaintextOutput.staticCall(e3Id, data, proof), @@ -1833,7 +1263,7 @@ describe("Enclave", function () { await makeRequest(enclave, usdcToken, { ...request, - startWindow: [await time.latest(), (await time.latest()) + 100], + inputWindow: [(await time.latest()) + 20, (await time.latest()) + 100], }); await setupAndPublishCommittee( @@ -1844,8 +1274,7 @@ describe("Enclave", function () { operator1, operator2, ); - await enclave.activate(e3Id); - await mine(2, { interval: request.duration }); + await mine(2, { interval: inputWindowDuration }); await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(await enclave.publishPlaintextOutput(e3Id, data, proof)) .to.emit(enclave, "PlaintextOutputPublished") diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 98b70fb739..9a2db8f877 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -11,13 +11,18 @@ import { poseidon2 } from "poseidon-lite"; import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; +import E3RefundManagerModule from "../../ignition/modules/e3RefundManager"; +import EnclaveModule from "../../ignition/modules/enclave"; import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockDecryptionVerifierModule from "../../ignition/modules/mockDecryptionVerifier"; +import MockE3ProgramModule from "../../ignition/modules/mockE3Program"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { BondingRegistry__factory as BondingRegistryFactory, CiphernodeRegistryOwnable__factory as CiphernodeRegistryFactory, + Enclave__factory as EnclaveFactory, } from "../../types"; const AddressOne = "0x0000000000000000000000000000000000000001"; @@ -82,6 +87,17 @@ describe("CiphernodeRegistryOwnable", function () { await ethers.getSigners(); const ownerAddress = await owner.getAddress(); + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const polynomial_degree = ethers.toBigInt(2048); + const plaintext_modulus = ethers.toBigInt(1032193); + const moduli = [ethers.toBigInt("18014398492704769")]; + const encodedE3ProgramParams = abiCoder.encode( + ["uint256", "uint256", "uint256[]"], + [polynomial_degree, plaintext_modulus, moduli], + ); + const encryptionSchemeId = + "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; + const usdcContract = await ignition.deploy(MockStableTokenModule, { parameters: { MockUSDC: { @@ -143,10 +159,54 @@ describe("CiphernodeRegistryOwnable", function () { }, ); + const enclaveContract = await ignition.deploy(EnclaveModule, { + parameters: { + Enclave: { + params: encodedE3ProgramParams, + owner: ownerAddress, + maxDuration: 60 * 60 * 24 * 30, // 30 days + registry: AddressOne, // placeholder, will be updated + bondingRegistry: + await bondingRegistryContract.bondingRegistry.getAddress(), + e3RefundManager: AddressOne, // placeholder, will be updated + feeToken: await usdcContract.mockUSDC.getAddress(), + timeoutConfig: { + committeeFormationWindow: 3600, + dkgWindow: 3600, + computeWindow: 3600, + decryptionWindow: 3600, + gracePeriod: 300, + }, + }, + }, + }); + + const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclave = EnclaveFactory.connect(enclaveAddress, owner); + + const e3RefundManagerContract = await ignition.deploy( + E3RefundManagerModule, + { + parameters: { + E3RefundManager: { + owner: ownerAddress, + enclave: enclaveAddress, + treasury: ownerAddress, + }, + }, + }, + ); + + const e3RefundManagerAddress = + await e3RefundManagerContract.e3RefundManager.getAddress(); + + await enclave.setE3RefundManager(e3RefundManagerAddress); + + // Deploy CiphernodeRegistry with real Enclave address const registryContract = await ignition.deploy(CiphernodeRegistryModule, { parameters: { CiphernodeRegistry: { - enclaveAddress: ownerAddress, + enclaveAddress: enclaveAddress, owner: ownerAddress, submissionWindow: SORTITION_SUBMISSION_WINDOW, }, @@ -158,6 +218,9 @@ describe("CiphernodeRegistryOwnable", function () { const registry = CiphernodeRegistryFactory.connect(registryAddress, owner); + // Update Enclave with correct registry address + await enclave.setCiphernodeRegistry(registryAddress); + const bondingRegistry = BondingRegistryFactory.connect( await bondingRegistryContract.bondingRegistry.getAddress(), owner, @@ -174,6 +237,23 @@ describe("CiphernodeRegistryOwnable", function () { await bondingRegistry.getAddress(), ); + // Set up mock E3Program and DecryptionVerifier for Enclave + const mockE3Program = await ignition.deploy(MockE3ProgramModule); + const mockDecryptionVerifier = await ignition.deploy( + MockDecryptionVerifierModule, + ); + + await enclave.enableE3Program( + await mockE3Program.mockE3Program.getAddress(), + ); + await enclave.setE3ProgramsParams([encodedE3ProgramParams]); + await enclave.setDecryptionVerifier( + encryptionSchemeId, + await mockDecryptionVerifier.mockDecryptionVerifier.getAddress(), + ); + + await bondingRegistry.setRewardDistributor(enclaveAddress); + await registry.setBondingRegistry(await bondingRegistry.getAddress()); const tree = new LeanIMT(hash); @@ -209,18 +289,62 @@ describe("CiphernodeRegistryOwnable", function () { operator1, operator2, registry, + enclave, bondingRegistry, licenseToken, ticketToken, usdcToken, tree, + mockE3Program, + mockDecryptionVerifier, request: { - e3Id: 1, + e3Id: 0, threshold: [2, 2] as [number, number], }, }; } + // Helper to make a request through the Enclave contract + async function makeRequest( + enclave: any, + usdcToken: any, + mockE3Program: any, + mockDecryptionVerifier: any, + signer?: Signer, + ) { + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const polynomial_degree = ethers.toBigInt(2048); + const plaintext_modulus = ethers.toBigInt(1032193); + const moduli = [ethers.toBigInt("18014398492704769")]; + const encodedE3ProgramParams = abiCoder.encode( + ["uint256", "uint256", "uint256[]"], + [polynomial_degree, plaintext_modulus, moduli], + ); + + const currentTime = await networkHelpers.time.latest(); + const requestParams = { + threshold: [2, 2] as [number, number], + inputWindow: [currentTime + 100, currentTime + 300] as [number, number], + e3Program: await mockE3Program.mockE3Program.getAddress(), + e3ProgramParams: encodedE3ProgramParams, + computeProviderParams: abiCoder.encode( + ["address"], + [await mockDecryptionVerifier.mockDecryptionVerifier.getAddress()], + ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], + ), + }; + + 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); + } + describe("constructor / initialize()", function () { it("correctly sets `_owner` and `enclave` ", async function () { const poseidonFactory = await ethers.getContractFactory("PoseidonT3"); @@ -272,75 +396,105 @@ describe("CiphernodeRegistryOwnable", function () { }); 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 rootAt for the requested e3Id after a successful request", async function () { + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + } = await loadFixture(setup); + // Request through Enclave + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); + expect(await registry.rootAt(0)).to.equal(await registry.root()); }); 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(), + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + } = await loadFixture(setup); + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, ); + expect(await registry.rootAt(0)).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 { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + } = await loadFixture(setup); + + const tx = await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, ); - 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, - ); + // Should emit CommitteeRequested from registry + await expect(tx).to.emit(registry, "CommitteeRequested"); }); 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; + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + } = await loadFixture(setup); + // We can verify by checking that root is stored after request + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); + expect(await registry.rootAt(0)).to.not.equal(0); }); }); 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); + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + notTheOwner, + operator1, + operator2, + } = await loadFixture(setup); + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); - await registry.connect(operator1).submitTicket(request.e3Id, 1); - await registry.connect(operator2).submitTicket(request.e3Id, 1); - await finalizeCommitteeAfterWindow(registry, request.e3Id); + await registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await finalizeCommitteeAfterWindow(registry, 0); await expect( registry .connect(notTheOwner) .publishCommittee( - request.e3Id, + 0, [await operator1.getAddress(), await operator2.getAddress()], data, dataHash, @@ -348,39 +502,61 @@ describe("CiphernodeRegistryOwnable", function () { ).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); + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + operator1, + operator2, + } = await loadFixture(setup); + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); 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.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await finalizeCommitteeAfterWindow(registry, 0); await registry.publishCommittee( - request.e3Id, + 0, [await operator1.getAddress(), await operator2.getAddress()], data, dataHash, ); - expect(await registry.committeePublicKey(request.e3Id)).to.equal( - dataHash, - ); + expect(await registry.committeePublicKey(0)).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); + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + operator1, + operator2, + } = await loadFixture(setup); + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); // 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 registry.connect(operator1).submitTicket(0, 1); + await registry.connect(operator2).submitTicket(0, 1); + await finalizeCommitteeAfterWindow(registry, 0); await expect( await registry.publishCommittee( - request.e3Id, + 0, [await operator1.getAddress(), await operator2.getAddress()], data, dataHash, @@ -388,7 +564,7 @@ describe("CiphernodeRegistryOwnable", function () { ) .to.emit(registry, "CommitteePublished") .withArgs( - request.e3Id, + 0, [await operator1.getAddress(), await operator2.getAddress()], data, ); @@ -498,29 +674,52 @@ describe("CiphernodeRegistryOwnable", function () { 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); + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + operator1, + operator2, + } = await loadFixture(setup); + const e3Id = 0; + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); - await registry.connect(operator1).submitTicket(request.e3Id, 1); - await registry.connect(operator2).submitTicket(request.e3Id, 1); - await finalizeCommitteeAfterWindow(registry, request.e3Id); + await registry.connect(operator1).submitTicket(e3Id, 1); + await registry.connect(operator2).submitTicket(e3Id, 1); + await finalizeCommitteeAfterWindow(registry, e3Id); await registry.publishCommittee( - request.e3Id, + e3Id, [await operator1.getAddress(), await operator2.getAddress()], data, dataHash, ); - expect(await registry.committeePublicKey(request.e3Id)).to.equal( - dataHash, - ); + expect(await registry.committeePublicKey(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); + const { + registry, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + } = await loadFixture(setup); + const e3Id = 0; + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); await expect( - registry.committeePublicKey(request.e3Id), + registry.committeePublicKey(e3Id), ).to.be.revertedWithCustomError(registry, "CommitteeNotPublished"); }); }); @@ -556,9 +755,22 @@ describe("CiphernodeRegistryOwnable", function () { 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); + const { + registry, + tree, + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + } = await loadFixture(setup); + const e3Id = 0; + await makeRequest( + enclave, + usdcToken, + mockE3Program, + mockDecryptionVerifier, + ); + expect(await registry.rootAt(e3Id)).to.equal(tree.root); }); }); diff --git a/packages/enclave-react/src/useEnclaveSDK.ts b/packages/enclave-react/src/useEnclaveSDK.ts index 52be055c44..9f47a26544 100644 --- a/packages/enclave-react/src/useEnclaveSDK.ts +++ b/packages/enclave-react/src/useEnclaveSDK.ts @@ -34,8 +34,6 @@ export interface UseEnclaveSDKReturn { error: string | null // Contract interaction methods (only the ones commonly used) requestE3: typeof EnclaveSDK.prototype.requestE3 - activateE3: typeof EnclaveSDK.prototype.activateE3 - publishInput: typeof EnclaveSDK.prototype.publishInput getThresholdBfvParamsSet: typeof EnclaveSDK.prototype.getThresholdBfvParamsSet // Event handling onEnclaveEvent: (eventType: T, callback: EventCallback) => void @@ -92,8 +90,8 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn throw new Error('Public client not available') } - if (sdk) { - sdk.cleanup() + if (sdkRef.current) { + sdkRef.current.cleanup() } const sdkConfig: SDKConfig = { @@ -118,7 +116,7 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn setError(errorMessage) console.error('SDK initialization failed:', err) } - }, [publicClient, walletClient, config.contracts, config.chainId]) + }, [publicClient, walletClient, config.contracts, config.chainId, config.thresholdBfvParamsPresetName]) // Initialize SDK when wagmi clients are available useEffect(() => { @@ -132,7 +130,7 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn if (isInitialized && publicClient && walletClient) { initializeSDK() } - }, [walletClient, initializeSDK]) + }, [walletClient, initializeSDK, isInitialized, publicClient]) // Cleanup on unmount useEffect(() => { @@ -143,6 +141,11 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn } }, []) + const getThresholdBfvParamsSet = useCallback(async () => { + if (!sdk) throw new Error('SDK not initialized') + return sdk.getThresholdBfvParamsSet() + }, [sdk]) + // Contract interaction methods const requestE3 = useCallback( (...args: Parameters) => { @@ -152,27 +155,6 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn [sdk], ) - const activateE3 = useCallback( - (...args: Parameters) => { - if (!sdk) throw new Error('SDK not initialized') - return sdk.activateE3(...args) - }, - [sdk], - ) - - const publishInput = useCallback( - (...args: Parameters) => { - if (!sdk) throw new Error('SDK not initialized') - return sdk.publishInput(...args) - }, - [sdk], - ) - - const getThresholdBfvParamsSet = useCallback(async () => { - if (!sdk) throw new Error('SDK not initialized') - return sdk.getThresholdBfvParamsSet() - }, [sdk]) - // Event handling methods const onEnclaveEvent = useCallback( (eventType: T, callback: EventCallback) => { @@ -195,8 +177,6 @@ export const useEnclaveSDK = (config: UseEnclaveSDKConfig): UseEnclaveSDKReturn isInitialized, error, requestE3, - activateE3, - publishInput, getThresholdBfvParamsSet, onEnclaveEvent, off, diff --git a/packages/enclave-sdk/src/contract-client.ts b/packages/enclave-sdk/src/contract-client.ts index 5271ed9edd..50ff5c16a1 100644 --- a/packages/enclave-sdk/src/contract-client.ts +++ b/packages/enclave-sdk/src/contract-client.ts @@ -102,12 +102,11 @@ export class ContractClient { /** * Request a new E3 computation - * request(address filter, uint32[2] threshold, uint256[2] startWindow, uint256 duration, IE3Program e3Program, bytes e3ProgramParams, bytes computeProviderParams, bytes customParams) + * request(uint32[2] threshold, uint256[2] inputWindow, IE3Program e3Program, bytes e3ProgramParams, bytes computeProviderParams, bytes customParams) */ public async requestE3( threshold: [number, number], - startWindow: [bigint, bigint], - duration: bigint, + inputWindow: [bigint, bigint], e3Program: `0x${string}`, e3ProgramParams: `0x${string}`, computeProviderParams: `0x${string}`, @@ -136,8 +135,7 @@ export class ContractClient { args: [ { threshold, - startWindow, - duration, + inputWindow, e3Program, e3ProgramParams, computeProviderParams, @@ -157,78 +155,6 @@ export class ContractClient { } } - /** - * Activate an E3 computation - * activate(uint256 e3Id) - */ - public async activateE3(e3Id: bigint, gasLimit?: 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.enclave, - abi: Enclave__factory.abi, - functionName: 'activate', - args: [e3Id], - account, - gas: gasLimit, - }) - - const hash = await this.walletClient.writeContract(request) - - return hash - } catch (error) { - throw new SDKError(`Failed to activate E3: ${error}`, 'ACTIVATE_E3_FAILED') - } - } - - /** - * Publish input for an E3 computation - * publishInput(uint256 e3Id, bytes memory data) - */ - public async publishInput(e3Id: bigint, data: `0x${string}`, gasLimit?: 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.enclave, - abi: Enclave__factory.abi, - functionName: 'publishInput', - args: [e3Id, data], - account, - gas: gasLimit, - }) - - const hash = await this.walletClient.writeContract(request) - - return hash - } catch (error) { - throw new SDKError(`Failed to publish input: ${error}`, 'PUBLISH_INPUT_FAILED') - } - } - /** * Publish ciphertext output for an E3 computation * publishCiphertextOutput(uint256 e3Id, bytes memory ciphertextOutput, bytes memory proof) diff --git a/packages/enclave-sdk/src/enclave-sdk.ts b/packages/enclave-sdk/src/enclave-sdk.ts index 6e831f12a2..911d4a79ff 100644 --- a/packages/enclave-sdk/src/enclave-sdk.ts +++ b/packages/enclave-sdk/src/enclave-sdk.ts @@ -288,8 +288,7 @@ export class EnclaveSDK { */ public async requestE3(params: { threshold: [number, number] - startWindow: [bigint, bigint] - duration: bigint + inputWindow: [bigint, bigint] e3Program: `0x${string}` e3ProgramParams: `0x${string}` computeProviderParams: `0x${string}` @@ -304,8 +303,7 @@ export class EnclaveSDK { return this.contractClient.requestE3( params.threshold, - params.startWindow, - params.duration, + params.inputWindow, params.e3Program, params.e3ProgramParams, params.computeProviderParams, @@ -327,28 +325,6 @@ export class EnclaveSDK { return this.contractClient.getE3PublicKey(e3Id) } - /** - * Activate an E3 computation - */ - public async activateE3(e3Id: bigint, gasLimit?: bigint): Promise { - if (!this.initialized) { - await this.initialize() - } - - return this.contractClient.activateE3(e3Id, gasLimit) - } - - /** - * Publish input for an E3 computation - */ - public async publishInput(e3Id: bigint, data: `0x${string}`, gasLimit?: bigint): Promise { - if (!this.initialized) { - await this.initialize() - } - - return this.contractClient.publishInput(e3Id, data, gasLimit) - } - /** * Publish ciphertext output for an E3 computation */ diff --git a/packages/enclave-sdk/src/index.ts b/packages/enclave-sdk/src/index.ts index 16a779995a..1da6643211 100644 --- a/packages/enclave-sdk/src/index.ts +++ b/packages/enclave-sdk/src/index.ts @@ -60,7 +60,7 @@ export { encodeBfvParams, encodeComputeProviderParams, encodeCustomParams, - calculateStartWindow, + calculateInputWindow, decodePlaintextOutput, type ComputeProviderParams, } from './utils' diff --git a/packages/enclave-sdk/src/types.ts b/packages/enclave-sdk/src/types.ts index 3ad921b262..5ab38010e2 100644 --- a/packages/enclave-sdk/src/types.ts +++ b/packages/enclave-sdk/src/types.ts @@ -72,7 +72,6 @@ export interface ContractInstances { export enum EnclaveEventType { // E3 Lifecycle Events E3_REQUESTED = 'E3Requested', - E3_ACTIVATED = 'E3Activated', CIPHERTEXT_OUTPUT_PUBLISHED = 'CiphertextOutputPublished', PLAINTEXT_OUTPUT_PUBLISHED = 'PlaintextOutputPublished', @@ -116,9 +115,7 @@ export interface E3 { seed: bigint threshold: readonly [number, number] requestBlock: bigint - startWindow: readonly [bigint, bigint] - duration: bigint - expiration: bigint + inputWindow: readonly [bigint, bigint] encryptionSchemeId: string e3Program: string e3ProgramParams: string @@ -170,11 +167,12 @@ export interface CommitteeRequestedData { seed: bigint threshold: [bigint, bigint] requestBlock: bigint - submissionDeadline: bigint + committeeDeadline: bigint } export interface CommitteePublishedData { e3Id: bigint + nodes: string[] publicKey: string } @@ -186,7 +184,6 @@ export interface CommitteeFinalizedData { // Event data mapping export interface EnclaveEventData { [EnclaveEventType.E3_REQUESTED]: E3RequestedData - [EnclaveEventType.E3_ACTIVATED]: E3ActivatedData [EnclaveEventType.CIPHERTEXT_OUTPUT_PUBLISHED]: CiphertextOutputPublishedData [EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED]: PlaintextOutputPublishedData [EnclaveEventType.E3_PROGRAM_ENABLED]: { e3Program: string } diff --git a/packages/enclave-sdk/src/utils.ts b/packages/enclave-sdk/src/utils.ts index 71d821bae3..e0f9539e09 100644 --- a/packages/enclave-sdk/src/utils.ts +++ b/packages/enclave-sdk/src/utils.ts @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { type Address, type Hash, type Log, encodeAbiParameters } from 'viem' +import { type Address, type Hash, type Log, PublicClient, encodeAbiParameters } from 'viem' import type { BfvParams } from './types' export class SDKError extends Error { @@ -54,9 +54,12 @@ export function generateEventId(log: Log): string { /** * Get the current timestamp in seconds + * from onchain + * @param publicClient - The public client to use */ -export function getCurrentTimestamp(): number { - return Math.floor(Date.now() / 1000) +export async function getCurrentTimestamp(publicClient: PublicClient): Promise { + const block = await publicClient.getBlock() + return block.timestamp } // Compute provider parameters structure @@ -77,7 +80,6 @@ export const DEFAULT_COMPUTE_PROVIDER_PARAMS: ComputeProviderParams = { export const DEFAULT_E3_CONFIG = { threshold_min: 2, threshold_max: 5, - window_size: 120, // 2 minutes in seconds duration: 1800, // 30 minutes in seconds payment_amount: '0', // 0 ETH in wei } as const @@ -147,12 +149,23 @@ export function encodeCustomParams(params: Record): `0x${string return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}` } +// inputWindow[0] is always larger than now and dkg deadline +export const inputWindowStartBuffer = 15n + /** * Calculate start window for E3 request + * @dev This function can be used for testing purposes, or for E3s which need to start as soon as possible. + * @param publicClient - The public client to use + * @param duration - The duration of the input window in seconds + * @param startBuffer - Buffer in seconds added to current timestamp for input window start */ -export function calculateStartWindow(windowSize: number = DEFAULT_E3_CONFIG.window_size): [bigint, bigint] { - const now = getCurrentTimestamp() - return [BigInt(now), BigInt(now + windowSize)] +export async function calculateInputWindow( + publicClient: PublicClient, + duration: number = DEFAULT_E3_CONFIG.duration, + startBuffer: bigint = inputWindowStartBuffer, +): Promise<[bigint, bigint]> { + const now = await getCurrentTimestamp(publicClient) + return [BigInt(now) + startBuffer, BigInt(now) + startBuffer + BigInt(duration)] } /** diff --git a/templates/default/contracts/MyProgram.sol b/templates/default/contracts/MyProgram.sol index ae3d4699e0..f02def5b41 100755 --- a/templates/default/contracts/MyProgram.sol +++ b/templates/default/contracts/MyProgram.sol @@ -8,6 +8,7 @@ pragma solidity >=0.8.27; import { IRiscZeroVerifier } from "@risc0/ethereum/contracts/IRiscZeroVerifier.sol"; import { IE3Program } from "@enclave-e3/contracts/contracts/interfaces/IE3Program.sol"; import { IEnclave } from "@enclave-e3/contracts/contracts/interfaces/IEnclave.sol"; +import { E3 } from "@enclave-e3/contracts/contracts/interfaces/IE3.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { LazyIMTData, InternalLazyIMT, PoseidonT3 } from "@zk-kit/lazy-imt.sol/InternalLazyIMT.sol"; @@ -35,6 +36,7 @@ contract MyProgram is IE3Program, Ownable { error VerifierAddressZero(); error AlreadyRegistered(); error EmptyInputData(); + error InputDeadlineReached(); event InputPublished(uint256 indexed e3Id, bytes data, uint256 index); @@ -65,9 +67,15 @@ contract MyProgram is IE3Program, Ownable { } /// @notice Validates input - /// @param sender The account that is submitting the input. + /// @param e3Id The e3 id for which to publish input /// @param data The input to be verified. - function validateInput(uint256 e3Id, address sender, bytes memory data) external { + function publishInput(uint256 e3Id, bytes memory data) external { + E3 memory e3 = enclave.getE3(e3Id); + + if (block.timestamp > e3.inputWindow[1]) { + revert InputDeadlineReached(); + } + if (data.length == 0) revert EmptyInputData(); // You can add your own validation logic here. diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index c88dfd2586..6149fa1dab 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -98,14 +98,16 @@ "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "registry": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", "bondingRegistry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "e3RefundManager": "0x0000000000000000000000000000000000000001", "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", "maxDuration": "2592000", + "timeoutConfig": "{\"committeeFormationWindow\":3600,\"dkgWindow\":7200,\"computeWindow\":86400,\"decryptionWindow\":3600,\"gracePeriod\":600}", "params": [ "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000" ] }, "proxyRecords": { - "initData": "0xefe0308b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe60000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d0000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000", + "initData": "0x8d158aa7000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d000000000000000000000000000000000000000000000000000000000000000e100000000000000000000000000000000000000000000000000000000000001c2000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e10000000000000000000000000000000000000000000000000000000000000025800000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000fc00100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000003fffffff000001", "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", "proxyAdminAddress": "0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D", @@ -114,20 +116,36 @@ "blockNumber": 13, "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, + "E3RefundManager": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "treasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "proxyRecords": { + "initData": "0xc0c53b8b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "proxyAdminAddress": "0x8e80FFe6Dc044F4A766Afd6e5a8732Fe0977A493", + "implementationAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 15, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, "MockComputeProvider": { - "blockNumber": 23, - "address": "0x59b670e9fA9D0A427751Af201D676719a970857b" + "blockNumber": 26, + "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "MockDecryptionVerifier": { - "blockNumber": 24, - "address": "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1" + "blockNumber": 27, + "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" }, "MockE3Program": { - "blockNumber": 25, - "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" + "blockNumber": 28, + "address": "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F" }, "ImageID": { - "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E" } } } \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index f28f9759dd..aaa84e54db 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0xc5a5C42992dECbae36851359345FE25997F5C42d" + address: "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690" deploy_block: 1 # Set to actual deploy block enclave: address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" diff --git a/templates/default/server/index.ts b/templates/default/server/index.ts index 9a4719b6c8..61f1430895 100644 --- a/templates/default/server/index.ts +++ b/templates/default/server/index.ts @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import express, { Request, Response } from 'express' -import { EnclaveSDK, EnclaveEventType, type E3ActivatedData } from '@enclave-e3/sdk' +import { EnclaveSDK, RegistryEventType, CommitteePublishedData } from '@enclave-e3/sdk' import { Log, PublicClient } from 'viem' import { handleTestInteraction } from './testHandler' import { getCheckedEnvVars } from './utils' @@ -116,26 +116,29 @@ function getActivationDefer(e3Id: bigint): Defer { return d } -async function handleE3ActivatedEvent(event: any) { - const data = event.data as E3ActivatedData +async function handleCommitteePublishedEvent(event: any) { + const data = event.data as CommitteePublishedData const e3Id = data.e3Id - const expiration = data.expiration - // This allows us to wait until the session has been activated avoiding race conditions const def = getActivationDefer(e3Id) - console.log(`🎯 E3 Activated: ${e3Id}, expiration: ${expiration}`) + const sdk = await createPrivateSDK() + const publicClient = sdk.getPublicClient() - const sessionKey = e3Id.toString() + console.log('📡 Fetching E3 data from contract...') + const e3 = await sdk.getE3(e3Id) - if (!e3Sessions.has(sessionKey)) { - const sdk = await createPrivateSDK() - console.log('📡 Fetching E3 data from contract...') + console.log('✅ Received E3 data from contract.') - const e3 = await sdk.getE3(e3Id) - console.log('✅ Received E3 data from contract.') + const expiration = e3.inputWindow[1] - e3Sessions.set(sessionKey, { + console.log(`🎯 Committee Published for: ${e3Id}, expiration: ${expiration}`) + + console.log(`📥 Setting up session for E3 ${e3Id}...`) + console.log(e3Sessions) + + if (!e3Sessions.has(e3Id.toString())) { + e3Sessions.set(e3Id.toString(), { e3Id, e3ProgramParams: e3.e3ProgramParams, expiration, @@ -143,10 +146,11 @@ async function handleE3ActivatedEvent(event: any) { isProcessing: false, isCompleted: false, }) + def.resolve() } - const currentTime = BigInt(Math.floor(Date.now() / 1000)) + const currentTime = (await publicClient.getBlock()).timestamp const sleepSeconds = expiration > currentTime ? Number(expiration - currentTime) : 0 if (sleepSeconds > 0) { @@ -210,7 +214,8 @@ async function setupEventListeners() { console.log('📡 Setting up event listeners...') - sdk.onEnclaveEvent(EnclaveEventType.E3_ACTIVATED, handleE3ActivatedEvent) + // we need to listen to CommitteePublished to know when an E3 is ready + sdk.onEnclaveEvent(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublishedEvent) await listenToInputPublishedEvents(sdk.getPublicClient(), PROGRAM_ADDRESS as `0x${string}`, 0n) diff --git a/templates/default/server/input.ts b/templates/default/server/input.ts new file mode 100644 index 0000000000..aeefa6708b --- /dev/null +++ b/templates/default/server/input.ts @@ -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. + +import { WalletClient } from 'viem' +import { MyProgram__factory as MyProgram } from '../types/factories/contracts' + +/** + * Publish an input to the program + * @param walletClient - The wallet client to use for the transaction + * @param e3Id - The E3 ID + * @param input - The input data + * @param sender - The sender address + * @param programAddress - The program contract address + */ +export const publishInput = async ( + walletClient: WalletClient, + e3Id: bigint, + input: `0x${string}`, + sender: `0x${string}`, + programAddress: `0x${string}`, +): Promise => { + await walletClient.writeContract({ + address: programAddress as `0x${string}`, + abi: MyProgram.abi, + functionName: 'publishInput', + args: [e3Id, input], + chain: walletClient.chain, + account: sender, + }) +} diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index ed9e81f8ef..8459432531 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -6,7 +6,7 @@ import { AllEventTypes, - calculateStartWindow, + calculateInputWindow, DEFAULT_COMPUTE_PROVIDER_PARAMS, DEFAULT_E3_CONFIG, E3, @@ -17,10 +17,13 @@ import { encodeComputeProviderParams, RegistryEventType, } from '@enclave-e3/sdk' -import { hexToBytes } from 'viem' +import { createWalletClient, hexToBytes, http } from 'viem' import assert from 'assert' import { describe, expect, it } from 'vitest' +import { publishInput } from '../server/input' +import { privateKeyToAccount } from 'viem/accounts' +import { hardhat } from 'viem/chains' export function getContractAddresses() { return { @@ -53,18 +56,12 @@ type E3StatePublished = E3Shared & { publicKey: `0x${string}` } -type E3StateActivated = E3Shared & { - type: 'activated' - publicKey: `0x${string}` - expiration: bigint -} - type E3StateOutputPublished = E3Shared & { type: 'output_published' plaintextOutput: string } -type E3State = E3StateRequested | E3StatePublished | E3StateActivated | E3StateOutputPublished +type E3State = E3StateRequested | E3StatePublished | E3StateOutputPublished async function setupEventListeners(sdk: EnclaveSDK, store: Map) { async function waitForEvent(type: T, trigger?: () => Promise): Promise> { @@ -99,7 +96,7 @@ async function setupEventListeners(sdk: EnclaveSDK, store: Map) } if (state.type !== 'requested') { - throw new Error(`State must be in the ${state.type} state`) + throw new Error(`State must be in the requested state`) } store.set(id, { @@ -109,26 +106,6 @@ async function setupEventListeners(sdk: EnclaveSDK, store: Map) }) }) - sdk.onEnclaveEvent(EnclaveEventType.E3_ACTIVATED, (event) => { - const id = event.data.e3Id - const state = store.get(id) - - if (!state) { - throw new Error(`State for ID '${id}' not found.`) - } - - if (state.type !== 'committee_published') { - throw new Error(`State must be in the ${state.type} state`) - } - - store.set(id, { - ...state, - expiration: event.data.expiration, - publicKey: event.data.committeePublicKey as `0x${string}`, - type: 'activated', - }) - }) - sdk.onEnclaveEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED, (event) => { const id = event.data.e3Id const state = store.get(id) @@ -137,8 +114,8 @@ async function setupEventListeners(sdk: EnclaveSDK, store: Map) throw new Error(`State for ID '${id}' not found.`) } - if (state.type !== 'activated') { - throw new Error(`State must be in the ${state.type} state`) + if (state.type !== 'committee_published') { + throw new Error(`State must be in the committee_published state`) } store.set(id, { @@ -156,6 +133,8 @@ describe('Integration', () => { const contracts = getContractAddresses() + const testPrivateKey = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + const store = new Map() const sdk = EnclaveSDK.create({ chainId: 31337, @@ -165,18 +144,29 @@ describe('Integration', () => { feeToken: contracts.feeToken, }, rpcUrl: 'ws://localhost:8545', - privateKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', thresholdBfvParamsPresetName: 'INSECURE_THRESHOLD_512', + privateKey: testPrivateKey, + }) + + const publicClient = sdk.getPublicClient() + + const account = privateKeyToAccount(testPrivateKey) + + const walletClient = createWalletClient({ + account, + chain: hardhat, + transport: http('http://localhost:8545'), }) it('should run an integration test', async () => { const { waitForEvent } = await setupEventListeners(sdk, store) const threshold: [number, number] = [DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max] - const startWindow = calculateStartWindow(100) - const duration = BigInt(20) + const duration = 30 + const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams) + const computeProviderParams = encodeComputeProviderParams( DEFAULT_COMPUTE_PROVIDER_PARAMS, true, // Mock the compute provider parameters, return 32 bytes of 0x00 @@ -197,8 +187,7 @@ describe('Integration', () => { console.log('Requested E3...') await sdk.requestE3({ threshold, - startWindow, - duration, + inputWindow, e3Program: contracts.e3Program, e3ProgramParams, computeProviderParams, @@ -223,15 +212,6 @@ describe('Integration', () => { let { e3Id } = state - // ACTIVATION phase - event = await waitForEvent(EnclaveEventType.E3_ACTIVATED, async () => { - await sdk.activateE3(e3Id) - }) - - state = store.get(0n) - assert(state, 'store should have activated state but it was falsey') - assert.strictEqual(state.type, 'activated') - // INPUT PUBLISHING phase console.log('PUBLISHING PRIVATE INPUT') const num1 = 1n @@ -241,9 +221,20 @@ describe('Integration', () => { const enc1 = await sdk.encryptNumber(num1, publicKeyBytes) const enc2 = await sdk.encryptNumber(num2, publicKeyBytes) - await sdk.publishInput(e3Id, `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`) - await sdk.publishInput(e3Id, `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`) - + await publishInput( + walletClient, + e3Id, + `0x${Array.from(enc1, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`, + account.address, + contracts.e3Program, + ) + await publishInput( + walletClient, + e3Id, + `0x${Array.from(enc2, (b) => b.toString(16).padStart(2, '0')).join('')}` as `0x${string}`, + account.address, + contracts.e3Program, + ) const plaintextEvent = await waitForEvent(EnclaveEventType.PLAINTEXT_OUTPUT_PUBLISHED) const parsed = hexToUint8Array(plaintextEvent.data.plaintextOutput) diff --git a/tests/integration/base.sh b/tests/integration/base.sh index 8899b5791e..b885048494 100755 --- a/tests/integration/base.sh +++ b/tests/integration/base.sh @@ -7,6 +7,7 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Source the file from the same directory source "$THIS_DIR/fns.sh" +source "$THIS_DIR/lib/utils.sh" heading "Start the EVM node" @@ -60,13 +61,18 @@ ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh \ --moduli 0xffffee001 \ --moduli 0xffffc4001 \ --degree 512 \ - --plaintext-modulus 10) + --plaintext-modulus 100) sleep 4 +CURRENT_TIMESTAMP=$(get_evm_timestamp) +INPUT_WINDOW_START=$((CURRENT_TIMESTAMP + 20)) +INPUT_WINDOW_END=$((CURRENT_TIMESTAMP + 30)) + pnpm committee:new \ --network localhost \ - --duration 4 \ + --input-window-start "$INPUT_WINDOW_START" \ + --input-window-end "$INPUT_WINDOW_END" \ --e3-params "$ENCODED_PARAMS" \ --threshold-quorum 2 \ --threshold-total 5 @@ -76,13 +82,10 @@ waiton "$SCRIPT_DIR/output/pubkey.bin" heading "Mock encrypted plaintext" $SCRIPT_DIR/lib/fake_encrypt.sh --input "$SCRIPT_DIR/output/pubkey.bin" --output "$SCRIPT_DIR/output/output.bin" --plaintext $PLAINTEXT --params "$ENCODED_PARAMS" -heading "Mock activate e3-id" -pnpm -s e3:activate --e3-id 0 --network localhost - heading "Mock publish input e3-id" -pnpm e3:publishInput --network localhost --e3-id 0 --data 0x12345678 +pnpm e3-program:publishInput --network localhost --e3-id 0 --data 0x12345678 -sleep 4 # wait for input deadline to pass +sleep 4 waiton "$SCRIPT_DIR/output/output.bin" diff --git a/tests/integration/lib/utils.sh b/tests/integration/lib/utils.sh new file mode 100644 index 0000000000..a2893022e8 --- /dev/null +++ b/tests/integration/lib/utils.sh @@ -0,0 +1,9 @@ +# Get the current block timestamp from a local EVM node +# Usage: get_evm_timestamp [rpc_url] +get_evm_timestamp() { + local rpc_url="${1:-http://localhost:8545}" + curl -s -X POST "$rpc_url" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \ + | jq -r '.result.timestamp' | xargs printf "%d\n" +} diff --git a/tests/integration/persist.sh b/tests/integration/persist.sh index 26e813291d..b36263188b 100755 --- a/tests/integration/persist.sh +++ b/tests/integration/persist.sh @@ -7,6 +7,7 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Source the file from the same directory source "$THIS_DIR/fns.sh" +source "$THIS_DIR/lib/utils.sh" heading "Start the EVM node" @@ -56,11 +57,16 @@ ENCODED_PARAMS=0x$($SCRIPT_DIR/lib/pack_e3_params.sh \ --moduli 0xffffee001 \ --moduli 0xffffc4001 \ --degree 512 \ - --plaintext-modulus 10) + --plaintext-modulus 100) + +CURRENT_TIMESTAMP=$(get_evm_timestamp) +INPUT_WINDOW_START=$((CURRENT_TIMESTAMP + 20)) +INPUT_WINDOW_END=$((CURRENT_TIMESTAMP + 30)) pnpm committee:new \ --network localhost \ - --duration 4 \ + --input-window-start "$INPUT_WINDOW_START" \ + --input-window-end "$INPUT_WINDOW_END" \ --e3-params "$ENCODED_PARAMS" \ --threshold-quorum 2 \ --threshold-total 5 @@ -75,19 +81,15 @@ sleep 2 # relaunch the aggregator enclave_nodes_start ag -sleep 2 +sleep 4 heading "Mock encrypted plaintext" $SCRIPT_DIR/lib/fake_encrypt.sh --input "$SCRIPT_DIR/output/pubkey.bin" --output "$SCRIPT_DIR/output/output.bin" --plaintext $PLAINTEXT --params "$ENCODED_PARAMS" -heading "Mock activate e3-id" - -pnpm -s e3:activate --e3-id 0 --network localhost - heading "Mock publish input e3-id" -pnpm e3:publishInput --network localhost --e3-id 0 --data 0x12345678 +pnpm e3-program:publishInput --network localhost --e3-id 0 --data 0x12345678 -sleep 4 # wait for input deadline to pass +sleep 6 # wait for input deadline to pass waiton "$SCRIPT_DIR/output/output.bin"