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

Issue:
[TASK] Message Recording
Incoming messages should be temporarily stored before being delivered to protocols. That way, messages can be retrospected upon in the case a misbehavior report needs to be generated.

The ideal place to store these messages is inside the work manager via the `deliver_message` function.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml
index e6a6bf58e..b6fbf6237 100644
--- a/.github/workflows/validate_pr.yml
+++ b/.github/workflows/validate_pr.yml
@@ -105,4 +105,9 @@ jobs:
         run: cargo install cargo-nextest --locked
 
       - name: tests
+        if: ${{ matrix.package != 'zk-saas-protocol' }}
         run: cargo nextest run --release --nocapture --package ${{ matrix.package }}
+
+      - name: tests (parallel enabled)
+        if: ${{ matrix.package == 'zk-saas-protocol' }}
+        run: cargo nextest run --features=parallel --release --nocapture --package ${{ matrix.package }}
diff --git a/Cargo.lock b/Cargo.lock
index bd0040fce..4a5fa3644 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -115,6 +115,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
 [[package]]
 name = "android-tzdata"
 version = "0.1.1"
@@ -764,6 +770,15 @@ dependencies = [
  "pin-project-lite 0.2.13",
 ]
 
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "atomic"
 version = "0.5.3"
@@ -776,6 +791,16 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
+[[package]]
+name = "atomic-write-file"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
+dependencies = [
+ "nix 0.27.1",
+ "rand 0.8.5",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -954,6 +979,9 @@ name = "bitflags"
 version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "bitvec"
@@ -1658,6 +1686,21 @@ dependencies = [
  "wasmtime-types",
 ]
 
+[[package]]
+name = "crc"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
 [[package]]
 name = "crc32fast"
 version = "1.4.0"
@@ -2136,6 +2179,7 @@ dependencies = [
  "pallet-jobs",
  "pallet-jobs-rpc-runtime-api",
  "parity-scale-codec 3.6.9",
+ "parking_lot 0.12.1",
  "protocol-macros",
  "rand 0.8.5",
  "rand_chacha 0.3.1",
@@ -2286,6 +2330,12 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
 [[package]]
 name = "downcast"
 version = "0.11.0"
@@ -2424,6 +2474,9 @@ name = "either"
 version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "elliptic-curve"
@@ -2586,6 +2639,17 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "ethabi"
 version = "15.0.0"
@@ -2930,6 +2994,12 @@ dependencies = [
  "scale-info",
 ]
 
+[[package]]
+name = "finl_unicode"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
+
 [[package]]
 name = "fixed-hash"
 version = "0.7.0"
@@ -2980,6 +3050,17 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin 0.9.8",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -3320,6 +3401,17 @@ dependencies = [
  "num_cpus",
 ]
 
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot 0.12.1",
+]
+
 [[package]]
 name = "futures-io"
 version = "0.3.30"
@@ -3450,6 +3542,7 @@ dependencies = [
  "sp-api",
  "sp-core",
  "sp-runtime",
+ "sqlx",
  "subxt-signer",
  "tangle-primitives",
  "tokio",
@@ -3767,9 +3860,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
 dependencies = [
  "ahash 0.8.8",
+ "allocator-api2",
  "serde",
 ]
 
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.3",
+]
+
 [[package]]
 name = "heck"
 version = "0.3.3"
@@ -3784,6 +3887,9 @@ name = "heck"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+dependencies = [
+ "unicode-segmentation",
+]
 
 [[package]]
 name = "hermit-abi"
@@ -4399,6 +4505,9 @@ name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin 0.5.2",
+]
 
 [[package]]
 name = "leb128"
@@ -4865,6 +4974,17 @@ dependencies = [
  "libsecp256k1-core",
 ]
 
+[[package]]
+name = "libsqlite3-sys"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "libz-sys"
 version = "1.1.15"
@@ -5093,6 +5213,16 @@ dependencies = [
  "rawpointer",
 ]
 
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest 0.10.7",
+]
+
 [[package]]
 name = "md5"
 version = "0.7.0"
@@ -5447,6 +5577,17 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags 2.4.2",
+ "cfg-if",
+ "libc",
+]
+
 [[package]]
 name = "no-std-net"
 version = "0.6.0"
@@ -5539,6 +5680,23 @@ dependencies = [
  "rand 0.8.5",
 ]
 
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.5",
+ "smallvec",
+ "zeroize",
+]
+
 [[package]]
 name = "num-complex"
 version = "0.4.5"
@@ -6205,6 +6363,17 @@ dependencies = [
  "futures-io",
 ]
 
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der 0.7.8",
+ "pkcs8 0.10.2",
+ "spki 0.7.3",
+]
+
 [[package]]
 name = "pkcs8"
 version = "0.9.0"
@@ -7299,6 +7468,26 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest 0.10.7",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8 0.10.2",
+ "rand_core 0.6.4",
+ "signature 2.2.0",
+ "spki 0.7.3",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "rtnetlink"
 version = "0.10.1"
@@ -7309,7 +7498,7 @@ dependencies = [
  "log",
  "netlink-packet-route",
  "netlink-proto",
- "nix",
+ "nix 0.24.3",
  "thiserror",
  "tokio",
 ]
@@ -8205,6 +8394,17 @@ dependencies = [
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.7",
+]
+
 [[package]]
 name = "sha2"
 version = "0.8.2"
@@ -9138,6 +9338,9 @@ name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
 
 [[package]]
 name = "spki"
@@ -9159,6 +9362,216 @@ dependencies = [
  "der 0.7.8",
 ]
 
