Repository: tangle-network/gadget (language: rust)

Issue:
[TASK] Add `dfns/cggmp21` Gadget
- [x] Add support for CGGMP21 gadget based on https://github.com/dfns/cggmp21
- [x] Add tests for CGGMP21 gadget


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 2cf791b48..a08063c86 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ members = [
     "gadget-common",
     "test-gadget",
     "protocols/mp-ecdsa",
+    "protocols/dfns-cggmp21",
     "protocols/zk-saas",
     "protocols/stub",
     "test-utils",
@@ -22,6 +23,8 @@ protocol-macros = { path = "./protocol-macros" }
 
 pallet-jobs-rpc-runtime-api = { git = "https://github.com/webb-tools/tangle" }
 pallet-jobs = { git = "https://github.com/webb-tools/tangle" }
+pallet-dkg = { git = "https://github.com/webb-tools/tangle" }
+pallet-zksaas = { git = "https://github.com/webb-tools/tangle" }
 tangle-primitives = { git = "https://github.com/webb-tools/tangle" }
 tangle-testnet-runtime = { git = "https://github.com/webb-tools/tangle" }
 tangle-mainnet-runtime = { git = "https://github.com/webb-tools/tangle" }
@@ -29,6 +32,7 @@ tangle-mainnet-runtime = { git = "https://github.com/webb-tools/tangle" }
 multi-party-ecdsa = { git = "https://github.com/webb-tools/cggmp-threshold-ecdsa/" }
 round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = [] }
 curv = { package = "curv-kzen", version = "0.10.0" }
+dfns-cggmp21 = { package = "cggmp21", version = "0.1.1", default-features = false }
 
 sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0"  }
 sp-io = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0"  }
