Skip to content

Commit

Permalink
feat: Add DFNS-CGGMP21 Blueprint
Browse files Browse the repository at this point in the history
  • Loading branch information
tbraun96 committed Nov 15, 2024
1 parent 47edd9a commit fcf90a4
Show file tree
Hide file tree
Showing 20 changed files with 1,126 additions and 4 deletions.
307 changes: 303 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"blueprints/incredible-squaring-eigenlayer",
"blueprints/incredible-squaring-symbiotic",
"blueprints/examples",
"blueprints/dfns-cggmp21",
"cli",
"gadget-io",
"blueprint-test-utils",
Expand Down Expand Up @@ -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" }
Expand Down
29 changes: 29 additions & 0 deletions blueprints/dfns-cggmp21/.gitignore
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions blueprints/dfns-cggmp21/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = []
Empty file.
7 changes: 7 additions & 0 deletions blueprints/dfns-cggmp21/build.rs
Original file line number Diff line number Diff line change
@@ -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();
}
150 changes: 150 additions & 0 deletions blueprints/dfns-cggmp21/src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! FROST Blueprint
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 FROST service
const NETWORK_PROTOCOL: &str = "/dfns/cggmp21/1.0.0";

/// FROST 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<NetworkMultiplexer>,
/// The key-value store for the service
pub store: Arc<sdk::store::LocalDatabase<DfnsStore>>,
/// Identity
pub identity: ecdsa::Pair,
}

#[derive(Serialize, Deserialize, Clone, Default)]
pub struct DfnsStore {
pub inner: Option<CoreKeyShare<Secp256k1>>,
pub refreshed_key: Option<KeyShare<Secp256k1, SecurityLevel128>>,
}

impl DfnsContext {
/// Create a new service context
pub fn new(config: sdk::config::StdGadgetConfiguration) -> eyre::Result<Self> {
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<LocalDatabase<DfnsStore>> {
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<u64> {
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<AccountId32, Public>)> {
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<BTreeMap<AccountId32, ecdsa::Public>> {
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<u64, eyre::Error> {
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,
})
}
}
105 changes: 105 additions & 0 deletions blueprints/dfns-cggmp21/src/key_refresh.rs
Original file line number Diff line number Diff line change
@@ -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<DfnsContext, JobCalled>,
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<Vec<u8>, 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<u16, Public> = 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::<Secp256k1, SecurityLevel128>(
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<R: RngCore + Send + 'static>(
mut rng: R,
) -> Result<PregeneratedPrimes, gadget_sdk::Error> {
let pregenerated_primes = tokio::task::spawn_blocking(move || {
cggmp21::PregeneratedPrimes::<SecurityLevel128>::generate(&mut rng)
})
.await
.map_err(|err| format!("Failed to generate pregenerated primes: {err:?}"))?;
Ok(pregenerated_primes)
}
Loading

0 comments on commit fcf90a4

Please sign in to comment.