diff --git a/rs/embedders/src/wasmtime_embedder/system_api/routing.rs b/rs/embedders/src/wasmtime_embedder/system_api/routing.rs index 8035a436ebf3..891546a51ba7 100644 --- a/rs/embedders/src/wasmtime_embedder/system_api/routing.rs +++ b/rs/embedders/src/wasmtime_embedder/system_api/routing.rs @@ -13,10 +13,10 @@ use ic_management_canister_types_private::{ LoadCanisterSnapshotArgs, MasterPublicKeyId, Method as Ic00Method, NodeMetricsHistoryArgs, Payload, ProvisionalTopUpCanisterArgs, ReadCanisterSnapshotDataArgs, ReadCanisterSnapshotMetadataArgs, RenameCanisterArgs, ReshareChainKeyArgs, - SchnorrPublicKeyArgs, SignWithECDSAArgs, SignWithSchnorrArgs, StoredChunksArgs, SubnetInfoArgs, - TakeCanisterSnapshotArgs, UninstallCodeArgs, UpdateSettingsArgs, - UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs, UploadChunkArgs, - VetKdDeriveKeyArgs, VetKdPublicKeyArgs, + SchnorrPublicKeyArgs, SetupInitialDKGArgs, SignWithECDSAArgs, SignWithSchnorrArgs, + StoredChunksArgs, SubnetInfoArgs, TakeCanisterSnapshotArgs, UninstallCodeArgs, + UpdateSettingsArgs, UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs, + UploadChunkArgs, VetKdDeriveKeyArgs, VetKdPublicKeyArgs, }; use ic_replicated_state::NetworkTopology; use itertools::Itertools; @@ -72,13 +72,16 @@ pub(super) fn resolve_destination( | Ok(Ic00Method::FlexibleHttpRequest) | Ok(Ic00Method::BitcoinSendTransactionInternal) | Ok(Ic00Method::BitcoinGetSuccessors) => Ok(own_subnet.get()), - // This message needs to be routed to the NNS subnet. We assume that - // this message can only be sent by canisters on the NNS subnet hence - // returning `own_subnet` here is fine. - // - // It might be cleaner to pipe in the actual NNS subnet id to this - // function and return that instead. - Ok(Ic00Method::SetupInitialDKG) => Ok(own_subnet.get()), + Ok(Ic00Method::SetupInitialDKG) => { + let args = SetupInitialDKGArgs::decode(payload)?; + // This message should be routed to the NNS subnet by default. We assume that + // this message can only be sent by canisters on the NNS subnet hence + // defaulting to `own_subnet` here is fine. + // + // It might be cleaner to pipe in the actual NNS subnet id to this + // function and return that instead. + Ok(args.get_subnet_id().unwrap_or(own_subnet).get()) + } Ok(Ic00Method::UpdateSettings) => { // Find the destination canister from the payload. let args = UpdateSettingsArgs::decode(payload)?; @@ -589,6 +592,12 @@ mod tests { Encode!(&args).unwrap() } + fn setup_initial_dkg_request(subnet_id: Option) -> Vec { + let args = + SetupInitialDKGArgs::new(vec![node_test_id(0)], RegistryVersion::from(100), subnet_id); + Encode!(&args).unwrap() + } + fn ecdsa_sign_request(key_id: EcdsaKeyId) -> Vec { let args = SignWithECDSAArgs { message_hash: [1; 32], @@ -669,6 +678,45 @@ mod tests { } } + #[test] + fn resolve_setup_initial_dkg_defaults_to_own_subnet() { + let logger = no_op_logger(); + let own_subnet = subnet_test_id(2); + assert_eq!( + resolve_destination( + &network_with_ecdsa_subnets(), + &Ic00Method::SetupInitialDKG.to_string(), + &setup_initial_dkg_request(None), + own_subnet, + canister_test_id(1), + false, + &logger, + ) + .unwrap(), + own_subnet.get() + ); + } + + #[test] + fn resolve_setup_initial_dkg_routes_to_requested_subnet() { + let logger = no_op_logger(); + let own_subnet = subnet_test_id(2); + let requested_subnet = subnet_test_id(1); + assert_eq!( + resolve_destination( + &network_with_ecdsa_subnets(), + &Ic00Method::SetupInitialDKG.to_string(), + &setup_initial_dkg_request(Some(requested_subnet)), + own_subnet, + canister_test_id(1), + false, + &logger, + ) + .unwrap(), + requested_subnet.get() + ); + } + #[test] fn resolve_reshare_chain_key_key_not_held_error() { let logger = no_op_logger(); diff --git a/rs/execution_environment/src/execution_environment/tests.rs b/rs/execution_environment/src/execution_environment/tests.rs index 6e9986a6bc2f..02e261849256 100644 --- a/rs/execution_environment/src/execution_environment/tests.rs +++ b/rs/execution_environment/src/execution_environment/tests.rs @@ -2273,6 +2273,7 @@ fn create_canister_xnet_called_from_nns() { fn setup_initial_dkg_sender_on_nns() { let own_subnet = subnet_test_id(1); let nns_subnet = subnet_test_id(2); + let other_subnet = subnet_test_id(3); let nns_canister = canister_test_id(1); let mut test = ExecutionTestBuilder::new() .with_subnet_type(SubnetType::System) @@ -2281,12 +2282,15 @@ fn setup_initial_dkg_sender_on_nns() { .with_caller(nns_subnet, nns_canister) .build(); let nodes = vec![node_test_id(1)]; - let args = ic00::SetupInitialDKGArgs::new(nodes, RegistryVersion::new(1)); - test.inject_call_to_ic00( - Method::SetupInitialDKG, - args.encode(), - test.canister_creation_fee().real(), - ); + for subnet_id in [None, Some(own_subnet), Some(other_subnet), Some(nns_subnet)] { + let args = + ic00::SetupInitialDKGArgs::new(nodes.clone(), RegistryVersion::new(1), subnet_id); + test.inject_call_to_ic00( + Method::SetupInitialDKG, + args.encode(), + test.canister_creation_fee().real(), + ); + } test.execute_all(); assert_eq!(0, test.xnet_messages().len()); } @@ -2303,33 +2307,38 @@ fn setup_initial_dkg_sender_not_on_nns() { .with_caller(other_subnet, other_canister) .build(); let nodes = vec![node_test_id(1)]; - let args = ic00::SetupInitialDKGArgs::new(nodes, RegistryVersion::new(1)); - test.inject_call_to_ic00( - Method::SetupInitialDKG, - args.encode(), - test.canister_creation_fee().real(), - ); + for subnet_id in [None, Some(own_subnet), Some(other_subnet), Some(nns_subnet)] { + let args = + ic00::SetupInitialDKGArgs::new(nodes.clone(), RegistryVersion::new(1), subnet_id); + test.inject_call_to_ic00( + Method::SetupInitialDKG, + args.encode(), + test.canister_creation_fee().real(), + ); + } test.execute_all(); - let response = test.xnet_messages()[0].clone(); - assert_eq!( - response, - Response { - originator: other_canister, - respondent: CanisterId::from(own_subnet), - originator_reply_callback: CallbackId::new(0), - refund: test.canister_creation_fee().real(), - response_payload: Payload::Reject(RejectContext::new( - RejectCode::CanisterError, - format!( - "{} is called by {}. It can only be called by NNS.", - ic00::Method::SetupInitialDKG, - other_canister, - ) - )), - deadline: NO_DEADLINE, - } - .into() - ); + assert_eq!(test.xnet_messages().len(), 4); + for response in test.xnet_messages().iter() { + assert_eq!( + response.clone(), + Response { + originator: other_canister, + respondent: CanisterId::from(own_subnet), + originator_reply_callback: CallbackId::new(0), + refund: test.canister_creation_fee().real(), + response_payload: Payload::Reject(RejectContext::new( + RejectCode::CanisterError, + format!( + "{} is called by {}. It can only be called by NNS.", + ic00::Method::SetupInitialDKG, + other_canister, + ) + )), + deadline: NO_DEADLINE, + } + .into() + ); + } } #[test] diff --git a/rs/nns/governance/src/proposals/fulfill_subnet_rental_request.rs b/rs/nns/governance/src/proposals/fulfill_subnet_rental_request.rs index 09c35907aba4..2e1b40a22939 100644 --- a/rs/nns/governance/src/proposals/fulfill_subnet_rental_request.rs +++ b/rs/nns/governance/src/proposals/fulfill_subnet_rental_request.rs @@ -239,6 +239,7 @@ impl ValidFulfillSubnetRentalRequest { subnet_type: SubnetType::Application, subnet_id_override: None, + initial_dkg_subnet_id: None, start_as_nns: false, is_halted: false, chain_key_config: None, diff --git a/rs/recovery/src/admin_helper.rs b/rs/recovery/src/admin_helper.rs index f2221dabd00a..4706cf899d57 100644 --- a/rs/recovery/src/admin_helper.rs +++ b/rs/recovery/src/admin_helper.rs @@ -195,6 +195,7 @@ impl AdminHelper { subnet_id: SubnetId, checkpoint_height: Height, state_hash: String, + initial_dkg_subnet_id: Option, chain_key_config: Option<(ChainKeyConfig, SubnetId)>, replacement_nodes: &[NodeId], registry_params: Option, @@ -208,6 +209,10 @@ impl AdminHelper { .add_argument("height", checkpoint_height) .add_argument("state-hash", state_hash); + if let Some(initial_dkg_subnet_id) = initial_dkg_subnet_id { + ic_admin.add_argument("initial-dkg-subnet-id", initial_dkg_subnet_id); + } + if let Some((config, subnet_id)) = chain_key_config { let key_requests = config .key_configs @@ -538,6 +543,7 @@ mod tests { Height::from(666), "fake_state_hash".to_string(), None, + None, &[], None, UNIX_EPOCH + Duration::from_nanos(123456), @@ -596,6 +602,7 @@ mod tests { subnet_id_from_str(FAKE_SUBNET_ID_1), Height::from(666), "fake_state_hash".to_string(), + Some(subnet_id_from_str(FAKE_SUBNET_ID_2)), Some((chain_key_config, subnet_id_from_str(FAKE_SUBNET_ID_2))), &[node_id_from_str(FAKE_NODE_ID)], Some(RegistryParams { @@ -620,6 +627,7 @@ mod tests { --subnet-index gpvux-2ejnk-3hgmh-cegwf-iekfc-b7rzs-hrvep-5euo2-3ywz3-k3hcb-cqe \ --height 666 \ --state-hash fake_state_hash \ + --initial-dkg-subnet-id mklno-zzmhy-zutel-oujwg-dzcli-h6nfy-2serg-gnwru-vuwck-hcxit-wqe \ --initial-chain-key-configs-to-request '[\ {\"subnet_id\":\"mklno-zzmhy-zutel-oujwg-dzcli-h6nfy-2serg-gnwru-vuwck-hcxit-wqe\",\"key_id\":\"ecdsa:Secp256k1:test_key_1\",\"pre_signatures_to_create_in_advance\":\"77\",\"max_queue_size\":\"30\"},\ {\"subnet_id\":\"mklno-zzmhy-zutel-oujwg-dzcli-h6nfy-2serg-gnwru-vuwck-hcxit-wqe\",\"key_id\":\"schnorr:Bip340Secp256k1:test_key_2\",\"pre_signatures_to_create_in_advance\":\"12\",\"max_queue_size\":\"32\"},\ diff --git a/rs/recovery/src/app_subnet_recovery.rs b/rs/recovery/src/app_subnet_recovery.rs index 6d94625caaf3..2bf04380d962 100644 --- a/rs/recovery/src/app_subnet_recovery.rs +++ b/rs/recovery/src/app_subnet_recovery.rs @@ -185,6 +185,11 @@ pub struct AppSubnetRecoveryArgs { #[clap(long, value_parser=crate::util::subnet_id_from_str)] pub chain_key_subnet_id: Option, + /// Optional subnet used to run `setup_initial_dkg` during recovery CUP proposal. + /// If not set, the request is handled by the NNS subnet. + #[clap(long, value_parser=crate::util::subnet_id_from_str)] + pub initial_dkg_subnet_id: Option, + /// If present the tool will start execution for the provided step, skipping the initial ones #[clap(long = "resume")] pub next_step: Option, @@ -364,6 +369,12 @@ impl RecoveryIterator for AppSubnetRecovery { "Enter ID of subnet to reshare Chain keys from: ", ); } + if self.params.initial_dkg_subnet_id.is_none() { + self.params.initial_dkg_subnet_id = read_optional_subnet_id( + &self.logger, + "Enter ID of subnet to setup initial DKG on (default: NNS): ", + ); + } } StepType::UploadState => { @@ -566,6 +577,7 @@ impl RecoveryIterator for AppSubnetRecovery { state_params.hash, self.params.replacement_nodes.as_ref().unwrap_or(&default), None, + self.params.initial_dkg_subnet_id, self.params.chain_key_subnet_id, )?)) } diff --git a/rs/recovery/src/lib.rs b/rs/recovery/src/lib.rs index 9b97c98864bd..d997269678e2 100644 --- a/rs/recovery/src/lib.rs +++ b/rs/recovery/src/lib.rs @@ -784,6 +784,7 @@ impl Recovery { state_hash: String, replacement_nodes: &[NodeId], registry_params: Option, + initial_dkg_subnet_id: Option, chain_key_subnet_id: Option, ) -> RecoveryResult> { let chain_key_config = chain_key_subnet_id @@ -811,6 +812,7 @@ impl Recovery { subnet_id, checkpoint_height, state_hash, + initial_dkg_subnet_id, chain_key_config, replacement_nodes, registry_params, diff --git a/rs/recovery/src/nns_recovery_failover_nodes.rs b/rs/recovery/src/nns_recovery_failover_nodes.rs index c1b4cbdaad57..253be60eff8d 100644 --- a/rs/recovery/src/nns_recovery_failover_nodes.rs +++ b/rs/recovery/src/nns_recovery_failover_nodes.rs @@ -412,6 +412,7 @@ impl RecoveryIterator for NNSRecoveryFailoverNodes { &[], Some(registry_params), None, + None, )?)) } else { Err(RecoveryError::StepSkipped) diff --git a/rs/recovery/src/recovery_state.rs b/rs/recovery/src/recovery_state.rs index b46326df2c48..987c8cd81c8b 100644 --- a/rs/recovery/src/recovery_state.rs +++ b/rs/recovery/src/recovery_state.rs @@ -197,6 +197,7 @@ mod tests { upload_method: None, wait_for_cup_node: None, chain_key_subnet_id: Some(fake_subnet_id()), + initial_dkg_subnet_id: None, next_step: None, upgrade_image_url: None, upgrade_image_hash: None, diff --git a/rs/recovery/subnet_splitting/src/subnet_splitting.rs b/rs/recovery/subnet_splitting/src/subnet_splitting.rs index bba478ca83fd..c8a55669b6fc 100644 --- a/rs/recovery/subnet_splitting/src/subnet_splitting.rs +++ b/rs/recovery/subnet_splitting/src/subnet_splitting.rs @@ -326,6 +326,7 @@ impl SubnetSplitting { state_hash, /*replacement_nodes=*/ &[], /*registry_params=*/ None, + /*initial_dkg_subnet_id=*/ None, /*chain_key_subnet_id=*/ None, ) } diff --git a/rs/registry/admin/bin/create_subnet.rs b/rs/registry/admin/bin/create_subnet.rs index a307321475d0..66561c3c82f2 100644 --- a/rs/registry/admin/bin/create_subnet.rs +++ b/rs/registry/admin/bin/create_subnet.rs @@ -14,7 +14,7 @@ use ic_protobuf::registry::subnet::v1::SubnetFeatures as SubnetFeaturesPb; use ic_registry_resource_limits::ResourceLimits; use ic_registry_subnet_features::SubnetFeatures; use ic_registry_subnet_type::SubnetType; -use ic_types::{NodeId, PrincipalId, ReplicaVersion}; +use ic_types::{NodeId, PrincipalId, ReplicaVersion, SubnetId}; use registry_canister::mutations::do_create_subnet; use registry_canister::mutations::do_create_subnet::CanisterCyclesCostSchedule; use std::collections::BTreeMap; @@ -38,6 +38,11 @@ pub(crate) struct ProposeToCreateSubnetCmd { // Assigns this subnet ID to the newly created subnet pub subnet_id_override: Option, + #[clap(long)] + /// Optional subnet that should handle `setup_initial_dkg` for subnet creation. + /// If not set, handling defaults to the NNS subnet. + pub initial_dkg_subnet_id: Option, + #[clap(long)] /// Maximum amount of bytes per message. This is a hard cap. pub max_ingress_bytes_per_message: Option, @@ -314,6 +319,7 @@ impl ProposeToCreateSubnetCmd { do_create_subnet::CreateSubnetPayload { node_ids, subnet_id_override: self.subnet_id_override, + initial_dkg_subnet_id: self.initial_dkg_subnet_id.map(SubnetId::from), max_ingress_bytes_per_message: self.max_ingress_bytes_per_message.unwrap_or_default(), max_ingress_messages_per_block: self.max_ingress_messages_per_block.unwrap_or_default(), max_ingress_bytes_per_block: self.max_ingress_bytes_per_block, @@ -401,6 +407,7 @@ mod tests { summary_file: None, subnet_handler_id: None, subnet_id_override: None, + initial_dkg_subnet_id: None, max_ingress_bytes_per_message: None, max_ingress_messages_per_block: None, max_ingress_bytes_per_block: None, @@ -519,6 +526,20 @@ mod tests { ); } + #[test] + fn cli_to_payload_conversion_includes_initial_dkg_subnet_id() { + let initial_dkg_subnet_id = PrincipalId::from_str("gxevo-lhkam-aaaaa-aaaap-yai").unwrap(); + let mut cmd = ProposeToCreateSubnetCmd { + initial_dkg_subnet_id: Some(initial_dkg_subnet_id), + ..empty_propose_to_create_subnet_cmd() + }; + cmd.apply_defaults_for_unset_fields(); + assert_eq!( + cmd.new_payload().initial_dkg_subnet_id, + Some(SubnetId::from(initial_dkg_subnet_id)) + ); + } + #[test] #[should_panic( expected = "must specify 'pre_signatures_to_create_in_advance' for key ecdsa:Secp256k1:some_key_name" diff --git a/rs/registry/admin/bin/recover_subnet.rs b/rs/registry/admin/bin/recover_subnet.rs index 1064ba93be5a..13ca9581ec91 100644 --- a/rs/registry/admin/bin/recover_subnet.rs +++ b/rs/registry/admin/bin/recover_subnet.rs @@ -25,6 +25,11 @@ pub(crate) struct ProposeToUpdateRecoveryCupCmd { /// The targeted subnet. subnet: SubnetDescriptor, + #[clap(long)] + /// Optional subnet that should handle `setup_initial_dkg` for subnet recovery. + /// If not set, handling defaults to the NNS subnet. + pub initial_dkg_subnet_id: Option, + #[clap(long, required = true)] /// The height of the CUP pub height: u64, @@ -220,6 +225,7 @@ impl ProposeToUpdateRecoveryCupCmd { }; do_recover_subnet::RecoverSubnetPayload { + initial_dkg_subnet_id: self.initial_dkg_subnet_id.map(SubnetId::from), height: self.height, time_ns: self.time_ns, subnet_id: subnet_id.get(), @@ -259,6 +265,7 @@ mod tests { ) -> do_recover_subnet::RecoverSubnetPayload { do_recover_subnet::RecoverSubnetPayload { subnet_id: subnet_id.get(), + initial_dkg_subnet_id: None, height, time_ns, state_hash: hex::decode(state_hash) @@ -277,6 +284,7 @@ mod tests { ) -> ProposeToUpdateRecoveryCupCmd { ProposeToUpdateRecoveryCupCmd { subnet: SubnetDescriptor::Id(subnet_id.get()), + initial_dkg_subnet_id: None, test_neuron_proposer: false, dry_run: false, json: true, @@ -393,6 +401,25 @@ mod tests { ); } + #[test] + fn cli_to_payload_conversion_includes_initial_dkg_subnet_id() { + let subnet_id = SubnetId::from(PrincipalId::new_user_test_id(1)); + let initial_dkg_subnet_id = PrincipalId::from_str("gxevo-lhkam-aaaaa-aaaap-yai").unwrap(); + let height = 1; + let time_ns = 2; + let state_hash = + "5d6601ac575f565b7c61d6bf5f9b25fa503bf7d756210a9a1fe8d8a32967f2e5".to_string(); + let cmd = ProposeToUpdateRecoveryCupCmd { + initial_dkg_subnet_id: Some(initial_dkg_subnet_id), + ..empty_propose_to_recover_subnet_cmd(subnet_id, height, time_ns, state_hash) + }; + + assert_eq!( + cmd.new_payload_for_subnet(subnet_id).initial_dkg_subnet_id, + Some(SubnetId::from(initial_dkg_subnet_id)) + ); + } + #[test] #[should_panic( expected = "must specify 'pre_signatures_to_create_in_advance' for key ecdsa:Secp256k1:some_key_name" diff --git a/rs/registry/canister/canister/registry.did b/rs/registry/canister/canister/registry.did index 285bf36bcb54..876ffbbd11ef 100644 --- a/rs/registry/canister/canister/registry.did +++ b/rs/registry/canister/canister/registry.did @@ -101,6 +101,7 @@ type CreateSubnetPayload = record { replica_version_id : text; dkg_interval_length : nat64; subnet_id_override : opt principal; + initial_dkg_subnet_id : opt principal; ssh_backup_access : vec text; initial_notary_delay_millis : nat64; subnet_type : SubnetType; @@ -309,6 +310,7 @@ type RecoverSubnetPayload = record { height : nat64; replacement_nodes : opt vec principal; subnet_id : principal; + initial_dkg_subnet_id : opt principal; registry_store_uri : opt record { text; text; nat64 }; chain_key_config : opt InitialChainKeyConfig; state_hash : blob; diff --git a/rs/registry/canister/canister/registry_test.did b/rs/registry/canister/canister/registry_test.did index 0ad1df46508b..3cda7e6ac418 100644 --- a/rs/registry/canister/canister/registry_test.did +++ b/rs/registry/canister/canister/registry_test.did @@ -101,6 +101,7 @@ type CreateSubnetPayload = record { replica_version_id : text; dkg_interval_length : nat64; subnet_id_override : opt principal; + initial_dkg_subnet_id : opt principal; ssh_backup_access : vec text; initial_notary_delay_millis : nat64; subnet_type : SubnetType; @@ -309,6 +310,7 @@ type RecoverSubnetPayload = record { height : nat64; replacement_nodes : opt vec principal; subnet_id : principal; + initial_dkg_subnet_id : opt principal; registry_store_uri : opt record { text; text; nat64 }; chain_key_config : opt InitialChainKeyConfig; state_hash : blob; diff --git a/rs/registry/canister/src/mutations/do_create_subnet.rs b/rs/registry/canister/src/mutations/do_create_subnet.rs index 281f05d83d92..91a11caf9418 100644 --- a/rs/registry/canister/src/mutations/do_create_subnet.rs +++ b/rs/registry/canister/src/mutations/do_create_subnet.rs @@ -61,6 +61,7 @@ impl Registry { let request = SetupInitialDKGArgs::new( payload.node_ids.clone(), RegistryVersion::new(self.latest_version()), + payload.initial_dkg_subnet_id, ); // 2a. Invoke NI-DKG on ic_00 @@ -181,12 +182,21 @@ impl Registry { } /// Validates runtime payload values that aren't checked by invariants. + /// Ensures that the initial DKG subnet exists. /// Ensures that the obsolete ECDSA keys are not specified. /// Ensures all nodes for new subnet a) exist and b) are not in another subnet. /// Ensure all nodes for new subnet are not already assigned as ApiBoundaryNode. /// Ensures that a valid `subnet_id` is specified for `KeyConfigRequest`s. /// Ensures that master public keys (a) exist and (b) are present on the requested subnet. fn validate_create_subnet_payload(&self, payload: &CreateSubnetPayload) { + if let Some(initial_dkg_subnet_id) = payload.initial_dkg_subnet_id + && self + .get_subnet(initial_dkg_subnet_id, self.latest_version()) + .is_err() + { + panic!("{LOG_PREFIX}Initial DKG subnet '{initial_dkg_subnet_id}' does not exist."); + } + // Verify that all Nodes exist payload.node_ids.iter().for_each(|node_id| { match self.get( @@ -267,6 +277,9 @@ pub struct CreateSubnetPayload { pub node_ids: Vec, pub subnet_id_override: Option, + /// Optional subnet that should handle `setup_initial_dkg`. + /// If not set, the request is handled by the NNS subnet. + pub initial_dkg_subnet_id: Option, pub max_ingress_bytes_per_message: u64, pub max_ingress_bytes_per_block: Option, @@ -607,6 +620,7 @@ mod test { use ic_management_canister_types_private::{EcdsaCurve, EcdsaKeyId, VetKdCurve, VetKdKeyId}; use ic_nervous_system_common_test_keys::{TEST_USER1_PRINCIPAL, TEST_USER2_PRINCIPAL}; use ic_registry_subnet_features::{ChainKeyConfig, DEFAULT_ECDSA_MAX_QUEUE_SIZE}; + use ic_test_utilities_types::ids::subnet_test_id; use ic_types::ReplicaVersion; // Note: this can only be unit-tested b/c it fails before we hit inter-canister calls @@ -865,6 +879,18 @@ mod test { futures::executor::block_on(registry.do_create_subnet(payload)); } + #[test] + #[should_panic(expected = "Initial DKG subnet 'hmpuf-nqpe4-aaaaa-aaaap-yai' does not exist")] + fn should_panic_if_initial_dkg_subnet_does_not_exist() { + let mut registry = invariant_compliant_registry(0); + let payload = CreateSubnetPayload { + initial_dkg_subnet_id: Some(subnet_test_id(9999)), + ..Default::default() + }; + + futures::executor::block_on(registry.do_create_subnet(payload)); + } + fn create_subnet_payload_with_key_config( key_id: MasterPublicKeyId, pre_signatures_to_create_in_advance: Option, diff --git a/rs/registry/canister/src/mutations/do_recover_subnet.rs b/rs/registry/canister/src/mutations/do_recover_subnet.rs index 880f1e64b3ab..3b37c226d2b8 100644 --- a/rs/registry/canister/src/mutations/do_recover_subnet.rs +++ b/rs/registry/canister/src/mutations/do_recover_subnet.rs @@ -98,6 +98,7 @@ impl Registry { let request = SetupInitialDKGArgs::new( dkg_nodes.clone(), RegistryVersion::new(pre_call_registry_version), + payload.initial_dkg_subnet_id, ); let initial_chain_key_config = @@ -222,13 +223,33 @@ impl Registry { self.maybe_apply_mutation_internal(mutations) } - /// Ensures the requested Chain keys exist somewhere. + /// Ensures that the initial DKG subnet is different from the subnet being recovered. + /// Ensures that the initial DKG subnet exists. + /// Ensures that the requested Chain keys exist somewhere. /// Ensures that a subnet_id is specified for ChainKeyRequests. /// Ensures that the requested key exists outside of the subnet being recovered. /// Ensures that the requested key exists on the specified subnet. /// This is similar to validation in do_create_subnet except for constraints to avoid requesting /// keys from the subnet. fn validate_recover_subnet_payload(&self, payload: &RecoverSubnetPayload) { + if let Some(initial_dkg_subnet_id) = payload.initial_dkg_subnet_id { + if initial_dkg_subnet_id.get() == payload.subnet_id { + panic!( + "{LOG_PREFIX}Initial DKG subnet must be different from the subnet being recovered.", + ); + } + + if self + .get_subnet(initial_dkg_subnet_id, self.latest_version()) + .is_err() + { + panic!( + "{LOG_PREFIX}Initial DKG subnet '{}' does not exist.", + initial_dkg_subnet_id + ); + } + } + let Some(initial_chain_key_config) = &payload.chain_key_config else { return; // Nothing to do. }; @@ -255,6 +276,9 @@ impl Registry { pub struct RecoverSubnetPayload { /// The subnet ID to add the recovery CUP to pub subnet_id: PrincipalId, + /// Optional subnet that should handle `setup_initial_dkg`. + /// If not set, the request is handled by the NNS subnet. + pub initial_dkg_subnet_id: Option, /// The height of the CUP pub height: u64, /// The block time to start from (nanoseconds from Epoch) @@ -509,6 +533,7 @@ mod test { fn get_default_recover_subnet_payload(subnet_id: SubnetId) -> RecoverSubnetPayload { RecoverSubnetPayload { subnet_id: subnet_id.get(), + initial_dkg_subnet_id: None, height: 0, time_ns: 0, state_hash: vec![], @@ -850,6 +875,30 @@ mod test { futures::executor::block_on(registry.do_recover_subnet(payload)); } + #[test] + #[should_panic( + expected = "Initial DKG subnet must be different from the subnet being recovered" + )] + fn do_recover_subnet_should_panic_if_initial_dkg_subnet_is_same_as_recovered_subnet() { + let mut registry = invariant_compliant_registry(0); + let subnet_id = subnet_test_id(1000); + let mut payload = get_default_recover_subnet_payload(subnet_id); + payload.initial_dkg_subnet_id = Some(subnet_id); + + futures::executor::block_on(registry.do_recover_subnet(payload)); + } + + #[test] + #[should_panic(expected = "Initial DKG subnet 'hmpuf-nqpe4-aaaaa-aaaap-yai' does not exist")] + fn do_recover_subnet_should_panic_if_initial_dkg_subnet_does_not_exist() { + let mut registry = invariant_compliant_registry(0); + let subnet_id = subnet_test_id(1000); + let mut payload = get_default_recover_subnet_payload(subnet_id); + payload.initial_dkg_subnet_id = Some(subnet_test_id(9999)); + + futures::executor::block_on(registry.do_recover_subnet(payload)); + } + fn recover_subnet_payload_with_key_config( subnet_id: SubnetId, key_id: MasterPublicKeyId, diff --git a/rs/registry/canister/src/mutations/do_split_subnet.rs b/rs/registry/canister/src/mutations/do_split_subnet.rs index 649ef92f787f..c83f21378fc5 100644 --- a/rs/registry/canister/src/mutations/do_split_subnet.rs +++ b/rs/registry/canister/src/mutations/do_split_subnet.rs @@ -114,8 +114,11 @@ impl Registry { }; let create_cup_contents = |nodes| async { - let request = - SetupInitialDKGArgs::new(nodes, RegistryVersion::new(pre_call_registry_version)); + let request = SetupInitialDKGArgs::new( + nodes, + RegistryVersion::new(pre_call_registry_version), + None, // Initial DKG request is handled by the NNS subnet + ); let raw_response = call( CanisterId::ic_00(), "setup_initial_dkg", diff --git a/rs/registry/canister/tests/create_subnet.rs b/rs/registry/canister/tests/create_subnet.rs index 07728149f072..237fc33bf6f9 100644 --- a/rs/registry/canister/tests/create_subnet.rs +++ b/rs/registry/canister/tests/create_subnet.rs @@ -303,6 +303,7 @@ fn test_accepted_proposal_with_chain_key_gets_keys_from_other_subnet(key_id: Mas let idkg_key_rotation_period_ms = Some(12345); let max_parallel_pre_signature_transcripts_in_creation = Some(12345); let payload = CreateSubnetPayload { + initial_dkg_subnet_id: Some(system_subnet_id), chain_key_config: Some(InitialChainKeyConfig { key_configs: vec![KeyConfigRequest { key_config: Some(KeyConfig { @@ -405,6 +406,7 @@ fn make_create_subnet_payload(node_ids: Vec) -> CreateSubnetPayload { CreateSubnetPayload { node_ids, subnet_id_override: None, + initial_dkg_subnet_id: None, max_ingress_bytes_per_message: 60 * 1024 * 1024, max_ingress_bytes_per_block: None, max_ingress_messages_per_block: 1000, diff --git a/rs/registry/canister/tests/recover_subnet.rs b/rs/registry/canister/tests/recover_subnet.rs index 2029b340b75f..a351abf4c966 100644 --- a/rs/registry/canister/tests/recover_subnet.rs +++ b/rs/registry/canister/tests/recover_subnet.rs @@ -161,6 +161,7 @@ fn test_recover_subnet_with_replacement_nodes() { let payload = RecoverSubnetPayload { subnet_id: subnet_id.get(), + initial_dkg_subnet_id: None, height: 10, time_ns: 1200, state_hash: vec![10, 20, 30], @@ -371,6 +372,7 @@ fn test_recover_subnet_gets_chain_keys_when_needed(key_id: MasterPublicKeyId) { let max_parallel_pre_signature_transcripts_in_creation = Some(12345); let payload = RecoverSubnetPayload { subnet_id: subnet_to_recover_subnet_id.get(), + initial_dkg_subnet_id: Some(system_subnet_id), height: 10, time_ns: 1200, state_hash: vec![10, 20, 30], @@ -623,6 +625,7 @@ fn test_recover_subnet_without_chain_key_removes_it_from_signing_list(key_id: Ma let max_parallel_pre_signature_transcripts_in_creation = Some(12345); let payload = RecoverSubnetPayload { subnet_id: subnet_to_recover_subnet_id.get(), + initial_dkg_subnet_id: None, height: 10, time_ns: 1200, state_hash: vec![10, 20, 30], @@ -798,6 +801,7 @@ fn test_recover_subnet_resets_the_halt_at_cup_height_flag() { let payload = RecoverSubnetPayload { subnet_id: subnet_to_recover_subnet_id.get(), + initial_dkg_subnet_id: None, height: 10, time_ns: 1200, state_hash: vec![10, 20, 30], @@ -1102,6 +1106,7 @@ fn test_recover_subnet_resets_cup_contents() { let max_parallel_pre_signature_transcripts_in_creation = Some(12345); let payload = RecoverSubnetPayload { subnet_id: subnet_to_recover_subnet_id.get(), + initial_dkg_subnet_id: None, height: 10, time_ns: 1200, state_hash: vec![10, 20, 30], diff --git a/rs/registry/canister/unreleased_changelog.md b/rs/registry/canister/unreleased_changelog.md index 754a289115d4..5b516acf97e8 100644 --- a/rs/registry/canister/unreleased_changelog.md +++ b/rs/registry/canister/unreleased_changelog.md @@ -9,7 +9,10 @@ on the process that this file is part of, see ## Added -Added type4.1 through type4.5 node reward types for cloud-engine sub-variants. +* Added an optional field `initial_dkg_subnet_id` to `CreateSubnetPayload` and `RecoverSubnetPayload` + which, when present, determines the subnet to which the resulting `SetupInitialDKG` management + canister call should be routed. +* Added type4.1 through type4.5 node reward types for cloud-engine sub-variants. ## Changed diff --git a/rs/replica_tests/tests/tests.rs b/rs/replica_tests/tests/tests.rs index ca8fed8b89e2..d47fb178c74a 100644 --- a/rs/replica_tests/tests/tests.rs +++ b/rs/replica_tests/tests/tests.rs @@ -1913,7 +1913,8 @@ fn setup_initial_dkg_method_interface() { // keys for other nodes, yet the test fixture instantiates the registry // for a single node. let node_ids = vec![canister.node_id()]; - let request_payload = ic00::SetupInitialDKGArgs::new(node_ids, RegistryVersion::new(1)); + let request_payload = + ic00::SetupInitialDKGArgs::new(node_ids, RegistryVersion::new(1), None); let response = canister .update(wasm().call_simple( ic00::IC_00, diff --git a/rs/tests/consensus/cup_explorer_test.rs b/rs/tests/consensus/cup_explorer_test.rs index 7958af35e8aa..aa6b2406cee0 100644 --- a/rs/tests/consensus/cup_explorer_test.rs +++ b/rs/tests/consensus/cup_explorer_test.rs @@ -183,6 +183,7 @@ fn test(env: TestEnv) { info!(log, "Recover subnet with unchanged state hash"); let recover_subnet_payload = RecoverSubnetPayload { subnet_id: app_subnet.subnet_id.get(), + initial_dkg_subnet_id: None, height: cup.height().get() + 1000, time_ns: cup .content diff --git a/rs/tests/consensus/subnet_recovery/common.rs b/rs/tests/consensus/subnet_recovery/common.rs index 935886ba4e89..170c61b3f699 100644 --- a/rs/tests/consensus/subnet_recovery/common.rs +++ b/rs/tests/consensus/subnet_recovery/common.rs @@ -274,6 +274,7 @@ struct TestConfig { subnet_size: usize, upgrade: bool, chain_key: bool, + remote_initial_dkg: bool, corrupt_cup: CupCorruption, local_recovery: bool, provision_write_access: bool, @@ -285,6 +286,7 @@ impl TestConfig { subnet_size: APP_NODES, upgrade: true, chain_key: false, + remote_initial_dkg: false, corrupt_cup: CupCorruption::NotCorrupted, local_recovery: false, provision_write_access: false, @@ -306,6 +308,11 @@ impl TestConfig { self } + fn with_remote_initial_dkg(mut self) -> Self { + self.remote_initial_dkg = true; + self + } + fn with_corrupt_cup(mut self, corrupt_cup: CupCorruption) -> Self { self.corrupt_cup = corrupt_cup; self @@ -327,6 +334,11 @@ pub fn test_with_chain_keys(env: TestEnv) { app_subnet_recovery_test(env, config); } +pub fn test_with_chain_keys_and_remote_initial_dkg(env: TestEnv) { + let config = TestConfig::new().with_chain_key().with_remote_initial_dkg(); + app_subnet_recovery_test(env, config); +} + pub fn test_without_chain_keys(env: TestEnv, corrupt_cup: CupCorruption) { let config = TestConfig::new().with_corrupt_cup(corrupt_cup); app_subnet_recovery_test(env, config); @@ -708,6 +720,7 @@ fn app_subnet_recovery_test(env: TestEnv, cfg: TestConfig) { upload_method: Some(DataLocation::Remote(upload_node.get_ip_addr())), wait_for_cup_node: Some(upload_node.get_ip_addr()), chain_key_subnet_id: cfg.chain_key.then_some(source_subnet_id), + initial_dkg_subnet_id: cfg.remote_initial_dkg.then_some(source_subnet_id), next_step: None, // Skip validating the output if the CUP is corrupted, as in this case no replica will be // running to compare the heights to. diff --git a/rs/tests/consensus/subnet_recovery/sr_app_same_nodes_with_chain_keys_test.rs b/rs/tests/consensus/subnet_recovery/sr_app_same_nodes_with_chain_keys_test.rs index 9fb8c343c124..621aa18ff680 100644 --- a/rs/tests/consensus/subnet_recovery/sr_app_same_nodes_with_chain_keys_test.rs +++ b/rs/tests/consensus/subnet_recovery/sr_app_same_nodes_with_chain_keys_test.rs @@ -2,7 +2,7 @@ use anyhow::Result; use ic_consensus_system_test_subnet_recovery::common::{ CHAIN_KEY_SUBNET_RECOVERY_TIMEOUT, setup_same_nodes_chain_keys as setup, - test_with_chain_keys as test, + test_with_chain_keys_and_remote_initial_dkg as test, }; use ic_system_test_driver::driver::group::SystemTestGroup; use ic_system_test_driver::systest; diff --git a/rs/tests/consensus/subnet_recovery/utils.rs b/rs/tests/consensus/subnet_recovery/utils.rs index 63fcf13a6204..c9201021e33f 100644 --- a/rs/tests/consensus/subnet_recovery/utils.rs +++ b/rs/tests/consensus/subnet_recovery/utils.rs @@ -409,6 +409,7 @@ pub mod local { upload_method: _, // ignored to choose "local" in local recoveries, see below wait_for_cup_node, chain_key_subnet_id, + initial_dkg_subnet_id, next_step, skip, } = &subnet_recovery.params; @@ -465,6 +466,7 @@ pub mod local { let upload_method_cli = r#"--upload-method "local" "#.to_string(); let wait_for_cup_node_cli = opt_cli_arg!(wait_for_cup_node); let chain_key_subnet_id_cli = opt_cli_arg!(chain_key_subnet_id); + let initial_dkg_subnet_id_cli = opt_cli_arg!(initial_dkg_subnet_id); let next_step_cli = opt_cli_arg!(next_step); let skip_cli = opt_vec_cli_arg!(skip); @@ -488,6 +490,7 @@ pub mod local { {upload_method_cli} \ {wait_for_cup_node_cli} \ {chain_key_subnet_id_cli} \ + {initial_dkg_subnet_id_cli} \ {next_step_cli} \ {skip_cli}"# ) diff --git a/rs/tests/consensus/tecdsa/utils/src/lib.rs b/rs/tests/consensus/tecdsa/utils/src/lib.rs index f332da446bab..70605a09a763 100644 --- a/rs/tests/consensus/tecdsa/utils/src/lib.rs +++ b/rs/tests/consensus/tecdsa/utils/src/lib.rs @@ -1079,6 +1079,7 @@ pub async fn create_new_subnet_with_keys( let payload = CreateSubnetPayload { node_ids, subnet_id_override: None, + initial_dkg_subnet_id: None, max_ingress_bytes_per_message: config.max_ingress_bytes_per_message, max_ingress_bytes_per_block: Some(config.max_ingress_bytes_per_block), max_ingress_messages_per_block: config.max_ingress_messages_per_block, diff --git a/rs/tests/driver/src/nns.rs b/rs/tests/driver/src/nns.rs index 2ebb62b103fd..16b02f19eb40 100644 --- a/rs/tests/driver/src/nns.rs +++ b/rs/tests/driver/src/nns.rs @@ -721,6 +721,27 @@ pub async fn submit_create_application_subnet_proposal( replica_version: ReplicaVersion, cost_schedule: Option, max_number_of_canisters: Option, +) -> ProposalId { + submit_create_application_subnet_proposal_with_initial_dkg_subnet( + governance, + node_ids, + replica_version, + cost_schedule, + max_number_of_canisters, + None, + ) + .await +} + +/// Submits a proposal for creating an application subnet and optionally +/// selecting the subnet that handles initial DKG. +pub async fn submit_create_application_subnet_proposal_with_initial_dkg_subnet( + governance: &Canister<'_>, + node_ids: Vec, + replica_version: ReplicaVersion, + cost_schedule: Option, + max_number_of_canisters: Option, + initial_dkg_subnet_id: Option, ) -> ProposalId { let config = subnet_configuration::get_default_config_params(SubnetType::Application, node_ids.len()); @@ -728,6 +749,7 @@ pub async fn submit_create_application_subnet_proposal( let payload = CreateSubnetPayload { node_ids, subnet_id_override: None, + initial_dkg_subnet_id, max_ingress_bytes_per_message: config.max_ingress_bytes_per_message, max_ingress_bytes_per_block: Some(config.max_ingress_bytes_per_block), max_ingress_messages_per_block: config.max_ingress_messages_per_block, diff --git a/rs/tests/execution/general_execution_tests/nns_shielding.rs b/rs/tests/execution/general_execution_tests/nns_shielding.rs index 63f8372a7db3..7130f31882e7 100644 --- a/rs/tests/execution/general_execution_tests/nns_shielding.rs +++ b/rs/tests/execution/general_execution_tests/nns_shielding.rs @@ -145,22 +145,26 @@ pub fn no_cycle_balance_limit_on_nns_subnet(env: TestEnv) { pub fn app_canister_attempt_initiating_dkg_fails(env: TestEnv) { let logger = env.logger(); let app_node = env.get_first_healthy_application_node_snapshot(); + let app_subnet_id = app_node + .subnet_id() + .expect("application node has no subnet"); let agent = app_node.build_default_agent(); block_on(async move { - let node_ids: Vec<_> = (0..4).map(node_test_id).collect(); - let request = SetupInitialDKGArgs::new(node_ids, RegistryVersion::from(2)); - let uni_can = UniversalCanister::new_with_retries(&agent, app_node.effective_canister_id(), &logger) .await; - let res = uni_can - .forward_to( - &Principal::management_canister(), - "setup_initial_dkg", - Encode!(&request).unwrap(), - ) - .await; + for subnet_id in [None, Some(app_subnet_id)] { + let node_ids: Vec<_> = (0..4).map(node_test_id).collect(); + let request = SetupInitialDKGArgs::new(node_ids, RegistryVersion::from(2), subnet_id); + let res = uni_can + .forward_to( + &Principal::management_canister(), + "setup_initial_dkg", + Encode!(&request).unwrap(), + ) + .await; - assert_reject(res, RejectCode::CanisterReject); + assert_reject(res, RejectCode::CanisterReject); + } }); } diff --git a/rs/tests/nns/BUILD.bazel b/rs/tests/nns/BUILD.bazel index 025bf8da128a..104ef6dcc482 100644 --- a/rs/tests/nns/BUILD.bazel +++ b/rs/tests/nns/BUILD.bazel @@ -33,7 +33,7 @@ system_test_nns( ], ) -system_test( +system_test_nns( name = "create_subnet_test", tags = [ "long_test", diff --git a/rs/tests/nns/create_subnet_test.rs b/rs/tests/nns/create_subnet_test.rs index 7810b8b93229..2f0571a66c97 100644 --- a/rs/tests/nns/create_subnet_test.rs +++ b/rs/tests/nns/create_subnet_test.rs @@ -30,7 +30,8 @@ use ic_system_test_driver::driver::test_env_api::{ HasPublicApiUrl, HasTopologySnapshot, IcNodeContainer, NnsInstallationBuilder, SshSession, }; use ic_system_test_driver::nns::{ - self, get_software_version_from_snapshot, submit_create_application_subnet_proposal, + self, get_software_version_from_snapshot, + submit_create_application_subnet_proposal_with_initial_dkg_subnet, }; use ic_system_test_driver::nns::{get_subnet_list_from_registry, vote_on_proposal}; use ic_system_test_driver::systest; @@ -47,7 +48,7 @@ use std::time::Duration; const NNS_PRE_MASTER: usize = 4; const APP_PRE_MASTER: usize = 4; const DKG_INTERVAL_LENGTH: u64 = 29; -const APP_SUBNETS: usize = 5; +const APP_SUBNETS: usize = 6; fn main() -> Result<()> { SystemTestGroup::new() @@ -64,6 +65,10 @@ pub fn setup(env: TestEnv) { Subnet::fast(SubnetType::System, NNS_PRE_MASTER) .with_dkg_interval_length(Height::from(DKG_INTERVAL_LENGTH)), ) + .add_subnet( + Subnet::fast(SubnetType::Application, APP_PRE_MASTER) + .with_dkg_interval_length(Height::from(DKG_INTERVAL_LENGTH)), + ) .with_unassigned_nodes(APP_PRE_MASTER * APP_SUBNETS) .setup_and_start(&env) .expect("failed to setup IC under test"); @@ -84,6 +89,11 @@ pub fn test(env: TestEnv) { install_nns_canisters(&env); let topology_snapshot = &env.topology_snapshot(); let subnet = topology_snapshot.root_subnet(); + let initial_dkg_subnet_id = topology_snapshot + .subnets() + .find(|s| s.subnet_id != subnet.subnet_id) + .expect("missing second subnet for setup_initial_dkg") + .subnet_id; let endpoint = subnet.nodes().next().unwrap(); // get IDs of all unassigned nodes @@ -112,18 +122,25 @@ pub fn test(env: TestEnv) { // Submit and adopt the configured number of create subnet proposals let mut proposal_ids = vec![]; - for _ in 0..APP_SUBNETS { + for proposal_idx in 0..APP_SUBNETS { let nodes = unassigned_nodes.by_ref().take(APP_PRE_MASTER).collect(); + let initial_dkg_subnet_id = if proposal_idx % 2 == 0 { + Some(initial_dkg_subnet_id) + } else { + None + }; info!( log, - "Submitting proposal to create subnet with nodes: {nodes:?}" + "Submitting proposal to create subnet with nodes: {nodes:?}, + initial_dkg_subnet_id: {initial_dkg_subnet_id:?}" ); - let proposal_id = submit_create_application_subnet_proposal( + let proposal_id = submit_create_application_subnet_proposal_with_initial_dkg_subnet( &governance, nodes, version.clone(), Some(CanisterCyclesCostSchedule::Normal), Some(0), + initial_dkg_subnet_id, ) .await; info!(log, "Voting on proposal {proposal_id}"); diff --git a/rs/types/management_canister_types/src/lib.rs b/rs/types/management_canister_types/src/lib.rs index 1cf7d63c9a8d..38558011d78e 100644 --- a/rs/types/management_canister_types/src/lib.rs +++ b/rs/types/management_canister_types/src/lib.rs @@ -2608,21 +2608,28 @@ impl<'a> Payload<'a> for CreateCanisterArgs { /// record { /// node_ids : vec principal; /// registry_version : nat64; +/// subnet_id : opt principal; /// } /// ``` #[derive(Debug, CandidType, Deserialize)] pub struct SetupInitialDKGArgs { node_ids: Vec, registry_version: u64, + subnet_id: Option, } impl Payload<'_> for SetupInitialDKGArgs {} impl SetupInitialDKGArgs { - pub fn new(node_ids: Vec, registry_version: RegistryVersion) -> Self { + pub fn new( + node_ids: Vec, + registry_version: RegistryVersion, + subnet_id: Option, + ) -> Self { Self { node_ids: node_ids.iter().map(|node_id| node_id.get()).collect(), registry_version: registry_version.get(), + subnet_id, } } @@ -2642,6 +2649,10 @@ impl SetupInitialDKGArgs { pub fn get_registry_version(&self) -> RegistryVersion { RegistryVersion::new(self.registry_version) } + + pub fn get_subnet_id(&self) -> Option { + self.subnet_id + } } /// Represents the response for a request to setup an initial DKG for a new