+[[package]]
+name = "sqlformat"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
+dependencies = [
+ "itertools 0.12.1",
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
+dependencies = [
+ "ahash 0.8.8",
+ "atoi",
+ "byteorder",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "dotenvy",
+ "either",
+ "event-listener 2.5.3",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "indexmap 2.2.3",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "rustls 0.21.10",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "sha2 0.10.8",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "webpki-roots 0.25.4",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
+dependencies = [
+ "atomic-write-file",
+ "dotenvy",
+ "either",
+ "heck 0.4.1",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2 0.10.8",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-sqlite",
+ "syn 1.0.109",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
+dependencies = [
+ "atoi",
+ "base64 0.21.7",
+ "bitflags 2.4.2",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest 0.10.7",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array 0.14.7",
+ "hex",
+ "hkdf",
+ "hmac 0.12.1",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand 0.8.5",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2 0.10.8",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
+dependencies = [
+ "atoi",
+ "base64 0.21.7",
+ "bitflags 2.4.2",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac 0.12.1",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand 0.8.5",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sha2 0.10.8",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "sqlx-core",
+ "tracing",
+ "url",
+ "urlencoding",
+]
+
 [[package]]
 name = "ss58-registry"
 version = "1.46.0"
@@ -9205,6 +9618,17 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "439558b73299d7afdf52c36399c3c812cca10fe5ca08429f38815df5bacf3ef7"
 
+[[package]]
+name = "stringprep"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6"
+dependencies = [
+ "finl_unicode",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
 [[package]]
 name = "strsim"
 version = "0.8.0"
@@ -10221,6 +10645,12 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
 
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
 [[package]]
 name = "universal-hash"
 version = "0.5.1"
@@ -10266,6 +10696,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
 [[package]]
 name = "uuid"
 version = "1.7.0"
@@ -11024,6 +11460,12 @@ dependencies = [
  "rustix 0.38.31",
 ]
 
+[[package]]
+name = "whoami"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
+
 [[package]]
 name = "wide"
 version = "0.7.15"
@@ -11504,6 +11946,7 @@ dependencies = [
  "dist-primitives",
  "fflonk",
  "frame-support",
+ "futures",
  "futures-util",
  "gadget-common",
  "gadget-core",
diff --git a/gadget-common/Cargo.toml b/gadget-common/Cargo.toml
index 121a17079..7f0f9a81f 100644
--- a/gadget-common/Cargo.toml
+++ b/gadget-common/Cargo.toml
@@ -37,6 +37,7 @@ anyhow = { workspace = true }
 futures = { workspace = true }
 round-based = { workspace = true }
 sp-api = { workspace = true }
+sqlx = { version = "0.7.3", features = ["runtime-tokio-rustls", "sqlite"] }
 
 frame-support = { workspace = true }
 sc-network = { workspace = true }
diff --git a/gadget-common/src/full_protocol.rs b/gadget-common/src/full_protocol.rs
index 136155a39..36a3bad22 100644
--- a/gadget-common/src/full_protocol.rs
+++ b/gadget-common/src/full_protocol.rs
@@ -11,6 +11,7 @@ use crate::Error;
 use async_trait::async_trait;
 use gadget_core::job::{BuiltExecutableJobWrapper, JobError};
 use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
+use parking_lot::Mutex;
 use sc_client_api::{Backend, BlockImportNotification};
 use sp_api::ProvideRuntimeApi;
 use sp_runtime::traits::Block;
@@ -19,6 +20,8 @@ use std::sync::Arc;
 use tangle_primitives::roles::RoleType;
 use tokio::sync::mpsc::UnboundedReceiver;
 
+pub type SharedOptional<T> = Arc<Mutex<Option<T>>>;
+
 #[async_trait]
 pub trait FullProtocolConfig: Clone + Send + Sync + Sized + 'static
 where
@@ -48,19 +51,15 @@ where
         protocol_message_rx: UnboundedReceiver<GadgetProtocolMessage>,
         additional_params: Self::AsyncProtocolParameters,
     ) -> Result<BuiltExecutableJobWrapper, JobError>;
-    async fn initialize_network_and_protocol(
-        &self,
-        jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
-    ) -> Result<(), Error>;
-    async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage>;
-
-    async fn send_message(
-        &self,
-        message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
-    ) -> Result<(), Error>;
+    fn network(&self) -> &Self::Network;
+    /// Given an input of metadata for a job, return a set of parameters that can be used to
+    /// construct a protocol.
+    ///
+    /// The provided work manager should only be used for querying recorded_messages
     async fn create_next_job(
         &self,
         job: JobInitMetadata<Self::Block>,
+        work_manager: &ProtocolWorkManager<WorkManager>,
     ) -> Result<Self::AsyncProtocolParameters, Error>;
 
     async fn process_block_import_notification(
@@ -83,12 +82,18 @@ where
 
     fn phase_filter(&self, job: GadgetJobType) -> bool;
 
-    fn jobs_client(&self) -> JobsClient<Self::Block, Self::Backend, Self::Client>;
+    fn jobs_client(&self) -> &SharedOptional<JobsClient<Self::Block, Self::Backend, Self::Client>>;
     fn pallet_tx(&self) -> Arc<dyn PalletSubmitter>;
 
     fn logger(&self) -> DebugLogger;
 
     fn client(&self) -> Self::Client;
+    fn get_jobs_client(&self) -> JobsClient<Self::Block, Self::Backend, Self::Client> {
+        self.jobs_client()
+            .lock()
+            .clone()
+            .expect("Jobs client not initialized")
+    }
     fn get_work_manager_config(&self) -> WorkManagerConfig {
         Default::default()
     }
@@ -138,7 +143,8 @@ where
         &self,
         jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
     ) -> Result<(Self::Network, Self::Protocol), Error> {
-        T::initialize_network_and_protocol(self, jobs_client).await?;
+        let jobs_client_store = T::jobs_client(self);
+        jobs_client_store.lock().replace(jobs_client);
         Ok((self.clone(), self.clone()))
     }
 
@@ -163,8 +169,9 @@ where
     async fn create_next_job(
         &self,
         job: JobInitMetadata<T::Block>,
+        work_manager: &ProtocolWorkManager<WorkManager>,
     ) -> Result<<Self as AsyncProtocol>::AdditionalParams, Error> {
-        T::create_next_job(self, job).await
+        T::create_next_job(self, job, work_manager).await
     }
 
     async fn process_block_import_notification(
@@ -196,7 +203,7 @@ where
     }
 
     fn client(&self) -> JobsClient<T::Block, T::Backend, T::Client> {
-        T::jobs_client(self)
+        T::get_jobs_client(self)
     }
 
     fn logger(&self) -> DebugLogger {
@@ -214,14 +221,14 @@ where
     <T::Client as ProvideRuntimeApi<T::Block>>::Api: JobsApiForGadget<T::Block>,
 {
     async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
-        T::next_message(self).await
+        T::network(self).next_message().await
     }
 
     async fn send_message(
         &self,
         message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
     ) -> Result<(), Error> {
-        T::send_message(self, message).await
+        T::network(self).send_message(message).await
     }
 }
 
diff --git a/gadget-common/src/gadget/mod.rs b/gadget-common/src/gadget/mod.rs
index 536dac6fa..1be73d87e 100644
--- a/gadget-common/src/gadget/mod.rs
+++ b/gadget-common/src/gadget/mod.rs
@@ -192,7 +192,11 @@ where
             let task_id = relevant_job.task_id;
             let retry_id = relevant_job.retry_id;
             log::trace!(target: "gadget", "[{}] Creating job for task {task_id} with retry id {retry_id}", self.protocol.name(), task_id = hex::encode(task_id), retry_id = retry_id);
-            match self.protocol.create_next_job(relevant_job).await {
+            match self
+                .protocol
+                .create_next_job(relevant_job, &self.job_manager)
+                .await
+            {
                 Ok(params) => {
                     match self
                         .protocol
@@ -284,6 +288,7 @@ where
     async fn create_next_job(
         &self,
         job: JobInitMetadata<B>,
+        work_manager: &ProtocolWorkManager<WorkManager>,
     ) -> Result<<Self as AsyncProtocol>::AdditionalParams, Error>;
 
     /// Process a block import notification
diff --git a/gadget-common/src/gadget/network/mod.rs b/gadget-common/src/gadget/network/mod.rs
index deaf3101c..b954cd45b 100644
--- a/gadget-common/src/gadget/network/mod.rs
+++ b/gadget-common/src/gadget/network/mod.rs
@@ -1,3 +1,4 @@
+use crate::client::AccountId;
 use crate::gadget::work_manager::WorkManager;
 use crate::Error;
 use async_trait::async_trait;
@@ -13,6 +14,10 @@ pub trait Network: Send + Sync + Clone + 'static {
         message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
     ) -> Result<(), Error>;
 
+    /// The ID of the king. Only relevant for ZkSaaS networks
+    fn greatest_authority_id(&self) -> Option<AccountId> {
+        None
+    }
     /// If the network implementation requires a custom runtime, this function
     /// should be manually implemented to keep the network alive
     async fn run(&self) -> Result<(), Error> {
diff --git a/gadget-common/src/keystore.rs b/gadget-common/src/keystore.rs
index e681dcc74..8fa1b4774 100644
--- a/gadget-common/src/keystore.rs
+++ b/gadget-common/src/keystore.rs
@@ -1,3 +1,4 @@
+use crate::Error;
 use async_trait::async_trait;
 use parking_lot::RwLock;
 use serde::de::DeserializeOwned;
@@ -5,6 +6,8 @@ use serde::Serialize;
 use sp_core::ecdsa::Pair as EcdsaPair;
 use sp_core::sr25519::Pair as Sr25519Pair;
 use sp_core::{keccak_256, Pair};
+use sqlx::sqlite::SqlitePoolOptions;
+use sqlx::{Pool, Row, Sqlite};
 use std::collections::HashMap;
 use std::sync::Arc;
 use tangle_primitives::jobs::JobId;
@@ -27,6 +30,12 @@ impl<P: Pair> GenericKeyStore<InMemoryBackend, P> {
     }
 }
 
+impl<P: Pair, Backend: KeystoreBackend> GenericKeyStore<Backend, P> {
+    pub fn new(backend: Backend, pair: P) -> Self {
+        GenericKeyStore { backend, pair }
+    }
+}
+
 impl<P: Pair, BE: KeystoreBackend> GenericKeyStore<BE, P> {
     pub fn pair(&self) -> &P {
         &self.pair
@@ -114,3 +123,93 @@ impl KeystoreBackend for InMemoryBackend {
         Ok(())
     }
 }
+
+#[derive(Clone)]
+pub struct SqliteBackend {
+    pool: Pool<Sqlite>,
+}
+
+impl SqliteBackend {
+    pub async fn in_memory() -> Result<Self, Box<dyn std::error::Error>> {
+        Self::new("sqlite://:memory:").await
+    }
+
+    // Initialize a new key-value store with a SqlitePool
+    pub async fn new(database_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
+        let pool = SqlitePoolOptions::new().connect(database_url).await?;
+
+        // Ensure the table exists
+        sqlx::query(
+            r"CREATE TABLE IF NOT EXISTS key_value_store (
+                key TEXT PRIMARY KEY,
+                value BLOB NOT NULL
+              )",
+        )
+        .execute(&pool)
+        .await?;
+
+        Ok(Self { pool })
+    }
+}
+
+#[async_trait]
+impl KeystoreBackend for SqliteBackend {
+    async fn get<T: DeserializeOwned>(&self, key: &[u8; 32]) -> Result<Option<T>, Error> {
+        let key = key_to_string(key);
+        let result = sqlx::query("SELECT value FROM key_value_store WHERE key = ?")
+            .bind(key)
+            .fetch_optional(&self.pool)
+            .await
+            .map_err(|err| Error::KeystoreError {
+                err: format!("Failed to fetch value: {:?}", err),
+            })?;
+
+        match result {
+            Some(row) => {
+                let value: Vec<u8> = row.get("value");
+                let value: T =
+                    bincode2::deserialize(&value).map_err(|rr| Error::KeystoreError {
+                        err: format!("Failed to deserialize value: {:?}", rr),
+                    })?;
+                Ok(Some(value))
+            }
+            None => Ok(None),
+        }
+    }
+
+    async fn set<T: Serialize + Send>(&self, key: &[u8; 32], value: T) -> Result<(), Error> {
+        let key = key_to_string(key);
+        let value = bincode2::serialize(&value).map_err(|rr| Error::KeystoreError {
+            err: format!("Failed to serialize value: {:?}", rr),
+        })?;
+
+        sqlx::query("INSERT INTO key_value_store (key, value) VALUES (?, ?)")
+            .bind(key)
+            .bind(value)
+            .execute(&self.pool)
+            .await
+            .map_err(|err| Error::KeystoreError {
+                err: format!("Failed to insert value: {:?}", err),
+            })?;
+        Ok(())
+    }
+}
+
+fn key_to_string(key: &[u8; 32]) -> String {
+    hex::encode(key)
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::prelude::KeystoreBackend;
+
+    #[tokio::test]
+    async fn test_in_memory_kv_store() {
+        let store = super::SqliteBackend::in_memory().await.unwrap();
+        let key = [0u8; 32];
+        let value = "hello".to_string();
+        store.set(&key, value.clone()).await.unwrap();
+        let result: String = store.get(&key).await.unwrap().unwrap();
+        assert_eq!(value, result);
+    }
+}
diff --git a/gadget-common/src/lib.rs b/gadget-common/src/lib.rs
index 256f49771..cf5869c28 100644
--- a/gadget-common/src/lib.rs
+++ b/gadget-common/src/lib.rs
@@ -27,10 +27,12 @@ pub mod prelude {
     pub use crate::gadget::message::GadgetProtocolMessage;
     pub use crate::gadget::work_manager::WorkManager;
     pub use crate::gadget::JobInitMetadata;
+    pub use crate::gadget::WorkManagerConfig;
     pub use crate::generate_setup_and_run_command;
     pub use crate::keystore::{ECDSAKeyStore, InMemoryBackend, KeystoreBackend};
     pub use crate::{BuiltExecutableJobWrapper, Error, JobBuilder, JobError, WorkManagerInterface};
     pub use async_trait::async_trait;
+    pub use gadget_core::job_manager::ProtocolWorkManager;
     pub use gadget_core::job_manager::SendFuture;
     pub use parking_lot::Mutex;
     pub use protocol_macros::protocol;
@@ -223,3 +225,139 @@ macro_rules! generate_setup_and_run_command {
         }
     };
 }
+
+#[macro_export]
+macro_rules! generate_protocol {
+    ($name:expr, $struct_name:ident, $async_proto_params:ty, $proto_gen_path:expr, $create_job_path:expr, $phase_filter:pat, $( $role_filter:pat ),*) => {
+        #[protocol]
+        pub struct $struct_name<
+            B: Block,
+            BE: Backend<B> + 'static,
+            C: ClientWithApi<B, BE>,
+            N: Network,
+            KBE: KeystoreBackend,
+        > where
+            <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
+        {
+            pallet_tx: Arc<dyn PalletSubmitter>,
+            logger: DebugLogger,
+            client: C,
+            network: N,
+            account_id: AccountId,
+            key_store: ECDSAKeyStore<KBE>,
+            jobs_client: Arc<Mutex<Option<JobsClient<B, BE, C>>>>,
+            _pd: std::marker::PhantomData<(B, BE)>,
+        }
+
+        #[async_trait]
+        impl<
+                B: Block,
+                BE: Backend<B> + 'static,
+                C: ClientWithApi<B, BE>,
+                N: Network,
+                KBE: KeystoreBackend,
+            > FullProtocolConfig for $struct_name<B, BE, C, N, KBE>
+        where
+            <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
+        {
+            type AsyncProtocolParameters = $async_proto_params;
+            type Client = C;
+            type Block = B;
+            type Backend = BE;
+            type Network = N;
+            type AdditionalNodeParameters = ();
+            type KeystoreBackend = KBE;
+
+            async fn new(
+                client: Self::Client,
+                pallet_tx: Arc<dyn PalletSubmitter>,
+                network: Self::Network,
+                logger: DebugLogger,
+                account_id: AccountId,
+                key_store: ECDSAKeyStore<Self::KeystoreBackend>,
+            ) -> Result<Self, Error> {
+                Ok(Self {
+                    pallet_tx,
+                    logger,
+                    client,
+                    network,
+                    account_id,
+                    key_store,
+                    jobs_client: Arc::new(parking_lot::Mutex::new(None)),
+                    _pd: std::marker::PhantomData,
+                })
+            }
+
+            async fn generate_protocol_from(
+                &self,
+                associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
+                associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
+                associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
+                associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
+                protocol_message_rx: UnboundedReceiver<GadgetProtocolMessage>,
+                additional_params: Self::AsyncProtocolParameters,
+            ) -> Result<BuiltExecutableJobWrapper, JobError> {
+                $proto_gen_path(
+                    self,
+                    associated_block_id,
+                    associated_retry_id,
+                    associated_session_id,
+                    associated_task_id,
+                    protocol_message_rx,
+                    additional_params,
+                )
+                .await
+            }
+
+            fn network(&self) -> &Self::Network {
+                &self.network
+            }
+
+            async fn create_next_job(
+                &self,
+                job: JobInitMetadata<Self::Block>,
+                work_manager: &ProtocolWorkManager<WorkManager>,
+            ) -> Result<Self::AsyncProtocolParameters, Error> {
+                $create_job_path(self, job, work_manager).await
+            }
+
+            fn account_id(&self) -> &AccountId {
+                &self.account_id
+            }
+
+            fn name(&self) -> String {
+                $name.to_string()
+            }
+
+            fn role_filter(&self, role: RoleType) -> bool {
+                $(
+                    if matches!(role, $role_filter) {
+                        return true;
+                    }
+                )*
+
+                false
+            }
+
+            fn phase_filter(&self, job: GadgetJobType) -> bool {
+                matches!(job, $phase_filter)
+            }
+
+            fn jobs_client(&self) -> &SharedOptional<JobsClient<Self::Block, Self::Backend, Self::Client>> {
+                &self.jobs_client
+            }
+
+            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()
+            }
+        }
+    };
+}
diff --git a/gadget-core/src/job_manager.rs b/gadget-core/src/job_manager.rs
index 0470c581b..0e9c692f1 100644
--- a/gadget-core/src/job_manager.rs
+++ b/gadget-core/src/job_manager.rs
@@ -4,7 +4,7 @@ use std::fmt::{Debug, Display};
 use std::future::Future;
 use std::ops::{Add, Sub};
 use std::{
-    collections::{HashMap, HashSet, VecDeque},
+    collections::{HashMap, VecDeque},
     hash::{Hash, Hasher},
     sync::Arc,
 };
@@ -44,7 +44,7 @@ impl<WM: WorkManagerInterface> Clone for ProtocolWorkManager<WM> {
 }
 
 pub struct WorkManagerInner<WM: WorkManagerInterface> {
-    pub active_tasks: HashSet<Job<WM>>,
+    pub active_tasks: HashMap<WM::TaskID, Job<WM>>,
     pub enqueued_tasks: VecDeque<Job<WM>>,
     // task hash => SSID => enqueued messages
     pub enqueued_messages: EnqueuedMessage<WM::TaskID, WM::RetryID, WM::ProtocolMessage>,
@@ -68,7 +68,7 @@ pub trait WorkManagerInterface: Send + Sync + 'static + Sized {
         + Sub<Output = Self::Clock>
         + Add<Output = Self::Clock>
         + 'static;
-    type ProtocolMessage: ProtocolMessageMetadata<Self> + Send + Sync + 'static;
+    type ProtocolMessage: ProtocolMessageMetadata<Self> + Send + Sync + Clone + 'static;
     type Error: Debug + Send + Sync + 'static;
     type SessionID: Copy + Hash + Eq + PartialEq + Display + Debug + Send + Sync + 'static;
     type TaskID: Copy + Hash + Eq + PartialEq + Debug + Send + Sync + AsRef<[u8]> + 'static;
@@ -147,7 +147,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
     ) -> Self {
         let this = Self {
             inner: Arc::new(RwLock::new(WorkManagerInner {
-                active_tasks: HashSet::new(),
+                active_tasks: HashMap::new(),
                 enqueued_tasks: VecDeque::new(),
                 enqueued_messages: HashMap::new(),
                 retry_id_tracker: HashMap::new(),
@@ -207,6 +207,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
             handle,
             task_hash,
             utility: self.utility.clone(),
+            recorded_messages: vec![],
         };
 
         if force_start {
@@ -249,7 +250,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
             .read()
             .active_tasks
             .iter()
-            .map(|r| r.metadata(now))
+            .map(|r| r.1.metadata(now))
             .collect()
     }
 
@@ -266,7 +267,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
         let now = self.utility.clock();
         let mut lock = self.inner.write();
         let cur_count = lock.active_tasks.len();
-        lock.active_tasks.retain(|job| {
+        lock.active_tasks.retain(|_, job| {
             let is_stalled = job.handle.has_stalled(now);
             let is_done = job.handle.is_done();
 
@@ -379,11 +380,13 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
 
                     while let Some(message) = enqueued_messages.pop_front() {
                         if should_deliver(&job, &message, job.task_hash) {
-                            if let Err(err) = job.handle.deliver_message(message) {
+                            if let Err(err) = job.handle.deliver_message(message.clone()) {
                                 self.utility.error(format!(
                                     "Unable to deliver message for job {:?}: {err:?}",
                                     hex::encode(job.task_hash)
                                 ));
+                            } else {
+                                lock.record_message(message)
                             }
                         } else {
                             self.utility.warn("Will not deliver enqueued message to async protocol since the message is no longer acceptable".to_string())
@@ -406,7 +409,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
             .retry_id_tracker
             .entry(task_hash)
             .or_insert(job.handle.retry_id()) = job.handle.retry_id();
-        lock.active_tasks.insert(job);
+        lock.active_tasks.insert(task_hash, job);
 
         let mut task = task.lock().take().expect("Should not happen");
         let logger = self.utility.clone();
@@ -443,7 +446,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
 
     pub fn job_exists(&self, job: &WM::TaskID) -> bool {
         let lock = self.inner.read();
-        lock.active_tasks.iter().any(|r| &r.task_hash == job)
+        lock.active_tasks.iter().any(|r| &r.1.task_hash == job)
             || lock.enqueued_tasks.iter().any(|j| &j.task_hash == job)
     }
 
@@ -464,36 +467,38 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
         let message_task_hash = msg.associated_task();
         let mut lock = self.inner.write();
 
-        // check the enqueued
+        // Check the enqueued
         for task in lock.enqueued_tasks.iter() {
             if should_deliver(task, &msg, message_task_hash) {
                 self.utility.debug(format!(
                     "Message is for this ENQUEUED execution in session: {}",
                     task.handle.session_id()
                 ));
-                if let Err(err) = task.handle.deliver_message(msg) {
+                if let Err(err) = task.handle.deliver_message(msg.clone()) {
                     return Err(WorkManagerError::DeliverMessageFailed {
                         reason: format!("{err:?}"),
                     });
                 }
 
+                lock.record_message(msg);
                 return Ok(DeliveryType::EnqueuedProtocol);
             }
         }
 
-        // check the currently signing
-        for task in lock.active_tasks.iter() {
+        // Check the currently signing
+        for (_, task) in lock.active_tasks.iter() {
             if should_deliver(task, &msg, message_task_hash) {
                 self.utility.debug(format!(
                     "Message is for this CURRENT execution in session: {}",
                     task.handle.session_id()
                 ));
-                if let Err(err) = task.handle.deliver_message(msg) {
+                if let Err(err) = task.handle.deliver_message(msg.clone()) {
                     return Err(WorkManagerError::DeliverMessageFailed {
                         reason: format!("{err:?}"),
                     });
                 }
 
+                lock.record_message(msg);
                 return Ok(DeliveryType::ActiveProtocol);
             }
         }
@@ -503,7 +508,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
         let current_running_session_ids: Vec<WM::SessionID> = lock
             .active_tasks
             .iter()
-            .map(|job| job.handle.session_id())
+            .map(|job| job.1.handle.session_id())
             .collect();
         let enqueued_session_ids: Vec<WM::SessionID> = lock
             .enqueued_tasks
@@ -528,7 +533,7 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
         WM::RetryID: std::ops::AddAssign<u32>,
     {
         let mut lock = self.inner.write();
-        lock.active_tasks.retain(|job| {
+        lock.active_tasks.retain(|_, job| {
             let should_keep = &job.task_hash != task_id;
 
             if !should_keep {
@@ -541,16 +546,55 @@ impl<WM: WorkManagerInterface> ProtocolWorkManager<WM> {
         *lock.retry_id_tracker.get_mut(task_id).expect("Must exist") += 1;
     }
 
+    pub fn visit_recorded_messages<V: FnOnce(&Vec<WM::ProtocolMessage>) -> T, T>(
+        &self,
+        task_hash: &WM::TaskID,
+        visitor: V,
+    ) -> Option<T> {
+        let lock = self.inner.read();
+        if let Some(active_job) = lock.active_tasks.get(task_hash) {
+            return Some(visitor(&active_job.recorded_messages));
+        }
+
+        if let Some(enqueued_job) = lock
+            .enqueued_tasks
+            .iter()
+            .find(|j| j.task_hash == *task_hash)
+        {
+            return Some(visitor(&enqueued_job.recorded_messages));
+        }
+
+        None
+    }
+
     pub fn poll_method(&self) -> PollMethod {
         *self.poll_method
     }
 }
 
+impl<WM: WorkManagerInterface> WorkManagerInner<WM> {
+    fn record_message(&mut self, msg: WM::ProtocolMessage) {
+        if let Some(active_job) = self.active_tasks.get_mut(&msg.associated_task()) {
+            active_job.recorded_messages.push(msg);
+            return;
+        }
+
+        if let Some(enqueued_job) = self
+            .enqueued_tasks
+            .iter_mut()
+            .find(|j| j.task_hash == msg.associated_task())
+        {
+            enqueued_job.recorded_messages.push(msg);
+        }
+    }
+}
+
 pub struct Job<WM: WorkManagerInterface> {
     task_hash: WM::TaskID,
     utility: Arc<WM>,
     handle: Arc<dyn ProtocolRemote<WM>>,
     task: Arc<parking_lot::Mutex<Option<Box<dyn ExecutableJob>>>>,
+    recorded_messages: Vec<WM::ProtocolMessage>,
 }
 
 impl<WM: WorkManagerInterface> Job<WM> {
@@ -1363,7 +1407,38 @@ mod tests {
         assert!(work_manager.latest_retry_id(&[0; 32]).is_none());
     }
 
+    #[tokio::test]
+    async fn test_record_message() {
+        let work_manager = ProtocolWorkManager::new(TestWorkManager, 1, 0, PollMethod::Manual);
+        let (remote, task, _rx) = generate_async_protocol(1, 1, 0);
+        work_manager.push_task([0; 32], true, remote, task).unwrap();
+        let message = TestMessage {
+            message: "test".to_string(),
+            associated_block_id: 0,
+            associated_session_id: 1,
+            associated_retry_id: 1,
+            associated_task: [0; 32],
+            associated_sender: 0,
+            associated_recipient: None,
+        };
+
+        work_manager.deliver_message(message.clone()).unwrap();
+        let lock = work_manager.inner.read();
+        let active_job = lock.active_tasks.get(&[0; 32]).unwrap();
+        assert_eq!(active_job.recorded_messages.len(), 1);
+        assert_eq!(active_job.recorded_messages[0], message);
+
+        drop(lock);
+        assert!(work_manager
+            .visit_recorded_messages(&[0; 32], |messages| {
+                assert_eq!(messages.len(), 1);
+                assert_eq!(messages[0], message);
+            })
+            .is_some());
+    }
+
     struct DummyRangeChecker<const N: u64>;
+    #[derive(Clone)]
     struct DummyProtocolMessage;
 
     impl<const N: u64> ProtocolMessageMetadata<DummyRangeChecker<N>> for DummyProtocolMessage {
diff --git a/protocols/bls/src/lib.rs b/protocols/bls/src/lib.rs
index 997ecc18f..4f6999831 100644
--- a/protocols/bls/src/lib.rs
+++ b/protocols/bls/src/lib.rs
@@ -1,10 +1,11 @@
 use crate::protocol::keygen::BlsKeygenAdditionalParams;
 use async_trait::async_trait;
+use gadget_common::full_protocol::SharedOptional;
 use gadget_common::gadget::JobInitMetadata;
 use gadget_common::prelude::*;
 use gadget_common::{
-    generate_setup_and_run_command, BuiltExecutableJobWrapper, Error, JobError,
-    WorkManagerInterface,
+    generate_protocol, generate_setup_and_run_command, BuiltExecutableJobWrapper, Error, JobError,
+    ProtocolWorkManager, WorkManagerInterface,
 };
 use protocol::signing::BlsSigningAdditionalParams;
 use protocol_macros::protocol;
@@ -12,293 +13,28 @@ use std::sync::Arc;
 
 pub mod protocol;
 
-#[protocol]
-pub struct BlsKeygenConfig<
-    B: Block,
-    BE: Backend<B> + 'static,
-    C: ClientWithApi<B, BE>,
-    N: Network,
-    KBE: KeystoreBackend,
-> where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    pallet_tx: Arc<dyn PalletSubmitter>,
-    logger: DebugLogger,
-    client: C,
-    network: N,
-    account_id: AccountId,
-    key_store: ECDSAKeyStore<KBE>,
-    jobs_client: Arc<Mutex<Option<JobsClient<B, BE, C>>>>,
-    _pd: std::marker::PhantomData<(B, BE)>,
-}
-
-#[async_trait]
-impl<
-        B: Block,
-        BE: Backend<B> + 'static,
-        C: ClientWithApi<B, BE>,
-        N: Network,
-        KBE: KeystoreBackend,
-    > FullProtocolConfig for BlsKeygenConfig<B, BE, C, N, KBE>
-where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    type AsyncProtocolParameters = BlsKeygenAdditionalParams;
-    type Client = C;
-    type Block = B;
-    type Backend = BE;
-    type Network = N;
-    type AdditionalNodeParameters = ();
-    type KeystoreBackend = KBE;
-
-    async fn new(
-        client: Self::Client,
-        pallet_tx: Arc<dyn PalletSubmitter>,
-        network: Self::Network,
-        logger: DebugLogger,
-        account_id: AccountId,
-        key_store: ECDSAKeyStore<Self::KeystoreBackend>,
-    ) -> Result<Self, Error> {
-        Ok(Self {
-            pallet_tx,
-            logger,
-            client,
-            network,
-            account_id,
-            key_store,
-            jobs_client: Arc::new(parking_lot::Mutex::new(None)),
-            _pd: std::marker::PhantomData,
-        })
-    }
-
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
-        protocol_message_rx: UnboundedReceiver<GadgetProtocolMessage>,
-        additional_params: Self::AsyncProtocolParameters,
-    ) -> Result<BuiltExecutableJobWrapper, JobError> {
-        protocol::keygen::generate_protocol_from(
-            self,
-            associated_block_id,
-            associated_retry_id,
-            associated_session_id,
-            associated_task_id,
-            protocol_message_rx,
-            additional_params,
-        )
-        .await
-    }
-
-    async fn initialize_network_and_protocol(
-        &self,
-        jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
-    ) -> Result<(), Error> {
-        self.jobs_client.lock().replace(jobs_client);
-        Ok(())
-    }
-
-    async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
-        self.network.next_message().await
-    }
-
-    async fn send_message(
-        &self,
-        message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
-    ) -> Result<(), Error> {
-        self.network.send_message(message).await
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<Self::Block>,
-    ) -> Result<Self::AsyncProtocolParameters, Error> {
-        protocol::keygen::create_next_job(self, job).await
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
-
-    fn name(&self) -> String {
-        "BLS_KEYGEN_PROTOCOL".to_string()
-    }
-
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(
-            role,
-            RoleType::Tss(ThresholdSignatureRoleType::GennaroDKGBls381)
-        )
-    }
-
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, GadgetJobType::DKGTSSPhaseOne(_))
-    }
-
-    fn jobs_client(&self) -> JobsClient<Self::Block, Self::Backend, Self::Client> {
-        self.jobs_client.lock().as_ref().unwrap().clone()
-    }
-
-    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()
-    }
-}
-
-#[protocol]
-pub struct BlsSigningConfig<
-    B: Block,
-    BE: Backend<B> + 'static,
-    C: ClientWithApi<B, BE>,
-    N: Network,
-    KBE: KeystoreBackend,
-> where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    pallet_tx: Arc<dyn PalletSubmitter>,
-    logger: DebugLogger,
-    client: C,
-    network: N,
-    account_id: AccountId,
-    key_store: ECDSAKeyStore<KBE>,
-    jobs_client: Arc<Mutex<Option<JobsClient<B, BE, C>>>>,
-    _pd: std::marker::PhantomData<(B, BE)>,
-}
-
-#[async_trait]
-impl<
-        B: Block,
-        BE: Backend<B> + 'static,
-        C: ClientWithApi<B, BE>,
-        N: Network,
-        KBE: KeystoreBackend,
-    > FullProtocolConfig for BlsSigningConfig<B, BE, C, N, KBE>
-where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    type AsyncProtocolParameters = BlsSigningAdditionalParams;
-    type Client = C;
-    type Block = B;
-    type Backend = BE;
-    type Network = N;
-    type AdditionalNodeParameters = ();
-    type KeystoreBackend = KBE;
-
-    async fn new(
-        client: Self::Client,
-        pallet_tx: Arc<dyn PalletSubmitter>,
-        network: Self::Network,
-        logger: DebugLogger,
-        account_id: AccountId,
-        key_store: ECDSAKeyStore<Self::KeystoreBackend>,
-    ) -> Result<Self, Error> {
-        Ok(Self {
-            pallet_tx,
-            logger,
-            client,
-            network,
-            account_id,
-            key_store,
-            jobs_client: Arc::new(parking_lot::Mutex::new(None)),
-            _pd: std::marker::PhantomData,
-        })
-    }
-
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
-        protocol_message_rx: UnboundedReceiver<GadgetProtocolMessage>,
-        additional_params: Self::AsyncProtocolParameters,
-    ) -> Result<BuiltExecutableJobWrapper, JobError> {
-        protocol::signing::generate_protocol_from(
-            self,
-            associated_block_id,
-            associated_retry_id,
-            associated_session_id,
-            associated_task_id,
-            protocol_message_rx,
-            additional_params,
-        )
-        .await
-    }
-
-    async fn initialize_network_and_protocol(
-        &self,
-        jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
-    ) -> Result<(), Error> {
-        self.jobs_client.lock().replace(jobs_client);
-        Ok(())
-    }
-
-    async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
-        self.network.next_message().await
-    }
-
-    async fn send_message(
-        &self,
-        message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
-    ) -> Result<(), Error> {
-        self.network.send_message(message).await
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<Self::Block>,
-    ) -> Result<Self::AsyncProtocolParameters, Error> {
-        protocol::signing::create_next_job(self, job).await
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
-
-    fn name(&self) -> String {
-        "BLS_SIGNING_PROTOCOL".to_string()
-    }
-
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(
-            role,
-            RoleType::Tss(ThresholdSignatureRoleType::GennaroDKGBls381)
-        )
-    }
-
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, GadgetJobType::DKGTSSPhaseTwo(_))
-    }
-
-    fn jobs_client(&self) -> JobsClient<Self::Block, Self::Backend, Self::Client> {
-        self.jobs_client.lock().as_ref().unwrap().clone()
-    }
-
-    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()
-    }
-}
-
-generate_setup_and_run_command!(BlsKeygenConfig, BlsSigningConfig);
+generate_protocol!(
+    "BLS-Keygen-Protocol",
+    BlsKeygenProtocol,
+    BlsKeygenAdditionalParams,
+    protocol::keygen::generate_protocol_from,
+    protocol::keygen::create_next_job,
+    GadgetJobType::DKGTSSPhaseOne(_),
+    RoleType::Tss(ThresholdSignatureRoleType::GennaroDKGBls381)
+);
+generate_protocol!(
+    "BLS-Signing-Protocol",
+    BlsSigningProtocol,
+    BlsSigningAdditionalParams,
+    protocol::signing::generate_protocol_from,
+    protocol::signing::create_next_job,
+    GadgetJobType::DKGTSSPhaseTwo(_),
+    RoleType::Tss(ThresholdSignatureRoleType::GennaroDKGBls381)
+);
+generate_setup_and_run_command!(BlsKeygenProtocol, BlsSigningProtocol);
 test_utils::generate_signing_and_keygen_tss_tests!(
     2,
     3,
+    2,
     ThresholdSignatureRoleType::GennaroDKGBls381
 );
diff --git a/protocols/bls/src/protocol/keygen.rs b/protocols/bls/src/protocol/keygen.rs
index 3351db4a1..025b3fca2 100644
--- a/protocols/bls/src/protocol/keygen.rs
+++ b/protocols/bls/src/protocol/keygen.rs
@@ -29,8 +29,9 @@ pub async fn create_next_job<
     N: Network,
     KBE: KeystoreBackend,
 >(
-    config: &crate::BlsKeygenConfig<B, BE, C, N, KBE>,
+    config: &crate::BlsKeygenProtocol<B, BE, C, N, KBE>,
     job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
 ) -> Result<BlsKeygenAdditionalParams, Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
@@ -72,7 +73,7 @@ pub async fn generate_protocol_from<
     N: Network,
     KBE: KeystoreBackend,
 >(
-    config: &crate::BlsKeygenConfig<B, BE, C, N, KBE>,
+    config: &crate::BlsKeygenProtocol<B, BE, C, N, KBE>,
     associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
     associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
     associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
diff --git a/protocols/bls/src/protocol/signing.rs b/protocols/bls/src/protocol/signing.rs
index 7d092e374..5be1fe342 100644
--- a/protocols/bls/src/protocol/signing.rs
+++ b/protocols/bls/src/protocol/signing.rs
@@ -8,7 +8,8 @@ use gadget_common::gadget::JobInitMetadata;
 use gadget_common::keystore::KeystoreBackend;
 use gadget_common::sp_core::keccak_256;
 use gadget_common::{
-    Backend, Block, BuiltExecutableJobWrapper, Error, JobBuilder, JobError, WorkManagerInterface,
+    Backend, Block, BuiltExecutableJobWrapper, Error, JobBuilder, JobError, ProtocolWorkManager,
+    WorkManagerInterface,
 };
 use gennaro_dkg::{Participant, SecretParticipantImpl};
 use itertools::Itertools;
@@ -38,8 +39,9 @@ pub async fn create_next_job<
     N: Network,
     KBE: KeystoreBackend,
 >(
-    config: &crate::BlsSigningConfig<B, BE, C, N, KBE>,
+    config: &crate::BlsSigningProtocol<B, BE, C, N, KBE>,
     job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
 ) -> Result<BlsSigningAdditionalParams, Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
@@ -99,7 +101,7 @@ pub async fn generate_protocol_from<
     N: Network,
     KBE: KeystoreBackend,
 >(
-    config: &crate::BlsSigningConfig<B, BE, C, N, KBE>,
+    config: &crate::BlsSigningProtocol<B, BE, C, N, KBE>,
     associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
     associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
     associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
@@ -114,7 +116,7 @@ where
     let network = config.network.clone();
     let result = Arc::new(tokio::sync::Mutex::new(None));
     let result_clone = result.clone();
-    let client = config.jobs_client();
+    let client = config.get_jobs_client();
     let role_type = additional_params.role_type;
     let job_id = additional_params.job_id;
     let logger = config.logger.clone();
diff --git a/protocols/dfns-cggmp21/Cargo.toml b/protocols/dfns-cggmp21/Cargo.toml
index 5478fcdb1..f714086cb 100644
--- a/protocols/dfns-cggmp21/Cargo.toml
+++ b/protocols/dfns-cggmp21/Cargo.toml
@@ -35,6 +35,5 @@ serde = { version = "1.0.193", features = ["derive"] }
 rand = { workspace = true }
 rand_chacha = { workspace = true }
 hex = { workspace = true }
-
-[dev-dependencies]
 test-utils = { workspace = true }
+parking_lot = { workspace = true }
diff --git a/protocols/dfns-cggmp21/src/lib.rs b/protocols/dfns-cggmp21/src/lib.rs
index 923dd9908..65257c456 100644
--- a/protocols/dfns-cggmp21/src/lib.rs
+++ b/protocols/dfns-cggmp21/src/lib.rs
@@ -1,173 +1,76 @@
-use gadget_common::client::JobsApiForGadget;
+use crate::protocols::key_refresh::DfnsCGGMP21KeyRefreshExtraParams;
+use crate::protocols::key_rotate::DfnsCGGMP21KeyRotateExtraParams;
+use crate::protocols::keygen::DfnsCGGMP21KeygenExtraParams;
+use crate::protocols::sign::DfnsCGGMP21SigningExtraParams;
+use async_trait::async_trait;
 use gadget_common::client::{AccountId, ClientWithApi, JobsClient, PalletSubmitter};
-use gadget_common::config::NetworkAndProtocolSetup;
+use gadget_common::client::{GadgetJobType, JobsApiForGadget};
 use gadget_common::debug_logger::DebugLogger;
+use gadget_common::full_protocol::SharedOptional;
 use gadget_common::gadget::network::Network;
+use gadget_common::gadget::JobInitMetadata;
 use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
+use gadget_common::prelude::*;
+use gadget_common::prelude::{FullProtocolConfig, GadgetProtocolMessage, Mutex, WorkManager};
+use gadget_common::{generate_protocol, generate_setup_and_run_command, Error};
+use gadget_core::job::{BuiltExecutableJobWrapper, JobError};
+use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
 use protocol_macros::protocol;
-use protocols::key_refresh::DfnsCGGMP21KeyRefreshProtocol;
-use protocols::key_rotate::DfnsCGGMP21KeyRotateProtocol;
-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;
+use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use test_utils::generate_signing_and_keygen_tss_tests;
+use tokio::sync::mpsc::UnboundedReceiver;
 
 pub mod constants;
 pub mod error;
 pub mod protocols;
 pub mod util;
 
-/// A Helper macro to declare a protocol, used
-/// to avoid code duplication.
-macro_rules! decl_porto {
-    ($name:ident + $proto:ident = $im:path) => {
-
-        #[protocol]
-        pub struct $name<
-            N: Network,
-            B: Block,
-            BE: Backend<B>,
-            KBE: KeystoreBackend,
-            C: ClientWithApi<B, BE>,
-        > where
-            <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-        {
-            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 $name<N, B, BE, KBE, C>
-        where
-            <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-        {
-            type Network = N;
-            type Protocol = $proto<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> {
-                use $im as m;
-                let protocol = m::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()
-            }
-        }
-
-    };
-    // recursive case with optional trailing comma
-    ($($name:ident + $proto:ident = $im:path),+ $(,)?) => {
-        $(decl_porto!($name + $proto = $im);)+
-    };
-}
-
-// A macro to declare all the protocols
-decl_porto!(
-    DfnsCGGMP21KeygenProtocolConfig + DfnsCGGMP21KeygenProtocol = protocols::keygen,
-    DfnsCGGMP21SigningProtocolConfig + DfnsCGGMP21SigningProtocol = protocols::sign,
-    DfnsCGGMP21KeyRefreshProtocolConfig + DfnsCGGMP21KeyRefreshProtocol = protocols::key_refresh,
-    DfnsCGGMP21KeyRotateProtocolConfig + DfnsCGGMP21KeyRotateProtocol = protocols::key_rotate,
+generate_protocol!(
+    "DFNS-Keygen-Protocol",
+    DfnsKeygenProtocol,
+    DfnsCGGMP21KeygenExtraParams,
+    protocols::keygen::generate_protocol_from,
+    protocols::keygen::create_next_job,
+    GadgetJobType::DKGTSSPhaseOne(_),
+    RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
+);
+generate_protocol!(
+    "DFNS-Signing-Protocol",
+    DfnsSigningProtocol,
+    DfnsCGGMP21SigningExtraParams,
+    protocols::sign::generate_protocol_from,
+    protocols::sign::create_next_job,
+    GadgetJobType::DKGTSSPhaseTwo(_),
+    RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
+);
+generate_protocol!(
+    "DFNS-Refresh-Protocol",
+    DfnsKeyRefreshProtocol,
+    DfnsCGGMP21KeyRefreshExtraParams,
+    protocols::key_refresh::generate_protocol_from,
+    protocols::key_refresh::create_next_job,
+    GadgetJobType::DKGTSSPhaseThree(_),
+    RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
+);
+generate_protocol!(
+    "DFNS-Rotate-Protocol",
+    DfnsKeyRotateProtocol,
+    DfnsCGGMP21KeyRotateExtraParams,
+    protocols::key_rotate::generate_protocol_from,
+    protocols::key_rotate::create_next_job,
+    GadgetJobType::DKGTSSPhaseFour(_),
+    RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
 );
 
-#[allow(clippy::too_many_arguments)]
-pub async fn run<B, BE, KBE, C, N, Tx>(
-    account_id: AccountId,
-    logger: DebugLogger,
-    keystore: ECDSAKeyStore<KBE>,
-    pallet_tx: Tx,
-    (client_keygen, client_signing, client_key_refresh, client_key_rotate): (C, C, C, C),
-    (network_keygen, network_signing, network_key_refresh, network_key_rotate): (N, N, N, N),
-) -> Result<(), gadget_common::Error>
-where
-    B: Block,
-    BE: Backend<B> + 'static,
-    C: ClientWithApi<B, BE>,
-    KBE: KeystoreBackend,
-    N: Network,
-    Tx: PalletSubmitter,
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    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.clone(),
-        client: client_signing,
-        logger: logger.clone(),
-        pallet_tx: pallet_tx.clone(),
-        _pd: std::marker::PhantomData,
-    };
-
-    let key_refresh_config = DfnsCGGMP21KeyRefreshProtocolConfig {
-        account_id,
-        network: network_key_refresh,
-        keystore_backend: keystore.clone(),
-        client: client_key_refresh,
-        logger: logger.clone(),
-        pallet_tx: pallet_tx.clone(),
-        _pd: std::marker::PhantomData,
-    };
-
-    let key_rotate_config = DfnsCGGMP21KeyRotateProtocolConfig {
-        account_id,
-        network: network_key_rotate,
-        keystore_backend: keystore,
-        client: client_key_rotate,
-        logger,
-        pallet_tx,
-        _pd: std::marker::PhantomData,
-    };
-
-    let keygen_future = keygen_config.execute();
-    let sign_future = sign_config.execute();
-    let key_refresh_future = key_refresh_config.execute();
-    let key_rotate_future = key_rotate_config.execute();
+generate_setup_and_run_command!(
+    DfnsKeygenProtocol,
+    DfnsSigningProtocol,
+    DfnsKeyRefreshProtocol,
+    DfnsKeyRotateProtocol
+);
 
-    tokio::select! {
-        res0 = keygen_future => res0,
-        res1 = sign_future => res1,
-        res2 = key_refresh_future => res2,
-        res3 = key_rotate_future => res3,
-    }
-}
+generate_signing_and_keygen_tss_tests!(2, 3, 4, ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1);
diff --git a/protocols/dfns-cggmp21/src/protocols/key_refresh.rs b/protocols/dfns-cggmp21/src/protocols/key_refresh.rs
index a2df20be4..3423b6aa2 100644
--- a/protocols/dfns-cggmp21/src/protocols/key_refresh.rs
+++ b/protocols/dfns-cggmp21/src/protocols/key_refresh.rs
@@ -1,17 +1,13 @@
-use async_trait::async_trait;
 use dfns_cggmp21::supported_curves::Secp256k1;
 use dfns_cggmp21::KeyShare;
-use gadget_common::client::{
-    AccountId, ClientWithApi, GadgetJobType, JobsApiForGadget, JobsClient,
-};
-use gadget_common::debug_logger::DebugLogger;
+use gadget_common::client::{AccountId, ClientWithApi, JobsApiForGadget};
 use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
 use gadget_common::gadget::network::Network;
 use gadget_common::gadget::work_manager::WorkManager;
-use gadget_common::gadget::{GadgetProtocol, JobInitMetadata, WorkManagerConfig};
-use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
-use gadget_common::protocol::AsyncProtocol;
-use gadget_common::{Block, BlockImportNotification};
+use gadget_common::gadget::JobInitMetadata;
+use gadget_common::keystore::KeystoreBackend;
+use gadget_common::prelude::FullProtocolConfig;
+use gadget_common::Block;
 use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
 use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
 use rand::SeedableRng;
@@ -23,164 +19,80 @@ use std::sync::Arc;
 use tangle_primitives::jobs::{
     DKGTSSKeyRefreshResult, DigitalSignatureScheme, JobId, JobResult, JobType,
 };
-use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use tangle_primitives::roles::RoleType;
 use tokio::sync::mpsc::UnboundedReceiver;
 
-pub struct DfnsCGGMP21KeyRefreshProtocol<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>,
-) -> DfnsCGGMP21KeyRefreshProtocol<B, BE, KBE, C, N>
-where
+pub(crate) async fn create_next_job<
     B: Block,
-    BE: Backend<B>,
+    BE: Backend<B> + 'static,
     C: ClientWithApi<B, BE>,
     KBE: KeystoreBackend,
     N: Network,
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    DfnsCGGMP21KeyRefreshProtocol {
-        client,
-        network,
-        key_store,
-        logger,
-        account_id,
-    }
-}
-
-#[async_trait]
-impl<
-        B: Block,
-        BE: Backend<B> + 'static,
-        C: ClientWithApi<B, BE>,
-        KBE: KeystoreBackend,
-        N: Network,
-    > GadgetProtocol<B, BE, C> for DfnsCGGMP21KeyRefreshProtocol<B, BE, KBE, C, N>
+>(
+    config: &crate::DfnsKeyRefreshProtocol<B, BE, C, N, KBE>,
+    job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
+) -> Result<DfnsCGGMP21KeyRefreshExtraParams, gadget_common::Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    fn name(&self) -> String {
-        "dfns-cggmp21-key-refresh".to_string()
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<B>,
-    ) -> Result<<Self as AsyncProtocol>::AdditionalParams, gadget_common::Error> {
-        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::DKGTSSPhaseThree(p3_job) = job.job_type else {
-            panic!("Should be valid type")
-        };
-
-        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 user_id_to_account_id_mapping = Arc::new(
-            participants
-                .clone()
-                .into_iter()
-                .enumerate()
-                .map(|r| (r.0 as UserID, r.1))
-                .collect(),
-        );
-
-        let key = self
-            .key_store
-            .get_job_result(p3_job.phase_one_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:?}"),
-            })?;
-
-        let phase_one_id_bytes = p3_job.phase_one_id.to_be_bytes();
-        let pregenerated_primes_key =
-            keccak_256(&[&b"dfns-cggmp21-keygen-primes"[..], &phase_one_id_bytes[..]].concat());
-        let pregenerated_primes = self
-            .key_store
-            .get(&pregenerated_primes_key)
-            .await?
-            .ok_or_else(|| gadget_common::Error::ClientError {
-                err: format!(
-                    "No pregenerated primes found for job ID: {}",
-                    p3_job.phase_one_id
-                ),
-            })?;
-
-        let params = DfnsCGGMP21KeyRefreshExtraParams {
-            phase_one_id: p3_job.phase_one_id,
-            key,
-            pregenerated_primes,
-            role_type,
-            job_id,
-            user_id_to_account_id_mapping,
-        };
-
-        Ok(params)
-    }
-
-    async fn process_block_import_notification(
-        &self,
-        _notification: BlockImportNotification<B>,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) -> Result<(), gadget_common::Error> {
-        Ok(())
-    }
-
-    async fn process_error(
-        &self,
-        error: gadget_common::Error,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) {
-        log::error!(target: "dfns_cggmp1", "Error: {error:?}");
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
-
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(
-            role,
-            RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
-        )
-    }
-
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, JobType::DKGTSSPhaseThree(_))
-    }
-
-    fn client(&self) -> JobsClient<B, BE, C> {
-        self.client.clone()
-    }
-
-    fn logger(&self) -> DebugLogger {
-        self.logger.clone()
-    }
-
-    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,
-        }
-    }
+    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::DKGTSSPhaseThree(p3_job) = job.job_type else {
+        panic!("Should be valid type")
+    };
+
+    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 user_id_to_account_id_mapping = Arc::new(
+        participants
+            .clone()
+            .into_iter()
+            .enumerate()
+            .map(|r| (r.0 as UserID, r.1))
+            .collect(),
+    );
+
+    let key = config
+        .key_store
+        .get_job_result(p3_job.phase_one_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:?}"),
+        })?;
+
+    let phase_one_id_bytes = p3_job.phase_one_id.to_be_bytes();
+    let pregenerated_primes_key =
+        keccak_256(&[&b"dfns-cggmp21-keygen-primes"[..], &phase_one_id_bytes[..]].concat());
+    let pregenerated_primes = config
+        .key_store
+        .get(&pregenerated_primes_key)
+        .await?
+        .ok_or_else(|| gadget_common::Error::ClientError {
+            err: format!(
+                "No pregenerated primes found for job ID: {}",
+                p3_job.phase_one_id
+            ),
+        })?;
+
+    let params = DfnsCGGMP21KeyRefreshExtraParams {
+        phase_one_id: p3_job.phase_one_id,
+        key,
+        pregenerated_primes,
+        role_type,
+        job_id,
+        user_id_to_account_id_mapping,
+    };
+
+    Ok(params)
 }
 
+#[derive(Clone)]
 pub struct DfnsCGGMP21KeyRefreshExtraParams {
     job_id: JobId,
     phase_one_id: JobId,
@@ -190,126 +102,122 @@ pub struct DfnsCGGMP21KeyRefreshExtraParams {
     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 DfnsCGGMP21KeyRefreshProtocol<B, BE, KBE, C, N>
+pub async fn generate_protocol_from<
+    B: Block,
+    BE: Backend<B> + 'static,
+    C: ClientWithApi<B, BE>,
+    KBE: KeystoreBackend,
+    N: Network,
+>(
+    config: &crate::DfnsKeyRefreshProtocol<B, BE, C, N, KBE>,
+    associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
+    associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
+    associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
+    associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
+    protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
+    additional_params: DfnsCGGMP21KeyRefreshExtraParams,
+) -> Result<BuiltExecutableJobWrapper, JobError>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    type AdditionalParams = DfnsCGGMP21KeyRefreshExtraParams;
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
-        protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
-        additional_params: Self::AdditionalParams,
-    ) -> Result<BuiltExecutableJobWrapper, JobError> {
-        let key_store = 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 (mapping, key, pregenerated_primes) = (
-            additional_params.user_id_to_account_id_mapping,
-            additional_params.key,
-            additional_params.pregenerated_primes,
-        );
-        let i = key.i;
-        let n = key.public_shares.len() as u16;
-        let t = key.vss_setup.as_ref().map(|x| x.min_signers).unwrap_or(n);
-
-        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 KeyRefresh 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-keyrefresh");
-                let eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
-                let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes);
-
-                let (
-                    key_refresh_tx_to_outbound,
-                    key_refresh_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 key_store = config.key_store.clone();
+    let protocol_output = Arc::new(tokio::sync::Mutex::new(None));
+    let protocol_output_clone = protocol_output.clone();
+    let client = config.get_jobs_client();
+    let id = config.account_id;
+    let logger = config.logger.clone();
+    let network = config.network.clone();
+
+    let (mapping, key, pregenerated_primes) = (
+        additional_params.user_id_to_account_id_mapping,
+        additional_params.key,
+        additional_params.pregenerated_primes,
+    );
+    let i = key.i;
+    let n = key.public_shares.len() as u16;
+    let t = key.vss_setup.as_ref().map(|x| x.min_signers).unwrap_or(n);
+
+    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 KeyRefresh 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-keyrefresh");
+            let eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
+            let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes);
+
+            let (
+                key_refresh_tx_to_outbound,
+                key_refresh_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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
+            let delivery = (key_refresh_rx_async_proto, key_refresh_tx_to_outbound);
+            let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
+            let aux_info = dfns_cggmp21::aux_info_gen(eid, i, n, pregenerated_primes)
+                .set_progress_tracer(&mut tracer)
+                .start(&mut rng, party)
+                .await
+                .map_err(|err| JobError {
+                    reason: format!("KeyRefresh protocol error: {err:?}"),
+                })?;
 
-                let mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
-                let delivery = (key_refresh_rx_async_proto, key_refresh_tx_to_outbound);
-                let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
-                let aux_info = dfns_cggmp21::aux_info_gen(eid, i, n, pregenerated_primes)
-                    .set_progress_tracer(&mut tracer)
-                    .start(&mut rng, party)
+            let key = key.update_aux(aux_info).map_err(|err| JobError {
+                reason: format!("KeyRefresh protocol error: {err:?}"),
+            })?;
+            let perf_report = tracer.get_report().map_err(|err| JobError {
+                reason: format!("KeyRefresh protocol error: {err:?}"),
+            })?;
+            logger.trace(format!("KeyRefresh protocol report: {perf_report}"));
+
+            logger.debug("Finished AsyncProtocol - KeyRefresh");
+
+            let job_result = JobResult::DKGPhaseThree(DKGTSSKeyRefreshResult {
+                signature_scheme: DigitalSignatureScheme::Ecdsa,
+            });
+            *protocol_output.lock().await = Some((key, job_result));
+            Ok(())
+        })
+        .post(async move {
+            // TODO: handle protocol blames
+            if let Some((local_key, job_result)) = protocol_output_clone.lock().await.take() {
+                // Update the local key share, of the phase one.
+                key_store
+                    .set_job_result(additional_params.phase_one_id, local_key)
                     .await
                     .map_err(|err| JobError {
-                        reason: format!("KeyRefresh protocol error: {err:?}"),
+                        reason: format!("Failed to store key: {err:?}"),
                     })?;
 
-                let key = key.update_aux(aux_info).map_err(|err| JobError {
-                    reason: format!("KeyRefresh protocol error: {err:?}"),
-                })?;
-                let perf_report = tracer.get_report().map_err(|err| JobError {
-                    reason: format!("KeyRefresh protocol error: {err:?}"),
-                })?;
-                logger.trace(format!("KeyRefresh protocol report: {perf_report}"));
-
-                logger.debug("Finished AsyncProtocol - KeyRefresh");
-
-                let job_result = JobResult::DKGPhaseThree(DKGTSSKeyRefreshResult {
-                    signature_scheme: DigitalSignatureScheme::Ecdsa,
-                });
-                *protocol_output.lock().await = Some((key, job_result));
-                Ok(())
-            })
-            .post(async move {
-                // TODO: handle protocol blames
-                if let Some((local_key, job_result)) = protocol_output_clone.lock().await.take() {
-                    // Update the local key share, of the phase one.
-                    key_store
-                        .set_job_result(additional_params.phase_one_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:?}"),
-                        })?;
-                }
+                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())
-    }
+            Ok(())
+        })
+        .build())
 }
