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

Issue:
[TASK] MPC refactor: add common functions to context derive
Now that we've built multiple MPCs, duplicated code can be abstracted away inside the SDK and test utils.

Various MPC-related functions could be added as auto-generated context functions

e.g.,

```rust
#[derive(Mpc)]
struct MyContext { // ... }
// ...
fn my_job(context: MyContext) {
    let (i, operators) = context.get_operators_and_self_party_index();
    // ...
}
```

As for testing, we can have the initialization handle much of the repetitive boilerplate. Afterward, there may be room for a macro to easily define tests, especially MPC-specific macros that often store keys locally using a call ID, and that call ID needs to be funneled into the next job (e.g., keygen -> signing) in order to retrieve data from the local store.


Discussion / hints:
I would rather build this as a custom MPC context @tbraun96, we shouldn't add too many things directly on the env.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 48ac1b0d7..45ab8df68 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2016,6 +2016,7 @@ dependencies = [
  "blueprint-manager",
  "cargo-tangle",
  "cargo_metadata",
+ "cargo_toml",
  "color-eyre",
  "futures",
  "gadget-io",
@@ -2298,6 +2299,16 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cargo_toml"
+version = "0.20.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0"
+dependencies = [
+ "serde",
+ "toml",
+]
+
 [[package]]
 name = "cbor4ii"
 version = "0.3.3"
