diff --git a/Cargo.lock b/Cargo.lock index cc093a8a..d5965595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1640,6 +1640,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backon" version = "1.2.0" @@ -2085,7 +2091,7 @@ checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" dependencies = [ "serde", "serde_repr", - "serde_with", + "serde_with 3.11.0", ] [[package]] @@ -2133,6 +2139,26 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -2358,6 +2384,49 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cggmp21" +version = "0.5.0" +source = "git+https://github.com/LFDT-Lockness/cggmp21#3be46fa71be8b2218c4e72495771769086793ad0" +dependencies = [ + "cggmp21-keygen", + "digest 0.10.7", + "futures", + "generic-ec", + "generic-ec-zkp", + "hex", + "key-share", + "paillier-zk", + "rand_core 0.6.4", + "rand_hash", + "round-based", + "serde", + "serde_with 2.3.3", + "sha2 0.10.8", + "thiserror", + "udigest", +] + +[[package]] +name = "cggmp21-keygen" +version = "0.4.0" +source = "git+https://github.com/LFDT-Lockness/cggmp21#3be46fa71be8b2218c4e72495771769086793ad0" +dependencies = [ + "digest 0.10.7", + "displaydoc", + "generic-ec", + "generic-ec-zkp", + "hex", + "key-share", + "rand_core 0.6.4", + "round-based", + "serde", + "serde_with 2.3.3", + "sha2 0.10.8", + "thiserror", + "udigest", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -3132,6 +3201,34 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dfns-cggmp21-blueprint" +version = "0.1.1" +dependencies = [ + "async-trait", + "bincode2", + "blueprint-metadata", + "cggmp21", + "color-eyre", + "ed25519-zebra 4.0.3", + "gadget-sdk", + "hex", + "k256", + "key-share", + "libp2p", + "lock_api", + "parking_lot 0.12.3", + "rand_chacha", + "round-based", + "serde", + "serde_json", + "sp-core", + "subxt-signer", + "tokio", + "tokio-util 0.7.12", + "tracing", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -4327,6 +4424,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fast-paillier" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da6ffbfab3f6dc72b28f6f33dc76705c1a56b2c119680c936d239990beb5ae6" +dependencies = [ + "bytemuck", + "rand_core 0.6.4", + "rug", + "serde", + "thiserror", +] + [[package]] name = "faster-hex" version = "0.9.0" @@ -4827,6 +4937,7 @@ dependencies = [ "bincode2", "bollard", "clap", + "color-eyre", "dashmap 6.1.0", "ed25519-zebra 4.0.3", "eigensdk", @@ -4886,11 +4997,75 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ + "serde", "typenum", "version_check", "zeroize", ] +[[package]] +name = "generic-ec" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e3268b3f97e2046ebf69e24a1e7e12dad4dec247d87f59268fd57ab514b5f8" +dependencies = [ + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "generic-ec-core", + "generic-ec-curves", + "hex", + "phantom-type 0.4.2", + "rand_core 0.6.4", + "rand_hash", + "serde", + "serde_with 2.3.3", + "subtle", + "udigest", + "zeroize", +] + +[[package]] +name = "generic-ec-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049128cc67cac6176ada5218e294ce46421470d92a7340c93d5cfd3ecfbc29a4" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "generic-ec-curves" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e663405a17b229dede990904edcfd2056cf6641f1240869a1f44fa13bd4ee4d" +dependencies = [ + "elliptic-curve", + "generic-ec-core", + "k256", + "rand_core 0.6.4", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "generic-ec-zkp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec6e906724b1f70cd13e7e2455165300f1ef2fd200e40aa6207a046a36af839d" +dependencies = [ + "generic-array", + "generic-ec", + "rand_core 0.6.4", + "serde", + "subtle", + "udigest", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -5270,6 +5445,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gmp-mpfr-sys" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0205cd82059bc63b63cf516d714352a30c44f2c74da9961dfda2617ae6b5918" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "group" version = "0.13.0" @@ -6699,6 +6884,20 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "key-share" +version = "0.5.0" +source = "git+https://github.com/LFDT-Lockness/cggmp21#3be46fa71be8b2218c4e72495771769086793ad0" +dependencies = [ + "displaydoc", + "generic-ec", + "generic-ec-zkp", + "hex", + "serde", + "serde_with 2.3.3", + "thiserror", +] + [[package]] name = "kstring" version = "2.0.2" @@ -8533,6 +8732,24 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "paillier-zk" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6436f191c74f9718d8c583bb8a0c855a8e7146a7f4357349200a78843c09c96c" +dependencies = [ + "digest 0.10.7", + "fast-paillier", + "generic-ec", + "rand_core 0.6.4", + "rand_hash", + "rug", + "serde", + "serde_with 3.11.0", + "thiserror", + "udigest", +] + [[package]] name = "parity-bip39" version = "2.0.1" @@ -8836,6 +9053,15 @@ dependencies = [ "educe", ] +[[package]] +name = "phantom-type" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68f5dc797c2a743e024e1c53215474598faf0408826a90249569ad7f47adeaa" +dependencies = [ + "educe", +] + [[package]] name = "pharos" version = "0.5.3" @@ -9421,6 +9647,17 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bc1dd921383c6564eb0b8252f5b3f6622b84d40c6e35f5e6790e1fd7abb7a9" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", + "udigest", +] + [[package]] name = "rand_xorshift" version = "0.3.0" @@ -9812,9 +10049,10 @@ checksum = "81564866f5617d497753563151d8beb80d61e925e904d94b7e8a202b721e931e" dependencies = [ "displaydoc", "futures-util", - "phantom-type", + "phantom-type 0.3.1", "round-based-derive", "thiserror", + "tokio", "tracing", ] @@ -9864,6 +10102,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "rug" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ae2c1089ec0575193eb9222881310cc1ed8bce3646ef8b81b44b518595b79d" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", + "serde", +] + [[package]] name = "ruint" version = "1.12.3" @@ -10676,6 +10927,21 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "serde", + "serde_json", + "serde_with_macros 2.3.3", + "time", +] + [[package]] name = "serde_with" version = "3.11.0" @@ -10690,10 +10956,22 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "serde_with_macros", + "serde_with_macros 3.11.0", "time", ] +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "serde_with_macros" version = "3.11.0" @@ -12238,7 +12516,7 @@ dependencies = [ "reqwest 0.12.9", "serde", "serde_json", - "serde_with", + "serde_with 3.11.0", "thiserror", "tokio", "tokio-stream", @@ -12867,6 +13145,27 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "udigest" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cd61fa9fb78569e9fe34acf0048fd8cb9ebdbacc47af740745487287043ff0" +dependencies = [ + "digest 0.10.7", + "udigest-derive", +] + +[[package]] +name = "udigest-derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "603329303137e0d59238ee4d6b9c085eada8e2a9d20666f3abd9dadf8f8543f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "uint" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 1f19314c..eacd3b1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "blueprints/incredible-squaring-eigenlayer", "blueprints/incredible-squaring-symbiotic", "blueprints/examples", + "blueprints/dfns-cggmp21", "cli", "gadget-io", "blueprint-test-utils", @@ -50,6 +51,7 @@ blueprint-serde = { version = "0.1.1", path = "./blueprint-serde", package = "ga blueprint-test-utils = { path = "./blueprint-test-utils" } gadget-sdk = { path = "./sdk", default-features = false, version = "0.4.0" } +dfns-cggmp21-blueprint = { path = "./blueprints/dfns-cggmp21", default-features = false, version = "0.1.1" } incredible-squaring-blueprint = { path = "./blueprints/incredible-squaring", default-features = false, version = "0.1.1" } incredible-squaring-blueprint-eigenlayer = { path = "./blueprints/incredible-squaring-eigenlayer", default-features = false, version = "0.1.1" } incredible-squaring-blueprint-symbiotic = { path = "./blueprints/incredible-squaring-symbiotic", default-features = false, version = "0.1.1" } diff --git a/blueprints/dfns-cggmp21/.gitignore b/blueprints/dfns-cggmp21/.gitignore new file mode 100644 index 00000000..97c7fc58 --- /dev/null +++ b/blueprints/dfns-cggmp21/.gitignore @@ -0,0 +1,29 @@ +# Generated by Cargo +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +.idea +.vscode +.zed + +.direnv +.DS_Store +# Solc Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/blueprints/dfns-cggmp21/Cargo.toml b/blueprints/dfns-cggmp21/Cargo.toml new file mode 100644 index 00000000..413db08d --- /dev/null +++ b/blueprints/dfns-cggmp21/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "dfns-cggmp21-blueprint" +version = "0.1.1" +description = "A DFNS CGGMP21 Blueprint that can run keygen and signing jobs on demand from the Tangle network" +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +publish = false + +[dependencies] +tracing = { workspace = true } +async-trait = { workspace = true } +gadget-sdk = { workspace = true, features = ["std"] } +color-eyre = { workspace = true } +lock_api = { workspace = true } +tokio = { workspace = true, default-features = false, features = ["full"] } +tokio-util = { workspace = true } +sp-core = { workspace = true } +subxt-signer = { workspace = true, features = ["sr25519", "subxt", "std"] } +parking_lot = { workspace = true } +libp2p = { workspace = true } +ed25519-zebra = { workspace = true, features = ["pkcs8", "default", "der", "std", "serde", "pem"] } +hex = { workspace = true } +k256 = { workspace = true } +serde_json = { workspace = true } +bincode2 = { workspace = true } + +cggmp21 = { git = "https://github.com/LFDT-Lockness/cggmp21", features = ["curve-secp256k1"] } +rand_chacha = "0.3.1" +serde = { version = "1.0.214", features = ["derive"] } +round-based = { version = "0.3.2", features = ["runtime-tokio"] } +key-share = { git = "https://github.com/LFDT-Lockness/cggmp21", features = ["serde"] } + +[build-dependencies] +blueprint-metadata = { workspace = true } + +[features] +default = ["std"] +std = [] diff --git a/blueprints/dfns-cggmp21/README.md b/blueprints/dfns-cggmp21/README.md new file mode 100644 index 00000000..e69de29b diff --git a/blueprints/dfns-cggmp21/build.rs b/blueprints/dfns-cggmp21/build.rs new file mode 100644 index 00000000..dcbb32ba --- /dev/null +++ b/blueprints/dfns-cggmp21/build.rs @@ -0,0 +1,7 @@ +fn main() { + println!("cargo:rerun-if-changed=src/cli"); + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=src/main.rs"); + println!("cargo:rerun-if-changed=src/*"); + blueprint_metadata::generate_json(); +} diff --git a/blueprints/dfns-cggmp21/src/context.rs b/blueprints/dfns-cggmp21/src/context.rs new file mode 100644 index 00000000..e8f56974 --- /dev/null +++ b/blueprints/dfns-cggmp21/src/context.rs @@ -0,0 +1,148 @@ +use cggmp21::security_level::SecurityLevel128; +use cggmp21::supported_curves::Secp256k1; +use cggmp21::KeyShare; +use color_eyre::eyre; +use gadget_sdk as sdk; +use gadget_sdk::network::{NetworkMultiplexer, StreamKey}; +use gadget_sdk::store::LocalDatabase; +use gadget_sdk::subxt_core::ext::sp_core::{ecdsa, keccak_256}; +use gadget_sdk::subxt_core::utils::AccountId32; +use key_share::CoreKeyShare; +use sdk::ctx::{KeystoreContext, ServicesContext, TangleClientContext}; +use sdk::tangle_subxt::tangle_testnet_runtime::api; +use serde::{Deserialize, Serialize}; +use sp_core::ecdsa::Public; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::Arc; + +/// The network protocol for the DFNS service +const NETWORK_PROTOCOL: &str = "/dfns/cggmp21/1.0.0"; + +/// DFNS-CGGMP21 Service Context that holds all the necessary context for the service +/// to run +#[derive(Clone, KeystoreContext, TangleClientContext, ServicesContext)] +pub struct DfnsContext { + /// The overreaching configuration for the service + #[config] + pub config: sdk::config::StdGadgetConfiguration, + /// The gossip handle for the network + pub network_backend: Arc, + /// The key-value store for the service + pub store: Arc>, + /// Identity + pub identity: ecdsa::Pair, +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct DfnsStore { + pub inner: Option>, + pub refreshed_key: Option>, +} + +impl DfnsContext { + /// Create a new service context + pub fn new(config: sdk::config::StdGadgetConfiguration) -> eyre::Result { + let network_config = config.libp2p_network_config(NETWORK_PROTOCOL)?; + let identity = network_config.ecdsa_key.clone(); + let gossip_handle = sdk::network::setup::start_p2p_network(network_config) + .map_err(|e| eyre::eyre!("Failed to start the network: {e:?}"))?; + let keystore_dir = PathBuf::from(config.keystore_uri.clone()).join("dfns.json"); + + Ok(Self { + store: Arc::new(LocalDatabase::open(keystore_dir)), + identity, + config, + network_backend: Arc::new(NetworkMultiplexer::new(gossip_handle)), + }) + } + + /// Get the key-value store + pub fn store(&self) -> Arc> { + self.store.clone() + } + + /// Get the configuration + pub fn config(&self) -> &sdk::config::StdGadgetConfiguration { + &self.config + } + + /// Get the network protocol + pub fn network_protocol(&self) -> &str { + NETWORK_PROTOCOL + } + + /// Get the current blueprint id + pub fn blueprint_id(&self) -> eyre::Result { + self.config() + .protocol_specific + .tangle() + .map(|c| c.blueprint_id) + .map_err(|e| eyre::eyre!("Failed to get blueprint id: {e}")) + } + + pub async fn get_party_index_and_operators( + &self, + ) -> eyre::Result<(usize, BTreeMap)> { + let parties = self.current_service_operators_ecdsa_keys().await?; + let ecdsa_id = self.config.first_ecdsa_signer()?.into_inner(); + let my_id = ecdsa_id.account_id(); + let index_of_my_id = parties + .iter() + .position(|(id, _)| id == my_id) + .ok_or_else(|| eyre::eyre!("Failed to get party index"))?; + + Ok((index_of_my_id, parties)) + } + + /// Get Current Service Operators' ECDSA Keys as a map. + pub async fn current_service_operators_ecdsa_keys( + &self, + ) -> eyre::Result> { + let client = self.tangle_client().await?; + let current_blueprint = self.blueprint_id()?; + let current_service_op = self.current_service_operators(&client).await?; + let storage = client.storage().at_latest().await?; + let mut map = BTreeMap::new(); + for (operator, _) in current_service_op { + let addr = api::storage() + .services() + .operators(current_blueprint, &operator); + let maybe_pref = storage.fetch(&addr).await?; + if let Some(pref) = maybe_pref { + map.insert(operator, ecdsa::Public(pref.key)); + } else { + return Err(eyre::eyre!( + "Failed to get operator's {operator} public ecdsa key" + )); + } + } + + Ok(map) + } + + /// Get the current call id for this job. + pub async fn current_call_id(&self) -> Result { + let client = self.tangle_client().await?; + let addr = api::storage().services().next_job_call_id(); + let storage = client.storage().at_latest().await?; + let maybe_call_id = storage.fetch_or_default(&addr).await?; + Ok(maybe_call_id.saturating_sub(1)) + } + + /// Get the network backend for keygen job + pub fn keygen_network_backend(&self, call_id: u64) -> impl sdk::network::Network { + self.network_backend.multiplex(StreamKey { + task_hash: keccak_256(&[&b"keygen"[..], &call_id.to_le_bytes()[..]].concat()), + round_id: -1, + }) + } + + /// Get the network backend for signing job + pub fn signing_network_backend(&self, call_id: u64) -> impl sdk::network::Network { + self.network_backend.multiplex(StreamKey { + task_hash: keccak_256(&[&b"signing"[..], &call_id.to_le_bytes()[..]].concat()), + round_id: -1, + }) + } +} diff --git a/blueprints/dfns-cggmp21/src/key_refresh.rs b/blueprints/dfns-cggmp21/src/key_refresh.rs new file mode 100644 index 00000000..2e0fc9fb --- /dev/null +++ b/blueprints/dfns-cggmp21/src/key_refresh.rs @@ -0,0 +1,105 @@ +use crate::context::DfnsContext; +use cggmp21::security_level::SecurityLevel128; +use cggmp21::supported_curves::Secp256k1; +use cggmp21::{ExecutionId, PregeneratedPrimes}; +use color_eyre::eyre::OptionExt; +use gadget_sdk::event_listener::tangle::jobs::{services_post_processor, services_pre_processor}; +use gadget_sdk::event_listener::tangle::TangleEventListener; +use gadget_sdk::network::round_based_compat::NetworkDeliveryWrapper; +use gadget_sdk::network::StreamKey; +use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled; +use gadget_sdk::{compute_sha256_hash, job}; +use rand_chacha::rand_core::{RngCore, SeedableRng}; +use round_based::runtime::TokioRuntime; +use sp_core::ecdsa::Public; +use std::collections::BTreeMap; + +#[job( + id = 0, + params(n), + event_listener( + listener = TangleEventListener, + pre_processor = services_pre_processor, + post_processor = services_post_processor, + ), +)] +/// Runs a [t; n] keygen using DFNS-CGGMP21. Returns the public key +pub async fn key_refresh(n: u16, context: DfnsContext) -> Result, gadget_sdk::Error> { + let blueprint_id = context.blueprint_id()?; + let call_id = context.current_call_id().await?; + let meta_deterministic_hash = compute_sha256_hash!( + n.to_be_bytes(), + blueprint_id.to_be_bytes(), + call_id.to_be_bytes(), + "dfns" + ); + let deterministic_hash = compute_sha256_hash!(meta_deterministic_hash, "dfns-key-refresh"); + // TODO: make sure it's okay to have a different execution id for signing vs keygen even if same between for all keygen or all signing + let eid = ExecutionId::new(&deterministic_hash); + let (i, operators) = context.get_party_index_and_operators().await?; + let parties: BTreeMap = operators + .into_iter() + .enumerate() + .map(|(j, (_, ecdsa))| (j as u16, ecdsa)) + .collect(); + + gadget_sdk::info!( + "Starting DFNS-CGGMP21 Signing for party {i}, n={n}, eid={}", + hex::encode(eid.as_bytes()) + ); + + let mut rng = rand_chacha::ChaChaRng::from_seed(deterministic_hash); + let network = context.network_backend.multiplex(StreamKey { + task_hash: deterministic_hash, + round_id: 0, + }); + let delivery = NetworkDeliveryWrapper::new(network, i as _, deterministic_hash, parties); + let party = round_based::party::MpcParty::connected(delivery).set_runtime(TokioRuntime); + + let key = hex::encode(meta_deterministic_hash); + let mut cggmp21_state = context + .store + .get(&key) + .ok_or_eyre("Keygen output not found in DB")?; + let keygen_output = cggmp21_state + .inner + .as_ref() + .ok_or_eyre("Keygen output not found")?; + + // This generate_pregenerated_orimes function can take awhile to run + let pregenerated_primes = generate_pregenerated_primes(rng.clone()).await?; + + // TODO: parameterize this + let result = cggmp21::key_refresh::( + eid, + keygen_output, + pregenerated_primes, + ) + .enforce_reliable_broadcast(true) + .start(&mut rng, party) + .await + .map_err(|err| gadget_sdk::Error::Other(err.to_string()))?; + + cggmp21_state.refreshed_key = Some(result.clone()); + + context.store.set(&key, cggmp21_state); + + let public_key = + bincode2::serialize(&result.shared_public_key).expect("Failed to serialize public key"); + // TODO: Note: Earlier this year, bincode failed to serialize this DirtyKeyShare + let serializable_share = + bincode2::serialize(&result.into_inner()).expect("Failed to serialize share"); + + Ok(public_key) +} + +async fn generate_pregenerated_primes( + mut rng: R, +) -> Result { + let pregenerated_primes = tokio::task::spawn_blocking(move || { + cggmp21::PregeneratedPrimes::::generate(&mut rng) + }) + .await + .map_err(|err| format!("Failed to generate pregenerated primes: {err:?}"))?; + Ok(pregenerated_primes) +} diff --git a/blueprints/dfns-cggmp21/src/keygen.rs b/blueprints/dfns-cggmp21/src/keygen.rs new file mode 100644 index 00000000..443f7a37 --- /dev/null +++ b/blueprints/dfns-cggmp21/src/keygen.rs @@ -0,0 +1,79 @@ +use crate::context::{DfnsContext, DfnsStore}; +use cggmp21::supported_curves::Secp256k1; +use cggmp21::ExecutionId; +use gadget_sdk::event_listener::tangle::jobs::{services_post_processor, services_pre_processor}; +use gadget_sdk::event_listener::tangle::TangleEventListener; +use gadget_sdk::network::round_based_compat::NetworkDeliveryWrapper; +use gadget_sdk::network::StreamKey; +use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled; +use gadget_sdk::{compute_sha256_hash, job}; +use rand_chacha::rand_core::SeedableRng; +use round_based::runtime::TokioRuntime; +use sp_core::ecdsa::Public; +use std::collections::BTreeMap; + +#[job( + id = 0, + params(n), + event_listener( + listener = TangleEventListener, + pre_processor = services_pre_processor, + post_processor = services_post_processor, + ), +)] +/// Runs a keygen using DFNS-CGGMP21. Returns the public key +pub async fn keygen(n: u16, context: DfnsContext) -> Result, gadget_sdk::Error> { + let blueprint_id = context.blueprint_id()?; + let call_id = context.current_call_id().await?; + let meta_deterministic_hash = compute_sha256_hash!( + n.to_be_bytes(), + blueprint_id.to_be_bytes(), + call_id.to_be_bytes(), + "dfns" + ); + let deterministic_hash = compute_sha256_hash!(meta_deterministic_hash, "dfns-keygen"); + let eid = ExecutionId::new(&deterministic_hash); + let (i, operators) = context.get_party_index_and_operators().await?; + let parties: BTreeMap = operators + .into_iter() + .enumerate() + .map(|(j, (_, ecdsa))| (j as u16, ecdsa)) + .collect(); + + gadget_sdk::info!( + "Starting DFNS-CGGMP21 Keygen for party {i}, n={n}, eid={}", + hex::encode(eid.as_bytes()) + ); + + let mut rng = rand_chacha::ChaChaRng::from_seed(deterministic_hash); + let network = context.network_backend.multiplex(StreamKey { + task_hash: deterministic_hash, + round_id: 0, + }); + let delivery = NetworkDeliveryWrapper::new(network, i as _, deterministic_hash, parties); + let party = round_based::party::MpcParty::connected(delivery).set_runtime(TokioRuntime); + + // TODO: Parameterize the Curve type + let result = cggmp21::keygen::(eid, i as u16, n) + .enforce_reliable_broadcast(true) + .start(&mut rng, party) + .await + .map_err(|err| gadget_sdk::Error::Other(err.to_string()))?; + + let key = hex::encode(meta_deterministic_hash); + context.store.set( + &key, + DfnsStore { + inner: Some(result.clone()), + refreshed_key: None, + }, + ); + + let public_key = + bincode2::serialize(&result.shared_public_key).expect("Failed to serialize public key"); + // TODO: Note: Earlier this year, bincode failed to serialize this DirtyKeyShare + let serializable_share = + bincode2::serialize(&result.into_inner()).expect("Failed to serialize share"); + + Ok(public_key) +} diff --git a/blueprints/dfns-cggmp21/src/lib.rs b/blueprints/dfns-cggmp21/src/lib.rs new file mode 100644 index 00000000..a6f2105e --- /dev/null +++ b/blueprints/dfns-cggmp21/src/lib.rs @@ -0,0 +1,4 @@ +pub mod context; +pub mod key_refresh; +pub mod keygen; +pub mod signing; diff --git a/blueprints/dfns-cggmp21/src/main.rs b/blueprints/dfns-cggmp21/src/main.rs new file mode 100644 index 00000000..ba43d32d --- /dev/null +++ b/blueprints/dfns-cggmp21/src/main.rs @@ -0,0 +1,39 @@ +use color_eyre::Result; +use dfns_cggmp21_blueprint::context::DfnsContext; +use gadget_sdk::info; +use gadget_sdk::runners::tangle::TangleConfig; +use gadget_sdk::runners::BlueprintRunner; +use sp_core::Pair; + +#[gadget_sdk::main(env)] +async fn main() { + let context = DfnsContext::new(env.clone())?; + + info!( + "Starting the Blueprint Runner for {} ...", + hex::encode(context.identity.public().as_ref()) + ); + + info!("~~~ Executing the DFNS-CGGMP21 blueprint ~~~"); + + let tangle_config = TangleConfig::default(); + let keygen = + dfns_cggmp21_blueprint::keygen::KeygenEventHandler::new(&env, context.clone()).await?; + + let key_refresh = + dfns_cggmp21_blueprint::key_refresh::KeyRefreshEventHandler::new(&env, context.clone()) + .await?; + + let signing = + dfns_cggmp21_blueprint::signing::SigningEventHandler::new(&env, context.clone()).await?; + + BlueprintRunner::new(tangle_config, env.clone()) + .job(keygen) + .job(key_refresh) + .job(signing) + .run() + .await?; + + info!("Exiting..."); + Ok(()) +} diff --git a/blueprints/dfns-cggmp21/src/signing.rs b/blueprints/dfns-cggmp21/src/signing.rs new file mode 100644 index 00000000..91d891db --- /dev/null +++ b/blueprints/dfns-cggmp21/src/signing.rs @@ -0,0 +1,102 @@ +use crate::context::DfnsContext; +use cggmp21::key_share::AnyKeyShare; +use cggmp21::security_level::SecurityLevel128; +use cggmp21::supported_curves::Secp256k1; +use cggmp21::{DataToSign, ExecutionId}; +use color_eyre::eyre::OptionExt; +use gadget_sdk::event_listener::tangle::jobs::{services_post_processor, services_pre_processor}; +use gadget_sdk::event_listener::tangle::TangleEventListener; +use gadget_sdk::network::round_based_compat::NetworkDeliveryWrapper; +use gadget_sdk::network::StreamKey; +use gadget_sdk::random::rand::seq::SliceRandom; +use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled; +use gadget_sdk::{compute_sha256_hash, job}; +use k256::sha2::Sha256; +use rand_chacha::rand_core::SeedableRng; +use round_based::runtime::TokioRuntime; +use sp_core::ecdsa::Public; +use std::collections::BTreeMap; + +#[job( + id = 0, + params(n, message_to_sign), + event_listener( + listener = TangleEventListener, + pre_processor = services_pre_processor, + post_processor = services_post_processor, + ), +)] +/// Runs a [t; n] keygen using DFNS-CGGMP21. Returns the public key +pub async fn signing( + n: u16, + message_to_sign: Vec, + context: DfnsContext, +) -> Result, gadget_sdk::Error> { + let blueprint_id = context.blueprint_id()?; + let call_id = context.current_call_id().await?; + let meta_deterministic_hash = compute_sha256_hash!( + n.to_be_bytes(), + blueprint_id.to_be_bytes(), + call_id.to_be_bytes(), + "dfns" + ); + let deterministic_hash = compute_sha256_hash!(meta_deterministic_hash, "dfns-signing"); + // TODO: make sure it's okay to have a different execution id for signing vs keygen even if same between for all keygen or all signing + let eid = ExecutionId::new(&deterministic_hash); + let (i, operators) = context.get_party_index_and_operators().await?; + let parties: BTreeMap = operators + .into_iter() + .enumerate() + .map(|(j, (_, ecdsa))| (j as u16, ecdsa)) + .collect(); + + gadget_sdk::info!( + "Starting DFNS-CGGMP21 Signing for party {i}, n={n}, eid={}", + hex::encode(eid.as_bytes()) + ); + + let mut rng = rand_chacha::ChaChaRng::from_seed(deterministic_hash); + let network = context.network_backend.multiplex(StreamKey { + task_hash: deterministic_hash, + round_id: 0, + }); + let delivery = NetworkDeliveryWrapper::new(network, i as _, deterministic_hash, parties); + let party = round_based::party::MpcParty::connected(delivery).set_runtime(TokioRuntime); + + let key = hex::encode(meta_deterministic_hash); + let keygen_output = context + .store + .get(&key) + .ok_or_eyre("Keygen output not found in DB")? + .refreshed_key + .ok_or_eyre("Keygen output not found")?; + // Choose `t` signers to perform signing + let t = keygen_output.min_signers(); + let shares = &keygen_output.public_shares; + let mut participants = (0..n).collect::>(); + participants.shuffle(&mut rng); + let participants = &participants[..usize::from(t)]; + println!("Signers: {participants:?}"); + let participants_shares = participants.iter().map(|i| &shares[usize::from(*i)]); + + // TODO: Parameterize the Curve type + let signing = + cggmp21::signing::(eid, i as _, participants, &keygen_output) + .enforce_reliable_broadcast(true); + let message_to_sign = DataToSign::::digest::(&message_to_sign); + let signature = signing + .sign(&mut rng, party, message_to_sign) + .await + .map_err(|err| gadget_sdk::Error::Other(err.to_string()))?; + + let public_key = &keygen_output.shared_public_key; + + signature + .verify(public_key, &message_to_sign) + .map_err(|err| gadget_sdk::Error::Other(err.to_string()))?; + + let serialized_signature = + bincode2::serialize(&signature).expect("Failed to serialize signature"); + + Ok(serialized_signature) +} diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 520ee08c..fcac779f 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -99,6 +99,7 @@ sysinfo = { workspace = true } dashmap = { workspace = true } lazy_static = "1.5.0" bincode2 = { workspace = true } +color-eyre = { workspace = true } [target.'cfg(not(target_family = "wasm"))'.dependencies.libp2p] diff --git a/sdk/src/config/gadget_config.rs b/sdk/src/config/gadget_config.rs index 5cc4203a..84b597d6 100644 --- a/sdk/src/config/gadget_config.rs +++ b/sdk/src/config/gadget_config.rs @@ -4,6 +4,7 @@ use crate::keystore::backend::GenericKeyStore; use crate::keystore::BackendExt; #[cfg(any(feature = "std", feature = "wasm"))] use crate::keystore::TanglePairSigner; +use crate::network::setup::NetworkConfig; use crate::utils::test_utils::get_client; use alloc::string::{String, ToString}; use core::fmt::Debug; @@ -129,6 +130,32 @@ impl Default for GadgetConfiguration { } impl GadgetConfiguration { + /// Returns a libp2p-friendly identity keypair. + pub fn libp2p_identity(&self) -> Result { + let ed25519 = *self.first_ed25519_signer()?.signer(); + let keypair = libp2p::identity::Keypair::ed25519_from_bytes(ed25519.seed()) + .map_err(|err| Error::ConfigurationError(err.to_string()))?; + Ok(keypair) + } + + /// Returns a new `NetworkConfig` for the current environment. + pub fn libp2p_network_config>( + &self, + network_name: T, + ) -> Result { + let network_identity = self.libp2p_identity()?; + + let my_ecdsa_key = self.first_ecdsa_signer()?; + let network_config = NetworkConfig::new_service_network( + network_identity, + my_ecdsa_key.signer().clone(), + self.bootnodes.clone(), + self.target_port, + network_name, + ); + + Ok(network_config) + } /// Loads the `KeyStore` from the current environment. /// /// # Errors diff --git a/sdk/src/config/mod.rs b/sdk/src/config/mod.rs index 2fd3fd3f..88d40f68 100644 --- a/sdk/src/config/mod.rs +++ b/sdk/src/config/mod.rs @@ -68,6 +68,8 @@ pub enum Error { MissingSymbioticContractAddresses, #[error("Bad RPC Connection: {0}")] BadRpcConnection(String), + #[error("Configuration error: {0}")] + ConfigurationError(String), } /// Loads the [`GadgetConfiguration`] from the current environment. diff --git a/sdk/src/error.rs b/sdk/src/error.rs index d5a58a42..c61c3c9e 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -70,6 +70,9 @@ pub enum Error { #[error("Bad argument decoding for {0}")] BadArgumentDecoding(String), + #[error("Color Eyre error: {0}")] + Generic(#[from] color_eyre::Report), + #[error("Other error: {0}")] Other(String), } diff --git a/sdk/src/network/mod.rs b/sdk/src/network/mod.rs index b54fed0d..55d9a671 100644 --- a/sdk/src/network/mod.rs +++ b/sdk/src/network/mod.rs @@ -19,6 +19,7 @@ pub mod handlers; #[cfg(target_family = "wasm")] pub mod matchbox; pub mod messaging; +pub mod round_based_compat; pub mod setup; #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)] diff --git a/sdk/src/network/round_based_compat.rs b/sdk/src/network/round_based_compat.rs new file mode 100644 index 00000000..d2872f19 --- /dev/null +++ b/sdk/src/network/round_based_compat.rs @@ -0,0 +1,221 @@ +use core::pin::Pin; +use core::sync::atomic::AtomicU64; +use core::task::{ready, Context, Poll}; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::sync::Arc; + +use crate::futures::prelude::*; +use crate::network::{self, IdentifierInfo, Network, NetworkMultiplexer, StreamKey, SubNetwork}; +use crate::subxt_core::ext::sp_core::ecdsa; +use round_based::{Delivery, Incoming, Outgoing}; +use round_based::{MessageDestination, MessageType, MsgId, PartyIndex}; +use stream::{SplitSink, SplitStream}; + +pub struct NetworkDeliveryWrapper { + /// The wrapped network implementation. + network: NetworkWrapper, +} + +impl NetworkDeliveryWrapper +where + N: Network + Unpin, + M: Clone + Send + Unpin + 'static, + M: serde::Serialize, + M: serde::de::DeserializeOwned, +{ + /// Create a new NetworkDeliveryWrapper over a network implementation with the given party index. + pub fn new( + network: N, + i: PartyIndex, + task_hash: [u8; 32], + parties: BTreeMap, + ) -> Self { + let mux = NetworkMultiplexer::new(network); + // By default, we create 4 substreams for each party. + let sub_streams = (1..5) + .map(|i| { + let key = StreamKey { + // This is a dummy task hash, it should be replaced with the actual task hash + task_hash: [0u8; 32], + round_id: i, + }; + let substream = mux.multiplex(key); + (key, substream) + }) + .collect(); + let network = NetworkWrapper { + me: i, + mux, + incoming_queue: VecDeque::new(), + outgoing_queue: VecDeque::new(), + sub_streams, + participants: parties, + task_hash, + next_msg_id: Arc::new(NextMessageId::default()), + _network: core::marker::PhantomData, + }; + NetworkDeliveryWrapper { network } + } +} + +/// A NetworkWrapper wraps a network implementation and implements [`Stream`] and [`Sink`] for +/// it. +pub struct NetworkWrapper { + /// The current party index. + me: PartyIndex, + /// Our network Multiplexer. + mux: NetworkMultiplexer, + /// A Map of substreams for each round. + sub_streams: HashMap, + /// A queue of incoming messages. + incoming_queue: VecDeque>, + /// A queue of outgoing messages. + outgoing_queue: VecDeque>, + /// Participants in the network with their corresponding ECDSA public keys. + // Note: This is a BTreeMap to ensure that the participants are sorted by their party index. + participants: BTreeMap, + next_msg_id: Arc, + task_hash: [u8; 32], + _network: core::marker::PhantomData, +} + +impl Delivery for NetworkDeliveryWrapper +where + N: Network + Unpin, + M: Clone + Send + Unpin + 'static, + M: serde::Serialize + serde::de::DeserializeOwned, + M: round_based::ProtocolMessage, +{ + type Send = SplitSink, Outgoing>; + type Receive = SplitStream>; + type SendError = crate::Error; + type ReceiveError = crate::Error; + + fn split(self) -> (Self::Receive, Self::Send) { + let (sink, stream) = self.network.split(); + (stream, sink) + } +} + +impl Stream for NetworkWrapper +where + N: Network + Unpin, + M: serde::de::DeserializeOwned + Unpin, + M: round_based::ProtocolMessage, +{ + type Item = Result, crate::Error>; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let sub_streams = self.sub_streams.values(); + // pull all substreams + let mut messages = Vec::new(); + for sub_stream in sub_streams { + let p = sub_stream.next_message().poll_unpin(cx); + let m = match p { + Poll::Ready(Some(msg)) => msg, + _ => continue, + }; + let msg = network::deserialize::(&m.payload)?; + messages.push((m.sender.user_id, m.recipient, msg)); + } + + // Sort the incoming messages by round. + messages.sort_by_key(|(_, _, msg)| msg.round()); + + let this = self.get_mut(); + // Push all messages to the incoming queue + messages + .into_iter() + .map(|(sender, recipient, msg)| Incoming { + id: this.next_msg_id.next(), + sender, + msg_type: match recipient { + Some(_) => MessageType::P2P, + None => MessageType::Broadcast, + }, + msg, + }) + .for_each(|m| this.incoming_queue.push_back(m)); + // Reorder the incoming queue by round message. + let maybe_msg = this.incoming_queue.pop_front(); + if let Some(msg) = maybe_msg { + Poll::Ready(Some(Ok(msg))) + } else { + // No message in the queue, and no message in the substreams. + // Tell the network to wake us up when a new message arrives. + cx.waker().wake_by_ref(); + Poll::Pending + } + } +} + +impl Sink> for NetworkWrapper +where + N: Network + Unpin, + M: Unpin + serde::Serialize, + M: round_based::ProtocolMessage, +{ + type Error = crate::Error; + + fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, msg: Outgoing) -> Result<(), Self::Error> { + self.get_mut().outgoing_queue.push_back(msg); + Ok(()) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + // Dequeue all messages and send them one by one to the network + let this = self.get_mut(); + while let Some(out) = this.outgoing_queue.pop_front() { + // Get the substream to send the message to. + let key = StreamKey { + task_hash: this.task_hash, + round_id: i32::from(out.msg.round()), + }; + let substream = this + .sub_streams + .entry(key) + .or_insert_with(|| this.mux.multiplex(key)); + let identifier_info = IdentifierInfo { + block_id: None, + session_id: None, + retry_id: None, + task_id: None, + }; + let (to, to_network_id) = match out.recipient { + MessageDestination::AllParties => (None, None), + MessageDestination::OneParty(p) => (Some(p), this.participants.get(&p).cloned()), + }; + let protocol_message = N::build_protocol_message( + identifier_info, + this.me, + to, + &out.msg, + this.participants.get(&this.me).cloned(), + to_network_id, + ); + let p = substream.send_message(protocol_message).poll_unpin(cx); + match ready!(p) { + Ok(()) => continue, + Err(e) => return Poll::Ready(Err(e)), + } + } + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } +} + +#[derive(Default)] +struct NextMessageId(AtomicU64); + +impl NextMessageId { + pub fn next(&self) -> MsgId { + self.0.fetch_add(1, core::sync::atomic::Ordering::Relaxed) + } +} diff --git a/sdk/src/utils/hashing.rs b/sdk/src/utils/hashing.rs new file mode 100644 index 00000000..6c16ea2d --- /dev/null +++ b/sdk/src/utils/hashing.rs @@ -0,0 +1,14 @@ +#[macro_export] +macro_rules! compute_sha256_hash { + ($($data:expr),*) => { + { + use k256::sha2::{Digest, Sha256}; + let mut hasher = Sha256::default(); + $(hasher.update($data);)* + let result = hasher.finalize(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(result.as_slice()); + hash + } + }; +} diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs index 92ee7bea..d453fc49 100644 --- a/sdk/src/utils/mod.rs +++ b/sdk/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod evm; +pub mod hashing; pub mod test_utils;