diff --git a/protocols/dfns-cggmp21/src/protocols/key_rotate.rs b/protocols/dfns-cggmp21/src/protocols/key_rotate.rs
index c57f31e7f..e631bf117 100644
--- a/protocols/dfns-cggmp21/src/protocols/key_rotate.rs
+++ b/protocols/dfns-cggmp21/src/protocols/key_rotate.rs
@@ -1,17 +1,13 @@
-use async_trait::async_trait;
 use dfns_cggmp21::supported_curves::Secp256k1;
 use dfns_cggmp21::KeyShare;
-use gadget_common::client::{
-    AccountId, ClientWithApi, GadgetJobType, JobsApiForGadget, JobsClient,
-};
-use gadget_common::debug_logger::DebugLogger;
+use gadget_common::client::{AccountId, ClientWithApi, JobsApiForGadget};
 use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
 use gadget_common::gadget::network::Network;
 use gadget_common::gadget::work_manager::WorkManager;
-use gadget_common::gadget::{GadgetProtocol, JobInitMetadata, WorkManagerConfig};
-use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
-use gadget_common::protocol::AsyncProtocol;
-use gadget_common::{Block, BlockImportNotification};
+use gadget_common::gadget::JobInitMetadata;
+use gadget_common::keystore::KeystoreBackend;
+use gadget_common::prelude::FullProtocolConfig;
+use gadget_common::Block;
 use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
 use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
 use rand::SeedableRng;
