Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add DFNS-CGGMP21 Blueprint #475

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
148 changes: 148 additions & 0 deletions blueprints/dfns-cggmp21/src/context.rs
Original file line number Diff line number Diff line change
@@ -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<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
Loading