From cd71c3fc4a64f0dae903bf3ac8178a84cf1ffb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=B3=CE=BB?= Date: Wed, 9 Oct 2024 06:55:04 +1100 Subject: [PATCH] Multichain (#139) * Reorganize evm on per contract basis * Fix not publishing correct key * Tidy up API * Tidy up API some more * Formatting * Add configuration management * Formatting * Plumb src_chain_id through events * Ensure only publish if src chain matches * Formatting * Derive solidity types from abi artifacts * Formatting * Fix risk of panic * Add contract compilation to rust tests --- .github/workflows/rust-ci.yml | 18 ++ package.json | 2 + packages/ciphernode/Cargo.lock | 207 +++++++++++++++++- .../aggregator/src/plaintext_aggregator.rs | 4 + .../aggregator/src/publickey_aggregator.rs | 49 ++++- packages/ciphernode/core/src/eventbus.rs | 20 +- packages/ciphernode/core/src/events.rs | 28 ++- packages/ciphernode/core/src/ordered_set.rs | 55 ++++- packages/ciphernode/enclave/Cargo.toml | 1 + .../ciphernode/enclave/src/bin/aggregator.rs | 37 ++-- packages/ciphernode/enclave/src/lib.rs | 13 ++ packages/ciphernode/enclave/src/main.rs | 23 +- packages/ciphernode/enclave_node/Cargo.toml | 1 + .../ciphernode/enclave_node/src/aggregator.rs | 61 ++++-- .../ciphernode/enclave_node/src/app_config.rs | 23 ++ .../ciphernode/enclave_node/src/ciphernode.rs | 32 ++- packages/ciphernode/enclave_node/src/lib.rs | 3 + packages/ciphernode/evm/Cargo.toml | 1 + packages/ciphernode/evm/src/caller.rs | 156 ------------- .../ciphernode/evm/src/ciphernode_registry.rs | 95 -------- .../evm/src/ciphernode_registry_sol.rs | 152 +++++++++++++ packages/ciphernode/evm/src/contracts.rs | 85 ------- packages/ciphernode/evm/src/enclave.rs | 99 --------- packages/ciphernode/evm/src/enclave_sol.rs | 21 ++ .../ciphernode/evm/src/enclave_sol_reader.rs | 127 +++++++++++ .../ciphernode/evm/src/enclave_sol_writer.rs | 118 ++++++++++ packages/ciphernode/evm/src/helpers.rs | 118 ++++++++++ packages/ciphernode/evm/src/lib.rs | 23 +- packages/ciphernode/evm/src/listener.rs | 142 ------------ packages/ciphernode/evm/src/manager.rs | 74 ------- .../ciphernode/evm/src/registry_filter_sol.rs | 134 ++++++++++++ .../ciphernode/router/src/committee_meta.rs | 12 +- packages/ciphernode/router/src/hooks.rs | 2 + packages/ciphernode/sortition/Cargo.toml | 1 + packages/ciphernode/sortition/src/distance.rs | 14 +- .../ciphernode/sortition/src/sortition.rs | 38 ++-- .../tests/test_aggregation_and_decryption.rs | 19 +- .../lib/ciphernode_config.yaml | 7 + tests/basic_integration/test.sh | 12 +- 39 files changed, 1237 insertions(+), 790 deletions(-) create mode 100644 packages/ciphernode/enclave/src/lib.rs create mode 100644 packages/ciphernode/enclave_node/src/app_config.rs delete mode 100644 packages/ciphernode/evm/src/caller.rs delete mode 100644 packages/ciphernode/evm/src/ciphernode_registry.rs create mode 100644 packages/ciphernode/evm/src/ciphernode_registry_sol.rs delete mode 100644 packages/ciphernode/evm/src/contracts.rs delete mode 100644 packages/ciphernode/evm/src/enclave.rs create mode 100644 packages/ciphernode/evm/src/enclave_sol.rs create mode 100644 packages/ciphernode/evm/src/enclave_sol_reader.rs create mode 100644 packages/ciphernode/evm/src/enclave_sol_writer.rs create mode 100644 packages/ciphernode/evm/src/helpers.rs delete mode 100644 packages/ciphernode/evm/src/listener.rs delete mode 100644 packages/ciphernode/evm/src/manager.rs create mode 100644 packages/ciphernode/evm/src/registry_filter_sol.rs create mode 100644 tests/basic_integration/lib/ciphernode_config.yaml diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 91b7f76a..f1bdd692 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -19,7 +19,25 @@ jobs: with: toolchain: 1.81.0 override: true + + # We need to setup node in order to compile the hardhat contracts to get the artifacts + - name: "Setup node" + uses: actions/setup-node@v2 + with: + node-version: 20 + + - name: Cache node modules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} + - name: "Install the dependencies" + run: "yarn install" + + - name: "Compile the contracts and generate the TypeChain bindings" + run: "yarn typechain" + # Now we can check rust formatting and run tests - name: Checking code format run: cd ./packages/ciphernode && cargo fmt -- --check diff --git a/package.json b/package.json index 4a47d8e6..902e691c 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,13 @@ "test:integration": "./tests/basic_integration/test.sh", "coverage": "yarn evm:coverage", "ciphernode:launch": "cd packages/ciphernode && ./scripts/launch.sh", + "ciphernode:lint": "cd packages/ciphernode && cargo fmt -- --check", "ciphernode:add": "cd packages/evm && yarn ciphernode:add", "ciphernode:remove": "cd packages/evm && yarn ciphernode:remove", "ciphernode:aggregator": "cd packages/ciphernode && ./scripts/aggregator.sh", "ciphernode:test": "cd packages/ciphernode && cargo test", "ciphernode:build": "cd packages/ciphernode && cargo build --release", + "preciphernode:build": "yarn evm:compile", "committee:new": "cd packages/evm && yarn committee:new", "committee:publish": "cd packages/evm && yarn hardhat committee:publish", "e3:activate": "cd packages/evm && yarn hardhat e3:activate", diff --git a/packages/ciphernode/Cargo.lock b/packages/ciphernode/Cargo.lock index f4b8a1d7..d7492d72 100644 --- a/packages/ciphernode/Cargo.lock +++ b/packages/ciphernode/Cargo.lock @@ -1422,6 +1422,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -1631,6 +1634,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "const-hex" version = "1.12.0" @@ -1650,12 +1673,41 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1778,7 +1830,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1867,7 +1919,7 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version 0.4.0", @@ -1927,6 +1979,15 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -2022,6 +2083,7 @@ dependencies = [ "actix-rt", "alloy", "clap", + "config", "enclave_node", "tokio", ] @@ -2065,6 +2127,7 @@ dependencies = [ "rand", "rand_chacha", "router", + "serde", "sortition", "test-helpers", "tokio", @@ -2153,6 +2216,7 @@ dependencies = [ "enclave-core", "futures-util", "sortition", + "tokio", ] [[package]] @@ -2578,6 +2642,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.5" @@ -2951,7 +3021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -3064,6 +3134,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "k256" version = "0.13.3" @@ -3738,7 +3819,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -4209,6 +4290,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -4292,6 +4383,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pem" version = "3.0.4" @@ -4310,15 +4407,49 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.11" +version = "2.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" dependencies = [ "memchr", "thiserror", "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "pest_meta" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -4970,6 +5101,18 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.6.0", + "serde", + "serde_derive", +] + [[package]] name = "router" version = "0.1.0" @@ -5029,6 +5172,16 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5301,6 +5454,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5448,6 +5610,7 @@ version = "0.1.0" dependencies = [ "actix", "alloy", + "anyhow", "enclave-core", "num", "rand", @@ -5860,11 +6023,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -5873,6 +6051,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -6056,6 +6236,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.5" @@ -6594,6 +6780,15 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yamux" version = "0.12.1" diff --git a/packages/ciphernode/aggregator/src/plaintext_aggregator.rs b/packages/ciphernode/aggregator/src/plaintext_aggregator.rs index 99231da3..ed9abb00 100644 --- a/packages/ciphernode/aggregator/src/plaintext_aggregator.rs +++ b/packages/ciphernode/aggregator/src/plaintext_aggregator.rs @@ -38,6 +38,7 @@ pub struct PlaintextAggregator { sortition: Addr, e3_id: E3id, state: PlaintextAggregatorState, + src_chain_id: u64, } impl PlaintextAggregator { @@ -49,12 +50,14 @@ impl PlaintextAggregator { threshold_m: usize, seed: Seed, ciphertext_output: Vec, + src_chain_id: u64, ) -> Self { PlaintextAggregator { fhe, bus, sortition, e3_id, + src_chain_id, state: PlaintextAggregatorState::Collecting { threshold_m, shares: OrderedSet::new(), @@ -183,6 +186,7 @@ impl Handler for PlaintextAggregator { let event = EnclaveEvent::from(PlaintextAggregated { decrypted_output, e3_id: self.e3_id.clone(), + src_chain_id: self.src_chain_id, }); self.bus.do_send(event); diff --git a/packages/ciphernode/aggregator/src/publickey_aggregator.rs b/packages/ciphernode/aggregator/src/publickey_aggregator.rs index 8f1d13b4..05e5d3cd 100644 --- a/packages/ciphernode/aggregator/src/publickey_aggregator.rs +++ b/packages/ciphernode/aggregator/src/publickey_aggregator.rs @@ -4,7 +4,7 @@ use enclave_core::{ E3id, EnclaveEvent, EventBus, KeyshareCreated, OrderedSet, PublicKeyAggregated, Seed, }; use fhe::{Fhe, GetAggregatePublicKey}; -use sortition::{GetHasNode, Sortition}; +use sortition::{GetHasNode, GetNodes, Sortition}; use std::sync::Arc; #[derive(Debug, Clone)] @@ -27,6 +27,14 @@ pub enum PublicKeyAggregatorState { #[rtype(result = "anyhow::Result<()>")] struct ComputeAggregate { pub keyshares: OrderedSet>, + pub e3_id: E3id, +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +struct NotifyNetwork { + pub pubkey: Vec, + pub e3_id: E3id, } pub struct PublicKeyAggregator { @@ -35,6 +43,7 @@ pub struct PublicKeyAggregator { sortition: Addr, e3_id: E3id, state: PublicKeyAggregatorState, + src_chain_id: u64, } /// Aggregate PublicKey for a committee of nodes. This actor listens for KeyshareCreated events @@ -51,12 +60,14 @@ impl PublicKeyAggregator { e3_id: E3id, threshold_m: usize, seed: Seed, + src_chain_id: u64, ) -> Self { PublicKeyAggregator { fhe, bus, e3_id, sortition, + src_chain_id, state: PublicKeyAggregatorState::Collecting { threshold_m, keyshares: OrderedSet::new(), @@ -158,6 +169,7 @@ impl Handler for PublicKeyAggregator { if let PublicKeyAggregatorState::Computing { keyshares } = &act.state { ctx.notify(ComputeAggregate { keyshares: keyshares.clone(), + e3_id, }) } @@ -170,23 +182,40 @@ impl Handler for PublicKeyAggregator { impl Handler for PublicKeyAggregator { type Result = Result<()>; - fn handle(&mut self, msg: ComputeAggregate, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ComputeAggregate, ctx: &mut Self::Context) -> Self::Result { let pubkey = self.fhe.get_aggregate_public_key(GetAggregatePublicKey { keyshares: msg.keyshares.clone(), })?; // Update the local state self.state = self.set_pubkey(pubkey.clone())?; - - // Dispatch the PublicKeyAggregated event - let event = EnclaveEvent::from(PublicKeyAggregated { + ctx.notify(NotifyNetwork { pubkey, - e3_id: self.e3_id.clone(), + e3_id: msg.e3_id, }); - - self.bus.do_send(event); - - // Return Ok(()) } } + +impl Handler for PublicKeyAggregator { + type Result = ResponseActFuture>; + fn handle(&mut self, msg: NotifyNetwork, _: &mut Self::Context) -> Self::Result { + Box::pin( + self.sortition + .send(GetNodes) + .into_actor(self) + .map(move |res, act, _| { + let nodes = res?; + + let event = EnclaveEvent::from(PublicKeyAggregated { + pubkey: msg.pubkey.clone(), + e3_id: msg.e3_id.clone(), + nodes: OrderedSet::from(nodes), + src_chain_id: act.src_chain_id, + }); + act.bus.do_send(event); + Ok(()) + }), + ) + } +} diff --git a/packages/ciphernode/core/src/eventbus.rs b/packages/ciphernode/core/src/eventbus.rs index 6ee89ad5..82bb41cb 100644 --- a/packages/ciphernode/core/src/eventbus.rs +++ b/packages/ciphernode/core/src/eventbus.rs @@ -1,7 +1,9 @@ use actix::prelude::*; use std::collections::{HashMap, HashSet}; -use super::events::{EnclaveEvent, EventId}; +use crate::EnclaveErrorType; + +use super::events::{EnclaveEvent, EventId, FromError}; #[derive(Message, Debug)] #[rtype(result = "()")] @@ -113,3 +115,19 @@ impl Handler for EventBus { } } } + +/// Trait to send errors directly to the bus +pub trait BusError { + fn err(&self, err_type: EnclaveErrorType, err: anyhow::Error); +} + +impl BusError for Addr { + fn err(&self, err_type: EnclaveErrorType, err: anyhow::Error) { + self.do_send(EnclaveEvent::from_error(err_type, err)) + } +} +impl BusError for Recipient { + fn err(&self, err_type: EnclaveErrorType, err: anyhow::Error) { + self.do_send(EnclaveEvent::from_error(err_type, err)) + } +} diff --git a/packages/ciphernode/core/src/events.rs b/packages/ciphernode/core/src/events.rs index 22b70648..e6b281e3 100644 --- a/packages/ciphernode/core/src/events.rs +++ b/packages/ciphernode/core/src/events.rs @@ -1,5 +1,9 @@ use actix::Message; -use alloy::{hex, primitives::Uint}; +use alloy::{ + hex, + primitives::{Uint, U256}, +}; +use alloy_primitives::ruint::ParseError; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ @@ -7,6 +11,8 @@ use std::{ hash::{DefaultHasher, Hash, Hasher}, }; +use crate::OrderedSet; + #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct E3id(pub String); impl fmt::Display for E3id { @@ -39,6 +45,13 @@ impl From<&str> for E3id { } } +impl TryFrom for U256 { + type Error = ParseError; + fn try_from(value: E3id) -> Result { + U256::from_str_radix(&value.0, 10) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct EventId(pub [u8; 32]); @@ -296,6 +309,8 @@ pub struct DecryptionshareCreated { pub struct PublicKeyAggregated { pub pubkey: Vec, pub e3_id: E3id, + pub nodes: OrderedSet, + pub src_chain_id: u64, } #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -305,11 +320,11 @@ pub struct E3Requested { pub threshold_m: usize, pub seed: Seed, pub params: Vec, - // threshold: usize, // TODO: - // computation_type: ??, // TODO: - // execution_model_type: ??, // TODO: - // input_deadline: ??, // TODO: - // availability_duration: ??, // TODO: + pub src_chain_id: u64, // threshold: usize, // TODO: + // computation_type: ??, // TODO: + // execution_model_type: ??, // TODO: + // input_deadline: ??, // TODO: + // availability_duration: ??, // TODO: } #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -331,6 +346,7 @@ pub struct CiphertextOutputPublished { pub struct PlaintextAggregated { pub e3_id: E3id, pub decrypted_output: Vec, + pub src_chain_id: u64, } #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/packages/ciphernode/core/src/ordered_set.rs b/packages/ciphernode/core/src/ordered_set.rs index 31eaedfa..0d0543e2 100644 --- a/packages/ciphernode/core/src/ordered_set.rs +++ b/packages/ciphernode/core/src/ordered_set.rs @@ -1,8 +1,9 @@ +use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::fmt; use std::hash::{Hash, Hasher}; -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct OrderedSet(BTreeSet); impl OrderedSet { @@ -29,6 +30,16 @@ impl OrderedSet { pub fn iter(&self) -> impl Iterator { self.0.iter() } + + pub fn from_iter>(iter: I) -> Self { + let mut set = Self::new(); + set.extend(iter); + set + } + + pub fn extend>(&mut self, iter: I) { + self.0.extend(iter); + } } impl Hash for OrderedSet { @@ -72,6 +83,12 @@ impl<'a, T: Ord> IntoIterator for &'a OrderedSet { } } +impl From> for OrderedSet { + fn from(vec: Vec) -> Self { + Self::from_iter(vec) + } +} + #[cfg(test)] mod tests { use super::*; @@ -191,4 +208,40 @@ mod tests { let vec: Vec<&i32> = (&set).into_iter().collect(); assert_eq!(vec, vec![&1, &2, &3]); } + + #[test] + fn test_from_vec() { + let vec = vec![ + "apple".to_string(), + "banana".to_string(), + "cherry".to_string(), + "apple".to_string(), + ]; + let set: OrderedSet = OrderedSet::from(vec); + + assert_eq!(set.len(), 3); + assert!(set.contains(&"apple".to_string())); + assert!(set.contains(&"banana".to_string())); + assert!(set.contains(&"cherry".to_string())); + + let vec_from_set: Vec<&String> = set.iter().collect(); + assert_eq!(vec_from_set, vec!["apple", "banana", "cherry"]); + } + + #[test] + fn test_extend() { + let mut set = OrderedSet::new(); + set.insert("apple".to_string()); + + set.extend(vec![ + "banana".to_string(), + "cherry".to_string(), + "apple".to_string(), + ]); + + assert_eq!(set.len(), 3); + assert!(set.contains(&"apple".to_string())); + assert!(set.contains(&"banana".to_string())); + assert!(set.contains(&"cherry".to_string())); + } } diff --git a/packages/ciphernode/enclave/Cargo.toml b/packages/ciphernode/enclave/Cargo.toml index 9bb7bd96..992aadf7 100644 --- a/packages/ciphernode/enclave/Cargo.toml +++ b/packages/ciphernode/enclave/Cargo.toml @@ -13,3 +13,4 @@ alloy = { workspace = true } clap = { workspace = true } actix-rt = { workspace = true } tokio = { workspace = true } +config = "0.14.0" diff --git a/packages/ciphernode/enclave/src/bin/aggregator.rs b/packages/ciphernode/enclave/src/bin/aggregator.rs index dc3b1745..cad49a8e 100644 --- a/packages/ciphernode/enclave/src/bin/aggregator.rs +++ b/packages/ciphernode/enclave/src/bin/aggregator.rs @@ -1,43 +1,32 @@ -use alloy::primitives::Address; use clap::Parser; +use enclave::load_config; use enclave_node::MainAggregator; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(short = 'r', long)] - rpc: String, - #[arg(short = 'e', long = "enclave-contract")] - enclave_contract: String, - #[arg(short = 'c', long = "registry-contract")] - registry_contract: String, - #[arg(short = 'f', long = "registry-filter-contract")] - registry_filter_contract: String, - #[arg(short = 'p', long = "pubkey-write-path")] - pubkey_write_path: Option, - #[arg(short = 't', long = "plaintext-write-path")] - plaintext_write_path: Option, + #[arg(short, long)] + pub config: String, + + // These are for testing and may be removed later + // or put under a compile flag + #[arg(short = 'k', long = "pubkey-write-path")] + pub pubkey_write_path: Option, + #[arg(short, long = "plaintext-write-path")] + pub plaintext_write_path: Option, } #[actix_rt::main] async fn main() -> Result<(), Box> { let args = Args::parse(); println!("LAUNCHING AGGREGATOR"); - let registry_contract = - Address::parse_checksummed(&args.registry_contract, None).expect("Invalid address"); - let registry_filter_contract = - Address::parse_checksummed(&args.registry_filter_contract, None).expect("Invalid address"); - let enclave_contract = - Address::parse_checksummed(&args.enclave_contract, None).expect("Invalid address"); + let config = load_config(&args.config)?; let (_, handle) = MainAggregator::attach( - &args.rpc, - enclave_contract, - registry_contract, - registry_filter_contract, + config, args.pubkey_write_path.as_deref(), args.plaintext_write_path.as_deref(), ) - .await; + .await?; let _ = tokio::join!(handle); Ok(()) } diff --git a/packages/ciphernode/enclave/src/lib.rs b/packages/ciphernode/enclave/src/lib.rs new file mode 100644 index 00000000..6a439030 --- /dev/null +++ b/packages/ciphernode/enclave/src/lib.rs @@ -0,0 +1,13 @@ +use config::{Config, ConfigError, File}; +use enclave_node::AppConfig; + +pub fn load_config(config_path: &str) -> Result { + let config_builder = Config::builder() + .add_source(File::with_name(&config_path).required(true)) + .build()?; + + // TODO: How do we ensure things like eth addresses are in a valid format? + let config: AppConfig = config_builder.try_deserialize()?; + + Ok(config) +} diff --git a/packages/ciphernode/enclave/src/main.rs b/packages/ciphernode/enclave/src/main.rs index 5b8fabe7..1c67b053 100644 --- a/packages/ciphernode/enclave/src/main.rs +++ b/packages/ciphernode/enclave/src/main.rs @@ -1,5 +1,6 @@ use alloy::primitives::Address; use clap::Parser; +use enclave::load_config; use enclave_node::MainCiphernode; const OWO: &str = r#" @@ -19,15 +20,11 @@ const OWO: &str = r#" #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct Args { - #[arg(short = 'a', long)] - address: String, - #[arg(short = 'r', long)] - rpc: String, - #[arg(short = 'e', long = "enclave-contract")] - enclave_contract: String, - #[arg(short = 'c', long = "registry-contract")] - registry_contract: String, +pub struct Args { + #[arg(short, long)] + pub address: String, + #[arg(short, long)] + pub config: String, } #[actix_rt::main] @@ -37,12 +34,8 @@ async fn main() -> Result<(), Box> { let args = Args::parse(); let address = Address::parse_checksummed(&args.address, None).expect("Invalid address"); println!("LAUNCHING CIPHERNODE: ({})", address); - let registry_contract = - Address::parse_checksummed(&args.registry_contract, None).expect("Invalid address"); - let enclave_contract = - Address::parse_checksummed(&args.enclave_contract, None).expect("Invalid address"); - let (_, handle) = - MainCiphernode::attach(address, &args.rpc, enclave_contract, registry_contract).await; + let config = load_config(&args.config)?; + let (_, handle) = MainCiphernode::attach(config, address).await?; let _ = tokio::join!(handle); Ok(()) } diff --git a/packages/ciphernode/enclave_node/Cargo.toml b/packages/ciphernode/enclave_node/Cargo.toml index dc40e29d..ea0e68bc 100644 --- a/packages/ciphernode/enclave_node/Cargo.toml +++ b/packages/ciphernode/enclave_node/Cargo.toml @@ -27,6 +27,7 @@ p2p = { path = "../p2p" } rand = { workspace = true } rand_chacha = { workspace = true } router = { path = "../router" } +serde = { workspace = true } sortition = { path = "../sortition" } test-helpers = { path = "../test_helpers" } tokio = { workspace = true } diff --git a/packages/ciphernode/enclave_node/src/aggregator.rs b/packages/ciphernode/enclave_node/src/aggregator.rs index d1048a8e..965e467f 100644 --- a/packages/ciphernode/enclave_node/src/aggregator.rs +++ b/packages/ciphernode/enclave_node/src/aggregator.rs @@ -1,20 +1,21 @@ use actix::{Actor, Addr, Context}; -use alloy::primitives::Address; +use anyhow::Result; use enclave_core::EventBus; -use evm::{connect_evm_caller, connect_evm_ciphernode_registry, connect_evm_enclave}; +use evm::{ + helpers::pull_eth_signer_from_env, CiphernodeRegistrySol, EnclaveSol, RegistryFilterSol, +}; use logger::SimpleLogger; use p2p::P2p; use rand::SeedableRng; use rand_chacha::rand_core::OsRng; -use router::{ - CommitteeMetaFactory, E3RequestRouter, LazyFhe, LazyPlaintextAggregator, - LazyPublicKeyAggregator, -}; +use router::{E3RequestRouter, LazyFhe, LazyPlaintextAggregator, LazyPublicKeyAggregator}; use sortition::Sortition; use std::sync::{Arc, Mutex}; use test_helpers::{PlaintextWriter, PublicKeyWriter}; use tokio::task::JoinHandle; +use crate::app_config::AppConfig; + /// Main Ciphernode Actor /// Suprvises all children // TODO: add supervision logic @@ -41,30 +42,44 @@ impl MainAggregator { } pub async fn attach( - rpc_url: &str, - enclave_contract: Address, - registry_contract: Address, - registry_filter_contract: Address, + config: AppConfig, pubkey_write_path: Option<&str>, plaintext_write_path: Option<&str>, - ) -> (Addr, JoinHandle<()>) { + ) -> Result<(Addr, JoinHandle<()>)> { let bus = EventBus::new(true).start(); let rng = Arc::new(Mutex::new( rand_chacha::ChaCha20Rng::from_rng(OsRng).expect("Failed to create RNG"), )); let sortition = Sortition::attach(bus.clone()); - - connect_evm_enclave(bus.clone(), rpc_url, enclave_contract).await; - let _ = connect_evm_ciphernode_registry(bus.clone(), rpc_url, registry_contract).await; - let _ = connect_evm_caller( - bus.clone(), - sortition.clone(), - rpc_url, - enclave_contract, - registry_filter_contract, - ) - .await; + let signer = pull_eth_signer_from_env("PRIVATE_KEY").await?; + for chain in config + .chains + .iter() + .filter(|chain| chain.enabled.unwrap_or(true)) + { + let rpc_url = &chain.rpc_url; + EnclaveSol::attach( + bus.clone(), + rpc_url, + &chain.contracts.enclave, + signer.clone(), + ) + .await?; + RegistryFilterSol::attach( + bus.clone(), + rpc_url, + &chain.contracts.filter_registry, + signer.clone(), + ) + .await?; + CiphernodeRegistrySol::attach( + bus.clone(), + rpc_url, + &chain.contracts.ciphernode_registry, + ) + .await?; + } let e3_manager = E3RequestRouter::builder(bus.clone()) .add_hook(LazyFhe::create(rng)) @@ -92,7 +107,7 @@ impl MainAggregator { SimpleLogger::attach("AGG", bus.clone()); let main_addr = MainAggregator::new(bus, sortition, p2p_addr, e3_manager).start(); - (main_addr, join_handle) + Ok((main_addr, join_handle)) } } diff --git a/packages/ciphernode/enclave_node/src/app_config.rs b/packages/ciphernode/enclave_node/src/app_config.rs new file mode 100644 index 00000000..42cd8587 --- /dev/null +++ b/packages/ciphernode/enclave_node/src/app_config.rs @@ -0,0 +1,23 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct ContractAddresses { + pub enclave: String, + pub ciphernode_registry: String, + pub filter_registry: String, +} + +#[derive(Debug, Deserialize)] +pub struct ChainConfig { + pub enabled: Option, + pub name: String, + pub rpc_url: String, // We may need multiple per chain for redundancy at a later point + pub contracts: ContractAddresses, +} + +#[derive(Debug, Deserialize)] +pub struct AppConfig { + pub chains: Vec, +} diff --git a/packages/ciphernode/enclave_node/src/ciphernode.rs b/packages/ciphernode/enclave_node/src/ciphernode.rs index e55914ac..d2644324 100644 --- a/packages/ciphernode/enclave_node/src/ciphernode.rs +++ b/packages/ciphernode/enclave_node/src/ciphernode.rs @@ -1,17 +1,20 @@ use actix::{Actor, Addr, Context}; use alloy::primitives::Address; +use anyhow::Result; use data::Data; use enclave_core::EventBus; -use evm::{connect_evm_ciphernode_registry, connect_evm_enclave}; +use evm::{CiphernodeRegistrySol, EnclaveSolReader}; use logger::SimpleLogger; use p2p::P2p; use rand::SeedableRng; use rand_chacha::rand_core::OsRng; -use router::{CiphernodeSelector, CommitteeMetaFactory, E3RequestRouter, LazyFhe, LazyKeyshare}; +use router::{CiphernodeSelector, E3RequestRouter, LazyFhe, LazyKeyshare}; use sortition::Sortition; use std::sync::{Arc, Mutex}; use tokio::task::JoinHandle; +use crate::app_config::AppConfig; + /// Main Ciphernode Actor /// Suprvises all children // TODO: add supervision logic @@ -47,11 +50,9 @@ impl MainCiphernode { } pub async fn attach( + config: AppConfig, address: Address, - rpc_url: &str, - enclave_contract: Address, - registry_contract: Address, - ) -> (Addr, JoinHandle<()>) { + ) -> Result<(Addr, JoinHandle<()>)> { let rng = Arc::new(Mutex::new( rand_chacha::ChaCha20Rng::from_rng(OsRng).expect("Failed to create RNG"), )); @@ -61,8 +62,21 @@ impl MainCiphernode { let selector = CiphernodeSelector::attach(bus.clone(), sortition.clone(), &address.to_string()); - connect_evm_enclave(bus.clone(), rpc_url, enclave_contract).await; - let _ = connect_evm_ciphernode_registry(bus.clone(), rpc_url, registry_contract).await; + for chain in config + .chains + .iter() + .filter(|chain| chain.enabled.unwrap_or(true)) + { + let rpc_url = &chain.rpc_url; + + EnclaveSolReader::attach(bus.clone(), rpc_url, &chain.contracts.enclave).await?; + CiphernodeRegistrySol::attach( + bus.clone(), + rpc_url, + &chain.contracts.ciphernode_registry, + ) + .await?; + } let e3_manager = E3RequestRouter::builder(bus.clone()) .add_hook(LazyFhe::create(rng)) @@ -82,7 +96,7 @@ impl MainCiphernode { address, bus, data, sortition, selector, p2p_addr, e3_manager, ) .start(); - (main_addr, join_handle) + Ok((main_addr, join_handle)) } } diff --git a/packages/ciphernode/enclave_node/src/lib.rs b/packages/ciphernode/enclave_node/src/lib.rs index a0e3ff23..96fffacb 100644 --- a/packages/ciphernode/enclave_node/src/lib.rs +++ b/packages/ciphernode/enclave_node/src/lib.rs @@ -1,5 +1,8 @@ mod aggregator; +mod app_config; mod ciphernode; pub use aggregator::*; pub use ciphernode::*; + +pub use app_config::*; diff --git a/packages/ciphernode/evm/Cargo.toml b/packages/ciphernode/evm/Cargo.toml index d99406de..05c53930 100644 --- a/packages/ciphernode/evm/Cargo.toml +++ b/packages/ciphernode/evm/Cargo.toml @@ -11,3 +11,4 @@ anyhow = { workspace = true } enclave-core = { path = "../core" } futures-util = { workspace = true } sortition = { path = "../sortition" } +tokio = { workspace = true } diff --git a/packages/ciphernode/evm/src/caller.rs b/packages/ciphernode/evm/src/caller.rs deleted file mode 100644 index 9f2c8b5c..00000000 --- a/packages/ciphernode/evm/src/caller.rs +++ /dev/null @@ -1,156 +0,0 @@ -use actix::prelude::*; -use alloy::primitives::{Address, Bytes, U256}; -use std::collections::HashMap; -use std::sync::Arc; - -use enclave_core::{EnclaveEvent, EventBus, Subscribe}; -use sortition::{GetNodes, Sortition}; - -use super::EVMContract; - -pub struct EvmCaller { - contracts: HashMap>, - bus: Addr, - sortition: Addr, -} - -impl Actor for EvmCaller { - type Context = Context; -} - -impl EvmCaller { - pub fn new(bus: Addr, sortition: Addr) -> Self { - Self { - contracts: HashMap::new(), - bus, - sortition, - } - } - - pub fn add_contract(&mut self, name: &str, contract: Arc) { - self.contracts.insert(name.to_string(), contract); - } - - pub fn attach(bus: Addr, sortition: Addr) -> Addr { - let addr = Self::new(bus.clone(), sortition).start(); - - bus.do_send(Subscribe::new( - "PublicKeyAggregated", - addr.clone().recipient(), - )); - - bus.do_send(Subscribe::new( - "PlaintextAggregated", - addr.clone().recipient(), - )); - - addr - } -} - -#[derive(Message)] -#[rtype(result = "()")] -pub struct AddContract { - pub name: String, - pub contract: Arc, -} - -impl Handler for EvmCaller { - type Result = (); - - fn handle(&mut self, msg: AddContract, _: &mut Self::Context) -> Self::Result { - self.add_contract(&msg.name, msg.contract); - } -} - -impl Handler for EvmCaller { - type Result = ResponseActFuture; - - fn handle(&mut self, msg: EnclaveEvent, _ctx: &mut Self::Context) -> Self::Result { - let contracts = self.contracts.clone(); - let sortition = self.sortition.clone(); - - Box::pin( - async move { - match msg { - EnclaveEvent::PublicKeyAggregated { data, .. } => { - if let Some(contract) = contracts.get("registry") { - let nodes = sortition.send(GetNodes).await.unwrap_or_default(); - let nodes: Vec
= nodes - .into_iter() - .filter_map(|node| node.parse().ok()) - .collect(); - - match contract - .publish_committee( - U256::from_str_radix(&data.e3_id.0, 10).unwrap(), - nodes, - Bytes::from(data.pubkey), - ) - .await - { - Ok(tx) => println!( - "Published committee public key {:?}", - tx.transaction_hash - ), - Err(e) => { - eprintln!("Failed to publish committee public key: {:?}", e) - } - } - } - } - EnclaveEvent::PlaintextAggregated { data, .. } => { - if let Some(contract) = contracts.get("enclave") { - println!("Publishing plaintext output {:?}", data.e3_id); - match contract - .publish_plaintext_output( - U256::from_str_radix(&data.e3_id.0, 10).unwrap(), - Bytes::from(data.decrypted_output), - Bytes::from(vec![1]), // TODO: Implement proof generation - ) - .await - { - Ok(tx) => { - println!("Published plaintext output {:?}", tx.transaction_hash) - } - Err(e) => eprintln!("Failed to publish plaintext: {:?}", e), - } - } - } - _ => {} - } - } - .into_actor(self) - .map(|_, _, _| ()), - ) - } -} - -pub async fn connect_evm_caller( - bus: Addr, - sortition: Addr, - rpc_url: &str, - enclave_contract: Address, - registry_contract: Address, -) -> Result, anyhow::Error> { - let evm_caller = EvmCaller::attach(bus.clone(), sortition.clone()); - - let enclave_instance = EVMContract::new(rpc_url, enclave_contract).await?; - let registry_instance = EVMContract::new(rpc_url, registry_contract).await?; - - evm_caller - .send(AddContract { - name: "enclave".to_string(), - contract: Arc::new(enclave_instance), - }) - .await?; - - evm_caller - .send(AddContract { - name: "registry".to_string(), - contract: Arc::new(registry_instance), - }) - .await?; - - Ok(evm_caller) -} diff --git a/packages/ciphernode/evm/src/ciphernode_registry.rs b/packages/ciphernode/evm/src/ciphernode_registry.rs deleted file mode 100644 index a1a1283b..00000000 --- a/packages/ciphernode/evm/src/ciphernode_registry.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::ContractEvent; -use crate::{AddEventHandler, AddListener, EvmContractManager, StartListening}; -use actix::Addr; -use alloy::{primitives::Address, sol}; -use anyhow::Result; -use enclave_core::{EnclaveEvent, EventBus}; - -sol! { - #[derive(Debug)] - event CiphernodeAdded( - address indexed node, - uint256 index, - uint256 numNodes, - uint256 size - ); - - #[derive(Debug)] - event CiphernodeRemoved( - address indexed node, - uint256 index, - uint256 numNodes, - uint256 size - ); -} - -impl From for enclave_core::CiphernodeAdded { - fn from(value: CiphernodeAdded) -> Self { - enclave_core::CiphernodeAdded { - address: value.node.to_string(), - // TODO: limit index and numNodes to uint32 at the solidity level - index: value - .index - .try_into() - .expect("Index exceeds usize capacity"), - num_nodes: value - .numNodes - .try_into() - .expect("NumNodes exceeds usize capacity"), - } - } -} - -impl From for enclave_core::CiphernodeRemoved { - fn from(value: CiphernodeRemoved) -> Self { - enclave_core::CiphernodeRemoved { - address: value.node.to_string(), - index: value - .index - .try_into() - .expect("Index exceeds usize capacity"), - num_nodes: value - .numNodes - .try_into() - .expect("NumNodes exceeds usize capacity"), - } - } -} - -impl ContractEvent for CiphernodeAdded { - fn process(&self, bus: Addr) -> Result<()> { - let data: enclave_core::CiphernodeAdded = self.clone().into(); - bus.do_send(EnclaveEvent::from(data)); - Ok(()) - } -} - -impl ContractEvent for CiphernodeRemoved { - fn process(&self, bus: Addr) -> Result<()> { - let data: enclave_core::CiphernodeRemoved = self.clone().into(); - bus.do_send(EnclaveEvent::from(data)); - Ok(()) - } -} - -pub async fn connect_evm_ciphernode_registry( - bus: Addr, - rpc_url: &str, - contract_address: Address, -) -> Result<()> { - let evm_manager = EvmContractManager::attach(bus.clone(), rpc_url).await; - let evm_listener = evm_manager.send(AddListener { contract_address }).await?; - - evm_listener - .send(AddEventHandler::::new()) - .await?; - - evm_listener - .send(AddEventHandler::::new()) - .await?; - - evm_listener.do_send(StartListening); - - println!("Evm is listening to {}", contract_address); - Ok(()) -} diff --git a/packages/ciphernode/evm/src/ciphernode_registry_sol.rs b/packages/ciphernode/evm/src/ciphernode_registry_sol.rs new file mode 100644 index 00000000..fa90c23b --- /dev/null +++ b/packages/ciphernode/evm/src/ciphernode_registry_sol.rs @@ -0,0 +1,152 @@ +use actix::{Actor, Addr, AsyncContext, Recipient, WrapFuture}; +use alloy::{ + eips::BlockNumberOrTag, + primitives::{Address, LogData, B256}, + rpc::types::Filter, + sol, + sol_types::SolEvent, +}; +use anyhow::Result; +use enclave_core::{EnclaveEvent, EventBus}; + +use crate::helpers::{self, create_readonly_provider, ReadonlyProvider}; + +sol!( + #[sol(rpc)] + ICiphernodeRegistry, + "../../evm/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json" +); + +impl From for enclave_core::CiphernodeAdded { + fn from(value: ICiphernodeRegistry::CiphernodeAdded) -> Self { + enclave_core::CiphernodeAdded { + address: value.node.to_string(), + // TODO: limit index and numNodes to uint32 at the solidity level + index: value + .index + .try_into() + .expect("Index exceeds usize capacity"), + num_nodes: value + .numNodes + .try_into() + .expect("NumNodes exceeds usize capacity"), + } + } +} + +impl From for EnclaveEvent { + fn from(value: ICiphernodeRegistry::CiphernodeAdded) -> Self { + let payload: enclave_core::CiphernodeAdded = value.into(); + EnclaveEvent::from(payload) + } +} + +impl From for enclave_core::CiphernodeRemoved { + fn from(value: ICiphernodeRegistry::CiphernodeRemoved) -> Self { + enclave_core::CiphernodeRemoved { + address: value.node.to_string(), + index: value + .index + .try_into() + .expect("Index exceeds usize capacity"), + num_nodes: value + .numNodes + .try_into() + .expect("NumNodes exceeds usize capacity"), + } + } +} + +impl From for EnclaveEvent { + fn from(value: ICiphernodeRegistry::CiphernodeRemoved) -> Self { + let payload: enclave_core::CiphernodeRemoved = value.into(); + EnclaveEvent::from(payload) + } +} + +fn extractor(data: &LogData, topic: Option<&B256>, _: u64) -> Option { + match topic { + Some(&ICiphernodeRegistry::CiphernodeAdded::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CiphernodeAdded::decode_log_data(data, true) + else { + println!("Error parsing event CiphernodeAdded"); // TODO: provide more info + return None; + }; + Some(EnclaveEvent::from(event)) + } + Some(&ICiphernodeRegistry::CiphernodeRemoved::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CiphernodeRemoved::decode_log_data(data, true) + else { + println!("Error parsing event CiphernodeRemoved"); // TODO: provide more info + return None; + }; + Some(EnclaveEvent::from(event)) + } + + _ => { + println!("Unknown event"); + return None; + } + } +} + +/// Connects to CiphernodeRegistry.sol converting EVM events to EnclaveEvents +pub struct CiphernodeRegistrySolReader { + provider: ReadonlyProvider, + contract_address: Address, + bus: Recipient, +} + +impl CiphernodeRegistrySolReader { + pub async fn new( + bus: Addr, + contract_address: Address, + rpc_url: &str, + ) -> Result { + let provider = create_readonly_provider(rpc_url).await?; + + Ok(Self { + contract_address, + provider, + bus: bus.into(), + }) + } + + pub async fn attach( + bus: Addr, + rpc_url: &str, + contract_address: &str, + ) -> Result> { + let addr = + CiphernodeRegistrySolReader::new(bus.clone(), contract_address.parse()?, rpc_url) + .await? + .start(); + + println!("CiphernodeRegistrySol is listening to {}", contract_address); + Ok(addr) + } +} + +impl Actor for CiphernodeRegistrySolReader { + type Context = actix::Context; + fn started(&mut self, ctx: &mut Self::Context) { + let bus = self.bus.clone(); + let provider = self.provider.clone(); + let filter = Filter::new() + .address(self.contract_address) + .from_block(BlockNumberOrTag::Latest); + + ctx.spawn( + async move { helpers::stream_from_evm(provider, filter, bus, extractor).await } + .into_actor(self), + ); + } +} + +pub struct CiphernodeRegistrySol; +impl CiphernodeRegistrySol { + pub async fn attach(bus: Addr, rpc_url: &str, contract_address: &str) -> Result<()> { + CiphernodeRegistrySolReader::attach(bus.clone(), rpc_url, contract_address).await?; + Ok(()) + } +} diff --git a/packages/ciphernode/evm/src/contracts.rs b/packages/ciphernode/evm/src/contracts.rs deleted file mode 100644 index 557dac21..00000000 --- a/packages/ciphernode/evm/src/contracts.rs +++ /dev/null @@ -1,85 +0,0 @@ -use alloy::{ - network::{Ethereum, EthereumWallet}, - primitives::{Address, Bytes, U256}, - providers::{ - fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller}, - Identity, ProviderBuilder, RootProvider, - }, - rpc::types::TransactionReceipt, - signers::local::PrivateKeySigner, - sol, - transports::BoxTransport, -}; -use anyhow::Result; -use std::env; -use std::sync::Arc; - -sol! { - #[derive(Debug)] - #[sol(rpc)] - contract Enclave { - function publishPlaintextOutput(uint256 e3Id, bytes memory plaintextOutput, bytes memory proof) external returns (bool success); - } - - #[derive(Debug)] - #[sol(rpc)] - contract RegistryFilter { - function publishCommittee(uint256 e3Id, address[] memory nodes, bytes memory publicKey) external onlyOwner; - } -} - -type ContractProvider = FillProvider< - JoinFill< - JoinFill, NonceFiller>, ChainIdFiller>, - WalletFiller, - >, - RootProvider, - BoxTransport, - Ethereum, ->; - -pub struct EVMContract { - pub provider: Arc, - pub contract_address: Address, -} - -impl EVMContract { - pub async fn new(rpc_url: &str, contract_address: Address) -> Result { - let signer: PrivateKeySigner = env::var("PRIVATE_KEY")?.parse()?; - let wallet = EthereumWallet::from(signer.clone()); - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .wallet(wallet) - .on_builtin(rpc_url) - .await?; - - Ok(Self { - provider: Arc::new(provider), - contract_address, - }) - } - - pub async fn publish_plaintext_output( - &self, - e3_id: U256, - plaintext_output: Bytes, - proof: Bytes, - ) -> Result { - let contract = Enclave::new(self.contract_address, &self.provider); - let builder = contract.publishPlaintextOutput(e3_id, plaintext_output, proof); - let receipt = builder.send().await?.get_receipt().await?; - Ok(receipt) - } - - pub async fn publish_committee( - &self, - e3_id: U256, - nodes: Vec
, - public_key: Bytes, - ) -> Result { - let contract = RegistryFilter::new(self.contract_address, &self.provider); - let builder = contract.publishCommittee(e3_id, nodes, public_key); - let receipt = builder.send().await?.get_receipt().await?; - Ok(receipt) - } -} diff --git a/packages/ciphernode/evm/src/enclave.rs b/packages/ciphernode/evm/src/enclave.rs deleted file mode 100644 index 9e482cb0..00000000 --- a/packages/ciphernode/evm/src/enclave.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::listener::ContractEvent; -use crate::{AddEventHandler, AddListener, EvmContractManager, StartListening}; -use actix::Addr; -use alloy::{primitives::Address, sol}; -use anyhow::Result; -use enclave_core::{EnclaveEvent, EventBus}; - -sol! { - #[derive(Debug)] - struct E3 { - uint256 seed; - uint32[2] threshold; - uint256[2] startWindow; - uint256 duration; - uint256 expiration; - bytes32 encryptionSchemeId; - address e3Program; - bytes e3ProgramParams; - address inputValidator; - address decryptionVerifier; - bytes32 committeePublicKey; - bytes32 ciphertextOutput; - bytes plaintextOutput; - } - - #[derive(Debug)] - event CiphertextOutputPublished( - uint256 indexed e3Id, - bytes ciphertextOutput - ); - - #[derive(Debug)] - event E3Requested( - uint256 e3Id, - E3 e3, - address filter, - address indexed e3Program - ); -} - -impl TryFrom<&E3Requested> for enclave_core::E3Requested { - type Error = anyhow::Error; - fn try_from(value: &E3Requested) -> Result { - let program_params = value.e3.e3ProgramParams.to_vec(); - Ok(enclave_core::E3Requested { - params: program_params.into(), - threshold_m: value.e3.threshold[0] as usize, - seed: value.e3.seed.into(), - e3_id: value.e3Id.to_string().into(), - }) - } -} - -impl From for enclave_core::CiphertextOutputPublished { - fn from(value: CiphertextOutputPublished) -> Self { - enclave_core::CiphertextOutputPublished { - e3_id: value.e3Id.to_string().into(), - ciphertext_output: value.ciphertextOutput.to_vec(), - } - } -} - -impl ContractEvent for E3Requested { - fn process(&self, bus: Addr) -> Result<()> { - let data: enclave_core::E3Requested = self.try_into()?; - - bus.do_send(EnclaveEvent::from(data)); - Ok(()) - } -} - -impl ContractEvent for CiphertextOutputPublished { - fn process(&self, bus: Addr) -> Result<()> { - let data: enclave_core::CiphertextOutputPublished = self.clone().into(); - bus.do_send(EnclaveEvent::from(data)); - Ok(()) - } -} - -pub async fn connect_evm_enclave(bus: Addr, rpc_url: &str, contract_address: Address) { - let evm_manager = EvmContractManager::attach(bus.clone(), rpc_url).await; - let evm_listener = evm_manager - .send(AddListener { contract_address }) - .await - .unwrap(); - - evm_listener - .send(AddEventHandler::::new()) - .await - .unwrap(); - - evm_listener - .send(AddEventHandler::::new()) - .await - .unwrap(); - evm_listener.do_send(StartListening); - - println!("Evm is listening to {}", contract_address); -} diff --git a/packages/ciphernode/evm/src/enclave_sol.rs b/packages/ciphernode/evm/src/enclave_sol.rs new file mode 100644 index 00000000..ba1631c0 --- /dev/null +++ b/packages/ciphernode/evm/src/enclave_sol.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use crate::{enclave_sol_reader::EnclaveSolReader, enclave_sol_writer::EnclaveSolWriter}; +use actix::Addr; +use alloy::signers::local::PrivateKeySigner; +use anyhow::Result; +use enclave_core::EventBus; + +pub struct EnclaveSol; +impl EnclaveSol { + pub async fn attach( + bus: Addr, + rpc_url: &str, + contract_address: &str, + signer: Arc, + ) -> Result<()> { + EnclaveSolReader::attach(bus.clone(), rpc_url, contract_address).await?; + EnclaveSolWriter::attach(bus, rpc_url, contract_address, signer).await?; + Ok(()) + } +} diff --git a/packages/ciphernode/evm/src/enclave_sol_reader.rs b/packages/ciphernode/evm/src/enclave_sol_reader.rs new file mode 100644 index 00000000..5fc561f5 --- /dev/null +++ b/packages/ciphernode/evm/src/enclave_sol_reader.rs @@ -0,0 +1,127 @@ +use crate::helpers::{self, create_readonly_provider, ReadonlyProvider}; +use actix::prelude::*; +use actix::{Addr, Recipient}; +use alloy::primitives::{LogData, B256}; +use alloy::{ + eips::BlockNumberOrTag, primitives::Address, rpc::types::Filter, sol, sol_types::SolEvent, +}; +use anyhow::Result; +use enclave_core::{EnclaveEvent, EventBus}; + +sol!( + #[sol(rpc)] + IEnclave, + "../../evm/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json" +); + +struct E3RequestedWithChainId(pub IEnclave::E3Requested, pub u64); + +impl From for enclave_core::E3Requested { + fn from(value: E3RequestedWithChainId) -> Self { + enclave_core::E3Requested { + params: value.0.e3.e3ProgramParams.to_vec(), + threshold_m: value.0.e3.threshold[0] as usize, + seed: value.0.e3.seed.into(), + e3_id: value.0.e3Id.to_string().into(), + src_chain_id: value.1, + } + } +} + +impl From for EnclaveEvent { + fn from(value: E3RequestedWithChainId) -> Self { + let payload: enclave_core::E3Requested = value.into(); + EnclaveEvent::from(payload) + } +} + +impl From for enclave_core::CiphertextOutputPublished { + fn from(value: IEnclave::CiphertextOutputPublished) -> Self { + enclave_core::CiphertextOutputPublished { + e3_id: value.e3Id.to_string().into(), + ciphertext_output: value.ciphertextOutput.to_vec(), + } + } +} + +impl From for EnclaveEvent { + fn from(value: IEnclave::CiphertextOutputPublished) -> Self { + let payload: enclave_core::CiphertextOutputPublished = value.into(); + EnclaveEvent::from(payload) + } +} + +fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { + match topic { + Some(&IEnclave::E3Requested::SIGNATURE_HASH) => { + let Ok(event) = IEnclave::E3Requested::decode_log_data(data, true) else { + println!("Error parsing event E3Requested"); // TODO: provide more info + return None; + }; + Some(EnclaveEvent::from(E3RequestedWithChainId(event, chain_id))) + } + Some(&IEnclave::CiphertextOutputPublished::SIGNATURE_HASH) => { + let Ok(event) = IEnclave::CiphertextOutputPublished::decode_log_data(data, true) else { + println!("Error parsing event CiphertextOutputPublished"); // TODO: provide more info + return None; + }; + Some(EnclaveEvent::from(event)) + } + + _ => { + println!("Unknown event"); + return None; + } + } +} + +/// Connects to Enclave.sol converting EVM events to EnclaveEvents +pub struct EnclaveSolReader { + provider: ReadonlyProvider, + contract_address: Address, + bus: Recipient, +} + +impl EnclaveSolReader { + pub async fn new( + bus: Addr, + contract_address: Address, + rpc_url: &str, + ) -> Result { + let provider = create_readonly_provider(rpc_url).await?; + Ok(Self { + contract_address, + provider, + bus: bus.into(), + }) + } + + pub async fn attach( + bus: Addr, + rpc_url: &str, + contract_address: &str, + ) -> Result> { + let addr = EnclaveSolReader::new(bus.clone(), contract_address.parse()?, rpc_url) + .await? + .start(); + + println!("Evm is listening to {}", contract_address); + Ok(addr) + } +} + +impl Actor for EnclaveSolReader { + type Context = actix::Context; + fn started(&mut self, ctx: &mut Self::Context) { + let bus = self.bus.clone(); + let provider = self.provider.clone(); + let filter = Filter::new() + .address(self.contract_address) + .from_block(BlockNumberOrTag::Latest); + + ctx.spawn( + async move { helpers::stream_from_evm(provider, filter, bus, extractor).await } + .into_actor(self), + ); + } +} diff --git a/packages/ciphernode/evm/src/enclave_sol_writer.rs b/packages/ciphernode/evm/src/enclave_sol_writer.rs new file mode 100644 index 00000000..0fdf5bb7 --- /dev/null +++ b/packages/ciphernode/evm/src/enclave_sol_writer.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use crate::helpers::create_provider_with_signer; +use crate::helpers::SignerProvider; +use actix::prelude::*; +use actix::Addr; +use alloy::signers::local::PrivateKeySigner; +use alloy::{primitives::Address, sol}; +use alloy::{ + primitives::{Bytes, U256}, + rpc::types::TransactionReceipt, +}; +use anyhow::Result; +use enclave_core::{BusError, E3id, EnclaveErrorType, PlaintextAggregated, Subscribe}; +use enclave_core::{EnclaveEvent, EventBus}; + +sol!( + #[sol(rpc)] + IEnclave, + "../../evm/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json" +); + +/// Consumes events from the event bus and calls EVM methods on the Enclave.sol contract +pub struct EnclaveSolWriter { + provider: SignerProvider, + contract_address: Address, + bus: Addr, +} + +impl EnclaveSolWriter { + pub async fn new( + bus: Addr, + rpc_url: &str, + contract_address: Address, + signer: Arc, + ) -> Result { + Ok(Self { + provider: create_provider_with_signer(rpc_url, signer).await?, + contract_address, + bus, + }) + } + + pub async fn attach( + bus: Addr, + rpc_url: &str, + contract_address: &str, + signer: Arc, + ) -> Result> { + let addr = EnclaveSolWriter::new(bus.clone(), rpc_url, contract_address.parse()?, signer) + .await? + .start(); + let _ = bus + .send(Subscribe::new("PlaintextAggregated", addr.clone().into())) + .await; + + Ok(addr) + } +} + +impl Actor for EnclaveSolWriter { + type Context = actix::Context; +} + +impl Handler for EnclaveSolWriter { + type Result = (); + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::PlaintextAggregated { data, .. } => { + // Only publish if the src and destination chains match + if self.provider.get_chain_id() == data.src_chain_id { + ctx.notify(data); + } + } + _ => (), + } + } +} + +impl Handler for EnclaveSolWriter { + type Result = ResponseFuture<()>; + fn handle(&mut self, msg: PlaintextAggregated, _: &mut Self::Context) -> Self::Result { + Box::pin({ + let e3_id = msg.e3_id.clone(); + let decrypted_output = msg.decrypted_output.clone(); + let contract_address = self.contract_address.clone(); + let provider = self.provider.clone(); + let bus = self.bus.clone(); + + async move { + let result = + publish_plaintext_output(provider, contract_address, e3_id, decrypted_output) + .await; + match result { + Ok(receipt) => { + println!("tx:{}", receipt.transaction_hash) + } + Err(err) => bus.err(EnclaveErrorType::Evm, err), + } + } + }) + } +} + +async fn publish_plaintext_output( + provider: SignerProvider, + contract_address: Address, + e3_id: E3id, + decrypted_output: Vec, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let decrypted_output = Bytes::from(decrypted_output); + let proof = Bytes::from(vec![1]); + let contract = IEnclave::new(contract_address, provider.get_provider()); + let builder = contract.publishPlaintextOutput(e3_id, decrypted_output, proof); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} diff --git a/packages/ciphernode/evm/src/helpers.rs b/packages/ciphernode/evm/src/helpers.rs new file mode 100644 index 00000000..81a2e7d6 --- /dev/null +++ b/packages/ciphernode/evm/src/helpers.rs @@ -0,0 +1,118 @@ +use std::{env, sync::Arc}; + +use actix::Recipient; +use alloy::{ + network::{Ethereum, EthereumWallet}, + primitives::{LogData, B256}, + providers::{ + fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller}, + Identity, Provider, ProviderBuilder, RootProvider, + }, + rpc::types::Filter, + signers::local::PrivateKeySigner, + transports::BoxTransport, +}; +use anyhow::{Context, Result}; +use enclave_core::{BusError, EnclaveErrorType, EnclaveEvent}; +use futures_util::stream::StreamExt; + +pub async fn stream_from_evm( + provider: WithChainId

, + filter: Filter, + bus: Recipient, + extractor: fn(&LogData, Option<&B256>, u64) -> Option, +) { + match provider + .get_provider() + .subscribe_logs(&filter) + .await + .context("Could not subscribe to stream") + { + Ok(subscription) => { + let mut stream = subscription.into_stream(); + while let Some(log) = stream.next().await { + let Some(event) = extractor(log.data(), log.topic0(), provider.get_chain_id()) + else { + continue; + }; + bus.do_send(event); + } + } + Err(e) => { + bus.err(EnclaveErrorType::Evm, e); + } + } +} + +#[derive(Clone)] +pub struct WithChainId

+where + P: Provider, +{ + provider: Arc

, + chain_id: u64, +} + +impl

WithChainId

+where + P: Provider, +{ + pub async fn new(provider: P) -> Result { + let chain_id = provider.get_chain_id().await?; + Ok(Self { + provider: Arc::new(provider), + chain_id, + }) + } + + pub fn get_provider(&self) -> Arc

{ + self.provider.clone() + } + + pub fn get_chain_id(&self) -> u64 { + self.chain_id + } +} + +pub type ReadonlyProvider = WithChainId>; + +pub async fn create_readonly_provider(rpc_url: &str) -> Result { + let provider = ProviderBuilder::new() + .on_builtin(rpc_url) + .await + .context("Could not create ReadOnlyProvider")? + .into(); + Ok(ReadonlyProvider::new(provider).await?) +} + +pub type SignerProvider = WithChainId< + FillProvider< + JoinFill< + JoinFill, NonceFiller>, ChainIdFiller>, + WalletFiller, + >, + RootProvider, + BoxTransport, + Ethereum, + >, +>; + +pub async fn create_provider_with_signer( + rpc_url: &str, + signer: Arc, +) -> Result { + let wallet = EthereumWallet::from(signer.clone()); + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_builtin(rpc_url) + .await?; + Ok(SignerProvider::new(provider).await?) +} + +pub async fn pull_eth_signer_from_env(var: &str) -> Result> { + let private_key = env::var(var)?; + let signer = private_key.parse()?; + env::remove_var(var); + Ok(Arc::new(signer)) +} diff --git a/packages/ciphernode/evm/src/lib.rs b/packages/ciphernode/evm/src/lib.rs index c977102d..65a5f5bd 100644 --- a/packages/ciphernode/evm/src/lib.rs +++ b/packages/ciphernode/evm/src/lib.rs @@ -1,13 +1,12 @@ -mod caller; -mod ciphernode_registry; -mod contracts; -mod enclave; -mod listener; -mod manager; +mod ciphernode_registry_sol; +mod enclave_sol; +mod enclave_sol_reader; +mod enclave_sol_writer; +pub mod helpers; +mod registry_filter_sol; -pub use caller::*; -pub use ciphernode_registry::*; -pub use contracts::*; -pub use enclave::*; -pub use listener::*; -pub use manager::*; +pub use ciphernode_registry_sol::{CiphernodeRegistrySol, CiphernodeRegistrySolReader}; +pub use enclave_sol::EnclaveSol; +pub use enclave_sol_reader::EnclaveSolReader; +pub use enclave_sol_writer::EnclaveSolWriter; +pub use registry_filter_sol::{RegistryFilterSol, RegistryFilterSolWriter}; diff --git a/packages/ciphernode/evm/src/listener.rs b/packages/ciphernode/evm/src/listener.rs deleted file mode 100644 index 1bac6d7d..00000000 --- a/packages/ciphernode/evm/src/listener.rs +++ /dev/null @@ -1,142 +0,0 @@ -use actix::{Actor, Addr, AsyncContext, Context, Handler, Message, WrapFuture}; -use alloy::{ - primitives::B256, - providers::{Provider, RootProvider}, - rpc::types::{Filter, Log}, - sol_types::SolEvent, - transports::BoxTransport, -}; -use anyhow::Result; -use enclave_core::{EnclaveErrorType, EnclaveEvent, EventBus, FromError}; -use futures_util::stream::StreamExt; -use std::collections::HashMap; -use std::marker::PhantomData; -use std::sync::Arc; - -pub trait ContractEvent: Send + Sync + 'static { - fn process(&self, bus: Addr) -> Result<()>; -} - -pub struct EvmEventListener { - provider: Arc>, - filter: Filter, - handlers: HashMap Result> + Send + Sync>>, - bus: Addr, -} - -impl EvmEventListener { - pub fn new( - provider: Arc>, - filter: Filter, - bus: Addr, - ) -> Self { - Self { - provider, - filter, - handlers: HashMap::new(), - bus, - } - } - - pub fn add_event_handler(&mut self) - where - E: SolEvent + ContractEvent + 'static, - { - let signature = E::SIGNATURE_HASH; - let handler = Arc::new(move |log: Log| -> Result> { - let event = log.log_decode::()?.inner.data; - Ok(Box::new(event)) - }); - self.handlers.insert(signature, handler); - } - - pub async fn listen(&self) -> Result<()> { - let mut stream = self - .provider - .subscribe_logs(&self.filter) - .await? - .into_stream(); - while let Some(log) = stream.next().await { - if let Some(topic0) = log.topic0() { - if let Some(decoder) = self.handlers.get(topic0) { - if let Ok(event) = decoder(log.clone()) { - if let Err(err) = event.process(self.bus.clone()) { - // Send enclave error to bus - self.bus - .clone() - .do_send(EnclaveEvent::from_error(EnclaveErrorType::Evm, err)); - } - } - } - } - } - - Ok(()) - } -} - -impl Actor for EvmEventListener { - type Context = Context; -} - -#[derive(Message)] -#[rtype(result = "()")] -pub struct AddEventHandler -where - E: SolEvent + ContractEvent + 'static, -{ - pub _marker: PhantomData, -} - -impl AddEventHandler -where - E: SolEvent + ContractEvent + 'static, -{ - pub fn new() -> Self { - Self { - _marker: PhantomData, - } - } -} - -impl Handler> for EvmEventListener -where - E: SolEvent + ContractEvent + 'static, -{ - type Result = (); - - fn handle(&mut self, _: AddEventHandler, _: &mut Self::Context) -> Self::Result { - self.add_event_handler::(); - } -} - -#[derive(Message)] -#[rtype(result = "()")] -pub struct StartListening; - -impl Handler for EvmEventListener { - type Result = (); - fn handle(&mut self, _: StartListening, ctx: &mut Self::Context) -> Self::Result { - let (provider, filter, handlers, bus) = ( - self.provider.clone(), - self.filter.clone(), - self.handlers.clone(), - self.bus.clone(), - ); - - ctx.spawn( - async move { - let listener = EvmEventListener { - provider, - filter, - handlers, - bus, - }; - if let Err(err) = listener.listen().await { - eprintln!("Error listening for events: {:?}", err); - } - } - .into_actor(self), - ); - } -} diff --git a/packages/ciphernode/evm/src/manager.rs b/packages/ciphernode/evm/src/manager.rs deleted file mode 100644 index 7c061fdd..00000000 --- a/packages/ciphernode/evm/src/manager.rs +++ /dev/null @@ -1,74 +0,0 @@ -use actix::{Actor, Addr, Context, Handler, Message}; -use alloy::{ - primitives::Address, - providers::{ProviderBuilder, RootProvider}, - rpc::types::{BlockNumberOrTag, Filter}, - transports::BoxTransport, -}; -use anyhow::Result; -use enclave_core::EventBus; -use std::sync::Arc; - -use super::{EvmEventListener, StartListening}; - -pub struct EvmContractManager { - bus: Addr, - provider: Arc>, - listeners: Vec>, -} - -impl EvmContractManager { - async fn new(bus: Addr, rpc_url: &str) -> Result { - let provider = ProviderBuilder::new().on_builtin(rpc_url).await?; - Ok(Self { - bus, - provider: Arc::new(provider), - listeners: vec![], - }) - } - - pub async fn attach(bus: Addr, rpc_url: &str) -> Addr { - EvmContractManager::new(bus.clone(), rpc_url) - .await - .unwrap() - .start() - } - - fn add_listener(&self, contract_address: Address) -> Addr { - let filter = Filter::new() - .address(contract_address) - .from_block(BlockNumberOrTag::Latest); - let listener = EvmEventListener::new(self.provider.clone(), filter, self.bus.clone()); - listener.start() - } -} - -impl Actor for EvmContractManager { - type Context = Context; -} - -#[derive(Message)] -#[rtype(result = "Addr")] -pub struct AddListener { - pub contract_address: Address, -} - -impl Handler for EvmContractManager { - type Result = Addr; - - fn handle(&mut self, msg: AddListener, _ctx: &mut Self::Context) -> Self::Result { - let listener = self.add_listener(msg.contract_address); - self.listeners.push(listener.clone()); - listener - } -} - -impl Handler for EvmContractManager { - type Result = (); - - fn handle(&mut self, _: StartListening, _ctx: &mut Self::Context) -> Self::Result { - for listener in &self.listeners { - listener.do_send(StartListening); - } - } -} diff --git a/packages/ciphernode/evm/src/registry_filter_sol.rs b/packages/ciphernode/evm/src/registry_filter_sol.rs new file mode 100644 index 00000000..bd96561c --- /dev/null +++ b/packages/ciphernode/evm/src/registry_filter_sol.rs @@ -0,0 +1,134 @@ +use crate::helpers::{create_provider_with_signer, SignerProvider}; +use actix::prelude::*; +use alloy::{ + primitives::{Address, Bytes, U256}, + rpc::types::TransactionReceipt, + signers::local::PrivateKeySigner, + sol, +}; +use anyhow::Result; +use enclave_core::{ + BusError, E3id, EnclaveErrorType, EnclaveEvent, EventBus, OrderedSet, PublicKeyAggregated, + Subscribe, +}; +use std::sync::Arc; + +sol!( + #[sol(rpc)] + NaiveRegistryFilter, + "../../evm/artifacts/contracts/registry/NaiveRegistryFilter.sol/NaiveRegistryFilter.json" +); + +pub struct RegistryFilterSolWriter { + provider: SignerProvider, + contract_address: Address, + bus: Addr, +} + +impl RegistryFilterSolWriter { + pub async fn new( + bus: Addr, + rpc_url: &str, + contract_address: Address, + signer: Arc, + ) -> Result { + Ok(Self { + provider: create_provider_with_signer(rpc_url, signer).await?, + contract_address, + bus, + }) + } + + pub async fn attach( + bus: Addr, + rpc_url: &str, + contract_address: &str, + signer: Arc, + ) -> Result> { + let addr = + RegistryFilterSolWriter::new(bus.clone(), rpc_url, contract_address.parse()?, signer) + .await? + .start(); + let _ = bus + .send(Subscribe::new("PublicKeyAggregated", addr.clone().into())) + .await; + + Ok(addr) + } +} + +impl Actor for RegistryFilterSolWriter { + type Context = actix::Context; +} + +impl Handler for RegistryFilterSolWriter { + type Result = (); + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg { + EnclaveEvent::PublicKeyAggregated { data, .. } => { + // Only publish if the src and destination chains match + if self.provider.get_chain_id() == data.src_chain_id { + ctx.notify(data); + } + } + _ => (), + } + } +} + +impl Handler for RegistryFilterSolWriter { + type Result = ResponseFuture<()>; + fn handle(&mut self, msg: PublicKeyAggregated, _: &mut Self::Context) -> Self::Result { + Box::pin({ + let e3_id = msg.e3_id.clone(); + let pubkey = msg.pubkey.clone(); + let contract_address = self.contract_address.clone(); + let provider = self.provider.clone(); + let bus = self.bus.clone(); + let nodes = msg.nodes.clone(); + + async move { + let result = + publish_committee(provider, contract_address, e3_id, nodes, pubkey).await; + match result { + Ok(receipt) => { + println!("tx:{}", receipt.transaction_hash); + } + Err(err) => bus.err(EnclaveErrorType::Evm, err), + } + } + }) + } +} + +pub async fn publish_committee( + provider: SignerProvider, + contract_address: Address, + e3_id: E3id, + nodes: OrderedSet, + public_key: Vec, +) -> Result { + let e3_id: U256 = e3_id.try_into()?; + let public_key = Bytes::from(public_key); + let nodes: Vec

= nodes + .into_iter() + .filter_map(|node| node.parse().ok()) + .collect(); + let contract = NaiveRegistryFilter::new(contract_address, provider.get_provider()); + let builder = contract.publishCommittee(e3_id, nodes, public_key); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) +} + +pub struct RegistryFilterSol; +impl RegistryFilterSol { + pub async fn attach( + bus: Addr, + rpc_url: &str, + contract_address: &str, + signer: Arc, + ) -> Result<()> { + RegistryFilterSolWriter::attach(bus.clone(), rpc_url, contract_address, signer).await?; + Ok(()) + } +} diff --git a/packages/ciphernode/router/src/committee_meta.rs b/packages/ciphernode/router/src/committee_meta.rs index 24103ab2..79d3c314 100644 --- a/packages/ciphernode/router/src/committee_meta.rs +++ b/packages/ciphernode/router/src/committee_meta.rs @@ -6,6 +6,7 @@ use super::EventHook; pub struct CommitteeMeta { pub threshold_m: usize, pub seed: Seed, + pub src_chain_id: u64, } pub struct CommitteeMetaFactory; @@ -17,10 +18,17 @@ impl CommitteeMetaFactory { return; }; let E3Requested { - threshold_m, seed, .. + threshold_m, + seed, + src_chain_id, + .. } = data; - ctx.meta = Some(CommitteeMeta { threshold_m, seed }); + ctx.meta = Some(CommitteeMeta { + threshold_m, + seed, + src_chain_id, + }); }) } } diff --git a/packages/ciphernode/router/src/hooks.rs b/packages/ciphernode/router/src/hooks.rs index f904f8e0..9e50c45a 100644 --- a/packages/ciphernode/router/src/hooks.rs +++ b/packages/ciphernode/router/src/hooks.rs @@ -70,6 +70,7 @@ impl LazyPlaintextAggregator { meta.threshold_m, meta.seed, data.ciphertext_output, + meta.src_chain_id, ) .start(), ); @@ -103,6 +104,7 @@ impl LazyPublicKeyAggregator { data.e3_id, meta.threshold_m, meta.seed, + meta.src_chain_id, ) .start(), ); diff --git a/packages/ciphernode/sortition/Cargo.toml b/packages/ciphernode/sortition/Cargo.toml index 8e540191..eb856507 100644 --- a/packages/ciphernode/sortition/Cargo.toml +++ b/packages/ciphernode/sortition/Cargo.toml @@ -14,3 +14,4 @@ rand = { workspace = true } alloy = { workspace = true, features = ["full"] } actix = { workspace = true } enclave-core = { path = "../core" } +anyhow = { workspace = true } diff --git a/packages/ciphernode/sortition/src/distance.rs b/packages/ciphernode/sortition/src/distance.rs index 421f8130..6e894f08 100644 --- a/packages/ciphernode/sortition/src/distance.rs +++ b/packages/ciphernode/sortition/src/distance.rs @@ -1,4 +1,5 @@ use alloy::primitives::{keccak256, Address}; +use anyhow::Result; use num::{BigInt, Num}; pub struct DistanceSortition { @@ -16,7 +17,7 @@ impl DistanceSortition { } } - pub fn get_committee(&mut self) -> Vec<(BigInt, Address)> { + pub fn get_committee(&mut self) -> Result> { let mut scores = self .registered_nodes .iter() @@ -24,14 +25,15 @@ impl DistanceSortition { let concat = address.to_string() + &self.random_seed.to_string(); let hash = keccak256(concat).to_string(); let without_prefix = hash.trim_start_matches("0x"); - let z = BigInt::from_str_radix(without_prefix, 16).unwrap(); + let z = BigInt::from_str_radix(without_prefix, 16)?; let score = z - BigInt::from(self.random_seed); - (score, *address) + Ok((score, *address)) }) - .collect::>(); + .collect::>>()?; scores.sort_by(|a, b| a.0.cmp(&b.0)); - let result = scores[0..self.size].to_vec(); - result + let size = std::cmp::min(self.size, scores.len()); + let result = scores[0..size].to_vec(); + Ok(result) } } diff --git a/packages/ciphernode/sortition/src/sortition.rs b/packages/ciphernode/sortition/src/sortition.rs index ca33e3dd..57894e8c 100644 --- a/packages/ciphernode/sortition/src/sortition.rs +++ b/packages/ciphernode/sortition/src/sortition.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use crate::DistanceSortition; use actix::prelude::*; +use alloy::primitives::Address; use enclave_core::{CiphernodeAdded, CiphernodeRemoved, EnclaveEvent, EventBus, Seed, Subscribe}; #[derive(Message, Clone, Debug, PartialEq, Eq)] @@ -38,18 +39,29 @@ impl Default for SortitionModule { impl SortitionList for SortitionModule { fn contains(&self, seed: Seed, size: usize, address: String) -> bool { - DistanceSortition::new( - seed.into(), - self.nodes - .clone() - .into_iter() - .map(|b| b.parse().unwrap()) - .collect(), - size, - ) - .get_committee() - .iter() - .any(|(_, addr)| addr.to_string() == address) + if self.nodes.len() == 0 { + eprintln!("ERROR: No nodes registered!"); + return false; + } + + let registered_nodes: Vec
= self + .nodes + .clone() + .into_iter() + // TODO: error handling + .map(|b| b.parse().unwrap()) + .collect(); + + let Ok(committee) = + DistanceSortition::new(seed.into(), registered_nodes, size).get_committee() + else { + eprintln!("Error: Could not get committee!"); + return false; + }; + + committee + .iter() + .any(|(_, addr)| addr.to_string() == address) } fn add(&mut self, address: String) { @@ -94,7 +106,7 @@ impl Default for Sortition { } impl Actor for Sortition { - type Context = Context; + type Context = actix::Context; } impl Handler for Sortition { diff --git a/packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs b/packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs index c9a45590..93cc648f 100644 --- a/packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs +++ b/packages/ciphernode/tests/tests/test_aggregation_and_decryption.rs @@ -1,8 +1,8 @@ use data::Data; use enclave_core::{ CiphernodeAdded, CiphernodeSelected, CiphertextOutputPublished, DecryptionshareCreated, - E3Requested, E3id, EnclaveEvent, EventBus, GetHistory, KeyshareCreated, PlaintextAggregated, - PublicKeyAggregated, ResetHistory, Seed, + E3Requested, E3id, EnclaveEvent, EventBus, GetHistory, KeyshareCreated, OrderedSet, + PlaintextAggregated, PublicKeyAggregated, ResetHistory, Seed, }; use fhe::{setup_crp_params, ParamsWithCrp, SharedRng}; use logger::SimpleLogger; @@ -120,6 +120,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { threshold_m: 3, seed: seed.clone(), params: params.to_bytes(), + src_chain_id: 1, }); // Send the computation requested event bus.send(event.clone()).await?; @@ -141,6 +142,7 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { .into_iter() .aggregate()?; + println!("&&&& {}", history[8].event_type()); assert_eq!(history.len(), 9); assert_eq!( history, @@ -152,7 +154,8 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { e3_id: e3_id.clone(), threshold_m: 3, seed: seed.clone(), - params: params.to_bytes() + params: params.to_bytes(), + src_chain_id: 1 }), EnclaveEvent::from(CiphernodeSelected { e3_id: e3_id.clone(), @@ -175,7 +178,9 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { }), EnclaveEvent::from(PublicKeyAggregated { pubkey: pubkey.to_bytes(), - e3_id: e3_id.clone() + e3_id: e3_id.clone(), + nodes: OrderedSet::from(eth_addrs.clone()), + src_chain_id: 1 }) ] ); @@ -241,7 +246,8 @@ async fn test_public_key_aggregation_and_decryption() -> Result<()> { }), EnclaveEvent::from(PlaintextAggregated { e3_id: e3_id.clone(), - decrypted_output: expected_raw_plaintext.clone() + decrypted_output: expected_raw_plaintext.clone(), + src_chain_id: 1 }) ] ); @@ -275,11 +281,13 @@ async fn test_p2p_actor_forwards_events_to_network() -> Result<()> { let evt_1 = EnclaveEvent::from(PlaintextAggregated { e3_id: E3id::new("1235"), decrypted_output: vec![1, 2, 3, 4], + src_chain_id: 1, }); let evt_2 = EnclaveEvent::from(PlaintextAggregated { e3_id: E3id::new("1236"), decrypted_output: vec![1, 2, 3, 4], + src_chain_id: 1, }); let local_evt_3 = EnclaveEvent::from(CiphernodeSelected { @@ -328,6 +336,7 @@ async fn test_p2p_actor_forwards_events_to_bus() -> Result<()> { threshold_m: 3, seed: seed.clone(), params: vec![1, 2, 3, 4], + src_chain_id: 1, }); // lets send an event from the network diff --git a/tests/basic_integration/lib/ciphernode_config.yaml b/tests/basic_integration/lib/ciphernode_config.yaml new file mode 100644 index 00000000..b8f6e9ce --- /dev/null +++ b/tests/basic_integration/lib/ciphernode_config.yaml @@ -0,0 +1,7 @@ +chains: + - name: "hardhat" + rpc_url: "ws://localhost:8545" + contracts: + enclave: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + ciphernode_registry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + filter_registry: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" diff --git a/tests/basic_integration/test.sh b/tests/basic_integration/test.sh index 942a4576..6da566c5 100755 --- a/tests/basic_integration/test.sh +++ b/tests/basic_integration/test.sh @@ -75,7 +75,7 @@ waiton-files() { done } -pkill -9 -f "target/debug/node" || true +pkill -9 -f "target/debug/enclave" || true pkill -9 -f "hardhat node" || true pkill -9 -f "target/debug/aggregator" || true @@ -98,19 +98,19 @@ done # Launch 4 ciphernodes heading "Launch ciphernode $CIPHERNODE_ADDRESS_1" -yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_1 --rpc "$RPC_URL" --enclave-contract $ENCLAVE_CONTRACT --registry-contract $REGISTRY_CONTRACT & +yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_1 --config "$SCRIPT_DIR/lib/ciphernode_config.yaml" & heading "Launch ciphernode $CIPHERNODE_ADDRESS_2" -yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_2 --rpc "$RPC_URL" --enclave-contract $ENCLAVE_CONTRACT --registry-contract $REGISTRY_CONTRACT & +yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_2 --config "$SCRIPT_DIR/lib/ciphernode_config.yaml" & heading "Launch ciphernode $CIPHERNODE_ADDRESS_3" -yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_3 --rpc "$RPC_URL" --enclave-contract $ENCLAVE_CONTRACT --registry-contract $REGISTRY_CONTRACT & +yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_3 --config "$SCRIPT_DIR/lib/ciphernode_config.yaml" & heading "Launch ciphernode $CIPHERNODE_ADDRESS_4" -yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_4 --rpc "$RPC_URL" --enclave-contract $ENCLAVE_CONTRACT --registry-contract $REGISTRY_CONTRACT & +yarn ciphernode:launch --address $CIPHERNODE_ADDRESS_4 --config "$SCRIPT_DIR/lib/ciphernode_config.yaml" & # NOTE: This node is configured to be an aggregator -PRIVATE_KEY=$PRIVATE_KEY yarn ciphernode:aggregator --rpc "$RPC_URL" --enclave-contract $ENCLAVE_CONTRACT --registry-contract $REGISTRY_CONTRACT --registry-filter-contract $REGISTRY_FILTER_CONTRACT --pubkey-write-path "$SCRIPT_DIR/output/pubkey.bin" --plaintext-write-path "$SCRIPT_DIR/output/plaintext.txt" & +PRIVATE_KEY=$PRIVATE_KEY yarn ciphernode:aggregator --config "$SCRIPT_DIR/lib/ciphernode_config.yaml" --pubkey-write-path "$SCRIPT_DIR/output/pubkey.bin" --plaintext-write-path "$SCRIPT_DIR/output/plaintext.txt" & sleep 1