@@ -23,172 +19,87 @@ use std::sync::Arc;
 use tangle_primitives::jobs::{
     DKGTSSKeyRotationResult, DigitalSignatureScheme, JobId, JobResult, JobType,
 };
-use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use tangle_primitives::roles::RoleType;
 use tokio::sync::mpsc::UnboundedReceiver;
 
-pub struct DfnsCGGMP21KeyRotateProtocol<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>,
-) -> DfnsCGGMP21KeyRotateProtocol<B, BE, KBE, C, N>
-where
+pub async fn create_next_job<
     B: Block,
-    BE: Backend<B>,
+    BE: Backend<B> + 'static,
     C: ClientWithApi<B, BE>,
     KBE: KeystoreBackend,
     N: Network,
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    DfnsCGGMP21KeyRotateProtocol {
-        client,
-        network,
-        key_store,
-        logger,
-        account_id,
-    }
-}
-
-#[async_trait]
-impl<
-        B: Block,
-        BE: Backend<B> + 'static,
-        C: ClientWithApi<B, BE>,
-        KBE: KeystoreBackend,
-        N: Network,
-    > GadgetProtocol<B, BE, C> for DfnsCGGMP21KeyRotateProtocol<B, BE, KBE, C, N>
+>(
+    config: &crate::DfnsKeyRotateProtocol<B, BE, C, N, KBE>,
+    job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
+) -> Result<DfnsCGGMP21KeyRotateExtraParams, gadget_common::Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    fn name(&self) -> String {
-        "dfns-cggmp21-key-rotate".to_string()
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<B>,
-    ) -> Result<<Self as AsyncProtocol>::AdditionalParams, gadget_common::Error> {
-        let job_id = job.job_id;
-
-        let JobType::DKGTSSPhaseFour(p4_job) = job.job_type else {
-            panic!("Should be valid type")
-        };
-        let phase_one_id = p4_job.phase_one_id;
-        let new_phase_one_id = p4_job.new_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 t = phase1_job.get_threshold().expect("Should exist") as u16;
-
-        let seed =
-            keccak_256(&[&job_id.to_be_bytes()[..], &job.retry_id.to_be_bytes()[..]].concat());
-        let mut rng = rand_chacha::ChaChaRng::from_seed(seed);
-
-        let (i, signers, mapping) =
-            super::util::choose_signers(&mut rng, &self.account_id, &participants, t)?;
-
-        let new_phase_one_result = self
-            .client
-            .query_job_result(job.at, job.role_type, new_phase_one_id)
-            .await?
-            .ok_or_else(|| gadget_common::Error::ClientError {
-                err: format!("No key found for job ID: {new_phase_one_id}"),
-            })?;
-
-        let new_key = match new_phase_one_result.result {
-            JobResult::DKGPhaseOne(r) => r.key,
-            _ => {
-                return Err(gadget_common::Error::ClientError {
-                    err: format!("Wrong job result type for job ID: {new_phase_one_id}"),
-                })
-            }
-        };
-
-        let key = self
-            .key_store
-            .get_job_result(phase_one_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:?}"),
-            })?;
-
-        let user_id_to_account_id_mapping = Arc::new(mapping);
-
-        let params = DfnsCGGMP21KeyRotateExtraParams {
-            i,
-            t,
-            signers,
-            job_id,
-            phase_one_id,
-            new_phase_one_id,
-            role_type: job.role_type,
-            key,
-            new_key: new_key.try_into().unwrap(),
-            user_id_to_account_id_mapping,
-        };
-        Ok(params)
-    }
-
-    async fn process_block_import_notification(
-        &self,
-        _notification: BlockImportNotification<B>,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) -> Result<(), gadget_common::Error> {
-        Ok(())
-    }
-
-    async fn process_error(
-        &self,
-        error: gadget_common::Error,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) {
-        log::error!(target: "gadget", "Error: {error:?}");
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
-
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(
-            role,
-            RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
-        )
-    }
-
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, JobType::DKGTSSPhaseFour(_))
-    }
-
-    fn client(&self) -> JobsClient<B, BE, C> {
-        self.client.clone()
-    }
-
-    fn logger(&self) -> DebugLogger {
-        self.logger.clone()
-    }
-
-    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,
+    let job_id = job.job_id;
+
+    let JobType::DKGTSSPhaseFour(p4_job) = job.job_type else {
+        panic!("Should be valid type")
+    };
+    let phase_one_id = p4_job.phase_one_id;
+    let new_phase_one_id = p4_job.new_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 t = phase1_job.get_threshold().expect("Should exist") as u16;
+
+    let seed = keccak_256(&[&job_id.to_be_bytes()[..], &job.retry_id.to_be_bytes()[..]].concat());
+    let mut rng = rand_chacha::ChaChaRng::from_seed(seed);
+
+    let (i, signers, mapping) =
+        super::util::choose_signers(&mut rng, &config.account_id, &participants, t)?;
+
+    let new_phase_one_result = config
+        .get_jobs_client()
+        .query_job_result(job.at, job.role_type, new_phase_one_id)
+        .await?
+        .ok_or_else(|| gadget_common::Error::ClientError {
+            err: format!("No key found for job ID: {new_phase_one_id}"),
+        })?;
+
+    let new_key = match new_phase_one_result.result {
+        JobResult::DKGPhaseOne(r) => r.key,
+        _ => {
+            return Err(gadget_common::Error::ClientError {
+                err: format!("Wrong job result type for job ID: {new_phase_one_id}"),
+            })
         }
-    }
+    };
+
+    let key = config
+        .key_store
+        .get_job_result(phase_one_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:?}"),
+        })?;
+
+    let user_id_to_account_id_mapping = Arc::new(mapping);
+
+    let params = DfnsCGGMP21KeyRotateExtraParams {
+        i,
+        t,
+        signers,
+        job_id,
+        phase_one_id,
+        new_phase_one_id,
+        role_type: job.role_type,
+        key,
+        new_key: new_key.try_into().unwrap(),
+        user_id_to_account_id_mapping,
+    };
+    Ok(params)
 }
 