@@ -99,12 +103,12 @@ ark-bn254 = { version = "0.4.0", default-features = false, features = ["curve"]
 tokio-rustls = "0.24.1"
 tokio = "1.32.0"
 bincode2 = "2"
-futures-util = "0.3.28"
+futures-util = "0.3.30"
 serde = "1.0.188"
 async-trait = "0.1.73"
 log = "0.4.20"
 parking_lot = "0.12.1"
-futures = "0.3.28"
+futures = "0.3.30"
 hex = "0.4.3"
 auto_impl = "1.1.0"
 uuid = "1.4.1"
diff --git a/flake.nix b/flake.nix
index 60fd8537f..3d5f38ac1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -30,6 +30,7 @@
             pkgs.pkg-config
             pkgs.clang
             pkgs.gmp
+            pkgs.om4
             # Protocol Buffers
             pkgs.protobuf
             # Mold Linker for faster builds (only on Linux)
diff --git a/gadget-common/src/keystore.rs b/gadget-common/src/keystore.rs
index 0f58428e3..01ce327d3 100644
--- a/gadget-common/src/keystore.rs
+++ b/gadget-common/src/keystore.rs
@@ -2,33 +2,38 @@ use async_trait::async_trait;
 use parking_lot::RwLock;
 use serde::de::DeserializeOwned;
 use serde::Serialize;
-use sp_core::ecdsa::Pair;
+use sp_core::ecdsa::Pair as EcdsaPair;
+use sp_core::sr25519::Pair as Sr25519Pair;
+use sp_core::Pair;
 use std::collections::HashMap;
 use std::sync::Arc;
 use tangle_primitives::jobs::JobId;
 
+pub type ECDSAKeyStore<BE> = GenericKeyStore<BE, EcdsaPair>;
+pub type Sr25519KeyStore<BE> = GenericKeyStore<BE, Sr25519Pair>;
+
 #[derive(Clone)]
-pub struct ECDSAKeyStore<BE: KeystoreBackend> {
+pub struct GenericKeyStore<BE: KeystoreBackend, P: Pair> {
     backend: BE,
-    pair: Pair,
+    pair: P,
 }
 
-impl ECDSAKeyStore<InMemoryBackend> {
-    pub fn in_memory(pair: Pair) -> Self {
-        ECDSAKeyStore {
+impl<P: Pair> GenericKeyStore<InMemoryBackend, P> {
+    pub fn in_memory(pair: P) -> Self {
+        GenericKeyStore {
             backend: InMemoryBackend::new(),
             pair,
         }
     }
 }
 
-impl<BE: KeystoreBackend> ECDSAKeyStore<BE> {
-    pub fn pair(&self) -> &Pair {
+impl<P: Pair, BE: KeystoreBackend> GenericKeyStore<BE, P> {
+    pub fn pair(&self) -> &P {
         &self.pair
     }
 }
 
-impl<BE: KeystoreBackend> ECDSAKeyStore<BE> {
+impl<P: Pair, BE: KeystoreBackend> GenericKeyStore<BE, P> {
     pub async fn get<T: DeserializeOwned>(
         &self,
         job_id: &JobId,
diff --git a/protocols/dfns-cggmp21/Cargo.toml b/protocols/dfns-cggmp21/Cargo.toml
new file mode 100644
index 000000000..c175be97f
--- /dev/null
+++ b/protocols/dfns-cggmp21/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "dfns-cggmp21-protocol"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time", "net"] }
+gadget-common = { workspace = true }
+gadget-core = { workspace = true }
+protocol-macros = { workspace = true }
+async-trait = { workspace = true }
+log = { workspace = true }
+dfns-cggmp21 = { workspace = true, features = ["all-curves"] }
+curv = { workspace = true }
+futures = { workspace = true }
+itertools = { workspace = true }
+bincode2 = { workspace = true }
+
+pallet-jobs-rpc-runtime-api = { workspace = true, features = ["std"] }
+pallet-jobs = { workspace = true, features = ["std"] }
+tangle-primitives = { workspace = true, features = ["std"] }
+
+sp-core = { workspace = true, features = ["std"] }
+sp-io = { workspace = true, features = ["std"] }
+sp-api = { workspace = true, features = ["std"] }
+sp-runtime = { workspace = true, features = ["std"] }
+sp-application-crypto = { workspace = true, features = ["std"] }
+
+sc-client-api = { workspace = true }
+
+frame-support = { workspace = true  }
+parity-scale-codec = { workspace = true }
+
+serde = { version = "1.0.193", features = ["derive"] }
+rand = { workspace = true }
+hex = { workspace = true }
+
+[dev-dependencies]
+test-utils = { workspace = true }
diff --git a/protocols/dfns-cggmp21/src/constants.rs b/protocols/dfns-cggmp21/src/constants.rs
new file mode 100644
index 000000000..7e0794b97
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/constants.rs
@@ -0,0 +1,29 @@
+// ================= Common ======================== //
+pub const DFNS_CGGMP21_KEYGEN_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keygen/1";
+pub const DFNS_CGGMP21_SIGNING_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/signing/1";
+
+// ============= Signing Protocol ======================= //
+
+pub mod signing_worker {
+    use std::time::Duration;
+
+    // the maximum number of tasks that the work manager tries to assign
+    pub const MAX_RUNNING_TASKS: usize = 2;
+
+    // the maximum number of tasks that can be enqueued,
+    // enqueued here implies not actively running but listening for messages
+    pub const MAX_ENQUEUED_TASKS: usize = 10;
+
+    // How often to poll the jobs to check completion status
+    pub const JOB_POLL_INTERVAL: Duration = Duration::from_millis(500);
+}
+
+// ============= Keygen Protocol ======================= //
+
+pub mod keygen_worker {
+    /// the maximum number of tasks that the work manager tries to assign
+    /// at any given time for the keygen protocol.
+    pub const MAX_RUNNING_TASKS: usize = 2;
+    /// the maximum number of tasks that can be enqueued.
+    pub const MAX_ENQUEUED_TASKS: usize = 10;
+}
diff --git a/protocols/dfns-cggmp21/src/error.rs b/protocols/dfns-cggmp21/src/error.rs
new file mode 100644
index 000000000..3b2b51501
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/error.rs
@@ -0,0 +1,33 @@
+use gadget_core::job::JobError;
+use std::fmt::{Display, Formatter};
+
+#[derive(Debug)]
+pub enum Error {
+    Keystore(String),
+    Signature(String),
+    Job(JobError),
+    InvalidKeygenPartyId,
+    InvalidSigningSet,
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        std::fmt::Debug::fmt(self, f)
+    }
+}
+
+impl std::error::Error for Error {}
+
+impl From<JobError> for Error {
+    fn from(value: JobError) -> Self {
+        Error::Job(value)
+    }
+}
+
+impl From<Error> for JobError {
+    fn from(val: Error) -> Self {
+        JobError {
+            reason: val.to_string(),
+        }
+    }
+}
diff --git a/protocols/dfns-cggmp21/src/lib.rs b/protocols/dfns-cggmp21/src/lib.rs
new file mode 100644
index 000000000..15f6ae188
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/lib.rs
@@ -0,0 +1,188 @@
+use gadget_common::client::{AccountId, ClientWithApi, JobsClient, PalletSubmitter};
+use gadget_common::config::NetworkAndProtocolSetup;
+use gadget_common::debug_logger::DebugLogger;
+use gadget_common::gadget::network::Network;
+use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
+use pallet_jobs_rpc_runtime_api::JobsApi;
+use protocol_macros::protocol;
+use protocols::keygen::DfnsCGGMP21KeygenProtocol;
+use protocols::sign::DfnsCGGMP21SigningProtocol;
+use sc_client_api::Backend;
+use sp_api::ProvideRuntimeApi;
+use sp_runtime::traits::Block;
+use std::sync::Arc;
+
+pub mod constants;
+pub mod error;
+pub mod protocols;
+pub mod util;
+
+#[protocol]
+pub struct DfnsCGGMP21KeygenProtocolConfig<
+    N: Network,
+    B: Block,
+    BE: Backend<B>,
+    KBE: KeystoreBackend,
+    C: ClientWithApi<B, BE>,
+> where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    pub account_id: AccountId,
+    pub network: N,
+    pub keystore_backend: ECDSAKeyStore<KBE>,
+    pub client: C,
+    pub logger: DebugLogger,
+    pub pallet_tx: Arc<dyn PalletSubmitter>,
+    pub _pd: std::marker::PhantomData<(B, BE)>,
+}
+
+#[protocol]
+pub struct DfnsCGGMP21SigningProtocolConfig<
+    N: Network,
+    B: Block,
+    BE: Backend<B>,
+    KBE: KeystoreBackend,
+    C: ClientWithApi<B, BE>,
+> where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    pub account_id: AccountId,
+    pub network: N,
+    pub keystore_backend: ECDSAKeyStore<KBE>,
+    pub client: C,
+    pub logger: DebugLogger,
+    pub pallet_tx: Arc<dyn PalletSubmitter>,
+    pub _pd: std::marker::PhantomData<(B, BE)>,
+}
+
+#[async_trait::async_trait]
+impl<N: Network, B: Block, BE: Backend<B>, KBE: KeystoreBackend, C: ClientWithApi<B, BE>>
+    NetworkAndProtocolSetup for DfnsCGGMP21KeygenProtocolConfig<N, B, BE, KBE, C>
+where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    type Network = N;
+    type Protocol = DfnsCGGMP21KeygenProtocol<B, BE, KBE, C, N>;
+    type Client = C;
+    type Block = B;
+    type Backend = BE;
+
+    async fn build_network_and_protocol(
+        &self,
+        jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
+    ) -> Result<(Self::Network, Self::Protocol), gadget_common::Error> {
+        let protocol = protocols::keygen::create_protocol(
+            self.account_id,
+            jobs_client,
+            self.network.clone(),
+            self.logger.clone(),
+            self.keystore_backend.clone(),
+        )
+        .await;
+
+        Ok((self.network.clone(), protocol))
+    }
+
+    fn pallet_tx(&self) -> Arc<dyn PalletSubmitter> {
+        self.pallet_tx.clone()
+    }
+
+    fn logger(&self) -> DebugLogger {
+        self.logger.clone()
+    }
+
+    fn client(&self) -> Self::Client {
+        self.client.clone()
+    }
+}
+
+#[async_trait::async_trait]
+impl<N: Network, B: Block, BE: Backend<B>, KBE: KeystoreBackend, C: ClientWithApi<B, BE>>
+    NetworkAndProtocolSetup for DfnsCGGMP21SigningProtocolConfig<N, B, BE, KBE, C>
+where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    type Network = N;
+    type Protocol = DfnsCGGMP21SigningProtocol<B, BE, KBE, C, N>;
+    type Client = C;
+    type Block = B;
+    type Backend = BE;
+
+    async fn build_network_and_protocol(
+        &self,
+        jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
+    ) -> Result<(Self::Network, Self::Protocol), gadget_common::Error> {
+        let protocol = protocols::sign::create_protocol(
+            self.account_id,
+            self.logger.clone(),
+            jobs_client,
+            self.network.clone(),
+            self.keystore_backend.clone(),
+        )
+        .await;
+        Ok((self.network.clone(), protocol))
+    }
+
+    fn pallet_tx(&self) -> Arc<dyn PalletSubmitter> {
+        self.pallet_tx.clone()
+    }
+
+    fn logger(&self) -> DebugLogger {
+        self.logger.clone()
+    }
+
+    fn client(&self) -> Self::Client {
+        self.client.clone()
+    }
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn run<B, BE, KBE, C, N, N2, Tx>(
+    account_id: AccountId,
+    client_keygen: C,
+    client_signing: C,
+    logger: DebugLogger,
+    keystore: ECDSAKeyStore<KBE>,
+    network_keygen: N,
+    network_signing: N2,
+    pallet_tx: Tx,
+) -> Result<(), gadget_common::Error>
+where
+    B: Block,
+    BE: Backend<B> + 'static,
+    C: ClientWithApi<B, BE>,
+    KBE: KeystoreBackend,
+    N: Network,
+    N2: Network,
+    Tx: PalletSubmitter,
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    let pallet_tx = Arc::new(pallet_tx) as Arc<dyn PalletSubmitter>;
+    let keygen_config = DfnsCGGMP21KeygenProtocolConfig {
+        account_id,
+        network: network_keygen,
+        keystore_backend: keystore.clone(),
+        client: client_keygen,
+        logger: logger.clone(),
+        pallet_tx: pallet_tx.clone(),
+        _pd: std::marker::PhantomData,
+    };
+
+    let sign_config = DfnsCGGMP21SigningProtocolConfig {
+        account_id,
+        network: network_signing,
+        keystore_backend: keystore,
+        client: client_signing,
+        logger,
+        pallet_tx: pallet_tx.clone(),
+        _pd: std::marker::PhantomData,
+    };
+
+    let keygen_future = keygen_config.execute();
+    let sign_future = sign_config.execute();
+
+    tokio::select! {
+        res0 = keygen_future => res0,
+        res1 = sign_future => res1,
+    }
+}
diff --git a/protocols/dfns-cggmp21/src/protocols/keygen.rs b/protocols/dfns-cggmp21/src/protocols/keygen.rs
new file mode 100644
index 000000000..2f2e09e57
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/protocols/keygen.rs
@@ -0,0 +1,531 @@
+use async_trait::async_trait;
+use dfns_cggmp21::supported_curves::Secp256k1;
+use dfns_cggmp21::KeyShare;
+use futures::StreamExt;
+use gadget_common::client::{AccountId, ClientWithApi, JobsClient};
+use gadget_common::debug_logger::DebugLogger;
+use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
+use gadget_common::gadget::network::Network;
+use gadget_common::gadget::work_manager::WebbWorkManager;
+use gadget_common::gadget::{JobInitMetadata, WebbGadgetProtocol, WorkManagerConfig};
+use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
+use gadget_common::protocol::AsyncProtocol;
+use gadget_common::{Block, BlockImportNotification};
+use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
+use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
+use itertools::Itertools;
+use pallet_jobs_rpc_runtime_api::JobsApi;
+use rand::SeedableRng;
+use sc_client_api::Backend;
+use sp_api::ProvideRuntimeApi;
+use sp_application_crypto::sp_core::keccak_256;
+use sp_core::{ecdsa, Pair};
+use std::collections::{BTreeMap, HashMap};
+use std::sync::Arc;
+use tangle_primitives::jobs::{
+    DKGTSSKeySubmissionResult, DigitalSignatureType, JobId, JobResult, JobType,
+};
+use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use tokio::sync::mpsc::UnboundedReceiver;
+
+use super::util::PublicKeyGossipMessage;
+
+pub struct DfnsCGGMP21KeygenProtocol<B: Block, BE, KBE: KeystoreBackend, C, N> {
+    client: JobsClient<B, BE, C>,
+    key_store: ECDSAKeyStore<KBE>,
+    network: N,
+    logger: DebugLogger,
+    account_id: AccountId,
+}
+
+pub async fn create_protocol<B, BE, KBE, C, N>(
+    account_id: AccountId,
+    client: JobsClient<B, BE, C>,
+    network: N,
+    logger: DebugLogger,
+    key_store: ECDSAKeyStore<KBE>,
+) -> DfnsCGGMP21KeygenProtocol<B, BE, KBE, C, N>
+where
+    B: Block,
+    BE: Backend<B>,
+    C: ClientWithApi<B, BE>,
+    KBE: KeystoreBackend,
+    N: Network,
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    DfnsCGGMP21KeygenProtocol {
+        client,
+        network,
+        key_store,
+        logger,
+        account_id,
+    }
+}
+
+#[async_trait]
+impl<
+        B: Block,
+        BE: Backend<B> + 'static,
+        C: ClientWithApi<B, BE>,
+        KBE: KeystoreBackend,
+        N: Network,
+    > WebbGadgetProtocol<B, BE, C> for DfnsCGGMP21KeygenProtocol<B, BE, KBE, C, N>
+where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    async fn create_next_job(
+        &self,
+        job: JobInitMetadata,
+    ) -> Result<<Self as AsyncProtocol>::AdditionalParams, gadget_common::Error> {
+        let now = job.now;
+        self.logger.info(format!("At finality notification {now}"));
+
+        let job_id = job.job_id;
+        let role_type = job.job_type.get_role_type();
+
+        // We can safely make this assumption because we are only creating jobs for phase one
+        let JobType::DKGTSSPhaseOne(p1_job) = job.job_type else {
+            panic!("Should be valid type")
+        };
+
+        let participants = p1_job.participants;
+        let threshold = p1_job.threshold;
+
+        let user_id_to_account_id_mapping = Arc::new(
+            participants
+                .clone()
+                .into_iter()
+                .enumerate()
+                .map(|r| (r.0 as UserID, r.1))
+                .collect(),
+        );
+
+        let params = DfnsCGGMP21KeygenExtraParams {
+            i: participants
+                .iter()
+                .position(|p| p == &self.account_id)
+                .expect("Should exist") as u16,
+            t: threshold as u16,
+            n: participants.len() as u16,
+            role_type,
+            job_id,
+            user_id_to_account_id_mapping,
+        };
+
+        Ok(params)
+    }
+
+    fn role_type(&self) -> RoleType {
+        RoleType::Tss(ThresholdSignatureRoleType::TssCGGMP)
+    }
+
+    async fn process_block_import_notification(
+        &self,
+        _notification: BlockImportNotification<B>,
+        _job_manager: &ProtocolWorkManager<WebbWorkManager>,
+    ) -> Result<(), gadget_common::Error> {
+        Ok(())
+    }
+
+    async fn process_error(
+        &self,
+        error: gadget_common::Error,
+        _job_manager: &ProtocolWorkManager<WebbWorkManager>,
+    ) {
+        log::error!(target: "dfns_cggmp1", "Error: {error:?}");
+    }
+
+    fn account_id(&self) -> &AccountId {
+        &self.account_id
+    }
+
+    fn is_phase_one(&self) -> bool {
+        true
+    }
+
+    fn client(&self) -> &JobsClient<B, BE, C> {
+        &self.client
+    }
+
+    fn logger(&self) -> &DebugLogger {
+        &self.logger
+    }
+
+    fn get_work_manager_config(&self) -> WorkManagerConfig {
+        WorkManagerConfig {
+            interval: None, // Manual polling
+            max_active_tasks: crate::constants::keygen_worker::MAX_RUNNING_TASKS,
+            max_pending_tasks: crate::constants::keygen_worker::MAX_ENQUEUED_TASKS,
+        }
+    }
+}
+
+pub struct DfnsCGGMP21KeygenExtraParams {
+    i: u16,
+    t: u16,
+    n: u16,
+    job_id: JobId,
+    role_type: RoleType,
+    user_id_to_account_id_mapping: Arc<HashMap<UserID, AccountId>>,
+}
+
+#[async_trait]
+impl<
+        B: Block,
+        BE: Backend<B> + 'static,
+        KBE: KeystoreBackend,
+        C: ClientWithApi<B, BE>,
+        N: Network,
+    > AsyncProtocol for DfnsCGGMP21KeygenProtocol<B, BE, KBE, C, N>
+where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    type AdditionalParams = DfnsCGGMP21KeygenExtraParams;
+    async fn generate_protocol_from(
+        &self,
+        associated_block_id: <WebbWorkManager as WorkManagerInterface>::Clock,
+        associated_retry_id: <WebbWorkManager as WorkManagerInterface>::RetryID,
+        associated_session_id: <WebbWorkManager as WorkManagerInterface>::SessionID,
+        associated_task_id: <WebbWorkManager as WorkManagerInterface>::TaskID,
+        protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
+        additional_params: Self::AdditionalParams,
+    ) -> Result<BuiltExecutableJobWrapper, JobError> {
+        let key_store = self.key_store.clone();
+        let key_store_for_gossip = self.key_store.clone();
+        let protocol_output = Arc::new(tokio::sync::Mutex::new(None));
+        let protocol_output_clone = protocol_output.clone();
+        let client = self.client.clone();
+        let id = self.account_id;
+        let logger = self.logger.clone();
+        let network = self.network.clone();
+
+        let (i, t, n, mapping) = (
+            additional_params.i,
+            additional_params.t,
+            additional_params.n,
+            additional_params.user_id_to_account_id_mapping,
+        );
+
+        Ok(JobBuilder::new()
+            .protocol(async move {
+                let mut rng = rand::rngs::StdRng::from_entropy();
+                let protocol_message_channel =
+                    super::util::CloneableUnboundedReceiver::from(protocol_message_channel);
+                logger.info(format!(
+                    "Starting Keygen Protocol with params: i={i}, t={t}, n={n}"
+                ));
+
+                let job_id_bytes = additional_params.job_id.to_be_bytes();
+                let mix = keccak_256(b"dnfs-cggmp21-keygen");
+                let eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
+                let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes);
+                let mix = keccak_256(b"dnfs-cggmp21-keygen-aux");
+                let aux_eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
+                let aux_eid = dfns_cggmp21::ExecutionId::new(&aux_eid_bytes);
+
+                let (
+                    keygen_tx_to_outbound,
+                    keygen_rx_async_proto,
+                    _broadcast_tx_to_outbound,
+                    _broadcast_rx_from_gadget,
+                ) = super::util::create_job_manager_to_async_protocol_channel_split::<_, (), _>(
+                    protocol_message_channel.clone(),
+                    associated_block_id,
+                    associated_retry_id,
+                    associated_session_id,
+                    associated_task_id,
+                    mapping.clone(),
+                    id,
+                    network.clone(),
+                );
+                let delivery = (keygen_rx_async_proto, keygen_tx_to_outbound);
+                let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
+                let incomplete_key_share = dfns_cggmp21::keygen::<Secp256k1>(eid, i, n)
+                    .set_threshold(t)
+                    .start(&mut rng, party)
+                    .await
+                    .map_err(|err| JobError {
+                        reason: format!("Keygen protocol error: {err:?}"),
+                    })?;
+
+                logger.debug("Finished AsyncProtocol - Incomplete Keygen");
+
+                let (
+                    keygen_tx_to_outbound,
+                    keygen_rx_async_proto,
+                    broadcast_tx_to_outbound,
+                    broadcast_rx_from_gadget,
+                ) = super::util::create_job_manager_to_async_protocol_channel_split(
+                    protocol_message_channel.clone(),
+                    associated_block_id,
+                    associated_retry_id,
+                    associated_session_id,
+                    associated_task_id,
+                    mapping,
+                    id,
+                    network,
+                );
+                let pregenerated_primes = dfns_cggmp21::PregeneratedPrimes::generate(&mut rng);
+                let delivery = (keygen_rx_async_proto, keygen_tx_to_outbound);
+                let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
+                let aux_info = dfns_cggmp21::aux_info_gen(aux_eid, i, n, pregenerated_primes)
+                    .start(&mut rng, party)
+                    .await
+                    .map_err(|err| JobError {
+                        reason: format!("Aux info protocol error: {err:?}"),
+                    })?;
+                logger.debug("Finished AsyncProtocol - Aux Info");
+
+                let key_share = dfns_cggmp21::KeyShare::make(incomplete_key_share, aux_info)
+                    .map_err(|err| JobError {
+                        reason: format!("Key share error: {err:?}"),
+                    })?;
+
+                logger.debug("Finished AsyncProtocol - Keygen");
+
+                let job_result = handle_public_key_gossip(
+                    key_store_for_gossip,
+                    &logger,
+                    &key_share,
+                    t,
+                    i,
+                    broadcast_tx_to_outbound,
+                    broadcast_rx_from_gadget,
+                )
+                .await?;
+
+                *protocol_output.lock().await = Some((key_share, job_result));
+                Ok(())
+            })
+            .post(async move {
+                // TODO: handle protocol blames
+                // Store the keys locally, as well as submitting them to the blockchain
+                if let Some((local_key, job_result)) = protocol_output_clone.lock().await.take() {
+                    key_store
+                        .set(additional_params.job_id, local_key)
+                        .await
+                        .map_err(|err| JobError {
+                            reason: format!("Failed to store key: {err:?}"),
+                        })?;
+
+                    client
+                        .submit_job_result(
+                            additional_params.role_type,
+                            additional_params.job_id,
+                            job_result,
+                        )
+                        .await
+                        .map_err(|err| JobError {
+                            reason: format!("Failed to submit job result: {err:?}"),
+                        })?;
+                }
+
+                Ok(())
+            })
+            .build())
+    }
+}
+
+#[allow(clippy::too_many_arguments)]
+async fn handle_public_key_gossip<KBE: KeystoreBackend>(
+    key_store: ECDSAKeyStore<KBE>,
+    logger: &DebugLogger,
+    local_key: &KeyShare<Secp256k1>,
+    t: u16,
+    i: u16,
+    broadcast_tx_to_outbound: futures::channel::mpsc::UnboundedSender<PublicKeyGossipMessage>,
+    mut broadcast_rx_from_gadget: futures::channel::mpsc::UnboundedReceiver<PublicKeyGossipMessage>,
+) -> Result<JobResult, JobError> {
+    let serialized_public_key = local_key.shared_public_key().to_bytes(true).to_vec();
+    let key_hashed = keccak_256(&serialized_public_key);
+    let signature = key_store.pair().sign_prehashed(&key_hashed).0.to_vec();
+    let my_id = key_store.pair().public();
+    let mut received_keys = BTreeMap::new();
+    received_keys.insert(i, signature.clone());
+    let mut received_participants = BTreeMap::new();
+    received_participants.insert(i, my_id);
+
+    broadcast_tx_to_outbound
+        .unbounded_send(PublicKeyGossipMessage {
+            from: i as _,
+            to: None,
+            signature,
+            id: my_id,
+        })
+        .map_err(|err| JobError {
+            reason: format!("Failed to send public key: {err:?}"),
+        })?;
+
+    for _ in 0..t {
+        let message = broadcast_rx_from_gadget
+            .next()
+            .await
+            .ok_or_else(|| JobError {
+                reason: "Failed to receive public key".to_string(),
+            })?;
+
+        let from = message.from;
+        logger.debug(format!("Received public key from {from}"));
+
+        if received_keys.contains_key(&(from as u16)) {
+            logger.warn("Received duplicate key");
+            continue;
+        }
+        // verify signature
+        let maybe_signature = sp_core::ecdsa::Signature::from_slice(&message.signature);
+        match maybe_signature.and_then(|s| s.recover_prehashed(&key_hashed)) {
+            Some(p) if p != message.id => {
+                logger.warn(format!(
+                    "Received invalid signature from {from} not signed by them"
+                ));
+            }
+            Some(p) if p == message.id => {
+                logger.debug(format!("Received valid signature from {from}"));
+            }
+            Some(_) => unreachable!("Should not happen"),
+            None => {
+                logger.warn(format!("Received invalid signature from {from}"));
+                continue;
+            }
+        }
+
+        received_keys.insert(from as u16, message.signature);
+        received_participants.insert(from as u16, message.id);
+        logger.debug(format!(
+            "Received {}/{} signatures",
+            received_keys.len(),
+            t + 1
+        ));
+    }
+
+    // Order and collect the map to ensure symmetric submission to blockchain
+    let signatures = received_keys
+        .into_iter()
+        .sorted_by_key(|x| x.0)
+        .map(|r| r.1)
+        .collect::<Vec<_>>();
+
+    let participants = received_participants
+        .into_iter()
+        .sorted_by_key(|x| x.0)
+        .map(|r| r.1 .0.to_vec())
+        .collect();
+
+    if signatures.len() < t as usize {
+        return Err(JobError {
+            reason: format!(
+                "Received {} signatures, expected at least {}",
+                signatures.len(),
+                t + 1,
+            ),
+        });
+    }
+
+    let res = DKGTSSKeySubmissionResult {
+        signature_type: DigitalSignatureType::Ecdsa,
+        key: serialized_public_key,
+        participants,
+        signatures,
+        threshold: t as _,
+    };
+    verify_generated_dkg_key_ecdsa(res.clone(), logger);
+    Ok(JobResult::DKGPhaseOne(res))
+}
+
+fn verify_generated_dkg_key_ecdsa(data: DKGTSSKeySubmissionResult, logger: &DebugLogger) {
+    // Ensure participants and signatures are not empty
+    assert!(!data.participants.is_empty(), "NoParticipantsFound",);
+    assert!(!data.signatures.is_empty(), "NoSignaturesFound");
+
+    // Generate the required ECDSA signers
+    let maybe_signers = data
+        .participants
+        .iter()
+        .map(|x| {
+            ecdsa::Public(
+                to_slice_33(x)
+                    .unwrap_or_else(|| panic!("Failed to convert input to ecdsa public key")),
+            )
+        })
+        .collect::<Vec<ecdsa::Public>>();
+
+    assert!(!maybe_signers.is_empty(), "NoParticipantsFound");
+
+    let mut known_signers: Vec<ecdsa::Public> = Default::default();
+
+    for signature in data.signatures {
+        // Ensure the required signer signature exists
+        let (maybe_authority, success) =
+            verify_signer_from_set_ecdsa(maybe_signers.clone(), &data.key, &signature);
+
+        if success {
+            let authority = maybe_authority.expect("CannotRetreiveSigner");
+
+            // Ensure no duplicate signatures
+            assert!(!known_signers.contains(&authority), "DuplicateSignature");
+
+            logger.debug(format!("Verified signature from {}", authority));
+            known_signers.push(authority);
+        }
+    }
+
+    // Ensure a sufficient number of unique signers are present
+    assert!(
+        known_signers.len() > data.threshold as usize,
+        "NotEnoughSigners"
+    );
+    logger.debug(format!(
+        "Verified {}/{} signatures",
+        known_signers.len(),
+        data.threshold + 1
+    ));
+}
+
+pub fn verify_signer_from_set_ecdsa(
+    maybe_signers: Vec<ecdsa::Public>,
+    msg: &[u8],
+    signature: &[u8],
+) -> (Option<ecdsa::Public>, bool) {
+    let mut signer = None;
+    let res = maybe_signers.iter().any(|x| {
+        if let Some(data) = recover_ecdsa_pub_key(msg, signature) {
+            let recovered = &data[..32];
+            if x.0[1..].to_vec() == recovered.to_vec() {
+                signer = Some(*x);
+                true
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    });
+
+    (signer, res)
+}
+
+pub fn recover_ecdsa_pub_key(data: &[u8], signature: &[u8]) -> Option<Vec<u8>> {
+    const SIGNATURE_LENGTH: usize = 65;
+    if signature.len() != SIGNATURE_LENGTH {
+        return None;
+    }
+    let mut sig = [0u8; SIGNATURE_LENGTH];
+    sig[..SIGNATURE_LENGTH].copy_from_slice(signature);
+
+    let hash = keccak_256(data);
+
+    sp_io::crypto::secp256k1_ecdsa_recover(&sig, &hash)
+        .ok()
+        .map(|x| x.to_vec())
+}
+
+pub fn to_slice_33(val: &[u8]) -> Option<[u8; 33]> {
+    const ECDSA_KEY_LENGTH: usize = 33;
+    if val.len() == ECDSA_KEY_LENGTH {
+        let mut key = [0u8; ECDSA_KEY_LENGTH];
+        key[..ECDSA_KEY_LENGTH].copy_from_slice(val);
+
+        return Some(key);
+    }
+    None
+}
diff --git a/protocols/dfns-cggmp21/src/protocols/mod.rs b/protocols/dfns-cggmp21/src/protocols/mod.rs
new file mode 100644
index 000000000..16b9199e9
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/protocols/mod.rs
@@ -0,0 +1,3 @@
+pub mod keygen;
+pub mod sign;
+pub mod util;
diff --git a/protocols/dfns-cggmp21/src/protocols/sign.rs b/protocols/dfns-cggmp21/src/protocols/sign.rs
new file mode 100644
index 000000000..ec0073f59
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/protocols/sign.rs
@@ -0,0 +1,352 @@
+use async_trait::async_trait;
+use dfns_cggmp21::supported_curves::Secp256k1;
+use dfns_cggmp21::KeyShare;
+use gadget_common::client::{AccountId, ClientWithApi, JobsClient};
+use gadget_common::debug_logger::DebugLogger;
+use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
+use gadget_common::gadget::network::Network;
+use gadget_common::gadget::work_manager::WebbWorkManager;
+use gadget_common::gadget::{JobInitMetadata, WebbGadgetProtocol, WorkManagerConfig};
+use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
+use gadget_common::protocol::AsyncProtocol;
+use gadget_common::{Block, BlockImportNotification};
+use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
+use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
+use pallet_jobs_rpc_runtime_api::JobsApi;
+use rand::SeedableRng;
+use sc_client_api::Backend;
+use sp_api::ProvideRuntimeApi;
+use sp_core::keccak_256;
+use std::collections::HashMap;
+use std::sync::Arc;
+use tangle_primitives::jobs::{
+    DKGTSSSignatureResult, DigitalSignatureType, JobId, JobResult, JobType,
+};
+use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use tokio::sync::mpsc::UnboundedReceiver;
+
+pub struct DfnsCGGMP21SigningProtocol<B: Block, BE, KBE: KeystoreBackend, C, N> {
+    client: JobsClient<B, BE, C>,
+    key_store: ECDSAKeyStore<KBE>,
+    network: N,
+    logger: DebugLogger,
+    account_id: AccountId,
+}
+
+pub async fn create_protocol<B, BE, KBE, C, N>(
+    account_id: AccountId,
+    logger: DebugLogger,
+    client: JobsClient<B, BE, C>,
+    network: N,
+    key_store: ECDSAKeyStore<KBE>,
+) -> DfnsCGGMP21SigningProtocol<B, BE, KBE, C, N>
+where
+    B: Block,
+    BE: Backend<B>,
+    C: ClientWithApi<B, BE>,
+    KBE: KeystoreBackend,
+    N: Network,
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    DfnsCGGMP21SigningProtocol {
+        client,
+        network,
+        key_store,
+        logger,
+        account_id,
+    }
+}
+
+#[async_trait]
+impl<
+        B: Block,
+        BE: Backend<B> + 'static,
+        C: ClientWithApi<B, BE>,
+        KBE: KeystoreBackend,
+        N: Network,
+    > WebbGadgetProtocol<B, BE, C> for DfnsCGGMP21SigningProtocol<B, BE, KBE, C, N>
+where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    async fn create_next_job(
+        &self,
+        job: JobInitMetadata,
+    ) -> Result<<Self as AsyncProtocol>::AdditionalParams, gadget_common::Error> {
+        let job_id = job.job_id;
+
+        let JobType::DKGTSSPhaseTwo(p2_job) = job.job_type else {
+            panic!("Should be valid type")
+        };
+        let input_data_to_sign = p2_job.submission;
+        let previous_job_id = p2_job.phase_one_id;
+
+        let phase1_job = job.phase1_job.expect("Should exist for a phase 2 job");
+        let participants = phase1_job.clone().get_participants().expect("Should exist");
+        let threshold = phase1_job.get_threshold().expect("Should exist") as u16;
+
+        let i = participants
+            .iter()
+            .position(|p| p == &self.account_id)
+            .expect("Should exist") as u16;
+        // TODO: decide how we will pick the signers.
+        // For now, we will just pick the first t participants.
+        let signers = participants
+            .iter()
+            .enumerate()
+            .take(threshold as usize)
+            .map(|(i, _)| i as u16)
+            .collect::<Vec<_>>();
+
+        let key = self
+            .key_store
+            .get(&previous_job_id)
+            .await
+            .map_err(|err| gadget_common::Error::ClientError {
+                err: err.to_string(),
+            })?
+            .ok_or_else(|| gadget_common::Error::ClientError {
+                err: format!("No key found for job ID: {job_id:?}"),
+            })?;
+
+        if participants.contains(&self.account_id) && signers.contains(&i) {
+            let user_id_to_account_id_mapping = Arc::new(
+                participants
+                    .clone()
+                    .into_iter()
+                    .enumerate()
+                    .map(|r| (r.0 as UserID, r.1))
+                    .collect(),
+            );
+
+            let params = DfnsCGGMP21SigningExtraParams {
+                i,
+                t: threshold,
+                signers,
+                job_id,
+                role_type: RoleType::Tss(ThresholdSignatureRoleType::TssCGGMP),
+                key,
+                input_data_to_sign,
+                user_id_to_account_id_mapping,
+            };
+            Ok(params)
+        } else {
+            Err(gadget_common::Error::ClientError {
+                err: format!(
+                    "Account ID {account_id:?} is not a participant or signer for job {job_id:?}",
+                    account_id = self.account_id,
+                    job_id = job_id
+                ),
+            })
+        }
+    }
+
+    async fn process_block_import_notification(
+        &self,
+        _notification: BlockImportNotification<B>,
+        _job_manager: &ProtocolWorkManager<WebbWorkManager>,
+    ) -> Result<(), gadget_common::Error> {
+        Ok(())
+    }
+
+    async fn process_error(
+        &self,
+        error: gadget_common::Error,
+        _job_manager: &ProtocolWorkManager<WebbWorkManager>,
+    ) {
+        log::error!(target: "gadget", "Error: {error:?}");
+    }
+
+    fn account_id(&self) -> &AccountId {
+        &self.account_id
+    }
+
+    fn role_type(&self) -> RoleType {
+        RoleType::Tss(ThresholdSignatureRoleType::TssCGGMP)
+    }
+
+    fn is_phase_one(&self) -> bool {
+        false
+    }
+
+    fn client(&self) -> &JobsClient<B, BE, C> {
+        &self.client
+    }
+
+    fn logger(&self) -> &DebugLogger {
+        &self.logger
+    }
+
+    fn get_work_manager_config(&self) -> WorkManagerConfig {
+        WorkManagerConfig {
+            interval: Some(crate::constants::signing_worker::JOB_POLL_INTERVAL),
+            max_active_tasks: crate::constants::signing_worker::MAX_RUNNING_TASKS,
+            max_pending_tasks: crate::constants::signing_worker::MAX_ENQUEUED_TASKS,
+        }
+    }
+}
+
+pub struct DfnsCGGMP21SigningExtraParams {
+    i: u16,
+    t: u16,
+    signers: Vec<u16>,
+    job_id: JobId,
+    role_type: RoleType,
+    key: KeyShare<Secp256k1>,
+    input_data_to_sign: Vec<u8>,
+    user_id_to_account_id_mapping: Arc<HashMap<UserID, AccountId>>,
+}
+
+#[async_trait]
+impl<
+        B: Block,
+        BE: Backend<B> + 'static,
+        KBE: KeystoreBackend,
+        C: ClientWithApi<B, BE>,
+        N: Network,
+    > AsyncProtocol for DfnsCGGMP21SigningProtocol<B, BE, KBE, C, N>
+where
+    <C as ProvideRuntimeApi<B>>::Api: JobsApi<B, AccountId>,
+{
+    type AdditionalParams = DfnsCGGMP21SigningExtraParams;
+    async fn generate_protocol_from(
+        &self,
+        associated_block_id: <WebbWorkManager as WorkManagerInterface>::Clock,
+        associated_retry_id: <WebbWorkManager as WorkManagerInterface>::RetryID,
+        associated_session_id: <WebbWorkManager as WorkManagerInterface>::SessionID,
+        associated_task_id: <WebbWorkManager as WorkManagerInterface>::TaskID,
+        protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
+        additional_params: Self::AdditionalParams,
+    ) -> Result<BuiltExecutableJobWrapper, JobError> {
+        let debug_logger_post = self.logger.clone();
+        let logger = debug_logger_post.clone();
+        let protocol_output = Arc::new(tokio::sync::Mutex::new(None));
+        let protocol_output_clone = protocol_output.clone();
+        let client = self.client.clone();
+        let id = self.account_id;
+        let network = self.network.clone();
+
+        let (i, signers, t, key, input_data_to_sign, mapping) = (
+            additional_params.i,
+            additional_params.signers,
+            additional_params.t,
+            additional_params.key,
+            additional_params.input_data_to_sign.clone(),
+            additional_params.user_id_to_account_id_mapping.clone(),
+        );
+
+        let public_key_bytes = key.shared_public_key().to_bytes(true).to_vec();
+        let input_data_to_sign2 = input_data_to_sign.clone();
+
+        Ok(JobBuilder::new()
+            .protocol(async move {
+                let mut rng = rand::rngs::StdRng::from_entropy();
+                let protocol_message_channel =
+                    super::util::CloneableUnboundedReceiver::from(protocol_message_channel);
+
+                logger.info(format!(
+                    "Starting Signing Protocol with params: i={i}, t={t}"
+                ));
+
+                let job_id_bytes = additional_params.job_id.to_be_bytes();
+                let mix = keccak_256(b"dnfs-cggmp21-signing");
+                let eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
+                let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes);
+                let (
+                    signing_tx_to_outbound,
+                    signing_rx_async_proto,
+                    _broadcast_tx_to_outbound,
+                    _broadcast_rx_from_gadget,
+                ) = super::util::create_job_manager_to_async_protocol_channel_split::<_, (), _>(
+                    protocol_message_channel.clone(),
+                    associated_block_id,
+                    associated_retry_id,
+                    associated_session_id,
+                    associated_task_id,
+                    mapping.clone(),
+                    id,
+                    network.clone(),
+                );
+
+                let delivery = (signing_rx_async_proto, signing_tx_to_outbound);
+                let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
+                let data_hash = keccak_256(&input_data_to_sign);
+                let data_to_sign = dfns_cggmp21::DataToSign::from_scalar(
+                    dfns_cggmp21::generic_ec::Scalar::from_be_bytes_mod_order(data_hash),
+                );
+                let signature = dfns_cggmp21::signing(eid, i, &signers, &key)
+                    .sign(&mut rng, party, data_to_sign)
+                    .await
+                    .map_err(|err| JobError {
+                        reason: format!("Signing protocol error: {err:?}"),
+                    })?;
+
+                // Normalize the signature
+                let signature = signature.normalize_s();
+                logger.debug("Finished AsyncProtocol - Signing");
+                *protocol_output.lock().await = Some(signature);
+                Ok(())
+            })
+            .post(async move {
+                // Submit the protocol output to the blockchain
+                if let Some(signature) = protocol_output_clone.lock().await.take() {
+                    let mut signature_bytes = [0u8; 65];
+                    signature.write_to_slice(&mut signature_bytes[0..64]);
+                    // To figure out the recovery ID, we need to try all possible values of v
+                    // in our case, v can be 0 or 1
+                    let mut v = 0u8;
+                    loop {
+                        let mut signature_bytes = signature_bytes;
+                        let data_hash = keccak_256(&input_data_to_sign2);
+                        signature_bytes[64] = v;
+                        let res =
+                            sp_io::crypto::secp256k1_ecdsa_recover(&signature_bytes, &data_hash);
+                        match res {
+                            Ok(key) if key[..32] == public_key_bytes[1..] => {
+                                // Found the correct v
+                                break;
+                            }
+                            Ok(_) => {
+                                // Found a key, but not the correct one
+                                // Try the other v value
+                                v = 1;
+                                continue;
+                            }
+                            Err(_) if v == 1 => {
+                                // We tried both v values, but no key was found
+                                // This should never happen, but if it does, we will just
+                                // leave v as 1 and break
+                                break;
+                            }
+                            Err(_) => {
+                                // No key was found, try the other v value
+                                v = 1;
+                                continue;
+                            }
+                        }
+                    }
+                    signature_bytes[64] = v + 27;
+
+                    let job_result = JobResult::DKGPhaseTwo(DKGTSSSignatureResult {
+                        signature_type: DigitalSignatureType::Ecdsa,
+                        data: additional_params.input_data_to_sign,
+                        signature: signature_bytes.to_vec(),
+                        signing_key: public_key_bytes,
+                    });
+
+                    client
+                        .submit_job_result(
+                            additional_params.role_type,
+                            additional_params.job_id,
+                            job_result,
+                        )
+                        .await
+                        .map_err(|err| JobError {
+                            reason: format!("Failed to submit job result: {err:?}"),
+                        })?;
+                }
+
+                Ok(())
+            })
+            .build())
+    }
+}
diff --git a/protocols/dfns-cggmp21/src/protocols/state_machine.rs b/protocols/dfns-cggmp21/src/protocols/state_machine.rs
new file mode 100644
index 000000000..db6c6ab9a
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/protocols/state_machine.rs
@@ -0,0 +1,163 @@
+use gadget_common::debug_logger::DebugLogger;
+use multi_party_ecdsa::gg_2020::state_machine::traits::RoundBlame;
+use multi_party_ecdsa::MessageRoundID;
+use round_based::{Msg, StateMachine};
+use sp_application_crypto::serde::Serialize;
+use std::collections::HashSet;
+use std::fmt::Debug;
+
+pub(crate) struct StateMachineWrapper<T> {
+    sm: T,
+    current_round_blame: tokio::sync::watch::Sender<CurrentRoundBlame>,
+    // stores a list of received messages
+    received_messages: HashSet<Vec<u8>>,
+    logger: DebugLogger,
+}
+
+impl<T: StateMachine + RoundBlame + Debug> StateMachineWrapper<T> {
+    pub fn new(
+        sm: T,
+        current_round_blame: tokio::sync::watch::Sender<CurrentRoundBlame>,
+        logger: DebugLogger,
+    ) -> Self {
+        Self {
+            sm,
+            current_round_blame,
+            logger,
+            received_messages: HashSet::new(),
+        }
+    }
+}
+
+impl<T> StateMachine for StateMachineWrapper<T>
+where
+    T: StateMachine + RoundBlame + Debug,
+    <T as StateMachine>::Err: Debug,
+    <T as StateMachine>::MessageBody: Serialize + MessageRoundID,
+{
+    type Err = T::Err;
+    type Output = T::Output;
+    type MessageBody = T::MessageBody;
+
+    fn handle_incoming(&mut self, msg: Msg<Self::MessageBody>) -> Result<(), Self::Err> {
+        let (round, sender, _receiver) = (msg.body.round_id(), msg.sender, msg.receiver);
+
+        self.logger.trace(format!(
+            "Handling incoming message round={}, sender={}",
+            round, sender
+        ));
+
+        if round < self.current_round() {
+            self.logger
+                .trace(format!("Message for round={round} is outdated, ignoring",));
+            return Ok(());
+        }
+
+        // Before passing to the state machine, make sure that we haven't already received the same
+        // message (this is needed as we use a gossiping protocol to send messages, and we don't
+        // want to process the same message twice)
+        let msg_serde = bincode2::serialize(&msg).expect("Failed to serialize message");
+        if !self.received_messages.insert(msg_serde) {
+            self.logger.trace(format!(
+                "Already received message for round={}, sender={}",
+                round, sender
+            ));
+            return Ok(());
+        }
+
+        let result = self.sm.handle_incoming(msg.clone());
+
+        if let Some(err) = result.as_ref().err() {
+            self.logger.error(format!("StateMachine error: {err:?}"));
+        }
+
+        // Get the round blame to update round blame
+        let round_blame = self.round_blame();
+
+        self.logger.trace(format!(
+            "SM After: {:?} || round_blame: {:?}",
+            &self.sm, round_blame
+        ));
+
+        result
+    }
+
+    fn message_queue(&mut self) -> &mut Vec<Msg<Self::MessageBody>> {
+        self.sm.message_queue()
+    }
+
+    fn wants_to_proceed(&self) -> bool {
+        self.sm.wants_to_proceed()
+    }
+
+    fn proceed(&mut self) -> Result<(), Self::Err> {
+        self.logger.trace(format!(
+            "Trying to proceed: current round ({:?}), waiting for msgs from parties: ({:?})",
+            self.current_round(),
+            self.round_blame(),
+        ));
+        let now = std::time::Instant::now();
+        let result = self.sm.proceed();
+
+        let elapsed = now.elapsed();
+        self.logger.trace(format!(
+            "Proceeded through SM in {}ms. New current round ({:?}), waiting for msgs from parties: ({:?})",
+            elapsed.as_millis(),
+            self.current_round(),
+            self.round_blame(),
+        ));
+
+        result
+    }
+
+    fn round_timeout(&self) -> Option<std::time::Duration> {
+        self.sm.round_timeout()
+    }
+
+    fn round_timeout_reached(&mut self) -> Self::Err {
+        self.sm.round_timeout_reached()
+    }
+
+    fn is_finished(&self) -> bool {
+        self.sm.is_finished()
+    }
+
+    fn pick_output(&mut self) -> Option<Result<Self::Output, Self::Err>> {
+        self.sm.pick_output()
+    }
+
+    fn current_round(&self) -> u16 {
+        self.sm.current_round()
+    }
+
+    fn total_rounds(&self) -> Option<u16> {
+        self.sm.total_rounds()
+    }
+
+    fn party_ind(&self) -> u16 {
+        self.sm.party_ind()
+    }
+
+    fn parties(&self) -> u16 {
+        self.sm.parties()
+    }
+}
+
+impl<T: StateMachine + RoundBlame> RoundBlame for StateMachineWrapper<T> {
+    fn round_blame(&self) -> (u16, Vec<u16>) {
+        let (unreceived_messages, blamed_parties) = self.sm.round_blame();
+        self.logger
+            .debug(format!("Not received messages from : {blamed_parties:?}"));
+        let _ = self.current_round_blame.send(CurrentRoundBlame {
+            unreceived_messages,
+            blamed_parties: blamed_parties.clone(),
+        });
+        (unreceived_messages, blamed_parties)
+    }
+}
+
+#[derive(Default, Debug)]
+pub struct CurrentRoundBlame {
+    pub unreceived_messages: u16,
+    pub blamed_parties: Vec<u16>,
+}
diff --git a/protocols/dfns-cggmp21/src/protocols/util.rs b/protocols/dfns-cggmp21/src/protocols/util.rs
new file mode 100644
index 000000000..a59fa1d4c
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/protocols/util.rs
@@ -0,0 +1,453 @@
+#![allow(clippy::type_complexity, clippy::too_many_arguments)]
+//! When delivering messages to an async protocol, we want o make sure we don't mix up voting and public key gossip messages
+//! Thus, this file contains a function that takes a channel from the gadget to the async protocol and splits it into two channels
+use dfns_cggmp21::round_based::{Incoming, MessageDestination, MessageType, Outgoing, PartyIndex};
+use futures::{Stream, StreamExt};
+use gadget_common::client::AccountId;
+use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
+use gadget_common::gadget::network::Network;
+use gadget_common::gadget::work_manager::WebbWorkManager;
+use gadget_core::job_manager::WorkManagerInterface;
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+use tokio::sync::mpsc::UnboundedReceiver;
+
+/// A Channel Receiver that can be cloned.
+///
+/// On the second clone, the original channel will stop sending messages
+/// and the new channel will start sending messages.
+pub struct CloneableUnboundedReceiver<T> {
+    rx: Arc<tokio::sync::Mutex<UnboundedReceiver<T>>>,
+    is_in_use: Arc<AtomicBool>,
+}
+
+impl<T: Clone> Clone for CloneableUnboundedReceiver<T> {
+    fn clone(&self) -> Self {
+        // on the clone, we switch the is_in_use flag to false
+        // and we return a new channel
+        self.is_in_use
+            .store(false, std::sync::atomic::Ordering::SeqCst);
+        Self {
+            rx: self.rx.clone(),
+            is_in_use: Arc::new(AtomicBool::new(true)),
+        }
+    }
+}
+
+impl<T> From<UnboundedReceiver<T>> for CloneableUnboundedReceiver<T> {
+    fn from(rx: UnboundedReceiver<T>) -> Self {
+        Self {
+            rx: Arc::new(tokio::sync::Mutex::new(rx)),
+            is_in_use: Arc::new(AtomicBool::new(false)),
+        }
+    }
+}
+
+impl<T> Stream for CloneableUnboundedReceiver<T> {
+    type Item = T;
+    fn poll_next(
+        self: std::pin::Pin<&mut Self>,
+        cx: &mut std::task::Context<'_>,
+    ) -> std::task::Poll<Option<Self::Item>> {
+        if !self.is_in_use.load(std::sync::atomic::Ordering::SeqCst) {
+            return std::task::Poll::Ready(None);
+        }
+        let mut rx = match self.rx.try_lock() {
+            Ok(rx) => rx,
+            Err(_) => return std::task::Poll::Pending,
+        };
+        let rx = &mut *rx;
+        tokio::pin!(rx);
+        rx.poll_recv(cx)
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum SplitChannelMessage<C1, C2> {
+    Channel1(C1),
+    Channel2(C2),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct VotingMessage {
+    pub from: UserID,
+    pub to: Option<UserID>,
+    pub payload: Vec<u8>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct PublicKeyGossipMessage {
+    pub from: UserID,
+    pub to: Option<UserID>,
+    pub signature: Vec<u8>,
+    pub id: AccountId,
+}
+
+/// All possible senders of a message
+#[derive(Debug, Default, Serialize, Deserialize)]
+pub enum MaybeSender {
+    /// We are the sender of the message
+    Myself,
+    /// The sender is someone else
+    /// it could also be us, double check the [`UserID`]
+    SomeoneElse(UserID),
+    /// The sender is unknown.
+    #[default]
+    Unknown,
+}
+
+impl MaybeSender {
+    /// Returns `true` if the maybe sender is [`Myself`].
+    ///
+    /// [`Myself`]: MaybeSender::Myself
+    #[must_use]
+    pub fn is_myself(&self) -> bool {
+        matches!(self, Self::Myself)
+    }
+
+    /// Returns `true` if the maybe sender is [`Myself`].
+    /// Or if the sender is [`SomeoneElse`] but the [`UserID`] is the same as `my_user_id`
+    ///
+    /// [`Myself`]: MaybeSender::Myself
+    /// [`SomeoneElse`]: MaybeSender::SomeoneElse
+    #[must_use]
+    pub fn is_myself_check(&self, my_user_id: UserID) -> bool {
+        match self {
+            Self::Myself => true,
+            Self::SomeoneElse(id) if (*id == my_user_id) => true,
+            _ => false,
+        }
+    }
+
+    /// Returns `true` if the maybe sender is [`SomeoneElse`].
+    ///
+    /// [`SomeoneElse`]: MaybeSender::SomeoneElse
+    #[must_use]
+    pub fn is_someone_else(&self) -> bool {
+        matches!(self, Self::SomeoneElse(..))
+    }
+
+    /// Returns `true` if the maybe sender is [`Unknown`].
+    ///
+    /// [`Unknown`]: MaybeSender::Unknown
+    #[must_use]
+    pub fn is_unknown(&self) -> bool {
+        matches!(self, Self::Unknown)
+    }
+
+    /// Returns the sender as [`UserID`] if it is knwon.
+    #[must_use]
+    pub fn as_user_id(&self) -> Option<UserID> {
+        match self {
+            Self::Myself => None,
+            Self::SomeoneElse(id) => Some(*id),
+            Self::Unknown => None,
+        }
+    }
+}
+
+#[derive(Debug, Default, Serialize, Deserialize)]
+pub enum MaybeReceiver {
+    /// The message is broadcasted to everyone
+    Broadcast,
+    /// The message is sent to a specific party
+    P2P(UserID),
+    /// The receiver is us.
+    Myself,
+    /// The receiver is unknown.
+    #[default]
+    Unknown,
+}
+
+impl MaybeReceiver {
+    /// Returns `true` if the maybe receiver is [`Broadcast`].
+    ///
+    /// [`Broadcast`]: MaybeReceiver::Broadcast
+    #[must_use]
+    pub fn is_broadcast(&self) -> bool {
+        matches!(self, Self::Broadcast)
+    }
+
+    /// Returns `true` if the maybe receiver is [`P2P`].
+    ///
+    /// [`P2P`]: MaybeReceiver::P2P
+    #[must_use]
+    pub fn is_p2_p(&self) -> bool {
+        matches!(self, Self::P2P(..))
+    }
+
+    /// Returns `true` if the maybe receiver is [`Myself`].
+    ///
+    /// [`Myself`]: MaybeReceiver::Myself
+    #[must_use]
+    pub fn is_myself(&self) -> bool {
+        matches!(self, Self::Myself)
+    }
+
+    /// Returns `true` if the maybe receiver is [`Myself`]
+    /// Or if the receiver is [`P2P`] but the [`UserID`] is the same as `my_user_id`
+    ///
+    /// [`Myself`]: MaybeReceiver::Myself
+    /// [`P2P`]: MaybeReceiver::P2P
+    #[must_use]
+    pub fn is_myself_check(&self, my_user_id: UserID) -> bool {
+        match self {
+            Self::Myself => true,
+            Self::P2P(id) if (*id == my_user_id) => true,
+            _ => false,
+        }
+    }
+
+    /// Returns `true` if the maybe receiver is [`Unknown`].
+    ///
+    /// [`Unknown`]: MaybeReceiver::Unknown
+    #[must_use]
+    pub fn is_unknown(&self) -> bool {
+        matches!(self, Self::Unknown)
+    }
+
+    /// Returns the receiver as [`UserID`] if it is knwon.
+    #[must_use]
+    pub fn as_user_id(&self) -> Option<UserID> {
+        match self {
+            Self::Broadcast => None,
+            Self::P2P(id) => Some(*id),
+            Self::Myself => None,
+            Self::Unknown => None,
+        }
+    }
+}
+
+/// A Simple trait to extract the sender and the receiver from a message
+pub trait MaybeSenderReceiver {
+    fn maybe_sender(&self) -> MaybeSender;
+    fn maybe_receiver(&self) -> MaybeReceiver;
+}
+
+impl MaybeSenderReceiver for PublicKeyGossipMessage {
+    fn maybe_sender(&self) -> MaybeSender {
+        MaybeSender::SomeoneElse(self.from)
+    }
+    fn maybe_receiver(&self) -> MaybeReceiver {
+        match self.to {
+            Some(id) => MaybeReceiver::P2P(id),
+            None => MaybeReceiver::Broadcast,
+        }
+    }
+}
+
+impl MaybeSenderReceiver for VotingMessage {
+    fn maybe_sender(&self) -> MaybeSender {
+        MaybeSender::SomeoneElse(self.from)
+    }
+    fn maybe_receiver(&self) -> MaybeReceiver {
+        match self.to {
+            Some(id) => MaybeReceiver::P2P(id),
+            None => MaybeReceiver::Broadcast,
+        }
+    }
+}
+
+impl<M> MaybeSenderReceiver for Outgoing<M> {
+    fn maybe_sender(&self) -> MaybeSender {
+        MaybeSender::Myself
+    }
+
+    fn maybe_receiver(&self) -> MaybeReceiver {
+        match self.recipient {
+            MessageDestination::AllParties => MaybeReceiver::Broadcast,
+            MessageDestination::OneParty(i) => MaybeReceiver::P2P(i as UserID),
+        }
+    }
+}
+
+impl<M> MaybeSenderReceiver for Incoming<M> {
+    fn maybe_sender(&self) -> MaybeSender {
+        MaybeSender::SomeoneElse(self.sender as UserID)
+    }
+
+    fn maybe_receiver(&self) -> MaybeReceiver {
+        match self.msg_type {
+            MessageType::Broadcast => MaybeReceiver::Broadcast,
+            MessageType::P2P => MaybeReceiver::Myself,
+        }
+    }
+}
+
+impl MaybeSenderReceiver for () {
+    fn maybe_sender(&self) -> MaybeSender {
+        MaybeSender::Unknown
+    }
+
+    fn maybe_receiver(&self) -> MaybeReceiver {
+        MaybeReceiver::Unknown
+    }
+}
+
+pub(crate) fn create_job_manager_to_async_protocol_channel_split<
+    N: Network + 'static,
+    C2: Serialize + DeserializeOwned + MaybeSenderReceiver + Send + 'static,
+    M: Serialize + DeserializeOwned + Send + 'static,
+>(
+    mut rx_gadget: CloneableUnboundedReceiver<GadgetProtocolMessage>,
+    associated_block_id: <WebbWorkManager as WorkManagerInterface>::Clock,
+    associated_retry_id: <WebbWorkManager as WorkManagerInterface>::RetryID,
+    associated_session_id: <WebbWorkManager as WorkManagerInterface>::SessionID,
+    associated_task_id: <WebbWorkManager as WorkManagerInterface>::TaskID,
+    user_id_mapping: Arc<HashMap<UserID, AccountId>>,
+    my_account_id: AccountId,
+    network: N,
+) -> (
+    futures::channel::mpsc::UnboundedSender<Outgoing<M>>,
+    futures::channel::mpsc::UnboundedReceiver<
+        Result<Incoming<M>, futures::channel::mpsc::TryRecvError>,
+    >,
+    futures::channel::mpsc::UnboundedSender<C2>,
+    futures::channel::mpsc::UnboundedReceiver<C2>,
+) {
+    let (tx_to_async_proto_1, rx_for_async_proto_1) = futures::channel::mpsc::unbounded();
+    let (tx_to_async_proto_2, rx_for_async_proto_2) = futures::channel::mpsc::unbounded();
+
+    // Take the messages from the gadget and send them to the async protocol
+    tokio::task::spawn(async move {
+        let mut id = 0;
+        while let Some(msg_orig) = rx_gadget.next().await {
+            if msg_orig.payload.is_empty() {
+                log::warn!(target: "gadget", "Received empty message from Peer {}", msg_orig.from);
+                continue;
+            }
+            match bincode2::deserialize::<SplitChannelMessage<M, C2>>(&msg_orig.payload) {
+                Ok(msg) => match msg {
+                    SplitChannelMessage::Channel1(msg) => {
+                        let msg_type = if msg_orig.to.is_some() {
+                            MessageType::P2P
+                        } else {
+                            MessageType::Broadcast
+                        };
+                        let incoming = Incoming {
+                            id,
+                            sender: msg_orig.from as PartyIndex,
+                            msg_type,
+                            msg,
+                        };
+
+                        if tx_to_async_proto_1.unbounded_send(Ok(incoming)).is_err() {
+                            log::error!(target: "gadget", "Failed to send Incoming message to protocol");
+                        }
+
+                        id += 1;
+                    }
+                    SplitChannelMessage::Channel2(msg) => {
+                        if tx_to_async_proto_2.unbounded_send(msg).is_err() {
+                            log::error!(target: "gadget", "Failed to send C2 message to protocol");
+                        }
+                    }
+                },
+                Err(err) => {
+                    log::error!(target: "gadget", "Failed to deserialize message: {err:?}");
+                }
+            }
+        }
+    });
+
+    let (tx_to_outbound_1, mut rx_to_outbound_1) =
+        futures::channel::mpsc::unbounded::<Outgoing<M>>();
+    let (tx_to_outbound_2, mut rx_to_outbound_2) = futures::channel::mpsc::unbounded::<C2>();
+    let network_clone = network.clone();
+    let user_id_mapping_clone = user_id_mapping.clone();
+    let my_user_id = user_id_mapping
+        .iter()
+        .find_map(|(user_id, account_id)| {
+            if *account_id == my_account_id {
+                Some(*user_id)
+            } else {
+                None
+            }
+        })
+        .expect("Failed to find my user id");
+    // Take the messages from the async protocol and send them to the gadget
+    tokio::task::spawn(async move {
+        let offline_task = async move {
+            while let Some(msg) = rx_to_outbound_1.next().await {
+                let from = msg.maybe_sender();
+                let to = msg.maybe_receiver();
+                let (to_account_id, from_account_id) = get_to_and_from_account_id(
+                    &user_id_mapping_clone,
+                    from.as_user_id().unwrap_or(my_user_id),
+                    to.as_user_id(),
+                );
+                let msg = SplitChannelMessage::<M, C2>::Channel1(msg.msg);
+                let msg = GadgetProtocolMessage {
+                    associated_block_id,
+                    associated_session_id,
+                    associated_retry_id,
+                    task_hash: associated_task_id,
+                    from: from.as_user_id().unwrap_or(my_user_id),
+                    to: to.as_user_id(),
+                    payload: bincode2::serialize(&msg).expect("Failed to serialize message"),
+                    from_network_id: from_account_id,
+                    to_network_id: to_account_id,
+                };
+
+                if let Err(err) = network.send_message(msg).await {
+                    log::error!(target:"gadget", "Failed to send message to outbound: {err:?}");
+                }
+            }
+        };
+
+        let voting_task = async move {
+            while let Some(msg) = rx_to_outbound_2.next().await {
+                let from = msg.maybe_sender();
+                let to = msg.maybe_receiver();
+                let (to_account_id, from_account_id) = get_to_and_from_account_id(
+                    &user_id_mapping,
+                    from.as_user_id().unwrap_or(my_user_id),
+                    to.as_user_id(),
+                );
+                let msg = SplitChannelMessage::<M, C2>::Channel2(msg);
+                let msg = GadgetProtocolMessage {
+                    associated_block_id,
+                    associated_session_id,
+                    associated_retry_id,
+                    task_hash: associated_task_id,
+                    from: from.as_user_id().unwrap_or(my_user_id),
+                    to: to.as_user_id(),
+                    payload: bincode2::serialize(&msg).expect("Failed to serialize message"),
+                    from_network_id: from_account_id,
+                    to_network_id: to_account_id,
+                };
+
+                if let Err(err) = network_clone.send_message(msg).await {
+                    log::error!(target:"gadget", "Failed to send message to outbound: {err:?}");
+                }
+            }
+        };
+
+        tokio::join!(offline_task, voting_task);
+    });
+
+    (
+        tx_to_outbound_1,
+        rx_for_async_proto_1,
+        tx_to_outbound_2,
+        rx_for_async_proto_2,
+    )
+}
+
+fn get_to_and_from_account_id(
+    mapping: &HashMap<UserID, AccountId>,
+    from: UserID,
+    to: Option<UserID>,
+) -> (Option<AccountId>, Option<AccountId>) {
+    let from_account_id = mapping.get(&from).cloned();
+    let to_account_id = if let Some(to) = to {
+        mapping.get(&to).cloned()
+    } else {
+        None
+    };
+
+    (to_account_id, from_account_id)
+}
diff --git a/protocols/dfns-cggmp21/src/util.rs b/protocols/dfns-cggmp21/src/util.rs
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/protocols/dfns-cggmp21/src/util.rs
@@ -0,0 +1,1 @@
+
diff --git a/protocols/mp-ecdsa/Cargo.toml b/protocols/mp-ecdsa/Cargo.toml
index 29b425c41..cd2b87794 100644
--- a/protocols/mp-ecdsa/Cargo.toml
+++ b/protocols/mp-ecdsa/Cargo.toml
@@ -24,6 +24,7 @@ pallet-jobs = { workspace = true, features = ["std"] }
 tangle-primitives = { workspace = true, features = ["std"] }
 
 sp-core = { workspace = true, features = ["std"] }
+sp-io = { workspace = true, features = ["std"] }
 sp-api = { workspace = true, features = ["std"] }
 sp-runtime = { workspace = true, features = ["std"] }
 sp-application-crypto = { workspace = true, features = ["std"] }
@@ -36,4 +37,4 @@ parity-scale-codec = { workspace = true }
 serde = { version = "1.0.193", features = ["derive"] }
 
 [dev-dependencies]
-test-utils = { workspace = true }
\ No newline at end of file
+test-utils = { workspace = true }
diff --git a/protocols/mp-ecdsa/src/constants.rs b/protocols/mp-ecdsa/src/constants.rs
index ae2afe7eb..5c9dbaf42 100644
--- a/protocols/mp-ecdsa/src/constants.rs
+++ b/protocols/mp-ecdsa/src/constants.rs
@@ -1,8 +1,6 @@
-// Constants for dkg-gadget
-
 // ================= Common ======================== //
-pub const DKG_KEYGEN_PROTOCOL_NAME: &str = "/webb-tools/dkg/keygen/1";
-pub const DKG_SIGNING_PROTOCOL_NAME: &str = "/webb-tools/dkg/signing/1";
+pub const MP_ECDSA_KEYGEN_PROTOCOL_NAME: &str = "/tangle/mp-ecdsa/keygen/1";
+pub const MP_ECDSA_SIGNING_PROTOCOL_NAME: &str = "/tangle/mp-ecdsa/signing/1";
 
 // ============= Signing Protocol ======================= //
 
diff --git a/protocols/mp-ecdsa/src/protocols/keygen.rs b/protocols/mp-ecdsa/src/protocols/keygen.rs
index 7468cd821..41603d0d9 100644
--- a/protocols/mp-ecdsa/src/protocols/keygen.rs
+++ b/protocols/mp-ecdsa/src/protocols/keygen.rs
@@ -3,7 +3,6 @@ use crate::protocols::util;
 use crate::protocols::util::PublicKeyGossipMessage;
 use async_trait::async_trait;
 use curv::elliptic::curves::Secp256k1;
-use frame_support::Hashable;
 use gadget_common::client::{AccountId, ClientWithApi, JobsClient};
 use gadget_common::debug_logger::DebugLogger;
 use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
@@ -22,6 +21,8 @@ use round_based::async_runtime::watcher::StderrWatcher;
 use round_based::{Msg, StateMachine};
 use sc_client_api::Backend;
 use sp_api::ProvideRuntimeApi;
+use sp_application_crypto::sp_core::keccak_256;
+use sp_core::{ecdsa, Pair};
 use std::collections::{BTreeMap, HashMap};
 use std::sync::Arc;
 use tangle_primitives::jobs::{
@@ -202,11 +203,11 @@ where
         additional_params: Self::AdditionalParams,
     ) -> Result<BuiltExecutableJobWrapper, JobError> {
         let key_store = self.key_store.clone();
+        let key_store2 = self.key_store.clone();
         let blame = self.round_blames.clone();
         let protocol_output = Arc::new(tokio::sync::Mutex::new(None));
         let protocol_output_clone = protocol_output.clone();
         let client = self.client.clone();
-        let id = self.account_id;
         let logger = self.logger.clone();
         let logger_clone = logger.clone();
         let round_blames = self.round_blames.clone();
@@ -272,11 +273,11 @@ where
                 logger.debug("Finished AsyncProtocol - Keygen");
 
                 let job_result = handle_public_key_gossip(
+                    key_store,
                     &logger,
                     &local_key,
                     t,
                     i,
-                    id,
                     broadcast_tx_to_outbound,
                     broadcast_rx_from_gadget,
                 )
@@ -300,7 +301,7 @@ where
 
                 // Store the keys locally, as well as submitting them to the blockchain
                 if let Some((local_key, job_result)) = protocol_output_clone.lock().await.take() {
-                    key_store
+                    key_store2
                         .set(additional_params.job_id, local_key)
                         .await
                         .map_err(|err| JobError {
@@ -325,19 +326,20 @@ where
     }
 }
 
-async fn handle_public_key_gossip(
+#[allow(clippy::too_many_arguments)]
+async fn handle_public_key_gossip<KBE: KeystoreBackend>(
+    key_store: ECDSAKeyStore<KBE>,
     logger: &DebugLogger,
     local_key: &LocalKey<Secp256k1>,
-    threshold: u16,
+    t: u16,
     i: u16,
-    my_id: AccountId,
     broadcast_tx_to_outbound: UnboundedSender<PublicKeyGossipMessage>,
     mut broadcast_rx_from_gadget: UnboundedReceiver<PublicKeyGossipMessage>,
 ) -> Result<JobResult, JobError> {
     let serialized_public_key = local_key.public_key().to_bytes(true).to_vec();
-    // Note: the signature is also empty in the DKG implementation
-    let signature = vec![];
-
+    let key_hashed = keccak_256(&serialized_public_key);
+    let signature = key_store.pair().sign_prehashed(&key_hashed).0.to_vec();
+    let my_id = key_store.pair().public();
     let mut received_keys = BTreeMap::new();
     received_keys.insert(i, signature.clone());
     let mut received_participants = BTreeMap::new();
@@ -354,7 +356,7 @@ async fn handle_public_key_gossip(
             reason: format!("Failed to send public key: {err:?}"),
         })?;
 
-    for idx in 0..threshold {
+    for _ in 0..t {
         let message = broadcast_rx_from_gadget
             .recv()
             .await
@@ -363,18 +365,37 @@ async fn handle_public_key_gossip(
             })?;
 
         let from = message.from;
-        logger.debug(format!(
-            "Received public key from {from} | {}/{threshold} received",
-            idx + 1
-        ));
+        logger.debug(format!("Received public key from {from}"));
 
         if received_keys.contains_key(&(from as u16)) {
             logger.warn("Received duplicate key");
             continue;
         }
+        // verify signature
+        let maybe_signature = sp_core::ecdsa::Signature::from_slice(&message.signature);
+        match maybe_signature.and_then(|s| s.recover_prehashed(&key_hashed)) {
+            Some(p) if p != message.id => {
+                logger.warn(format!(
+                    "Received invalid signature from {from} not signed by them"
+                ));
+            }
+            Some(p) if p == message.id => {
+                logger.debug(format!("Received valid signature from {from}"));
+            }
+            Some(_) => unreachable!("Should not happen"),
+            None => {
+                logger.warn(format!("Received invalid signature from {from}"));
+                continue;
+            }
+        }
 
         received_keys.insert(from as u16, message.signature);
         received_participants.insert(from as u16, message.id);
+        logger.debug(format!(
+            "Received {}/{} signatures",
+            received_keys.len(),
+            t + 1
+        ));
     }
 
     // Order and collect the map to ensure symmetric submission to blockchain
@@ -382,19 +403,129 @@ async fn handle_public_key_gossip(
         .into_iter()
         .sorted_by_key(|x| x.0)
         .map(|r| r.1)
-        .collect();
+        .collect::<Vec<_>>();
 
     let participants = received_participants
         .into_iter()
         .sorted_by_key(|x| x.0)
-        .map(|r| r.1.identity())
+        .map(|r| r.1 .0.to_vec())
         .collect();
 
-    Ok(JobResult::DKGPhaseOne(DKGTSSKeySubmissionResult {
+    if signatures.len() < t as usize {
+        return Err(JobError {
+            reason: format!(
+                "Received {} signatures, expected at least {}",
+                signatures.len(),
+                t + 1,
+            ),
+        });
+    }
+
+    let res = DKGTSSKeySubmissionResult {
         signature_type: DigitalSignatureType::Ecdsa,
         key: serialized_public_key,
         participants,
         signatures,
-        threshold: threshold as _,
-    }))
+        threshold: t as _,
+    };
+    verify_generated_dkg_key_ecdsa(res.clone(), logger);
+    Ok(JobResult::DKGPhaseOne(res))
+}
+
+fn verify_generated_dkg_key_ecdsa(data: DKGTSSKeySubmissionResult, logger: &DebugLogger) {
+    // Ensure participants and signatures are not empty
+    assert!(!data.participants.is_empty(), "NoParticipantsFound",);
+    assert!(!data.signatures.is_empty(), "NoSignaturesFound");
+
+    // Generate the required ECDSA signers
+    let maybe_signers = data
+        .participants
+        .iter()
+        .map(|x| {
+            ecdsa::Public(
+                to_slice_33(x)
+                    .unwrap_or_else(|| panic!("Failed to convert input to ecdsa public key")),
+            )
+        })
+        .collect::<Vec<ecdsa::Public>>();
+
+    assert!(!maybe_signers.is_empty(), "NoParticipantsFound");
+
+    let mut known_signers: Vec<ecdsa::Public> = Default::default();
+
+    for signature in data.signatures {
+        // Ensure the required signer signature exists
+        let (maybe_authority, success) =
+            verify_signer_from_set_ecdsa(maybe_signers.clone(), &data.key, &signature);
+
+        if success {
+            let authority = maybe_authority.expect("CannotRetreiveSigner");
+
+            // Ensure no duplicate signatures
+            assert!(!known_signers.contains(&authority), "DuplicateSignature");
+
+            logger.debug(format!("Verified signature from {}", authority));
+            known_signers.push(authority);
+        }
+    }
+
+    // Ensure a sufficient number of unique signers are present
+    assert!(
+        known_signers.len() > data.threshold as usize,
+        "NotEnoughSigners"
+    );
+    logger.debug(format!(
+        "Verified {}/{} signatures",
+        known_signers.len(),
+        data.threshold + 1
+    ));
+}
+
+pub fn verify_signer_from_set_ecdsa(
+    maybe_signers: Vec<ecdsa::Public>,
+    msg: &[u8],
+    signature: &[u8],
+) -> (Option<ecdsa::Public>, bool) {
+    let mut signer = None;
+    let res = maybe_signers.iter().any(|x| {
+        if let Some(data) = recover_ecdsa_pub_key(msg, signature) {
+            let recovered = &data[..32];
+            if x.0[1..].to_vec() == recovered.to_vec() {
+                signer = Some(*x);
+                true
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    });
+
+    (signer, res)
+}
+
+pub fn recover_ecdsa_pub_key(data: &[u8], signature: &[u8]) -> Option<Vec<u8>> {
+    const SIGNATURE_LENGTH: usize = 65;
+    if signature.len() != SIGNATURE_LENGTH {
+        return None;
+    }
+    let mut sig = [0u8; SIGNATURE_LENGTH];
+    sig[..SIGNATURE_LENGTH].copy_from_slice(signature);
+
+    let hash = keccak_256(data);
+
+    sp_io::crypto::secp256k1_ecdsa_recover(&sig, &hash)
+        .ok()
+        .map(|x| x.to_vec())
+}
+
+pub fn to_slice_33(val: &[u8]) -> Option<[u8; 33]> {
+    const ECDSA_KEY_LENGTH: usize = 33;
+    if val.len() == ECDSA_KEY_LENGTH {
+        let mut key = [0u8; ECDSA_KEY_LENGTH];
+        key[..ECDSA_KEY_LENGTH].copy_from_slice(val);
+
+        return Some(key);
+    }
+    None
 }
diff --git a/protocols/mp-ecdsa/src/protocols/sign.rs b/protocols/mp-ecdsa/src/protocols/sign.rs
index f6bd3e0bb..5956d0877 100644
--- a/protocols/mp-ecdsa/src/protocols/sign.rs
+++ b/protocols/mp-ecdsa/src/protocols/sign.rs
@@ -22,7 +22,6 @@ use multi_party_ecdsa::gg_2020::state_machine::sign::{
     CompletedOfflineStage, OfflineStage, PartialSignature, SignManual,
 };
 use pallet_jobs_rpc_runtime_api::JobsApi;
-use parity_scale_codec::Encode;
 use round_based::async_runtime::watcher::StderrWatcher;
 use round_based::{Msg, StateMachine};
 use sc_client_api::Backend;
@@ -324,7 +323,7 @@ where
 
                 // Submit the protocol output to the blockchain
                 if let Some(signature) = protocol_output_clone.lock().await.take() {
-                    let signature: Vec<u8> = signature.encode();
+                    let signature: Vec<u8> = signature.0.to_vec();
 
                     let job_result = JobResult::DKGPhaseTwo(DKGTSSSignatureResult {
                         signature_type: DigitalSignatureType::Ecdsa,
@@ -424,7 +423,5 @@ async fn voting_stage(
     })?;
 
     // Convert the signature to a substrate-compatible format
-    crate::util::convert_signature(debug_logger, &signature).ok_or_else(|| JobError {
-        reason: "Failed to convert signature to Substrate-compatible format".to_string(),
-    })
+    Ok(crate::util::convert_signature(&signature))
 }
diff --git a/protocols/mp-ecdsa/src/util.rs b/protocols/mp-ecdsa/src/util.rs
index ab8d32abb..c53d5186e 100644
--- a/protocols/mp-ecdsa/src/util.rs
+++ b/protocols/mp-ecdsa/src/util.rs
@@ -1,40 +1,16 @@
 use curv::arithmetic::Converter;
-use gadget_common::debug_logger::DebugLogger;
 use multi_party_ecdsa::gg_2020::party_i::SignatureRecid;
 use sp_core::ecdsa::Signature;
 
-pub fn convert_signature(
-    debug_logger: &DebugLogger,
-    sig_recid: &SignatureRecid,
-) -> Option<Signature> {
+pub fn convert_signature(sig_recid: &SignatureRecid) -> Signature {
     let r = sig_recid.r.to_bigint().to_bytes();
     let s = sig_recid.s.to_bigint().to_bytes();
     let v = sig_recid.recid + 27u8;
-
-    let mut sig_vec: Vec<u8> = Vec::new();
-
-    for _ in 0..(32 - r.len()) {
-        sig_vec.extend([0]);
-    }
-    sig_vec.extend_from_slice(&r);
-
-    for _ in 0..(32 - s.len()) {
-        sig_vec.extend([0]);
-    }
-    sig_vec.extend_from_slice(&s);
-
-    sig_vec.extend([v]);
-
-    if 65 != sig_vec.len() {
-        debug_logger.warn(format!(
-            "Invalid signature len: {}, expected 65",
-            sig_vec.len()
-        ));
-        return None;
-    }
-
     let mut dkg_sig_arr: [u8; 65] = [0; 65];
-    dkg_sig_arr.copy_from_slice(&sig_vec[0..65]);
 
-    Some(Signature(dkg_sig_arr))
+    dkg_sig_arr[0..32].copy_from_slice(&r);
+    dkg_sig_arr[32..64].copy_from_slice(&s);
+    dkg_sig_arr[64] = v;
+
+    Signature(dkg_sig_arr)
 }


Test patch:
diff --git a/protocols/dfns-cggmp21/tests/ecdsa.rs b/protocols/dfns-cggmp21/tests/ecdsa.rs
new file mode 100644
index 000000000..257d9aa87
--- /dev/null
+++ b/protocols/dfns-cggmp21/tests/ecdsa.rs
@@ -0,0 +1,164 @@
+#[cfg(test)]
+mod tests {
+    use futures::stream::FuturesUnordered;
+    use futures::StreamExt;
+    use tangle_primitives::jobs::{
+        DKGTSSPhaseOneJobType, DKGTSSPhaseTwoJobType, JobId, JobSubmission, JobType,
+    };
+    use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+    use test_utils::mock::{id_to_public, Jobs, MockBackend, RuntimeOrigin};
+    use test_utils::sync::substrate_test_channel::MultiThreadedTestExternalities;
+
+    #[tokio::test(flavor = "multi_thread")]
+    async fn test_externalities_gadget_starts() {
+        test_utils::setup_log();
+        new_test_ext::<1>()
+            .await
+            .execute_with_async(|| {
+                assert_eq!(1, 1);
+            })
+            .await
+    }
+
+    #[tokio::test(flavor = "multi_thread")]
+    async fn test_externalities_keygen() {
+        test_utils::setup_log();
+        const N: usize = 3;
+        const T: usize = N - 1;
+
+        let ext = new_test_ext::<N>().await;
+        assert_eq!(wait_for_keygen::<N, T>(&ext).await, 0);
+    }
+
+    #[tokio::test(flavor = "multi_thread")]
+    async fn test_externalities_signing() {
+        test_utils::setup_log();
+        const N: usize = 3;
+        const T: usize = N - 1;
+
+        let ext = new_test_ext::<N>().await;
+        let keygen_job_id = wait_for_keygen::<N, T>(&ext).await;
+        assert_eq!(wait_for_signing::<N>(&ext, keygen_job_id).await, 1);
+    }
+
+    #[tokio::test(flavor = "multi_thread")]
+    async fn test_externalities_parallel_jobs() {
+        test_utils::setup_log();
+        const N: usize = 3;
+        const T: usize = N - 1;
+        const TEST_COUNT: usize = 2;
+
+        let ext = new_test_ext::<N>().await;
+        let futures = FuturesUnordered::new();
+
+        for _ in 0..TEST_COUNT {
+            let ext = ext.clone();
+            futures.push(Box::pin(async move {
+                let keygen_job_id = wait_for_keygen::<N, T>(&ext).await;
+                wait_for_signing::<N>(&ext, keygen_job_id).await;
+            }));
+        }
+
+        futures.collect::<()>().await;
+    }
+
+    async fn wait_for_keygen<const N: usize, const T: usize>(
+        ext: &MultiThreadedTestExternalities,
+    ) -> JobId {
+        let job_id = ext
+            .execute_with_async(|| {
+                let job_id = Jobs::next_job_id();
+                let identities = (0..N).map(|i| id_to_public(i as u8)).collect::<Vec<_>>();
+
+                let submission = JobSubmission {
+                    expiry: 100,
+                    ttl: 100,
+                    job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType {
+                        participants: identities.clone(),
+                        threshold: T as _,
+                        permitted_caller: None,
+                        role_type: ThresholdSignatureRoleType::TssCGGMP,
+                    }),
+                };
+
+                assert!(Jobs::submit_job(RuntimeOrigin::signed(identities[0]), submission).is_ok());
+
+                log::info!(target: "gadget", "******* Submitted Keygen Job {job_id}");
+                job_id
+            })
+            .await;
+
+        test_utils::wait_for_job_completion(
+            ext,
+            RoleType::Tss(ThresholdSignatureRoleType::TssCGGMP),
+            job_id,
+        )
+        .await;
+        job_id
+    }
+
+    async fn wait_for_signing<const N: usize>(
+        ext: &MultiThreadedTestExternalities,
+        keygen_job_id: JobId,
+    ) -> JobId {
+        let job_id = ext
+            .execute_with_async(move || {
+                let submission = Vec::from("Hello, world!");
+                let job_id = Jobs::next_job_id();
+                let identities = (0..N).map(|i| id_to_public(i as u8)).collect::<Vec<_>>();
+                let submission = JobSubmission {
+                    expiry: 100,
+                    ttl: 100,
+                    job_type: JobType::DKGTSSPhaseTwo(DKGTSSPhaseTwoJobType {
+                        phase_one_id: keygen_job_id,
+                        submission,
+                        role_type: ThresholdSignatureRoleType::TssCGGMP,
+                    }),
+                };
+
+                assert!(Jobs::submit_job(RuntimeOrigin::signed(identities[0]), submission).is_ok());
+
+                log::info!(target: "gadget", "******* Submitted Signing Job {job_id}");
+                job_id
+            })
+            .await;
+
+        test_utils::wait_for_job_completion(
+            ext,
+            RoleType::Tss(ThresholdSignatureRoleType::TssCGGMP),
+            job_id,
+        )
+        .await;
+        job_id
+    }
+
+    async fn new_test_ext<const N: usize>() -> MultiThreadedTestExternalities {
+        test_utils::mock::new_test_ext::<N, 2, _, _, _>((), |_, mut node_input| async move {
+            let keygen_client = node_input.mock_clients.pop().expect("No keygen client");
+            let signing_client = node_input.mock_clients.pop().expect("No signing client");
+
+            let keygen_network = node_input.mock_networks.pop().expect("No keygen network");
+            let signing_network = node_input.mock_networks.pop().expect("No signing network");
+            let account_id = node_input.account_id;
+
+            let logger = node_input.logger.clone();
+            let (pallet_tx, keystore) = (node_input.pallet_tx, node_input.keystore);
+            logger.info("Starting gadget");
+            if let Err(err) = dfns_cggmp21_protocol::run::<_, MockBackend, _, _, _, _, _>(
+                account_id,
+                keygen_client,
+                signing_client,
+                logger.clone(),
+                keystore,
+                keygen_network,
+                signing_network,
+                pallet_tx,
+            )
+            .await
+            {
+                log::error!(target: "gadget", "Error running gadget: {err:?}");
+            }
+        })
+        .await
+    }
+}
diff --git a/protos/zk-gadget-test/test.proto b/protos/zk-gadget-test/test.proto
deleted file mode 100644
index 8ad65f68b..000000000
--- a/protos/zk-gadget-test/test.proto
+++ /dev/null
@@ -1,12 +0,0 @@
-syntax = "proto3";
-package test_blockchain_zk;
-
-message GetLatestHeaderRequest {}
-message GetLatestHeaderResponse {
-  uint64 latest_block_number = 1;
-  uint64 session_id = 2;
-}
-
-service Auth {
-  rpc GetLatestHeader(GetLatestHeaderRequest) returns (GetLatestHeaderResponse) {}
-}
\ No newline at end of file
diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml
index af362a84d..308a4b91b 100644
--- a/test-utils/Cargo.toml
+++ b/test-utils/Cargo.toml
@@ -28,8 +28,10 @@ sp-application-crypto = { workspace = true }
 pallet-balances = { workspace = true }
 pallet-timestamp = { workspace = true }
 pallet-jobs = { workspace = true }
+pallet-dkg = { workspace = true }
+pallet-zksaas = { workspace = true }
 sp-externalities = { workspace = true }
-tangle-primitives = { workspace = true }
+tangle-primitives = { workspace = true, features = ["verifying"] }
 scale-info = { workspace = true }
 parity-scale-codec = { workspace = true }
-tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
\ No newline at end of file
+tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
diff --git a/test-utils/src/mock.rs b/test-utils/src/mock.rs
index a3f1fde38..9fafcf288 100644
--- a/test-utils/src/mock.rs
+++ b/test-utils/src/mock.rs
@@ -46,16 +46,18 @@ use gadget_common::locks::TokioMutexExt;
 use gadget_common::Error;
 use gadget_core::job_manager::WorkManagerInterface;
 use sp_core::ecdsa;
-use sp_io::crypto::ecdsa_generate;
 use sp_keystore::{testing::MemoryKeystore, KeystoreExt, KeystorePtr};
 use sp_std::sync::Arc;
 use tangle_primitives::jobs::traits::{JobToFee, MPCHandler};
 use tangle_primitives::jobs::{
-    JobId, JobSubmission, JobType, JobWithResult, PhaseResult, ReportValidatorOffence,
+    JobId, JobResult, JobSubmission, JobType, JobWithResult, PhaseResult, ReportValidatorOffence,
     RpcResponseJobsData, ValidatorOffenceType,
 };
 use tangle_primitives::roles::traits::RolesHandler;
 use tangle_primitives::roles::RoleType;
+use tangle_primitives::verifier::{
+    arkworks::ArkworksVerifierGroth16Bn254, circom::CircomVerifierGroth16Bn254,
+};
 use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
 
 /// Key type for DKG keys
@@ -112,46 +114,17 @@ impl pallet_timestamp::Config for Runtime {
     type WeightInfo = ();
 }
 
-pub struct MockDKGPallet;
-
-impl MockDKGPallet {
-    fn job_to_fee(job: &JobSubmission<AccountId, BlockNumber>) -> Balance {
-        if job.job_type.is_phase_one() {
-            job.job_type
-                .clone()
-                .get_participants()
-                .unwrap()
-                .len()
-                .try_into()
-                .unwrap()
-        } else {
-            20
-        }
-    }
-}
-
-pub struct MockZkSaasPallet;
-impl MockZkSaasPallet {
-    fn job_to_fee(job: &JobSubmission<AccountId, BlockNumber>) -> Balance {
-        if job.job_type.is_phase_one() {
-            10
-        } else {
-            20
-        }
-    }
-}
-
-pub struct MockJobToFeeHandler;
+pub struct JobToFeeHandler;
 
-impl JobToFee<AccountId, BlockNumber> for MockJobToFeeHandler {
+impl JobToFee<AccountId, BlockNumber> for JobToFeeHandler {
     type Balance = Balance;
 
     fn job_to_fee(job: &JobSubmission<AccountId, BlockNumber>) -> Balance {
         match job.job_type {
-            JobType::DKGTSSPhaseOne(_) => MockDKGPallet::job_to_fee(job),
-            JobType::DKGTSSPhaseTwo(_) => MockDKGPallet::job_to_fee(job),
-            JobType::ZkSaaSPhaseOne(_) => MockZkSaasPallet::job_to_fee(job),
-            JobType::ZkSaaSPhaseTwo(_) => MockZkSaasPallet::job_to_fee(job),
+            JobType::DKGTSSPhaseOne(_) => Dkg::job_to_fee(job),
+            JobType::DKGTSSPhaseTwo(_) => Dkg::job_to_fee(job),
+            JobType::ZkSaaSPhaseOne(_) => ZkSaaS::job_to_fee(job),
+            JobType::ZkSaaSPhaseTwo(_) => ZkSaaS::job_to_fee(job),
         }
     }
 }
@@ -169,20 +142,27 @@ impl RolesHandler<AccountId> for MockRolesHandler {
     }
 
     fn get_validator_role_key(address: AccountId) -> Option<Vec<u8>> {
-        let validators = (0..8).map(id_to_public).collect::<Vec<_>>();
-        if validators.contains(&address) {
-            Some(mock_pub_key().to_raw_vec())
-        } else {
-            None
-        }
+        let validators = (0..8).map(id_to_pair).collect::<Vec<_>>();
+        validators.iter().find_map(|p| {
+            if p.public() == address {
+                Some(p.public().to_raw_vec())
+            } else {
+                None
+            }
+        })
     }
 }
 
 pub struct MockMPCHandler;
 
 impl MPCHandler<AccountId, BlockNumber, Balance> for MockMPCHandler {
-    fn verify(_data: JobWithResult<AccountId>) -> DispatchResult {
-        Ok(())
+    fn verify(data: JobWithResult<AccountId>) -> DispatchResult {
+        match data.result {
+            JobResult::DKGPhaseOne(_) => Dkg::verify(data.result),
+            JobResult::DKGPhaseTwo(_) => Dkg::verify(data.result),
+            JobResult::ZkSaaSPhaseOne(_) => ZkSaaS::verify(data),
+            JobResult::ZkSaaSPhaseTwo(_) => ZkSaaS::verify(data),
+        }
     }
 
     fn verify_validator_report(
@@ -205,7 +185,7 @@ parameter_types! {
 impl pallet_jobs::Config for Runtime {
     type RuntimeEvent = RuntimeEvent;
     type Currency = Balances;
-    type JobToFee = MockJobToFeeHandler;
+    type JobToFee = JobToFeeHandler;
     type RolesHandler = MockRolesHandler;
     type MPCHandler = MockMPCHandler;
     type ForceOrigin = EnsureSigned<AccountId>;
@@ -213,6 +193,22 @@ impl pallet_jobs::Config for Runtime {
     type WeightInfo = ();
 }
 
+impl pallet_dkg::Config for Runtime {
+    type RuntimeEvent = RuntimeEvent;
+    type Currency = Balances;
+    type UpdateOrigin = EnsureSigned<AccountId>;
+    type WeightInfo = ();
+}
+
+impl pallet_zksaas::Config for Runtime {
+    type RuntimeEvent = RuntimeEvent;
+    type Currency = Balances;
+
+    type UpdateOrigin = EnsureSigned<AccountId>;
+    type Verifier = (ArkworksVerifierGroth16Bn254, CircomVerifierGroth16Bn254);
+    type WeightInfo = ();
+}
+
 construct_runtime!(
     pub enum Runtime
     {
@@ -220,6 +216,8 @@ construct_runtime!(
         Timestamp: pallet_timestamp,
         Balances: pallet_balances,
         Jobs: pallet_jobs,
+        Dkg: pallet_dkg,
+        ZkSaaS: pallet_zksaas,
     }
 );
 
@@ -269,7 +267,7 @@ impl ProvideRuntimeApi<Block> for Runtime {
 }
 
 pub fn id_to_pair(id: u8) -> ecdsa::Pair {
-    ecdsa::Pair::from_string(&format!("//{id}//password"), None).unwrap()
+    ecdsa::Pair::from_string(&format!("//Alice///{id}"), None).expect("static values are valid")
 }
 
 pub fn id_to_public(id: u8) -> ecdsa::Public {
@@ -530,10 +528,6 @@ where
     ext
 }
 
-fn mock_pub_key() -> ecdsa::Public {
-    ecdsa_generate(KEY_TYPE, None)
-}
-
 pub mod mock_wrapper_client {
     use crate::mock::RuntimeOrigin;
     use crate::sync::substrate_test_channel::MultiThreadedTestExternalities;
@@ -671,9 +665,9 @@ pub mod mock_wrapper_client {
             self.ext
                 .execute_with_async(move || {
                     let origin = RuntimeOrigin::signed(id);
-                    if let Err(err) =
-                        crate::mock::Jobs::submit_job_result(origin, role_type, job_id, result)
-                    {
+                    let res =
+                        crate::mock::Jobs::submit_job_result(origin, role_type, job_id, result);
+                    if let Err(err) = res {
                         let err = format!("Pallet tx error: {err:?}");
                         if err.contains("JobNotFound") {
                             // Job has already been submitted (assumption only for tests)