@@ -4770,6 +4781,8 @@ dependencies = [
  "gadget-sdk",
  "proc-macro2",
  "quote",
+ "round-based",
+ "serde",
  "syn 2.0.87",
  "trybuild",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 2a8443799..88d876600 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -156,6 +156,7 @@ lazy_static = "1.5.0"
 jsonrpc-core = "18.0.0"
 jsonrpc-http-server = "18.0.0"
 tempfile = "3.10.1"
+cargo_toml = { version = "0.20.5" }
 
 alloy-primitives = "0.7.2"
 alloy-json-abi = "0.7.2"
diff --git a/blueprints/examples/src/eigen_context.rs b/blueprints/examples/src/eigen_context.rs
index 92a9900d6..85d5c78a3 100644
--- a/blueprints/examples/src/eigen_context.rs
+++ b/blueprints/examples/src/eigen_context.rs
@@ -4,7 +4,7 @@ use gadget_sdk::event_listener::evm::contracts::EvmContractEventListener;
 use gadget_sdk::event_utils::InitializableEventHandler;
 use gadget_sdk::subxt_core::ext::sp_runtime::traits::Zero;
 use gadget_sdk::utils::evm::get_provider_http;
-use gadget_sdk::{config::StdGadgetConfiguration, ctx::EigenlayerContext, job, load_abi};
+use gadget_sdk::{config::StdGadgetConfiguration, contexts::EigenlayerContext, job, load_abi};
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use std::env;
diff --git a/blueprints/examples/src/services_context.rs b/blueprints/examples/src/services_context.rs
index 7e1b295d6..6ff4255ed 100644
--- a/blueprints/examples/src/services_context.rs
+++ b/blueprints/examples/src/services_context.rs
@@ -1,11 +1,11 @@
-use gadget_sdk::ctx::TangleClientContext;
+use gadget_sdk::config::StdGadgetConfiguration;
+use gadget_sdk::contexts::{ServicesContext, TangleClientContext};
 use gadget_sdk::event_listener::tangle::jobs::{services_post_processor, services_pre_processor};
 use gadget_sdk::event_listener::tangle::TangleEventListener;
 use gadget_sdk::event_utils::InitializableEventHandler;
 use gadget_sdk::job;
 use gadget_sdk::subxt_core::utils::AccountId32;
 use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled;
-use gadget_sdk::{config::StdGadgetConfiguration, ctx::ServicesContext};
 
 #[derive(Clone, ServicesContext, TangleClientContext)]
 pub struct ExampleServiceContext {
diff --git a/blueprints/incredible-squaring-eigenlayer/src/contexts/aggregator.rs b/blueprints/incredible-squaring-eigenlayer/src/contexts/aggregator.rs
index d79a67b19..dcde25852 100644
--- a/blueprints/incredible-squaring-eigenlayer/src/contexts/aggregator.rs
+++ b/blueprints/incredible-squaring-eigenlayer/src/contexts/aggregator.rs
@@ -15,7 +15,7 @@ use eigensdk::{
 };
 use gadget_sdk::{
     config::StdGadgetConfiguration,
-    ctx::{EigenlayerContext, KeystoreContext},
+    contexts::{EigenlayerContext, KeystoreContext},
     debug, error, info,
     runners::{BackgroundService, RunnerError},
 };
diff --git a/blueprints/incredible-squaring-eigenlayer/src/contexts/x_square.rs b/blueprints/incredible-squaring-eigenlayer/src/contexts/x_square.rs
index 0011574b0..1a216ca33 100644
--- a/blueprints/incredible-squaring-eigenlayer/src/contexts/x_square.rs
+++ b/blueprints/incredible-squaring-eigenlayer/src/contexts/x_square.rs
@@ -1,5 +1,5 @@
 use crate::contexts::client::AggregatorClient;
-use gadget_sdk::{config::StdGadgetConfiguration, ctx::KeystoreContext};
+use gadget_sdk::{config::StdGadgetConfiguration, contexts::KeystoreContext};
 
 #[derive(Clone, KeystoreContext)]
 pub struct EigenSquareContext {
diff --git a/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs b/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs
index d4b57b535..ae05883b5 100644
--- a/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs
+++ b/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs
@@ -8,7 +8,7 @@ use alloy_sol_types::SolType;
 use color_eyre::Result;
 use eigensdk::crypto_bls::BlsKeyPair;
 use eigensdk::crypto_bls::OperatorId;
-use gadget_sdk::ctx::KeystoreContext;
+use gadget_sdk::contexts::KeystoreContext;
 use gadget_sdk::event_listener::evm::contracts::EvmContractEventListener;
 use gadget_sdk::keystore::BackendExt;
 use gadget_sdk::{error, info, job};
diff --git a/macros/context-derive/Cargo.toml b/macros/context-derive/Cargo.toml
index 93e8997ab..7bf82ec21 100644
--- a/macros/context-derive/Cargo.toml
+++ b/macros/context-derive/Cargo.toml
@@ -27,6 +27,8 @@ gadget-sdk = { path = "../../sdk", features = ["std"] }
 alloy-network = { workspace = true }
 alloy-provider = { workspace = true }
 alloy-transport = { workspace = true }
+round-based = { workspace = true }
+serde = { workspace = true }
 
 [features]
 default = ["std"]
diff --git a/macros/context-derive/src/eigenlayer.rs b/macros/context-derive/src/eigenlayer.rs
index 82920ffec..c81734da6 100644
--- a/macros/context-derive/src/eigenlayer.rs
+++ b/macros/context-derive/src/eigenlayer.rs
@@ -29,11 +29,11 @@ pub fn generate_context_impl(
         use eigensdk::client_elcontracts::reader::ELChainReader;
         use eigensdk::logging::get_test_logger;
         use gadget_sdk::utils::evm::get_slasher_address;
-        use gadget_sdk::ctx::BigInt;
+        use gadget_sdk::contexts::BigInt;
         use alloy_primitives::{U256, FixedBytes};
 
         #[async_trait::async_trait]
-        impl #impl_generics gadget_sdk::ctx::EigenlayerContext for #name #ty_generics #where_clause {
+        impl #impl_generics gadget_sdk::contexts::EigenlayerContext for #name #ty_generics #where_clause {
             async fn avs_registry_reader(&self) -> Result<eigensdk::client_avsregistry::reader::AvsRegistryChainReader, std::io::Error> {
                 let http_rpc_endpoint = #field_access.http_rpc_endpoint.clone();
                 let gadget_sdk::config::ProtocolSpecificSettings::Eigenlayer(contract_addresses) = &#field_access.protocol_specific else {
diff --git a/macros/context-derive/src/evm.rs b/macros/context-derive/src/evm.rs
index 59eb80826..d62381016 100644
--- a/macros/context-derive/src/evm.rs
+++ b/macros/context-derive/src/evm.rs
@@ -20,7 +20,7 @@ pub fn generate_context_impl(
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
 
     quote! {
-        impl #impl_generics gadget_sdk::ctx::EVMProviderContext for #name #ty_generics #where_clause {
+        impl #impl_generics gadget_sdk::contexts::EVMProviderContext for #name #ty_generics #where_clause {
             type Network = alloy_network::Ethereum;
             type Transport = alloy_transport::BoxTransport;
             type Provider = alloy_provider::fillers::FillProvider<
diff --git a/macros/context-derive/src/keystore.rs b/macros/context-derive/src/keystore.rs
index 689081c7f..b0cddbfc2 100644
--- a/macros/context-derive/src/keystore.rs
+++ b/macros/context-derive/src/keystore.rs
@@ -30,14 +30,14 @@ pub fn generate_context_impl(
 
     quote! {
         #[cfg(not(feature = "std"))]
-        impl #impl_generics_without_rwlock gadget_sdk::ctx::KeystoreContext<RwLock> for #name #ty_generics #where_clause_without_rwlock {
+        impl #impl_generics_without_rwlock gadget_sdk::contexts::KeystoreContext<RwLock> for #name #ty_generics #where_clause_without_rwlock {
             fn keystore(&self) -> Result<gadget_sdk::keystore::backend::GenericKeyStore<RwLock>, gadget_sdk::config::Error> {
                 #field_access.keystore()
             }
         }
 
         #[cfg(feature = "std")]
-        impl #impl_generics gadget_sdk::ctx::KeystoreContext<gadget_sdk::ext::parking_lot::RawRwLock> for #name #ty_generics #where_clause {
+        impl #impl_generics gadget_sdk::contexts::KeystoreContext<gadget_sdk::ext::parking_lot::RawRwLock> for #name #ty_generics #where_clause {
             fn keystore(&self) -> Result<gadget_sdk::keystore::backend::GenericKeyStore<gadget_sdk::ext::parking_lot::RawRwLock>, gadget_sdk::config::Error> {
                 #field_access.keystore()
             }
diff --git a/macros/context-derive/src/lib.rs b/macros/context-derive/src/lib.rs
index 6bf0263a3..04a416501 100644
--- a/macros/context-derive/src/lib.rs
+++ b/macros/context-derive/src/lib.rs
@@ -19,6 +19,8 @@ mod eigenlayer;
 mod evm;
 /// Keystore context extension implementation.
 mod keystore;
+/// MPC context extension implementation.
+mod mpc;
 /// Services context extension implementation.
 mod services;
 /// Tangle Subxt Client context extension implementation.
@@ -88,3 +90,16 @@ pub fn derive_eigenlayer_context(input: TokenStream) -> TokenStream {
         Err(err) => TokenStream::from(err.to_compile_error()),
     }
 }
+
+/// Derive macro for generating Context Extensions trait implementation for `MPCContext`.
+#[proc_macro_derive(MPCContext, attributes(config))]
+pub fn derive_mpc_context(input: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(input as syn::DeriveInput);
+    let result = cfg::find_config_field(&input.ident, &input.data)
+        .map(|config_field| mpc::generate_context_impl(input, config_field));
+
+    match result {
+        Ok(expanded) => TokenStream::from(expanded),
+        Err(err) => TokenStream::from(err.to_compile_error()),
+    }
+}
diff --git a/macros/context-derive/src/mpc.rs b/macros/context-derive/src/mpc.rs
new file mode 100644
index 000000000..f06361931
--- /dev/null
+++ b/macros/context-derive/src/mpc.rs
@@ -0,0 +1,158 @@
+use quote::quote;
+use syn::DeriveInput;
+
+use crate::cfg::FieldInfo;
+
+/// Generate the `MPCContext` implementation for the given struct.
+#[allow(clippy::too_many_lines)]
+pub fn generate_context_impl(
+    DeriveInput {
+        ident: name,
+        generics,
+        ..
+    }: DeriveInput,
+    config_field: FieldInfo,
+) -> proc_macro2::TokenStream {
+    let _field_access = match config_field {
+        FieldInfo::Named(ident) => quote! { self.#ident },
+        FieldInfo::Unnamed(index) => quote! { self.#index },
+    };
+
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+
+    quote! {
+        #[gadget_sdk::async_trait::async_trait]
+        impl #impl_generics gadget_sdk::contexts::MPCContext for #name #ty_generics #where_clause {
+            /// Returns a reference to the configuration
+            #[inline]
+            fn config(&self) -> &gadget_sdk::config::StdGadgetConfiguration {
+                &self.config
+            }
+
+            /// Returns the network protocol identifier for this context
+            #[inline]
+            fn network_protocol(&self) -> String {
+                let name = stringify!(#name).to_string();
+                format!("/{}/1.0.0", name.to_lowercase())
+            }
+
+            fn create_network_delivery_wrapper<M>(
+                &self,
+                mux: std::sync::Arc<gadget_sdk::network::NetworkMultiplexer>,
+                party_index: gadget_sdk::round_based::PartyIndex,
+                task_hash: [u8; 32],
+                parties: std::collections::BTreeMap<gadget_sdk::round_based::PartyIndex, gadget_sdk::subxt_core::ext::sp_core::ecdsa::Public>,
+            ) -> Result<gadget_sdk::network::round_based_compat::NetworkDeliveryWrapper<M>, gadget_sdk::Error>
+            where
+                M: Clone + Send + Unpin + 'static + gadget_sdk::serde::Serialize + gadget_sdk::serde::de::DeserializeOwned + gadget_sdk::round_based::ProtocolMessage,
+            {
+                Ok(gadget_sdk::network::round_based_compat::NetworkDeliveryWrapper::new(mux, party_index, task_hash, parties))
+            }
+
+            async fn get_party_index(
+                &self,
+            ) -> Result<gadget_sdk::round_based::PartyIndex, gadget_sdk::Error> {
+                Ok(self.get_party_index_and_operators().await?.0 as _)
+            }
+
+            async fn get_participants(
+                &self,
+                client: &gadget_sdk::ext::subxt::OnlineClient<gadget_sdk::clients::tangle::runtime::TangleConfig>,
+            ) -> Result<
+                    std::collections::BTreeMap<gadget_sdk::round_based::PartyIndex, gadget_sdk::subxt::utils::AccountId32>,
+                    gadget_sdk::Error,
+                > {
+                Ok(self.get_party_index_and_operators().await?.1.into_iter().enumerate().map(|(i, (id, _))| (i as _, id)).collect())
+            }
+
+            /// Retrieves the current blueprint ID from the configuration
+            ///
+            /// # Errors
+            /// Returns an error if the blueprint ID is not found in the configuration
+            fn blueprint_id(&self) -> gadget_sdk::color_eyre::Result<u64> {
+                self.config()
+                    .protocol_specific
+                    .tangle()
+                    .map(|c| c.blueprint_id)
+                    .map_err(|err| gadget_sdk::color_eyre::Report::msg("Blueprint ID not found in configuration: {err}"))
+            }
+
+            /// Retrieves the current party index and operator mapping
+            ///
+            /// # Errors
+            /// Returns an error if:
+            /// - Failed to retrieve operator keys
+            /// - Current party is not found in the operator list
+            async fn get_party_index_and_operators(
+                &self,
+            ) -> gadget_sdk::color_eyre::Result<(usize, std::collections::BTreeMap<gadget_sdk::subxt::utils::AccountId32, gadget_sdk::subxt_core::ext::sp_core::ecdsa::Public>)> {
+                let parties = self.current_service_operators_ecdsa_keys().await?;
+                let my_id = self.config.first_sr25519_signer()?.account_id();
+
+                gadget_sdk::trace!(
+                    "Looking for {my_id:?} in parties: {:?}",
+                    parties.keys().collect::<Vec<_>>()
+                );
+
+                let index_of_my_id = parties
+                    .iter()
+                    .position(|(id, _)| id == &my_id)
+                    .ok_or_else(|| gadget_sdk::color_eyre::Report::msg("Party not found in operator list"))?;
+
+                Ok((index_of_my_id, parties))
+            }
+
+            /// Retrieves the ECDSA keys for all current service operators
+            ///
+            /// # Errors
+            /// Returns an error if:
+            /// - Failed to connect to the Tangle client
+            /// - Failed to retrieve operator information
+            /// - Missing ECDSA key for any operator
+            async fn current_service_operators_ecdsa_keys(
+                &self,
+            ) -> gadget_sdk::color_eyre::Result<std::collections::BTreeMap<gadget_sdk::subxt::utils::AccountId32, gadget_sdk::subxt_core::ext::sp_core::ecdsa::Public>> {
+                let client = self.tangle_client().await?;
+                let current_blueprint = self.blueprint_id()?;
+                let current_service_op = self.current_service_operators(&client).await?;
+                let storage = client.storage().at_latest().await?;
+
+                let mut map = std::collections::BTreeMap::new();
+                for (operator, _) in current_service_op {
+                    let addr = gadget_sdk::ext::tangle_subxt::tangle_testnet_runtime::api::storage()
+                        .services()
+                        .operators(current_blueprint, &operator);
+
+                    let maybe_pref = storage.fetch(&addr).await.map_err(|err| {
+                        gadget_sdk::color_eyre::Report::msg("Failed to fetch operator storage for {operator}: {err}")
+                    })?;
+
+                    if let Some(pref) = maybe_pref {
+                        map.insert(operator, gadget_sdk::subxt_core::ext::sp_core::ecdsa::Public(pref.key));
+                    } else {
+                        return Err(gadget_sdk::color_eyre::Report::msg("Missing ECDSA key for operator {operator}"));
+                    }
+                }
+
+                Ok(map)
+            }
+
+            /// Retrieves the current call ID for this job
+            ///
+            /// # Errors
+            /// Returns an error if failed to retrieve the call ID from storage
+            async fn current_call_id(&self) -> gadget_sdk::color_eyre::Result<u64> {
+                let client = self.tangle_client().await?;
+                let addr = gadget_sdk::ext::tangle_subxt::tangle_testnet_runtime::api::storage().services().next_job_call_id();
+                let storage = client.storage().at_latest().await?;
+
+                let maybe_call_id = storage
+                    .fetch_or_default(&addr)
+                    .await
+                    .map_err(|err| gadget_sdk::color_eyre::Report::msg("Failed to fetch current call ID: {err}"))?;
+
+                Ok(maybe_call_id.saturating_sub(1))
+            }
+        }
+    }
+}
diff --git a/macros/context-derive/src/services.rs b/macros/context-derive/src/services.rs
index ce33b281e..4b3e8ea05 100644
--- a/macros/context-derive/src/services.rs
+++ b/macros/context-derive/src/services.rs
@@ -21,7 +21,7 @@ pub fn generate_context_impl(
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
 
     quote! {
-        impl #impl_generics gadget_sdk::ctx::ServicesContext for #name #ty_generics #where_clause {
+        impl #impl_generics gadget_sdk::contexts::ServicesContext for #name #ty_generics #where_clause {
             type Config = gadget_sdk::ext::subxt::PolkadotConfig;
             fn current_blueprint(
                 &self,
diff --git a/macros/context-derive/src/subxt.rs b/macros/context-derive/src/subxt.rs
index 60052f1e9..b8e7f2999 100644
--- a/macros/context-derive/src/subxt.rs
+++ b/macros/context-derive/src/subxt.rs
@@ -20,7 +20,7 @@ pub fn generate_context_impl(
     let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
 
     quote! {
-        impl #impl_generics gadget_sdk::ctx::TangleClientContext for #name #ty_generics #where_clause {
+        impl #impl_generics gadget_sdk::contexts::TangleClientContext for #name #ty_generics #where_clause {
             type Config = gadget_sdk::ext::subxt::PolkadotConfig;
             fn tangle_client(&self) -> impl core::future::Future<Output = Result<gadget_sdk::ext::subxt::OnlineClient<Self::Config>, gadget_sdk::ext::subxt::Error>> {
                 use gadget_sdk::ext::subxt;
diff --git a/sdk/src/contexts/eigenlayer.rs b/sdk/src/contexts/eigenlayer.rs
new file mode 100644
index 000000000..17f034f33
--- /dev/null
+++ b/sdk/src/contexts/eigenlayer.rs
@@ -0,0 +1,132 @@
+use alloy_primitives::{Address, Bytes, FixedBytes, U256};
+use eigensdk::client_avsregistry::reader::AvsRegistryChainReader;
+use eigensdk::client_avsregistry::writer::AvsRegistryChainWriter;
+use eigensdk::client_elcontracts::writer::Operator;
+use eigensdk::services_avsregistry::chaincaller::AvsRegistryServiceChainCaller;
+use eigensdk::services_blsaggregation::bls_agg::BlsAggregatorService;
+use eigensdk::services_operatorsinfo::operatorsinfo_inmemory::OperatorInfoServiceInMemory;
+use eigensdk::types::operator::OperatorPubKeys;
+use eigensdk::utils::binding::OperatorStateRetriever;
+use eigensdk::utils::binding::StakeRegistry::StakeUpdate;
+use num_bigint::BigInt;
+use std::collections::HashMap;
+
+/// `EigenlayerContext` trait provides access to Eigenlayer utilities
+#[async_trait::async_trait]
+pub trait EigenlayerContext {
+    /// Provides a reader for the AVS registry.
+    async fn avs_registry_reader(
+        &self,
+    ) -> color_eyre::Result<AvsRegistryChainReader, std::io::Error>;
+
+    /// Provides a writer for the AVS registry.
+    async fn avs_registry_writer(
+        &self,
+        private_key: String,
+    ) -> color_eyre::Result<AvsRegistryChainWriter, std::io::Error>;
+
+    /// Provides an operator info service.
+    async fn operator_info_service_in_memory(
+        &self,
+    ) -> color_eyre::Result<OperatorInfoServiceInMemory, std::io::Error>;
+
+    /// Provides an AVS registry service chain caller.
+    async fn avs_registry_service_chain_caller_in_memory(
+        &self,
+    ) -> color_eyre::Result<
+        AvsRegistryServiceChainCaller<AvsRegistryChainReader, OperatorInfoServiceInMemory>,
+        std::io::Error,
+    >;
+
+    /// Provides a BLS aggregation service.
+    async fn bls_aggregation_service_in_memory(
+        &self,
+    ) -> color_eyre::Result<
+        BlsAggregatorService<
+            AvsRegistryServiceChainCaller<AvsRegistryChainReader, OperatorInfoServiceInMemory>,
+        >,
+        std::io::Error,
+    >;
+
+    /// Get Operator stake in Quorums at a given block.
+    async fn get_operator_stake_in_quorums_at_block(
+        &self,
+        block_number: u32,
+        quorum_numbers: Bytes,
+    ) -> color_eyre::Result<Vec<Vec<OperatorStateRetriever::Operator>>, std::io::Error>;
+
+    /// Get an Operator's stake in Quorums at current block.
+    async fn get_operator_stake_in_quorums_at_current_block(
+        &self,
+        operator_id: FixedBytes<32>,
+    ) -> color_eyre::Result<HashMap<u8, BigInt>, std::io::Error>;
+
+    /// Get an Operator by ID.
+    async fn get_operator_by_id(
+        &self,
+        operator_id: [u8; 32],
+    ) -> color_eyre::Result<Address, std::io::Error>;
+
+    /// Get an Operator stake history.
+    async fn get_operator_stake_history(
+        &self,
+        operator_id: FixedBytes<32>,
+        quorum_number: u8,
+    ) -> color_eyre::Result<Vec<StakeUpdate>, std::io::Error>;
+
+    /// Get an Operator stake update at a given index.
+    async fn get_operator_stake_update_at_index(
+        &self,
+        quorum_number: u8,
+        operator_id: FixedBytes<32>,
+        index: U256,
+    ) -> color_eyre::Result<StakeUpdate, std::io::Error>;
+
+    /// Get an Operator's stake at a given block number.
+    async fn get_operator_stake_at_block_number(
+        &self,
+        operator_id: FixedBytes<32>,
+        quorum_number: u8,
+        block_number: u32,
+    ) -> color_eyre::Result<u128, std::io::Error>;
+
+    /// Get an Operator's [`details`](OperatorDetails).
+    async fn get_operator_details(
+        &self,
+        operator_addr: Address,
+    ) -> color_eyre::Result<Operator, std::io::Error>;
+
+    /// Get an Operator's latest stake update.
+    async fn get_latest_stake_update(
+        &self,
+        operator_id: FixedBytes<32>,
+        quorum_number: u8,
+    ) -> color_eyre::Result<StakeUpdate, std::io::Error>;
+
+    /// Get an Operator's ID as [`FixedBytes`] from its [`Address`].
+    async fn get_operator_id(
+        &self,
+        operator_addr: Address,
+    ) -> color_eyre::Result<FixedBytes<32>, std::io::Error>;
+
+    /// Get the total stake at a given block number from a given index.
+    async fn get_total_stake_at_block_number_from_index(
+        &self,
+        quorum_number: u8,
+        block_number: u32,
+        index: U256,
+    ) -> color_eyre::Result<u128, std::io::Error>;
+
+    /// Get the total stake history length of a given quorum.
+    async fn get_total_stake_history_length(
+        &self,
+        quorum_number: u8,
+    ) -> color_eyre::Result<U256, std::io::Error>;
+
+    /// Provides the public keys of existing registered operators within the provided block range.
+    async fn query_existing_registered_operator_pub_keys(
+        &self,
+        start_block: u64,
+        to_block: u64,
+    ) -> color_eyre::Result<(Vec<Address>, Vec<OperatorPubKeys>), std::io::Error>;
+}
diff --git a/sdk/src/contexts/evm_provider.rs b/sdk/src/contexts/evm_provider.rs
new file mode 100644
index 000000000..757a00ade
--- /dev/null
+++ b/sdk/src/contexts/evm_provider.rs
@@ -0,0 +1,12 @@
+use std::future::Future;
+
+/// `EVMProviderContext` trait provides access to the EVM provider from the context.
+pub trait EVMProviderContext {
+    type Network: alloy_network::Network;
+    type Transport: alloy_transport::Transport + Clone;
+    type Provider: alloy_provider::Provider<Self::Transport, Self::Network>;
+    /// Get the EVM provider from the context.
+    fn evm_provider(
+        &self,
+    ) -> impl Future<Output = color_eyre::Result<Self::Provider, alloy_transport::TransportError>>;
+}
diff --git a/sdk/src/contexts/gossip_network.rs b/sdk/src/contexts/gossip_network.rs
new file mode 100644
index 000000000..e048a3a39
--- /dev/null
+++ b/sdk/src/contexts/gossip_network.rs
@@ -0,0 +1,5 @@
+/// `GossipNetworkContext` trait provides access to the network client from the context.
+pub trait GossipNetworkContext {
+    /// Get the Goossip client from the context.
+    fn gossip_network(&self) -> &crate::network::gossip::GossipHandle;
+}
diff --git a/sdk/src/contexts/keystore.rs b/sdk/src/contexts/keystore.rs
new file mode 100644
index 000000000..98c1c67b7
--- /dev/null
+++ b/sdk/src/contexts/keystore.rs
@@ -0,0 +1,7 @@
+use crate::keystore::backend::GenericKeyStore;
+
+/// `KeystoreContext` trait provides access to the generic keystore from the context.
+pub trait KeystoreContext<RwLock: lock_api::RawRwLock> {
+    /// Get the keystore client from the context.
+    fn keystore(&self) -> color_eyre::Result<GenericKeyStore<RwLock>, crate::config::Error>;
+}
diff --git a/sdk/src/contexts/mod.rs b/sdk/src/contexts/mod.rs
new file mode 100644
index 000000000..8e4a801fb
--- /dev/null
+++ b/sdk/src/contexts/mod.rs
@@ -0,0 +1,68 @@
+//! A set of traits and utilities that provide a common interface for interacting with the Gadget SDK.
+//!
+//! Usually, when you need access to the SDK, you will need to pass the Context to your jobs/functions. In your code, you will create a struct that encapsulates all the things that you would need from outside world from your job.
+//! for example, if you need to interact with the network, you will need to have a network client in your struct. If you need to interact with the database storage, you will need to have a db client in your struct. And so on.
+//!
+//! This module provides a set of traits that you can implement for your struct to make it a context-aware struct by adding new functionalities to it.
+//!
+//! # Example
+//!
+//! ```rust,no_run
+//! use gadget_sdk::config::StdGadgetConfiguration;
+//! use gadget_sdk::contexts::KeystoreContext;
+//! use gadget_sdk::event_listener::tangle::jobs::{
+//!     services_post_processor, services_pre_processor,
+//! };
+//! use gadget_sdk::event_listener::tangle::TangleEventListener;
+//! use gadget_sdk::job;
+//! use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled;
+//!
+//! // This your struct that encapsulates all the things you need from outside world.
+//! // By deriving KeystoreContext, you can now access the keystore client from your struct.
+//! #[derive(Clone, Debug, KeystoreContext)]
+//! struct MyContext {
+//!     foo: String,
+//!     bar: u64,
+//!     #[config]
+//!     sdk_config: StdGadgetConfiguration,
+//! }
+//!
+//! #[job(
+//!     id = 0,
+//!     params(who),
+//!     result(_),
+//!     event_listener(
+//!         listener = TangleEventListener<MyContext, JobCalled>,
+//!         pre_processor = services_pre_processor,
+//!         post_processor = services_post_processor,
+//!     )
+//! )]
+//! async fn my_job(who: String, ctx: MyContext) -> Result<String, std::convert::Infallible> {
+//!     // Access the keystore client from the context.
+//!     let keystore = ctx.keystore();
+//!     // Do something with the keystore client.
+//!     // ...
+//!     Ok(format!("Hello, {}!", who))
+//! }
+//! ```
+
+// derives
+pub use gadget_context_derive::*;
+pub use num_bigint::BigInt;
+
+// A macro that takes an arbitrary mod, like "eigenlayer", then writes that as well as a line saying "pub use eigenlayer::*;" to the file.
+// This way, we can easily add new modules to the SDK without having to manually import each time
+macro_rules! create_module_derive {
+    ($module:ident) => {
+        mod $module;
+        pub use $module::*;
+    };
+}
+
+create_module_derive!(eigenlayer);
+create_module_derive!(evm_provider);
+create_module_derive!(gossip_network);
+create_module_derive!(keystore);
+create_module_derive!(mpc);
+create_module_derive!(services);
+create_module_derive!(tangle_client);
diff --git a/sdk/src/contexts/mpc.rs b/sdk/src/contexts/mpc.rs
new file mode 100644
index 000000000..73eb0dd2f
--- /dev/null
+++ b/sdk/src/contexts/mpc.rs
@@ -0,0 +1,63 @@
+use crate::network::NetworkMultiplexer;
+use round_based::PartyIndex;
+use std::collections::BTreeMap;
+use std::sync::Arc;
+use subxt_core::utils::AccountId32;
+
+/// `MPCContext` trait provides access to MPC (Multi-Party Computation) functionality from the context.
+#[async_trait::async_trait]
+pub trait MPCContext {
+    /// Returns a reference to the configuration
+    fn config(&self) -> &crate::config::StdGadgetConfiguration;
+
+    /// Returns the network protocol identifier
+    fn network_protocol(&self) -> String;
+
+    /// Creates a network delivery wrapper for MPC communication
+    fn create_network_delivery_wrapper<M>(
+        &self,
+        mux: Arc<NetworkMultiplexer>,
+        party_index: PartyIndex,
+        task_hash: [u8; 32],
+        parties: BTreeMap<PartyIndex, subxt_core::ext::sp_core::ecdsa::Public>,
+    ) -> color_eyre::Result<
+        crate::network::round_based_compat::NetworkDeliveryWrapper<M>,
+        crate::Error,
+    >
+    where
+        M: Clone
+            + Send
+            + Unpin
+            + 'static
+            + serde::Serialize
+            + serde::de::DeserializeOwned
+            + round_based::ProtocolMessage;
+
+    /// Gets the party index from the participants map
+    async fn get_party_index(&self) -> color_eyre::Result<PartyIndex, crate::Error>;
+
+    /// Gets the participants in the MPC protocol
+    async fn get_participants(
+        &self,
+        client: &subxt::OnlineClient<crate::clients::tangle::runtime::TangleConfig>,
+    ) -> color_eyre::Result<BTreeMap<PartyIndex, AccountId32>, crate::Error>;
+
+    /// Gets the current blueprint ID
+    fn blueprint_id(&self) -> color_eyre::Result<u64>;
+
+    /// Gets the party index and operator mapping
+    async fn get_party_index_and_operators(
+        &self,
+    ) -> color_eyre::Result<(
+        usize,
+        BTreeMap<AccountId32, crate::subxt_core::ext::sp_core::ecdsa::Public>,
+    )>;
+
+    /// Gets the ECDSA keys for all current service operators
+    async fn current_service_operators_ecdsa_keys(
+        &self,
+    ) -> color_eyre::Result<BTreeMap<AccountId32, crate::subxt_core::ext::sp_core::ecdsa::Public>>;
+
+    /// Gets the current call ID for this job
+    async fn current_call_id(&self) -> color_eyre::Result<u64>;
+}
diff --git a/sdk/src/contexts/services.rs b/sdk/src/contexts/services.rs
new file mode 100644
index 000000000..05d0329da
--- /dev/null
+++ b/sdk/src/contexts/services.rs
@@ -0,0 +1,100 @@
+use std::future::Future;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::{Service, ServiceBlueprint};
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::sp_arithmetic::per_things::Percent;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::pallet_multi_asset_delegation::types::operator::OperatorMetadata;
+use tangle_subxt::tangle_testnet_runtime::api::assets::events::burned::Balance;
+use tangle_subxt::tangle_testnet_runtime::api::assets::events::accounts_destroyed::AssetId;
+use tangle_subxt::tangle_testnet_runtime::api::system::storage::types::number::Number;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::pallet_multi_asset_delegation::types::delegator::DelegatorMetadata;
+
+/// `ServicesContext` trait provides access to the current service and current blueprint from the context.
+pub trait ServicesContext {
+    type Config: subxt::Config;
+    /// Get the current blueprint information from the context.
+    fn current_blueprint(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+    ) -> impl Future<Output = color_eyre::Result<ServiceBlueprint, subxt::Error>>;
+
+    /// Query the current blueprint owner from the context.
+    fn current_blueprint_owner(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+    ) -> impl Future<Output = color_eyre::Result<subxt::utils::AccountId32, subxt::Error>>;
+
+    /// Get the current service operators with their restake exposure  from the context.
+    /// This function will return a list of service operators that are selected to run this service
+    /// instance.
+    fn current_service_operators(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+    ) -> impl Future<Output = color_eyre::Result<Vec<(subxt::utils::AccountId32, Percent)>, subxt::Error>>;
+
+    #[allow(clippy::type_complexity)]
+    /// Get metadata for a list of operators from the context.
+    fn operators_metadata(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+        operators: Vec<subxt::utils::AccountId32>,
+    ) -> impl Future<
+        Output = color_eyre::Result<
+            Vec<(
+                subxt::utils::AccountId32,
+                OperatorMetadata<subxt::utils::AccountId32, Balance, AssetId>,
+            )>,
+            subxt::Error,
+        >,
+    >;
+
+    /// Get metadata for a single operator from the context.
+    /// This function will return the metadata for a single operator.
+    fn operator_metadata(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+        operator: subxt::utils::AccountId32,
+    ) -> impl Future<
+        Output = color_eyre::Result<
+            Option<OperatorMetadata<subxt::utils::AccountId32, Balance, AssetId>>,
+            subxt::Error,
+        >,
+    >;
+
+    /// Get the current service instance from the context.
+    fn service_instance(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+    ) -> impl Future<
+        Output = color_eyre::Result<
+            Service<subxt::utils::AccountId32, Number, AssetId>,
+            subxt::Error,
+        >,
+    >;
+
+    #[allow(clippy::type_complexity)]
+    /// Get delegations for a list of operators from the context.
+    fn operator_delegations(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+        operators: Vec<subxt::utils::AccountId32>,
+    ) -> impl Future<
+        Output = color_eyre::Result<
+            Vec<(
+                subxt::utils::AccountId32, // operator
+                Option<DelegatorMetadata<subxt::utils::AccountId32, AssetId, Balance>>,
+            )>,
+            subxt::Error,
+        >,
+    >;
+
+    /// Get delegations for a single operator from the context.
+    fn operator_delegation(
+        &self,
+        client: &subxt::OnlineClient<Self::Config>,
+        operator: subxt::utils::AccountId32,
+    ) -> impl Future<
+        Output = color_eyre::Result<
+            Option<DelegatorMetadata<subxt::utils::AccountId32, AssetId, Balance>>,
+            subxt::Error,
+        >,
+    >;
+}
diff --git a/sdk/src/contexts/tangle_client.rs b/sdk/src/contexts/tangle_client.rs
new file mode 100644
index 000000000..031dbb5c2
--- /dev/null
+++ b/sdk/src/contexts/tangle_client.rs
@@ -0,0 +1,10 @@
+use std::future::Future;
+
+/// `TangleClientContext` trait provides access to the Tangle client from the context.
+pub trait TangleClientContext {
+    type Config: subxt::Config;
+    /// Get the Tangle client from the context.
+    fn tangle_client(
+        &self,
+    ) -> impl Future<Output = color_eyre::Result<subxt::OnlineClient<Self::Config>, subxt::Error>>;
+}
diff --git a/sdk/src/ctx.rs b/sdk/src/ctx.rs
deleted file mode 100644
index 358f68276..000000000
--- a/sdk/src/ctx.rs
+++ /dev/null
@@ -1,307 +0,0 @@
-//! A set of traits and utilities that provide a common interface for interacting with the Gadget SDK.
-//!
-//! Usually, when you need access to the SDK, you will need to pass the Context to your jobs/functions. In your code, you will create a struct that encapsulates all the things that you would need from outside world from your job.
-//! for example, if you need to interact with the network, you will need to have a network client in your struct. If you need to interact with the database storage, you will need to have a db client in your struct. And so on.
-//!
-//! This module provides a set of traits that you can implement for your struct to make it a context-aware struct by adding new functionalities to it.
-//!
-//! # Example
-//!
-//! ```rust,no_run
-//! use gadget_sdk::config::StdGadgetConfiguration;
-//! use gadget_sdk::ctx::KeystoreContext;
-//! use gadget_sdk::event_listener::tangle::jobs::{
-//!     services_post_processor, services_pre_processor,
-//! };
-//! use gadget_sdk::event_listener::tangle::TangleEventListener;
-//! use gadget_sdk::job;
-//! use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled;
-//!
-//! // This your struct that encapsulates all the things you need from outside world.
-//! // By deriving KeystoreContext, you can now access the keystore client from your struct.
-//! #[derive(Clone, Debug, KeystoreContext)]
-//! struct MyContext {
-//!     foo: String,
-//!     bar: u64,
-//!     #[config]
-//!     sdk_config: StdGadgetConfiguration,
-//! }
-//!
-//! #[job(
-//!     id = 0,
-//!     params(who),
-//!     result(_),
-//!     event_listener(
-//!         listener = TangleEventListener<MyContext, JobCalled>,
-//!         pre_processor = services_pre_processor,
-//!         post_processor = services_post_processor,
-//!     )
-//! )]
-//! async fn my_job(who: String, ctx: MyContext) -> Result<String, std::convert::Infallible> {
-//!     // Access the keystore client from the context.
-//!     let keystore = ctx.keystore();
-//!     // Do something with the keystore client.
-//!     // ...
-//!     Ok(format!("Hello, {}!", who))
-//! }
-//! ```
-
-use crate::keystore::backend::GenericKeyStore;
-use alloy_primitives::{Address, Bytes, FixedBytes, U256};
-use tangle_subxt::tangle_testnet_runtime::api::assets::events::accounts_destroyed::AssetId;
-use tangle_subxt::tangle_testnet_runtime::api::assets::events::burned::Balance;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::pallet_multi_asset_delegation::types::delegator::DelegatorMetadata;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::pallet_multi_asset_delegation::types::operator::OperatorMetadata;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::Service;
-use tangle_subxt::tangle_testnet_runtime::api::system::storage::types::number::Number;
-use core::future::Future;
-use eigensdk::types::operator::{Operator, OperatorPubKeys};
-use eigensdk::utils::binding::OperatorStateRetriever;
-use eigensdk::utils::binding::StakeRegistry::StakeUpdate;
-use eigensdk::{
-    client_avsregistry::{reader::AvsRegistryChainReader, writer::AvsRegistryChainWriter},
-    services_avsregistry::chaincaller::AvsRegistryServiceChainCaller,
-    services_blsaggregation::bls_agg::BlsAggregatorService,
-    services_operatorsinfo::operatorsinfo_inmemory::OperatorInfoServiceInMemory,
-};
-pub use num_bigint::BigInt;
-use std::collections::HashMap;
-// derives
-pub use gadget_context_derive::*;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::{
-    sp_arithmetic::per_things::Percent, tangle_primitives::services::ServiceBlueprint,
-};
-
-/// `KeystoreContext` trait provides access to the generic keystore from the context.
-pub trait KeystoreContext<RwLock: lock_api::RawRwLock> {
-    /// Get the keystore client from the context.
-    fn keystore(&self) -> Result<GenericKeyStore<RwLock>, crate::config::Error>;
-}
-
-/// `GossipNetworkContext` trait provides access to the network client from the context.
-pub trait GossipNetworkContext {
-    /// Get the Goossip client from the context.
-    fn gossip_network(&self) -> &crate::network::gossip::GossipHandle;
-}
-
-/// `EVMProviderContext` trait provides access to the EVM provider from the context.
-pub trait EVMProviderContext {
-    type Network: alloy_network::Network;
-    type Transport: alloy_transport::Transport + Clone;
-    type Provider: alloy_provider::Provider<Self::Transport, Self::Network>;
-    /// Get the EVM provider from the context.
-    fn evm_provider(
-        &self,
-    ) -> impl Future<Output = Result<Self::Provider, alloy_transport::TransportError>>;
-}
-
-/// `TangleClientContext` trait provides access to the Tangle client from the context.
-pub trait TangleClientContext {
-    type Config: subxt::Config;
-    /// Get the Tangle client from the context.
-    fn tangle_client(
-        &self,
-    ) -> impl Future<Output = Result<subxt::OnlineClient<Self::Config>, subxt::Error>>;
-}
-
-/// `ServicesContext` trait provides access to the current service and current blueprint from the context.
-pub trait ServicesContext {
-    type Config: subxt::Config;
-    /// Get the current blueprint information from the context.
-    fn current_blueprint(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-    ) -> impl Future<Output = Result<ServiceBlueprint, subxt::Error>>;
-
-    /// Query the current blueprint owner from the context.
-    fn current_blueprint_owner(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-    ) -> impl Future<Output = Result<subxt::utils::AccountId32, subxt::Error>>;
-
-    /// Get the current service operators with their restake exposure  from the context.
-    /// This function will return a list of service operators that are selected to run this service
-    /// instance.
-    fn current_service_operators(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-    ) -> impl Future<Output = Result<Vec<(subxt::utils::AccountId32, Percent)>, subxt::Error>>;
-
-    #[allow(clippy::type_complexity)]
-    /// Get metadata for a list of operators from the context.
-    fn operators_metadata(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-        operators: Vec<subxt::utils::AccountId32>,
-    ) -> impl Future<
-        Output = Result<
-            Vec<(
-                subxt::utils::AccountId32,
-                OperatorMetadata<subxt::utils::AccountId32, Balance, AssetId>,
-            )>,
-            subxt::Error,
-        >,
-    >;
-
-    /// Get metadata for a single operator from the context.
-    /// This function will return the metadata for a single operator.
-    fn operator_metadata(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-        operator: subxt::utils::AccountId32,
-    ) -> impl Future<
-        Output = Result<
-            Option<OperatorMetadata<subxt::utils::AccountId32, Balance, AssetId>>,
-            subxt::Error,
-        >,
-    >;
-
-    /// Get the current service instance from the context.
-    fn service_instance(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-    ) -> impl Future<Output = Result<Service<subxt::utils::AccountId32, Number, AssetId>, subxt::Error>>;
-
-    #[allow(clippy::type_complexity)]
-    /// Get delegations for a list of operators from the context.
-    fn operator_delegations(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-        operators: Vec<subxt::utils::AccountId32>,
-    ) -> impl Future<
-        Output = Result<
-            Vec<(
-                subxt::utils::AccountId32, // operator
-                Option<DelegatorMetadata<subxt::utils::AccountId32, AssetId, Balance>>,
-            )>,
-            subxt::Error,
-        >,
-    >;
-
-    /// Get delegations for a single operator from the context.
-    fn operator_delegation(
-        &self,
-        client: &subxt::OnlineClient<Self::Config>,
-        operator: subxt::utils::AccountId32,
-    ) -> impl Future<
-        Output = Result<
-            Option<DelegatorMetadata<subxt::utils::AccountId32, AssetId, Balance>>,
-            subxt::Error,
-        >,
-    >;
-}
-
-/// `EigenlayerContext` trait provides access to Eigenlayer utilities
-#[async_trait::async_trait]
-pub trait EigenlayerContext {
-    /// Provides a reader for the AVS registry.
-    async fn avs_registry_reader(&self) -> Result<AvsRegistryChainReader, std::io::Error>;
-
-    /// Provides a writer for the AVS registry.
-    async fn avs_registry_writer(
-        &self,
-        private_key: String,
-    ) -> Result<AvsRegistryChainWriter, std::io::Error>;
-
-    /// Provides an operator info service.
-    async fn operator_info_service_in_memory(
-        &self,
-    ) -> Result<OperatorInfoServiceInMemory, std::io::Error>;
-
-    /// Provides an AVS registry service chain caller.
-    async fn avs_registry_service_chain_caller_in_memory(
-        &self,
-    ) -> Result<
-        AvsRegistryServiceChainCaller<AvsRegistryChainReader, OperatorInfoServiceInMemory>,
-        std::io::Error,
-    >;
-
-    /// Provides a BLS aggregation service.
-    async fn bls_aggregation_service_in_memory(
-        &self,
-    ) -> Result<
-        BlsAggregatorService<
-            AvsRegistryServiceChainCaller<AvsRegistryChainReader, OperatorInfoServiceInMemory>,
-        >,
-        std::io::Error,
-    >;
-
-    /// Get Operator stake in Quorums at a given block.
-    async fn get_operator_stake_in_quorums_at_block(
-        &self,
-        block_number: u32,
-        quorum_numbers: Bytes,
-    ) -> Result<Vec<Vec<OperatorStateRetriever::Operator>>, std::io::Error>;
-
-    /// Get an Operator's stake in Quorums at current block.
-    async fn get_operator_stake_in_quorums_at_current_block(
-        &self,
-        operator_id: FixedBytes<32>,
-    ) -> Result<HashMap<u8, BigInt>, std::io::Error>;
-
-    /// Get an Operator by ID.
-    async fn get_operator_by_id(&self, operator_id: [u8; 32]) -> Result<Address, std::io::Error>;
-
-    /// Get an Operator stake history.
-    async fn get_operator_stake_history(
-        &self,
-        operator_id: FixedBytes<32>,
-        quorum_number: u8,
-    ) -> Result<Vec<StakeUpdate>, std::io::Error>;
-
-    /// Get an Operator stake update at a given index.
-    async fn get_operator_stake_update_at_index(
-        &self,
-        quorum_number: u8,
-        operator_id: FixedBytes<32>,
-        index: U256,
-    ) -> Result<StakeUpdate, std::io::Error>;
-
-    /// Get an Operator's stake at a given block number.
-    async fn get_operator_stake_at_block_number(
-        &self,
-        operator_id: FixedBytes<32>,
-        quorum_number: u8,
-        block_number: u32,
-    ) -> Result<u128, std::io::Error>;
-
-    /// Get an Operator's [`details`](OperatorDetails).
-    async fn get_operator_details(
-        &self,
-        operator_addr: Address,
-    ) -> Result<Operator, std::io::Error>;
-
-    /// Get an Operator's latest stake update.
-    async fn get_latest_stake_update(
-        &self,
-        operator_id: FixedBytes<32>,
-        quorum_number: u8,
-    ) -> Result<StakeUpdate, std::io::Error>;
-
-    /// Get an Operator's ID as [`FixedBytes`] from its [`Address`].
-    async fn get_operator_id(
-        &self,
-        operator_addr: Address,
-    ) -> Result<FixedBytes<32>, std::io::Error>;
-
-    /// Get the total stake at a given block number from a given index.
-    async fn get_total_stake_at_block_number_from_index(
-        &self,
-        quorum_number: u8,
-        block_number: u32,
-        index: U256,
-    ) -> Result<u128, std::io::Error>;
-
-    /// Get the total stake history length of a given quorum.
-    async fn get_total_stake_history_length(
-        &self,
-        quorum_number: u8,
-    ) -> Result<U256, std::io::Error>;
-
-    /// Provides the public keys of existing registered operators within the provided block range.
-    async fn query_existing_registered_operator_pub_keys(
-        &self,
-        start_block: u64,
-        to_block: u64,
-    ) -> Result<(Vec<Address>, Vec<OperatorPubKeys>), std::io::Error>;
-}
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
index 7feadd159..945bf0f03 100644
--- a/sdk/src/lib.rs
+++ b/sdk/src/lib.rs
@@ -63,8 +63,7 @@ pub mod tracer;
 pub mod tx;
 
 /// Gadget Context and context extensions
-pub mod ctx;
-
+pub mod contexts;
 pub mod docker;
 pub mod utils;
 
@@ -72,11 +71,15 @@ pub mod utils;
 pub use alloy_rpc_types;
 pub use async_trait;
 pub use clap;
+pub use color_eyre;
 pub use error::Error;
 pub use futures;
 pub use gadget_blueprint_proc_macro::*;
 pub use libp2p;
 pub use parking_lot;
+pub use round_based;
+pub use serde;
+pub use subxt;
 pub use subxt_core;
 pub use tangle_subxt;
 pub use tokio;


Test patch:
diff --git a/blueprint-test-utils/Cargo.toml b/blueprint-test-utils/Cargo.toml
index 958c2d80c..b9900b983 100644
--- a/blueprint-test-utils/Cargo.toml
+++ b/blueprint-test-utils/Cargo.toml
@@ -58,6 +58,7 @@ alloy-transport = { workspace = true }
 testcontainers = { workspace = true }
 uuid = { workspace = true, features = ["v4"] }
 tempfile = { workspace = true }
+cargo_toml = { workspace = true }
 
 [dev-dependencies]
 cargo_metadata = { workspace = true }
diff --git a/blueprint-test-utils/src/anvil.rs b/blueprint-test-utils/src/anvil.rs
index 56773c6f0..74bbff07d 100644
--- a/blueprint-test-utils/src/anvil.rs
+++ b/blueprint-test-utils/src/anvil.rs
@@ -101,3 +101,16 @@ pub async fn mine_anvil_blocks(container: &ContainerAsync<GenericImage>, n: u32)
     output.stdout_to_vec().await.unwrap();
     assert_eq!(output.exit_code().await.unwrap().unwrap(), 0);
 }
+
+pub const ANVIL_PRIVATE_KEYS: [&str; 10] = [
+    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
+    "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
+    "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
+    "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
+    "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
+    "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
+    "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
+    "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
+    "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
+    "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
+];
diff --git a/blueprint-test-utils/src/lib.rs b/blueprint-test-utils/src/lib.rs
index 0b500477a..480bd8af7 100644
--- a/blueprint-test-utils/src/lib.rs
+++ b/blueprint-test-utils/src/lib.rs
@@ -1,5 +1,5 @@
 #![allow(unused_imports)]
-use crate::test_ext::{ANVIL_PRIVATE_KEYS, NAME_IDS};
+use crate::test_ext::NAME_IDS;
 use api::services::events::JobResultSubmitted;
 use blueprint_manager::config::BlueprintManagerConfig;
 use blueprint_manager::executor::BlueprintManagerHandle;
@@ -8,7 +8,7 @@ use gadget_sdk::clients::tangle::runtime::{TangleClient, TangleConfig};
 use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api;
 use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::runtime_types;
 use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::runtime_types::sp_arithmetic::per_things::Percent;
-use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::call::{Args, Job};
+pub use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::call::{Args, Job};
 use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::create_blueprint::Blueprint;
 use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::register::{Preferences, RegistrationArgs};
 use gadget_sdk::keystore;
@@ -24,17 +24,17 @@ use std::net::IpAddr;
 use std::path::{Path, PathBuf};
 use std::time::Duration;
 use alloy_primitives::hex;
+use cargo_toml::Manifest;
 use color_eyre::eyre::eyre;
 use subxt::tx::{Signer, TxProgress};
 use subxt::utils::AccountId32;
 use url::Url;
 use uuid::Uuid;
-use gadget_sdk::{info, error};
+use gadget_sdk::{error, info};
 
+pub use cargo_tangle::deploy::Opts;
 pub use gadget_sdk::logging::setup_log;
 
-use cargo_tangle::deploy::Opts;
-
 pub type InputValue = runtime_types::tangle_primitives::services::field::Field<AccountId32>;
 pub type OutputValue = runtime_types::tangle_primitives::services::field::Field<AccountId32>;
 
@@ -42,10 +42,16 @@ pub mod anvil;
 pub mod binding;
 pub mod eigenlayer_test_env;
 pub mod helpers;
+pub mod mpc;
 pub mod symbiotic_test_env;
 pub mod sync;
 pub mod tangle;
 pub mod test_ext;
+use anvil::ANVIL_PRIVATE_KEYS;
+pub use gadget_sdk;
+pub use gadget_sdk::ext::blueprint_serde::BoundedVec;
+pub use tangle::transactions::{get_next_call_id, submit_job, wait_for_completion_of_tangle_job};
+pub use tempfile;
 
 pub type TestClient = TangleClient;
 
@@ -358,419 +364,38 @@ pub fn inject_random_key<P: AsRef<Path>>(keystore_path: P) -> color_eyre::Result
     Ok(())
 }
 
-pub async fn create_blueprint(
-    client: &TestClient,
-    account_id: &TanglePairSigner<sp_core::sr25519::Pair>,
-    blueprint: Blueprint,
-) -> Result<(), Box<dyn Error>> {
-    let call = api::tx().services().create_blueprint(blueprint);
-    let res = client
-        .tx()
-        .sign_and_submit_then_watch_default(&call, account_id)
-        .await?;
-    wait_for_in_block_success(res).await?;
-    Ok(())
-}
-
-pub async fn join_delegators(
-    client: &TestClient,
-    account_id: &TanglePairSigner<sp_core::sr25519::Pair>,
-) -> Result<(), Box<dyn Error>> {
-    info!("Joining delegators ...");
-    let call_pre = api::tx()
-        .multi_asset_delegation()
-        .join_operators(1_000_000_000_000_000);
-    let res_pre = client
-        .tx()
-        .sign_and_submit_then_watch_default(&call_pre, account_id)
-        .await?;
-
-    wait_for_in_block_success(res_pre).await?;
-    Ok(())
-}
-
-pub async fn register_blueprint(
-    client: &TestClient,
-    account_id: &TanglePairSigner<sp_core::sr25519::Pair>,
-    blueprint_id: u64,
-    preferences: Preferences,
-    registration_args: RegistrationArgs,
-) -> Result<(), Box<dyn Error>> {
-    info!("Registering to blueprint {blueprint_id} to become an operator ...");
-    let call = api::tx()
-        .services()
-        .register(blueprint_id, preferences, registration_args);
-    let res = client
-        .tx()
-        .sign_and_submit_then_watch_default(&call, account_id)
-        .await?;
-    wait_for_in_block_success(res).await?;
-    Ok(())
-}
-
-pub async fn submit_job(
-    client: &TestClient,
-    user: &TanglePairSigner<sp_core::sr25519::Pair>,
-    service_id: u64,
-    job_type: Job,
-    job_params: Args,
-) -> Result<(), Box<dyn Error>> {
-    let call = api::tx().services().call(service_id, job_type, job_params);
-    let res = client
-        .tx()
-        .sign_and_submit_then_watch_default(&call, user)
-        .await?;
-    wait_for_in_block_success(res).await?;
-    Ok(())
-}
-
-/// Requests a service with a given blueprint. This is meant for testing, and will allow any node
-/// to make a call to run a service, and will have all nodes running the service.
-pub async fn request_service(
-    client: &TestClient,
-    user: &TanglePairSigner<sp_core::sr25519::Pair>,
-    blueprint_id: u64,
-    test_nodes: Vec<AccountId32>,
-) -> Result<(), Box<dyn Error>> {
-    let call = api::tx().services().request(
-        blueprint_id,
-        test_nodes.clone(),
-        test_nodes,
-        Default::default(),
-        vec![0],
-        1000,
-    );
-    let res = client
-        .tx()
-        .sign_and_submit_then_watch_default(&call, user)
-        .await?;
-    wait_for_in_block_success(res).await?;
-    Ok(())
-}
-
-pub async fn wait_for_in_block_success(
-    mut res: TxProgress<TangleConfig, TestClient>,
-) -> Result<(), Box<dyn Error>> {
-    while let Some(Ok(event)) = res.next().await {
-        let Some(block) = event.as_in_block() else {
-            continue;
-        };
-        block.wait_for_success().await?;
-    }
-    Ok(())
-}
-
-pub async fn wait_for_completion_of_tangle_job(
-    client: &TestClient,
-    service_id: u64,
-    call_id: u64,
-    required_count: usize,
-) -> Result<JobResultSubmitted, Box<dyn Error>> {
-    let mut count = 0;
-    let mut blocks = client.blocks().subscribe_best().await?;
-    while let Some(Ok(block)) = blocks.next().await {
-        let events = block.events().await?;
-        let results = events.find::<JobResultSubmitted>().collect::<Vec<_>>();
-        info!(
-            %service_id,
-            %call_id,
-            %required_count,
-            %count,
-            "Waiting for job completion. Found {} results ...",
-            results.len()
-        );
-        for result in results {
-            match result {
-                Ok(result) => {
-                    if result.service_id == service_id && result.call_id == call_id {
-                        count += 1;
-                        if count == required_count {
-                            return Ok(result);
-                        }
-                    }
-                }
-                Err(err) => {
-                    error!("Failed to get job result: {err}");
-                }
-            }
-        }
+/// Returns the output of "git rev-parse --show-toplevel" to get the root of the git repository as a PathBuf.
+/// If it's not in a git repo, default to return the current directory
+pub fn get_blueprint_base_dir() -> PathBuf {
+    let output = std::process::Command::new("git")
+        .arg("rev-parse")
+        .arg("--show-toplevel")
+        .output()
+        .expect("Failed to run git command");
+
+    if output.status.success() {
+        let path = std::str::from_utf8(&output.stdout)
+            .expect("Failed to convert output to string")
+            .trim();
+        PathBuf::from(path)
+    } else {
+        std::env::current_dir().expect("Failed to get current directory")
     }
-    Err("Failed to get job result".into())
-}
-
-pub async fn get_next_blueprint_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
-    let call = api::storage().services().next_blueprint_id();
-    let res = client
-        .storage()
-        .at_latest()
-        .await?
-        .fetch_or_default(&call)
-        .await?;
-    Ok(res)
 }
 
-pub async fn get_next_service_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
-    let call = api::storage().services().next_instance_id();
-    let res = client
-        .storage()
-        .at_latest()
-        .await?
-        .fetch_or_default(&call)
-        .await?;
-    Ok(res)
-}
-
-pub async fn get_next_call_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
-    let call = api::storage().services().next_job_call_id();
-    let res = client
-        .storage()
-        .at_latest()
-        .await?
-        .fetch_or_default(&call)
-        .await?;
-    Ok(res)
-}
-
-/// Approves a service request. This is meant for testing, and will always approve the request.
-pub async fn approve_service(
-    client: &TestClient,
-    caller: &TanglePairSigner<sp_core::sr25519::Pair>,
-    request_id: u64,
-    restaking_percent: u8,
-) -> Result<(), Box<dyn Error>> {
-    gadget_sdk::info!("Approving service request ...");
-    let call = api::tx()
-        .services()
-        .approve(request_id, Percent(restaking_percent));
-    let res = client
-        .tx()
-        .sign_and_submit_then_watch_default(&call, caller)
-        .await?;
-    res.wait_for_finalized_success().await?;
-    Ok(())
-}
-
-pub async fn get_next_request_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
-    gadget_sdk::info!("Fetching next request ID ...");
-    let next_request_id_addr = api::storage().services().next_service_request_id();
-    let next_request_id = client
-        .storage()
-        .at_latest()
-        .await
-        .expect("Failed to fetch latest block")
-        .fetch_or_default(&next_request_id_addr)
-        .await
-        .expect("Failed to fetch next request ID");
-    Ok(next_request_id)
-}
-
-#[macro_export]
-macro_rules! test_blueprint {
-    (
-        $blueprint_path:expr,
-        $blueprint_name:expr,
-        $N:expr,
-        [$($input:expr),+],
-        [$($expected_output:expr),+]
-    ) => {
-        use $crate::{
-            get_next_call_id, run_test_blueprint_manager,
-            submit_job, wait_for_completion_of_tangle_job, Opts, setup_log,
-        };
-
-        use $crate::test_ext::new_test_ext_blueprint_manager;
-
-        #[tokio::test(flavor = "multi_thread")]
-        async fn test_externalities_standard() {
-            setup_log();
-            let mut base_path = std::env::current_dir().expect("Failed to get current directory");
-
-            let tmp_dir = tempfile::TempDir::new().unwrap(); // Create a temporary directory for the keystores
-            let tmp_dir_path = format!("{}", tmp_dir.path().display());
-
-            base_path.push($blueprint_path);
-            base_path
-                .canonicalize()
-                .expect("File could not be normalized");
-
-            let manifest_path = base_path.join("Cargo.toml");
-
-
-            let http_addr = "http://127.0.0.1:9944";
-            let ws_addr = "ws://127.0.0.1:9944";
-
-            let opts = Opts {
-                pkg_name: Some($blueprint_name.to_string()),
-                http_rpc_url: http_addr.to_string(),
-                ws_rpc_url: ws_addr.to_string(),
-                manifest_path,
-                signer: None,
-                signer_evm: None,
-            };
-
-            new_test_ext_blueprint_manager::<$N, 1, String, _, _>(
-                tmp_dir_path,
-                opts,
-                run_test_blueprint_manager,
-            )
-            .await
-            .execute_with_async(move |client, handles, blueprint| async move {
-                let keypair = handles[0].sr25519_id().clone();
-                let selected_service = &blueprint.services[0];
-                let service_id = selected_service.id;
-                let call_id = get_next_call_id(client)
-                    .await
-                    .expect("Failed to get next job id");
-
-                info!(
-                    "Submitting job with params service ID: {service_id}, call ID: {call_id}"
-                );
-
-                let mut job_args = Args::new();
-                for input in [$($input),+] {
-                    job_args.push(input);
-                }
-
-                submit_job(
-                    client,
-                    &keypair,
-                    service_id,
-                    Job::from(call_id as u8),
-                    job_args,
-                )
-                .await
-                .expect("Failed to submit job");
-
-                let job_results = wait_for_completion_of_tangle_job(client, service_id, call_id, $N)
-                    .await
-                    .expect("Failed to wait for job completion");
-
-                assert_eq!(job_results.service_id, service_id);
-                assert_eq!(job_results.call_id, call_id);
-
-                let expected_outputs = vec![$($expected_output),+];
-                assert_eq!(job_results.result.len(), expected_outputs.len(), "Number of outputs doesn't match expected");
-
-                for (result, expected) in job_results.result.into_iter().zip(expected_outputs.into_iter()) {
-                    assert_eq!(result, expected);
-                }
-            })
-            .await
-        }
-    };
-}
-
-#[cfg(test)]
-mod test_macros {
-    use super::*;
-
-    test_blueprint!(
-        "./blueprints/incredible-squaring-eigen/", // Path to the blueprint's dir
-        "incredible-squaring-blueprint",           // Name of the package
-        5,                                         // Number of nodes
-        [InputValue::Uint64(5)],
-        [OutputValue::Uint64(25)] // Expected output: each input squared
-    );
-}
-
-#[cfg(test)]
-mod tests_standard {
-    use super::*;
-    use crate::test_ext::new_test_ext_blueprint_manager;
-
-    use cargo_tangle::deploy::Opts;
-    use gadget_sdk::config::protocol::EigenlayerContractAddresses;
-    use gadget_sdk::config::Protocol;
-    use gadget_sdk::logging::setup_log;
-    use gadget_sdk::{error, info};
-    use helpers::BlueprintProcessManager;
-    use std::sync::Arc;
-    use tokio::sync::Mutex;
-
-    /// This test requires that `yarn install` has been executed inside the
-    /// `./blueprints/incredible-squaring/` directory
-    /// The other requirement is that there is a locally-running tangle node
-    #[tokio::test(flavor = "multi_thread")]
-    #[allow(clippy::needless_return)]
-    async fn test_externalities_gadget_starts() {
-        setup_log();
-        let mut base_path = std::env::current_dir().expect("Failed to get current directory");
-        let tmp_dir = tempfile::TempDir::new().unwrap(); // Create a temporary directory for the keystores
-        let tmp_dir_path = format!("{}", tmp_dir.path().display());
-
-        base_path.push("../blueprints/incredible-squaring");
-        base_path
-            .canonicalize()
-            .expect("File could not be normalized");
-
-        let manifest_path = base_path.join("Cargo.toml");
-
-        let opts = Opts {
-            pkg_name: Some("incredible-squaring-blueprint".to_string()),
-            http_rpc_url: "http://127.0.0.1:9944".to_string(),
-            ws_rpc_url: "ws://127.0.0.1:9944".to_string(),
-            manifest_path,
-            signer: None,
-            signer_evm: None,
-        };
-        // --ws-external
-        const INPUT: u64 = 10;
-        const OUTPUT: u64 = INPUT.pow(2);
-
-        new_test_ext_blueprint_manager::<5, 1, String, _, _>(
-            tmp_dir_path,
-            opts,
-            run_test_blueprint_manager,
+pub fn read_cargo_toml_file<P: AsRef<Path>>(path: P) -> std::io::Result<Manifest> {
+    let manifest = cargo_toml::Manifest::from_path(path).map_err(|err| {
+        std::io::Error::new(
+            std::io::ErrorKind::Other,
+            format!("Failed to read Cargo.toml: {err}"),
         )
-        .await
-        .execute_with_async(move |client, handles, blueprint| async move {
-            // At this point, blueprint has been deployed, every node has registered
-            // as an operator for the relevant services, and, all gadgets are running
-
-            // What's left: Submit a job, wait for the job to finish, then assert the job results
-            let keypair = handles[0].sr25519_id().clone();
-            let selected_service = &blueprint.services[0];
-            let service_id = selected_service.id;
-            let call_id = get_next_call_id(client)
-                .await
-                .expect("Failed to get next job id")
-                .saturating_sub(1);
-
-            info!("Submitting job with params service ID: {service_id}, call ID: {call_id}");
-
-            // Pass the arguments
-            let mut job_args = Args::new();
-            let input =
-                api::runtime_types::tangle_primitives::services::field::Field::Uint64(INPUT);
-            job_args.push(input);
-
-            // Next step: submit a job under that service/job id
-            if let Err(err) = submit_job(
-                client,
-                &keypair,
-                service_id,
-                Job::from(call_id as u8),
-                job_args,
-            )
-            .await
-            {
-                error!("Failed to submit job: {err}");
-                panic!("Failed to submit job: {err}");
-            }
-
-            // Step 2: wait for the job to complete
-            let job_results =
-                wait_for_completion_of_tangle_job(client, service_id, call_id, handles.len())
-                    .await
-                    .expect("Failed to wait for job completion");
-
-            // Step 3: Get the job results, compare to expected value(s)
-            let expected_result =
-                api::runtime_types::tangle_primitives::services::field::Field::Uint64(OUTPUT);
-            assert_eq!(job_results.service_id, service_id);
-            assert_eq!(job_results.call_id, call_id);
-            assert_eq!(job_results.result[0], expected_result);
-        })
-        .await
+    })?;
+    if manifest.package.is_none() {
+        return Err(std::io::Error::new(
+            std::io::ErrorKind::Other,
+            "No package section found in Cargo.toml",
+        ));
     }
+
+    Ok(manifest)
 }
diff --git a/blueprint-test-utils/src/mpc.rs b/blueprint-test-utils/src/mpc.rs
new file mode 100644
index 000000000..5f95fd346
--- /dev/null
+++ b/blueprint-test-utils/src/mpc.rs
@@ -0,0 +1,101 @@
+#[macro_export]
+macro_rules! mpc_generate_keygen_and_signing_tests {
+    (
+        $blueprint_path:literal,
+        $N:tt,
+        $T:tt,
+        $keygen_job_id:tt,
+        [$($keygen_inputs:expr),+],
+        [$($expected_keygen_outputs:expr),*],
+        $signing_job_id:tt,
+        [$($signing_inputs:expr),+],
+        [$($expected_signing_outputs:expr),*],
+        $atomic_keygen_call_id_store:expr,
+    ) => {
+        $crate::tangle_blueprint_test_template!(
+            $blueprint_path,
+            $N,
+            |client, handles, blueprint| async move {
+                let keypair = handles[0].sr25519_id().clone();
+                let service = &blueprint.services[$keygen_job_id as usize];
+
+                let service_id = service.id;
+                gadget_sdk::info!(
+                    "Submitting KEYGEN job {} with service ID {service_id}", $keygen_job_id
+                );
+
+                let job_args = vec![$($keygen_inputs),+];
+
+                let job = submit_job(
+                    client,
+                    &keypair,
+                    service_id,
+                    Job::from($keygen_job_id as u8),
+                    job_args,
+                )
+                .await
+                .expect("Failed to submit job");
+
+                let keygen_call_id = job.call_id;
+                $atomic_keygen_call_id_store.store(keygen_call_id, std::sync::atomic::Ordering::Relaxed);
+
+                gadget_sdk::info!(
+                    "Submitted KEYGEN job {} with service ID {service_id} has call id {keygen_call_id}", $keygen_job_id,
+                );
+
+                let job_results = wait_for_completion_of_tangle_job(client, service_id, keygen_call_id, $T)
+                    .await
+                    .expect("Failed to wait for job completion");
+
+                assert_eq!(job_results.service_id, service_id);
+                assert_eq!(job_results.call_id, keygen_call_id);
+
+                let expected_outputs = vec![$($expected_keygen_outputs),*];
+                assert_eq!(job_results.result.len(), expected_outputs.len(), "Number of keygen outputs doesn't match expected");
+
+                for (result, expected) in job_results.result.into_iter().zip(expected_outputs.into_iter()) {
+                    assert_eq!(result, expected);
+                }
+
+                // ~~~~~ Now, run a signing job ~~~~~
+                let service = &blueprint.services[$signing_job_id as usize];
+
+                let service_id = service.id;
+                gadget_sdk::info!(
+                    "Submitting SIGNING job {} with service ID {service_id}", $signing_job_id
+                );
+
+                // Pass the arguments
+                let job_args = vec![$($signing_inputs),+];
+
+                 let job = submit_job(
+                    client,
+                    &keypair,
+                    service_id,
+                    Job::from($signing_job_id as u8),
+                    job_args,
+                )
+                .await
+                .expect("Failed to submit job");
+
+                let signing_call_id = job.call_id;
+
+                gadget_sdk::info!(
+                    "Submitted SIGNING job {} with service ID {service_id} has call id {signing_call_id}", $keygen_job_id
+                );
+
+                let job_results =
+                    wait_for_completion_of_tangle_job(client, service_id, signing_call_id, $T)
+                        .await
+                        .expect("Failed to wait for job completion");
+
+                let expected_outputs = vec![$($expected_signing_outputs),*];
+                assert_eq!(job_results.result.len(), expected_outputs.len(), "Number of signing outputs doesn't match expected");
+
+                for (result, expected) in job_results.result.into_iter().zip(expected_outputs.into_iter()) {
+                    assert_eq!(result, expected);
+                }
+            },
+        );
+    };
+}
diff --git a/blueprint-test-utils/src/tangle/mod.rs b/blueprint-test-utils/src/tangle/mod.rs
new file mode 100644
index 000000000..7985bad72
--- /dev/null
+++ b/blueprint-test-utils/src/tangle/mod.rs
@@ -0,0 +1,166 @@
+use crate::tangle::node::{Error, SubstrateNode, TANGLE_NODE_ENV};
+
+pub mod node;
+pub mod transactions;
+
+/// Run a Tangle node with the default settings.
+/// The node will shut down when the returned handle is dropped.
+pub fn run() -> Result<SubstrateNode, Error> {
+    let tangle_from_env = std::env::var(TANGLE_NODE_ENV).unwrap_or_else(|_| "tangle".to_string());
+    let builder = SubstrateNode::builder()
+        .binary_paths([
+            "../tangle/target/release/tangle",
+            "../../tangle/target/release/tangle",
+            &tangle_from_env,
+        ])
+        .arg("validator")
+        .arg_val("rpc-cors", "all")
+        .arg_val("rpc-methods", "unsafe")
+        .arg("rpc-external")
+        .arg_val("sealing", "manual")
+        .clone();
+    builder.spawn()
+}
+
+#[macro_export]
+/// A template that makes creating domain-specific macros for tangle-based blueprints easier
+macro_rules! tangle_blueprint_test_template {
+    (
+        $blueprint_path:expr,
+        $N:tt,
+        $test_logic:expr,
+    ) => {
+        pub use $crate::{
+            run_test_blueprint_manager,
+            Opts, setup_log,
+            tangle, get_blueprint_base_dir, read_cargo_toml_file,
+            submit_job, wait_for_completion_of_tangle_job, Job, Args,
+        };
+
+        use $crate::test_ext::new_test_ext_blueprint_manager;
+
+        #[tokio::test(flavor = "multi_thread")]
+        async fn test_blueprint() {
+            setup_log();
+            let tangle_node = tangle::run().expect("Failed to start tangle node");
+            let mut base_path = get_blueprint_base_dir();
+
+            let tmp_dir = $crate::tempfile::TempDir::new().unwrap();
+            let tmp_dir_path = format!("{}", tmp_dir.path().display());
+
+            base_path.push($blueprint_path);
+            base_path
+                .canonicalize()
+                .expect("File could not be found/normalized");
+
+            let manifest_path = base_path.join("Cargo.toml");
+            log::info!(target: "gadget", "Manifest path: {manifest_path:?}");
+            let manifest = read_cargo_toml_file(&manifest_path).expect("Failed to read blueprint's Cargo.toml");
+            let blueprint_name = manifest.package.as_ref().unwrap().name.clone();
+
+            let ws_port = tangle_node.ws_port();
+            let http_rpc_url = format!("http://127.0.0.1:{ws_port}");
+            let ws_rpc_url = format!("ws://127.0.0.1:{ws_port}");
+
+            let opts = Opts {
+                pkg_name: Some(blueprint_name),
+                http_rpc_url,
+                ws_rpc_url,
+                manifest_path,
+                signer: None,
+                signer_evm: None,
+            };
+
+            new_test_ext_blueprint_manager::<$N, 1, String, _, _>(
+                tmp_dir_path,
+                opts,
+                run_test_blueprint_manager,
+            )
+            .await
+            .execute_with_async($test_logic)
+            .await
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! test_tangle_blueprint {
+    (
+        $blueprint_path:expr,
+        $N:tt,
+        $T:tt,
+        $job_id:tt,
+        [$($inputs:expr),+],
+        [$($expected_output:expr),+]
+    ) => {
+        tangle_blueprint_test_template!(
+            $blueprint_path,
+            $N,
+            |client, handles, blueprint| async move {
+                let keypair = handles[0].sr25519_id().clone();
+                let selected_service = &blueprint.services[$job_id];
+                let service_id = selected_service.id;
+
+                gadget_sdk::info!(
+                    "Submitting job {} with service ID {service_id}", $job_id
+                );
+
+                let job_args = vec![$($inputs),+];
+
+                let job = submit_job(
+                    client,
+                    &keypair,
+                    service_id,
+                    Job::from($job_id as u8),
+                    job_args,
+                )
+                .await
+                .expect("Failed to submit job");
+
+                let call_id = job.call_id;
+
+                gadget_sdk::info!(
+                    "Submitted job {} with service ID {service_id} has call id {call_id}", $job_id
+                );
+
+                let job_results = wait_for_completion_of_tangle_job(client, service_id, call_id, $T)
+                    .await
+                    .expect("Failed to wait for job completion");
+
+                assert_eq!(job_results.service_id, service_id);
+                assert_eq!(job_results.call_id, call_id);
+
+                let expected_outputs = vec![$($expected_output),+];
+                assert_eq!(job_results.result.len(), expected_outputs.len(), "Number of outputs doesn't match expected");
+
+                for (result, expected) in job_results.result.into_iter().zip(expected_outputs.into_iter()) {
+                    assert_eq!(result, expected);
+                }
+            },
+        );
+    };
+    (
+        $blueprint_path:expr,
+        $N:tt,
+        $job_id:tt,
+        [$($input:expr),+],
+        [$($expected_output:expr),+]
+    ) => {
+        test_tangle_blueprint!($blueprint_path, $N, $N, $job_id, [$($input),+], [$($expected_output),+]);
+    };
+}
+
+#[cfg(test)]
+mod test_incredible_squaring {
+    use crate::{InputValue, OutputValue};
+
+    const KEYGEN_JOB_ID: usize = 0;
+    const N: usize = 5;
+    test_tangle_blueprint!(
+        "./blueprints/incredible-squaring/", // Path to the blueprint's dir relative to the git repo root, or, if not in a git repo, the current working directory
+        N,                                   // Number of nodes
+        KEYGEN_JOB_ID,                       // Job ID
+        [InputValue::Uint64(5)],             // Inputs
+        [OutputValue::Uint64(25)]            // Expected output: input squared
+    );
+}
diff --git a/blueprint-test-utils/src/tangle.rs b/blueprint-test-utils/src/tangle/node.rs
similarity index 94%
rename from blueprint-test-utils/src/tangle.rs
rename to blueprint-test-utils/src/tangle/node.rs
index f6511749d..4fa8e71a6 100644
--- a/blueprint-test-utils/src/tangle.rs
+++ b/blueprint-test-utils/src/tangle/node.rs
@@ -12,7 +12,7 @@ pub const TANGLE_NODE_ENV: &str = "TANGLE_NODE";
 
 #[derive(Debug)]
 pub enum Error {
-    Io(std::io::Error),
+    Io(io::Error),
     CouldNotExtractPort(String),
     CouldNotExtractP2pAddress(String),
     CouldNotExtractP2pPort(String),
@@ -108,6 +108,10 @@ impl SubstrateNodeBuilder {
         let path = String::from_utf8(path.stdout).expect("bad path");
         let mut bin_path = OsString::new();
         for binary_path in &self.binary_paths {
+            let binary_path = &std::path::absolute(binary_path)
+                .expect("bad path")
+                .into_os_string();
+            log::info!(target: "gadget", "Trying to spawn binary at {:?}", binary_path);
             self.custom_flags
                 .insert("base-path".into(), Some(path.clone().into()));
 
@@ -369,18 +373,3 @@ impl SubstrateNodeInfo {
             .ok_or_else(|| Error::CouldNotExtractP2pPort(self.log.clone()))
     }
 }
-
-/// Run a Tangle node with the default settings.
-/// The node will shut down when the returned handle is dropped.
-pub fn run() -> Result<SubstrateNode, Error> {
-    let tangle_from_env = std::env::var(TANGLE_NODE_ENV).unwrap_or_else(|_| "tangle".to_string());
-    let builder = SubstrateNode::builder()
-        .binary_paths(["../tangle/target/release/tangle", &tangle_from_env])
-        .arg("validator")
-        .arg_val("rpc-cors", "all")
-        .arg_val("rpc-methods", "unsafe")
-        .arg("rpc-external")
-        .arg_val("sealing", "manual")
-        .clone();
-    builder.spawn()
-}
diff --git a/blueprint-test-utils/src/tangle/transactions.rs b/blueprint-test-utils/src/tangle/transactions.rs
new file mode 100644
index 000000000..7d2b4eae8
--- /dev/null
+++ b/blueprint-test-utils/src/tangle/transactions.rs
@@ -0,0 +1,233 @@
+use gadget_sdk::keystore::TanglePairSigner;
+use std::error::Error;
+use gadget_sdk::event_listener::tangle::AccountId32;
+use gadget_sdk::{error, info};
+use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api;
+use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::runtime_types::sp_arithmetic::per_things::Percent;
+use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::call::{Args, Job};
+use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::create_blueprint::Blueprint;
+use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::calls::types::register::{Preferences, RegistrationArgs};
+use gadget_sdk::tangle_subxt::tangle_testnet_runtime::api::services::events::{JobCalled, JobResultSubmitted};
+use subxt::tx::TxProgress;
+use gadget_sdk::clients::tangle::runtime::TangleConfig;
+use gadget_sdk::subxt_core::tx::signer::Signer;
+use crate::TestClient;
+
+pub async fn create_blueprint(
+    client: &TestClient,
+    account_id: &TanglePairSigner<sp_core::sr25519::Pair>,
+    blueprint: Blueprint,
+) -> Result<(), Box<dyn Error>> {
+    let call = api::tx().services().create_blueprint(blueprint);
+    let res = client
+        .tx()
+        .sign_and_submit_then_watch_default(&call, account_id)
+        .await?;
+    wait_for_in_block_success(res).await?;
+    Ok(())
+}
+
+pub async fn join_operators(
+    client: &TestClient,
+    account_id: &TanglePairSigner<sp_core::sr25519::Pair>,
+) -> Result<(), Box<dyn Error>> {
+    info!("Joining delegators ...");
+    let call_pre = api::tx()
+        .multi_asset_delegation()
+        .join_operators(1_000_000_000_000_000);
+    let res_pre = client
+        .tx()
+        .sign_and_submit_then_watch_default(&call_pre, account_id)
+        .await?;
+
+    wait_for_in_block_success(res_pre).await?;
+    Ok(())
+}
+
+pub async fn register_blueprint(
+    client: &TestClient,
+    account_id: &TanglePairSigner<sp_core::sr25519::Pair>,
+    blueprint_id: u64,
+    preferences: Preferences,
+    registration_args: RegistrationArgs,
+) -> Result<(), Box<dyn Error>> {
+    info!("Registering to blueprint {blueprint_id} to become an operator ...");
+    let call = api::tx()
+        .services()
+        .register(blueprint_id, preferences, registration_args);
+    let res = client
+        .tx()
+        .sign_and_submit_then_watch_default(&call, account_id)
+        .await?;
+    wait_for_in_block_success(res).await?;
+    Ok(())
+}
+
+pub async fn submit_job(
+    client: &TestClient,
+    user: &TanglePairSigner<sp_core::sr25519::Pair>,
+    service_id: u64,
+    job_id: Job,
+    job_params: Args,
+) -> Result<JobCalled, Box<dyn Error>> {
+    let call = api::tx().services().call(service_id, job_id, job_params);
+    let events = client
+        .tx()
+        .sign_and_submit_then_watch_default(&call, user)
+        .await?
+        .wait_for_finalized_success()
+        .await?;
+
+    let job_called_events = events.find::<JobCalled>().collect::<Vec<_>>();
+    for job_called in job_called_events {
+        let job_called = job_called?;
+        if job_called.service_id == service_id
+            && job_called.job == job_id
+            && user.account_id() == job_called.caller
+        {
+            return Ok(job_called);
+        }
+    }
+
+    Err("Failed to find JobCalled event".into())
+}
+
+/// Requests a service with a given blueprint. This is meant for testing, and will allow any node
+/// to make a call to run a service, and will have all nodes running the service.
+pub async fn request_service(
+    client: &TestClient,
+    user: &TanglePairSigner<sp_core::sr25519::Pair>,
+    blueprint_id: u64,
+    test_nodes: Vec<AccountId32>,
+) -> Result<(), Box<dyn Error>> {
+    let call = api::tx().services().request(
+        blueprint_id,
+        test_nodes.clone(),
+        test_nodes,
+        Default::default(),
+        vec![0],
+        1000,
+    );
+    let res = client
+        .tx()
+        .sign_and_submit_then_watch_default(&call, user)
+        .await?;
+    wait_for_in_block_success(res).await?;
+    Ok(())
+}
+
+pub async fn wait_for_in_block_success(
+    mut res: TxProgress<TangleConfig, TestClient>,
+) -> Result<(), Box<dyn Error>> {
+    while let Some(Ok(event)) = res.next().await {
+        let Some(block) = event.as_in_block() else {
+            continue;
+        };
+        block.wait_for_success().await?;
+    }
+    Ok(())
+}
+
+pub async fn wait_for_completion_of_tangle_job(
+    client: &TestClient,
+    service_id: u64,
+    call_id: u64,
+    required_count: usize,
+) -> Result<JobResultSubmitted, Box<dyn Error>> {
+    let mut count = 0;
+    let mut blocks = client.blocks().subscribe_best().await?;
+    while let Some(Ok(block)) = blocks.next().await {
+        let events = block.events().await?;
+        let results = events.find::<JobResultSubmitted>().collect::<Vec<_>>();
+        info!(
+            %service_id,
+            %call_id,
+            %required_count,
+            %count,
+            "Waiting for job completion. Found {} results ...",
+            results.len()
+        );
+        for result in results {
+            match result {
+                Ok(result) => {
+                    if result.service_id == service_id && result.call_id == call_id {
+                        count += 1;
+                        if count == required_count {
+                            return Ok(result);
+                        }
+                    }
+                }
+                Err(err) => {
+                    error!("Failed to get job result: {err}");
+                }
+            }
+        }
+    }
+    Err("Failed to get job result".into())
+}
+
+pub async fn get_next_blueprint_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
+    let call = api::storage().services().next_blueprint_id();
+    let res = client
+        .storage()
+        .at_latest()
+        .await?
+        .fetch_or_default(&call)
+        .await?;
+    Ok(res)
+}
+
+pub async fn get_next_service_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
+    let call = api::storage().services().next_instance_id();
+    let res = client
+        .storage()
+        .at_latest()
+        .await?
+        .fetch_or_default(&call)
+        .await?;
+    Ok(res)
+}
+
+pub async fn get_next_call_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
+    let call = api::storage().services().next_job_call_id();
+    let res = client
+        .storage()
+        .at_latest()
+        .await?
+        .fetch_or_default(&call)
+        .await?;
+    Ok(res)
+}
+
+/// Approves a service request. This is meant for testing, and will always approve the request.
+pub async fn approve_service(
+    client: &TestClient,
+    caller: &TanglePairSigner<sp_core::sr25519::Pair>,
+    request_id: u64,
+    restaking_percent: u8,
+) -> Result<(), Box<dyn Error>> {
+    info!("Approving service request ...");
+    let call = api::tx()
+        .services()
+        .approve(request_id, Percent(restaking_percent));
+    let res = client
+        .tx()
+        .sign_and_submit_then_watch_default(&call, caller)
+        .await?;
+    res.wait_for_finalized_success().await?;
+    Ok(())
+}
+
+pub async fn get_next_request_id(client: &TestClient) -> Result<u64, Box<dyn Error>> {
+    info!("Fetching next request ID ...");
+    let next_request_id_addr = api::storage().services().next_service_request_id();
+    let next_request_id = client
+        .storage()
+        .at_latest()
+        .await
+        .expect("Failed to fetch latest block")
+        .fetch_or_default(&next_request_id_addr)
+        .await
+        .expect("Failed to fetch next request ID");
+    Ok(next_request_id)
+}
diff --git a/blueprint-test-utils/src/test_ext.rs b/blueprint-test-utils/src/test_ext.rs
index 1237c2dc6..3ae44daa3 100644
--- a/blueprint-test-utils/src/test_ext.rs
+++ b/blueprint-test-utils/src/test_ext.rs
@@ -39,21 +39,10 @@ use gadget_sdk::{error, info, warn};
 use gadget_sdk::clients::tangle::services::{RpcServicesWithBlueprint, ServicesClient};
 use gadget_sdk::subxt_core::config::Header;
 use gadget_sdk::utils::test_utils::get_client;
+use crate::tangle::transactions;
 
 const LOCAL_BIND_ADDR: &str = "127.0.0.1";
 pub const NAME_IDS: [&str; 5] = ["Alice", "Bob", "Charlie", "Dave", "Eve"];
-pub const ANVIL_PRIVATE_KEYS: [&str; 10] = [
-    "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
-    "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
-    "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
-    "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
-    "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a",
-    "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba",
-    "0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e",
-    "0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
-    "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
-    "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6",
-];
 
 /// - `N`: number of nodes
 /// - `K`: Number of networks accessible per node (should be equal to the number of services in a given blueprint)
@@ -181,7 +170,7 @@ pub async fn new_test_ext_blueprint_manager<
                 },
             };
 
-            if let Err(err) = super::join_delegators(&client, &keypair).await {
+            if let Err(err) = transactions::join_operators(&client, &keypair).await {
                 let _span = handle.span().enter();
 
                 let err_str = format!("{err}");
@@ -193,7 +182,7 @@ pub async fn new_test_ext_blueprint_manager<
                 }
             }
 
-            if let Err(err) = super::register_blueprint(
+            if let Err(err) = transactions::register_blueprint(
                 &client,
                 &keypair,
                 blueprint_id,
@@ -226,13 +215,14 @@ pub async fn new_test_ext_blueprint_manager<
     info!("Requesting service for blueprint ID {blueprint_id} using Alice's keys ...");
 
     if let Err(err) =
-        super::request_service(&client, handles[0].sr25519_id(), blueprint_id, all_nodes).await
+        transactions::request_service(&client, handles[0].sr25519_id(), blueprint_id, all_nodes)
+            .await
     {
         error!("Failed to register service: {err}");
         panic!("Failed to register service: {err}");
     }
 
-    let next_request_id = super::get_next_request_id(&client)
+    let next_request_id = transactions::get_next_request_id(&client)
         .await
         .expect("Failed to get next request ID")
         .saturating_sub(1);
@@ -244,7 +234,9 @@ pub async fn new_test_ext_blueprint_manager<
         let client = client.clone();
         let task = async move {
             let keypair = handle.sr25519_id().clone();
-            if let Err(err) = super::approve_service(&client, &keypair, next_request_id, 20).await {
+            if let Err(err) =
+                transactions::approve_service(&client, &keypair, next_request_id, 20).await
+            {
                 let _span = handle.span().enter();
                 error!("Failed to approve service request {next_request_id}: {err}");
                 panic!("Failed to approve service request {next_request_id}: {err}");
diff --git a/macros/context-derive/tests/tests.rs b/macros/context-derive/tests/tests.rs
index 436f77a55..df21859f9 100644
--- a/macros/context-derive/tests/tests.rs
+++ b/macros/context-derive/tests/tests.rs
@@ -1,13 +1,15 @@
+mod ui;
+
 #[cfg(test)]
 mod tests {
     #[test]
     fn test_derive_context() {
         let t = trybuild::TestCases::new();
-        t.pass("tests/ui/01_basic.rs");
-        t.pass("tests/ui/02_unnamed_fields.rs");
-        t.pass("tests/ui/03_generic_struct.rs");
-        t.compile_fail("tests/ui/04_missing_config_attr.rs");
-        t.compile_fail("tests/ui/05_not_a_struct.rs");
-        t.compile_fail("tests/ui/06_unit_struct.rs");
+        t.pass("tests/ui/basic.rs");
+        t.pass("tests/ui/unnamed_fields.rs");
+        t.pass("tests/ui/generic_struct.rs");
+        t.compile_fail("tests/ui/missing_config_attr.rs");
+        t.compile_fail("tests/ui/not_a_struct.rs");
+        t.compile_fail("tests/ui/unit_struct.rs");
     }
 }
diff --git a/macros/context-derive/tests/ui/01_basic.rs b/macros/context-derive/tests/ui/01_basic.rs
deleted file mode 100644
index d73d7a0d7..000000000
--- a/macros/context-derive/tests/ui/01_basic.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use gadget_sdk::config::StdGadgetConfiguration;
-use gadget_sdk::ctx::{EVMProviderContext, KeystoreContext, ServicesContext, TangleClientContext};
-
-#[derive(KeystoreContext, EVMProviderContext, TangleClientContext, ServicesContext)]
-struct MyContext {
-    foo: String,
-    #[config]
-    sdk_config: StdGadgetConfiguration,
-}
-
-fn main() {
-    let body = async {
-        let ctx = MyContext {
-            foo: "bar".to_string(),
-            sdk_config: Default::default(),
-        };
-        let _keystore = ctx.keystore();
-        let _evm_provider = ctx.evm_provider().await;
-        let tangle_client = ctx.tangle_client().await.unwrap();
-        let _services = ctx.current_service_operators(&tangle_client).await.unwrap();
-    };
-
-    let _ = body;
-}
diff --git a/macros/context-derive/tests/ui/basic.rs b/macros/context-derive/tests/ui/basic.rs
new file mode 100644
index 000000000..a07eeb6bc
--- /dev/null
+++ b/macros/context-derive/tests/ui/basic.rs
@@ -0,0 +1,98 @@
+use gadget_sdk::async_trait::async_trait;
+use gadget_sdk::config::{GadgetConfiguration, StdGadgetConfiguration};
+use gadget_sdk::contexts::{
+    EVMProviderContext, KeystoreContext, MPCContext, ServicesContext, TangleClientContext,
+};
+use gadget_sdk::network::{Network, NetworkMultiplexer, ProtocolMessage};
+use gadget_sdk::store::LocalDatabase;
+use gadget_sdk::subxt_core::ext::sp_core::ecdsa::Public;
+use gadget_sdk::subxt_core::tx::signer::Signer;
+use gadget_sdk::Error;
+use round_based::ProtocolMessage as RoundBasedProtocolMessage;
+use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
+use std::sync::Arc;
+
+#[derive(KeystoreContext, EVMProviderContext, TangleClientContext, ServicesContext, MPCContext)]
+#[allow(dead_code)]
+struct MyContext {
+    foo: String,
+    #[config]
+    config: StdGadgetConfiguration,
+    store: Arc<LocalDatabase<u64>>,
+}
+
+#[allow(dead_code)]
+fn main() {
+    let body = async {
+        let ctx = MyContext {
+            foo: "bar".to_string(),
+            config: GadgetConfiguration::default(),
+            store: Arc::new(LocalDatabase::open("test.json")),
+        };
+
+        // Test existing context functions
+        let _keystore = ctx.keystore();
+        let _evm_provider = ctx.evm_provider().await;
+        let tangle_client = ctx.tangle_client().await.unwrap();
+        let _services = ctx.current_service_operators(&tangle_client).await.unwrap();
+
+        // Test MPC context utility functions
+        let _config = ctx.config();
+        let _protocol = ctx.network_protocol();
+
+        // Test MPC context functions
+
+        let mux = Arc::new(NetworkMultiplexer::new(StubNetwork));
+        let party_index = 0;
+        let task_hash = [0u8; 32];
+        let mut parties = BTreeMap::<u16, _>::new();
+        parties.insert(0, Public([0u8; 33]));
+
+        // Test network delivery wrapper creation
+        let _network_wrapper = ctx.create_network_delivery_wrapper::<StubMessage>(
+            mux.clone(),
+            party_index,
+            task_hash,
+            parties.clone(),
+        );
+
+        // Test party index retrieval
+        let _party_idx = ctx.get_party_index().await;
+
+        // Test participants retrieval
+        let _participants = ctx.get_participants(&tangle_client).await;
+
+        // Test blueprint ID retrieval
+        let _blueprint_id = ctx.blueprint_id();
+
+        // Test party index and operators retrieval
+        let _party_idx_ops = ctx.get_party_index_and_operators().await;
+
+        // Test service operators ECDSA keys retrieval
+        let _operator_keys = ctx.current_service_operators_ecdsa_keys().await;
+
+        // Test current call ID retrieval
+        let _call_id = ctx.current_call_id().await;
+    };
+
+    drop(body);
+}
+
+#[derive(RoundBasedProtocolMessage, Clone, Serialize, Deserialize)]
+enum StubMessage {}
+
+#[allow(dead_code)]
+struct StubNetwork;
+
+#[async_trait]
+impl Network for StubNetwork {
+    async fn next_message(&self) -> Option<ProtocolMessage> {
+        None
+    }
+
+    async fn send_message(&self, message: ProtocolMessage) -> Result<(), Error> {
+        drop(message);
+        Ok(())
+    }
+}
diff --git a/macros/context-derive/tests/ui/03_generic_struct.rs b/macros/context-derive/tests/ui/generic_struct.rs
similarity index 66%
rename from macros/context-derive/tests/ui/03_generic_struct.rs
rename to macros/context-derive/tests/ui/generic_struct.rs
index e7c8c1768..c0eecfd47 100644
--- a/macros/context-derive/tests/ui/03_generic_struct.rs
+++ b/macros/context-derive/tests/ui/generic_struct.rs
@@ -1,7 +1,10 @@
-use gadget_sdk::config::StdGadgetConfiguration;
-use gadget_sdk::ctx::{EVMProviderContext, KeystoreContext, ServicesContext, TangleClientContext};
+use gadget_sdk::config::{GadgetConfiguration, StdGadgetConfiguration};
+use gadget_sdk::contexts::{
+    EVMProviderContext, KeystoreContext, ServicesContext, TangleClientContext,
+};
 
 #[derive(KeystoreContext, EVMProviderContext, TangleClientContext, ServicesContext)]
+#[allow(dead_code)]
 struct MyContext<T, U> {
     foo: T,
     bar: U,
@@ -9,12 +12,13 @@ struct MyContext<T, U> {
     sdk_config: StdGadgetConfiguration,
 }
 
+#[allow(dead_code)]
 fn main() {
     let body = async {
         let ctx = MyContext {
             foo: "bar".to_string(),
             bar: 42,
-            sdk_config: Default::default(),
+            sdk_config: GadgetConfiguration::default(),
         };
         let _keystore = ctx.keystore();
         let _evm_provider = ctx.evm_provider().await.unwrap();
@@ -22,5 +26,5 @@ fn main() {
         let _services = ctx.current_service_operators(&tangle_client).await.unwrap();
     };
 
-    let _ = body;
+    drop(body);
 }
diff --git a/macros/context-derive/tests/ui/04_missing_config_attr.rs b/macros/context-derive/tests/ui/missing_config_attr.rs
similarity index 79%
rename from macros/context-derive/tests/ui/04_missing_config_attr.rs
rename to macros/context-derive/tests/ui/missing_config_attr.rs
index 4910f45bb..bf04065e2 100644
--- a/macros/context-derive/tests/ui/04_missing_config_attr.rs
+++ b/macros/context-derive/tests/ui/missing_config_attr.rs
@@ -1,5 +1,5 @@
 use gadget_sdk::config::StdGadgetConfiguration;
-use gadget_sdk::ctx::KeystoreContext;
+use gadget_sdk::contexts::KeystoreContext;
 
 #[derive(KeystoreContext)]
 struct MyContext {
diff --git a/macros/context-derive/tests/ui/04_missing_config_attr.stderr b/macros/context-derive/tests/ui/missing_config_attr.stderr
similarity index 80%
rename from macros/context-derive/tests/ui/04_missing_config_attr.stderr
rename to macros/context-derive/tests/ui/missing_config_attr.stderr
index 52300b241..b7d5f4514 100644
--- a/macros/context-derive/tests/ui/04_missing_config_attr.stderr
+++ b/macros/context-derive/tests/ui/missing_config_attr.stderr
@@ -1,5 +1,5 @@
 error: No field with #[config] attribute found, please add #[config] to the field that holds the `gadget_sdk::config::GadgetConfiguration`
- --> tests/ui/04_missing_config_attr.rs:5:8
+ --> tests/ui/missing_config_attr.rs:5:8
   |
 5 | struct MyContext {
   |        ^^^^^^^^^
diff --git a/macros/context-derive/tests/ui/mod.rs b/macros/context-derive/tests/ui/mod.rs
new file mode 100644
index 000000000..c324553ce
--- /dev/null
+++ b/macros/context-derive/tests/ui/mod.rs
@@ -0,0 +1,3 @@
+mod basic;
+mod generic_struct;
+mod unnamed_fields;
diff --git a/macros/context-derive/tests/ui/05_not_a_struct.rs b/macros/context-derive/tests/ui/not_a_struct.rs
similarity index 67%
rename from macros/context-derive/tests/ui/05_not_a_struct.rs
rename to macros/context-derive/tests/ui/not_a_struct.rs
index fadc6ff20..b765a40b3 100644
--- a/macros/context-derive/tests/ui/05_not_a_struct.rs
+++ b/macros/context-derive/tests/ui/not_a_struct.rs
@@ -1,4 +1,4 @@
-use gadget_sdk::ctx::KeystoreContext;
+use gadget_sdk::contexts::KeystoreContext;
 
 #[derive(KeystoreContext)]
 enum MyContext {
diff --git a/macros/context-derive/tests/ui/05_not_a_struct.stderr b/macros/context-derive/tests/ui/not_a_struct.stderr
similarity index 74%
rename from macros/context-derive/tests/ui/05_not_a_struct.stderr
rename to macros/context-derive/tests/ui/not_a_struct.stderr
index ae7418163..b3bbcd81b 100644
--- a/macros/context-derive/tests/ui/05_not_a_struct.stderr
+++ b/macros/context-derive/tests/ui/not_a_struct.stderr
@@ -1,5 +1,5 @@
 error: Context Extensions traits can only be derived for structs
- --> tests/ui/05_not_a_struct.rs:4:6
+ --> tests/ui/not_a_struct.rs:4:6
   |
 4 | enum MyContext {
   |      ^^^^^^^^^
diff --git a/macros/context-derive/tests/ui/06_unit_struct.rs b/macros/context-derive/tests/ui/unit_struct.rs
similarity index 58%
rename from macros/context-derive/tests/ui/06_unit_struct.rs
rename to macros/context-derive/tests/ui/unit_struct.rs
index 497207705..87702c27f 100644
--- a/macros/context-derive/tests/ui/06_unit_struct.rs
+++ b/macros/context-derive/tests/ui/unit_struct.rs
@@ -1,4 +1,4 @@
-use gadget_sdk::ctx::KeystoreContext;
+use gadget_sdk::contexts::KeystoreContext;
 
 #[derive(KeystoreContext)]
 struct MyContext;
diff --git a/macros/context-derive/tests/ui/06_unit_struct.stderr b/macros/context-derive/tests/ui/unit_struct.stderr
similarity index 76%
rename from macros/context-derive/tests/ui/06_unit_struct.stderr
rename to macros/context-derive/tests/ui/unit_struct.stderr
index 69ff63112..a0715d796 100644
--- a/macros/context-derive/tests/ui/06_unit_struct.stderr
+++ b/macros/context-derive/tests/ui/unit_struct.stderr
@@ -1,5 +1,5 @@
 error: Context Extensions traits cannot be derived for unit structs
- --> tests/ui/06_unit_struct.rs:4:8
+ --> tests/ui/unit_struct.rs:4:8
   |
 4 | struct MyContext;
   |        ^^^^^^^^^
diff --git a/macros/context-derive/tests/ui/02_unnamed_fields.rs b/macros/context-derive/tests/ui/unnamed_fields.rs
similarity index 57%
rename from macros/context-derive/tests/ui/02_unnamed_fields.rs
rename to macros/context-derive/tests/ui/unnamed_fields.rs
index 14c75cb39..fb63f8ae2 100644
--- a/macros/context-derive/tests/ui/02_unnamed_fields.rs
+++ b/macros/context-derive/tests/ui/unnamed_fields.rs
@@ -1,16 +1,20 @@
-use gadget_sdk::config::StdGadgetConfiguration;
-use gadget_sdk::ctx::{EVMProviderContext, KeystoreContext, ServicesContext, TangleClientContext};
+use gadget_sdk::config::{GadgetConfiguration, StdGadgetConfiguration};
+use gadget_sdk::contexts::{
+    EVMProviderContext, KeystoreContext, ServicesContext, TangleClientContext,
+};
 
 #[derive(KeystoreContext, EVMProviderContext, TangleClientContext, ServicesContext)]
+#[allow(dead_code)]
 struct MyContext(String, #[config] StdGadgetConfiguration);
 
+#[allow(dead_code)]
 fn main() {
     let body = async {
-        let ctx = MyContext("bar".to_string(), Default::default());
+        let ctx = MyContext("bar".to_string(), GadgetConfiguration::default());
         let _keystore = ctx.keystore();
         let _evm_provider = ctx.evm_provider().await;
         let tangle_client = ctx.tangle_client().await.unwrap();
         let _services = ctx.current_service_operators(&tangle_client).await;
     };
-    let _ = body;
+    drop(body);
 }