+#[derive(Clone)]
 pub struct DfnsCGGMP21KeyRotateExtraParams {
     i: u16,
     t: u16,
@@ -202,162 +113,157 @@ pub struct DfnsCGGMP21KeyRotateExtraParams {
     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 DfnsCGGMP21KeyRotateProtocol<B, BE, KBE, C, N>
+pub async fn generate_protocol_from<
+    B: Block,
+    BE: Backend<B> + 'static,
+    C: ClientWithApi<B, BE>,
+    KBE: KeystoreBackend,
+    N: Network,
+>(
+    config: &crate::DfnsKeyRotateProtocol<B, BE, C, N, KBE>,
+    associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
+    associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
+    associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
+    associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
+    protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
+    additional_params: DfnsCGGMP21KeyRotateExtraParams,
+) -> Result<BuiltExecutableJobWrapper, JobError>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    type AdditionalParams = DfnsCGGMP21KeyRotateExtraParams;
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager 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 phase_one_id = additional_params.phase_one_id;
-        let network = self.network.clone();
-
-        let (i, signers, t, new_phase_one_id, key, new_key, mapping) = (
-            additional_params.i,
-            additional_params.signers,
-            additional_params.t,
-            additional_params.new_phase_one_id,
-            additional_params.key,
-            additional_params.new_key.clone(),
-            additional_params.user_id_to_account_id_mapping.clone(),
-        );
-
-        let public_key_bytes = key.shared_public_key().to_bytes(true).to_vec();
-        let new_key2 = new_key.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 Key Rotation 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-key-rotate");
-                let eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
-                let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes);
-                let (
-                    key_rotate_tx_to_outbound,
-                    key_rotate_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 = (key_rotate_rx_async_proto, key_rotate_tx_to_outbound);
-                let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
-                let data_hash = keccak_256(&new_key);
-                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!("Key Rotation protocol error: {err:?}"),
-                    })?;
-
-                // Normalize the signature
-                let signature = signature.normalize_s();
-                logger.debug("Finished AsyncProtocol - Key Rotation");
-                *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(&new_key2);
-                        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;
-                            }
+    let debug_logger_post = config.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 = config.get_jobs_client();
+    let id = config.account_id;
+    let phase_one_id = additional_params.phase_one_id;
+    let network = config.network.clone();
+
+    let (i, signers, t, new_phase_one_id, key, new_key, mapping) = (
+        additional_params.i,
+        additional_params.signers,
+        additional_params.t,
+        additional_params.new_phase_one_id,
+        additional_params.key,
+        additional_params.new_key.clone(),
+        additional_params.user_id_to_account_id_mapping.clone(),
+    );
+
+    let public_key_bytes = key.shared_public_key().to_bytes(true).to_vec();
+    let new_key2 = new_key.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 Key Rotation 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-key-rotate");
+            let eid_bytes = [&job_id_bytes[..], &mix[..]].concat();
+            let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes);
+            let (
+                key_rotate_tx_to_outbound,
+                key_rotate_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 = (key_rotate_rx_async_proto, key_rotate_tx_to_outbound);
+            let party = dfns_cggmp21::round_based::MpcParty::connected(delivery);
+            let data_hash = keccak_256(&new_key);
+            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!("Key Rotation protocol error: {err:?}"),
+                })?;
+
+            // Normalize the signature
+            let signature = signature.normalize_s();
+            logger.debug("Finished AsyncProtocol - Key Rotation");
+            *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(&new_key2);
+                    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;
                         }
                     }
-                    // Add 27 to the recovery ID
-                    signature_bytes[64] = v + 27;
-
-                    let job_result = JobResult::DKGPhaseFour(DKGTSSKeyRotationResult {
-                        signature_scheme: DigitalSignatureScheme::Ecdsa,
-                        signature: signature_bytes.to_vec().try_into().unwrap(),
-                        phase_one_id,
-                        new_phase_one_id,
-                        new_key: new_key2.try_into().unwrap(),
-                        key: public_key_bytes.try_into().unwrap(),
-                    });
-
-                    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:?}"),
-                        })?;
                 }
+                // Add 27 to the recovery ID
+                signature_bytes[64] = v + 27;
+
+                let job_result = JobResult::DKGPhaseFour(DKGTSSKeyRotationResult {
+                    signature_scheme: DigitalSignatureScheme::Ecdsa,
+                    signature: signature_bytes.to_vec().try_into().unwrap(),
+                    phase_one_id,
+                    new_phase_one_id,
+                    new_key: new_key2.try_into().unwrap(),
+                    key: public_key_bytes.try_into().unwrap(),
+                });
+
+                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())
-    }
+            Ok(())
+        })
+        .build())
 }
diff --git a/protocols/dfns-cggmp21/src/protocols/keygen.rs b/protocols/dfns-cggmp21/src/protocols/keygen.rs
index 8b5953146..facb75e34 100644
--- a/protocols/dfns-cggmp21/src/protocols/keygen.rs
+++ b/protocols/dfns-cggmp21/src/protocols/keygen.rs
@@ -1,19 +1,18 @@
-use async_trait::async_trait;
 use dfns_cggmp21::supported_curves::Secp256k1;
 use dfns_cggmp21::KeyShare;
 use futures::StreamExt;
 use gadget_common::client::{
-    AccountId, ClientWithApi, GadgetJobResult, GadgetJobType, JobsApiForGadget, JobsClient,
-    MaxKeyLen, MaxParticipants, MaxSignatureLen,
+    AccountId, ClientWithApi, GadgetJobResult, JobsApiForGadget, MaxKeyLen, MaxParticipants,
+    MaxSignatureLen,
 };
 use gadget_common::debug_logger::DebugLogger;
+use gadget_common::full_protocol::FullProtocolConfig;
 use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
 use gadget_common::gadget::network::Network;
 use gadget_common::gadget::work_manager::WorkManager;
-use gadget_common::gadget::{GadgetProtocol, JobInitMetadata, WorkManagerConfig};
+use gadget_common::gadget::JobInitMetadata;
 use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
-use gadget_common::protocol::AsyncProtocol;
-use gadget_common::{Block, BlockImportNotification};
+use gadget_common::Block;
 use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
 use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
 use itertools::Itertools;
@@ -27,145 +26,61 @@ use std::sync::Arc;
 use tangle_primitives::jobs::{
     DKGTSSKeySubmissionResult, DigitalSignatureScheme, JobId, JobResult, JobType,
 };
-use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use tangle_primitives::roles::RoleType;
 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
+pub async fn create_next_job<
     B: Block,
-    BE: Backend<B>,
-    C: ClientWithApi<B, BE>,
+    BE: Backend<B> + 'static,
     KBE: KeystoreBackend,
+    C: ClientWithApi<B, BE>,
     N: Network,
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    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,
-    > GadgetProtocol<B, BE, C> for DfnsCGGMP21KeygenProtocol<B, BE, KBE, C, N>
+>(
+    config: &crate::DfnsKeygenProtocol<B, BE, C, N, KBE>,
+    job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
+) -> Result<DfnsCGGMP21KeygenExtraParams, gadget_common::Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    fn name(&self) -> String {
-        "dfns-cggmp21-keygen".to_string()
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<B>,
-    ) -> Result<<Self as AsyncProtocol>::AdditionalParams, gadget_common::Error> {
-        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)
-    }
-
-    async fn process_block_import_notification(
-        &self,
-        _notification: BlockImportNotification<B>,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) -> Result<(), gadget_common::Error> {
-        Ok(())
-    }
-
-    async fn process_error(
-        &self,
-        error: gadget_common::Error,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) {
-        log::error!(target: "dfns_cggmp1", "Error: {error:?}");
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
+    let job_id = job.job_id;
+    let role_type = job.job_type.get_role_type();
 
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(
-            role,
-            RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
-        )
-    }
+    // 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")
+    };
 
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, JobType::DKGTSSPhaseOne(_))
-    }
+    let participants = p1_job.participants;
+    let threshold = p1_job.threshold;
 
-    fn client(&self) -> JobsClient<B, BE, C> {
-        self.client.clone()
-    }
+    let user_id_to_account_id_mapping = Arc::new(
+        participants
+            .clone()
+            .into_iter()
+            .enumerate()
+            .map(|r| (r.0 as UserID, r.1))
+            .collect(),
+    );
 
-    fn logger(&self) -> DebugLogger {
-        self.logger.clone()
-    }
+    let params = DfnsCGGMP21KeygenExtraParams {
+        i: participants
+            .iter()
+            .position(|p| p == &config.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,
+    };
 
-    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,
-        }
-    }
+    Ok(params)
 }
 
+#[derive(Clone)]
 pub struct DfnsCGGMP21KeygenExtraParams {
     i: u16,
     t: u16,
@@ -175,182 +90,180 @@ pub struct DfnsCGGMP21KeygenExtraParams {
     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>
+pub async fn generate_protocol_from<
+    B: Block,
+    BE: Backend<B> + 'static,
+    KBE: KeystoreBackend,
+    C: ClientWithApi<B, BE>,
+    N: Network,
+>(
+    config: &crate::DfnsKeygenProtocol<B, BE, C, N, KBE>,
+    associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
+    associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
+    associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
+    associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
+    protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
+    additional_params: DfnsCGGMP21KeygenExtraParams,
+) -> Result<BuiltExecutableJobWrapper, JobError>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    type AdditionalParams = DfnsCGGMP21KeygenExtraParams;
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
-        protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
-        additional_params: Self::AdditionalParams,
-    ) -> Result<BuiltExecutableJobWrapper, JobError> {
-        let key_store = self.key_store.clone();
-        let key_store2 = 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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
-                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_progress_tracer(&mut tracer)
-                    .set_threshold(t)
-                    .start(&mut rng, party)
-                    .await
-                    .map_err(|err| JobError {
-                        reason: format!("Keygen protocol error: {err:?}"),
-                    })?;
+    let key_store = config.key_store.clone();
+    let key_store2 = config.key_store.clone();
+    let protocol_output = Arc::new(tokio::sync::Mutex::new(None));
+    let protocol_output_clone = protocol_output.clone();
+    let client = config.get_jobs_client();
+    let id = config.account_id;
+    let logger = config.logger.clone();
+    let network = config.network.clone();
+
+    let (i, t, n, mapping) = (
+        additional_params.i,
+        additional_params.t,
+        additional_params.n,
+        additional_params.user_id_to_account_id_mapping,
+    );
 
-                let perf_report = tracer.get_report().map_err(|err| JobError {
+    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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
+            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_progress_tracer(&mut tracer)
+                .set_threshold(t)
+                .start(&mut rng, party)
+                .await
+                .map_err(|err| JobError {
                     reason: format!("Keygen protocol error: {err:?}"),
                 })?;
-                logger.trace(format!("Incomplete Keygen protocol report: {perf_report}"));
-                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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
-
-                let pregenerated_primes_key =
-                    keccak_256(&[&b"dfns-cggmp21-keygen-primes"[..], &job_id_bytes[..]].concat());
-                let pregenerated_primes = dfns_cggmp21::PregeneratedPrimes::generate(&mut rng);
-                key_store2
-                    .set(&pregenerated_primes_key, pregenerated_primes.clone())
-                    .await
-                    .map_err(|err| JobError {
-                        reason: format!("Failed to store pregenerated primes: {err:?}"),
-                    })?;
-                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)
-                    .set_progress_tracer(&mut tracer)
-                    .start(&mut rng, party)
+
+            let perf_report = tracer.get_report().map_err(|err| JobError {
+                reason: format!("Keygen protocol error: {err:?}"),
+            })?;
+            logger.trace(format!("Incomplete Keygen protocol report: {perf_report}"));
+            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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
+
+            let pregenerated_primes_key =
+                keccak_256(&[&b"dfns-cggmp21-keygen-primes"[..], &job_id_bytes[..]].concat());
+            let pregenerated_primes = dfns_cggmp21::PregeneratedPrimes::generate(&mut rng);
+            key_store2
+                .set(&pregenerated_primes_key, pregenerated_primes.clone())
+                .await
+                .map_err(|err| JobError {
+                    reason: format!("Failed to store pregenerated primes: {err:?}"),
+                })?;
+            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)
+                .set_progress_tracer(&mut tracer)
+                .start(&mut rng, party)
+                .await
+                .map_err(|err| JobError {
+                    reason: format!("Aux info protocol error: {err:?}"),
+                })?;
+            let perf_report = tracer.get_report().map_err(|err| JobError {
+                reason: format!("Aux info protocol error: {err:?}"),
+            })?;
+            logger.trace(format!("Aux info protocol report: {perf_report}"));
+            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_store2,
+                &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_job_result(additional_params.job_id, local_key)
                     .await
                     .map_err(|err| JobError {
-                        reason: format!("Aux info protocol error: {err:?}"),
+                        reason: format!("Failed to store key: {err:?}"),
                     })?;
-                let perf_report = tracer.get_report().map_err(|err| JobError {
-                    reason: format!("Aux info protocol error: {err:?}"),
-                })?;
-                logger.trace(format!("Aux info protocol report: {perf_report}"));
-                logger.debug("Finished AsyncProtocol - Aux Info");
 
-                let key_share = dfns_cggmp21::KeyShare::make(incomplete_key_share, aux_info)
+                client
+                    .submit_job_result(
+                        additional_params.role_type,
+                        additional_params.job_id,
+                        job_result,
+                    )
+                    .await
                     .map_err(|err| JobError {
-                        reason: format!("Key share error: {err:?}"),
+                        reason: format!("Failed to submit job result: {err:?}"),
                     })?;
+            }
 
-                logger.debug("Finished AsyncProtocol - Keygen");
-
-                let job_result = handle_public_key_gossip(
-                    key_store2,
-                    &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_job_result(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())
-    }
+            Ok(())
+        })
+        .build())
 }
 
 #[allow(clippy::too_many_arguments)]
