From a256f633d1d8494a0a6c1ec9794f578dddf04369 Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Thu, 2 Apr 2026 10:57:55 +0000 Subject: [PATCH 1/5] feat: generate dictator neuron secret key at runtime --- Cargo.lock | 1 + rs/tests/testnets/mainnet_nns/BUILD.bazel | 1 + rs/tests/testnets/mainnet_nns/Cargo.toml | 1 + rs/tests/testnets/mainnet_nns/src/lib.rs | 47 +++++++---- .../testnets/mainnet_nns/src/proposals.rs | 81 +++++++++---------- 5 files changed, 73 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e7db9de3187..596191ee09cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15230,6 +15230,7 @@ dependencies = [ "ic_consensus_system_test_utils", "nix 0.24.3", "once_cell", + "rand 0.8.5", "registry-canister", "serde", "slog", diff --git a/rs/tests/testnets/mainnet_nns/BUILD.bazel b/rs/tests/testnets/mainnet_nns/BUILD.bazel index 49023d3cfeac..41ed7a48a299 100644 --- a/rs/tests/testnets/mainnet_nns/BUILD.bazel +++ b/rs/tests/testnets/mainnet_nns/BUILD.bazel @@ -33,6 +33,7 @@ rust_library( "@crate_index//:ic-agent", "@crate_index//:nix", "@crate_index//:once_cell", + "@crate_index//:rand", "@crate_index//:serde", "@crate_index//:slog", "@crate_index//:ssh2", diff --git a/rs/tests/testnets/mainnet_nns/Cargo.toml b/rs/tests/testnets/mainnet_nns/Cargo.toml index 5a4210a09546..1978f913bb04 100644 --- a/rs/tests/testnets/mainnet_nns/Cargo.toml +++ b/rs/tests/testnets/mainnet_nns/Cargo.toml @@ -27,6 +27,7 @@ ic-types = { path = "../../../types/types" } ic_consensus_system_test_utils = { path = "../../consensus/utils" } nix = { workspace = true } once_cell = "1.21" +rand = { workspace = true } registry-canister = { path = "../../../registry/canister" } serde = { workspace = true } slog = { workspace = true } diff --git a/rs/tests/testnets/mainnet_nns/src/lib.rs b/rs/tests/testnets/mainnet_nns/src/lib.rs index 69ef0e754b87..ab596393c82d 100644 --- a/rs/tests/testnets/mainnet_nns/src/lib.rs +++ b/rs/tests/testnets/mainnet_nns/src/lib.rs @@ -1,5 +1,7 @@ use anyhow::Result; use ic_base_types::PrincipalId; +use ic_canister_client::{Ed25519KeyPair, Sender}; +use ic_canister_client_sender::SigKeys; use ic_consensus_system_test_utils::rw_message::install_nns_and_check_progress; use ic_crypto_utils_threshold_sig_der::public_key_der_to_pem; use ic_limits::DKG_INTERVAL_HEIGHT; @@ -30,9 +32,9 @@ use std::sync::mpsc::{self, Receiver}; use std::{io::Write, process::Command}; use url::Url; -use crate::proposals::NEURON_CONTROLLER; -use crate::proposals::NEURON_SECRET_KEY_PEM; -use crate::proposals::ProposalWithMainnetState; +use crate::proposals::{ + ProposalWithMainnetState, RECOVERED_NNS_DICTATOR_NEURON_IDENTITY, RecoveredNnsDictatorNeuron, +}; pub const MAINNET_NODE_VM_RESOURCE_OVERRIDES: VmResourceOverrides = VmResourceOverrides { boot_image_minimal_size_gibibytes: Some(ImageSizeGiB::new(192)), @@ -191,7 +193,9 @@ fn setup_recovered_nns( .join() .unwrap_or_else(|e| panic!("Failed to fetch the mainnet ic-replay because {e:?}")); - let neuron_id: NeuronId = setup_test_neuron(&env); + let neuron_identity = Ed25519KeyPair::generate(&mut rand::thread_rng()); + let neuron_principal = Sender::SigKeys(SigKeys::Ed25519(neuron_identity)).get_principal_id(); + let neuron_id = setup_test_neuron(&env, neuron_principal); // Wait until the aux node is setup and we have fetched ic-recovery before starting the recovery let aux_node = rx_aux_node.recv().unwrap(); @@ -200,7 +204,13 @@ fn setup_recovered_nns( .unwrap_or_else(|e| panic!("Failed to fetch the mainnet ic-recovery because {e:?}")); recover_nns_subnet(&env, &nns_node, &recovered_nns_node, &aux_node); - ProposalWithMainnetState::write_dictator_neuron_id_to_env(&env, neuron_id); + ProposalWithMainnetState::write_dictator_neuron_identity_to_env( + &env, + RecoveredNnsDictatorNeuron { + neuron_id, + neuron_secret_key_pem: neuron_identity.to_pem(), + }, + ); test_recovered_nns(&env, &recovered_nns_node); @@ -378,15 +388,14 @@ fn fetch_ic_config(env: &TestEnv, nns_node: &IcNodeSnapshot) { ); } -fn setup_test_neuron(env: &TestEnv) -> NeuronId { - let neuron_id = with_neuron_for_tests(env); - with_trusted_neurons_following_neuron_for_tests(env, neuron_id); +fn setup_test_neuron(env: &TestEnv, controller: PrincipalId) -> NeuronId { + let neuron_id = with_neuron_for_tests(env, controller); + with_trusted_neurons_following_neuron_for_tests(env, neuron_id, controller); neuron_id } -fn with_neuron_for_tests(env: &TestEnv) -> NeuronId { +fn with_neuron_for_tests(env: &TestEnv, controller: PrincipalId) -> NeuronId { let logger: slog::Logger = env.logger(); - let controller = PrincipalId::from_str(NEURON_CONTROLLER).unwrap(); info!(logger, "Create a neuron followed by trusted neurons ..."); // The neuron's stake must be large enough to be eligible to make proposals (> reject cost fee), @@ -418,12 +427,14 @@ fn with_neuron_for_tests(env: &TestEnv) -> NeuronId { neuron_id } -fn with_trusted_neurons_following_neuron_for_tests(env: &TestEnv, neuron_id: NeuronId) { - let NeuronId(id) = neuron_id; - let controller = PrincipalId::from_str(NEURON_CONTROLLER).unwrap(); +fn with_trusted_neurons_following_neuron_for_tests( + env: &TestEnv, + neuron_id: NeuronId, + controller: PrincipalId, +) { ic_replay(env, |cmd| { cmd.arg("with-trusted-neurons-following-neuron-for-tests") - .arg(id.to_string()) + .arg(neuron_id.0.to_string()) .arg(controller.to_string()); }); } @@ -710,7 +721,13 @@ fn write_sh_lib(env: &TestEnv, neuron_id: NeuronId, http_gateway: &Url) { let pem = env.get_path("neuron_secret_key.pem"); let mut pem_file = File::create(&pem).unwrap(); pem_file - .write_all(NEURON_SECRET_KEY_PEM.as_bytes()) + .write_all( + RECOVERED_NNS_DICTATOR_NEURON_IDENTITY + .get() + .unwrap() + .1 + .as_bytes(), + ) .unwrap(); let neuron_id_number = neuron_id.0; fs::write( diff --git a/rs/tests/testnets/mainnet_nns/src/proposals.rs b/rs/tests/testnets/mainnet_nns/src/proposals.rs index aed2ef04e9bc..fefbdcfade20 100644 --- a/rs/tests/testnets/mainnet_nns/src/proposals.rs +++ b/rs/tests/testnets/mainnet_nns/src/proposals.rs @@ -35,29 +35,25 @@ use url::Url; * `[name_of_the_proposal]`. * * IMPORTANT: Before making any proposal with this module, you MUST call -* `ProposalWithMainnetState::read_dictator_neuron_id_from_env(&env)` once in your `test` function -* (not in `setup`, as those functions run in different processes). This is necessary, so that the -* neuron ID is initialized from the test environment and used in subsequent proposals. +* `ProposalWithMainnetState::read_dictator_neuron_identity_from_env(&env)` once in your `test` +* function (not in `setup`, as those functions run in different processes). This is necessary, so +* that the neuron identity is initialized from the test environment and used in subsequent +* proposals. */ -// Test neuron secret key and corresponding controller principal -pub(crate) const NEURON_CONTROLLER: &str = - "bc7vk-kulc6-vswcu-ysxhv-lsrxo-vkszu-zxku3-xhzmh-iac7m-lwewm-2ae"; -pub(crate) const NEURON_SECRET_KEY_PEM: &str = "-----BEGIN PRIVATE KEY----- -MFMCAQEwBQYDK2VwBCIEIKohpVANxO4xElQYXElAOXZHwJSVHERLE8feXSfoKwxX -oSMDIQBqgs2z86b+S5X9HvsxtE46UZwfDHtebwmSQWSIcKr2ew== ------END PRIVATE KEY-----"; - -static RECOVERED_NNS_DICTATOR_NEURON_ID: OnceCell = OnceCell::new(); +// Test neuron ID and secret key of its controller encoded in PEM format. +pub(crate) static RECOVERED_NNS_DICTATOR_NEURON_IDENTITY: OnceCell<(NeuronId, String)> = + OnceCell::new(); #[derive(Deserialize, Serialize)] pub struct RecoveredNnsDictatorNeuron { - recovered_nns_dictator_neuron_id: NeuronId, + pub neuron_id: NeuronId, + pub neuron_secret_key_pem: String, } impl TestEnvAttribute for RecoveredNnsDictatorNeuron { fn attribute_name() -> String { - String::from("recovered_nns_dictator_neuron_id") + String::from("recovered_nns_dictator_neuron_identity") } } @@ -67,50 +63,49 @@ pub struct ProposalWithMainnetState { } impl ProposalWithMainnetState { - /// Initializes a ProposalWithMainnetState instance reading the neuron ID from the static - /// variable, which must have been initialized before via `read_dictator_neuron_id_from_env`. + /// Initializes a ProposalWithMainnetState instance reading the neuron identty from the static + /// variable, which must have been initialized before via + /// `read_dictator_neuron_identity_from_env`. /// /// This function is not intended to be called externally (thus it is not `pub`), but only called /// at the beginning of each proposal function below. fn new() -> Self { - let neuron_id = *RECOVERED_NNS_DICTATOR_NEURON_ID.get().expect( - "'read_dictator_neuron_id_from_env' must be called before using ProposalWithMainnetState", + let (neuron_id, secret_key_pem) = RECOVERED_NNS_DICTATOR_NEURON_IDENTITY.get().expect( + "'read_dictator_neuron_identity_from_env' must be called before using ProposalWithMainnetState", ); - let sig_keys = - SigKeys::from_pem(NEURON_SECRET_KEY_PEM).expect("Failed to parse secret key"); + let sig_keys = SigKeys::from_pem(secret_key_pem).expect("Failed to parse secret key"); let proposal_sender = Sender::SigKeys(sig_keys); Self { - neuron_id, + neuron_id: *neuron_id, proposal_sender, } } - /// Writes the given dictator neuron ID to the test environment, so that it can be read later on - /// potentially by a different process. Currently used in the `setup` function that creates a - /// testnet with NNS mainnet state. - pub fn write_dictator_neuron_id_to_env(env: &TestEnv, neuron_id: NeuronId) { - RecoveredNnsDictatorNeuron { - recovered_nns_dictator_neuron_id: neuron_id, - } - .write_attribute(env); + /// Writes the given dictator neuron identity to the test environment, so that it can be read + /// later on potentially by a different process. Currently used in the `setup` function that + /// creates a testnet with NNS mainnet state. + pub fn write_dictator_neuron_identity_to_env( + env: &TestEnv, + neuron: RecoveredNnsDictatorNeuron, + ) { + neuron.write_attribute(env); - RECOVERED_NNS_DICTATOR_NEURON_ID - .set(neuron_id) - .expect("'write_dictator_neuron_id_to_env' can only be called once"); + RECOVERED_NNS_DICTATOR_NEURON_IDENTITY + .set((neuron.neuron_id, neuron.neuron_secret_key_pem)) + .expect("'write_dictator_neuron_identity_to_env' can only be called once"); } - /// Initializes the static variable holding the dictator neuron ID by reading it from the test - /// environment. This function MUST be called once before using any of the proposal functions - /// below. - pub fn read_dictator_neuron_id_from_env(env: &TestEnv) { - let neuron_id = RecoveredNnsDictatorNeuron::try_read_attribute(env) - .expect("'write_dictator_neuron_id_to_env' must be called before reading") - .recovered_nns_dictator_neuron_id; - - RECOVERED_NNS_DICTATOR_NEURON_ID - .set(neuron_id) - .expect("'read_dictator_neuron_id_from_env' can only be called once"); + /// Initializes the static variable holding the dictator neuron identity by reading it from the + /// test environment. This function MUST be called once before using any of the proposal + /// functions below. + pub fn read_dictator_neuron_identity_from_env(env: &TestEnv) { + let neuron = RecoveredNnsDictatorNeuron::try_read_attribute(env) + .expect("'write_dictator_neuron_identity_to_env' must be called before reading"); + + RECOVERED_NNS_DICTATOR_NEURON_IDENTITY + .set((neuron.neuron_id, neuron.neuron_secret_key_pem)) + .expect("'read_dictator_neuron_identity_from_env' can only be called once"); } /// Code duplicate of rs/tests/consensus/utils/src/upgrade.rs:bless_replica_version From 1c8bae07f8569b69987953fc730d1b3d6b50e864 Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Tue, 7 Apr 2026 12:35:59 +0000 Subject: [PATCH 2/5] style --- rs/tests/testnets/mainnet_nns/src/lib.rs | 25 ++++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/rs/tests/testnets/mainnet_nns/src/lib.rs b/rs/tests/testnets/mainnet_nns/src/lib.rs index ab596393c82d..27c2de94b1ce 100644 --- a/rs/tests/testnets/mainnet_nns/src/lib.rs +++ b/rs/tests/testnets/mainnet_nns/src/lib.rs @@ -193,9 +193,7 @@ fn setup_recovered_nns( .join() .unwrap_or_else(|e| panic!("Failed to fetch the mainnet ic-replay because {e:?}")); - let neuron_identity = Ed25519KeyPair::generate(&mut rand::thread_rng()); - let neuron_principal = Sender::SigKeys(SigKeys::Ed25519(neuron_identity)).get_principal_id(); - let neuron_id = setup_test_neuron(&env, neuron_principal); + let (neuron_id, neuron_secret_key_pem) = setup_test_neuron(&env); // Wait until the aux node is setup and we have fetched ic-recovery before starting the recovery let aux_node = rx_aux_node.recv().unwrap(); @@ -208,7 +206,7 @@ fn setup_recovered_nns( &env, RecoveredNnsDictatorNeuron { neuron_id, - neuron_secret_key_pem: neuron_identity.to_pem(), + neuron_secret_key_pem, }, ); @@ -388,10 +386,12 @@ fn fetch_ic_config(env: &TestEnv, nns_node: &IcNodeSnapshot) { ); } -fn setup_test_neuron(env: &TestEnv, controller: PrincipalId) -> NeuronId { - let neuron_id = with_neuron_for_tests(env, controller); - with_trusted_neurons_following_neuron_for_tests(env, neuron_id, controller); - neuron_id +fn setup_test_neuron(env: &TestEnv) -> (NeuronId, String) { + let neuron_identity = Ed25519KeyPair::generate(&mut rand::thread_rng()); + let neuron_principal = Sender::SigKeys(SigKeys::Ed25519(neuron_identity)).get_principal_id(); + let neuron_id = with_neuron_for_tests(env, neuron_principal); + with_trusted_neurons_following_neuron_for_tests(env, neuron_id, neuron_principal); + (neuron_id, neuron_identity.to_pem()) } fn with_neuron_for_tests(env: &TestEnv, controller: PrincipalId) -> NeuronId { @@ -429,12 +429,12 @@ fn with_neuron_for_tests(env: &TestEnv, controller: PrincipalId) -> NeuronId { fn with_trusted_neurons_following_neuron_for_tests( env: &TestEnv, - neuron_id: NeuronId, + NeuronId(neuron_id): NeuronId, controller: PrincipalId, ) { ic_replay(env, |cmd| { cmd.arg("with-trusted-neurons-following-neuron-for-tests") - .arg(neuron_id.0.to_string()) + .arg(neuron_id.to_string()) .arg(controller.to_string()); }); } @@ -713,7 +713,7 @@ fn setup_ic(env: TestEnv) { /// This script can be sourced such that we can easily use the legacy /// nns-tools shell scripts in /testnet/tools/nns-tools/ with the dynamic /// testnet deployed by this system-test. -fn write_sh_lib(env: &TestEnv, neuron_id: NeuronId, http_gateway: &Url) { +fn write_sh_lib(env: &TestEnv, NeuronId(neuron_id): NeuronId, http_gateway: &Url) { let logger: slog::Logger = env.logger(); let set_testnet_env_vars_sh_path = env.get_path(PATH_SET_TESTNET_ENV_VARS_SH); let set_testnet_env_vars_sh_str = set_testnet_env_vars_sh_path.display(); @@ -729,14 +729,13 @@ fn write_sh_lib(env: &TestEnv, neuron_id: NeuronId, http_gateway: &Url) { .as_bytes(), ) .unwrap(); - let neuron_id_number = neuron_id.0; fs::write( &set_testnet_env_vars_sh_path, format!( "export IC_ADMIN={ic_admin:?};\n\ export PEM={pem:?};\n\ export NNS_URL=\"{http_gateway}\";\n\ - export NEURON_ID={neuron_id_number:?};\n\ + export NEURON_ID={neuron_id:?};\n\ " ), ) From 473bc27e1c219ec5aa976e979baeda5ccbdf7a4f Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Wed, 8 Apr 2026 13:12:27 +0000 Subject: [PATCH 3/5] docs --- rs/tests/testnets/mainnet_nns/src/proposals.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/tests/testnets/mainnet_nns/src/proposals.rs b/rs/tests/testnets/mainnet_nns/src/proposals.rs index fefbdcfade20..76b17cf98d50 100644 --- a/rs/tests/testnets/mainnet_nns/src/proposals.rs +++ b/rs/tests/testnets/mainnet_nns/src/proposals.rs @@ -63,8 +63,8 @@ pub struct ProposalWithMainnetState { } impl ProposalWithMainnetState { - /// Initializes a ProposalWithMainnetState instance reading the neuron identty from the static - /// variable, which must have been initialized before via + /// Initializes a [`ProposalWithMainnetState`] instance reading the neuron identity from the + /// static variable, which must have been initialized before via /// `read_dictator_neuron_identity_from_env`. /// /// This function is not intended to be called externally (thus it is not `pub`), but only called From e30058018f94a99a178621e947d029ec71f38100 Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Wed, 8 Apr 2026 13:28:31 +0000 Subject: [PATCH 4/5] unwrap -> expect --- rs/tests/testnets/mainnet_nns/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/tests/testnets/mainnet_nns/src/lib.rs b/rs/tests/testnets/mainnet_nns/src/lib.rs index 9ad9d713fbd0..cc0ce8e00fa1 100644 --- a/rs/tests/testnets/mainnet_nns/src/lib.rs +++ b/rs/tests/testnets/mainnet_nns/src/lib.rs @@ -736,7 +736,7 @@ fn write_sh_lib(env: &TestEnv, NeuronId(neuron_id): NeuronId, http_gateway: &Url .write_all( RECOVERED_NNS_DICTATOR_NEURON_IDENTITY .get() - .unwrap() + .expect("'write_dictator_neuron_identity_to_env' should have been called before 'write_sh_lib'") .1 .as_bytes(), ) From 4063b81766eae311c3fbb3025721acb6f4b693f2 Mon Sep 17 00:00:00 2001 From: Pierugo Pace Date: Wed, 8 Apr 2026 13:29:03 +0000 Subject: [PATCH 5/5] docs: slash --- rs/tests/testnets/mainnet_nns/src/proposals.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/tests/testnets/mainnet_nns/src/proposals.rs b/rs/tests/testnets/mainnet_nns/src/proposals.rs index 76b17cf98d50..64e4661b3c40 100644 --- a/rs/tests/testnets/mainnet_nns/src/proposals.rs +++ b/rs/tests/testnets/mainnet_nns/src/proposals.rs @@ -41,7 +41,7 @@ use url::Url; * proposals. */ -// Test neuron ID and secret key of its controller encoded in PEM format. +/// Test neuron ID and secret key of its controller encoded in PEM format. pub(crate) static RECOVERED_NNS_DICTATOR_NEURON_IDENTITY: OnceCell<(NeuronId, String)> = OnceCell::new();