diff --git a/protocols/dfns-cggmp21/src/protocols/sign.rs b/protocols/dfns-cggmp21/src/protocols/sign.rs
index 80bab7696..059aa8b5f 100644
--- a/protocols/dfns-cggmp21/src/protocols/sign.rs
+++ b/protocols/dfns-cggmp21/src/protocols/sign.rs
@@ -1,17 +1,12 @@
-use async_trait::async_trait;
 use dfns_cggmp21::supported_curves::Secp256k1;
 use dfns_cggmp21::KeyShare;
-use gadget_common::client::{
-    AccountId, ClientWithApi, GadgetJobType, JobsApiForGadget, JobsClient,
-};
-use gadget_common::debug_logger::DebugLogger;
+use gadget_common::client::{AccountId, ClientWithApi, JobsApiForGadget};
 use gadget_common::gadget::message::{GadgetProtocolMessage, UserID};
-use gadget_common::gadget::network::Network;
 use gadget_common::gadget::work_manager::WorkManager;
-use gadget_common::gadget::{GadgetProtocol, JobInitMetadata, WorkManagerConfig};
-use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
-use gadget_common::protocol::AsyncProtocol;
-use gadget_common::{Block, BlockImportNotification};
+use gadget_common::gadget::JobInitMetadata;
+use gadget_common::keystore::KeystoreBackend;
+use gadget_common::prelude::{FullProtocolConfig, Network};
+use gadget_common::Block;
 use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
 use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
 use rand::SeedableRng;
@@ -23,152 +18,67 @@ use std::sync::Arc;
 use tangle_primitives::jobs::{
     DKGTSSSignatureResult, DigitalSignatureScheme, JobId, JobResult, JobType,
 };
-use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType};
+use tangle_primitives::roles::RoleType;
 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,
-    client: JobsClient<B, BE, C>,
-    network: N,
-    logger: DebugLogger,
-    key_store: ECDSAKeyStore<KBE>,
-) -> DfnsCGGMP21SigningProtocol<B, BE, KBE, C, N>
-where
+pub async fn create_next_job<
     B: Block,
-    BE: Backend<B>,
-    C: ClientWithApi<B, BE>,
+    BE: Backend<B> + 'static,
     KBE: KeystoreBackend,
+    C: ClientWithApi<B, BE>,
     N: Network,
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    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,
-    > GadgetProtocol<B, BE, C> for DfnsCGGMP21SigningProtocol<B, BE, KBE, C, N>
+>(
+    config: &crate::DfnsSigningProtocol<B, BE, C, N, KBE>,
+    job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
+) -> Result<DfnsCGGMP21SigningExtraParams, gadget_common::Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    fn name(&self) -> String {
-        "dfns-cggmp21-signing".to_string()
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<B>,
-    ) -> 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.to_vec();
-        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 t = phase1_job.get_threshold().expect("Should exist") as u16;
-
-        let seed =
-            keccak_256(&[&job_id.to_be_bytes()[..], &job.retry_id.to_be_bytes()[..]].concat());
-        let mut rng = rand_chacha::ChaChaRng::from_seed(seed);
-
-        let (i, signers, mapping) =
-            super::util::choose_signers(&mut rng, &self.account_id, &participants, t)?;
-        let key = self
-            .key_store
-            .get_job_result(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:?}"),
-            })?;
-
-        let user_id_to_account_id_mapping = Arc::new(mapping);
-
-        let params = DfnsCGGMP21SigningExtraParams {
-            i,
-            t,
-            signers,
-            job_id,
-            role_type: job.role_type,
-            key,
-            input_data_to_sign,
-            user_id_to_account_id_mapping,
-        };
-        Ok(params)
-    }
-
-    async fn process_block_import_notification(
-        &self,
-        _notification: BlockImportNotification<B>,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) -> Result<(), gadget_common::Error> {
-        Ok(())
-    }
-
-    async fn process_error(
-        &self,
-        error: gadget_common::Error,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) {
-        log::error!(target: "gadget", "Error: {error:?}");
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
-
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(
-            role,
-            RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1)
-        )
-    }
-
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, JobType::DKGTSSPhaseTwo(_))
-    }
-
-    fn client(&self) -> JobsClient<B, BE, C> {
-        self.client.clone()
-    }
-
-    fn logger(&self) -> DebugLogger {
-        self.logger.clone()
-    }
-
-    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,
-        }
-    }
+    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.to_vec();
+    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 t = phase1_job.get_threshold().expect("Should exist") as u16;
+
+    let seed = keccak_256(&[&job_id.to_be_bytes()[..], &job.retry_id.to_be_bytes()[..]].concat());
+    let mut rng = rand_chacha::ChaChaRng::from_seed(seed);
+
+    let (i, signers, mapping) =
+        super::util::choose_signers(&mut rng, &config.account_id, &participants, t)?;
+    let key = config
+        .key_store
+        .get_job_result(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:?}"),
+        })?;
+
+    let user_id_to_account_id_mapping = Arc::new(mapping);
+
+    let params = DfnsCGGMP21SigningExtraParams {
+        i,
+        t,
+        signers,
+        job_id,
+        role_type: job.role_type,
+        key,
+        input_data_to_sign,
+        user_id_to_account_id_mapping,
+    };
+    Ok(params)
 }
 
+#[derive(Clone)]
 pub struct DfnsCGGMP21SigningExtraParams {
     i: u16,
     t: u16,
@@ -180,163 +90,158 @@ pub struct DfnsCGGMP21SigningExtraParams {
     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>
+pub async fn generate_protocol_from<
+    B: Block,
+    BE: Backend<B> + 'static,
+    KBE: KeystoreBackend,
+    C: ClientWithApi<B, BE>,
+    N: Network,
+>(
+    config: &crate::DfnsSigningProtocol<B, BE, C, N, KBE>,
+    associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
+    associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
+    associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
+    associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
+    protocol_message_channel: UnboundedReceiver<GadgetProtocolMessage>,
+    additional_params: DfnsCGGMP21SigningExtraParams,
+) -> Result<BuiltExecutableJobWrapper, JobError>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
-    type AdditionalParams = DfnsCGGMP21SigningExtraParams;
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager 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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
-                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)
-                    .set_progress_tracer(&mut tracer)
-                    .sign(&mut rng, party, data_to_sign)
-                    .await
-                    .map_err(|err| JobError {
-                        reason: format!("Signing protocol error: {err:?}"),
-                    })?;
-
-                let perf_report = tracer.get_report().map_err(|err| JobError {
+    let debug_logger_post = config.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 = config.get_jobs_client();
+    let id = config.account_id;
+    let network = config.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 mut tracer = dfns_cggmp21::progress::PerfProfiler::new();
+            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)
+                .set_progress_tracer(&mut tracer)
+                .sign(&mut rng, party, data_to_sign)
+                .await
+                .map_err(|err| JobError {
                     reason: format!("Signing protocol error: {err:?}"),
                 })?;
-                logger.trace(format!("Signing protocol report: {perf_report}"));
-                // 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;
-                            }
+
+            let perf_report = tracer.get_report().map_err(|err| JobError {
+                reason: format!("Signing protocol error: {err:?}"),
+            })?;
+            logger.trace(format!("Signing protocol report: {perf_report}"));
+            // 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_scheme: DigitalSignatureScheme::Ecdsa,
-                        data: additional_params.input_data_to_sign.try_into().unwrap(),
-                        signature: signature_bytes.to_vec().try_into().unwrap(),
-                        verifying_key: public_key_bytes.try_into().unwrap(),
-                    });
-
-                    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:?}"),
-                        })?;
                 }
+                signature_bytes[64] = v + 27;
+
+                let job_result = JobResult::DKGPhaseTwo(DKGTSSSignatureResult {
+                    signature_scheme: DigitalSignatureScheme::Ecdsa,
+                    data: additional_params.input_data_to_sign.try_into().unwrap(),
+                    signature: signature_bytes.to_vec().try_into().unwrap(),
+                    verifying_key: public_key_bytes.try_into().unwrap(),
+                });
+
+                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())
-    }
+            Ok(())
+        })
+        .build())
 }
diff --git a/protocols/stub/README.md b/protocols/stub/README.md
index 894ff4ff9..9541156d8 100644
--- a/protocols/stub/README.md
+++ b/protocols/stub/README.md
@@ -9,10 +9,10 @@ tangle-primitives = { workspace = true }
 protocol-macros = { workspace = true }
 ```
 
-#### Step 2: Create a Config struct as such:
+#### Step 2: Create a struct representing the protocol as such:
 ```rust
 #[protocol]
-pub struct StubConfig<
+pub struct StubProtocol<
     B: Block,
     BE: Backend<B> + 'static,
     C: ClientWithApi<B, BE>,
@@ -31,10 +31,10 @@ pub struct StubConfig<
 }
 ```
 
-#### Step 2: Implement `FullProtocolConfig` for `StubConfig`
+#### Step 2: Implement `FullProtocolConfig` for `StubProtocol`
 
 #### Step 3: Running the protocol
-At the bottom of lib.rs, add the macro: `generate_setup_and_run_command!(StubConfig, StubConfig, ...)`. For real applications, you will want to add all the protocol configs you wish to run concurrently.
+At the bottom of lib.rs, add the macro: `generate_setup_and_run_command!(StubProtocol, StubProtocol, ...)`. For real applications, you will want to add all the protocol configs you wish to run concurrently.
 
 #### Step 4: Testing the protocol:
-After generating the macro above, run this macro with appropriate `T` and `N` values: `test_utils::generate_signing_and_keygen_tss_tests!(T, N, ThresholdSignatureRoleType::StubProtocol)`
\ No newline at end of file
+After generating the macro above, run this macro with appropriate `T`, `N`, and `K` values: `test_utils::generate_signing_and_keygen_tss_tests!(T, N, K, ThresholdSignatureRoleType::StubProtocol)`. Note: the `K` value is the number of expected protocols to run concurrently. It should match the number of items in the `generate_setup_and_run_command!` macro. 
\ No newline at end of file
diff --git a/protocols/stub/src/lib.rs b/protocols/stub/src/lib.rs
index 62cfde5c7..56881da2f 100644
--- a/protocols/stub/src/lib.rs
+++ b/protocols/stub/src/lib.rs
@@ -1,8 +1,10 @@
+use gadget_common::full_protocol::SharedOptional;
 use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend};
 use gadget_common::prelude::*;
+use gadget_common::ProtocolWorkManager;
 
 #[protocol]
-pub struct StubConfig<
+pub struct StubProtocol<
     B: Block,
     BE: Backend<B> + 'static,
     C: ClientWithApi<B, BE>,
@@ -27,7 +29,7 @@ impl<
         C: ClientWithApi<B, BE>,
         N: Network,
         KBE: KeystoreBackend,
-    > FullProtocolConfig for StubConfig<B, BE, C, N, KBE>
+    > FullProtocolConfig for StubProtocol<B, BE, C, N, KBE>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
 {
@@ -70,28 +72,14 @@ where
         Ok(JobBuilder::new().protocol(async move { Ok(()) }).build())
     }
 
-    async fn initialize_network_and_protocol(
-        &self,
-        jobs_client: JobsClient<Self::Block, Self::Backend, Self::Client>,
-    ) -> Result<(), Error> {
-        self.jobs_client.lock().replace(jobs_client);
-        Ok(())
-    }
-
-    async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
-        self.network.next_message().await
-    }
-
-    async fn send_message(
-        &self,
-        message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
-    ) -> Result<(), Error> {
-        self.network.send_message(message).await
+    fn network(&self) -> &Self::Network {
+        &self.network
     }
 
     async fn create_next_job(
         &self,
         _job: JobInitMetadata<Self::Block>,
+        _work_manager: &ProtocolWorkManager<WorkManager>,
     ) -> Result<Self::AsyncProtocolParameters, Error> {
         Ok(())
     }
@@ -112,11 +100,8 @@ where
         false
     }
 
-    fn jobs_client(&self) -> JobsClient<Self::Block, Self::Backend, Self::Client> {
-        self.jobs_client
-            .lock()
-            .clone()
-            .expect("Should be initialized")
+    fn jobs_client(&self) -> &SharedOptional<JobsClient<Self::Block, Self::Backend, Self::Client>> {
+        &self.jobs_client
     }
 
     fn pallet_tx(&self) -> Arc<dyn PalletSubmitter> {
@@ -136,7 +121,7 @@ where
 // For example, you may supply both a KeygenConfig and a SigningConfig to generate a main entry point that runs both protocols concurrently.
 // In the below example, the generated run command will run two duplicated protocols concurrently. In a real example, you would want to supply
 // The config type for each protocol you want to run in tandem.
-generate_setup_and_run_command!(StubConfig, StubConfig);
+generate_setup_and_run_command!(StubProtocol, StubProtocol);
 
 // An Example usage of generating signing and keygen tests
 // Note: since the StubProtocol does not submit any JobResults, the wait_for_job_completion function will not work as expected. This is just an example of how to use the macro.
diff --git a/protocols/zk-saas/Cargo.toml b/protocols/zk-saas/Cargo.toml
index cf8eecd1e..c489a7f8e 100644
--- a/protocols/zk-saas/Cargo.toml
+++ b/protocols/zk-saas/Cargo.toml
@@ -30,6 +30,7 @@ parking_lot = { workspace = true }
 log = { workspace = true }
 bytes = { workspace = true, features = ["serde"] }
 fflonk = { git = "https://github.com/w3f/fflonk", features = ["std"] }
+futures = { workspace = true }
 
 
 sp-runtime = { workspace = true }
diff --git a/protocols/zk-saas/src/lib.rs b/protocols/zk-saas/src/lib.rs
index ea501117a..efa52ef42 100644
--- a/protocols/zk-saas/src/lib.rs
+++ b/protocols/zk-saas/src/lib.rs
@@ -1,87 +1,37 @@
 use crate::network::ZkNetworkService;
-use crate::protocol::ZkProtocol;
+use crate::protocol::ZkJobAdditionalParams;
 use async_trait::async_trait;
 use gadget_common::client::{
     AccountId, ClientWithApi, JobsApiForGadget, JobsClient, PalletSubmitter,
 };
-use gadget_common::config::NetworkAndProtocolSetup;
 use gadget_common::debug_logger::DebugLogger;
-use gadget_common::Error;
+use gadget_common::full_protocol::SharedOptional;
+use gadget_common::prelude::*;
+use gadget_common::{generate_protocol, Error};
 use mpc_net::prod::RustlsCertificate;
+use network::ZkProtocolNetworkConfig;
 use protocol_macros::protocol;
 use sc_client_api::Backend;
 use sp_api::ProvideRuntimeApi;
 use sp_runtime::traits::Block;
-use std::net::SocketAddr;
 use std::sync::Arc;
+use tangle_primitives::roles::ZeroKnowledgeRoleType;
 use tokio_rustls::rustls::{Certificate, PrivateKey, RootCertStore};
 
 pub mod network;
 pub mod protocol;
 
-#[protocol]
-pub struct ZkGadgetConfig<B: Block, C: ClientWithApi<B, BE>, BE: Backend<B>>
-where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    pub king_bind_addr: Option<SocketAddr>,
-    pub client_only_king_addr: Option<SocketAddr>,
-    pub public_identity_der: Vec<u8>,
-    pub private_identity_der: Vec<u8>,
-    pub client_only_king_public_identity_der: Option<Vec<u8>>,
-    pub account_id: AccountId,
-    pub pallet_tx: Arc<dyn PalletSubmitter>,
-    pub client: C,
-    pub logger: DebugLogger,
-    pub _pd: std::marker::PhantomData<(B, C, BE)>,
-}
-
-#[async_trait]
-impl<B: Block, C: ClientWithApi<B, BE>, BE: Backend<B>> NetworkAndProtocolSetup
-    for ZkGadgetConfig<B, C, BE>
-where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
-    type Network = ZkNetworkService;
-    type Protocol = ZkProtocol<B, C, BE>;
-    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), Error> {
-        let network = create_zk_network(self).await?;
-        let zk_protocol = ZkProtocol {
-            client: jobs_client,
-            account_id: self.account_id,
-            network: network.clone(),
-            logger: self.logger.clone(),
-        };
-
-        Ok((network, zk_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()
-    }
-}
-
-pub async fn create_zk_network<B: Block, C: ClientWithApi<B, BE>, BE: Backend<B>>(
-    config: &ZkGadgetConfig<B, C, BE>,
-) -> Result<ZkNetworkService, Error>
-where
-    <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
-{
+generate_protocol!(
+    "ZK-Protocol",
+    ZkProtocol,
+    ZkJobAdditionalParams,
+    protocol::generate_protocol_from,
+    protocol::create_next_job,
+    GadgetJobType::ZkSaaSPhaseTwo(_),
+    RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16)
+);
+
+pub async fn create_zk_network(config: ZkProtocolNetworkConfig) -> Result<ZkNetworkService, Error> {
     let our_identity = RustlsCertificate {
         cert: Certificate(config.public_identity_der.clone()),
         private_key: PrivateKey(config.private_identity_der.clone()),
@@ -109,3 +59,5 @@ where
         ZkNetworkService::new_client(king_addr, config.account_id, our_identity, king_certs).await
     }
 }
+
+generate_setup_and_run_command!(ZkProtocol);
diff --git a/protocols/zk-saas/src/network/mod.rs b/protocols/zk-saas/src/network/mod.rs
index 327ef071e..636c08cd7 100644
--- a/protocols/zk-saas/src/network/mod.rs
+++ b/protocols/zk-saas/src/network/mod.rs
@@ -529,6 +529,10 @@ impl Network for ZkNetworkService {
         }
     }
 
+    fn greatest_authority_id(&self) -> Option<AccountId> {
+        self.king_id()
+    }
+
     async fn run(&self) -> Result<(), Error> {
         match self {
             Self::King {
@@ -568,3 +572,13 @@ impl Network for ZkNetworkService {
         }
     }
 }
+
+#[derive(Clone)]
+pub struct ZkProtocolNetworkConfig {
+    pub king_bind_addr: Option<SocketAddr>,
+    pub client_only_king_addr: Option<SocketAddr>,
+    pub public_identity_der: Vec<u8>,
+    pub private_identity_der: Vec<u8>,
+    pub client_only_king_public_identity_der: Option<Vec<u8>>,
+    pub account_id: AccountId,
+}
diff --git a/protocols/zk-saas/src/protocol/mod.rs b/protocols/zk-saas/src/protocol/mod.rs
index 053d76854..b2091785e 100644
--- a/protocols/zk-saas/src/protocol/mod.rs
+++ b/protocols/zk-saas/src/protocol/mod.rs
@@ -1,4 +1,3 @@
-use crate::network::ZkNetworkService;
 use crate::protocol::proto_gen::ZkAsyncProtocolParameters;
 use ark_circom::CircomReduction;
 use ark_crypto_primitives::snark::SNARK;
@@ -9,17 +8,13 @@ use ark_groth16::{Groth16, ProvingKey};
 use ark_poly::{EvaluationDomain, Radix2EvaluationDomain};
 use ark_relations::r1cs::SynthesisError;
 use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
-use async_trait::async_trait;
 use futures_util::TryFutureExt;
-use gadget_common::client::{
-    AccountId, ClientWithApi, GadgetJobType, JobsApiForGadget, JobsClient, MaxSubmissionLen,
-};
-use gadget_common::debug_logger::DebugLogger;
+use gadget_common::client::{AccountId, ClientWithApi, JobsApiForGadget, MaxSubmissionLen};
 use gadget_common::gadget::message::GadgetProtocolMessage;
 use gadget_common::gadget::work_manager::WorkManager;
-use gadget_common::gadget::{GadgetProtocol, JobInitMetadata, WorkManagerConfig};
-use gadget_common::protocol::AsyncProtocol;
-use gadget_common::{BlockImportNotification, Error};
+use gadget_common::gadget::JobInitMetadata;
+use gadget_common::prelude::{KeystoreBackend, Network};
+use gadget_common::Error;
 use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError};
 use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface};
 use groth16::proving_key::PackedProvingKeyShare;
@@ -33,7 +28,7 @@ use tangle_primitives::jobs::{
     ArkworksProofResult, HyperData, JobId, JobResult, JobType, ZkSaaSPhaseTwoRequest,
     ZkSaaSProofResult, ZkSaaSSystem,
 };
-use tangle_primitives::roles::{RoleType, ZeroKnowledgeRoleType};
+use tangle_primitives::roles::RoleType;
 use tangle_primitives::verifier::to_field_elements;
 use tokio::sync::mpsc::UnboundedReceiver;
 
@@ -42,103 +37,47 @@ pub mod proto_gen;
 type F = ark_bn254::Fr;
 type E = ark_bn254::Bn254;
 
-pub struct ZkProtocol<B: Block, C, BE> {
-    pub client: JobsClient<B, BE, C>,
-    pub account_id: AccountId,
-    pub network: ZkNetworkService,
-    pub logger: DebugLogger,
-}
-
 pub trait AdditionalProtocolParams: Send + Sync + Clone + 'static {
     fn n_parties(&self) -> usize;
     fn party_id(&self) -> u32;
 }
 
-#[async_trait]
-impl<B, C, BE> GadgetProtocol<B, BE, C> for ZkProtocol<B, C, BE>
+pub async fn create_next_job<B, C, BE, N: Network, KBE>(
+    config: &crate::ZkProtocol<B, BE, C, N, KBE>,
+    job: JobInitMetadata<B>,
+    _work_manager: &ProtocolWorkManager<WorkManager>,
+) -> Result<ZkJobAdditionalParams, Error>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
     B: Block,
     C: ClientWithApi<B, BE> + 'static,
     BE: Backend<B> + 'static,
+    KBE: KeystoreBackend,
 {
-    fn name(&self) -> String {
-        "zk-saas".to_string()
-    }
-
-    async fn create_next_job(
-        &self,
-        job: JobInitMetadata<B>,
-    ) -> Result<<Self as AsyncProtocol>::AdditionalParams, Error> {
-        let (now, job_id) = (job.now, job.job_id);
-
-        self.logger
-            .info(format!("Received a finality notification at {now}"));
-
-        let role_type = job.job_type.get_role_type();
-        let JobType::ZkSaaSPhaseTwo(phase_two) = job.job_type else {
-            panic!("Should be valid")
-        };
-        let JobType::ZkSaaSPhaseOne(phase_one) = job.phase1_job.expect("Should exist") else {
-            panic!("Should be valid")
-        };
-
-        let participants = phase_one.participants;
-        let params = ZkJobAdditionalParams {
-            n_parties: participants.len(),
-            party_id: participants
-                .iter()
-                .position(|p| p == &self.account_id)
-                .expect("Should exist") as _,
-            job_id,
-            role_type,
-            system: phase_one.system,
-            request: phase_two.request,
-            participants: participants.into_inner(),
-        };
-
-        Ok(params)
-    }
-
-    async fn process_block_import_notification(
-        &self,
-        _notification: BlockImportNotification<B>,
-        _job_manager: &ProtocolWorkManager<WorkManager>,
-    ) -> Result<(), Error> {
-        Ok(())
-    }
-
-    async fn process_error(&self, error: Error, _job_manager: &ProtocolWorkManager<WorkManager>) {
-        log::error!(target: "gadget", "Received an error: {error:?}");
-    }
-
-    fn account_id(&self) -> &AccountId {
-        &self.account_id
-    }
-
-    fn role_filter(&self, role: RoleType) -> bool {
-        matches!(role, RoleType::ZkSaaS(ZeroKnowledgeRoleType::ZkSaaSGroth16))
-    }
-
-    fn phase_filter(&self, job: GadgetJobType) -> bool {
-        matches!(job, JobType::ZkSaaSPhaseTwo(_))
-    }
-
-    fn client(&self) -> JobsClient<B, BE, C> {
-        self.client.clone()
-    }
-
-    fn logger(&self) -> DebugLogger {
-        self.logger.clone()
-    }
-
-    fn get_work_manager_config(&self) -> WorkManagerConfig {
-        WorkManagerConfig {
-            interval: None,
-            max_active_tasks: 2,
-            max_pending_tasks: 10,
-        }
-    }
+    let (_now, job_id) = (job.now, job.job_id);
+    let role_type = job.job_type.get_role_type();
+    let JobType::ZkSaaSPhaseTwo(phase_two) = job.job_type else {
+        panic!("Should be valid")
+    };
+    let JobType::ZkSaaSPhaseOne(phase_one) = job.phase1_job.expect("Should exist") else {
+        panic!("Should be valid")
+    };
+
+    let participants = phase_one.participants;
+    let params = ZkJobAdditionalParams {
+        n_parties: participants.len(),
+        party_id: participants
+            .iter()
+            .position(|p| p == &config.account_id)
+            .expect("Should exist") as _,
+        job_id,
+        role_type,
+        system: phase_one.system,
+        request: phase_two.request,
+        participants: participants.into_inner(),
+    };
+
+    Ok(params)
 }
 
 #[derive(Clone, Debug)]
@@ -161,253 +100,248 @@ impl AdditionalProtocolParams for ZkJobAdditionalParams {
     }
 }
 
-#[async_trait]
-impl<B: Block, C: ClientWithApi<B, BE> + 'static, BE: Backend<B> + 'static> AsyncProtocol
-    for ZkProtocol<B, C, BE>
+pub async fn generate_protocol_from<B, C, BE, N: Network, KBE>(
+    config: &crate::ZkProtocol<B, BE, C, N, KBE>,
+    associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
+    associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
+    associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
+    associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
+    protocol_message_rx: UnboundedReceiver<GadgetProtocolMessage>,
+    additional_params: ZkJobAdditionalParams,
+) -> Result<BuiltExecutableJobWrapper, JobError>
 where
     <C as ProvideRuntimeApi<B>>::Api: JobsApiForGadget<B>,
+    B: Block,
+    C: ClientWithApi<B, BE> + 'static,
+    BE: Backend<B> + 'static,
+    KBE: KeystoreBackend,
 {
-    type AdditionalParams = ZkJobAdditionalParams;
-
-    async fn generate_protocol_from(
-        &self,
-        associated_block_id: <WorkManager as WorkManagerInterface>::Clock,
-        associated_retry_id: <WorkManager as WorkManagerInterface>::RetryID,
-        associated_session_id: <WorkManager as WorkManagerInterface>::SessionID,
-        associated_task_id: <WorkManager as WorkManagerInterface>::TaskID,
-        protocol_message_rx: UnboundedReceiver<GadgetProtocolMessage>,
-        additional_params: Self::AdditionalParams,
-    ) -> Result<BuiltExecutableJobWrapper, JobError> {
-        let client = self.client.clone();
-        let rxs = zk_setup_rxs(additional_params.n_parties(), protocol_message_rx).await?;
-        let other_network_ids = zk_setup_phase_order_participants(
-            additional_params.participants.clone(),
-            &self.network,
-        )
-        .map_err(|err| JobError {
-            reason: format!("Failed to setup phase order participants: {err:?}"),
-        })?;
-
-        let params = ZkAsyncProtocolParameters::<_, _, _, B, BE> {
-            associated_block_id,
-            associated_retry_id,
-            associated_session_id,
-            associated_task_id,
-            rxs,
-            party_id: additional_params.party_id(),
-            n_parties: additional_params.n_parties(),
-            my_network_id: self.network.my_id(),
-            other_network_ids,
-            network: self.network.clone(),
-            client: self.client.clone(),
-            extra_parameters: additional_params.clone(),
-            _pd: Default::default(),
-        };
-
-        Ok(JobBuilder::new()
-            .protocol(async move {
-                log::debug!(
-                    "Running ZkSaaS for {:?} with JobId: {}",
-                    params.extra_parameters.role_type,
-                    params.extra_parameters.job_id
-                );
-                let ZkSaaSSystem::Groth16(ref system) = params.extra_parameters.system;
-                let ZkSaaSPhaseTwoRequest::Groth16(ref job) = params.extra_parameters.request;
-                let HyperData::Raw(ref proving_key_bytes) = system.proving_key else {
-                    return Err(JobError {
-                        reason: "Only raw proving key is supported".to_string(),
-                    });
-                };
-
-                let pk = ProvingKey::<E>::deserialize_compressed(&proving_key_bytes[..]).map_err(
-                    |err| JobError {
+    let pallet_tx = config.pallet_tx.clone();
+    let rxs = zk_setup_rxs(additional_params.n_parties(), protocol_message_rx).await?;
+    let other_network_ids =
+        zk_setup_phase_order_participants(additional_params.participants.clone(), &config.network)
+            .map_err(|err| JobError {
+                reason: format!("Failed to setup phase order participants: {err:?}"),
+            })?;
+
+    let params = ZkAsyncProtocolParameters::<_, _, _, B, BE> {
+        associated_block_id,
+        associated_retry_id,
+        associated_session_id,
+        associated_task_id,
+        rxs,
+        party_id: additional_params.party_id(),
+        n_parties: additional_params.n_parties(),
+        my_network_id: config.account_id,
+        other_network_ids,
+        network: config.network.clone(),
+        client: config.client.clone(),
+        extra_parameters: additional_params.clone(),
+        _pd: Default::default(),
+    };
+
+    Ok(JobBuilder::new()
+        .protocol(async move {
+            log::debug!(
+                "Running ZkSaaS for {:?} with JobId: {}",
+                params.extra_parameters.role_type,
+                params.extra_parameters.job_id
+            );
+            let ZkSaaSSystem::Groth16(ref system) = params.extra_parameters.system;
+            let ZkSaaSPhaseTwoRequest::Groth16(ref job) = params.extra_parameters.request;
+            let HyperData::Raw(ref proving_key_bytes) = system.proving_key else {
+                return Err(JobError {
+                    reason: "Only raw proving key is supported".to_string(),
+                });
+            };
+
+            let pk =
+                ProvingKey::<E>::deserialize_compressed(&proving_key_bytes[..]).map_err(|err| {
+                    JobError {
                         reason: format!("Failed to deserialize proving key: {err:?}"),
-                    },
-                )?;
-                let l = params.n_parties / 4;
-                let pp = PackedSharingParams::new(l);
-                let crs_shares =
-                    PackedProvingKeyShare::<E>::pack_from_arkworks_proving_key(&pk, pp);
-                let our_qap_share =
-                    job.qap_shares
-                        .get(params.party_id as usize)
-                        .ok_or_else(|| JobError {
-                            reason: "Failed to get our qap share".to_string(),
-                        })?;
-                let HyperData::Raw(ref qap_a) = our_qap_share.a else {
-                    return Err(JobError {
-                        reason: "Only raw qap_a is supported".to_string(),
-                    });
-                };
-                let HyperData::Raw(ref qap_b) = our_qap_share.b else {
-                    return Err(JobError {
-                        reason: "Only raw qap_b is supported".to_string(),
-                    });
-                };
-                let HyperData::Raw(ref qap_c) = our_qap_share.c else {
-                    return Err(JobError {
-                        reason: "Only raw qap_c is supported".to_string(),
-                    });
-                };
-                let our_a_share =
-                    job.a_shares
-                        .get(params.party_id as usize)
-                        .ok_or_else(|| JobError {
-                            reason: "Failed to get our a share".to_string(),
-                        })?;
-                let HyperData::Raw(a_share_bytes) = our_a_share else {
-                    return Err(JobError {
-                        reason: "Only raw a_share is supported".to_string(),
-                    });
-                };
-
-                let our_ax_share =
-                    job.ax_shares
-                        .get(params.party_id as usize)
-                        .ok_or_else(|| JobError {
-                            reason: "Failed to get our ax share".to_string(),
-                        })?;
-
-                let HyperData::Raw(ax_share_bytes) = our_ax_share else {
-                    return Err(JobError {
-                        reason: "Only raw ax_share is supported".to_string(),
-                    });
-                };
-                let m = system.num_inputs + system.num_constraints;
-                let domain = Radix2EvaluationDomain::<F>::new(m as usize)
-                    .ok_or(SynthesisError::PolynomialDegreeTooLarge)
-                    .map_err(|err| JobError {
-                        reason: format!("Failed to create evaluation domain: {err:?}"),
-                    })?;
-                let qap_share = groth16::qap::PackedQAPShare {
-                    num_inputs: system.num_inputs as _,
-                    num_constraints: system.num_constraints as _,
-                    a: to_field_elements(qap_a).map_err(|err| JobError {
-                        reason: format!("Failed to convert a to field elements: {err:?}"),
-                    })?,
-                    b: to_field_elements(qap_b).map_err(|err| JobError {
-                        reason: format!("Failed to convert b to field elements: {err:?}"),
-                    })?,
-                    c: to_field_elements(qap_c).map_err(|err| JobError {
-                        reason: format!("Failed to convert c to field elements: {err:?}"),
-                    })?,
-                    domain,
-                };
-                let a_share = to_field_elements(a_share_bytes).map_err(|err| JobError {
-                    reason: format!("Failed to convert a_share to field elements: {err:?}"),
+                    }
                 })?;
-                let ax_share = to_field_elements(ax_share_bytes).map_err(|err| JobError {
-                    reason: format!("Failed to convert ax_share to field elements: {err:?}"),
+            let l = params.n_parties / 4;
+            let pp = PackedSharingParams::new(l);
+            let crs_shares = PackedProvingKeyShare::<E>::pack_from_arkworks_proving_key(&pk, pp);
+            let our_qap_share = job
+                .qap_shares
+                .get(params.party_id as usize)
+                .ok_or_else(|| JobError {
+                    reason: "Failed to get our qap share".to_string(),
                 })?;
-                let crs_share =
-                    crs_shares
-                        .get(params.party_id as usize)
-                        .ok_or_else(|| JobError {
-                            reason: "Failed to get crs share".to_string(),
-                        })?;
-                let h_share = groth16::ext_wit::circom_h(qap_share, &pp, &params)
-                    .map_err(|err| JobError {
-                        reason: format!("Failed to compute circom_h: {err:?}"),
-                    })
-                    .await?;
-                let pi_a_share = groth16::prove::A::<E> {
-                    L: Default::default(),
-                    N: Default::default(),
-                    r: <E as Pairing>::ScalarField::zero(),
-                    pp: &pp,
-                    S: &crs_share.s,
-                    a: &a_share,
-                }
-                .compute(&params, MultiplexedStreamID::Zero)
-                .map_err(|err| JobError {
-                    reason: format!("Failed to compute pi_a_share: {err:?}"),
-                })
-                .await?;
-                let pi_b_share = groth16::prove::B::<E> {
-                    Z: Default::default(),
-                    K: Default::default(),
-                    s: <E as Pairing>::ScalarField::zero(),
-                    pp: &pp,
-                    V: &crs_share.v,
-                    a: &a_share,
-                }
-                .compute(&params, MultiplexedStreamID::Zero)
+            let HyperData::Raw(ref qap_a) = our_qap_share.a else {
+                return Err(JobError {
+                    reason: "Only raw qap_a is supported".to_string(),
+                });
+            };
+            let HyperData::Raw(ref qap_b) = our_qap_share.b else {
+                return Err(JobError {
+                    reason: "Only raw qap_b is supported".to_string(),
+                });
+            };
+            let HyperData::Raw(ref qap_c) = our_qap_share.c else {
+                return Err(JobError {
+                    reason: "Only raw qap_c is supported".to_string(),
+                });
+            };
+            let our_a_share =
+                job.a_shares
+                    .get(params.party_id as usize)
+                    .ok_or_else(|| JobError {
+                        reason: "Failed to get our a share".to_string(),
+                    })?;
+            let HyperData::Raw(a_share_bytes) = our_a_share else {
+                return Err(JobError {
+                    reason: "Only raw a_share is supported".to_string(),
+                });
+            };
+
+            let our_ax_share =
+                job.ax_shares
+                    .get(params.party_id as usize)
+                    .ok_or_else(|| JobError {
+                        reason: "Failed to get our ax share".to_string(),
+                    })?;
+
+            let HyperData::Raw(ax_share_bytes) = our_ax_share else {
+                return Err(JobError {
+                    reason: "Only raw ax_share is supported".to_string(),
+                });
+            };
+            let m = system.num_inputs + system.num_constraints;
+            let domain = Radix2EvaluationDomain::<F>::new(m as usize)
+                .ok_or(SynthesisError::PolynomialDegreeTooLarge)
                 .map_err(|err| JobError {
-                    reason: format!("Failed to compute pi_b_share: {err:?}"),
-                })
-                .await?;
-                let pi_c_share = groth16::prove::C::<E> {
-                    W: &crs_share.w,
-                    U: &crs_share.u,
-                    A: pi_a_share,
-                    M: Default::default(),
-                    r: <E as Pairing>::ScalarField::zero(),
-                    s: <E as Pairing>::ScalarField::zero(),
-                    pp: &pp,
-                    H: &crs_share.h,
-                    a: &a_share,
-                    ax: &ax_share,
-                    h: &h_share,
-                }
-                .compute(&params)
+                    reason: format!("Failed to create evaluation domain: {err:?}"),
+                })?;
+            let qap_share = groth16::qap::PackedQAPShare {
+                num_inputs: system.num_inputs as _,
+                num_constraints: system.num_constraints as _,
+                a: to_field_elements(qap_a).map_err(|err| JobError {
+                    reason: format!("Failed to convert a to field elements: {err:?}"),
+                })?,
+                b: to_field_elements(qap_b).map_err(|err| JobError {
+                    reason: format!("Failed to convert b to field elements: {err:?}"),
+                })?,
+                c: to_field_elements(qap_c).map_err(|err| JobError {
+                    reason: format!("Failed to convert c to field elements: {err:?}"),
+                })?,
+                domain,
+            };
+            let a_share = to_field_elements(a_share_bytes).map_err(|err| JobError {
+                reason: format!("Failed to convert a_share to field elements: {err:?}"),
+            })?;
+            let ax_share = to_field_elements(ax_share_bytes).map_err(|err| JobError {
+                reason: format!("Failed to convert ax_share to field elements: {err:?}"),
+            })?;
+            let crs_share = crs_shares
+                .get(params.party_id as usize)
+                .ok_or_else(|| JobError {
+                    reason: "Failed to get crs share".to_string(),
+                })?;
+            let h_share = groth16::ext_wit::circom_h(qap_share, &pp, &params)
                 .map_err(|err| JobError {
-                    reason: format!("Failed to compute pi_c_share: {err:?}"),
+                    reason: format!("Failed to compute circom_h: {err:?}"),
                 })
                 .await?;
-                if params.is_king() {
-                    let (mut a, mut b, c) = (pi_a_share, pi_b_share, pi_c_share);
-                    // These elements are needed to construct the full proof, they are part of the proving key.
-                    // however, we can just send these values to the client, not the full proving key.
-                    a += pk.a_query[0] + pk.vk.alpha_g1;
-                    b += pk.b_g2_query[0] + pk.vk.beta_g2;
-
-                    let proof = ark_groth16::Proof::<E> {
-                        a: a.into_affine(),
-                        b: b.into_affine(),
-                        c: c.into_affine(),
-                    };
-
-                    // Verify the proof
-                    // convert the public inputs from string to bigints
-                    let public_inputs =
-                        to_field_elements(&job.public_input).map_err(|err| JobError {
-                            reason: format!(
-                                "Failed to convert public inputs to field elements: {err:?}"
-                            ),
-                        })?;
-                    let pvk = ark_groth16::prepare_verifying_key(&pk.vk);
-                    let verified = Groth16::<E, CircomReduction>::verify_with_processed_vk(
-                        &pvk,
-                        public_inputs.as_slice(),
-                        &proof,
-                    )
-                    .unwrap();
-                    if verified {
-                        log::info!("Proof verified");
-                    } else {
-                        log::error!("Proof verification failed");
-                    }
-                    let mut proof_bytes = Vec::new();
-                    proof.serialize_compressed(&mut proof_bytes).unwrap();
-                    let result = ZkSaaSProofResult::Arkworks(ArkworksProofResult {
-                        proof: proof_bytes.try_into().unwrap(),
-                    });
+            let pi_a_share = groth16::prove::A::<E> {
+                L: Default::default(),
+                N: Default::default(),
+                r: <E as Pairing>::ScalarField::zero(),
+                pp: &pp,
+                S: &crs_share.s,
+                a: &a_share,
+            }
+            .compute(&params, MultiplexedStreamID::Zero)
+            .map_err(|err| JobError {
+                reason: format!("Failed to compute pi_a_share: {err:?}"),
+            })
+            .await?;
+            let pi_b_share = groth16::prove::B::<E> {
+                Z: Default::default(),
+                K: Default::default(),
+                s: <E as Pairing>::ScalarField::zero(),
+                pp: &pp,
+                V: &crs_share.v,
+                a: &a_share,
+            }
+            .compute(&params, MultiplexedStreamID::Zero)
+            .map_err(|err| JobError {
+                reason: format!("Failed to compute pi_b_share: {err:?}"),
+            })
+            .await?;
+            let pi_c_share = groth16::prove::C::<E> {
+                W: &crs_share.w,
+                U: &crs_share.u,
+                A: pi_a_share,
+                M: Default::default(),
+                r: <E as Pairing>::ScalarField::zero(),
+                s: <E as Pairing>::ScalarField::zero(),
+                pp: &pp,
+                H: &crs_share.h,
+                a: &a_share,
+                ax: &ax_share,
+                h: &h_share,
+            }
+            .compute(&params)
+            .map_err(|err| JobError {
+                reason: format!("Failed to compute pi_c_share: {err:?}"),
+            })
+            .await?;
+            if params.is_king() {
+                let (mut a, mut b, c) = (pi_a_share, pi_b_share, pi_c_share);
+                // These elements are needed to construct the full proof, they are part of the proving key.
+                // however, we can just send these values to the client, not the full proving key.
+                a += pk.a_query[0] + pk.vk.alpha_g1;
+                b += pk.b_g2_query[0] + pk.vk.beta_g2;
+
+                let proof = ark_groth16::Proof::<E> {
+                    a: a.into_affine(),
+                    b: b.into_affine(),
+                    c: c.into_affine(),
+                };
 
-                    client
-                        .submit_job_result(
-                            params.extra_parameters.role_type,
-                            params.extra_parameters.job_id,
-                            JobResult::ZkSaaSPhaseTwo(result),
-                        )
-                        .await
-                        .map_err(|err| JobError {
-                            reason: err.to_string(),
-                        })?;
+                // Verify the proof
+                // convert the public inputs from string to bigints
+                let public_inputs =
+                    to_field_elements(&job.public_input).map_err(|err| JobError {
+                        reason: format!(
+                            "Failed to convert public inputs to field elements: {err:?}"
+                        ),
+                    })?;
+                let pvk = ark_groth16::prepare_verifying_key(&pk.vk);
+                let verified = Groth16::<E, CircomReduction>::verify_with_processed_vk(
+                    &pvk,
+                    public_inputs.as_slice(),
+                    &proof,
+                )
+                .unwrap();
+                if verified {
+                    log::info!("Proof verified");
+                } else {
+                    log::error!("Proof verification failed");
                 }
-                Ok(())
-            })
-            .build())
-    }
+                let mut proof_bytes = Vec::new();
+                proof.serialize_compressed(&mut proof_bytes).unwrap();
+                let result = ZkSaaSProofResult::Arkworks(ArkworksProofResult {
+                    proof: proof_bytes.try_into().unwrap(),
+                });
+
+                pallet_tx
+                    .submit_job_result(
+                        params.extra_parameters.role_type,
+                        params.extra_parameters.job_id,
+                        JobResult::ZkSaaSPhaseTwo(result),
+                    )
+                    .await
+                    .map_err(|err| JobError {
+                        reason: err.to_string(),
+                    })?;
+            }
+            Ok(())
+        })
+        .build())
 }
 
 async fn zk_setup_rxs(
@@ -468,15 +402,20 @@ async fn zk_setup_rxs(
     Ok(rxs)
 }
 
-fn zk_setup_phase_order_participants(
+pub trait ZkNetworkExt {
+    fn king_id(&self) -> Option<AccountId>;
+}
+
+fn zk_setup_phase_order_participants<N: Network>(
     mut participants: Vec<AccountId>,
-    network: &ZkNetworkService,
+    network: &N,
 ) -> Result<HashMap<u32, AccountId>, gadget_common::Error> {
-    let king_id = network
-        .king_id()
-        .ok_or_else(|| gadget_common::Error::ClientError {
-            err: "King id not found".to_string(),
-        })?;
+    let king_id =
+        network
+            .greatest_authority_id()
+            .ok_or_else(|| gadget_common::Error::ClientError {
+                err: "King id not found".to_string(),
+            })?;
 
     // The king should be moved into the 0th index of the participants, swapping positions with the 0th participant
     let king_index = participants


Test patch:
diff --git a/protocols/dfns-cggmp21/tests/ecdsa.rs b/protocols/dfns-cggmp21/tests/dfns.rs
similarity index 75%
rename from protocols/dfns-cggmp21/tests/ecdsa.rs
rename to protocols/dfns-cggmp21/tests/dfns.rs
index 1adf49622..1b9febeae 100644
--- a/protocols/dfns-cggmp21/tests/ecdsa.rs
+++ b/protocols/dfns-cggmp21/tests/dfns.rs
@@ -9,39 +9,6 @@ mod tests {
     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_keyrefresh() {
         test_utils::setup_log();
@@ -231,44 +198,16 @@ mod tests {
     }
 
     async fn new_test_ext<const N: usize>() -> MultiThreadedTestExternalities {
-        test_utils::mock::new_test_ext::<N, 4, _, _, _>((), |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 keyrefresh_client = node_input.mock_clients.pop().expect("No keyrefresh client");
-            let keyrotate_client = node_input.mock_clients.pop().expect("No keyrotate 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 keyrefresh_network = node_input
-                .mock_networks
-                .pop()
-                .expect("No keyrefresh network");
-            let keyrotate_network = node_input
-                .mock_networks
-                .pop()
-                .expect("No keyrotate network");
+        test_utils::mock::new_test_ext::<N, 4, _, _, _>((), |node_input| async move {
+            let clients = node_input.mock_clients;
+            let networks = node_input.mock_networks;
             let account_id = node_input.account_id;
-
             let logger = node_input.logger.clone();
-            let (pallet_tx, keystore) = (node_input.pallet_tx, node_input.keystore);
+
+            let (pallet_tx, key_store) = (node_input.pallet_tx, node_input.keystore);
             logger.info("Starting gadget");
-            if let Err(err) = dfns_cggmp21_protocol::run::<_, MockBackend, _, _, _, _>(
-                account_id,
-                logger.clone(),
-                keystore,
-                pallet_tx,
-                (
-                    keygen_client,
-                    signing_client,
-                    keyrefresh_client,
-                    keyrotate_client,
-                ),
-                (
-                    keygen_network,
-                    signing_network,
-                    keyrefresh_network,
-                    keyrotate_network,
-                ),
+            if let Err(err) = dfns_cggmp21_protocol::run::<_, MockBackend, _, _, _>(
+                clients, pallet_tx, networks, logger, account_id, key_store,
             )
             .await
             {
diff --git a/protocols/zk-saas/tests/zksaas.rs b/protocols/zk-saas/tests/zksaas.rs
index e4b16f67c..5b0b0e004 100644
--- a/protocols/zk-saas/tests/zksaas.rs
+++ b/protocols/zk-saas/tests/zksaas.rs
@@ -28,7 +28,7 @@ mod tests {
     use tangle_primitives::verifier::from_field_elements;
     use test_utils::mock::{id_to_public, Jobs, MockBackend, RuntimeOrigin};
     use test_utils::sync::substrate_test_channel::MultiThreadedTestExternalities;
-    use zk_saas_protocol::ZkGadgetConfig;
+    use zk_saas_protocol::network::ZkProtocolNetworkConfig;
 
     #[tokio::test(flavor = "multi_thread")]
     async fn test_zk_job() {
@@ -188,21 +188,34 @@ mod tests {
             let logger = node_info.logger.clone();
             let client = node_info.mock_clients.pop().expect("Should have client");
             let pallet_tx = node_info.pallet_tx;
+            let key_store = node_info.keystore;
 
-            let config = ZkGadgetConfig::<_, _, MockBackend> {
+            let network_cfg = ZkProtocolNetworkConfig {
+                account_id: node_info.account_id,
                 king_bind_addr,
                 client_only_king_addr,
+                client_only_king_public_identity_der,
                 public_identity_der,
                 private_identity_der,
-                client_only_king_public_identity_der,
-                account_id: node_info.account_id,
-                logger,
-                client,
-                pallet_tx: Arc::new(pallet_tx),
-                _pd: Default::default(),
             };
 
-            if let Err(err) = config.execute().await {
+            // For testing, override the generated networks from the NodeInput, and use what we will
+            // for testnet/mainnet
+            let network = zk_saas_protocol::create_zk_network(network_cfg)
+                .await
+                .expect("Should create network");
+
+            // ZkSaaS only requires 1 client and 1 network, no need to use the NodeInput's vectors
+            if let Err(err) = zk_saas_protocol::run::<_, MockBackend, _, _, _>(
+                vec![client],
+                pallet_tx,
+                vec![network],
+                logger,
+                node_info.account_id,
+                key_store,
+            )
+            .await
+            {
                 log::error!(target: "gadget", "Failed to run zk protocol: {err:?}");
             }
         })
diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs
index b02ca35e6..3cb66358e 100644
--- a/test-utils/src/lib.rs
+++ b/test-utils/src/lib.rs
@@ -54,7 +54,7 @@ pub fn remove_job(role_type: RoleType, job_id: JobId) {
 /// returns a future that runs the protocols.
 #[allow(clippy::crate_in_macro_def)]
 macro_rules! generate_signing_and_keygen_tss_tests {
-    ($t:expr, $n:expr, $threshold_sig_ty:expr) => {
+    ($t:expr, $n:expr, $proto_count:expr, $threshold_sig_ty:expr) => {
         #[cfg(test)]
         mod tests {
             use std::sync::Arc;
@@ -168,7 +168,7 @@ use gadget_common::full_protocol::FullProtocolConfig;
             }
 
             async fn new_test_ext<const N: usize>() -> MultiThreadedTestExternalities {
-                test_utils::mock::new_test_ext::<N, 2, (), _, _>((), crate::setup_node).await
+                test_utils::mock::new_test_ext::<N, $proto_count, (), _, _>((), crate::setup_node).await
             }
         }
     };