From 9d6dd3c590d739d0ddddd1dfc0a3836349434e16 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sat, 1 Feb 2025 15:22:37 +0100 Subject: [PATCH 01/76] added code for fetching pending proposals and for submiting new proposals --- Cargo.lock | 2 + rs/nns/handlers/root/impl/BUILD.bazel | 33 ++++ rs/nns/handlers/root/impl/Cargo.toml | 7 + .../handlers/root/impl/backup/backup-root.did | 0 rs/nns/handlers/root/impl/backup/canister.rs | 96 +++++++++++ rs/nns/handlers/root/impl/backup/tests/mod.rs | 63 +++++++ .../root/impl/src/backup_root_proposals.rs | 161 ++++++++++++++++++ rs/nns/handlers/root/impl/src/lib.rs | 1 + .../handlers/root/impl/src/root_proposals.rs | 4 +- 9 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 rs/nns/handlers/root/impl/backup/backup-root.did create mode 100644 rs/nns/handlers/root/impl/backup/canister.rs create mode 100644 rs/nns/handlers/root/impl/backup/tests/mod.rs create mode 100644 rs/nns/handlers/root/impl/src/backup_root_proposals.rs diff --git a/Cargo.lock b/Cargo.lock index 2d8516e738e..501543a5317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10837,10 +10837,12 @@ dependencies = [ "ic-state-machine-tests", "ic-test-utilities", "ic-test-utilities-compare-dirs", + "ic-test-utilities-load-wasm", "ic-types", "lazy_static", "maplit", "on_wire", + "pocket-ic", "pretty_assertions", "prost 0.13.4", "registry-canister", diff --git a/rs/nns/handlers/root/impl/BUILD.bazel b/rs/nns/handlers/root/impl/BUILD.bazel index 4ac7d056804..777cba411d4 100644 --- a/rs/nns/handlers/root/impl/BUILD.bazel +++ b/rs/nns/handlers/root/impl/BUILD.bazel @@ -149,6 +149,39 @@ rust_canister( ], ) +rust_canister( + name = "backup-root-canister", + srcs = ["backup/canister.rs"], + aliases = ALIASES, + proc_macro_deps = MACRO_DEPENDENCIES, + service_file = ":backup/backup-root.did", + deps = DEPENDENCIES + [ + ":build_script", + ":root", + ], +) + +rust_test( + name = "backup-root-canister-tests", + srcs = glob(["backup/**/*.rs"]), + data = [ + ":backup-root-canister", + "//rs/pocket_ic_server:pocket-ic-server", + ], + crate_root = "backup/canister.rs", + proc_macro_deps = MACRO_DEPENDENCIES, + deps = DEPENDENCIES + DEV_DEPENDENCIES + [ + ":build_script", + ":root", + "//packages/pocket-ic", + "//rs/test_utilities/load_wasm", + ], + env = { + "BACKUP_ROOT_WASM_PATH": "$(rootpath :backup-root-canister)", + "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" + } +) + rust_canister( name = "upgrade-test-canister", srcs = ["test_canisters/upgrade_test_canister.rs"], diff --git a/rs/nns/handlers/root/impl/Cargo.toml b/rs/nns/handlers/root/impl/Cargo.toml index 3e591c022d4..fb56421121c 100644 --- a/rs/nns/handlers/root/impl/Cargo.toml +++ b/rs/nns/handlers/root/impl/Cargo.toml @@ -14,6 +14,10 @@ path = "canister/canister.rs" name = "upgrade-test-canister" path = "test_canisters/upgrade_test_canister.rs" +[[bin]] +name = "backup-root-canister" +path = "backup/canister.rs" + [lib] path = "src/lib.rs" @@ -70,3 +74,6 @@ candid_parser = { workspace = true } ic-state-machine-tests = { path = "../../../../state_machine_tests" } pretty_assertions = { workspace = true } tokio = { workspace = true } +pocket-ic.path = "../../../../../packages/pocket-ic" +ic-test-utilities-load-wasm.path = "../../../../test_utilities/load_wasm" + diff --git a/rs/nns/handlers/root/impl/backup/backup-root.did b/rs/nns/handlers/root/impl/backup/backup-root.did new file mode 100644 index 00000000000..e69de29bb2d diff --git a/rs/nns/handlers/root/impl/backup/canister.rs b/rs/nns/handlers/root/impl/backup/canister.rs new file mode 100644 index 00000000000..9c9671d1cf3 --- /dev/null +++ b/rs/nns/handlers/root/impl/backup/canister.rs @@ -0,0 +1,96 @@ +use ic_base_types::{PrincipalId, SubnetId}; +use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; +use ic_nervous_system_common::serve_metrics; +use ic_nns_handler_root::{ + backup_root_proposals::ChangeSubnetHaltStatus, encode_metrics, + root_proposals::RootProposalBallot, +}; +use std::cell::RefCell; + +#[cfg(target_arch = "wasm32")] +use ic_cdk::println; + +use ic_cdk::{post_upgrade, query, update}; + +fn caller() -> PrincipalId { + PrincipalId::from(ic_cdk::caller()) +} + +thread_local! { + // How this value was chosen: queues become full at 500. This is 1/3 of that, which seems to be + // a reasonable balance. + static AVAILABLE_MANAGEMENT_CANISTER_CALL_SLOT_COUNT: RefCell = const { RefCell::new(167) }; +} + +// canister_init and canister_post_upgrade are needed here +// to ensure that printer hook is set up, otherwise error +// messages are quite obscure. +#[export_name = "canister_init"] +fn canister_init() { + println!("canister_init"); +} + +#[post_upgrade] +fn canister_post_upgrade() { + println!("canister_post_upgrade"); +} + +ic_nervous_system_common_build_metadata::define_get_build_metadata_candid_method_cdk! {} + +#[update(hidden = true)] +async fn submit_root_proposal_to_change_subnet_halt_status( + subnet_id: SubnetId, + halt: bool, +) -> Result<(), String> { + ic_nns_handler_root::backup_root_proposals::submit_root_proposal_to_change_subnet_halt_status( + caller(), + subnet_id, + halt, + ) + .await +} + +#[update(hidden = true)] +async fn vote_on_root_proposal_to_change_subnet_halt_status( + _proposer: PrincipalId, + _ballot: RootProposalBallot, +) -> Result<(), String> { + Ok(()) +} + +#[update(hidden = true)] +fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { + ic_nns_handler_root::backup_root_proposals::get_pending_root_proposals_to_change_subnet_halt_status() +} + +/// Resources to serve for a given http_request +/// Serve an HttpRequest made to this canister +#[query(hidden = true, decoding_quota = 10000)] +pub fn http_request(request: HttpRequest) -> HttpResponse { + match request.path() { + "/metrics" => serve_metrics(encode_metrics), + _ => HttpResponseBuilder::not_found().build(), + } +} + +// When run on native this prints the candid service definition of this +// canister, from the methods annotated with `candid_method` above. +// +// Note that `cargo test` calls `main`, and `export_service` (which defines +// `__export_service` in the current scope) needs to be called exactly once. So +// in addition to `not(target_arch = "wasm32")` we have a `not(test)` guard here +// to avoid calling `export_service`, which we need to call in the test below. +#[cfg(not(any(target_arch = "wasm32", test)))] +fn main() { + // The line below generates did types and service definition from the + // methods annotated with `candid_method` above. The definition is then + // obtained with `__export_service()`. + candid::export_service!(); + std::print!("{}", __export_service()); +} + +#[cfg(any(target_arch = "wasm32", test))] +fn main() {} + +#[cfg(test)] +mod tests; diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs new file mode 100644 index 00000000000..f2cb7c50150 --- /dev/null +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +use candid::Principal; +use ic_nns_handler_root::backup_root_proposals::ChangeSubnetHaltStatus; +use pocket_ic::{PocketIc, PocketIcBuilder}; + +fn fetch_backup_canister_wasm() -> Vec { + let path: PathBuf = std::env::var("BACKUP_ROOT_WASM_PATH") + .expect("Path should be set in environment variable BACKUP_ROOT_WASM_PATH") + .try_into() + .unwrap(); + std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) +} + +fn init_pocket_ic() -> (PocketIc, Principal) { + let wasm = fetch_backup_canister_wasm(); + let pic = PocketIcBuilder::new() + .with_application_subnet() + .with_nns_subnet() + .build(); + let app_subnets = pic.topology().get_app_subnets(); + + let subnet_id = app_subnets.first().expect("Should contain one app subnet"); + + let canister = pic.create_canister_on_subnet(None, None, *subnet_id); + pic.add_cycles(canister, 100_000_000_000_000); + pic.install_canister(canister, wasm, candid::encode_one(()).unwrap(), None); + (pic, canister) +} + +#[test] +fn fetch_pending_proposals_empty() { + let (pic, canister) = init_pocket_ic(); + let response = pic + .update_call( + canister, + Principal::anonymous(), + "get_pending_root_proposals_to_change_subnet_halt_status", + candid::encode_one(()).unwrap(), + ) + .expect("Should be able to fetch pending root proposals to upgrade governance canister"); + + let response: Vec = + candid::decode_one(&response).expect("Should be able to decode response"); + + assert!(response.is_empty()) +} + +#[test] +fn fetch_pending_proposals_submited_one() { + let (pic, canister) = init_pocket_ic(); + + let subnet_id = pic.get_subnet(canister).unwrap(); + + let response = pic.update_call( + canister, + Principal::anonymous(), + "submit_root_proposal_to_change_subnet_halt_status", + candid::encode_args((subnet_id, true)).unwrap(), + ); + + assert!(response.is_ok()) +} diff --git a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs new file mode 100644 index 00000000000..d190b0fc881 --- /dev/null +++ b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs @@ -0,0 +1,161 @@ +use std::{cell::RefCell, collections::BTreeMap}; + +use candid::CandidType; +use ic_base_types::NodeId; +use ic_base_types::{PrincipalId, SubnetId}; +use ic_nns_common::registry::get_value; +use ic_protobuf::{ + registry::subnet::{self, v1::SubnetRecord}, + types::v1::EquivocationProof, +}; +use ic_registry_keys::make_subnet_record_key; +use serde::Deserialize; + +use crate::{ + now_seconds, + root_proposals::{get_node_operator_pid_of_node, RootProposalBallot}, +}; + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct ChangeSubnetHaltStatus { + /// The id of the NNS subnet. + pub subnet_id: SubnetId, + /// The principal id of the proposer (must be one of the node + /// operators of the NNS subnet according to the registry at + /// time of submission). + pub proposer: PrincipalId, + /// The ballots cast by node operators. + pub node_operator_ballots: Vec<(PrincipalId, RootProposalBallot)>, + /// The timestamp, in seconds, at which the proposal was submitted. + pub submission_timestamp_seconds: u64, + /// Should the new status be halted (true) or unhalted (false) + pub halt: bool, +} + +impl ChangeSubnetHaltStatus { + fn is_byzantine_majority(&self, ballot: RootProposalBallot) -> bool { + let num_nodes = self.node_operator_ballots.len(); + let max_faults = (num_nodes - 1) / 3; + let votes_for_ballot: usize = self + .node_operator_ballots + .iter() + .map(|(_, b)| match ballot.eq(b) { + true => 1, + false => 0, + }) + .sum(); + votes_for_ballot >= (num_nodes - max_faults) + } + + /// For a root proposal to have a byzantine majority of yes, it + /// needs to collect N - f ""yes"" votes, where N is the total number + /// of nodes (same as the number of ballots) and f = (N - 1) / 3. + fn is_byzantine_majority_yes(&self) -> bool { + self.is_byzantine_majority(RootProposalBallot::Yes) + } + + /// For a root proposal to have a byzantine majority of no, it + /// needs to collect f + 1 "no" votes, where N s the total number + /// of nodes (same as the number of ballots) and f = (N - 1) / 3. + fn is_byzantine_majority_no(&self) -> bool { + self.is_byzantine_majority(RootProposalBallot::No) + } +} + +thread_local! { + static PROPOSALS: RefCell> = const { RefCell::new(BTreeMap::new()) }; +} + +pub fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { + // Return the pending proposals + PROPOSALS.with(|proposals| proposals.borrow().values().cloned().collect()) +} + +async fn get_subnet_record(subnet_id: SubnetId) -> Result<(SubnetRecord, u64), String> { + get_value(make_subnet_record_key(subnet_id).as_bytes(), None) + .await + .map_err(|e| e.to_string()) +} + +async fn get_node_operator_ballots_for_subnet( + subnet_record: SubnetRecord, + record_version: u64, + caller: PrincipalId, +) -> Result, String> { + let node_ids: Vec = subnet_record + .membership + .iter() + .map(|node_raw| { + NodeId::from(PrincipalId::try_from(node_raw).expect("Should be able to decode node id")) + }) + .collect(); + + let mut node_operator_ballots = Vec::new(); + + for node_id in node_ids { + let node_operator_id = get_node_operator_pid_of_node(&node_id, record_version).await?; + + let ballot = match node_operator_id == caller { + true => RootProposalBallot::Yes, + false => RootProposalBallot::Undecided, + }; + + node_operator_ballots.push((node_operator_id, ballot)) + } + + Ok(node_operator_ballots) +} + +pub async fn submit_root_proposal_to_change_subnet_halt_status( + caller: PrincipalId, + subnet_id: SubnetId, + halt: bool, +) -> Result<(), String> { + let now = now_seconds(); + + let (subnet_record, version) = get_subnet_record(subnet_id.clone()).await?; + + if subnet_record.is_halted == halt { + return Err(format!( + "Subnet halt status is already: {}", + subnet_record.is_halted + )); + } + + let node_operator_ballots = + get_node_operator_ballots_for_subnet(subnet_record.clone(), version, caller).await?; + + // The proposer is not among node operators of the subnet + if !node_operator_ballots + .iter() + .any(|(_, ballot)| ballot.eq(&RootProposalBallot::Yes)) + { + let message = format!( + "[Backup root canister] Invalid proposal. Caller: {} must be among the node operators of the nns subnet.",caller + ); + println!("{}", message); + return Err(message); + } + + PROPOSALS.with(|proposals| { + if let Some(proposal) = proposals.borrow().get(&caller) { + println!( + "Current root proposal {:?} from {} is going to be overwritten.", + proposal, caller, + ); + } + + proposals.borrow_mut().insert( + caller, + ChangeSubnetHaltStatus { + subnet_id, + proposer: caller, + node_operator_ballots, + submission_timestamp_seconds: now, + halt, + }, + ) + }); + + Ok(()) +} diff --git a/rs/nns/handlers/root/impl/src/lib.rs b/rs/nns/handlers/root/impl/src/lib.rs index f8bf2398c64..dabd97b81ca 100644 --- a/rs/nns/handlers/root/impl/src/lib.rs +++ b/rs/nns/handlers/root/impl/src/lib.rs @@ -14,6 +14,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +pub mod backup_root_proposals; pub mod canister_management; pub mod init; pub mod pb; diff --git a/rs/nns/handlers/root/impl/src/root_proposals.rs b/rs/nns/handlers/root/impl/src/root_proposals.rs index 7d7f30fa148..b9a17a8938a 100644 --- a/rs/nns/handlers/root/impl/src/root_proposals.rs +++ b/rs/nns/handlers/root/impl/src/root_proposals.rs @@ -33,7 +33,7 @@ const MAX_TIME_FOR_GOVERNANCE_UPGRADE_ROOT_PROPOSAL: u64 = 60 * 60 * 24 * 7; /// Root proposals are initialized with one ballot per node at creation /// in the "Undecided" state. These ballots are then changed when the node /// operators vote. -#[derive(Clone, Debug, CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq)] pub enum RootProposalBallot { Yes, No, @@ -529,7 +529,7 @@ async fn get_nns_membership(subnet_id: &SubnetId) -> Result<(Vec, u64), } /// Returns the principal corresponding to the node operator of the given node. -async fn get_node_operator_pid_of_node( +pub async fn get_node_operator_pid_of_node( node_id: &NodeId, version: u64, ) -> Result { From e4d49522cd724a05c1308a9ad88c9f41e20d7f6f Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sat, 1 Feb 2025 23:40:20 +0100 Subject: [PATCH 02/76] adding submiting proposals --- rs/nns/handlers/root/impl/backup/tests/mod.rs | 94 +++++++++++++++++-- .../root/impl/src/backup_root_proposals.rs | 7 +- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index f2cb7c50150..b4aa1cabb3b 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -1,34 +1,96 @@ -use std::path::PathBuf; +use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; +use ic_base_types::{NodeId, PrincipalId}; +use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_root::backup_root_proposals::ChangeSubnetHaltStatus; +use ic_protobuf::registry::subnet::v1::SubnetListRecord; +use ic_registry_transport::pb::v1::RegistryAtomicMutateRequest; +use ic_types::crypto::canister_threshold_sig::PublicKey; use pocket_ic::{PocketIc, PocketIcBuilder}; +use registry_canister::{ + init::RegistryCanisterInitPayload, + test_helpers::{ + add_fake_subnet, get_invariant_compliant_subnet_record, + prepare_registry_with_nodes_and_node_operator_id, + }, +}; -fn fetch_backup_canister_wasm() -> Vec { - let path: PathBuf = std::env::var("BACKUP_ROOT_WASM_PATH") - .expect("Path should be set in environment variable BACKUP_ROOT_WASM_PATH") +fn fetch_canister_wasm(env: &str) -> Vec { + let path: PathBuf = std::env::var(env) + .expect(&format!("Path should be set in environment variable {env}")) .try_into() .unwrap(); std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) } +fn prepare_registry(nns_id: PrincipalId, app_id: PrincipalId) -> Vec { + let mut total_mutations = vec![]; + let mut operators_with_nodes = BTreeMap::new(); + + for no in 0..11 { + let no_principal = PrincipalId::new_user_test_id(total_mutations.len() as u64); + let (mutation, no_nodes) = + prepare_registry_with_nodes_and_node_operator_id(no, 4, no_principal.clone()); + + operators_with_nodes.insert(&no_principal, no_nodes); + total_mutations.push(mutation); + } + + // First 40 nodes goes to nns => 10 node operators * 4 nodes each + let mut subnet_list_record = SubnetListRecord::default(); + let nns_nodes: BTreeMap = + operators_with_nodes + .values() + .take(10) + .fold(BTreeMap::new(), |mut acc, next| { + acc.extend(next); + acc + }); + + add_fake_subnet( + nns_id, + &mut subnet_list_record, + get_invariant_compliant_subnet_record(nns_nodes.keys().cloned().collect()), + &nns_nodes, + ); + + total_mutations +} + fn init_pocket_ic() -> (PocketIc, Principal) { - let wasm = fetch_backup_canister_wasm(); let pic = PocketIcBuilder::new() .with_application_subnet() .with_nns_subnet() .build(); + let registry = pic + .create_canister_with_id(None, None, REGISTRY_CANISTER_ID.into()) + .unwrap(); + pic.add_cycles(registry, 100_000_000_000_000); + + pic.install_canister( + registry, + fetch_canister_wasm("REGISTRY_WASM_PATH"), + candid::encode_one(RegistryCanisterInitPayload { mutations: vec![] }).unwrap(), + None, + ); + let app_subnets = pic.topology().get_app_subnets(); let subnet_id = app_subnets.first().expect("Should contain one app subnet"); let canister = pic.create_canister_on_subnet(None, None, *subnet_id); pic.add_cycles(canister, 100_000_000_000_000); - pic.install_canister(canister, wasm, candid::encode_one(()).unwrap(), None); + pic.install_canister( + canister, + fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), + candid::encode_one(()).unwrap(), + None, + ); (pic, canister) } -#[test] +// #[test] fn fetch_pending_proposals_empty() { let (pic, canister) = init_pocket_ic(); let response = pic @@ -58,6 +120,22 @@ fn fetch_pending_proposals_submited_one() { "submit_root_proposal_to_change_subnet_halt_status", candid::encode_args((subnet_id, true)).unwrap(), ); + let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); + println!("{:?}", response); + + assert!(response.is_ok()); + + let response = pic + .update_call( + canister, + Principal::anonymous(), + "get_pending_root_proposals_to_change_subnet_halt_status", + candid::encode_one(()).unwrap(), + ) + .expect("Should be able to fetch remaining proposals"); + + let response: Vec = + candid::decode_one(&response).expect("Should be able to decode response"); - assert!(response.is_ok()) + assert!(response.len() == 1) } diff --git a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs index d190b0fc881..614cd963009 100644 --- a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs +++ b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs @@ -4,10 +4,7 @@ use candid::CandidType; use ic_base_types::NodeId; use ic_base_types::{PrincipalId, SubnetId}; use ic_nns_common::registry::get_value; -use ic_protobuf::{ - registry::subnet::{self, v1::SubnetRecord}, - types::v1::EquivocationProof, -}; +use ic_protobuf::registry::subnet::v1::SubnetRecord; use ic_registry_keys::make_subnet_record_key; use serde::Deserialize; @@ -131,7 +128,7 @@ pub async fn submit_root_proposal_to_change_subnet_halt_status( .any(|(_, ballot)| ballot.eq(&RootProposalBallot::Yes)) { let message = format!( - "[Backup root canister] Invalid proposal. Caller: {} must be among the node operators of the nns subnet.",caller + "[Backup root canister] Invalid proposal. Caller: {} must be among the node operators of the subnet.",caller ); println!("{}", message); return Err(message); From 78e0bbc7c776eafb232831a575053fdd4c9f050d Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 2 Feb 2025 00:21:20 +0100 Subject: [PATCH 03/76] commenting problematic code --- rs/nns/handlers/root/impl/BUILD.bazel | 7 +++-- rs/nns/handlers/root/impl/backup/tests/mod.rs | 29 ++++++++++--------- rs/registry/canister/BUILD.bazel | 11 +++++++ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/rs/nns/handlers/root/impl/BUILD.bazel b/rs/nns/handlers/root/impl/BUILD.bazel index 777cba411d4..2c303192265 100644 --- a/rs/nns/handlers/root/impl/BUILD.bazel +++ b/rs/nns/handlers/root/impl/BUILD.bazel @@ -79,7 +79,7 @@ DEV_DEPENDENCIES = [ "//rs/types/types", "//rs/test_utilities", "//rs/test_utilities/compare_dirs", - "//rs/registry/canister", + "//rs/registry/canister:canister--test_feature", "@crate_index//:tempfile", "@crate_index//:assert_matches", "@crate_index//:hex", @@ -167,17 +167,18 @@ rust_test( data = [ ":backup-root-canister", "//rs/pocket_ic_server:pocket-ic-server", + "//rs/registry/canister" ], crate_root = "backup/canister.rs", proc_macro_deps = MACRO_DEPENDENCIES, deps = DEPENDENCIES + DEV_DEPENDENCIES + [ ":build_script", - ":root", + ":root--test_feature", "//packages/pocket-ic", - "//rs/test_utilities/load_wasm", ], env = { "BACKUP_ROOT_WASM_PATH": "$(rootpath :backup-root-canister)", + "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister)", "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" } ) diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index b4aa1cabb3b..dd4b5866602 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -9,11 +9,11 @@ use ic_registry_transport::pb::v1::RegistryAtomicMutateRequest; use ic_types::crypto::canister_threshold_sig::PublicKey; use pocket_ic::{PocketIc, PocketIcBuilder}; use registry_canister::{ + // common::test_helpers::{ + // add_fake_subnet, get_invariant_compliant_subnet_record, + // prepare_registry_with_nodes_and_node_operator_id, + // }, init::RegistryCanisterInitPayload, - test_helpers::{ - add_fake_subnet, get_invariant_compliant_subnet_record, - prepare_registry_with_nodes_and_node_operator_id, - }, }; fn fetch_canister_wasm(env: &str) -> Vec { @@ -30,10 +30,11 @@ fn prepare_registry(nns_id: PrincipalId, app_id: PrincipalId) -> Vec) = + (RegistryAtomicMutateRequest::default(), BTreeMap::new()); + // prepare_registry_with_nodes_and_node_operator_id(no, 4, no_principal.clone()); - operators_with_nodes.insert(&no_principal, no_nodes); + operators_with_nodes.insert(no_principal, no_nodes); total_mutations.push(mutation); } @@ -44,16 +45,16 @@ fn prepare_registry(nns_id: PrincipalId, app_id: PrincipalId) -> Vec Date: Sun, 2 Feb 2025 01:55:37 +0100 Subject: [PATCH 04/76] initialize 40 node nns --- Cargo.lock | 1 + rs/nns/handlers/root/impl/BUILD.bazel | 10 +- rs/nns/handlers/root/impl/Cargo.toml | 1 + rs/nns/handlers/root/impl/backup/tests/mod.rs | 124 ++++++++++++++---- .../root/impl/backup/tests/test_helpers.rs | 116 ++++++++++++++++ 5 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 rs/nns/handlers/root/impl/backup/tests/test_helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 501543a5317..81b2c9cc6b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10833,6 +10833,7 @@ dependencies = [ "ic-protobuf", "ic-registry-keys", "ic-registry-routing-table", + "ic-registry-subnet-type", "ic-registry-transport", "ic-state-machine-tests", "ic-test-utilities", diff --git a/rs/nns/handlers/root/impl/BUILD.bazel b/rs/nns/handlers/root/impl/BUILD.bazel index 2c303192265..db45ea6b3fd 100644 --- a/rs/nns/handlers/root/impl/BUILD.bazel +++ b/rs/nns/handlers/root/impl/BUILD.bazel @@ -167,20 +167,22 @@ rust_test( data = [ ":backup-root-canister", "//rs/pocket_ic_server:pocket-ic-server", - "//rs/registry/canister" + "//rs/registry/canister:registry-canister" ], crate_root = "backup/canister.rs", proc_macro_deps = MACRO_DEPENDENCIES, deps = DEPENDENCIES + DEV_DEPENDENCIES + [ ":build_script", - ":root--test_feature", + ":root", "//packages/pocket-ic", + "//rs/registry/subnet_type" ], env = { "BACKUP_ROOT_WASM_PATH": "$(rootpath :backup-root-canister)", - "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister)", + "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" - } + }, + version = "0.9.0" ) rust_canister( diff --git a/rs/nns/handlers/root/impl/Cargo.toml b/rs/nns/handlers/root/impl/Cargo.toml index fb56421121c..9b868a7aa79 100644 --- a/rs/nns/handlers/root/impl/Cargo.toml +++ b/rs/nns/handlers/root/impl/Cargo.toml @@ -76,4 +76,5 @@ pretty_assertions = { workspace = true } tokio = { workspace = true } pocket-ic.path = "../../../../../packages/pocket-ic" ic-test-utilities-load-wasm.path = "../../../../test_utilities/load_wasm" +ic-registry-subnet-type.path = "../../../../registry/subnet_type" diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index dd4b5866602..5c10416fbc9 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -1,21 +1,31 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; -use ic_base_types::{NodeId, PrincipalId}; +use ic_base_types::{CanisterId, NodeId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_root::backup_root_proposals::ChangeSubnetHaltStatus; -use ic_protobuf::registry::subnet::v1::SubnetListRecord; -use ic_registry_transport::pb::v1::RegistryAtomicMutateRequest; -use ic_types::crypto::canister_threshold_sig::PublicKey; +use ic_protobuf::registry::{ + crypto::v1::PublicKey, + replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, + routing_table::v1::RoutingTable as RoutingTablePB, + subnet::v1::SubnetListRecord, +}; +use ic_registry_keys::{ + make_blessed_replica_versions_key, make_replica_version_key, make_routing_table_record_key, +}; +use ic_registry_routing_table::{CanisterIdRange, RoutingTable}; +use ic_registry_transport::{insert, pb::v1::RegistryAtomicMutateRequest}; +use maplit::btreemap; use pocket_ic::{PocketIc, PocketIcBuilder}; -use registry_canister::{ - // common::test_helpers::{ - // add_fake_subnet, get_invariant_compliant_subnet_record, - // prepare_registry_with_nodes_and_node_operator_id, - // }, - init::RegistryCanisterInitPayload, +use prost::Message; +use registry_canister::init::RegistryCanisterInitPayload; +use test_helpers::{ + add_fake_subnet, get_invariant_compliant_subnet_record, + prepare_registry_with_nodes_and_node_operator_id, }; +mod test_helpers; + fn fetch_canister_wasm(env: &str) -> Vec { let path: PathBuf = std::env::var(env) .expect(&format!("Path should be set in environment variable {env}")) @@ -24,18 +34,40 @@ fn fetch_canister_wasm(env: &str) -> Vec { std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) } -fn prepare_registry(nns_id: PrincipalId, app_id: PrincipalId) -> Vec { +fn prepare_registry(nns_id: Principal, app_id: Principal) -> Vec { + let nns_id: SubnetId = SubnetId::from(PrincipalId(nns_id)); + let app_id: SubnetId = SubnetId::from(PrincipalId(app_id)); let mut total_mutations = vec![]; let mut operators_with_nodes = BTreeMap::new(); + const MOCK_HASH: &str = "d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a1"; + let release_package_url = "http://release_package.tar.zst".to_string(); + let replica_version = insert( + make_replica_version_key(env!("CARGO_PKG_VERSION")).as_bytes(), + ReplicaVersionRecord { + release_package_sha256_hex: MOCK_HASH.into(), + release_package_urls: vec![release_package_url], + guest_launch_measurement_sha256_hex: None, + } + .encode_to_vec(), + ); + total_mutations.push(replica_version); + let blessed_replica_versions = insert( + make_blessed_replica_versions_key().as_bytes(), + BlessedReplicaVersions { + blessed_version_ids: vec![env!("CARGO_PKG_VERSION").to_string()], + } + .encode_to_vec(), + ); + total_mutations.push(blessed_replica_versions); + for no in 0..11 { let no_principal = PrincipalId::new_user_test_id(total_mutations.len() as u64); - let (mutation, no_nodes): (RegistryAtomicMutateRequest, BTreeMap) = - (RegistryAtomicMutateRequest::default(), BTreeMap::new()); - // prepare_registry_with_nodes_and_node_operator_id(no, 4, no_principal.clone()); + let (mutation, no_nodes) = + prepare_registry_with_nodes_and_node_operator_id(no * 4, 4, no_principal.clone()); operators_with_nodes.insert(no_principal, no_nodes); - total_mutations.push(mutation); + total_mutations.extend(mutation.mutations); } // First 40 nodes goes to nns => 10 node operators * 4 nodes each @@ -49,14 +81,53 @@ fn prepare_registry(nns_id: PrincipalId, app_id: PrincipalId) -> Vec = operators_with_nodes + .values() + .skip(10) + .take(1) + .fold(BTreeMap::new(), |mut acc, next| { + acc.extend(next.clone()); + acc + }); + + let mutations = add_fake_subnet( + app_id, + &mut subnet_list_record, + get_invariant_compliant_subnet_record( + app_nodes.keys().cloned().collect(), + ic_registry_subnet_type::SubnetType::Application, + ), + &app_nodes, + ); + total_mutations.extend(mutations); + + let routing_table = RoutingTable::try_from(btreemap! { + CanisterIdRange { + start: CanisterId::from(0), + end: CanisterId::from(u64::MAX), + } => nns_id, + }) + .unwrap(); + total_mutations.push(insert( + make_routing_table_record_key().as_bytes(), + RoutingTablePB::from(routing_table).encode_to_vec(), + )); + + vec![RegistryAtomicMutateRequest { + mutations: total_mutations, + ..Default::default() + }] } fn init_pocket_ic() -> (PocketIc, Principal) { @@ -69,10 +140,15 @@ fn init_pocket_ic() -> (PocketIc, Principal) { .unwrap(); pic.add_cycles(registry, 100_000_000_000_000); + let nns_id = pic.topology().get_nns().unwrap(); + let app_subnets = pic.topology().get_app_subnets(); pic.install_canister( registry, fetch_canister_wasm("REGISTRY_WASM_PATH"), - candid::encode_one(RegistryCanisterInitPayload { mutations: vec![] }).unwrap(), + candid::encode_one(RegistryCanisterInitPayload { + mutations: prepare_registry(nns_id, app_subnets.first().unwrap().clone()), + }) + .unwrap(), None, ); diff --git a/rs/nns/handlers/root/impl/backup/tests/test_helpers.rs b/rs/nns/handlers/root/impl/backup/tests/test_helpers.rs new file mode 100644 index 00000000000..c78cc80d96b --- /dev/null +++ b/rs/nns/handlers/root/impl/backup/tests/test_helpers.rs @@ -0,0 +1,116 @@ +use std::collections::BTreeMap; + +use ic_base_types::{NodeId, PrincipalId, SubnetId}; +use ic_nns_test_utils::registry::{ + create_subnet_threshold_signing_pubkey_and_cup_mutations, new_node_keys_and_node_id, +}; +use ic_protobuf::registry::crypto::v1::PublicKey; +use ic_protobuf::registry::node::v1::{IPv4InterfaceConfig, NodeRecord}; +use ic_protobuf::registry::subnet::v1::{SubnetListRecord, SubnetRecord}; +use ic_registry_keys::{make_subnet_list_record_key, make_subnet_record_key}; +use ic_registry_transport::pb::v1::{RegistryAtomicMutateRequest, RegistryMutation}; +use ic_registry_transport::upsert; +use ic_types::ReplicaVersion; +use prost::Message; +use registry_canister::mutations::do_create_subnet::CreateSubnetPayload; +use registry_canister::mutations::node_management::common::make_add_node_registry_mutations; +use registry_canister::mutations::node_management::do_add_node::connection_endpoint_from_string; + +//TODO: find a way to use rs/registry/canister/src/common through bazel + +pub fn add_fake_subnet( + subnet_id: SubnetId, + subnet_list_record: &mut SubnetListRecord, + subnet_record: SubnetRecord, + node_ids_and_dkg_pks: &BTreeMap, +) -> Vec { + let new_subnet = upsert( + make_subnet_record_key(subnet_id).into_bytes(), + subnet_record.encode_to_vec(), + ); + + subnet_list_record.subnets.push(subnet_id.get().into_vec()); + let subnet_list_mutation = upsert( + make_subnet_list_record_key().into_bytes(), + subnet_list_record.encode_to_vec(), + ); + + let mut subnet_threshold_pk_and_cup_mutations = + create_subnet_threshold_signing_pubkey_and_cup_mutations(subnet_id, node_ids_and_dkg_pks); + + // remaining mutations are added by do_create_subnet but don't + // trip invariants in current test setups when left out + subnet_threshold_pk_and_cup_mutations.append(&mut vec![ + subnet_list_mutation, + new_subnet, + // new_subnet_dkg, + // new_subnet_threshold_signing_pubkey, + // routing_table_mutation, + ]); + subnet_threshold_pk_and_cup_mutations +} + +pub fn get_invariant_compliant_subnet_record( + node_ids: Vec, + subnet_type: ic_registry_subnet_type::SubnetType, +) -> SubnetRecord { + CreateSubnetPayload { + unit_delay_millis: 10, + gossip_retransmission_request_ms: 10_000, + gossip_registry_poll_period_ms: 2000, + gossip_pfn_evaluation_period_ms: 50, + gossip_receive_check_cache_size: 1, + gossip_max_duplicity: 1, + gossip_max_chunk_wait_ms: 200, + gossip_max_artifact_streams_per_peer: 1, + replica_version_id: ReplicaVersion::default().into(), + node_ids, + subnet_type, + ..Default::default() + } + .into() +} + +pub fn prepare_registry_with_nodes_and_node_operator_id( + start_mutation_id: u8, + nodes: u64, + node_operator_id: PrincipalId, +) -> (RegistryAtomicMutateRequest, BTreeMap) { + // Prepare a transaction to add the nodes to the registry + let mut mutations = Vec::::default(); + let node_ids_and_dkg_pks: BTreeMap = (0..nodes) + .map(|id| { + let (valid_pks, node_id) = new_node_keys_and_node_id(); + let dkg_dealing_encryption_pk = valid_pks.dkg_dealing_encryption_key().clone(); + let effective_id: u8 = start_mutation_id + (id as u8); + let node_record = NodeRecord { + xnet: Some(connection_endpoint_from_string(&format!( + "128.0.{effective_id}.1:1234" + ))), + http: Some(connection_endpoint_from_string(&format!( + "[fe80::{effective_id}]:4321" + ))), + public_ipv4_config: Some(IPv4InterfaceConfig { + ip_addr: format!("128.0.{effective_id}.1"), + ..Default::default() + }), + node_operator_id: node_operator_id.into_vec(), + // Preset this field to Some(), in order to allow seamless creation of ApiBoundaryNodeRecord if needed. + domain: Some(format!("node{effective_id}.example.com")), + ..Default::default() + }; + mutations.append(&mut make_add_node_registry_mutations( + node_id, + node_record, + valid_pks, + )); + (node_id, dkg_dealing_encryption_pk) + }) + .collect(); + + let mutate_request = RegistryAtomicMutateRequest { + mutations, + preconditions: vec![], + }; + (mutate_request, node_ids_and_dkg_pks) +} From 4d694fa41a5b61767ab2fad60b73aa2df04e7bee Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 2 Feb 2025 13:37:15 +0100 Subject: [PATCH 05/76] refactoring test setup --- rs/nns/handlers/root/impl/backup/tests/mod.rs | 216 +++++++++++------- 1 file changed, 128 insertions(+), 88 deletions(-) diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index 5c10416fbc9..f95889d2b5e 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -1,11 +1,10 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; -use ic_base_types::{CanisterId, NodeId, PrincipalId, SubnetId}; +use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_root::backup_root_proposals::ChangeSubnetHaltStatus; use ic_protobuf::registry::{ - crypto::v1::PublicKey, replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, routing_table::v1::RoutingTable as RoutingTablePB, subnet::v1::SubnetListRecord, @@ -14,7 +13,11 @@ use ic_registry_keys::{ make_blessed_replica_versions_key, make_replica_version_key, make_routing_table_record_key, }; use ic_registry_routing_table::{CanisterIdRange, RoutingTable}; -use ic_registry_transport::{insert, pb::v1::RegistryAtomicMutateRequest}; +use ic_registry_subnet_type::SubnetType; +use ic_registry_transport::{ + insert, + pb::v1::{RegistryAtomicMutateRequest, RegistryMutation}, +}; use maplit::btreemap; use pocket_ic::{PocketIc, PocketIcBuilder}; use prost::Message; @@ -34,12 +37,7 @@ fn fetch_canister_wasm(env: &str) -> Vec { std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) } -fn prepare_registry(nns_id: Principal, app_id: Principal) -> Vec { - let nns_id: SubnetId = SubnetId::from(PrincipalId(nns_id)); - let app_id: SubnetId = SubnetId::from(PrincipalId(app_id)); - let mut total_mutations = vec![]; - let mut operators_with_nodes = BTreeMap::new(); - +fn add_replica_version_records(total_mutations: &mut Vec) { const MOCK_HASH: &str = "d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a1"; let release_package_url = "http://release_package.tar.zst".to_string(); let replica_version = insert( @@ -60,69 +58,80 @@ fn prepare_registry(nns_id: Principal, app_id: Principal) -> Vec 10 node operators * 4 nodes each - let mut subnet_list_record = SubnetListRecord::default(); - let nns_nodes: BTreeMap = - operators_with_nodes - .values() - .take(10) - .fold(BTreeMap::new(), |mut acc, next| { - acc.extend(next.clone()); - acc - }); - - let mutations = add_fake_subnet( - nns_id, - &mut subnet_list_record, - get_invariant_compliant_subnet_record( - nns_nodes.keys().cloned().collect(), - ic_registry_subnet_type::SubnetType::System, - ), - &nns_nodes, - ); - total_mutations.extend(mutations); - - let app_nodes: BTreeMap = operators_with_nodes - .values() - .skip(10) - .take(1) - .fold(BTreeMap::new(), |mut acc, next| { - acc.extend(next.clone()); - acc - }); - - let mutations = add_fake_subnet( - app_id, - &mut subnet_list_record, - get_invariant_compliant_subnet_record( - app_nodes.keys().cloned().collect(), - ic_registry_subnet_type::SubnetType::Application, - ), - &app_nodes, - ); - total_mutations.extend(mutations); - +fn add_routing_table_record(total_mutations: &mut Vec, nns_id: PrincipalId) { let routing_table = RoutingTable::try_from(btreemap! { CanisterIdRange { start: CanisterId::from(0), end: CanisterId::from(u64::MAX), - } => nns_id, + } => SubnetId::new(nns_id), }) .unwrap(); total_mutations.push(insert( make_routing_table_record_key().as_bytes(), RoutingTablePB::from(routing_table).encode_to_vec(), )); +} + +struct SubnetNodeOperatorArg { + subnet_id: PrincipalId, + subnet_type: SubnetType, + node_operators: Vec, +} + +struct RegistryPreparationArguments { + subnet_node_operators: Vec, +} + +fn prepare_registry( + registry_preparation_args: &mut RegistryPreparationArguments, +) -> Vec { + // let nns_id: SubnetId = SubnetId::from(PrincipalId(nns_id)); + // let app_id: SubnetId = SubnetId::from(PrincipalId(app_id)); + let mut total_mutations = vec![]; + let mut subnet_list_record = SubnetListRecord::default(); + + add_replica_version_records(&mut total_mutations); + + let mut operator_mutation_ids: u8 = 0; + for arg in ®istry_preparation_args.subnet_node_operators { + let mut current_subnet_nodes = BTreeMap::new(); + for operator in &arg.node_operators { + let (mutation, nodes) = prepare_registry_with_nodes_and_node_operator_id( + operator_mutation_ids * 4, + 4, + operator.clone(), + ); + operator_mutation_ids += 1; + + total_mutations.extend(mutation.mutations); + current_subnet_nodes.extend(nodes); + } + + let mutations = add_fake_subnet( + arg.subnet_id.into(), + &mut subnet_list_record, + get_invariant_compliant_subnet_record( + current_subnet_nodes.keys().cloned().collect(), + arg.subnet_type, + ), + ¤t_subnet_nodes, + ); + total_mutations.extend(mutations); + } + + add_routing_table_record( + &mut total_mutations, + registry_preparation_args + .subnet_node_operators + .iter() + .find_map(|arg| match arg.subnet_type { + SubnetType::System => Some(arg.subnet_id.clone()), + _ => None, + }) + .expect("Missing system subnet"), + ); vec![RegistryAtomicMutateRequest { mutations: total_mutations, @@ -130,23 +139,46 @@ fn prepare_registry(nns_id: Principal, app_id: Principal) -> Vec (PocketIc, Principal) { - let pic = PocketIcBuilder::new() - .with_application_subnet() - .with_nns_subnet() - .build(); +fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Principal) { + let mut builder = PocketIcBuilder::new(); + + for arg in &arguments.subnet_node_operators { + if arg.subnet_type == SubnetType::System { + builder = builder.with_nns_subnet(); + continue; + } + + builder = builder.with_application_subnet(); + } + + let pic = builder.build(); + let nns = pic.topology().get_nns().expect("Should contain nns"); + let arg_nns = arguments + .subnet_node_operators + .iter_mut() + .find(|arg| arg.subnet_type == SubnetType::System) + .unwrap(); + arg_nns.subnet_id = nns.into(); + + for (arg, subnet_id) in arguments + .subnet_node_operators + .iter_mut() + .filter(|arg| arg.subnet_type == SubnetType::Application) + .zip(pic.topology().get_app_subnets()) + { + arg.subnet_id = subnet_id.into() + } + let registry = pic .create_canister_with_id(None, None, REGISTRY_CANISTER_ID.into()) .unwrap(); pic.add_cycles(registry, 100_000_000_000_000); - let nns_id = pic.topology().get_nns().unwrap(); - let app_subnets = pic.topology().get_app_subnets(); pic.install_canister( registry, fetch_canister_wasm("REGISTRY_WASM_PATH"), candid::encode_one(RegistryCanisterInitPayload { - mutations: prepare_registry(nns_id, app_subnets.first().unwrap().clone()), + mutations: prepare_registry(arguments), }) .unwrap(), None, @@ -167,27 +199,35 @@ fn init_pocket_ic() -> (PocketIc, Principal) { (pic, canister) } -// #[test] -fn fetch_pending_proposals_empty() { - let (pic, canister) = init_pocket_ic(); - let response = pic - .update_call( - canister, - Principal::anonymous(), - "get_pending_root_proposals_to_change_subnet_halt_status", - candid::encode_one(()).unwrap(), - ) - .expect("Should be able to fetch pending root proposals to upgrade governance canister"); - - let response: Vec = - candid::decode_one(&response).expect("Should be able to decode response"); - - assert!(response.is_empty()) -} - #[test] fn fetch_pending_proposals_submited_one() { - let (pic, canister) = init_pocket_ic(); + let mut args = RegistryPreparationArguments { + subnet_node_operators: vec![ + SubnetNodeOperatorArg { + subnet_id: PrincipalId::new_subnet_test_id(0), + subnet_type: SubnetType::System, + node_operators: vec![ + // Each has 4 nodes so this is 40 nodes in total + PrincipalId::new_user_test_id(0), + PrincipalId::new_user_test_id(1), + PrincipalId::new_user_test_id(2), + PrincipalId::new_user_test_id(3), + PrincipalId::new_user_test_id(4), + PrincipalId::new_user_test_id(5), + PrincipalId::new_user_test_id(6), + PrincipalId::new_user_test_id(7), + PrincipalId::new_user_test_id(8), + PrincipalId::new_user_test_id(9), + ], + }, + SubnetNodeOperatorArg { + subnet_id: PrincipalId::new_subnet_test_id(0), + subnet_type: SubnetType::Application, + node_operators: vec![PrincipalId::new_user_test_id(999)], + }, + ], + }; + let (pic, canister) = init_pocket_ic(&mut args); let subnet_id = pic.get_subnet(canister).unwrap(); From 40a29525d5e1f9a8076ffd8da7a279df1f0b1b59 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 2 Feb 2025 14:05:47 +0100 Subject: [PATCH 06/76] more refactoring of tests --- rs/nns/handlers/root/impl/backup/tests/mod.rs | 133 +++++++++++++----- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index f95889d2b5e..6b6bb2510c0 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -84,6 +84,37 @@ struct RegistryPreparationArguments { subnet_node_operators: Vec, } +impl Default for RegistryPreparationArguments { + fn default() -> Self { + Self { + subnet_node_operators: vec![ + SubnetNodeOperatorArg { + subnet_id: PrincipalId::new_subnet_test_id(0), + subnet_type: SubnetType::System, + node_operators: vec![ + // Each has 4 nodes so this is 40 nodes in total + PrincipalId::new_user_test_id(0), + PrincipalId::new_user_test_id(1), + PrincipalId::new_user_test_id(2), + PrincipalId::new_user_test_id(3), + PrincipalId::new_user_test_id(4), + PrincipalId::new_user_test_id(5), + PrincipalId::new_user_test_id(6), + PrincipalId::new_user_test_id(7), + PrincipalId::new_user_test_id(8), + PrincipalId::new_user_test_id(9), + ], + }, + SubnetNodeOperatorArg { + subnet_id: PrincipalId::new_subnet_test_id(0), + subnet_type: SubnetType::Application, + node_operators: vec![PrincipalId::new_user_test_id(999)], + }, + ], + } + } +} + fn prepare_registry( registry_preparation_args: &mut RegistryPreparationArguments, ) -> Vec { @@ -199,52 +230,28 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr (pic, canister) } -#[test] -fn fetch_pending_proposals_submited_one() { - let mut args = RegistryPreparationArguments { - subnet_node_operators: vec![ - SubnetNodeOperatorArg { - subnet_id: PrincipalId::new_subnet_test_id(0), - subnet_type: SubnetType::System, - node_operators: vec![ - // Each has 4 nodes so this is 40 nodes in total - PrincipalId::new_user_test_id(0), - PrincipalId::new_user_test_id(1), - PrincipalId::new_user_test_id(2), - PrincipalId::new_user_test_id(3), - PrincipalId::new_user_test_id(4), - PrincipalId::new_user_test_id(5), - PrincipalId::new_user_test_id(6), - PrincipalId::new_user_test_id(7), - PrincipalId::new_user_test_id(8), - PrincipalId::new_user_test_id(9), - ], - }, - SubnetNodeOperatorArg { - subnet_id: PrincipalId::new_subnet_test_id(0), - subnet_type: SubnetType::Application, - node_operators: vec![PrincipalId::new_user_test_id(999)], - }, - ], - }; - let (pic, canister) = init_pocket_ic(&mut args); - - let subnet_id = pic.get_subnet(canister).unwrap(); - +fn submit_proposal( + pic: &PocketIc, + canister: Principal, + sender: Principal, + subnet_id: Principal, + to_halt: bool, +) -> Result<(), String> { let response = pic.update_call( - canister, - Principal::anonymous(), + canister.into(), + sender, "submit_root_proposal_to_change_subnet_halt_status", - candid::encode_args((subnet_id, true)).unwrap(), + candid::encode_args((subnet_id, to_halt)).unwrap(), ); let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); println!("{:?}", response); + response +} - assert!(response.is_ok()); - +fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { let response = pic .update_call( - canister, + canister.into(), Principal::anonymous(), "get_pending_root_proposals_to_change_subnet_halt_status", candid::encode_one(()).unwrap(), @@ -254,5 +261,55 @@ fn fetch_pending_proposals_submited_one() { let response: Vec = candid::decode_one(&response).expect("Should be able to decode response"); + response +} + +#[test] +fn fetch_pending_proposals_submited_one() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let nns = pic.topology().get_nns().unwrap(); + let no_in_subnet = args + .subnet_node_operators + .iter() + .find_map(|arg| match arg.subnet_id.0 == nns { + true => arg.node_operators.first(), + false => None, + }) + .expect("Should be able to find subnet and a node operator with nodes in it"); + + let response = submit_proposal(&pic, canister, no_in_subnet.0.clone(), nns, true); + assert!(response.is_ok()); + + let response = get_pending(&pic, canister); + assert!(response.len() == 1) } + +#[test] +fn disallow_proposals_from_node_operators_not_in_subnet() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let nns = pic.topology().get_nns().unwrap(); + let no_not_in_subnet = args + .subnet_node_operators + .iter() + .find_map(|arg| match arg.subnet_id.0 != nns { + true => arg.node_operators.first(), + false => None, + }) + .expect("Should be able to find subnet and a node operator with nodes in it"); + + // Try with a node operator that is not in the subnet + let response = submit_proposal(&pic, canister, no_not_in_subnet.0.clone(), nns, true); + assert!(response.is_err()); + + // Try with anonymous principal + let response = submit_proposal(&pic, canister, Principal::anonymous(), nns, true); + assert!(response.is_err()); + + let response = get_pending(&pic, canister); + assert!(response.len() == 0) +} From b5e41692f40359d2f935a01bc1db8511a4c665aa Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 2 Feb 2025 20:55:30 +0100 Subject: [PATCH 07/76] adding more checks to tests --- rs/nns/handlers/root/impl/backup/tests/mod.rs | 114 ++++++++++++++---- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index 6b6bb2510c0..c6fb646dba7 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -77,7 +77,8 @@ fn add_routing_table_record(total_mutations: &mut Vec, nns_id: struct SubnetNodeOperatorArg { subnet_id: PrincipalId, subnet_type: SubnetType, - node_operators: Vec, + // Operator id : number of nodes in subnet + node_operators: BTreeMap, } struct RegistryPreparationArguments { @@ -93,22 +94,26 @@ impl Default for RegistryPreparationArguments { subnet_type: SubnetType::System, node_operators: vec![ // Each has 4 nodes so this is 40 nodes in total - PrincipalId::new_user_test_id(0), - PrincipalId::new_user_test_id(1), - PrincipalId::new_user_test_id(2), - PrincipalId::new_user_test_id(3), - PrincipalId::new_user_test_id(4), - PrincipalId::new_user_test_id(5), - PrincipalId::new_user_test_id(6), - PrincipalId::new_user_test_id(7), - PrincipalId::new_user_test_id(8), - PrincipalId::new_user_test_id(9), - ], + (PrincipalId::new_user_test_id(0), 4), + (PrincipalId::new_user_test_id(1), 4), + (PrincipalId::new_user_test_id(2), 4), + (PrincipalId::new_user_test_id(3), 4), + (PrincipalId::new_user_test_id(4), 4), + (PrincipalId::new_user_test_id(5), 4), + (PrincipalId::new_user_test_id(6), 4), + (PrincipalId::new_user_test_id(7), 4), + (PrincipalId::new_user_test_id(8), 4), + (PrincipalId::new_user_test_id(9), 4), + ] + .into_iter() + .collect(), }, SubnetNodeOperatorArg { subnet_id: PrincipalId::new_subnet_test_id(0), subnet_type: SubnetType::Application, - node_operators: vec![PrincipalId::new_user_test_id(999)], + node_operators: vec![(PrincipalId::new_user_test_id(999), 4)] + .into_iter() + .collect(), }, ], } @@ -118,8 +123,6 @@ impl Default for RegistryPreparationArguments { fn prepare_registry( registry_preparation_args: &mut RegistryPreparationArguments, ) -> Vec { - // let nns_id: SubnetId = SubnetId::from(PrincipalId(nns_id)); - // let app_id: SubnetId = SubnetId::from(PrincipalId(app_id)); let mut total_mutations = vec![]; let mut subnet_list_record = SubnetListRecord::default(); @@ -128,13 +131,13 @@ fn prepare_registry( let mut operator_mutation_ids: u8 = 0; for arg in ®istry_preparation_args.subnet_node_operators { let mut current_subnet_nodes = BTreeMap::new(); - for operator in &arg.node_operators { + for (operator, num_nodes) in &arg.node_operators { let (mutation, nodes) = prepare_registry_with_nodes_and_node_operator_id( - operator_mutation_ids * 4, - 4, + operator_mutation_ids, + *num_nodes as u64, operator.clone(), ); - operator_mutation_ids += 1; + operator_mutation_ids += num_nodes; total_mutations.extend(mutation.mutations); current_subnet_nodes.extend(nodes); @@ -274,7 +277,15 @@ fn fetch_pending_proposals_submited_one() { .subnet_node_operators .iter() .find_map(|arg| match arg.subnet_id.0 == nns { - true => arg.node_operators.first(), + true => { + let operator_principals = arg + .node_operators + .iter() + .map(|(principal, _)| principal) + .collect::>(); + + operator_principals.first().cloned() + } false => None, }) .expect("Should be able to find subnet and a node operator with nodes in it"); @@ -284,7 +295,58 @@ fn fetch_pending_proposals_submited_one() { let response = get_pending(&pic, canister); - assert!(response.len() == 1) + assert!(response.len() == 1); + let proposal = response.first().unwrap(); + + let node_operators_in_subnet = args + .subnet_node_operators + .iter() + .find_map(|arg| { + if arg.subnet_id.0 == nns { + Some(arg.node_operators.clone()) + } else { + None + } + }) + .expect("Should find the corresponding number of node operators"); + + let expected_ballots: u8 = node_operators_in_subnet.values().sum(); + assert_eq!( + proposal.node_operator_ballots.len(), + expected_ballots as usize, + "Received:\n{:?}\nExpected (key * value):\n{:?}", + proposal.node_operator_ballots, + node_operators_in_subnet + ); + assert!(proposal.proposer.eq(no_in_subnet)); + + let voted_yes: Vec<_> = proposal + .node_operator_ballots + .iter() + .filter(|(_, ballot)| { + ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Yes) + }) + .collect(); + + let (no_principal, _) = voted_yes.first().unwrap(); + assert_eq!(no_principal, no_in_subnet); + assert_eq!( + voted_yes.len(), + *node_operators_in_subnet.get(no_in_subnet).unwrap() as usize + ); + + let voted_undecided: Vec<_> = proposal + .node_operator_ballots + .iter() + .filter(|(_, ballot)| { + ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Undecided) + }) + .collect(); + // All others still didn't vote since its just been proposed + assert_eq!( + voted_undecided.len() as u8, + expected_ballots - voted_yes.len() as u8 + ); } #[test] @@ -297,7 +359,15 @@ fn disallow_proposals_from_node_operators_not_in_subnet() { .subnet_node_operators .iter() .find_map(|arg| match arg.subnet_id.0 != nns { - true => arg.node_operators.first(), + true => { + let operator_principals = arg + .node_operators + .iter() + .map(|(principal, _)| principal) + .collect::>(); + + operator_principals.first().cloned() + } false => None, }) .expect("Should be able to find subnet and a node operator with nodes in it"); From d2396db8191ac714cb48c26c4f702bbd87656710 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 15:10:40 +0100 Subject: [PATCH 08/76] adding voting possiblity --- rs/nns/handlers/root/impl/backup/canister.rs | 19 +- rs/nns/handlers/root/impl/backup/tests/mod.rs | 183 +++++++++++++++++- .../root/impl/src/backup_root_proposals.rs | 80 +++++++- 3 files changed, 270 insertions(+), 12 deletions(-) diff --git a/rs/nns/handlers/root/impl/backup/canister.rs b/rs/nns/handlers/root/impl/backup/canister.rs index 9c9671d1cf3..2811eb1ed36 100644 --- a/rs/nns/handlers/root/impl/backup/canister.rs +++ b/rs/nns/handlers/root/impl/backup/canister.rs @@ -5,7 +5,6 @@ use ic_nns_handler_root::{ backup_root_proposals::ChangeSubnetHaltStatus, encode_metrics, root_proposals::RootProposalBallot, }; -use std::cell::RefCell; #[cfg(target_arch = "wasm32")] use ic_cdk::println; @@ -16,12 +15,6 @@ fn caller() -> PrincipalId { PrincipalId::from(ic_cdk::caller()) } -thread_local! { - // How this value was chosen: queues become full at 500. This is 1/3 of that, which seems to be - // a reasonable balance. - static AVAILABLE_MANAGEMENT_CANISTER_CALL_SLOT_COUNT: RefCell = const { RefCell::new(167) }; -} - // canister_init and canister_post_upgrade are needed here // to ensure that printer hook is set up, otherwise error // messages are quite obscure. @@ -42,6 +35,8 @@ async fn submit_root_proposal_to_change_subnet_halt_status( subnet_id: SubnetId, halt: bool, ) -> Result<(), String> { + //TODO: Create a separate thing that polls nns for node operators and store them in memory + // If nns is down we won't be able to call registry canister ic_nns_handler_root::backup_root_proposals::submit_root_proposal_to_change_subnet_halt_status( caller(), subnet_id, @@ -52,10 +47,14 @@ async fn submit_root_proposal_to_change_subnet_halt_status( #[update(hidden = true)] async fn vote_on_root_proposal_to_change_subnet_halt_status( - _proposer: PrincipalId, - _ballot: RootProposalBallot, + proposer: PrincipalId, + ballot: RootProposalBallot, ) -> Result<(), String> { - Ok(()) + ic_nns_handler_root::backup_root_proposals::vote_on_root_proposal_to_change_subnet_halt_status( + caller(), + proposer, + ballot, + ) } #[update(hidden = true)] diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index c6fb646dba7..1087e97b50d 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -3,7 +3,9 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; -use ic_nns_handler_root::backup_root_proposals::ChangeSubnetHaltStatus; +use ic_nns_handler_root::{ + backup_root_proposals::ChangeSubnetHaltStatus, root_proposals::RootProposalBallot, +}; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, routing_table::v1::RoutingTable as RoutingTablePB, @@ -263,10 +265,33 @@ fn get_pending(pic: &PocketIc, canister: Principal) -> Vec = candid::decode_one(&response).expect("Should be able to decode response"); + println!("{:?}", response); response } +fn vote( + pic: &PocketIc, + canister: Principal, + sender: Principal, + proposer: PrincipalId, + ballot: RootProposalBallot, +) -> Result<(), String> { + let response = pic + .update_call( + canister.into(), + sender, + "vote_on_root_proposal_to_change_subnet_halt_status", + candid::encode_args((proposer, ballot)).unwrap(), + ) + .expect("Should be able to call vote function"); + + let response: Result<(), String> = + candid::decode_one(&response).expect("Should be able to decode response"); + println!("{:?}", response); + response +} + #[test] fn fetch_pending_proposals_submited_one() { let mut args = RegistryPreparationArguments::default(); @@ -383,3 +408,159 @@ fn disallow_proposals_from_node_operators_not_in_subnet() { let response = get_pending(&pic, canister); assert!(response.len() == 0) } + +#[test] +fn place_proposal_and_vote_yes_with_one_node_operator() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let nns = pic.topology().get_nns().unwrap(); + let mut node_operators = args + .subnet_node_operators + .iter() + .find_map(|arg| match arg.subnet_id.0 == nns { + true => { + let operator_principals = arg + .node_operators + .iter() + .map(|(principal, _)| principal) + .collect::>(); + + Some(operator_principals) + } + false => None, + }) + .expect("Should be able to find subnet and a node operators with nodes in it"); + + let proposer = node_operators.pop().unwrap(); + let response = submit_proposal(&pic, canister, proposer.0.clone(), nns, true); + assert!(response.is_ok()); + + let first_voter = node_operators.pop().unwrap(); + let response = vote( + &pic, + canister, + first_voter.0.clone(), + proposer.clone(), + RootProposalBallot::Yes, + ); + assert!(response.is_ok()); + + let second_voter = node_operators.pop().unwrap(); + let response = vote( + &pic, + canister, + second_voter.0.clone(), + proposer.clone(), + RootProposalBallot::No, + ); + assert!(response.is_ok()); + + let non_existant_voter = Principal::anonymous(); + let response = vote( + &pic, + canister, + non_existant_voter, + proposer.clone(), + RootProposalBallot::Yes, + ); + assert!(response.is_err()); + + let try_vote_second_again = vote( + &pic, + canister, + second_voter.0.clone(), + proposer.clone(), + RootProposalBallot::Yes, + ); + assert!(try_vote_second_again.is_err()); + + let proposals = get_pending(&pic, canister); + let proposal = proposals.first().unwrap(); + + let voted_yes: Vec<(PrincipalId, RootProposalBallot)> = proposal + .node_operator_ballots + .iter() + .filter(|(_, ballot)| ballot == &RootProposalBallot::Yes) + .cloned() + .collect(); + + let total_nodes_in_subnet_from_yes_voters: Vec<(PrincipalId, u8)> = args + .subnet_node_operators + .iter() + .find_map(|subnet_arg| match subnet_arg.subnet_id.0.eq(&nns) { + false => None, + true => Some( + subnet_arg + .node_operators + .clone() + .into_iter() + .filter(|(principal, _)| principal.eq(proposer) || principal.eq(first_voter)) + .collect(), + ), + }) + .unwrap(); + + assert_eq!( + voted_yes.len(), + total_nodes_in_subnet_from_yes_voters + .iter() + .map(|(_, nodes)| nodes) + .sum::() as usize + ); + + let mut voted_yes = voted_yes.iter().map(|(p, _)| p).collect::>(); + voted_yes.sort(); + voted_yes.dedup(); + + let mut total_nodes_in_subnet_from_yes_voters = total_nodes_in_subnet_from_yes_voters + .iter() + .map(|(p, _)| p) + .collect::>(); + total_nodes_in_subnet_from_yes_voters.sort(); + total_nodes_in_subnet_from_yes_voters.dedup(); + assert_eq!(voted_yes, total_nodes_in_subnet_from_yes_voters); + + let voted_no: Vec<(PrincipalId, RootProposalBallot)> = proposal + .node_operator_ballots + .iter() + .filter(|(_, ballot)| ballot == &RootProposalBallot::No) + .cloned() + .collect(); + + let total_nodes_in_subnet_from_no_voters: Vec<(PrincipalId, u8)> = args + .subnet_node_operators + .iter() + .find_map(|subnet_arg| match subnet_arg.subnet_id.0.eq(&nns) { + false => None, + true => Some( + subnet_arg + .node_operators + .clone() + .into_iter() + .filter(|(principal, _)| principal.eq(second_voter)) + .collect(), + ), + }) + .unwrap(); + + assert_eq!( + voted_no.len(), + total_nodes_in_subnet_from_no_voters + .iter() + .map(|(_, nodes)| nodes) + .sum::() as usize + ); + + let mut voted_no = voted_no.iter().map(|(p, _)| p).collect::>(); + voted_no.sort(); + voted_no.dedup(); + + let mut total_nodes_in_subnet_from_no_voters = total_nodes_in_subnet_from_no_voters + .iter() + .map(|(p, _)| p) + .collect::>(); + total_nodes_in_subnet_from_no_voters.sort(); + total_nodes_in_subnet_from_no_voters.dedup(); + assert_eq!(voted_no, total_nodes_in_subnet_from_no_voters); +} diff --git a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs index 614cd963009..00e58a6ef00 100644 --- a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs +++ b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs @@ -137,7 +137,7 @@ pub async fn submit_root_proposal_to_change_subnet_halt_status( PROPOSALS.with(|proposals| { if let Some(proposal) = proposals.borrow().get(&caller) { println!( - "Current root proposal {:?} from {} is going to be overwritten.", + "Current proposal {:?} from {} is going to be overwritten.", proposal, caller, ); } @@ -156,3 +156,81 @@ pub async fn submit_root_proposal_to_change_subnet_halt_status( Ok(()) } + +pub fn vote_on_root_proposal_to_change_subnet_halt_status( + caller: PrincipalId, + proposer: PrincipalId, + ballot: RootProposalBallot, +) -> Result<(), String> { + if ballot.eq(&RootProposalBallot::Undecided) { + return Err("Cannot register an undecided vote".to_string()); + } + + PROPOSALS.with(|proposals| { + let mut proposals = proposals.borrow_mut(); + let proposal = proposals.get_mut(&proposer).ok_or({ + let message = format!("No change subnet halt status from {} is pending", proposer); + println!("{}", message); + message + })?; + + // Check if the proposal timed out? + + let mut voted_on: i32 = 0; + for (_, node_operator_ballot) in &mut proposal + .node_operator_ballots + .iter_mut() + .filter(|(node_operator, _)| node_operator == &caller) + { + // Already voted + if !node_operator_ballot.eq(&&RootProposalBallot::Undecided) { + let message = format!("Caller: {} has already voted on this proposal", caller); + println!("{}", message); + return Err(message); + } + + // Register a vote + *node_operator_ballot = ballot.clone(); + voted_on += 1; + } + + if voted_on == 0 { + let message = format!( + "Caller: {} is not eligible to vote on root change status proposal", + caller + ); + println!("{}", message); + return Err(message); + } + + let mut votes_yes: u32 = 0; + let mut votes_no: u32 = 0; + let mut votes_undecided: u32 = 0; + + for (_, ballot) in &proposal.node_operator_ballots { + match ballot { + RootProposalBallot::Yes => votes_yes += 1, + RootProposalBallot::No => votes_no += 1, + RootProposalBallot::Undecided => votes_undecided += 1, + } + } + + println!("Vote(s) on root proposal to change subnet status from proposer {}: Current tally: {} Yes, {} No, {} Undecided", proposer, votes_yes, votes_no, votes_undecided); + if proposal.is_byzantine_majority_yes() { + println!( + "Proposal from proposer {} has received majority yes", + proposer + ); + // Update the status somewhere from where the orchestrator is polling + proposals.remove(&proposer); + } else if proposal.is_byzantine_majority_no() { + println!( + "Proposal from proposer {} has received majority no", + proposer + ); + proposals.remove(&proposer); + } + + Ok(()) + }) +} From d45fba0bf337effa332b308c4abfb86ce23d654d Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 15:22:13 +0100 Subject: [PATCH 09/76] adding tests to verify byzantine majority --- rs/nns/handlers/root/impl/backup/tests/mod.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs index 1087e97b50d..03d449f46d1 100644 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ b/rs/nns/handlers/root/impl/backup/tests/mod.rs @@ -564,3 +564,64 @@ fn place_proposal_and_vote_yes_with_one_node_operator() { total_nodes_in_subnet_from_no_voters.dedup(); assert_eq!(voted_no, total_nodes_in_subnet_from_no_voters); } + +#[test] +fn test_byzantine_majority() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let nns = pic.topology().get_nns().unwrap(); + let mut node_operators = args + .subnet_node_operators + .iter() + .find_map(|arg| match arg.subnet_id.0 == nns { + true => { + let operator_principals = arg + .node_operators + .iter() + .map(|(principal, _)| principal) + .collect::>(); + + Some(operator_principals) + } + false => None, + }) + .expect("Should be able to find subnet and a node operators with nodes in it"); + + let proposer = node_operators.pop().unwrap(); + let response = submit_proposal(&pic, canister, proposer.0.clone(), nns, true); + assert!(response.is_ok()); + + // For this test we have 40 nodes spread across 10 node operators. + // max faults = (40 - 1) / 3 = 13 + // needed yes => 40 - 13 = 27 + // Each operator has 4 nodes which means that we need 7 node operators + // to vote yes to adopt the proposal. + + // Since one is the proposer it means we require 6 more + + // First 5 should be able to vote and still fetch the proposal. After the 6th + // votes the proposal will be removed meaning it should no longer be fetchable + + for voter in 0..5 { + let voter = node_operators + .get(voter) + .expect("Should exist for this example"); + + let response = vote(&pic, canister, voter.0, *proposer, RootProposalBallot::Yes); + assert!(response.is_ok()); + + let pending_proposals = get_pending(&pic, canister); + assert!(pending_proposals.len().eq(&1)); + } + + // After the 6th one goes in it should no longer be fetchable + let voter = node_operators + .get(5) + .expect("Should exist for this example"); + let response = vote(&pic, canister, voter.0, *proposer, RootProposalBallot::Yes); + assert!(response.is_ok()); + + let pending_proposals = get_pending(&pic, canister); + assert!(pending_proposals.is_empty()); +} From 0933f42c3ae62d0cc5f87d5933145a0acfb066b1 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 16:48:43 +0100 Subject: [PATCH 10/76] extracting into separate create --- Cargo.lock | 54 +- Cargo.toml | 1 + rs/nns/handlers/recovery/impl/BUILD.bazel | 164 +++++ rs/nns/handlers/recovery/impl/Cargo.toml | 69 ++ rs/nns/handlers/recovery/impl/build.rs | 3 + .../impl/canister}/canister.rs | 41 +- .../impl/canister/recovery.did} | 0 .../recovery/impl/canister/tests/mod.rs | 372 +++++++++++ .../impl/canister}/tests/test_helpers.rs | 0 rs/nns/handlers/recovery/impl/src/lib.rs | 23 + rs/nns/handlers/recovery/impl/src/metrics.rs | 4 + .../impl/src/update_nns_status_proposal.rs | 40 ++ rs/nns/handlers/root/impl/BUILD.bazel | 38 +- rs/nns/handlers/root/impl/Cargo.toml | 8 - rs/nns/handlers/root/impl/backup/tests/mod.rs | 627 ------------------ .../root/impl/src/backup_root_proposals.rs | 236 ------- rs/nns/handlers/root/impl/src/lib.rs | 1 - .../handlers/root/impl/src/root_proposals.rs | 4 +- 18 files changed, 750 insertions(+), 935 deletions(-) create mode 100644 rs/nns/handlers/recovery/impl/BUILD.bazel create mode 100644 rs/nns/handlers/recovery/impl/Cargo.toml create mode 100644 rs/nns/handlers/recovery/impl/build.rs rename rs/nns/handlers/{root/impl/backup => recovery/impl/canister}/canister.rs (77%) rename rs/nns/handlers/{root/impl/backup/backup-root.did => recovery/impl/canister/recovery.did} (100%) create mode 100644 rs/nns/handlers/recovery/impl/canister/tests/mod.rs rename rs/nns/handlers/{root/impl/backup => recovery/impl/canister}/tests/test_helpers.rs (100%) create mode 100644 rs/nns/handlers/recovery/impl/src/lib.rs create mode 100644 rs/nns/handlers/recovery/impl/src/metrics.rs create mode 100644 rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs delete mode 100644 rs/nns/handlers/root/impl/backup/tests/mod.rs delete mode 100644 rs/nns/handlers/root/impl/src/backup_root_proposals.rs diff --git a/Cargo.lock b/Cargo.lock index 81b2c9cc6b1..3989c90c22d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10801,7 +10801,7 @@ dependencies = [ ] [[package]] -name = "ic-nns-handler-root" +name = "ic-nns-handler-recovery" version = "0.9.0" dependencies = [ "assert_matches", @@ -10827,8 +10827,6 @@ dependencies = [ "ic-nervous-system-runtime", "ic-nns-common", "ic-nns-constants", - "ic-nns-handler-root-interface", - "ic-nns-handler-root-protobuf-generator", "ic-nns-test-utils", "ic-protobuf", "ic-registry-keys", @@ -10853,6 +10851,56 @@ dependencies = [ "tokio", ] +[[package]] +name = "ic-nns-handler-root" +version = "0.9.0" +dependencies = [ + "assert_matches", + "build-info", + "build-info-build", + "candid", + "candid_parser", + "canister-test", + "dfn_candid", + "hex", + "ic-base-types", + "ic-canisters-http-types", + "ic-cdk 0.16.0", + "ic-cdk-macros 0.9.0", + "ic-crypto-sha2", + "ic-management-canister-types", + "ic-metrics-encoder", + "ic-nervous-system-clients", + "ic-nervous-system-common", + "ic-nervous-system-common-build-metadata", + "ic-nervous-system-proxied-canister-calls-tracker", + "ic-nervous-system-root", + "ic-nervous-system-runtime", + "ic-nns-common", + "ic-nns-constants", + "ic-nns-handler-root-interface", + "ic-nns-handler-root-protobuf-generator", + "ic-nns-test-utils", + "ic-protobuf", + "ic-registry-keys", + "ic-registry-routing-table", + "ic-registry-transport", + "ic-state-machine-tests", + "ic-test-utilities", + "ic-test-utilities-compare-dirs", + "ic-types", + "lazy_static", + "maplit", + "on_wire", + "pretty_assertions", + "prost 0.13.4", + "registry-canister", + "serde", + "serde_bytes", + "tempfile", + "tokio", +] + [[package]] name = "ic-nns-handler-root-interface" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b17630a8d17..7f294ab7eaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -211,6 +211,7 @@ members = [ "rs/nns/governance/protobuf_generator", "rs/nns/handlers/lifeline/impl", "rs/nns/handlers/lifeline/interface", + "rs/nns/handlers/recovery/impl", "rs/nns/handlers/root/impl", "rs/nns/handlers/root/impl/protobuf_generator", "rs/nns/handlers/root/interface", diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel new file mode 100644 index 00000000000..3ee16f60c28 --- /dev/null +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -0,0 +1,164 @@ +load("@rules_rust//cargo:defs.bzl", "cargo_build_script") +load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test") +load("//bazel:canisters.bzl", "rust_canister") +load("//bazel:defs.bzl", "rust_ic_test_suite") +load("//bazel:prost.bzl", "generated_files_check") + +package(default_visibility = ["//visibility:public"]) + +exports_files(["canister/recovery.did"]) + +# See rs/nervous_system/feature_test.md +BASE_DEPENDENCIES = [ + # Keep sorted. + "//rs/crypto/sha2", + "//rs/nervous_system/clients", + "//rs/nervous_system/common", + "//rs/nervous_system/proxied_canister_calls_tracker", + "//rs/nervous_system/root", + "//rs/nervous_system/runtime", + "//rs/nns/common", + "//rs/nns/constants", + "//rs/nns/handlers/root/interface", + "//rs/protobuf", + "//rs/registry/keys", + "//rs/registry/routing_table", + "//rs/registry/transport", + "//rs/rust_canisters/dfn_candid", + "//rs/rust_canisters/http_types", + "//rs/rust_canisters/on_wire", + "//rs/types/base_types", + "//rs/types/management_canister_types", + "@crate_index//:build-info", + "@crate_index//:candid", + "@crate_index//:ic-cdk", + "@crate_index//:ic-metrics-encoder", + "@crate_index//:lazy_static", + "@crate_index//:maplit", + "@crate_index//:prost", + "@crate_index//:serde", + "@crate_index//:serde_bytes", +] + +# Each target declared in this file may choose either these (release-ready) +# dependencies (`DEPENDENCIES`), or `DEPENDENCIES_WITH_TEST_FEATURES` feature previews. +DEPENDENCIES = BASE_DEPENDENCIES + +DEPENDENCIES_WITH_TEST_FEATURES = BASE_DEPENDENCIES + +MACRO_DEPENDENCIES = [ + # Keep sorted. + "//rs/nervous_system/common/build_metadata", + "@crate_index//:ic-cdk-macros", +] + +BUILD_DEPENDENCIES = [ + # Keep sorted. + "@crate_index//:build-info-build", +] + +ALIASES = {} + +DEV_DEPENDENCIES = [ + # Keep sorted. + "//rs/state_machine_tests", + "@crate_index//:candid_parser", + "@crate_index//:pretty_assertions", + "@crate_index//:tokio", +] + select({ + "@rules_rust//rust/platform:wasm32-unknown-unknown": [], + "//conditions:default": [ + "//rs/rust_canisters/canister_test", + "//rs/nns/handlers/root/impl/protobuf_generator:lib", + "//rs/nns/test_utils", + "//rs/types/types", + "//rs/test_utilities", + "//rs/test_utilities/compare_dirs", + "//rs/registry/canister:canister--test_feature", + "@crate_index//:tempfile", + "@crate_index//:assert_matches", + "@crate_index//:hex", + ], +}) + +MACRO_DEV_DEPENDENCIES = [] + +LIB_SRCS = glob( + ["src/**"], + exclude = [ + "**/*tests.rs", + "**/tests/**", + ], +) + +cargo_build_script( + name = "build_script", + srcs = ["build.rs"], + aliases = ALIASES, + data = [], # build script data (e.g. template files) goes here + version = "0.9.0", + deps = BUILD_DEPENDENCIES, +) + +rust_library( + name = "recovery", + srcs = LIB_SRCS, + aliases = ALIASES, + crate_name = "ic_nns_handler_recovery", + proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.9.0", + deps = DEPENDENCIES + [":build_script"], +) + +rust_library( + name = "recovery--test_feature", + srcs = LIB_SRCS, + aliases = ALIASES, + crate_features = ["test"], + crate_name = "ic_nns_handler_recovery", + proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.9.0", + deps = DEPENDENCIES_WITH_TEST_FEATURES + [":build_script"], +) + +rust_test( + name = "recovery_test", + srcs = glob(["src/**/*.rs"]), + deps = [":root--test_feature"] + DEPENDENCIES + DEV_DEPENDENCIES, +) + +rust_canister( + name = "recovery-canister", + srcs = ["canister/canister.rs"], + aliases = ALIASES, + proc_macro_deps = MACRO_DEPENDENCIES, + service_file = ":canister/recovery.did", + deps = DEPENDENCIES + [ + ":build_script", + ":recovery", + ], +) + +rust_test( + name = "recovery-canister-tests", + srcs = glob(["backup/**/*.rs"]), + data = [ + ":backup-root-canister", + "//rs/pocket_ic_server:pocket-ic-server", + "//rs/registry/canister:registry-canister" + ], + crate_root = "recovery/canister.rs", + proc_macro_deps = MACRO_DEPENDENCIES, + deps = DEPENDENCIES + DEV_DEPENDENCIES + [ + ":build_script", + ":recovery", + "//packages/pocket-ic", + "//rs/registry/subnet_type" + ], + env = { + "BACKUP_ROOT_WASM_PATH": "$(rootpath :recovery-canister)", + "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", + "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" + }, + version = "0.9.0" +) diff --git a/rs/nns/handlers/recovery/impl/Cargo.toml b/rs/nns/handlers/recovery/impl/Cargo.toml new file mode 100644 index 00000000000..b9dd99edc12 --- /dev/null +++ b/rs/nns/handlers/recovery/impl/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "ic-nns-handler-recovery" +version.workspace = true +authors.workspace = true +edition.workspace = true +description.workspace = true +documentation.workspace = true + +[[bin]] +name = "recovery-canister" +path = "canister/canister.rs" + +[lib] +path = "src/lib.rs" + +[dependencies] +build-info = { workspace = true } +candid = { workspace = true } +dfn_candid = { path = "../../../../rust_canisters/dfn_candid" } +ic-base-types = { path = "../../../../types/base_types" } +ic-canisters-http-types = { path = "../../../../rust_canisters/http_types" } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +ic-crypto-sha2 = { path = "../../../../crypto/sha2" } +ic-management-canister-types = { path = "../../../../types/management_canister_types" } +ic-metrics-encoder = "1" +ic-nervous-system-clients = { path = "../../../../nervous_system/clients" } +ic-nervous-system-common = { path = "../../../../nervous_system/common" } +ic-nervous-system-common-build-metadata = { path = "../../../../nervous_system/common/build_metadata" } +ic-nervous-system-proxied-canister-calls-tracker = { path = "../../../../nervous_system/proxied_canister_calls_tracker" } +ic-nervous-system-root = { path = "../../../../nervous_system/root" } +ic-nervous-system-runtime = { path = "../../../../nervous_system/runtime" } +ic-nns-common = { path = "../../../common" } +ic-nns-constants = { path = "../../../constants" } +ic-protobuf = { path = "../../../../protobuf" } +ic-registry-keys = { path = "../../../../registry/keys" } +ic-registry-routing-table = { path = "../../../../registry/routing_table" } +ic-registry-transport = { path = "../../../../registry/transport" } +lazy_static = { workspace = true } +maplit = "1.0.2" +on_wire = { path = "../../../../rust_canisters/on_wire" } +prost = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +assert_matches = { workspace = true } +canister-test = { path = "../../../../rust_canisters/canister_test" } +hex = { workspace = true } +ic-nns-constants = { path = "../../../constants" } +ic-nns-test-utils = { path = "../../../../nns/test_utils" } +ic-test-utilities = { path = "../../../../test_utilities" } +ic-test-utilities-compare-dirs = { path = "../../../../test_utilities/compare_dirs" } +ic-types = { path = "../../../../types/types" } +on_wire = { path = "../../../../rust_canisters/on_wire" } +registry-canister = { path = "../../../../registry/canister" } +tempfile = { workspace = true } + +[build-dependencies] +build-info-build = { workspace = true } + +[dev-dependencies] +candid_parser = { workspace = true } +ic-state-machine-tests = { path = "../../../../state_machine_tests" } +pretty_assertions = { workspace = true } +tokio = { workspace = true } +pocket-ic.path = "../../../../../packages/pocket-ic" +ic-test-utilities-load-wasm.path = "../../../../test_utilities/load_wasm" +ic-registry-subnet-type.path = "../../../../registry/subnet_type" diff --git a/rs/nns/handlers/recovery/impl/build.rs b/rs/nns/handlers/recovery/impl/build.rs new file mode 100644 index 00000000000..d36778f606d --- /dev/null +++ b/rs/nns/handlers/recovery/impl/build.rs @@ -0,0 +1,3 @@ +fn main() { + build_info_build::build_script(); +} diff --git a/rs/nns/handlers/root/impl/backup/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs similarity index 77% rename from rs/nns/handlers/root/impl/backup/canister.rs rename to rs/nns/handlers/recovery/impl/canister/canister.rs index 2811eb1ed36..f2f27fade8c 100644 --- a/rs/nns/handlers/root/impl/backup/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -1,15 +1,12 @@ use ic_base_types::{PrincipalId, SubnetId}; use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_nervous_system_common::serve_metrics; -use ic_nns_handler_root::{ - backup_root_proposals::ChangeSubnetHaltStatus, encode_metrics, - root_proposals::RootProposalBallot, -}; #[cfg(target_arch = "wasm32")] use ic_cdk::println; use ic_cdk::{post_upgrade, query, update}; +use ic_nns_handler_recovery::metrics::encode_metrics; fn caller() -> PrincipalId { PrincipalId::from(ic_cdk::caller()) @@ -32,34 +29,36 @@ ic_nervous_system_common_build_metadata::define_get_build_metadata_candid_method #[update(hidden = true)] async fn submit_root_proposal_to_change_subnet_halt_status( - subnet_id: SubnetId, - halt: bool, + _subnet_id: SubnetId, + _halt: bool, ) -> Result<(), String> { //TODO: Create a separate thing that polls nns for node operators and store them in memory // If nns is down we won't be able to call registry canister - ic_nns_handler_root::backup_root_proposals::submit_root_proposal_to_change_subnet_halt_status( - caller(), - subnet_id, - halt, - ) - .await + // ic_nns_handler_root::backup_root_proposals::submit_root_proposal_to_change_subnet_halt_status( + // caller(), + // subnet_id, + // halt, + // ) + // .await + Ok(()) } #[update(hidden = true)] async fn vote_on_root_proposal_to_change_subnet_halt_status( - proposer: PrincipalId, - ballot: RootProposalBallot, + _proposer: PrincipalId, ) -> Result<(), String> { - ic_nns_handler_root::backup_root_proposals::vote_on_root_proposal_to_change_subnet_halt_status( - caller(), - proposer, - ballot, - ) + // ic_nns_handler_root::backup_root_proposals::vote_on_root_proposal_to_change_subnet_halt_status( + // caller(), + // proposer, + // ballot, + // ) + Ok(()) } #[update(hidden = true)] -fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { - ic_nns_handler_root::backup_root_proposals::get_pending_root_proposals_to_change_subnet_halt_status() +fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { + // ic_nns_handler_root::backup_root_proposals::get_pending_root_proposals_to_change_subnet_halt_status() + vec![] } /// Resources to serve for a given http_request diff --git a/rs/nns/handlers/root/impl/backup/backup-root.did b/rs/nns/handlers/recovery/impl/canister/recovery.did similarity index 100% rename from rs/nns/handlers/root/impl/backup/backup-root.did rename to rs/nns/handlers/recovery/impl/canister/recovery.did diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs new file mode 100644 index 00000000000..f87447c8aa6 --- /dev/null +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -0,0 +1,372 @@ +use std::{collections::BTreeMap, path::PathBuf}; + +use candid::Principal; +use ic_base_types::{CanisterId, PrincipalId, SubnetId}; +use ic_nns_constants::REGISTRY_CANISTER_ID; +use ic_protobuf::registry::{ + replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, + routing_table::v1::RoutingTable as RoutingTablePB, + subnet::v1::SubnetListRecord, +}; +use ic_registry_keys::{ + make_blessed_replica_versions_key, make_replica_version_key, make_routing_table_record_key, +}; +use ic_registry_routing_table::{CanisterIdRange, RoutingTable}; +use ic_registry_subnet_type::SubnetType; +use ic_registry_transport::{ + insert, + pb::v1::{RegistryAtomicMutateRequest, RegistryMutation}, +}; +use maplit::btreemap; +use pocket_ic::{PocketIc, PocketIcBuilder}; +use prost::Message; +use registry_canister::init::RegistryCanisterInitPayload; +use test_helpers::{ + add_fake_subnet, get_invariant_compliant_subnet_record, + prepare_registry_with_nodes_and_node_operator_id, +}; + +mod test_helpers; + +fn fetch_canister_wasm(env: &str) -> Vec { + let path: PathBuf = std::env::var(env) + .expect(&format!("Path should be set in environment variable {env}")) + .try_into() + .unwrap(); + std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) +} + +fn add_replica_version_records(total_mutations: &mut Vec) { + const MOCK_HASH: &str = "d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a1"; + let release_package_url = "http://release_package.tar.zst".to_string(); + let replica_version = insert( + make_replica_version_key(env!("CARGO_PKG_VERSION")).as_bytes(), + ReplicaVersionRecord { + release_package_sha256_hex: MOCK_HASH.into(), + release_package_urls: vec![release_package_url], + guest_launch_measurement_sha256_hex: None, + } + .encode_to_vec(), + ); + total_mutations.push(replica_version); + let blessed_replica_versions = insert( + make_blessed_replica_versions_key().as_bytes(), + BlessedReplicaVersions { + blessed_version_ids: vec![env!("CARGO_PKG_VERSION").to_string()], + } + .encode_to_vec(), + ); + total_mutations.push(blessed_replica_versions); +} + +fn add_routing_table_record(total_mutations: &mut Vec, nns_id: PrincipalId) { + let routing_table = RoutingTable::try_from(btreemap! { + CanisterIdRange { + start: CanisterId::from(0), + end: CanisterId::from(u64::MAX), + } => SubnetId::new(nns_id), + }) + .unwrap(); + total_mutations.push(insert( + make_routing_table_record_key().as_bytes(), + RoutingTablePB::from(routing_table).encode_to_vec(), + )); +} + +struct SubnetNodeOperatorArg { + subnet_id: PrincipalId, + subnet_type: SubnetType, + // Operator id : number of nodes in subnet + node_operators: BTreeMap, +} + +struct RegistryPreparationArguments { + subnet_node_operators: Vec, +} + +impl Default for RegistryPreparationArguments { + fn default() -> Self { + Self { + subnet_node_operators: vec![ + SubnetNodeOperatorArg { + subnet_id: PrincipalId::new_subnet_test_id(0), + subnet_type: SubnetType::System, + node_operators: vec![ + // Each has 4 nodes so this is 40 nodes in total + (PrincipalId::new_user_test_id(0), 4), + (PrincipalId::new_user_test_id(1), 4), + (PrincipalId::new_user_test_id(2), 4), + (PrincipalId::new_user_test_id(3), 4), + (PrincipalId::new_user_test_id(4), 4), + (PrincipalId::new_user_test_id(5), 4), + (PrincipalId::new_user_test_id(6), 4), + (PrincipalId::new_user_test_id(7), 4), + (PrincipalId::new_user_test_id(8), 4), + (PrincipalId::new_user_test_id(9), 4), + ] + .into_iter() + .collect(), + }, + SubnetNodeOperatorArg { + subnet_id: PrincipalId::new_subnet_test_id(0), + subnet_type: SubnetType::Application, + node_operators: vec![(PrincipalId::new_user_test_id(999), 4)] + .into_iter() + .collect(), + }, + ], + } + } +} + +fn prepare_registry( + registry_preparation_args: &mut RegistryPreparationArguments, +) -> Vec { + let mut total_mutations = vec![]; + let mut subnet_list_record = SubnetListRecord::default(); + + add_replica_version_records(&mut total_mutations); + + let mut operator_mutation_ids: u8 = 0; + for arg in ®istry_preparation_args.subnet_node_operators { + let mut current_subnet_nodes = BTreeMap::new(); + for (operator, num_nodes) in &arg.node_operators { + let (mutation, nodes) = prepare_registry_with_nodes_and_node_operator_id( + operator_mutation_ids, + *num_nodes as u64, + operator.clone(), + ); + operator_mutation_ids += num_nodes; + + total_mutations.extend(mutation.mutations); + current_subnet_nodes.extend(nodes); + } + + let mutations = add_fake_subnet( + arg.subnet_id.into(), + &mut subnet_list_record, + get_invariant_compliant_subnet_record( + current_subnet_nodes.keys().cloned().collect(), + arg.subnet_type, + ), + ¤t_subnet_nodes, + ); + total_mutations.extend(mutations); + } + + add_routing_table_record( + &mut total_mutations, + registry_preparation_args + .subnet_node_operators + .iter() + .find_map(|arg| match arg.subnet_type { + SubnetType::System => Some(arg.subnet_id.clone()), + _ => None, + }) + .expect("Missing system subnet"), + ); + + vec![RegistryAtomicMutateRequest { + mutations: total_mutations, + ..Default::default() + }] +} + +fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Principal) { + let mut builder = PocketIcBuilder::new(); + + for arg in &arguments.subnet_node_operators { + if arg.subnet_type == SubnetType::System { + builder = builder.with_nns_subnet(); + continue; + } + + builder = builder.with_application_subnet(); + } + + let pic = builder.build(); + let nns = pic.topology().get_nns().expect("Should contain nns"); + let arg_nns = arguments + .subnet_node_operators + .iter_mut() + .find(|arg| arg.subnet_type == SubnetType::System) + .unwrap(); + arg_nns.subnet_id = nns.into(); + + for (arg, subnet_id) in arguments + .subnet_node_operators + .iter_mut() + .filter(|arg| arg.subnet_type == SubnetType::Application) + .zip(pic.topology().get_app_subnets()) + { + arg.subnet_id = subnet_id.into() + } + + let registry = pic + .create_canister_with_id(None, None, REGISTRY_CANISTER_ID.into()) + .unwrap(); + pic.add_cycles(registry, 100_000_000_000_000); + + pic.install_canister( + registry, + fetch_canister_wasm("REGISTRY_WASM_PATH"), + candid::encode_one(RegistryCanisterInitPayload { + mutations: prepare_registry(arguments), + }) + .unwrap(), + None, + ); + + let app_subnets = pic.topology().get_app_subnets(); + + let subnet_id = app_subnets.first().expect("Should contain one app subnet"); + + let canister = pic.create_canister_on_subnet(None, None, *subnet_id); + pic.add_cycles(canister, 100_000_000_000_000); + pic.install_canister( + canister, + fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), + candid::encode_one(()).unwrap(), + None, + ); + (pic, canister) +} + +fn submit_proposal( + pic: &PocketIc, + canister: Principal, + sender: Principal, + subnet_id: Principal, + to_halt: bool, +) -> Result<(), String> { + let response = pic.update_call( + canister.into(), + sender, + "submit_root_proposal_to_change_subnet_halt_status", + candid::encode_args((subnet_id, to_halt)).unwrap(), + ); + let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); + println!("{:?}", response); + response +} + +fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { + let response = pic + .update_call( + canister.into(), + Principal::anonymous(), + "get_pending_root_proposals_to_change_subnet_halt_status", + candid::encode_one(()).unwrap(), + ) + .expect("Should be able to fetch remaining proposals"); + + let response: Vec = + candid::decode_one(&response).expect("Should be able to decode response"); + println!("{:?}", response); + + response +} + +fn vote( + pic: &PocketIc, + canister: Principal, + sender: Principal, + proposer: PrincipalId, + ballot: u8, +) -> Result<(), String> { + let response = pic + .update_call( + canister.into(), + sender, + "vote_on_root_proposal_to_change_subnet_halt_status", + candid::encode_args((proposer, ballot)).unwrap(), + ) + .expect("Should be able to call vote function"); + + let response: Result<(), String> = + candid::decode_one(&response).expect("Should be able to decode response"); + println!("{:?}", response); + response +} + +// #[test] +// fn fetch_pending_proposals_submited_one() { +// let mut args = RegistryPreparationArguments::default(); +// let (pic, canister) = init_pocket_ic(&mut args); + +// let nns = pic.topology().get_nns().unwrap(); +// let no_in_subnet = args +// .subnet_node_operators +// .iter() +// .find_map(|arg| match arg.subnet_id.0 == nns { +// true => { +// let operator_principals = arg +// .node_operators +// .iter() +// .map(|(principal, _)| principal) +// .collect::>(); + +// operator_principals.first().cloned() +// } +// false => None, +// }) +// .expect("Should be able to find subnet and a node operator with nodes in it"); + +// let response = submit_proposal(&pic, canister, no_in_subnet.0.clone(), nns, true); +// assert!(response.is_ok()); + +// let response = get_pending(&pic, canister); + +// assert!(response.len() == 1); +// let proposal = response.first().unwrap(); + +// let node_operators_in_subnet = args +// .subnet_node_operators +// .iter() +// .find_map(|arg| { +// if arg.subnet_id.0 == nns { +// Some(arg.node_operators.clone()) +// } else { +// None +// } +// }) +// .expect("Should find the corresponding number of node operators"); + +// let expected_ballots: u8 = node_operators_in_subnet.values().sum(); +// assert_eq!( +// proposal.node_operator_ballots.len(), +// expected_ballots as usize, +// "Received:\n{:?}\nExpected (key * value):\n{:?}", +// proposal.node_operator_ballots, +// node_operators_in_subnet +// ); +// assert!(proposal.proposer.eq(no_in_subnet)); + +// let voted_yes: Vec<_> = proposal +// .node_operator_ballots +// .iter() +// .filter(|(_, ballot)| { +// ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Yes) +// }) +// .collect(); + +// let (no_principal, _) = voted_yes.first().unwrap(); +// assert_eq!(no_principal, no_in_subnet); +// assert_eq!( +// voted_yes.len(), +// *node_operators_in_subnet.get(no_in_subnet).unwrap() as usize +// ); + +// let voted_undecided: Vec<_> = proposal +// .node_operator_ballots +// .iter() +// .filter(|(_, ballot)| { +// ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Undecided) +// }) +// .collect(); +// // All others still didn't vote since its just been proposed +// assert_eq!( +// voted_undecided.len() as u8, +// expected_ballots - voted_yes.len() as u8 +// ); +// } diff --git a/rs/nns/handlers/root/impl/backup/tests/test_helpers.rs b/rs/nns/handlers/recovery/impl/canister/tests/test_helpers.rs similarity index 100% rename from rs/nns/handlers/root/impl/backup/tests/test_helpers.rs rename to rs/nns/handlers/recovery/impl/canister/tests/test_helpers.rs diff --git a/rs/nns/handlers/recovery/impl/src/lib.rs b/rs/nns/handlers/recovery/impl/src/lib.rs new file mode 100644 index 00000000000..6c82a09356a --- /dev/null +++ b/rs/nns/handlers/recovery/impl/src/lib.rs @@ -0,0 +1,23 @@ +use std::time::{Duration, SystemTime}; + +use ic_cdk::api::time; + +pub mod metrics; +pub mod update_nns_status_proposal; + +pub fn now_nanoseconds() -> u64 { + if cfg!(target_arch = "wasm32") { + time() + } else { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get time since epoch") + .as_nanos() + .try_into() + .expect("Failed to convert time to u64") + } +} + +pub fn now_seconds() -> u64 { + Duration::from_nanos(now_nanoseconds()).as_secs() +} diff --git a/rs/nns/handlers/recovery/impl/src/metrics.rs b/rs/nns/handlers/recovery/impl/src/metrics.rs new file mode 100644 index 00000000000..473e9e94d3b --- /dev/null +++ b/rs/nns/handlers/recovery/impl/src/metrics.rs @@ -0,0 +1,4 @@ +//TODO: expose metrics +pub fn encode_metrics(_w: &mut ic_metrics_encoder::MetricsEncoder>) -> std::io::Result<()> { + Ok(()) +} diff --git a/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs b/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs new file mode 100644 index 00000000000..74f04dd9043 --- /dev/null +++ b/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs @@ -0,0 +1,40 @@ +use ic_base_types::{NodeId, PrincipalId}; + +pub enum NnsHealthStatus { + Healthy, + Unhealthy, +} + +pub enum Ballot { + Yes, + No, + Undecided, +} + +pub struct NodeOperatorBallot { + pub principal: PrincipalId, + pub node_tied_to_ballot: NodeId, + pub ballot: Ballot, + pub signature: Vec, +} + +pub struct RecoveryProposalDetails { + /// Should be equal to recovery proposal + pub payload: String, + + /// The principal id of the proposer (must be one of the node + /// operators of the NNS subnet according to the registry at + /// time of submission). + pub proposer: PrincipalId, + + /// The timestamp, in seconds, at which the proposal was submitted. + pub submission_timestamp_seconds: u64, + + /// List containing the + pub node_operator_ballots: Vec, +} + +pub struct UpdateNnsHealthStatus { + pub status: NnsHealthStatus, + pub recovery_proposal: Option, +} diff --git a/rs/nns/handlers/root/impl/BUILD.bazel b/rs/nns/handlers/root/impl/BUILD.bazel index db45ea6b3fd..4ac7d056804 100644 --- a/rs/nns/handlers/root/impl/BUILD.bazel +++ b/rs/nns/handlers/root/impl/BUILD.bazel @@ -79,7 +79,7 @@ DEV_DEPENDENCIES = [ "//rs/types/types", "//rs/test_utilities", "//rs/test_utilities/compare_dirs", - "//rs/registry/canister:canister--test_feature", + "//rs/registry/canister", "@crate_index//:tempfile", "@crate_index//:assert_matches", "@crate_index//:hex", @@ -149,42 +149,6 @@ rust_canister( ], ) -rust_canister( - name = "backup-root-canister", - srcs = ["backup/canister.rs"], - aliases = ALIASES, - proc_macro_deps = MACRO_DEPENDENCIES, - service_file = ":backup/backup-root.did", - deps = DEPENDENCIES + [ - ":build_script", - ":root", - ], -) - -rust_test( - name = "backup-root-canister-tests", - srcs = glob(["backup/**/*.rs"]), - data = [ - ":backup-root-canister", - "//rs/pocket_ic_server:pocket-ic-server", - "//rs/registry/canister:registry-canister" - ], - crate_root = "backup/canister.rs", - proc_macro_deps = MACRO_DEPENDENCIES, - deps = DEPENDENCIES + DEV_DEPENDENCIES + [ - ":build_script", - ":root", - "//packages/pocket-ic", - "//rs/registry/subnet_type" - ], - env = { - "BACKUP_ROOT_WASM_PATH": "$(rootpath :backup-root-canister)", - "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", - "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" - }, - version = "0.9.0" -) - rust_canister( name = "upgrade-test-canister", srcs = ["test_canisters/upgrade_test_canister.rs"], diff --git a/rs/nns/handlers/root/impl/Cargo.toml b/rs/nns/handlers/root/impl/Cargo.toml index 9b868a7aa79..3e591c022d4 100644 --- a/rs/nns/handlers/root/impl/Cargo.toml +++ b/rs/nns/handlers/root/impl/Cargo.toml @@ -14,10 +14,6 @@ path = "canister/canister.rs" name = "upgrade-test-canister" path = "test_canisters/upgrade_test_canister.rs" -[[bin]] -name = "backup-root-canister" -path = "backup/canister.rs" - [lib] path = "src/lib.rs" @@ -74,7 +70,3 @@ candid_parser = { workspace = true } ic-state-machine-tests = { path = "../../../../state_machine_tests" } pretty_assertions = { workspace = true } tokio = { workspace = true } -pocket-ic.path = "../../../../../packages/pocket-ic" -ic-test-utilities-load-wasm.path = "../../../../test_utilities/load_wasm" -ic-registry-subnet-type.path = "../../../../registry/subnet_type" - diff --git a/rs/nns/handlers/root/impl/backup/tests/mod.rs b/rs/nns/handlers/root/impl/backup/tests/mod.rs deleted file mode 100644 index 03d449f46d1..00000000000 --- a/rs/nns/handlers/root/impl/backup/tests/mod.rs +++ /dev/null @@ -1,627 +0,0 @@ -use std::{collections::BTreeMap, path::PathBuf}; - -use candid::Principal; -use ic_base_types::{CanisterId, PrincipalId, SubnetId}; -use ic_nns_constants::REGISTRY_CANISTER_ID; -use ic_nns_handler_root::{ - backup_root_proposals::ChangeSubnetHaltStatus, root_proposals::RootProposalBallot, -}; -use ic_protobuf::registry::{ - replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, - routing_table::v1::RoutingTable as RoutingTablePB, - subnet::v1::SubnetListRecord, -}; -use ic_registry_keys::{ - make_blessed_replica_versions_key, make_replica_version_key, make_routing_table_record_key, -}; -use ic_registry_routing_table::{CanisterIdRange, RoutingTable}; -use ic_registry_subnet_type::SubnetType; -use ic_registry_transport::{ - insert, - pb::v1::{RegistryAtomicMutateRequest, RegistryMutation}, -}; -use maplit::btreemap; -use pocket_ic::{PocketIc, PocketIcBuilder}; -use prost::Message; -use registry_canister::init::RegistryCanisterInitPayload; -use test_helpers::{ - add_fake_subnet, get_invariant_compliant_subnet_record, - prepare_registry_with_nodes_and_node_operator_id, -}; - -mod test_helpers; - -fn fetch_canister_wasm(env: &str) -> Vec { - let path: PathBuf = std::env::var(env) - .expect(&format!("Path should be set in environment variable {env}")) - .try_into() - .unwrap(); - std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) -} - -fn add_replica_version_records(total_mutations: &mut Vec) { - const MOCK_HASH: &str = "d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a1"; - let release_package_url = "http://release_package.tar.zst".to_string(); - let replica_version = insert( - make_replica_version_key(env!("CARGO_PKG_VERSION")).as_bytes(), - ReplicaVersionRecord { - release_package_sha256_hex: MOCK_HASH.into(), - release_package_urls: vec![release_package_url], - guest_launch_measurement_sha256_hex: None, - } - .encode_to_vec(), - ); - total_mutations.push(replica_version); - let blessed_replica_versions = insert( - make_blessed_replica_versions_key().as_bytes(), - BlessedReplicaVersions { - blessed_version_ids: vec![env!("CARGO_PKG_VERSION").to_string()], - } - .encode_to_vec(), - ); - total_mutations.push(blessed_replica_versions); -} - -fn add_routing_table_record(total_mutations: &mut Vec, nns_id: PrincipalId) { - let routing_table = RoutingTable::try_from(btreemap! { - CanisterIdRange { - start: CanisterId::from(0), - end: CanisterId::from(u64::MAX), - } => SubnetId::new(nns_id), - }) - .unwrap(); - total_mutations.push(insert( - make_routing_table_record_key().as_bytes(), - RoutingTablePB::from(routing_table).encode_to_vec(), - )); -} - -struct SubnetNodeOperatorArg { - subnet_id: PrincipalId, - subnet_type: SubnetType, - // Operator id : number of nodes in subnet - node_operators: BTreeMap, -} - -struct RegistryPreparationArguments { - subnet_node_operators: Vec, -} - -impl Default for RegistryPreparationArguments { - fn default() -> Self { - Self { - subnet_node_operators: vec![ - SubnetNodeOperatorArg { - subnet_id: PrincipalId::new_subnet_test_id(0), - subnet_type: SubnetType::System, - node_operators: vec![ - // Each has 4 nodes so this is 40 nodes in total - (PrincipalId::new_user_test_id(0), 4), - (PrincipalId::new_user_test_id(1), 4), - (PrincipalId::new_user_test_id(2), 4), - (PrincipalId::new_user_test_id(3), 4), - (PrincipalId::new_user_test_id(4), 4), - (PrincipalId::new_user_test_id(5), 4), - (PrincipalId::new_user_test_id(6), 4), - (PrincipalId::new_user_test_id(7), 4), - (PrincipalId::new_user_test_id(8), 4), - (PrincipalId::new_user_test_id(9), 4), - ] - .into_iter() - .collect(), - }, - SubnetNodeOperatorArg { - subnet_id: PrincipalId::new_subnet_test_id(0), - subnet_type: SubnetType::Application, - node_operators: vec![(PrincipalId::new_user_test_id(999), 4)] - .into_iter() - .collect(), - }, - ], - } - } -} - -fn prepare_registry( - registry_preparation_args: &mut RegistryPreparationArguments, -) -> Vec { - let mut total_mutations = vec![]; - let mut subnet_list_record = SubnetListRecord::default(); - - add_replica_version_records(&mut total_mutations); - - let mut operator_mutation_ids: u8 = 0; - for arg in ®istry_preparation_args.subnet_node_operators { - let mut current_subnet_nodes = BTreeMap::new(); - for (operator, num_nodes) in &arg.node_operators { - let (mutation, nodes) = prepare_registry_with_nodes_and_node_operator_id( - operator_mutation_ids, - *num_nodes as u64, - operator.clone(), - ); - operator_mutation_ids += num_nodes; - - total_mutations.extend(mutation.mutations); - current_subnet_nodes.extend(nodes); - } - - let mutations = add_fake_subnet( - arg.subnet_id.into(), - &mut subnet_list_record, - get_invariant_compliant_subnet_record( - current_subnet_nodes.keys().cloned().collect(), - arg.subnet_type, - ), - ¤t_subnet_nodes, - ); - total_mutations.extend(mutations); - } - - add_routing_table_record( - &mut total_mutations, - registry_preparation_args - .subnet_node_operators - .iter() - .find_map(|arg| match arg.subnet_type { - SubnetType::System => Some(arg.subnet_id.clone()), - _ => None, - }) - .expect("Missing system subnet"), - ); - - vec![RegistryAtomicMutateRequest { - mutations: total_mutations, - ..Default::default() - }] -} - -fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Principal) { - let mut builder = PocketIcBuilder::new(); - - for arg in &arguments.subnet_node_operators { - if arg.subnet_type == SubnetType::System { - builder = builder.with_nns_subnet(); - continue; - } - - builder = builder.with_application_subnet(); - } - - let pic = builder.build(); - let nns = pic.topology().get_nns().expect("Should contain nns"); - let arg_nns = arguments - .subnet_node_operators - .iter_mut() - .find(|arg| arg.subnet_type == SubnetType::System) - .unwrap(); - arg_nns.subnet_id = nns.into(); - - for (arg, subnet_id) in arguments - .subnet_node_operators - .iter_mut() - .filter(|arg| arg.subnet_type == SubnetType::Application) - .zip(pic.topology().get_app_subnets()) - { - arg.subnet_id = subnet_id.into() - } - - let registry = pic - .create_canister_with_id(None, None, REGISTRY_CANISTER_ID.into()) - .unwrap(); - pic.add_cycles(registry, 100_000_000_000_000); - - pic.install_canister( - registry, - fetch_canister_wasm("REGISTRY_WASM_PATH"), - candid::encode_one(RegistryCanisterInitPayload { - mutations: prepare_registry(arguments), - }) - .unwrap(), - None, - ); - - let app_subnets = pic.topology().get_app_subnets(); - - let subnet_id = app_subnets.first().expect("Should contain one app subnet"); - - let canister = pic.create_canister_on_subnet(None, None, *subnet_id); - pic.add_cycles(canister, 100_000_000_000_000); - pic.install_canister( - canister, - fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), - candid::encode_one(()).unwrap(), - None, - ); - (pic, canister) -} - -fn submit_proposal( - pic: &PocketIc, - canister: Principal, - sender: Principal, - subnet_id: Principal, - to_halt: bool, -) -> Result<(), String> { - let response = pic.update_call( - canister.into(), - sender, - "submit_root_proposal_to_change_subnet_halt_status", - candid::encode_args((subnet_id, to_halt)).unwrap(), - ); - let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); - println!("{:?}", response); - response -} - -fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { - let response = pic - .update_call( - canister.into(), - Principal::anonymous(), - "get_pending_root_proposals_to_change_subnet_halt_status", - candid::encode_one(()).unwrap(), - ) - .expect("Should be able to fetch remaining proposals"); - - let response: Vec = - candid::decode_one(&response).expect("Should be able to decode response"); - println!("{:?}", response); - - response -} - -fn vote( - pic: &PocketIc, - canister: Principal, - sender: Principal, - proposer: PrincipalId, - ballot: RootProposalBallot, -) -> Result<(), String> { - let response = pic - .update_call( - canister.into(), - sender, - "vote_on_root_proposal_to_change_subnet_halt_status", - candid::encode_args((proposer, ballot)).unwrap(), - ) - .expect("Should be able to call vote function"); - - let response: Result<(), String> = - candid::decode_one(&response).expect("Should be able to decode response"); - println!("{:?}", response); - response -} - -#[test] -fn fetch_pending_proposals_submited_one() { - let mut args = RegistryPreparationArguments::default(); - let (pic, canister) = init_pocket_ic(&mut args); - - let nns = pic.topology().get_nns().unwrap(); - let no_in_subnet = args - .subnet_node_operators - .iter() - .find_map(|arg| match arg.subnet_id.0 == nns { - true => { - let operator_principals = arg - .node_operators - .iter() - .map(|(principal, _)| principal) - .collect::>(); - - operator_principals.first().cloned() - } - false => None, - }) - .expect("Should be able to find subnet and a node operator with nodes in it"); - - let response = submit_proposal(&pic, canister, no_in_subnet.0.clone(), nns, true); - assert!(response.is_ok()); - - let response = get_pending(&pic, canister); - - assert!(response.len() == 1); - let proposal = response.first().unwrap(); - - let node_operators_in_subnet = args - .subnet_node_operators - .iter() - .find_map(|arg| { - if arg.subnet_id.0 == nns { - Some(arg.node_operators.clone()) - } else { - None - } - }) - .expect("Should find the corresponding number of node operators"); - - let expected_ballots: u8 = node_operators_in_subnet.values().sum(); - assert_eq!( - proposal.node_operator_ballots.len(), - expected_ballots as usize, - "Received:\n{:?}\nExpected (key * value):\n{:?}", - proposal.node_operator_ballots, - node_operators_in_subnet - ); - assert!(proposal.proposer.eq(no_in_subnet)); - - let voted_yes: Vec<_> = proposal - .node_operator_ballots - .iter() - .filter(|(_, ballot)| { - ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Yes) - }) - .collect(); - - let (no_principal, _) = voted_yes.first().unwrap(); - assert_eq!(no_principal, no_in_subnet); - assert_eq!( - voted_yes.len(), - *node_operators_in_subnet.get(no_in_subnet).unwrap() as usize - ); - - let voted_undecided: Vec<_> = proposal - .node_operator_ballots - .iter() - .filter(|(_, ballot)| { - ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Undecided) - }) - .collect(); - // All others still didn't vote since its just been proposed - assert_eq!( - voted_undecided.len() as u8, - expected_ballots - voted_yes.len() as u8 - ); -} - -#[test] -fn disallow_proposals_from_node_operators_not_in_subnet() { - let mut args = RegistryPreparationArguments::default(); - let (pic, canister) = init_pocket_ic(&mut args); - - let nns = pic.topology().get_nns().unwrap(); - let no_not_in_subnet = args - .subnet_node_operators - .iter() - .find_map(|arg| match arg.subnet_id.0 != nns { - true => { - let operator_principals = arg - .node_operators - .iter() - .map(|(principal, _)| principal) - .collect::>(); - - operator_principals.first().cloned() - } - false => None, - }) - .expect("Should be able to find subnet and a node operator with nodes in it"); - - // Try with a node operator that is not in the subnet - let response = submit_proposal(&pic, canister, no_not_in_subnet.0.clone(), nns, true); - assert!(response.is_err()); - - // Try with anonymous principal - let response = submit_proposal(&pic, canister, Principal::anonymous(), nns, true); - assert!(response.is_err()); - - let response = get_pending(&pic, canister); - assert!(response.len() == 0) -} - -#[test] -fn place_proposal_and_vote_yes_with_one_node_operator() { - let mut args = RegistryPreparationArguments::default(); - let (pic, canister) = init_pocket_ic(&mut args); - - let nns = pic.topology().get_nns().unwrap(); - let mut node_operators = args - .subnet_node_operators - .iter() - .find_map(|arg| match arg.subnet_id.0 == nns { - true => { - let operator_principals = arg - .node_operators - .iter() - .map(|(principal, _)| principal) - .collect::>(); - - Some(operator_principals) - } - false => None, - }) - .expect("Should be able to find subnet and a node operators with nodes in it"); - - let proposer = node_operators.pop().unwrap(); - let response = submit_proposal(&pic, canister, proposer.0.clone(), nns, true); - assert!(response.is_ok()); - - let first_voter = node_operators.pop().unwrap(); - let response = vote( - &pic, - canister, - first_voter.0.clone(), - proposer.clone(), - RootProposalBallot::Yes, - ); - assert!(response.is_ok()); - - let second_voter = node_operators.pop().unwrap(); - let response = vote( - &pic, - canister, - second_voter.0.clone(), - proposer.clone(), - RootProposalBallot::No, - ); - assert!(response.is_ok()); - - let non_existant_voter = Principal::anonymous(); - let response = vote( - &pic, - canister, - non_existant_voter, - proposer.clone(), - RootProposalBallot::Yes, - ); - assert!(response.is_err()); - - let try_vote_second_again = vote( - &pic, - canister, - second_voter.0.clone(), - proposer.clone(), - RootProposalBallot::Yes, - ); - assert!(try_vote_second_again.is_err()); - - let proposals = get_pending(&pic, canister); - let proposal = proposals.first().unwrap(); - - let voted_yes: Vec<(PrincipalId, RootProposalBallot)> = proposal - .node_operator_ballots - .iter() - .filter(|(_, ballot)| ballot == &RootProposalBallot::Yes) - .cloned() - .collect(); - - let total_nodes_in_subnet_from_yes_voters: Vec<(PrincipalId, u8)> = args - .subnet_node_operators - .iter() - .find_map(|subnet_arg| match subnet_arg.subnet_id.0.eq(&nns) { - false => None, - true => Some( - subnet_arg - .node_operators - .clone() - .into_iter() - .filter(|(principal, _)| principal.eq(proposer) || principal.eq(first_voter)) - .collect(), - ), - }) - .unwrap(); - - assert_eq!( - voted_yes.len(), - total_nodes_in_subnet_from_yes_voters - .iter() - .map(|(_, nodes)| nodes) - .sum::() as usize - ); - - let mut voted_yes = voted_yes.iter().map(|(p, _)| p).collect::>(); - voted_yes.sort(); - voted_yes.dedup(); - - let mut total_nodes_in_subnet_from_yes_voters = total_nodes_in_subnet_from_yes_voters - .iter() - .map(|(p, _)| p) - .collect::>(); - total_nodes_in_subnet_from_yes_voters.sort(); - total_nodes_in_subnet_from_yes_voters.dedup(); - assert_eq!(voted_yes, total_nodes_in_subnet_from_yes_voters); - - let voted_no: Vec<(PrincipalId, RootProposalBallot)> = proposal - .node_operator_ballots - .iter() - .filter(|(_, ballot)| ballot == &RootProposalBallot::No) - .cloned() - .collect(); - - let total_nodes_in_subnet_from_no_voters: Vec<(PrincipalId, u8)> = args - .subnet_node_operators - .iter() - .find_map(|subnet_arg| match subnet_arg.subnet_id.0.eq(&nns) { - false => None, - true => Some( - subnet_arg - .node_operators - .clone() - .into_iter() - .filter(|(principal, _)| principal.eq(second_voter)) - .collect(), - ), - }) - .unwrap(); - - assert_eq!( - voted_no.len(), - total_nodes_in_subnet_from_no_voters - .iter() - .map(|(_, nodes)| nodes) - .sum::() as usize - ); - - let mut voted_no = voted_no.iter().map(|(p, _)| p).collect::>(); - voted_no.sort(); - voted_no.dedup(); - - let mut total_nodes_in_subnet_from_no_voters = total_nodes_in_subnet_from_no_voters - .iter() - .map(|(p, _)| p) - .collect::>(); - total_nodes_in_subnet_from_no_voters.sort(); - total_nodes_in_subnet_from_no_voters.dedup(); - assert_eq!(voted_no, total_nodes_in_subnet_from_no_voters); -} - -#[test] -fn test_byzantine_majority() { - let mut args = RegistryPreparationArguments::default(); - let (pic, canister) = init_pocket_ic(&mut args); - - let nns = pic.topology().get_nns().unwrap(); - let mut node_operators = args - .subnet_node_operators - .iter() - .find_map(|arg| match arg.subnet_id.0 == nns { - true => { - let operator_principals = arg - .node_operators - .iter() - .map(|(principal, _)| principal) - .collect::>(); - - Some(operator_principals) - } - false => None, - }) - .expect("Should be able to find subnet and a node operators with nodes in it"); - - let proposer = node_operators.pop().unwrap(); - let response = submit_proposal(&pic, canister, proposer.0.clone(), nns, true); - assert!(response.is_ok()); - - // For this test we have 40 nodes spread across 10 node operators. - // max faults = (40 - 1) / 3 = 13 - // needed yes => 40 - 13 = 27 - // Each operator has 4 nodes which means that we need 7 node operators - // to vote yes to adopt the proposal. - - // Since one is the proposer it means we require 6 more - - // First 5 should be able to vote and still fetch the proposal. After the 6th - // votes the proposal will be removed meaning it should no longer be fetchable - - for voter in 0..5 { - let voter = node_operators - .get(voter) - .expect("Should exist for this example"); - - let response = vote(&pic, canister, voter.0, *proposer, RootProposalBallot::Yes); - assert!(response.is_ok()); - - let pending_proposals = get_pending(&pic, canister); - assert!(pending_proposals.len().eq(&1)); - } - - // After the 6th one goes in it should no longer be fetchable - let voter = node_operators - .get(5) - .expect("Should exist for this example"); - let response = vote(&pic, canister, voter.0, *proposer, RootProposalBallot::Yes); - assert!(response.is_ok()); - - let pending_proposals = get_pending(&pic, canister); - assert!(pending_proposals.is_empty()); -} diff --git a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs b/rs/nns/handlers/root/impl/src/backup_root_proposals.rs deleted file mode 100644 index 00e58a6ef00..00000000000 --- a/rs/nns/handlers/root/impl/src/backup_root_proposals.rs +++ /dev/null @@ -1,236 +0,0 @@ -use std::{cell::RefCell, collections::BTreeMap}; - -use candid::CandidType; -use ic_base_types::NodeId; -use ic_base_types::{PrincipalId, SubnetId}; -use ic_nns_common::registry::get_value; -use ic_protobuf::registry::subnet::v1::SubnetRecord; -use ic_registry_keys::make_subnet_record_key; -use serde::Deserialize; - -use crate::{ - now_seconds, - root_proposals::{get_node_operator_pid_of_node, RootProposalBallot}, -}; - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct ChangeSubnetHaltStatus { - /// The id of the NNS subnet. - pub subnet_id: SubnetId, - /// The principal id of the proposer (must be one of the node - /// operators of the NNS subnet according to the registry at - /// time of submission). - pub proposer: PrincipalId, - /// The ballots cast by node operators. - pub node_operator_ballots: Vec<(PrincipalId, RootProposalBallot)>, - /// The timestamp, in seconds, at which the proposal was submitted. - pub submission_timestamp_seconds: u64, - /// Should the new status be halted (true) or unhalted (false) - pub halt: bool, -} - -impl ChangeSubnetHaltStatus { - fn is_byzantine_majority(&self, ballot: RootProposalBallot) -> bool { - let num_nodes = self.node_operator_ballots.len(); - let max_faults = (num_nodes - 1) / 3; - let votes_for_ballot: usize = self - .node_operator_ballots - .iter() - .map(|(_, b)| match ballot.eq(b) { - true => 1, - false => 0, - }) - .sum(); - votes_for_ballot >= (num_nodes - max_faults) - } - - /// For a root proposal to have a byzantine majority of yes, it - /// needs to collect N - f ""yes"" votes, where N is the total number - /// of nodes (same as the number of ballots) and f = (N - 1) / 3. - fn is_byzantine_majority_yes(&self) -> bool { - self.is_byzantine_majority(RootProposalBallot::Yes) - } - - /// For a root proposal to have a byzantine majority of no, it - /// needs to collect f + 1 "no" votes, where N s the total number - /// of nodes (same as the number of ballots) and f = (N - 1) / 3. - fn is_byzantine_majority_no(&self) -> bool { - self.is_byzantine_majority(RootProposalBallot::No) - } -} - -thread_local! { - static PROPOSALS: RefCell> = const { RefCell::new(BTreeMap::new()) }; -} - -pub fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { - // Return the pending proposals - PROPOSALS.with(|proposals| proposals.borrow().values().cloned().collect()) -} - -async fn get_subnet_record(subnet_id: SubnetId) -> Result<(SubnetRecord, u64), String> { - get_value(make_subnet_record_key(subnet_id).as_bytes(), None) - .await - .map_err(|e| e.to_string()) -} - -async fn get_node_operator_ballots_for_subnet( - subnet_record: SubnetRecord, - record_version: u64, - caller: PrincipalId, -) -> Result, String> { - let node_ids: Vec = subnet_record - .membership - .iter() - .map(|node_raw| { - NodeId::from(PrincipalId::try_from(node_raw).expect("Should be able to decode node id")) - }) - .collect(); - - let mut node_operator_ballots = Vec::new(); - - for node_id in node_ids { - let node_operator_id = get_node_operator_pid_of_node(&node_id, record_version).await?; - - let ballot = match node_operator_id == caller { - true => RootProposalBallot::Yes, - false => RootProposalBallot::Undecided, - }; - - node_operator_ballots.push((node_operator_id, ballot)) - } - - Ok(node_operator_ballots) -} - -pub async fn submit_root_proposal_to_change_subnet_halt_status( - caller: PrincipalId, - subnet_id: SubnetId, - halt: bool, -) -> Result<(), String> { - let now = now_seconds(); - - let (subnet_record, version) = get_subnet_record(subnet_id.clone()).await?; - - if subnet_record.is_halted == halt { - return Err(format!( - "Subnet halt status is already: {}", - subnet_record.is_halted - )); - } - - let node_operator_ballots = - get_node_operator_ballots_for_subnet(subnet_record.clone(), version, caller).await?; - - // The proposer is not among node operators of the subnet - if !node_operator_ballots - .iter() - .any(|(_, ballot)| ballot.eq(&RootProposalBallot::Yes)) - { - let message = format!( - "[Backup root canister] Invalid proposal. Caller: {} must be among the node operators of the subnet.",caller - ); - println!("{}", message); - return Err(message); - } - - PROPOSALS.with(|proposals| { - if let Some(proposal) = proposals.borrow().get(&caller) { - println!( - "Current proposal {:?} from {} is going to be overwritten.", - proposal, caller, - ); - } - - proposals.borrow_mut().insert( - caller, - ChangeSubnetHaltStatus { - subnet_id, - proposer: caller, - node_operator_ballots, - submission_timestamp_seconds: now, - halt, - }, - ) - }); - - Ok(()) -} - -pub fn vote_on_root_proposal_to_change_subnet_halt_status( - caller: PrincipalId, - proposer: PrincipalId, - ballot: RootProposalBallot, -) -> Result<(), String> { - if ballot.eq(&RootProposalBallot::Undecided) { - return Err("Cannot register an undecided vote".to_string()); - } - - PROPOSALS.with(|proposals| { - let mut proposals = proposals.borrow_mut(); - let proposal = proposals.get_mut(&proposer).ok_or({ - let message = format!("No change subnet halt status from {} is pending", proposer); - println!("{}", message); - message - })?; - - // Check if the proposal timed out? - - let mut voted_on: i32 = 0; - for (_, node_operator_ballot) in &mut proposal - .node_operator_ballots - .iter_mut() - .filter(|(node_operator, _)| node_operator == &caller) - { - // Already voted - if !node_operator_ballot.eq(&&RootProposalBallot::Undecided) { - let message = format!("Caller: {} has already voted on this proposal", caller); - println!("{}", message); - return Err(message); - } - - // Register a vote - *node_operator_ballot = ballot.clone(); - voted_on += 1; - } - - if voted_on == 0 { - let message = format!( - "Caller: {} is not eligible to vote on root change status proposal", - caller - ); - println!("{}", message); - return Err(message); - } - - let mut votes_yes: u32 = 0; - let mut votes_no: u32 = 0; - let mut votes_undecided: u32 = 0; - - for (_, ballot) in &proposal.node_operator_ballots { - match ballot { - RootProposalBallot::Yes => votes_yes += 1, - RootProposalBallot::No => votes_no += 1, - RootProposalBallot::Undecided => votes_undecided += 1, - } - } - - println!("Vote(s) on root proposal to change subnet status from proposer {}: Current tally: {} Yes, {} No, {} Undecided", proposer, votes_yes, votes_no, votes_undecided); - if proposal.is_byzantine_majority_yes() { - println!( - "Proposal from proposer {} has received majority yes", - proposer - ); - // Update the status somewhere from where the orchestrator is polling - proposals.remove(&proposer); - } else if proposal.is_byzantine_majority_no() { - println!( - "Proposal from proposer {} has received majority no", - proposer - ); - proposals.remove(&proposer); - } - - Ok(()) - }) -} diff --git a/rs/nns/handlers/root/impl/src/lib.rs b/rs/nns/handlers/root/impl/src/lib.rs index dabd97b81ca..f8bf2398c64 100644 --- a/rs/nns/handlers/root/impl/src/lib.rs +++ b/rs/nns/handlers/root/impl/src/lib.rs @@ -14,7 +14,6 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -pub mod backup_root_proposals; pub mod canister_management; pub mod init; pub mod pb; diff --git a/rs/nns/handlers/root/impl/src/root_proposals.rs b/rs/nns/handlers/root/impl/src/root_proposals.rs index b9a17a8938a..7d7f30fa148 100644 --- a/rs/nns/handlers/root/impl/src/root_proposals.rs +++ b/rs/nns/handlers/root/impl/src/root_proposals.rs @@ -33,7 +33,7 @@ const MAX_TIME_FOR_GOVERNANCE_UPGRADE_ROOT_PROPOSAL: u64 = 60 * 60 * 24 * 7; /// Root proposals are initialized with one ballot per node at creation /// in the "Undecided" state. These ballots are then changed when the node /// operators vote. -#[derive(Clone, Debug, CandidType, Deserialize, PartialEq)] +#[derive(Clone, Debug, CandidType, Deserialize)] pub enum RootProposalBallot { Yes, No, @@ -529,7 +529,7 @@ async fn get_nns_membership(subnet_id: &SubnetId) -> Result<(Vec, u64), } /// Returns the principal corresponding to the node operator of the given node. -pub async fn get_node_operator_pid_of_node( +async fn get_node_operator_pid_of_node( node_id: &NodeId, version: u64, ) -> Result { From d3a56dc1f8a96b56800c2431f462083d88962477 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 17:14:07 +0100 Subject: [PATCH 11/76] changing the model --- .../impl/src/update_nns_status_proposal.rs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs b/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs index 74f04dd9043..a58646921aa 100644 --- a/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs @@ -1,40 +1,45 @@ -use ic_base_types::{NodeId, PrincipalId}; +use std::cell::RefCell; -pub enum NnsHealthStatus { - Healthy, - Unhealthy, -} +use candid::CandidType; +use ic_base_types::{NodeId, PrincipalId}; +use serde::Deserialize; +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] pub enum Ballot { Yes, No, Undecided, } +#[derive(Clone, Debug, CandidType, Deserialize)] pub struct NodeOperatorBallot { pub principal: PrincipalId, - pub node_tied_to_ballot: NodeId, + pub nodes_tied_to_ballot: Vec, pub ballot: Ballot, pub signature: Vec, } -pub struct RecoveryProposalDetails { - /// Should be equal to recovery proposal - pub payload: String, +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum RecoveryPayload { + Halt, + DoRecovery { height: u64, state_hash: String }, + Unhalt, +} +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct RecoveryProposal { /// The principal id of the proposer (must be one of the node /// operators of the NNS subnet according to the registry at /// time of submission). pub proposer: PrincipalId, - /// The timestamp, in seconds, at which the proposal was submitted. pub submission_timestamp_seconds: u64, - - /// List containing the + /// The ballots cast by node operators. pub node_operator_ballots: Vec, + /// Payload for the proposal + pub payload: RecoveryPayload, } -pub struct UpdateNnsHealthStatus { - pub status: NnsHealthStatus, - pub recovery_proposal: Option, +thread_local! { + static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; } From e3713b65bb287d5e8dbfe2821674ce6bebcdb4da Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 18:39:13 +0100 Subject: [PATCH 12/76] implementing syncing node operators from nns --- Cargo.lock | 2 + rs/nns/handlers/recovery/impl/BUILD.bazel | 8 ++-- rs/nns/handlers/recovery/impl/Cargo.toml | 2 + .../recovery/impl/canister/canister.rs | 38 ++++++++++++---- .../recovery/impl/canister/tests/mod.rs | 34 ++++++++++++++ rs/nns/handlers/recovery/impl/src/lib.rs | 1 + .../recovery/impl/src/node_operator_sync.rs | 44 +++++++++++++++++++ .../handlers/root/impl/src/root_proposals.rs | 6 +-- 8 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 rs/nns/handlers/recovery/impl/src/node_operator_sync.rs diff --git a/Cargo.lock b/Cargo.lock index 3989c90c22d..e22cc3ba295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10816,6 +10816,7 @@ dependencies = [ "ic-canisters-http-types", "ic-cdk 0.16.0", "ic-cdk-macros 0.9.0", + "ic-cdk-timers", "ic-crypto-sha2", "ic-management-canister-types", "ic-metrics-encoder", @@ -10827,6 +10828,7 @@ dependencies = [ "ic-nervous-system-runtime", "ic-nns-common", "ic-nns-constants", + "ic-nns-handler-root", "ic-nns-test-utils", "ic-protobuf", "ic-registry-keys", diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index 3ee16f60c28..04d66783793 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -29,7 +29,9 @@ BASE_DEPENDENCIES = [ "//rs/rust_canisters/on_wire", "//rs/types/base_types", "//rs/types/management_canister_types", + "//rs/nns/handlers/root/impl:root", "@crate_index//:build-info", + "@crate_index//:ic-cdk-timers", "@crate_index//:candid", "@crate_index//:ic-cdk", "@crate_index//:ic-metrics-encoder", @@ -141,13 +143,13 @@ rust_canister( rust_test( name = "recovery-canister-tests", - srcs = glob(["backup/**/*.rs"]), + srcs = glob(["canister/**/*.rs"]), data = [ - ":backup-root-canister", + ":recovery-canister", "//rs/pocket_ic_server:pocket-ic-server", "//rs/registry/canister:registry-canister" ], - crate_root = "recovery/canister.rs", + crate_root = "canister/canister.rs", proc_macro_deps = MACRO_DEPENDENCIES, deps = DEPENDENCIES + DEV_DEPENDENCIES + [ ":build_script", diff --git a/rs/nns/handlers/recovery/impl/Cargo.toml b/rs/nns/handlers/recovery/impl/Cargo.toml index b9dd99edc12..0a1a0de55eb 100644 --- a/rs/nns/handlers/recovery/impl/Cargo.toml +++ b/rs/nns/handlers/recovery/impl/Cargo.toml @@ -21,6 +21,7 @@ ic-base-types = { path = "../../../../types/base_types" } ic-canisters-http-types = { path = "../../../../rust_canisters/http_types" } ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } +ic-cdk-timers = { workspace = true } ic-crypto-sha2 = { path = "../../../../crypto/sha2" } ic-management-canister-types = { path = "../../../../types/management_canister_types" } ic-metrics-encoder = "1" @@ -30,6 +31,7 @@ ic-nervous-system-common-build-metadata = { path = "../../../../nervous_system/c ic-nervous-system-proxied-canister-calls-tracker = { path = "../../../../nervous_system/proxied_canister_calls_tracker" } ic-nervous-system-root = { path = "../../../../nervous_system/root" } ic-nervous-system-runtime = { path = "../../../../nervous_system/runtime" } +ic-nns-handler-root = { path = "../../root/impl" } ic-nns-common = { path = "../../../common" } ic-nns-constants = { path = "../../../constants" } ic-protobuf = { path = "../../../../protobuf" } diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index f2f27fade8c..2060bdb8cf8 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -1,28 +1,25 @@ use ic_base_types::{PrincipalId, SubnetId}; use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; +use ic_cdk_macros::init; use ic_nervous_system_common::serve_metrics; #[cfg(target_arch = "wasm32")] use ic_cdk::println; use ic_cdk::{post_upgrade, query, update}; -use ic_nns_handler_recovery::metrics::encode_metrics; +use ic_nns_handler_recovery::{ + metrics::encode_metrics, + node_operator_sync::{get_node_operators_in_nns, sync_node_operators, SimpleNodeRecord}, +}; fn caller() -> PrincipalId { PrincipalId::from(ic_cdk::caller()) } -// canister_init and canister_post_upgrade are needed here -// to ensure that printer hook is set up, otherwise error -// messages are quite obscure. -#[export_name = "canister_init"] -fn canister_init() { - println!("canister_init"); -} - #[post_upgrade] fn canister_post_upgrade() { println!("canister_post_upgrade"); + init(); } ic_nervous_system_common_build_metadata::define_get_build_metadata_candid_method_cdk! {} @@ -61,6 +58,11 @@ fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { vec![] } +#[query] +fn get_current_nns_node_operators() -> Vec { + get_node_operators_in_nns() +} + /// Resources to serve for a given http_request /// Serve an HttpRequest made to this canister #[query(hidden = true, decoding_quota = 10000)] @@ -90,5 +92,23 @@ fn main() { #[cfg(any(target_arch = "wasm32", test))] fn main() {} +#[init] +fn init() { + ic_cdk_timers::set_timer(std::time::Duration::from_secs(0), || { + ic_cdk::spawn(setup_node_operator_update()); + }); + ic_cdk_timers::set_timer_interval(std::time::Duration::from_secs(60 * 60 * 24), || { + ic_cdk::spawn(setup_node_operator_update()); + }); +} + +async fn setup_node_operator_update() { + ic_cdk::println!("Started Sync for new node operators on NNS"); + if let Err(e) = sync_node_operators().await { + ic_cdk::println!("{}", e); + } + ic_cdk::println!("Sync completed") +} + #[cfg(test)] mod tests; diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index f87447c8aa6..4098717d11f 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -3,6 +3,7 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; +use ic_nns_handler_recovery::node_operator_sync::SimpleNodeRecord; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, routing_table::v1::RoutingTable as RoutingTablePB, @@ -229,6 +230,15 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr candid::encode_one(()).unwrap(), None, ); + + // Tick for initial sync + // 1 - fetch nns + // 1 - fetch membership + // 40 - fetch node operators for nodes + for _ in 0..42 { + pic.tick(); + } + (pic, canister) } @@ -289,6 +299,30 @@ fn vote( response } +fn get_current_node_operators(pic: &PocketIc, canister: Principal) -> Vec { + let response = pic + .query_call( + canister.into(), + Principal::anonymous(), + "get_current_nns_node_operators", + candid::encode_one(()).unwrap(), + ) + .expect("Should be able to fetch nns node operators"); + + let response = candid::decode_one(&response).expect("Should be able to decode response"); + println!("{:?}", response); + response +} + +#[test] +fn node_providers_are_synced_from_registry() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let current_node_operators = get_current_node_operators(&pic, canister); + assert!(!current_node_operators.is_empty()) +} + // #[test] // fn fetch_pending_proposals_submited_one() { // let mut args = RegistryPreparationArguments::default(); diff --git a/rs/nns/handlers/recovery/impl/src/lib.rs b/rs/nns/handlers/recovery/impl/src/lib.rs index 6c82a09356a..1e50b7ef9a5 100644 --- a/rs/nns/handlers/recovery/impl/src/lib.rs +++ b/rs/nns/handlers/recovery/impl/src/lib.rs @@ -3,6 +3,7 @@ use std::time::{Duration, SystemTime}; use ic_cdk::api::time; pub mod metrics; +pub mod node_operator_sync; pub mod update_nns_status_proposal; pub fn now_nanoseconds() -> u64 { diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs new file mode 100644 index 00000000000..46441ca88aa --- /dev/null +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -0,0 +1,44 @@ +use std::cell::RefCell; + +use candid::CandidType; +use ic_base_types::{NodeId, PrincipalId}; +use ic_nns_handler_root::root_proposals::{ + get_nns_membership, get_nns_subnet_id, get_node_operator_pid_of_node, +}; +use serde::Deserialize; + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct SimpleNodeRecord { + pub node_principal: NodeId, + pub operator_principal: PrincipalId, +} + +thread_local! { + static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +pub async fn sync_node_operators() -> Result<(), String> { + let nns_subnet_id = get_nns_subnet_id().await?; + let (nns_nodes, subnet_membership_registry_version) = + get_nns_membership(&nns_subnet_id).await?; + + let mut new_simple_records = vec![]; + + for node in nns_nodes { + let node_operator_id = + get_node_operator_pid_of_node(&node, subnet_membership_registry_version).await?; + + new_simple_records.push(SimpleNodeRecord { + node_principal: node, + operator_principal: node_operator_id, + }); + } + + NODE_OPERATORS_IN_NNS.replace(new_simple_records); + + Ok(()) +} + +pub fn get_node_operators_in_nns() -> Vec { + NODE_OPERATORS_IN_NNS.with_borrow(|records| records.clone()) +} diff --git a/rs/nns/handlers/root/impl/src/root_proposals.rs b/rs/nns/handlers/root/impl/src/root_proposals.rs index 7d7f30fa148..e96eebf0b6e 100644 --- a/rs/nns/handlers/root/impl/src/root_proposals.rs +++ b/rs/nns/handlers/root/impl/src/root_proposals.rs @@ -486,7 +486,7 @@ pub fn get_pending_root_proposals_to_upgrade_governance_canister( /// In order to get the subnet id of the NNS, we get the routing table and /// figure out which subnet has the governance canister's id. -async fn get_nns_subnet_id() -> Result { +pub async fn get_nns_subnet_id() -> Result { let routing_table = RoutingTable::try_from( get_value::(make_routing_table_record_key().as_bytes(), None) .await @@ -510,7 +510,7 @@ async fn get_nns_subnet_id() -> Result { /// Returns the membership for the nns subnetwork, and the version at which it /// was fetched. -async fn get_nns_membership(subnet_id: &SubnetId) -> Result<(Vec, u64), String> { +pub async fn get_nns_membership(subnet_id: &SubnetId) -> Result<(Vec, u64), String> { let (subnet_registry_entry, version) = get_value::(make_subnet_record_key(*subnet_id).as_bytes(), None) .await @@ -529,7 +529,7 @@ async fn get_nns_membership(subnet_id: &SubnetId) -> Result<(Vec, u64), } /// Returns the principal corresponding to the node operator of the given node. -async fn get_node_operator_pid_of_node( +pub async fn get_node_operator_pid_of_node( node_id: &NodeId, version: u64, ) -> Result { From 57ff08fc9212ad90ecf243403a8717b5232ab6a3 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 19:02:11 +0100 Subject: [PATCH 13/76] started logic for submitting new proposal --- .../recovery/impl/canister/canister.rs | 6 +- rs/nns/handlers/recovery/impl/src/lib.rs | 2 +- .../recovery/impl/src/recovery_proposal.rs | 101 ++++++++++++++++++ .../impl/src/update_nns_status_proposal.rs | 45 -------- 4 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 rs/nns/handlers/recovery/impl/src/recovery_proposal.rs delete mode 100644 rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 2060bdb8cf8..8823741b55d 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -10,6 +10,7 @@ use ic_cdk::{post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, node_operator_sync::{get_node_operators_in_nns, sync_node_operators, SimpleNodeRecord}, + recovery_proposal::{get_recovery_proposals, RecoveryProposal}, }; fn caller() -> PrincipalId { @@ -53,9 +54,8 @@ async fn vote_on_root_proposal_to_change_subnet_halt_status( } #[update(hidden = true)] -fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { - // ic_nns_handler_root::backup_root_proposals::get_pending_root_proposals_to_change_subnet_halt_status() - vec![] +fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { + get_recovery_proposals() } #[query] diff --git a/rs/nns/handlers/recovery/impl/src/lib.rs b/rs/nns/handlers/recovery/impl/src/lib.rs index 1e50b7ef9a5..2fa839285ba 100644 --- a/rs/nns/handlers/recovery/impl/src/lib.rs +++ b/rs/nns/handlers/recovery/impl/src/lib.rs @@ -4,7 +4,7 @@ use ic_cdk::api::time; pub mod metrics; pub mod node_operator_sync; -pub mod update_nns_status_proposal; +pub mod recovery_proposal; pub fn now_nanoseconds() -> u64 { if cfg!(target_arch = "wasm32") { diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs new file mode 100644 index 00000000000..9eb13198d26 --- /dev/null +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -0,0 +1,101 @@ +use std::cell::RefCell; + +use candid::CandidType; +use ic_base_types::{NodeId, PrincipalId}; +use serde::Deserialize; + +use crate::node_operator_sync::get_node_operators_in_nns; + +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] +pub enum Ballot { + Yes, + No, + Undecided, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct NodeOperatorBallot { + pub principal: PrincipalId, + pub nodes_tied_to_ballot: Vec, + pub ballot: Ballot, + pub signature: Vec, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum RecoveryPayload { + Halt, + DoRecovery { height: u64, state_hash: String }, + Unhalt, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct RecoveryProposal { + /// The principal id of the proposer (must be one of the node + /// operators of the NNS subnet according to the registry at + /// time of submission). + pub proposer: PrincipalId, + /// The timestamp, in seconds, at which the proposal was submitted. + pub submission_timestamp_seconds: u64, + /// The ballots cast by node operators. + pub node_operator_ballots: Vec, + /// Payload for the proposal + pub payload: RecoveryPayload, +} + +impl RecoveryProposal { + fn is_byzantine_majority(&self, ballot: Ballot) -> bool { + let total_nodes_nodes = self + .node_operator_ballots + .iter() + .map(|bal| bal.nodes_tied_to_ballot.len()) + .sum::(); + let max_faults = (total_nodes_nodes - 1) / 3; + let votes_for_ballot = self + .node_operator_ballots + .iter() + .map(|vote| match vote.ballot == ballot { + // Each vote has the weight of 1 times + // the amount of nodes the node operator + // has in the nns subnet + true => 1 * vote.nodes_tied_to_ballot.len(), + false => 0, + }) + .sum::(); + votes_for_ballot >= (total_nodes_nodes - max_faults) + } + + /// For a root proposal to have a byzantine majority of no, it + /// needs to collect f + 1 "no" votes, where N s the total number + /// of nodes (same as the number of ballots) and f = (N - 1) / 3. + fn is_byzantine_majority_no(&self) -> bool { + self.is_byzantine_majority(Ballot::No) + } + + /// For a root proposal to have a byzantine majority of no, it + /// needs to collect f + 1 "no" votes, where N s the total number + /// of nodes (same as the number of ballots) and f = (N - 1) / 3. + fn is_byzantine_majority_yes(&self) -> bool { + self.is_byzantine_majority(Ballot::Yes) + } +} + +thread_local! { + static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +pub struct NewRecoveryProposal { + pub payload: RecoveryPayload, + pub signature: Vec, +} + +pub fn get_recovery_proposals() -> Vec { + PROPOSALS.with_borrow(|proposals| proposals.clone()) +} + +pub fn submit_new_recovery_proposal(new_proposal: NewRecoveryProposal, caller: PrincipalId) { + let nodes_in_nns = get_node_operators_in_nns(); + + // Check if the caller has nodes in nns + + // Check if the proposal of the same type exists +} diff --git a/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs b/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs deleted file mode 100644 index a58646921aa..00000000000 --- a/rs/nns/handlers/recovery/impl/src/update_nns_status_proposal.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::cell::RefCell; - -use candid::CandidType; -use ic_base_types::{NodeId, PrincipalId}; -use serde::Deserialize; - -#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] -pub enum Ballot { - Yes, - No, - Undecided, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct NodeOperatorBallot { - pub principal: PrincipalId, - pub nodes_tied_to_ballot: Vec, - pub ballot: Ballot, - pub signature: Vec, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub enum RecoveryPayload { - Halt, - DoRecovery { height: u64, state_hash: String }, - Unhalt, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct RecoveryProposal { - /// The principal id of the proposer (must be one of the node - /// operators of the NNS subnet according to the registry at - /// time of submission). - pub proposer: PrincipalId, - /// The timestamp, in seconds, at which the proposal was submitted. - pub submission_timestamp_seconds: u64, - /// The ballots cast by node operators. - pub node_operator_ballots: Vec, - /// Payload for the proposal - pub payload: RecoveryPayload, -} - -thread_local! { - static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; -} From 2482029ce5a6381996110949de92ba921a292d96 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 22:39:45 +0100 Subject: [PATCH 14/76] refactoring proposing logic --- .../recovery/impl/canister/canister.rs | 31 +-- rs/nns/handlers/recovery/impl/src/lib.rs | 21 -- .../recovery/impl/src/recovery_proposal.rs | 190 +++++++++++++++++- 3 files changed, 196 insertions(+), 46 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 8823741b55d..ab19f43b523 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -10,7 +10,10 @@ use ic_cdk::{post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, node_operator_sync::{get_node_operators_in_nns, sync_node_operators, SimpleNodeRecord}, - recovery_proposal::{get_recovery_proposals, RecoveryProposal}, + recovery_proposal::{ + get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner, + NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal, + }, }; fn caller() -> PrincipalId { @@ -26,31 +29,15 @@ fn canister_post_upgrade() { ic_nervous_system_common_build_metadata::define_get_build_metadata_candid_method_cdk! {} #[update(hidden = true)] -async fn submit_root_proposal_to_change_subnet_halt_status( - _subnet_id: SubnetId, - _halt: bool, +async fn submit_new_recovery_proposal( + new_recovery_proposal: NewRecoveryProposal, ) -> Result<(), String> { - //TODO: Create a separate thing that polls nns for node operators and store them in memory - // If nns is down we won't be able to call registry canister - // ic_nns_handler_root::backup_root_proposals::submit_root_proposal_to_change_subnet_halt_status( - // caller(), - // subnet_id, - // halt, - // ) - // .await - Ok(()) + submit_recovery_proposal(new_recovery_proposal, caller()) } #[update(hidden = true)] -async fn vote_on_root_proposal_to_change_subnet_halt_status( - _proposer: PrincipalId, -) -> Result<(), String> { - // ic_nns_handler_root::backup_root_proposals::vote_on_root_proposal_to_change_subnet_halt_status( - // caller(), - // proposer, - // ballot, - // ) - Ok(()) +async fn vote_on_proposal(vote: VoteOnRecoveryProposal) -> Result<(), String> { + vote_on_proposal_inner(caller(), vote.ballot, vote.signature) } #[update(hidden = true)] diff --git a/rs/nns/handlers/recovery/impl/src/lib.rs b/rs/nns/handlers/recovery/impl/src/lib.rs index 2fa839285ba..899439f4963 100644 --- a/rs/nns/handlers/recovery/impl/src/lib.rs +++ b/rs/nns/handlers/recovery/impl/src/lib.rs @@ -1,24 +1,3 @@ -use std::time::{Duration, SystemTime}; - -use ic_cdk::api::time; - pub mod metrics; pub mod node_operator_sync; pub mod recovery_proposal; - -pub fn now_nanoseconds() -> u64 { - if cfg!(target_arch = "wasm32") { - time() - } else { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Failed to get time since epoch") - .as_nanos() - .try_into() - .expect("Failed to convert time to u64") - } -} - -pub fn now_seconds() -> u64 { - Duration::from_nanos(now_nanoseconds()).as_secs() -} diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 9eb13198d26..5867b11a056 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -2,9 +2,10 @@ use std::cell::RefCell; use candid::CandidType; use ic_base_types::{NodeId, PrincipalId}; +use ic_nns_handler_root::now_seconds; use serde::Deserialize; -use crate::node_operator_sync::get_node_operators_in_nns; +use crate::node_operator_sync::{get_node_operators_in_nns, SimpleNodeRecord}; #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] pub enum Ballot { @@ -83,19 +84,202 @@ thread_local! { static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; } +#[derive(Debug, CandidType, Deserialize, Clone)] pub struct NewRecoveryProposal { pub payload: RecoveryPayload, pub signature: Vec, } +#[derive(Debug, CandidType, Deserialize, Clone)] +pub struct VoteOnRecoveryProposal { + pub signature: Vec, + pub ballot: Ballot, +} + pub fn get_recovery_proposals() -> Vec { PROPOSALS.with_borrow(|proposals| proposals.clone()) } -pub fn submit_new_recovery_proposal(new_proposal: NewRecoveryProposal, caller: PrincipalId) { +pub fn submit_recovery_proposal( + new_proposal: NewRecoveryProposal, + caller: PrincipalId, +) -> Result<(), String> { let nodes_in_nns = get_node_operators_in_nns(); // Check if the caller has nodes in nns + if !nodes_in_nns + .iter() + .any(|node| node.operator_principal == caller) + { + let message = format!( + "Caller: {} is not eligible to submit proposals to this canister", + caller + ); + ic_cdk::println!("{}", message); + return Err(message); + } + + PROPOSALS.with_borrow_mut(|proposals| { + match proposals.len() { + // There is no proposals currently and the only possible proposal to be placed is + // HALT NNS Subnet + 0 => match &new_proposal.payload { + RecoveryPayload::Halt => { + proposals.push(RecoveryProposal { + proposer: caller, + submission_timestamp_seconds: now_seconds(), + node_operator_ballots: initialize_ballots_from_node_operators( + &nodes_in_nns, + ), + payload: RecoveryPayload::Halt, + }); + } + _ => { + let message = format!( + "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + ); + ic_cdk::println!("{}", message); + return Err(message); + } + }, + 1 => { + // The only possible previous proposal is a proposal to HALT NNS subnet + // Ensure that previous proposal is voted in + let first = proposals.first().expect("Must have at least one proposal"); + + // No need to check if it is a majority no because it will be removed if it is + if !first.is_byzantine_majority_yes() { + let message = format!( + "Caller {} tried to place proposal {:?} which and the previous proposal wasn't executed", caller, new_proposal + ); + ic_cdk::println!("{}", message); + return Err(message); + } + + // Its possible to either request recovery or unhalt the nns subnet if the issues + // self corrected + match &new_proposal.payload { + RecoveryPayload::DoRecovery { height: _, state_hash: _ } | RecoveryPayload::Unhalt => { + proposals.push(RecoveryProposal { + proposer: caller, + submission_timestamp_seconds: now_seconds(), + node_operator_ballots: initialize_ballots_from_node_operators(&nodes_in_nns), + payload: new_proposal.payload.clone(), + }); + }, + _ => { + let message = format!( + "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + ); + ic_cdk::println!("{}", message); + return Err(message); + } + } + }, + 2 => { + // There are two previous options: + // 1. Recovery - if this is previous proposal allow placing of the next only if it is voted in + // 2. Unhalt - if this is previous proposal don't allow placing new proposal + let second_proposal = proposals.get(1).expect("Must have at least two proposals"); + match (&second_proposal.payload, &new_proposal.payload) { + (RecoveryPayload::DoRecovery { height: _, state_hash: _ }, RecoveryPayload::Unhalt) => { + if !second_proposal.is_byzantine_majority_yes() { + let message = format!("Caller {} tried to place proposal {:?} before the outcome of proposal {:?} is decided", caller, new_proposal, second_proposal); + ic_cdk::println!("{}", message); + return Err(message); + } + proposals.push(RecoveryProposal { proposer: caller, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots_from_node_operators(&nodes_in_nns), payload: RecoveryPayload::Unhalt }); + }, + (_, _) => { + let message = format!( + "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + ); + ic_cdk::println!("{}", message); + return Err(message); + } + } + }, + 3 => { + // Already submited all three proposals. + let message = format!( + "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + ); + ic_cdk::println!("{}", message); + return Err(message); + }, + _ => unreachable!( + "There is an error in the logic since its not possible to have 3 proposals" + ), + } + vote_on_last_proposal(caller, proposals, Ballot::Yes, new_proposal.signature) + }) +} + +fn initialize_ballots_from_node_operators( + simple_node_records: &Vec, +) -> Vec { + simple_node_records + .iter() + .fold(Vec::new(), |mut acc, next| { + match acc + .iter_mut() + .find(|operator_ballot| operator_ballot.principal == next.operator_principal) + { + Some(existing_ballot) => { + existing_ballot + .nodes_tied_to_ballot + .push(next.node_principal); + } + None => acc.push(NodeOperatorBallot { + principal: next.operator_principal, + nodes_tied_to_ballot: vec![next.node_principal], + ballot: Ballot::Undecided, + signature: vec![], + }), + } + acc + }) +} + +pub fn vote_on_proposal_inner( + caller: PrincipalId, + ballot: Ballot, + signature: Vec, +) -> Result<(), String> { + PROPOSALS + .with_borrow_mut(|proposals| vote_on_last_proposal(caller, proposals, ballot, signature)) +} + +fn vote_on_last_proposal( + caller: PrincipalId, + proposals: &mut Vec, + ballot: Ballot, + signature: Vec, +) -> Result<(), String> { + let last_proposal = proposals + .last_mut() + .ok_or(format!("There are no proposals"))?; + + let correlated_ballot = last_proposal + .node_operator_ballots + .iter_mut() + .find(|ballot| ballot.principal.eq(&caller)) + .ok_or(format!( + "Caller {} is not eligible to vote on this proposal", + caller + ))?; + + if correlated_ballot.ballot != Ballot::Undecided { + return Err("Vote already submitted".to_string()); + } + + correlated_ballot.ballot = ballot; + correlated_ballot.signature = signature; + + // If the outcome is no, remove this proposal + if last_proposal.is_byzantine_majority_no() { + proposals.pop(); + } - // Check if the proposal of the same type exists + Ok(()) } From bd9eaf8b1ce8683affaec77c251fb5053403a546 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 22:53:04 +0100 Subject: [PATCH 15/76] fixing formatting --- .../recovery/impl/src/recovery_proposal.rs | 94 +++++++++++-------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 5867b11a056..bd055e6f757 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -121,27 +121,28 @@ pub fn submit_recovery_proposal( PROPOSALS.with_borrow_mut(|proposals| { match proposals.len() { - // There is no proposals currently and the only possible proposal to be placed is - // HALT NNS Subnet - 0 => match &new_proposal.payload { - RecoveryPayload::Halt => { - proposals.push(RecoveryProposal { - proposer: caller, - submission_timestamp_seconds: now_seconds(), - node_operator_ballots: initialize_ballots_from_node_operators( - &nodes_in_nns, - ), - payload: RecoveryPayload::Halt, - }); - } - _ => { - let message = format!( - "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal - ); - ic_cdk::println!("{}", message); - return Err(message); + 0 => { + // There is no proposals currently and the only possible proposal to be placed is + // HALT NNS Subnet + match &new_proposal.payload { + RecoveryPayload::Halt => { + proposals.push(RecoveryProposal { + proposer: caller, + submission_timestamp_seconds: now_seconds(), + node_operator_ballots: initialize_ballots(&nodes_in_nns), + payload: RecoveryPayload::Halt, + }); + } + _ => { + let message = format!( + "Caller {} tried to place proposal {:?} which is currently not allowed", + caller, new_proposal + ); + ic_cdk::println!("{}", message); + return Err(message); + } } - }, + } 1 => { // The only possible previous proposal is a proposal to HALT NNS subnet // Ensure that previous proposal is voted in @@ -149,9 +150,7 @@ pub fn submit_recovery_proposal( // No need to check if it is a majority no because it will be removed if it is if !first.is_byzantine_majority_yes() { - let message = format!( - "Caller {} tried to place proposal {:?} which and the previous proposal wasn't executed", caller, new_proposal - ); + let message = format!("Can't submit a proposal until the previous is decided"); ic_cdk::println!("{}", message); return Err(message); } @@ -159,54 +158,73 @@ pub fn submit_recovery_proposal( // Its possible to either request recovery or unhalt the nns subnet if the issues // self corrected match &new_proposal.payload { - RecoveryPayload::DoRecovery { height: _, state_hash: _ } | RecoveryPayload::Unhalt => { + RecoveryPayload::DoRecovery { + height: _, + state_hash: _, + } + | RecoveryPayload::Unhalt => { proposals.push(RecoveryProposal { proposer: caller, submission_timestamp_seconds: now_seconds(), - node_operator_ballots: initialize_ballots_from_node_operators(&nodes_in_nns), + node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: new_proposal.payload.clone(), }); - }, + } _ => { let message = format!( - "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + "Caller {} tried to place proposal {:?} which is currently not allowed", + caller, new_proposal ); ic_cdk::println!("{}", message); return Err(message); } } - }, + } 2 => { // There are two previous options: // 1. Recovery - if this is previous proposal allow placing of the next only if it is voted in // 2. Unhalt - if this is previous proposal don't allow placing new proposal let second_proposal = proposals.get(1).expect("Must have at least two proposals"); match (&second_proposal.payload, &new_proposal.payload) { - (RecoveryPayload::DoRecovery { height: _, state_hash: _ }, RecoveryPayload::Unhalt) => { + ( + RecoveryPayload::DoRecovery { + height: _, + state_hash: _, + }, + RecoveryPayload::Unhalt, + ) => { if !second_proposal.is_byzantine_majority_yes() { - let message = format!("Caller {} tried to place proposal {:?} before the outcome of proposal {:?} is decided", caller, new_proposal, second_proposal); + let message = + format!("Can't submit a proposal until the previous is decided"); ic_cdk::println!("{}", message); return Err(message); } - proposals.push(RecoveryProposal { proposer: caller, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots_from_node_operators(&nodes_in_nns), payload: RecoveryPayload::Unhalt }); - }, + proposals.push(RecoveryProposal { + proposer: caller, + submission_timestamp_seconds: now_seconds(), + node_operator_ballots: initialize_ballots(&nodes_in_nns), + payload: RecoveryPayload::Unhalt, + }); + } (_, _) => { let message = format!( - "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + "Caller {} tried to place proposal {:?} which is currently not allowed", + caller, new_proposal ); ic_cdk::println!("{}", message); return Err(message); } } - }, + } 3 => { // Already submited all three proposals. let message = format!( - "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal + "Caller {} tried to place proposal {:?} which is currently not allowed", + caller, new_proposal ); ic_cdk::println!("{}", message); return Err(message); - }, + } _ => unreachable!( "There is an error in the logic since its not possible to have 3 proposals" ), @@ -215,9 +233,7 @@ pub fn submit_recovery_proposal( }) } -fn initialize_ballots_from_node_operators( - simple_node_records: &Vec, -) -> Vec { +fn initialize_ballots(simple_node_records: &Vec) -> Vec { simple_node_records .iter() .fold(Vec::new(), |mut acc, next| { From 0701b65f12c5f97d4fd1c4af4dce4bad1586f7db Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 3 Feb 2025 23:53:53 +0100 Subject: [PATCH 16/76] adding tests for first proposal --- .../recovery/impl/canister/canister.rs | 4 +- .../recovery/impl/canister/tests/mod.rs | 137 +++--------- .../tests/node_providers_sync_tests.rs | 10 + .../canister/tests/proposal_logic_tests.rs | 202 ++++++++++++++++++ .../impl/canister/tests/voting_tests.rs | 0 5 files changed, 248 insertions(+), 105 deletions(-) create mode 100644 rs/nns/handlers/recovery/impl/canister/tests/node_providers_sync_tests.rs create mode 100644 rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs create mode 100644 rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index ab19f43b523..2b2644cd575 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -1,4 +1,4 @@ -use ic_base_types::{PrincipalId, SubnetId}; +use ic_base_types::PrincipalId; use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_cdk_macros::init; use ic_nervous_system_common::serve_metrics; @@ -41,7 +41,7 @@ async fn vote_on_proposal(vote: VoteOnRecoveryProposal) -> Result<(), String> { } #[update(hidden = true)] -fn get_pending_root_proposals_to_change_subnet_halt_status() -> Vec { +fn get_pending_recovery_proposals() -> Vec { get_recovery_proposals() } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 4098717d11f..de0ed76a117 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -3,7 +3,10 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; -use ic_nns_handler_recovery::node_operator_sync::SimpleNodeRecord; +use ic_nns_handler_recovery::{ + node_operator_sync::SimpleNodeRecord, + recovery_proposal::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, +}; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, routing_table::v1::RoutingTable as RoutingTablePB, @@ -27,7 +30,10 @@ use test_helpers::{ prepare_registry_with_nodes_and_node_operator_id, }; +mod node_providers_sync_tests; +mod proposal_logic_tests; mod test_helpers; +mod voting_tests; fn fetch_canister_wasm(env: &str) -> Vec { let path: PathBuf = std::env::var(env) @@ -235,7 +241,13 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr // 1 - fetch nns // 1 - fetch membership // 40 - fetch node operators for nodes - for _ in 0..42 { + let ticks = arguments + .subnet_node_operators + .iter() + .filter(|subnet| subnet.subnet_type.eq(&SubnetType::System)) + .map(|subnet_arg| subnet_arg.node_operators.values().sum::()) + .sum::(); + for _ in 0..(ticks + 2) { pic.tick(); } @@ -246,32 +258,30 @@ fn submit_proposal( pic: &PocketIc, canister: Principal, sender: Principal, - subnet_id: Principal, - to_halt: bool, + arg: NewRecoveryProposal, ) -> Result<(), String> { let response = pic.update_call( canister.into(), sender, - "submit_root_proposal_to_change_subnet_halt_status", - candid::encode_args((subnet_id, to_halt)).unwrap(), + "submit_new_recovery_proposal", + candid::encode_one(arg).unwrap(), ); let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); println!("{:?}", response); response } -fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { +fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { let response = pic .update_call( canister.into(), Principal::anonymous(), - "get_pending_root_proposals_to_change_subnet_halt_status", + "get_pending_recovery_proposals", candid::encode_one(()).unwrap(), ) .expect("Should be able to fetch remaining proposals"); - let response: Vec = - candid::decode_one(&response).expect("Should be able to decode response"); + let response = candid::decode_one(&response).expect("Should be able to decode response"); println!("{:?}", response); response @@ -281,15 +291,14 @@ fn vote( pic: &PocketIc, canister: Principal, sender: Principal, - proposer: PrincipalId, - ballot: u8, + arg: VoteOnRecoveryProposal, ) -> Result<(), String> { let response = pic .update_call( canister.into(), sender, - "vote_on_root_proposal_to_change_subnet_halt_status", - candid::encode_args((proposer, ballot)).unwrap(), + "vote_on_proposal", + candid::encode_one(arg).unwrap(), ) .expect("Should be able to call vote function"); @@ -314,93 +323,15 @@ fn get_current_node_operators(pic: &PocketIc, canister: Principal) -> Vec BTreeMap { + arguments + .subnet_node_operators + .iter() + .find_map(|subnet| match subnet.subnet_type.eq(&SubnetType::System) { + false => None, + true => Some(subnet.node_operators.clone()), + }) + .unwrap() } - -// #[test] -// fn fetch_pending_proposals_submited_one() { -// let mut args = RegistryPreparationArguments::default(); -// let (pic, canister) = init_pocket_ic(&mut args); - -// let nns = pic.topology().get_nns().unwrap(); -// let no_in_subnet = args -// .subnet_node_operators -// .iter() -// .find_map(|arg| match arg.subnet_id.0 == nns { -// true => { -// let operator_principals = arg -// .node_operators -// .iter() -// .map(|(principal, _)| principal) -// .collect::>(); - -// operator_principals.first().cloned() -// } -// false => None, -// }) -// .expect("Should be able to find subnet and a node operator with nodes in it"); - -// let response = submit_proposal(&pic, canister, no_in_subnet.0.clone(), nns, true); -// assert!(response.is_ok()); - -// let response = get_pending(&pic, canister); - -// assert!(response.len() == 1); -// let proposal = response.first().unwrap(); - -// let node_operators_in_subnet = args -// .subnet_node_operators -// .iter() -// .find_map(|arg| { -// if arg.subnet_id.0 == nns { -// Some(arg.node_operators.clone()) -// } else { -// None -// } -// }) -// .expect("Should find the corresponding number of node operators"); - -// let expected_ballots: u8 = node_operators_in_subnet.values().sum(); -// assert_eq!( -// proposal.node_operator_ballots.len(), -// expected_ballots as usize, -// "Received:\n{:?}\nExpected (key * value):\n{:?}", -// proposal.node_operator_ballots, -// node_operators_in_subnet -// ); -// assert!(proposal.proposer.eq(no_in_subnet)); - -// let voted_yes: Vec<_> = proposal -// .node_operator_ballots -// .iter() -// .filter(|(_, ballot)| { -// ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Yes) -// }) -// .collect(); - -// let (no_principal, _) = voted_yes.first().unwrap(); -// assert_eq!(no_principal, no_in_subnet); -// assert_eq!( -// voted_yes.len(), -// *node_operators_in_subnet.get(no_in_subnet).unwrap() as usize -// ); - -// let voted_undecided: Vec<_> = proposal -// .node_operator_ballots -// .iter() -// .filter(|(_, ballot)| { -// ballot.eq(&ic_nns_handler_root::root_proposals::RootProposalBallot::Undecided) -// }) -// .collect(); -// // All others still didn't vote since its just been proposed -// assert_eq!( -// voted_undecided.len() as u8, -// expected_ballots - voted_yes.len() as u8 -// ); -// } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/node_providers_sync_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/node_providers_sync_tests.rs new file mode 100644 index 00000000000..d3d621c513a --- /dev/null +++ b/rs/nns/handlers/recovery/impl/canister/tests/node_providers_sync_tests.rs @@ -0,0 +1,10 @@ +use crate::tests::{get_current_node_operators, init_pocket_ic, RegistryPreparationArguments}; + +#[test] +fn node_providers_are_synced_from_registry() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let current_node_operators = get_current_node_operators(&pic, canister); + assert!(!current_node_operators.is_empty()) +} diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs new file mode 100644 index 00000000000..5ba7933e0ef --- /dev/null +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -0,0 +1,202 @@ +use candid::Principal; +use ic_nns_handler_recovery::recovery_proposal::{ + Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, +}; +use ic_registry_subnet_type::SubnetType; + +use crate::tests::{get_pending, vote}; + +use super::{ + extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, + RegistryPreparationArguments, +}; + +// First proposal tests + +#[test] +fn place_first_proposal() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let first = node_operators.keys().next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + + assert!(response.is_ok()); + + let pending_proposals = get_pending(&pic, canister); + + assert!(pending_proposals.len().eq(&1)); + let only_proposal = pending_proposals.first().unwrap(); + + let registered_votes: Vec<_> = only_proposal + .node_operator_ballots + .iter() + .filter(|ballot| ballot.ballot != Ballot::Undecided) + .collect(); + + assert!(registered_votes.len().eq(&1)); + + let only_vote = registered_votes.first().unwrap(); + assert_eq!(only_vote.ballot, Ballot::Yes); + + let number_of_nodes_for_first = node_operators.get(first).unwrap(); + assert!(only_vote + .nodes_tied_to_ballot + .len() + .eq(&(*number_of_nodes_for_first as usize))); + assert_eq!(&only_vote.principal, first) +} + +#[test] +fn place_non_halt_first_proposal() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let first = node_operators.keys().next().unwrap(); + + let invalid_first_proposals = vec![ + NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + signature: "Not important yet".as_bytes().to_vec(), + }, + NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ]; + + for proposal in invalid_first_proposals { + let response = submit_proposal(&pic, canister, first.0.clone(), proposal); + + assert!(response.is_err()); + let pending_proposals = get_pending(&pic, canister); + assert!(pending_proposals.is_empty()); + } +} + +#[test] +fn replace_first_proposal_after_voting_no_on_the_first() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_ok()); + + // Try resubmitting + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_err()); + + // To achieve byzantine majority in default setup 7 + // node operators should vote "no" + // This will remove the proposal + for _ in 0..7 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::No, + }, + ); + + assert!(response.is_ok()); + } + + // Resubmit + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_ok()); +} + +#[test] +fn disallow_unknown_node_operators_from_placing_proposals() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let response = submit_proposal( + &pic, + canister, + Principal::anonymous(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_err()); +} + +#[test] +fn disallow_node_operators_from_different_subnets_from_placing_proposals() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let non_system_subnet = args + .subnet_node_operators + .iter() + .find_map(|subnet| match !subnet.subnet_type.eq(&SubnetType::System) { + false => None, + true => Some(subnet.node_operators.clone()), + }) + .unwrap(); + let first_node_operator = non_system_subnet.keys().next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first_node_operator.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_err()); +} +// Second proposal tests + +// Third proposal tests + +// Nth proposal tests diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs new file mode 100644 index 00000000000..e69de29bb2d From 811d91181ceb4e682442421f7638291954682d3f Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 01:20:39 +0100 Subject: [PATCH 17/76] adding tests for second proposal --- .../canister/tests/proposal_logic_tests.rs | 238 ++++++++++++++++++ .../recovery/impl/src/recovery_proposal.rs | 10 +- 2 files changed, 245 insertions(+), 3 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 5ba7933e0ef..b64a046fa44 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -3,6 +3,7 @@ use ic_nns_handler_recovery::recovery_proposal::{ Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, }; use ic_registry_subnet_type::SubnetType; +use pocket_ic::PocketIc; use crate::tests::{get_pending, vote}; @@ -195,7 +196,244 @@ fn disallow_node_operators_from_different_subnets_from_placing_proposals() { ); assert!(response.is_err()); } + // Second proposal tests +fn place_and_execute_first_proposal( + args: &mut RegistryPreparationArguments, +) -> (PocketIc, Principal) { + let (pic, canister) = init_pocket_ic(args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_ok()); + + // To achieve byzantine majority in default setup 7 + // node operators should vote "Yes" + // The first one voted "Yes" when submitting the proposal + // which leaves room for 6 more + for _ in 0..6 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + + assert!(response.is_ok()); + } + + let pending_proposals = get_pending(&pic, canister); + let first_proposal = pending_proposals.first().unwrap(); + assert!(first_proposal.is_byzantine_majority_yes()); + (pic, canister) +} + +#[test] +fn place_second_proposal_recovery() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + let pending_proposals = get_pending(&pic, canister); + let latest_proposal = pending_proposals.last().unwrap(); + assert!(latest_proposal.payload.eq(&new_proposal.payload)); + assert!( + !latest_proposal.is_byzantine_majority_no() && !latest_proposal.is_byzantine_majority_yes() + ) +} + +#[test] +fn place_second_proposal_unhalt() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + let pending_proposals = get_pending(&pic, canister); + let latest_proposal = pending_proposals.last().unwrap(); + assert!(latest_proposal.payload.eq(&new_proposal.payload)); + assert!( + !latest_proposal.is_byzantine_majority_no() && !latest_proposal.is_byzantine_majority_yes() + ) +} + +#[test] +fn place_second_proposal_halt() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_err()); +} + +#[test] +fn second_proposal_vote_against() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the second + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 7 votes to vote against this + for _ in 0..7 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::No, + }, + ); + assert!(response.is_ok()) + } + + let pending = get_pending(&pic, canister); + assert!(pending.len().eq(&1)) +} + +#[test] +fn second_proposal_recovery_vote_in() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the second + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 6 more to vote in + for _ in 0..6 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_ok()) + } + + let pending = get_pending(&pic, canister); + assert!(pending.len().eq(&2)); + let latest_proposal = pending.last().unwrap(); + assert!(latest_proposal.is_byzantine_majority_yes()) +} + +#[test] +fn second_proposal_unhalt_vote_in() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the second + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 6 more to vote in + for _ in 0..6 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_ok()) + } + + let pending = get_pending(&pic, canister); + assert!(pending.is_empty()); +} // Third proposal tests diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index bd055e6f757..b4aa9af2517 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -22,7 +22,7 @@ pub struct NodeOperatorBallot { pub signature: Vec, } -#[derive(Clone, Debug, CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub enum RecoveryPayload { Halt, DoRecovery { height: u64, state_hash: String }, @@ -68,14 +68,14 @@ impl RecoveryProposal { /// For a root proposal to have a byzantine majority of no, it /// needs to collect f + 1 "no" votes, where N s the total number /// of nodes (same as the number of ballots) and f = (N - 1) / 3. - fn is_byzantine_majority_no(&self) -> bool { + pub fn is_byzantine_majority_no(&self) -> bool { self.is_byzantine_majority(Ballot::No) } /// For a root proposal to have a byzantine majority of no, it /// needs to collect f + 1 "no" votes, where N s the total number /// of nodes (same as the number of ballots) and f = (N - 1) / 3. - fn is_byzantine_majority_yes(&self) -> bool { + pub fn is_byzantine_majority_yes(&self) -> bool { self.is_byzantine_majority(Ballot::Yes) } } @@ -295,6 +295,10 @@ fn vote_on_last_proposal( // If the outcome is no, remove this proposal if last_proposal.is_byzantine_majority_no() { proposals.pop(); + } else if last_proposal.is_byzantine_majority_yes() { + if let RecoveryPayload::Unhalt = last_proposal.payload { + proposals.clear(); + } } Ok(()) From 3a892c3f8b9612f75fdc21dd64c6be454aec4351 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 01:44:51 +0100 Subject: [PATCH 18/76] adding tests for the third proposal --- .../canister/tests/proposal_logic_tests.rs | 171 ++++++++++++++++++ .../impl/canister/tests/voting_tests.rs | 3 + .../recovery/impl/src/recovery_proposal.rs | 2 +- 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index b64a046fa44..e8e01bc549b 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -436,5 +436,176 @@ fn second_proposal_unhalt_vote_in() { } // Third proposal tests +#[test] +fn submit_first_two_second_not_voted_in_place_third() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the second + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // Place the third + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_err()); +} + +fn place_and_execute_second_proposal( + args: &mut RegistryPreparationArguments, +) -> (PocketIc, Principal) { + let (pic, canister) = place_and_execute_first_proposal(args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the second + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 6 more to vote in + for _ in 0..6 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_ok()) + } + + let pending = get_pending(&pic, canister); + assert!(pending.len().eq(&2)); + let latest = pending.last().unwrap(); + assert!(latest.is_byzantine_majority_yes()); + (pic, canister) +} + +#[test] +fn submit_first_two_second_voted_in_place_third() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_second_proposal(&mut args); + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the third + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + let pending = get_pending(&pic, canister); + assert!(pending.len().eq(&3)); + let latest = pending.last().unwrap(); + assert!(!latest.is_byzantine_majority_no() && !latest.is_byzantine_majority_yes()) +} + +#[test] +fn vote_against_last_proposal() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_second_proposal(&mut args); + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the third + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 7 votes to vote against this proposal + for _ in 0..7 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::No, + }, + ); + + assert!(response.is_ok()) + } + + let pending = get_pending(&pic, canister); + assert!(pending.len().eq(&2)); + let latest = pending.last().unwrap(); + // Poping the 3rd proposal doesn't affect the 2nd + assert!(latest.is_byzantine_majority_yes()); +} + +#[test] +fn vote_in_last_proposal() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_second_proposal(&mut args); + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the third + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 6 votes to vote for this proposal + for _ in 0..6 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + + assert!(response.is_ok()) + } + + // Reset back to the initial state + let pending = get_pending(&pic, canister); + assert!(pending.is_empty()); +} // Nth proposal tests diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index e69de29bb2d..f44ff34da5d 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -0,0 +1,3 @@ +// Disallow double vote +// Disallow vote from anonymous +// Allow all node operators to vote even if already executed diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index b4aa9af2517..b7405c5ae13 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -226,7 +226,7 @@ pub fn submit_recovery_proposal( return Err(message); } _ => unreachable!( - "There is an error in the logic since its not possible to have 3 proposals" + "There is an error in the logic since its not possible to have more than 3 proposals" ), } vote_on_last_proposal(caller, proposals, Ballot::Yes, new_proposal.signature) From 95cf990bc7a84c6bd644f76402b54da99b0fd57d Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 01:50:11 +0100 Subject: [PATCH 19/76] tests for nth proposal --- .../canister/tests/proposal_logic_tests.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index e8e01bc549b..7654abc199b 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -609,3 +609,41 @@ fn vote_in_last_proposal() { } // Nth proposal tests +#[test] +fn place_any_proposal_after_there_are_three() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_second_proposal(&mut args); + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the third + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::Unhalt, + signature: "Not important yet".as_bytes().to_vec(), + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + let payloads = vec![ + RecoveryPayload::Halt, + RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + RecoveryPayload::Unhalt, + ]; + for payload in payloads { + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + + assert!(response.is_err()) + } +} From b044c6931e53b7f1d554e373cc222c7d43a404b4 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 01:59:24 +0100 Subject: [PATCH 20/76] adding voting tests --- .../impl/canister/tests/voting_tests.rs | 125 +++++++++++++++++- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index f44ff34da5d..2ec6b2e685d 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,3 +1,122 @@ -// Disallow double vote -// Disallow vote from anonymous -// Allow all node operators to vote even if already executed +use candid::Principal; +use ic_nns_handler_recovery::recovery_proposal::{ + Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, +}; + +use crate::tests::{ + extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote, + RegistryPreparationArguments, +}; + +#[test] +fn disallow_double_vote() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + + assert!(response.is_ok()); + + let second = node_operators_iterator.next().unwrap(); + let response = vote( + &pic, + canister, + second.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_ok()); + + let response = vote( + &pic, + canister, + second.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_err()); +} + +#[test] +fn disallow_vote_anonymous() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + + assert!(response.is_ok()); + + let response = vote( + &pic, + canister, + Principal::anonymous(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_err()); +} + +#[test] +fn allow_votes_even_if_executed() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + signature: "Not important yet".as_bytes().to_vec(), + }, + ); + + assert!(response.is_ok()); + + for no in node_operators_iterator { + let response = vote( + &pic, + canister, + no.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_ok()); + } +} From 5864e44419924fa4fcaef228c986c0794ed80fef Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 11:39:25 +0100 Subject: [PATCH 21/76] adding todo --- .../recovery/impl/canister/tests/proposal_logic_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 7654abc199b..0bca70b5a0f 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -13,7 +13,7 @@ use super::{ }; // First proposal tests - +// TODO: Allow to place multiple recover subnets proposals #[test] fn place_first_proposal() { let mut args = RegistryPreparationArguments::default(); From 02c43a4d0b8e87f741cb1a528e861e43822ea6e1 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 13:01:12 +0100 Subject: [PATCH 22/76] extracting placing proposal and voting logic --- .../canister/tests/proposal_logic_tests.rs | 67 +++++++++---------- .../impl/canister/tests/voting_tests.rs | 7 +- .../recovery/impl/src/recovery_proposal.rs | 5 +- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 0bca70b5a0f..184f23cb5a4 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -28,12 +28,23 @@ fn place_first_proposal() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()); + let response = vote( + &pic, + canister, + first.0.clone(), + VoteOnRecoveryProposal { + payload: "Not important yet".as_bytes().to_vec(), + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_ok()); + let pending_proposals = get_pending(&pic, canister); assert!(pending_proposals.len().eq(&1)); @@ -72,11 +83,9 @@ fn place_non_halt_first_proposal() { height: 123, state_hash: "123".to_string(), }, - signature: "Not important yet".as_bytes().to_vec(), }, NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }, ]; @@ -104,7 +113,6 @@ fn replace_first_proposal_after_voting_no_on_the_first() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()); @@ -116,7 +124,6 @@ fn replace_first_proposal_after_voting_no_on_the_first() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_err()); @@ -133,6 +140,7 @@ fn replace_first_proposal_after_voting_no_on_the_first() { next.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::No, }, ); @@ -147,7 +155,6 @@ fn replace_first_proposal_after_voting_no_on_the_first() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()); @@ -164,7 +171,6 @@ fn disallow_unknown_node_operators_from_placing_proposals() { Principal::anonymous(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_err()); @@ -191,7 +197,6 @@ fn disallow_node_operators_from_different_subnets_from_placing_proposals() { first_node_operator.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_err()); @@ -213,16 +218,13 @@ fn place_and_execute_first_proposal( first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()); // To achieve byzantine majority in default setup 7 // node operators should vote "Yes" - // The first one voted "Yes" when submitting the proposal - // which leaves room for 6 more - for _ in 0..6 { + for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); let response = vote( @@ -232,6 +234,7 @@ fn place_and_execute_first_proposal( VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, + payload: "Not important yet".as_bytes().to_vec(), }, ); @@ -258,7 +261,6 @@ fn place_second_proposal_recovery() { height: 123, state_hash: "123".to_string(), }, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -282,7 +284,6 @@ fn place_second_proposal_unhalt() { let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -310,7 +311,6 @@ fn place_second_proposal_halt() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_err()); @@ -331,7 +331,6 @@ fn second_proposal_vote_against() { height: 123, state_hash: "123".to_string(), }, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -347,6 +346,7 @@ fn second_proposal_vote_against() { VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), ballot: Ballot::No, + payload: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()) @@ -371,13 +371,12 @@ fn second_proposal_recovery_vote_in() { height: 123, state_hash: "123".to_string(), }, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); - // We need 6 more to vote in - for _ in 0..6 { + // We need 7 to vote in + for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); let response = vote( @@ -387,6 +386,7 @@ fn second_proposal_recovery_vote_in() { VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, + payload: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()) @@ -410,13 +410,12 @@ fn second_proposal_unhalt_vote_in() { // Place the second let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); - // We need 6 more to vote in - for _ in 0..6 { + // We need 7 to vote it in + for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); let response = vote( @@ -426,6 +425,7 @@ fn second_proposal_unhalt_vote_in() { VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, + payload: "Not important yet".as_bytes().to_vec(), }, ); assert!(response.is_ok()) @@ -451,7 +451,6 @@ fn submit_first_two_second_not_voted_in_place_third() { height: 123, state_hash: "123".to_string(), }, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -459,7 +458,6 @@ fn submit_first_two_second_not_voted_in_place_third() { // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_err()); @@ -480,13 +478,12 @@ fn place_and_execute_second_proposal( height: 123, state_hash: "123".to_string(), }, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); - // We need 6 more to vote in - for _ in 0..6 { + // We need 7 to vote it in + for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); let response = vote( @@ -495,6 +492,7 @@ fn place_and_execute_second_proposal( next.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, }, ); @@ -519,7 +517,6 @@ fn submit_first_two_second_voted_in_place_third() { // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -541,7 +538,6 @@ fn vote_against_last_proposal() { // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -556,6 +552,7 @@ fn vote_against_last_proposal() { next.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::No, }, ); @@ -581,13 +578,12 @@ fn vote_in_last_proposal() { // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); - // We need 6 votes to vote for this proposal - for _ in 0..6 { + // We need 7 votes to vote for this proposal + for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); let response = vote( @@ -596,6 +592,7 @@ fn vote_in_last_proposal() { next.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, }, ); @@ -620,7 +617,6 @@ fn place_any_proposal_after_there_are_three() { // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, - signature: "Not important yet".as_bytes().to_vec(), }; let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); assert!(response.is_ok()); @@ -638,10 +634,7 @@ fn place_any_proposal_after_there_are_three() { &pic, canister, first.0.clone(), - NewRecoveryProposal { - payload, - signature: "Not important yet".as_bytes().to_vec(), - }, + NewRecoveryProposal { payload }, ); assert!(response.is_err()) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 2ec6b2e685d..d410fa36688 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -23,7 +23,6 @@ fn disallow_double_vote() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); @@ -36,6 +35,7 @@ fn disallow_double_vote() { second.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, }, ); @@ -47,6 +47,7 @@ fn disallow_double_vote() { second.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, }, ); @@ -68,7 +69,6 @@ fn disallow_vote_anonymous() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); @@ -80,6 +80,7 @@ fn disallow_vote_anonymous() { Principal::anonymous(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, }, ); @@ -101,7 +102,6 @@ fn allow_votes_even_if_executed() { first.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, - signature: "Not important yet".as_bytes().to_vec(), }, ); @@ -114,6 +114,7 @@ fn allow_votes_even_if_executed() { no.0.clone(), VoteOnRecoveryProposal { signature: "Not important yet".as_bytes().to_vec(), + payload: "Not important yet".as_bytes().to_vec(), ballot: Ballot::Yes, }, ); diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index b7405c5ae13..709eaf4bec7 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -87,12 +87,13 @@ thread_local! { #[derive(Debug, CandidType, Deserialize, Clone)] pub struct NewRecoveryProposal { pub payload: RecoveryPayload, - pub signature: Vec, } #[derive(Debug, CandidType, Deserialize, Clone)] pub struct VoteOnRecoveryProposal { + pub payload: Vec, pub signature: Vec, + pub ballot: Ballot, } @@ -229,7 +230,7 @@ pub fn submit_recovery_proposal( "There is an error in the logic since its not possible to have more than 3 proposals" ), } - vote_on_last_proposal(caller, proposals, Ballot::Yes, new_proposal.signature) + Ok(()) }) } From 5c5b2e18546d565c0d4611f0b1fca9629b90346c Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 13:14:12 +0100 Subject: [PATCH 23/76] allowing submitting multiple recovery proposals --- .../canister/tests/proposal_logic_tests.rs | 58 +++++++++++++++++++ .../recovery/impl/src/recovery_proposal.rs | 23 +++++--- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 184f23cb5a4..6ccf3f3c3cc 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -398,6 +398,64 @@ fn second_proposal_recovery_vote_in() { assert!(latest_proposal.is_byzantine_majority_yes()) } +#[test] +fn second_proposal_recovery_vote_in_and_resubmit() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = place_and_execute_first_proposal(&mut args); + + let node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.keys(); + let first = node_operators_iterator.next().unwrap(); + + // Place the second + let new_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + }, + }; + let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + assert!(response.is_ok()); + + // We need 7 to vote in + for _ in 0..7 { + let next = node_operators_iterator.next().unwrap(); + + let response = vote( + &pic, + canister, + next.0.clone(), + VoteOnRecoveryProposal { + signature: "Not important yet".as_bytes().to_vec(), + ballot: Ballot::Yes, + payload: "Not important yet".as_bytes().to_vec(), + }, + ); + assert!(response.is_ok()) + } + + let resubmitted_proposal = NewRecoveryProposal { + payload: RecoveryPayload::DoRecovery { + height: 456, + state_hash: "456".to_string(), + }, + }; + let response = submit_proposal( + &pic, + canister, + first.0.clone(), + resubmitted_proposal.clone(), + ); + assert!(response.is_ok()); + + let pending = get_pending(&pic, canister); + assert!(pending.len().eq(&2)); + + let last = pending.last().unwrap(); + assert!(!last.is_byzantine_majority_no() && !last.is_byzantine_majority_yes()); + assert_eq!(last.payload, resubmitted_proposal.payload) +} + #[test] fn second_proposal_unhalt_vote_in() { let mut args = RegistryPreparationArguments::default(); diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 709eaf4bec7..5928ae0bc38 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -186,6 +186,12 @@ pub fn submit_recovery_proposal( // 1. Recovery - if this is previous proposal allow placing of the next only if it is voted in // 2. Unhalt - if this is previous proposal don't allow placing new proposal let second_proposal = proposals.get(1).expect("Must have at least two proposals"); + if !second_proposal.is_byzantine_majority_yes() { + let message = + format!("Can't submit a proposal until the previous is decided"); + ic_cdk::println!("{}", message); + return Err(message); + } match (&second_proposal.payload, &new_proposal.payload) { ( RecoveryPayload::DoRecovery { @@ -194,19 +200,22 @@ pub fn submit_recovery_proposal( }, RecoveryPayload::Unhalt, ) => { - if !second_proposal.is_byzantine_majority_yes() { - let message = - format!("Can't submit a proposal until the previous is decided"); - ic_cdk::println!("{}", message); - return Err(message); - } proposals.push(RecoveryProposal { proposer: caller, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: RecoveryPayload::Unhalt, }); - } + }, + // Allow submitting a new recovery proposal only if the current one + // is voted in. This could happen if the recovery from this proposal + // failed and we need to submit a new one with different args. + (RecoveryPayload::DoRecovery { height: _, state_hash: _ }, RecoveryPayload::DoRecovery { height: _, state_hash: _ }) => { + // Remove the second_one + proposals.pop(); + + proposals.push(RecoveryProposal { proposer: caller, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: new_proposal.payload.clone() }); + }, (_, _) => { let message = format!( "Caller {} tried to place proposal {:?} which is currently not allowed", From db4f0b63fa1e40a4bc76bf9bb62576b6036b3367 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 15:46:14 +0100 Subject: [PATCH 24/76] adding signing of payload --- Cargo.lock | 3 + rs/nns/handlers/recovery/impl/BUILD.bazel | 3 + rs/nns/handlers/recovery/impl/Cargo.toml | 3 + .../recovery/impl/canister/canister.rs | 2 +- .../recovery/impl/canister/tests/mod.rs | 99 ++++-- .../canister/tests/proposal_logic_tests.rs | 288 ++++++++---------- .../impl/canister/tests/voting_tests.rs | 73 ++--- .../recovery/impl/src/recovery_proposal.rs | 81 ++++- 8 files changed, 304 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e22cc3ba295..460d4787bd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10811,6 +10811,7 @@ dependencies = [ "candid_parser", "canister-test", "dfn_candid", + "ed25519-dalek", "hex", "ic-base-types", "ic-canisters-http-types", @@ -10840,12 +10841,14 @@ dependencies = [ "ic-test-utilities-compare-dirs", "ic-test-utilities-load-wasm", "ic-types", + "itertools 0.12.1", "lazy_static", "maplit", "on_wire", "pocket-ic", "pretty_assertions", "prost 0.13.4", + "rand 0.8.5", "registry-canister", "serde", "serde_bytes", diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index 04d66783793..cfcfccf0265 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -40,6 +40,9 @@ BASE_DEPENDENCIES = [ "@crate_index//:prost", "@crate_index//:serde", "@crate_index//:serde_bytes", + "@crate_index//:ed25519-dalek", + "@crate_index//:rand", + "@crate_index//:itertools", ] # Each target declared in this file may choose either these (release-ready) diff --git a/rs/nns/handlers/recovery/impl/Cargo.toml b/rs/nns/handlers/recovery/impl/Cargo.toml index 0a1a0de55eb..7185e9928c6 100644 --- a/rs/nns/handlers/recovery/impl/Cargo.toml +++ b/rs/nns/handlers/recovery/impl/Cargo.toml @@ -44,6 +44,9 @@ on_wire = { path = "../../../../rust_canisters/on_wire" } prost = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } +ed25519-dalek.workspace = true +rand.workspace = true +itertools.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] assert_matches = { workspace = true } diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 2b2644cd575..7bdc8efc4a6 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -37,7 +37,7 @@ async fn submit_new_recovery_proposal( #[update(hidden = true)] async fn vote_on_proposal(vote: VoteOnRecoveryProposal) -> Result<(), String> { - vote_on_proposal_inner(caller(), vote.ballot, vote.signature) + vote_on_proposal_inner(caller(), vote) } #[update(hidden = true)] diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index de0ed76a117..a745238aee5 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -1,11 +1,12 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; +use ed25519_dalek::SigningKey; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery::{ node_operator_sync::SimpleNodeRecord, - recovery_proposal::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery_proposal::{Ballot, NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, }; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, @@ -24,6 +25,7 @@ use ic_registry_transport::{ use maplit::btreemap; use pocket_ic::{PocketIc, PocketIcBuilder}; use prost::Message; +use rand::rngs::OsRng; use registry_canister::init::RegistryCanisterInitPayload; use test_helpers::{ add_fake_subnet, get_invariant_compliant_subnet_record, @@ -80,11 +82,31 @@ fn add_routing_table_record(total_mutations: &mut Vec, nns_id: )); } +#[derive(Clone, Debug, PartialEq, Eq)] +struct NodeOperatorArg { + principal: PrincipalId, + num_nodes: u8, + signing_key: SigningKey, +} + +impl NodeOperatorArg { + fn new(num_nodes: u8) -> Self { + let mut csprng = OsRng; + let signing_key = SigningKey::generate(&mut csprng); + + Self { + principal: PrincipalId::new_self_authenticating(signing_key.verifying_key().as_bytes()), + num_nodes, + signing_key, + } + } +} + struct SubnetNodeOperatorArg { subnet_id: PrincipalId, subnet_type: SubnetType, // Operator id : number of nodes in subnet - node_operators: BTreeMap, + node_operators: Vec, } struct RegistryPreparationArguments { @@ -100,26 +122,22 @@ impl Default for RegistryPreparationArguments { subnet_type: SubnetType::System, node_operators: vec![ // Each has 4 nodes so this is 40 nodes in total - (PrincipalId::new_user_test_id(0), 4), - (PrincipalId::new_user_test_id(1), 4), - (PrincipalId::new_user_test_id(2), 4), - (PrincipalId::new_user_test_id(3), 4), - (PrincipalId::new_user_test_id(4), 4), - (PrincipalId::new_user_test_id(5), 4), - (PrincipalId::new_user_test_id(6), 4), - (PrincipalId::new_user_test_id(7), 4), - (PrincipalId::new_user_test_id(8), 4), - (PrincipalId::new_user_test_id(9), 4), - ] - .into_iter() - .collect(), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + NodeOperatorArg::new(4), + ], }, SubnetNodeOperatorArg { subnet_id: PrincipalId::new_subnet_test_id(0), subnet_type: SubnetType::Application, - node_operators: vec![(PrincipalId::new_user_test_id(999), 4)] - .into_iter() - .collect(), + node_operators: vec![NodeOperatorArg::new(4)], }, ], } @@ -137,13 +155,13 @@ fn prepare_registry( let mut operator_mutation_ids: u8 = 0; for arg in ®istry_preparation_args.subnet_node_operators { let mut current_subnet_nodes = BTreeMap::new(); - for (operator, num_nodes) in &arg.node_operators { + for operator_arg in &arg.node_operators { let (mutation, nodes) = prepare_registry_with_nodes_and_node_operator_id( operator_mutation_ids, - *num_nodes as u64, - operator.clone(), + operator_arg.num_nodes as u64, + operator_arg.principal.clone(), ); - operator_mutation_ids += num_nodes; + operator_mutation_ids += operator_arg.num_nodes; total_mutations.extend(mutation.mutations); current_subnet_nodes.extend(nodes); @@ -245,7 +263,13 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr .subnet_node_operators .iter() .filter(|subnet| subnet.subnet_type.eq(&SubnetType::System)) - .map(|subnet_arg| subnet_arg.node_operators.values().sum::()) + .map(|subnet_arg| { + subnet_arg + .node_operators + .iter() + .map(|operator_arg| operator_arg.num_nodes) + .sum::() + }) .sum::(); for _ in 0..(ticks + 2) { pic.tick(); @@ -287,6 +311,33 @@ fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { response } +fn vote_with_only_ballot( + pic: &PocketIc, + canister: Principal, + sender: &mut NodeOperatorArg, + ballot: Ballot, +) -> Result<(), String> { + // Add logic for signing so that this is valid + let pending = get_pending(pic, canister); + let last = pending.last().unwrap(); + let signature = last.sign(&mut sender.signing_key); + let mut parts = [[0; 32]; 2]; + parts[0].copy_from_slice(&signature[..32]); + parts[1].copy_from_slice(&signature[32..]); + + vote( + pic, + canister, + sender.principal.0.clone(), + VoteOnRecoveryProposal { + payload: last.payload().expect("Should be able to fetch payload"), + signature: parts, + public_key: sender.signing_key.verifying_key().to_bytes(), + ballot, + }, + ) +} + fn vote( pic: &PocketIc, canister: Principal, @@ -325,7 +376,7 @@ fn get_current_node_operators(pic: &PocketIc, canister: Principal) -> Vec BTreeMap { +) -> Vec { arguments .subnet_node_operators .iter() diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 6ccf3f3c3cc..b5cfda4fab9 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -1,11 +1,9 @@ use candid::Principal; -use ic_nns_handler_recovery::recovery_proposal::{ - Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, -}; +use ic_nns_handler_recovery::recovery_proposal::{Ballot, NewRecoveryProposal, RecoveryPayload}; use ic_registry_subnet_type::SubnetType; use pocket_ic::PocketIc; -use crate::tests::{get_pending, vote}; +use crate::tests::{get_pending, vote_with_only_ballot}; use super::{ extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, @@ -19,13 +17,13 @@ fn place_first_proposal() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let first = node_operators.keys().next().unwrap(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let first = node_operators.iter_mut().next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -33,16 +31,7 @@ fn place_first_proposal() { assert!(response.is_ok()); - let response = vote( - &pic, - canister, - first.0.clone(), - VoteOnRecoveryProposal { - payload: "Not important yet".as_bytes().to_vec(), - signature: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = vote_with_only_ballot(&pic, canister, first, Ballot::Yes); assert!(response.is_ok()); let pending_proposals = get_pending(&pic, canister); @@ -61,12 +50,11 @@ fn place_first_proposal() { let only_vote = registered_votes.first().unwrap(); assert_eq!(only_vote.ballot, Ballot::Yes); - let number_of_nodes_for_first = node_operators.get(first).unwrap(); assert!(only_vote .nodes_tied_to_ballot .len() - .eq(&(*number_of_nodes_for_first as usize))); - assert_eq!(&only_vote.principal, first) + .eq(&(first.num_nodes as usize))); + assert_eq!(only_vote.principal, first.principal) } #[test] @@ -75,7 +63,7 @@ fn place_non_halt_first_proposal() { let (pic, canister) = init_pocket_ic(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let first = node_operators.keys().next().unwrap(); + let first = node_operators.iter().next().unwrap(); let invalid_first_proposals = vec![ NewRecoveryProposal { @@ -90,7 +78,7 @@ fn place_non_halt_first_proposal() { ]; for proposal in invalid_first_proposals { - let response = submit_proposal(&pic, canister, first.0.clone(), proposal); + let response = submit_proposal(&pic, canister, first.principal.0.clone(), proposal); assert!(response.is_err()); let pending_proposals = get_pending(&pic, canister); @@ -103,14 +91,14 @@ fn replace_first_proposal_after_voting_no_on_the_first() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -121,7 +109,7 @@ fn replace_first_proposal_after_voting_no_on_the_first() { let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -134,16 +122,7 @@ fn replace_first_proposal_after_voting_no_on_the_first() { for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::No, - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::No); assert!(response.is_ok()); } @@ -152,7 +131,7 @@ fn replace_first_proposal_after_voting_no_on_the_first() { let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -189,12 +168,12 @@ fn disallow_node_operators_from_different_subnets_from_placing_proposals() { true => Some(subnet.node_operators.clone()), }) .unwrap(); - let first_node_operator = non_system_subnet.keys().next().unwrap(); + let first_node_operator = non_system_subnet.iter().next().unwrap(); let response = submit_proposal( &pic, canister, - first_node_operator.0.clone(), + first_node_operator.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -208,14 +187,14 @@ fn place_and_execute_first_proposal( ) -> (PocketIc, Principal) { let (pic, canister) = init_pocket_ic(args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -227,16 +206,7 @@ fn place_and_execute_first_proposal( for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - payload: "Not important yet".as_bytes().to_vec(), - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::Yes); assert!(response.is_ok()); } @@ -253,7 +223,7 @@ fn place_second_proposal_recovery() { let (pic, canister) = place_and_execute_first_proposal(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); let new_proposal = NewRecoveryProposal { @@ -262,7 +232,12 @@ fn place_second_proposal_recovery() { state_hash: "123".to_string(), }, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); let pending_proposals = get_pending(&pic, canister); @@ -279,13 +254,18 @@ fn place_second_proposal_unhalt() { let (pic, canister) = place_and_execute_first_proposal(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); let pending_proposals = get_pending(&pic, canister); @@ -302,13 +282,13 @@ fn place_second_proposal_halt() { let (pic, canister) = place_and_execute_first_proposal(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -321,8 +301,8 @@ fn second_proposal_vote_against() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the second @@ -332,23 +312,19 @@ fn second_proposal_vote_against() { state_hash: "123".to_string(), }, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 votes to vote against this for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::No, - payload: "Not important yet".as_bytes().to_vec(), - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::No); assert!(response.is_ok()) } @@ -361,8 +337,8 @@ fn second_proposal_recovery_vote_in() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the second @@ -372,23 +348,19 @@ fn second_proposal_recovery_vote_in() { state_hash: "123".to_string(), }, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 to vote in for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - payload: "Not important yet".as_bytes().to_vec(), - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::Yes); assert!(response.is_ok()) } @@ -403,8 +375,8 @@ fn second_proposal_recovery_vote_in_and_resubmit() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the second @@ -414,23 +386,19 @@ fn second_proposal_recovery_vote_in_and_resubmit() { state_hash: "123".to_string(), }, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 to vote in for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - payload: "Not important yet".as_bytes().to_vec(), - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::Yes); assert!(response.is_ok()) } @@ -443,7 +411,7 @@ fn second_proposal_recovery_vote_in_and_resubmit() { let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), resubmitted_proposal.clone(), ); assert!(response.is_ok()); @@ -461,31 +429,27 @@ fn second_proposal_unhalt_vote_in() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the second let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 to vote it in for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - payload: "Not important yet".as_bytes().to_vec(), - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::Yes); assert!(response.is_ok()) } @@ -500,7 +464,7 @@ fn submit_first_two_second_not_voted_in_place_third() { let (pic, canister) = place_and_execute_first_proposal(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); // Place the second @@ -510,14 +474,24 @@ fn submit_first_two_second_not_voted_in_place_third() { state_hash: "123".to_string(), }, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_err()); } @@ -526,8 +500,8 @@ fn place_and_execute_second_proposal( ) -> (PocketIc, Principal) { let (pic, canister) = place_and_execute_first_proposal(args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the second @@ -537,23 +511,19 @@ fn place_and_execute_second_proposal( state_hash: "123".to_string(), }, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 to vote it in for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::Yes); assert!(response.is_ok()) } @@ -569,14 +539,19 @@ fn submit_first_two_second_voted_in_place_third() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_second_proposal(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); let pending = get_pending(&pic, canister); @@ -589,31 +564,27 @@ fn submit_first_two_second_voted_in_place_third() { fn vote_against_last_proposal() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_second_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 votes to vote against this proposal for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::No, - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::No); assert!(response.is_ok()) } @@ -629,31 +600,27 @@ fn vote_against_last_proposal() { fn vote_in_last_proposal() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_second_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); // We need 7 votes to vote for this proposal for _ in 0..7 { let next = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - next.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = vote_with_only_ballot(&pic, canister, next, Ballot::Yes); assert!(response.is_ok()) } @@ -669,14 +636,19 @@ fn place_any_proposal_after_there_are_three() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_second_proposal(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); // Place the third let new_proposal = NewRecoveryProposal { payload: RecoveryPayload::Unhalt, }; - let response = submit_proposal(&pic, canister, first.0.clone(), new_proposal.clone()); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + new_proposal.clone(), + ); assert!(response.is_ok()); let payloads = vec![ @@ -691,7 +663,7 @@ fn place_any_proposal_after_there_are_three() { let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload }, ); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index d410fa36688..f1f2279d3b7 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,11 +1,8 @@ -use candid::Principal; -use ic_nns_handler_recovery::recovery_proposal::{ - Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, -}; +use ic_nns_handler_recovery::recovery_proposal::{Ballot, NewRecoveryProposal, RecoveryPayload}; use crate::tests::{ - extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote, - RegistryPreparationArguments, + extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote_with_only_ballot, + NodeOperatorArg, RegistryPreparationArguments, }; #[test] @@ -13,14 +10,14 @@ fn disallow_double_vote() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -29,28 +26,10 @@ fn disallow_double_vote() { assert!(response.is_ok()); let second = node_operators_iterator.next().unwrap(); - let response = vote( - &pic, - canister, - second.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = vote_with_only_ballot(&pic, canister, second, Ballot::Yes); assert!(response.is_ok()); - let response = vote( - &pic, - canister, - second.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = vote_with_only_ballot(&pic, canister, second, Ballot::Yes); assert!(response.is_err()); } @@ -60,13 +39,13 @@ fn disallow_vote_anonymous() { let (pic, canister) = init_pocket_ic(&mut args); let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators_iterator = node_operators.iter(); let first = node_operators_iterator.next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -74,16 +53,8 @@ fn disallow_vote_anonymous() { assert!(response.is_ok()); - let response = vote( - &pic, - canister, - Principal::anonymous(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = + vote_with_only_ballot(&pic, canister, &mut NodeOperatorArg::new(10), Ballot::Yes); assert!(response.is_err()); } @@ -92,14 +63,14 @@ fn allow_votes_even_if_executed() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.keys(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); let response = submit_proposal( &pic, canister, - first.0.clone(), + first.principal.0.clone(), NewRecoveryProposal { payload: RecoveryPayload::Halt, }, @@ -108,16 +79,10 @@ fn allow_votes_even_if_executed() { assert!(response.is_ok()); for no in node_operators_iterator { - let response = vote( - &pic, - canister, - no.0.clone(), - VoteOnRecoveryProposal { - signature: "Not important yet".as_bytes().to_vec(), - payload: "Not important yet".as_bytes().to_vec(), - ballot: Ballot::Yes, - }, - ); + let response = vote_with_only_ballot(&pic, canister, no, Ballot::Yes); assert!(response.is_ok()); } } + +#[test] +fn disallow_votes_bad_signature() {} diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 5928ae0bc38..4bea4a53d90 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use candid::CandidType; +use ed25519_dalek::{ed25519::signature::SignerMut, Signature, SigningKey}; use ic_base_types::{NodeId, PrincipalId}; use ic_nns_handler_root::now_seconds; use serde::Deserialize; @@ -19,9 +20,18 @@ pub struct NodeOperatorBallot { pub principal: PrincipalId, pub nodes_tied_to_ballot: Vec, pub ballot: Ballot, - pub signature: Vec, + pub signature: [[u8; 32]; 2], + pub payload: Vec, + pub pub_key: [u8; 32], } +// struct { +// pub pub_key: 32 bytes, +// pub proposal_type: &str, +// pub payload: Vec, +// pub signature: 64 bytes +// } + #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub enum RecoveryPayload { Halt, @@ -43,6 +53,26 @@ pub struct RecoveryProposal { pub payload: RecoveryPayload, } +impl RecoveryProposal { + pub fn sign(&self, signing_key: &mut SigningKey) -> [u8; 64] { + let signature = signing_key.sign( + &self + .payload() + .expect("Should be able to encode recovery proposal"), + ); + signature.to_bytes() + } + + pub fn payload(&self) -> Result, candid::Error> { + let self_with_empty_ballots = Self { + node_operator_ballots: vec![], + ..self.clone() + }; + + candid::encode_one(self_with_empty_ballots) + } +} + impl RecoveryProposal { fn is_byzantine_majority(&self, ballot: Ballot) -> bool { let total_nodes_nodes = self @@ -92,7 +122,8 @@ pub struct NewRecoveryProposal { #[derive(Debug, CandidType, Deserialize, Clone)] pub struct VoteOnRecoveryProposal { pub payload: Vec, - pub signature: Vec, + pub signature: [[u8; 32]; 2], + pub public_key: [u8; 32], pub ballot: Ballot, } @@ -260,7 +291,9 @@ fn initialize_ballots(simple_node_records: &Vec) -> Vec) -> Vec, + vote: VoteOnRecoveryProposal, ) -> Result<(), String> { - PROPOSALS - .with_borrow_mut(|proposals| vote_on_last_proposal(caller, proposals, ballot, signature)) + PROPOSALS.with_borrow_mut(|proposals| vote_on_last_proposal(caller, proposals, vote)) } fn vote_on_last_proposal( caller: PrincipalId, proposals: &mut Vec, - ballot: Ballot, - signature: Vec, + vote: VoteOnRecoveryProposal, ) -> Result<(), String> { let last_proposal = proposals .last_mut() @@ -299,8 +329,16 @@ fn vote_on_last_proposal( return Err("Vote already submitted".to_string()); } - correlated_ballot.ballot = ballot; - correlated_ballot.signature = signature; + // Ensure that the payload can be deserialized in last proposal + // This ensures that the versions match + + // Ensure that the signature is valid + is_valid_signature(&caller, &vote.public_key, &vote.signature, &vote.payload)?; + + correlated_ballot.ballot = vote.ballot; + correlated_ballot.signature = vote.signature; + correlated_ballot.payload = vote.payload; + correlated_ballot.pub_key = vote.public_key; // If the outcome is no, remove this proposal if last_proposal.is_byzantine_majority_no() { @@ -313,3 +351,24 @@ fn vote_on_last_proposal( Ok(()) } + +fn is_valid_signature( + caller: &PrincipalId, + pub_key: &[u8; 32], + submitted_signature: &[[u8; 32]; 2], + raw_payload: &Vec, +) -> Result<(), String> { + let principal_from_pub_key = PrincipalId::new_self_authenticating(pub_key.as_slice()); + if !principal_from_pub_key.eq(caller) { + return Err("Caller and public key sent differ!".to_string()); + } + + let loaded_public_key = ed25519_dalek::VerifyingKey::from_bytes(pub_key) + .map_err(|e| format!("Invalid public key: {:?}", e))?; + let signature = ed25519_dalek::Signature::from_slice(submitted_signature.as_flattened()) + .map_err(|e| format!("Invalid signature: {:?}", e))?; + + loaded_public_key + .verify_strict(&raw_payload, &signature) + .map_err(|e| format!("Signature not doesn't match: {:?}", e)) +} From 0f4e1d85b880e3d5125fb7246b587372071d0e98 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 16:16:40 +0100 Subject: [PATCH 25/76] adding tests for signature verification --- .../recovery/impl/canister/tests/mod.rs | 4 +- .../impl/canister/tests/voting_tests.rs | 126 +++++++++++++++++- .../recovery/impl/src/recovery_proposal.rs | 11 +- 3 files changed, 130 insertions(+), 11 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index a745238aee5..f7980178f3d 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -330,7 +330,9 @@ fn vote_with_only_ballot( canister, sender.principal.0.clone(), VoteOnRecoveryProposal { - payload: last.payload().expect("Should be able to fetch payload"), + payload: last + .signature_payload() + .expect("Should be able to fetch payload"), signature: parts, public_key: sender.signing_key.verifying_key().to_bytes(), ballot, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index f1f2279d3b7..592453cb684 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,10 +1,16 @@ -use ic_nns_handler_recovery::recovery_proposal::{Ballot, NewRecoveryProposal, RecoveryPayload}; +use candid::Principal; +use ed25519_dalek::SigningKey; +use ic_nns_handler_recovery::recovery_proposal::{ + Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, +}; use crate::tests::{ - extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote_with_only_ballot, - NodeOperatorArg, RegistryPreparationArguments, + extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote, + vote_with_only_ballot, NodeOperatorArg, RegistryPreparationArguments, }; +use super::get_pending; + #[test] fn disallow_double_vote() { let mut args = RegistryPreparationArguments::default(); @@ -85,4 +91,116 @@ fn allow_votes_even_if_executed() { } #[test] -fn disallow_votes_bad_signature() {} +fn disallow_votes_bad_signature() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }, + ); + + assert!(response.is_ok()); + + let response = vote( + &pic, + canister, + first.principal.0.clone(), + VoteOnRecoveryProposal { + payload: vec![], + signature: [[0; 32]; 2], + public_key: first.signing_key.verifying_key().to_bytes(), + ballot: Ballot::Yes, + }, + ); + assert!(response.is_err()); + + let response = vote_with_only_ballot(&pic, canister, first, Ballot::Yes); + assert!(response.is_ok()) +} + +#[test] +fn disallow_votes_wrong_public_key() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }, + ); + assert!(response.is_ok()); + + let pending = get_pending(&pic, canister); + let last_proposal = pending.last().unwrap(); + + let mut new_key_pair = SigningKey::generate(&mut rand::rngs::OsRng); + let signature = last_proposal.sign(&mut new_key_pair); + let mut parts = [[0; 32]; 2]; + parts[0].copy_from_slice(&signature[..32]); + parts[1].copy_from_slice(&signature[32..]); + + let response = vote( + &pic, + canister, + first.principal.0.clone(), + VoteOnRecoveryProposal { + payload: last_proposal + .signature_payload() + .expect("Should be able to serialize payload"), + signature: parts, + public_key: new_key_pair.verifying_key().to_bytes(), + ballot: Ballot::Yes, + }, + ); + + assert!(response.is_err()) +} + +#[test] +fn disallow_votes_anonymous() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); + let first = node_operators_iterator.next().unwrap(); + + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }, + ); + assert!(response.is_ok()); + + let response = vote( + &pic, + canister, + Principal::anonymous(), + VoteOnRecoveryProposal { + payload: vec![], + signature: [[0; 32]; 2], + public_key: [0; 32], + ballot: Ballot::Yes, + }, + ); + assert!(response.is_err()) +} diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 4bea4a53d90..5032f834a4a 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use candid::CandidType; -use ed25519_dalek::{ed25519::signature::SignerMut, Signature, SigningKey}; +use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; use ic_base_types::{NodeId, PrincipalId}; use ic_nns_handler_root::now_seconds; use serde::Deserialize; @@ -57,19 +57,18 @@ impl RecoveryProposal { pub fn sign(&self, signing_key: &mut SigningKey) -> [u8; 64] { let signature = signing_key.sign( &self - .payload() + .signature_payload() .expect("Should be able to encode recovery proposal"), ); signature.to_bytes() } - pub fn payload(&self) -> Result, candid::Error> { - let self_with_empty_ballots = Self { + pub fn signature_payload(&self) -> Result, candid::Error> { + let self_without_ballots = Self { node_operator_ballots: vec![], ..self.clone() }; - - candid::encode_one(self_with_empty_ballots) + candid::encode_one(self_without_ballots) } } From 72f79fd84eac05a71b597ad589ef2250f6560b37 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 16:20:58 +0100 Subject: [PATCH 26/76] transition to query calls because of signatures --- rs/nns/handlers/recovery/impl/canister/canister.rs | 2 +- rs/nns/handlers/recovery/impl/canister/tests/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 7bdc8efc4a6..b06b604f80d 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -40,7 +40,7 @@ async fn vote_on_proposal(vote: VoteOnRecoveryProposal) -> Result<(), String> { vote_on_proposal_inner(caller(), vote) } -#[update(hidden = true)] +#[query(hidden = true)] fn get_pending_recovery_proposals() -> Vec { get_recovery_proposals() } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index f7980178f3d..10b689f47ba 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -297,7 +297,7 @@ fn submit_proposal( fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { let response = pic - .update_call( + .query_call( canister.into(), Principal::anonymous(), "get_pending_recovery_proposals", From 8e231bd793575a3a5cede5ea5385d57317dd376b Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 16:39:25 +0100 Subject: [PATCH 27/76] extracting security metadata --- .../recovery/impl/canister/tests/mod.rs | 16 +++-- .../impl/canister/tests/voting_tests.rs | 30 ++++++---- .../recovery/impl/src/recovery_proposal.rs | 59 ++++++++++--------- 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 10b689f47ba..a6e68431993 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -6,7 +6,9 @@ use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery::{ node_operator_sync::SimpleNodeRecord, - recovery_proposal::{Ballot, NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery_proposal::{ + Ballot, NewRecoveryProposal, RecoveryProposal, SecurityMetadata, VoteOnRecoveryProposal, + }, }; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, @@ -330,11 +332,13 @@ fn vote_with_only_ballot( canister, sender.principal.0.clone(), VoteOnRecoveryProposal { - payload: last - .signature_payload() - .expect("Should be able to fetch payload"), - signature: parts, - public_key: sender.signing_key.verifying_key().to_bytes(), + security_metadata: SecurityMetadata { + payload: last + .signature_payload() + .expect("Should be able to fetch payload"), + signature: parts, + pub_key: sender.signing_key.verifying_key().to_bytes(), + }, ballot, }, ) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 592453cb684..71f242d62c1 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,7 +1,7 @@ use candid::Principal; use ed25519_dalek::SigningKey; use ic_nns_handler_recovery::recovery_proposal::{ - Ballot, NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, + Ballot, NewRecoveryProposal, RecoveryPayload, SecurityMetadata, VoteOnRecoveryProposal, }; use crate::tests::{ @@ -115,10 +115,12 @@ fn disallow_votes_bad_signature() { canister, first.principal.0.clone(), VoteOnRecoveryProposal { - payload: vec![], - signature: [[0; 32]; 2], - public_key: first.signing_key.verifying_key().to_bytes(), ballot: Ballot::Yes, + security_metadata: SecurityMetadata { + payload: vec![], + signature: [[0; 32]; 2], + pub_key: first.signing_key.verifying_key().to_bytes(), + }, }, ); assert!(response.is_err()); @@ -160,11 +162,13 @@ fn disallow_votes_wrong_public_key() { canister, first.principal.0.clone(), VoteOnRecoveryProposal { - payload: last_proposal - .signature_payload() - .expect("Should be able to serialize payload"), - signature: parts, - public_key: new_key_pair.verifying_key().to_bytes(), + security_metadata: SecurityMetadata { + payload: last_proposal + .signature_payload() + .expect("Should be able to serialize payload"), + signature: parts, + pub_key: new_key_pair.verifying_key().to_bytes(), + }, ballot: Ballot::Yes, }, ); @@ -196,9 +200,11 @@ fn disallow_votes_anonymous() { canister, Principal::anonymous(), VoteOnRecoveryProposal { - payload: vec![], - signature: [[0; 32]; 2], - public_key: [0; 32], + security_metadata: SecurityMetadata { + payload: vec![], + signature: [[0; 32]; 2], + pub_key: [0; 32], + }, ballot: Ballot::Yes, }, ); diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 5032f834a4a..ce379420902 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -16,21 +16,29 @@ pub enum Ballot { } #[derive(Clone, Debug, CandidType, Deserialize)] -pub struct NodeOperatorBallot { - pub principal: PrincipalId, - pub nodes_tied_to_ballot: Vec, - pub ballot: Ballot, +pub struct SecurityMetadata { pub signature: [[u8; 32]; 2], pub payload: Vec, pub pub_key: [u8; 32], } -// struct { -// pub pub_key: 32 bytes, -// pub proposal_type: &str, -// pub payload: Vec, -// pub signature: 64 bytes -// } +impl SecurityMetadata { + pub fn empty() -> Self { + Self { + signature: [[0; 32]; 2], + payload: vec![], + pub_key: [0; 32], + } + } +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct NodeOperatorBallot { + pub principal: PrincipalId, + pub nodes_tied_to_ballot: Vec, + pub ballot: Ballot, + pub security_metadata: SecurityMetadata, +} #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub enum RecoveryPayload { @@ -120,10 +128,7 @@ pub struct NewRecoveryProposal { #[derive(Debug, CandidType, Deserialize, Clone)] pub struct VoteOnRecoveryProposal { - pub payload: Vec, - pub signature: [[u8; 32]; 2], - pub public_key: [u8; 32], - + pub security_metadata: SecurityMetadata, pub ballot: Ballot, } @@ -290,9 +295,7 @@ fn initialize_ballots(simple_node_records: &Vec) -> Vec, + security_metadata: &SecurityMetadata, ) -> Result<(), String> { - let principal_from_pub_key = PrincipalId::new_self_authenticating(pub_key.as_slice()); + let principal_from_pub_key = + PrincipalId::new_self_authenticating(security_metadata.pub_key.as_slice()); if !principal_from_pub_key.eq(caller) { return Err("Caller and public key sent differ!".to_string()); } - let loaded_public_key = ed25519_dalek::VerifyingKey::from_bytes(pub_key) + let loaded_public_key = ed25519_dalek::VerifyingKey::from_bytes(&security_metadata.pub_key) .map_err(|e| format!("Invalid public key: {:?}", e))?; - let signature = ed25519_dalek::Signature::from_slice(submitted_signature.as_flattened()) - .map_err(|e| format!("Invalid signature: {:?}", e))?; + let signature = + ed25519_dalek::Signature::from_slice(security_metadata.signature.as_flattened()) + .map_err(|e| format!("Invalid signature: {:?}", e))?; loaded_public_key - .verify_strict(&raw_payload, &signature) + .verify_strict(&security_metadata.payload, &signature) .map_err(|e| format!("Signature not doesn't match: {:?}", e)) } From 3fa544584de334183231966bd8a17af9f429b08f Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 17:20:32 +0100 Subject: [PATCH 28/76] extracting security metadata and ballot to interfaces --- Cargo.lock | 10 ++++ Cargo.toml | 1 + rs/nns/handlers/recovery/impl/BUILD.bazel | 1 + rs/nns/handlers/recovery/impl/Cargo.toml | 1 + .../recovery/impl/canister/tests/mod.rs | 5 +- .../canister/tests/proposal_logic_tests.rs | 3 +- .../impl/canister/tests/voting_tests.rs | 3 +- .../recovery/impl/src/recovery_proposal.rs | 50 ++----------------- .../handlers/recovery/interface/BUILD.bazel | 43 ++++++++++++++++ rs/nns/handlers/recovery/interface/Cargo.toml | 12 +++++ rs/nns/handlers/recovery/interface/src/lib.rs | 32 ++++++++++++ .../interface/src/security_metadata.rs | 49 ++++++++++++++++++ 12 files changed, 159 insertions(+), 51 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/BUILD.bazel create mode 100644 rs/nns/handlers/recovery/interface/Cargo.toml create mode 100644 rs/nns/handlers/recovery/interface/src/lib.rs create mode 100644 rs/nns/handlers/recovery/interface/src/security_metadata.rs diff --git a/Cargo.lock b/Cargo.lock index 460d4787bd3..3bf282062f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10829,6 +10829,7 @@ dependencies = [ "ic-nervous-system-runtime", "ic-nns-common", "ic-nns-constants", + "ic-nns-handler-recovery-interface", "ic-nns-handler-root", "ic-nns-test-utils", "ic-protobuf", @@ -10856,6 +10857,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "ic-nns-handler-recovery-interface" +version = "0.9.0" +dependencies = [ + "candid", + "ed25519-dalek", + "serde", +] + [[package]] name = "ic-nns-handler-root" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 7f294ab7eaa..8699d8bf77c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -437,6 +437,7 @@ members = [ "rs/xnet/hyper", "rs/xnet/payload_builder", "rs/xnet/uri", + "rs/nns/handlers/recovery/interface", ] resolver = "2" diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index cfcfccf0265..135c0728827 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -30,6 +30,7 @@ BASE_DEPENDENCIES = [ "//rs/types/base_types", "//rs/types/management_canister_types", "//rs/nns/handlers/root/impl:root", + "//rs/nns/handlers/recovery/interface", "@crate_index//:build-info", "@crate_index//:ic-cdk-timers", "@crate_index//:candid", diff --git a/rs/nns/handlers/recovery/impl/Cargo.toml b/rs/nns/handlers/recovery/impl/Cargo.toml index 7185e9928c6..590de56d219 100644 --- a/rs/nns/handlers/recovery/impl/Cargo.toml +++ b/rs/nns/handlers/recovery/impl/Cargo.toml @@ -47,6 +47,7 @@ serde_bytes = { workspace = true } ed25519-dalek.workspace = true rand.workspace = true itertools.workspace = true +ic-nns-handler-recovery-interface = { path = "../interface" } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] assert_matches = { workspace = true } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index a6e68431993..a089956a167 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -6,10 +6,9 @@ use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery::{ node_operator_sync::SimpleNodeRecord, - recovery_proposal::{ - Ballot, NewRecoveryProposal, RecoveryProposal, SecurityMetadata, VoteOnRecoveryProposal, - }, + recovery_proposal::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, }; +use ic_nns_handler_recovery_interface::{security_metadata::SecurityMetadata, Ballot}; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, routing_table::v1::RoutingTable as RoutingTablePB, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index b5cfda4fab9..bf7e8e4410d 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -1,5 +1,6 @@ use candid::Principal; -use ic_nns_handler_recovery::recovery_proposal::{Ballot, NewRecoveryProposal, RecoveryPayload}; +use ic_nns_handler_recovery::recovery_proposal::{NewRecoveryProposal, RecoveryPayload}; +use ic_nns_handler_recovery_interface::Ballot; use ic_registry_subnet_type::SubnetType; use pocket_ic::PocketIc; diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 71f242d62c1..66282e3ee79 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,8 +1,9 @@ use candid::Principal; use ed25519_dalek::SigningKey; use ic_nns_handler_recovery::recovery_proposal::{ - Ballot, NewRecoveryProposal, RecoveryPayload, SecurityMetadata, VoteOnRecoveryProposal, + NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, }; +use ic_nns_handler_recovery_interface::{security_metadata::SecurityMetadata, Ballot}; use crate::tests::{ extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote, diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index ce379420902..ae5c3e35d23 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -3,35 +3,12 @@ use std::cell::RefCell; use candid::CandidType; use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; use ic_base_types::{NodeId, PrincipalId}; +use ic_nns_handler_recovery_interface::{security_metadata::SecurityMetadata, Ballot}; use ic_nns_handler_root::now_seconds; use serde::Deserialize; use crate::node_operator_sync::{get_node_operators_in_nns, SimpleNodeRecord}; -#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] -pub enum Ballot { - Yes, - No, - Undecided, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct SecurityMetadata { - pub signature: [[u8; 32]; 2], - pub payload: Vec, - pub pub_key: [u8; 32], -} - -impl SecurityMetadata { - pub fn empty() -> Self { - Self { - signature: [[0; 32]; 2], - payload: vec![], - pub_key: [0; 32], - } - } -} - #[derive(Clone, Debug, CandidType, Deserialize)] pub struct NodeOperatorBallot { pub principal: PrincipalId, @@ -335,7 +312,9 @@ fn vote_on_last_proposal( // This ensures that the versions match // Ensure that the signature is valid - is_valid_signature(&caller, &vote.security_metadata)?; + vote.security_metadata + .validate_metadata(&caller.0) + .map_err(|e| e.to_string())?; correlated_ballot.ballot = vote.ballot; correlated_ballot.security_metadata = vote.security_metadata.clone(); @@ -351,24 +330,3 @@ fn vote_on_last_proposal( Ok(()) } - -fn is_valid_signature( - caller: &PrincipalId, - security_metadata: &SecurityMetadata, -) -> Result<(), String> { - let principal_from_pub_key = - PrincipalId::new_self_authenticating(security_metadata.pub_key.as_slice()); - if !principal_from_pub_key.eq(caller) { - return Err("Caller and public key sent differ!".to_string()); - } - - let loaded_public_key = ed25519_dalek::VerifyingKey::from_bytes(&security_metadata.pub_key) - .map_err(|e| format!("Invalid public key: {:?}", e))?; - let signature = - ed25519_dalek::Signature::from_slice(security_metadata.signature.as_flattened()) - .map_err(|e| format!("Invalid signature: {:?}", e))?; - - loaded_public_key - .verify_strict(&security_metadata.payload, &signature) - .map_err(|e| format!("Signature not doesn't match: {:?}", e)) -} diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel new file mode 100644 index 00000000000..67b318dc026 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -0,0 +1,43 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +DEPENDENCIES = [ + # Keep sorted. + "//rs/nervous_system/clients", + "//rs/nns/constants", + "//rs/types/base_types", + "@crate_index//:candid", + "@crate_index//:ic-cdk", + "@crate_index//:serde", + "@crate_index//:ed25519-dalek", +] + +MACRO_DEPENDENCIES = [ + # Keep sorted. + "@crate_index//:async-trait", +] + +DEV_DEPENDENCIES = [] + +MACRO_DEV_DEPENDENCIES = [] + +ALIASES = {} + +rust_library( + name = "interface", + srcs = glob(["src/**/*.rs"]), + aliases = ALIASES, + crate_name = "ic_nns_handler_recovery_interface", + proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.1.0", + deps = DEPENDENCIES, +) + +rust_test( + name = "recovery_interface_test", + aliases = ALIASES, + crate = ":interface", + proc_macro_deps = MACRO_DEPENDENCIES + MACRO_DEV_DEPENDENCIES, + deps = DEPENDENCIES + DEV_DEPENDENCIES, +) diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml new file mode 100644 index 00000000000..c0fb8b53233 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ic-nns-handler-recovery-interface" +version.workspace = true +authors.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true + +[dependencies] +candid = { workspace = true } +serde = { workspace = true } +ed25519-dalek.workspace = true diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs new file mode 100644 index 00000000000..fcb0532e3f1 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -0,0 +1,32 @@ +use candid::CandidType; +use serde::Deserialize; + +pub mod security_metadata; + +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] +pub enum Ballot { + Yes, + No, + Undecided, +} + +pub enum RecoveryError { + InvalidPubKey(String), + InvalidSignatureFormat(String), + InvalidSignature(String), + + PrincipalPublicKeyMismatch(String), +} + +type Result = std::result::Result; + +impl ToString for RecoveryError { + fn to_string(&self) -> String { + match self { + Self::InvalidPubKey(s) + | Self::InvalidSignatureFormat(s) + | Self::InvalidSignature(s) + | Self::PrincipalPublicKeyMismatch(s) => s.to_string(), + } + } +} diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs new file mode 100644 index 00000000000..2d2c45c64c1 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -0,0 +1,49 @@ +use super::*; +use candid::{CandidType, Principal}; +use ed25519_dalek::{Signature, VerifyingKey}; +use serde::Deserialize; + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct SecurityMetadata { + pub signature: [[u8; 32]; 2], + pub payload: Vec, + pub pub_key: [u8; 32], +} + +impl SecurityMetadata { + pub fn empty() -> Self { + Self { + signature: [[0; 32]; 2], + payload: vec![], + pub_key: [0; 32], + } + } + + pub fn validate_metadata(&self, caller: &Principal) -> Result<()> { + self.principal_matches_public_key(caller)?; + self.verify() + } + + pub fn verify(&self) -> Result<()> { + let loaded_public_key = VerifyingKey::from_bytes(&self.pub_key) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + let signature = Signature::from_slice(self.signature.as_flattened()) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + loaded_public_key + .verify_strict(&self.payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } + + pub fn principal_matches_public_key(&self, principal: &Principal) -> Result<()> { + let loaded_principal = Principal::self_authenticating(self.pub_key); + + match loaded_principal.eq(principal) { + true => Ok(()), + false => Err(RecoveryError::PrincipalPublicKeyMismatch(format!( + "Expected {}, got {}", + loaded_principal, principal, + ))), + } + } +} From 28abb63b3b909799b9d25f24d0e928d61e27f124 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 17:37:39 +0100 Subject: [PATCH 29/76] extracting proposal interface --- .../recovery/impl/canister/canister.rs | 3 +- .../recovery/impl/canister/tests/mod.rs | 8 +- .../canister/tests/proposal_logic_tests.rs | 6 +- .../impl/canister/tests/voting_tests.rs | 8 +- .../recovery/impl/src/recovery_proposal.rs | 112 +++--------------- rs/nns/handlers/recovery/interface/src/lib.rs | 8 +- .../recovery/interface/src/recovery.rs | 88 ++++++++++++++ 7 files changed, 123 insertions(+), 110 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/recovery.rs diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index b06b604f80d..91cfefda4f5 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -12,9 +12,10 @@ use ic_nns_handler_recovery::{ node_operator_sync::{get_node_operators_in_nns, sync_node_operators, SimpleNodeRecord}, recovery_proposal::{ get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner, - NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal, + NewRecoveryProposal, VoteOnRecoveryProposal, }, }; +use ic_nns_handler_recovery_interface::recovery::RecoveryProposal; fn caller() -> PrincipalId { PrincipalId::from(ic_cdk::caller()) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index a089956a167..5f7e38b7ebc 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -6,9 +6,11 @@ use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery::{ node_operator_sync::SimpleNodeRecord, - recovery_proposal::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery_proposal::{NewRecoveryProposal, VoteOnRecoveryProposal}, +}; +use ic_nns_handler_recovery_interface::{ + recovery::RecoveryProposal, security_metadata::SecurityMetadata, Ballot, }; -use ic_nns_handler_recovery_interface::{security_metadata::SecurityMetadata, Ballot}; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, routing_table::v1::RoutingTable as RoutingTablePB, @@ -321,7 +323,7 @@ fn vote_with_only_ballot( // Add logic for signing so that this is valid let pending = get_pending(pic, canister); let last = pending.last().unwrap(); - let signature = last.sign(&mut sender.signing_key); + let signature = last.sign(&mut sender.signing_key).unwrap(); let mut parts = [[0; 32]; 2]; parts[0].copy_from_slice(&signature[..32]); parts[1].copy_from_slice(&signature[32..]); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index bf7e8e4410d..edeb70bdd1c 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -1,6 +1,6 @@ use candid::Principal; -use ic_nns_handler_recovery::recovery_proposal::{NewRecoveryProposal, RecoveryPayload}; -use ic_nns_handler_recovery_interface::Ballot; +use ic_nns_handler_recovery::recovery_proposal::NewRecoveryProposal; +use ic_nns_handler_recovery_interface::{recovery::RecoveryPayload, Ballot}; use ic_registry_subnet_type::SubnetType; use pocket_ic::PocketIc; @@ -55,7 +55,7 @@ fn place_first_proposal() { .nodes_tied_to_ballot .len() .eq(&(first.num_nodes as usize))); - assert_eq!(only_vote.principal, first.principal) + assert_eq!(only_vote.principal, first.principal.0) } #[test] diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 66282e3ee79..b740eef6f23 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,9 +1,9 @@ use candid::Principal; use ed25519_dalek::SigningKey; -use ic_nns_handler_recovery::recovery_proposal::{ - NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal, +use ic_nns_handler_recovery::recovery_proposal::{NewRecoveryProposal, VoteOnRecoveryProposal}; +use ic_nns_handler_recovery_interface::{ + recovery::RecoveryPayload, security_metadata::SecurityMetadata, Ballot, }; -use ic_nns_handler_recovery_interface::{security_metadata::SecurityMetadata, Ballot}; use crate::tests::{ extract_node_operators_from_init_data, init_pocket_ic, submit_proposal, vote, @@ -153,7 +153,7 @@ fn disallow_votes_wrong_public_key() { let last_proposal = pending.last().unwrap(); let mut new_key_pair = SigningKey::generate(&mut rand::rngs::OsRng); - let signature = last_proposal.sign(&mut new_key_pair); + let signature = last_proposal.sign(&mut new_key_pair).unwrap(); let mut parts = [[0; 32]; 2]; parts[0].copy_from_slice(&signature[..32]); parts[1].copy_from_slice(&signature[32..]); diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index ae5c3e35d23..757822c630d 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -1,99 +1,17 @@ use std::cell::RefCell; use candid::CandidType; -use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; -use ic_base_types::{NodeId, PrincipalId}; -use ic_nns_handler_recovery_interface::{security_metadata::SecurityMetadata, Ballot}; +use ic_base_types::PrincipalId; +use ic_nns_handler_recovery_interface::{ + recovery::{NodeOperatorBallot, RecoveryPayload, RecoveryProposal}, + security_metadata::SecurityMetadata, + Ballot, +}; use ic_nns_handler_root::now_seconds; use serde::Deserialize; use crate::node_operator_sync::{get_node_operators_in_nns, SimpleNodeRecord}; -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct NodeOperatorBallot { - pub principal: PrincipalId, - pub nodes_tied_to_ballot: Vec, - pub ballot: Ballot, - pub security_metadata: SecurityMetadata, -} - -#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] -pub enum RecoveryPayload { - Halt, - DoRecovery { height: u64, state_hash: String }, - Unhalt, -} - -#[derive(Clone, Debug, CandidType, Deserialize)] -pub struct RecoveryProposal { - /// The principal id of the proposer (must be one of the node - /// operators of the NNS subnet according to the registry at - /// time of submission). - pub proposer: PrincipalId, - /// The timestamp, in seconds, at which the proposal was submitted. - pub submission_timestamp_seconds: u64, - /// The ballots cast by node operators. - pub node_operator_ballots: Vec, - /// Payload for the proposal - pub payload: RecoveryPayload, -} - -impl RecoveryProposal { - pub fn sign(&self, signing_key: &mut SigningKey) -> [u8; 64] { - let signature = signing_key.sign( - &self - .signature_payload() - .expect("Should be able to encode recovery proposal"), - ); - signature.to_bytes() - } - - pub fn signature_payload(&self) -> Result, candid::Error> { - let self_without_ballots = Self { - node_operator_ballots: vec![], - ..self.clone() - }; - candid::encode_one(self_without_ballots) - } -} - -impl RecoveryProposal { - fn is_byzantine_majority(&self, ballot: Ballot) -> bool { - let total_nodes_nodes = self - .node_operator_ballots - .iter() - .map(|bal| bal.nodes_tied_to_ballot.len()) - .sum::(); - let max_faults = (total_nodes_nodes - 1) / 3; - let votes_for_ballot = self - .node_operator_ballots - .iter() - .map(|vote| match vote.ballot == ballot { - // Each vote has the weight of 1 times - // the amount of nodes the node operator - // has in the nns subnet - true => 1 * vote.nodes_tied_to_ballot.len(), - false => 0, - }) - .sum::(); - votes_for_ballot >= (total_nodes_nodes - max_faults) - } - - /// For a root proposal to have a byzantine majority of no, it - /// needs to collect f + 1 "no" votes, where N s the total number - /// of nodes (same as the number of ballots) and f = (N - 1) / 3. - pub fn is_byzantine_majority_no(&self) -> bool { - self.is_byzantine_majority(Ballot::No) - } - - /// For a root proposal to have a byzantine majority of no, it - /// needs to collect f + 1 "no" votes, where N s the total number - /// of nodes (same as the number of ballots) and f = (N - 1) / 3. - pub fn is_byzantine_majority_yes(&self) -> bool { - self.is_byzantine_majority(Ballot::Yes) - } -} - thread_local! { static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; } @@ -140,7 +58,7 @@ pub fn submit_recovery_proposal( match &new_proposal.payload { RecoveryPayload::Halt => { proposals.push(RecoveryProposal { - proposer: caller, + proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: RecoveryPayload::Halt, @@ -177,7 +95,7 @@ pub fn submit_recovery_proposal( } | RecoveryPayload::Unhalt => { proposals.push(RecoveryProposal { - proposer: caller, + proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: new_proposal.payload.clone(), @@ -213,7 +131,7 @@ pub fn submit_recovery_proposal( RecoveryPayload::Unhalt, ) => { proposals.push(RecoveryProposal { - proposer: caller, + proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: RecoveryPayload::Unhalt, @@ -226,7 +144,7 @@ pub fn submit_recovery_proposal( // Remove the second_one proposals.pop(); - proposals.push(RecoveryProposal { proposer: caller, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: new_proposal.payload.clone() }); + proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: new_proposal.payload.clone() }); }, (_, _) => { let message = format!( @@ -261,16 +179,16 @@ fn initialize_ballots(simple_node_records: &Vec) -> Vec { existing_ballot .nodes_tied_to_ballot - .push(next.node_principal); + .push(next.node_principal.get().0.clone()); } None => acc.push(NodeOperatorBallot { - principal: next.operator_principal, - nodes_tied_to_ballot: vec![next.node_principal], + principal: next.operator_principal.0.clone(), + nodes_tied_to_ballot: vec![next.node_principal.get().0.clone()], ballot: Ballot::Undecided, security_metadata: SecurityMetadata::empty(), }), @@ -298,7 +216,7 @@ fn vote_on_last_proposal( let correlated_ballot = last_proposal .node_operator_ballots .iter_mut() - .find(|ballot| ballot.principal.eq(&caller)) + .find(|ballot| ballot.principal.eq(&caller.0)) .ok_or(format!( "Caller {} is not eligible to vote on this proposal", caller diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index fcb0532e3f1..a06f7aed78a 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -1,6 +1,7 @@ use candid::CandidType; use serde::Deserialize; +pub mod recovery; pub mod security_metadata; #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] @@ -10,12 +11,14 @@ pub enum Ballot { Undecided, } +#[derive(Debug)] pub enum RecoveryError { InvalidPubKey(String), InvalidSignatureFormat(String), InvalidSignature(String), - PrincipalPublicKeyMismatch(String), + + PayloadSerialization(String), } type Result = std::result::Result; @@ -26,7 +29,8 @@ impl ToString for RecoveryError { Self::InvalidPubKey(s) | Self::InvalidSignatureFormat(s) | Self::InvalidSignature(s) - | Self::PrincipalPublicKeyMismatch(s) => s.to_string(), + | Self::PrincipalPublicKeyMismatch(s) + | Self::PayloadSerialization(s) => s.to_string(), } } } diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs new file mode 100644 index 00000000000..4212534645f --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -0,0 +1,88 @@ +use crate::*; +use candid::{CandidType, Principal}; +use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; +use serde::Deserialize; + +use crate::{security_metadata::SecurityMetadata, Ballot}; + +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +pub enum RecoveryPayload { + Halt, + DoRecovery { height: u64, state_hash: String }, + Unhalt, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct NodeOperatorBallot { + pub principal: Principal, + pub nodes_tied_to_ballot: Vec, + pub ballot: Ballot, + pub security_metadata: SecurityMetadata, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct RecoveryProposal { + /// The principal id of the proposer (must be one of the node + /// operators of the NNS subnet according to the registry at + /// time of submission). + pub proposer: Principal, + /// The timestamp, in seconds, at which the proposal was submitted. + pub submission_timestamp_seconds: u64, + /// The ballots cast by node operators. + pub node_operator_ballots: Vec, + /// Payload for the proposal + pub payload: RecoveryPayload, +} + +impl RecoveryProposal { + pub fn sign(&self, signing_key: &mut SigningKey) -> Result<[u8; 64]> { + let signature = signing_key.sign(&self.signature_payload()?); + Ok(signature.to_bytes()) + } + + pub fn signature_payload(&self) -> Result> { + let self_without_ballots = Self { + node_operator_ballots: vec![], + ..self.clone() + }; + candid::encode_one(self_without_ballots) + .map_err(|e| RecoveryError::PayloadSerialization(e.to_string())) + } +} + +impl RecoveryProposal { + fn is_byzantine_majority(&self, ballot: Ballot) -> bool { + let total_nodes_nodes = self + .node_operator_ballots + .iter() + .map(|bal| bal.nodes_tied_to_ballot.len()) + .sum::(); + let max_faults = (total_nodes_nodes - 1) / 3; + let votes_for_ballot = self + .node_operator_ballots + .iter() + .map(|vote| match vote.ballot == ballot { + // Each vote has the weight of 1 times + // the amount of nodes the node operator + // has in the nns subnet + true => 1 * vote.nodes_tied_to_ballot.len(), + false => 0, + }) + .sum::(); + votes_for_ballot >= (total_nodes_nodes - max_faults) + } + + /// For a root proposal to have a byzantine majority of no, it + /// needs to collect f + 1 "no" votes, where N s the total number + /// of nodes (same as the number of ballots) and f = (N - 1) / 3. + pub fn is_byzantine_majority_no(&self) -> bool { + self.is_byzantine_majority(Ballot::No) + } + + /// For a root proposal to have a byzantine majority of no, it + /// needs to collect f + 1 "no" votes, where N s the total number + /// of nodes (same as the number of ballots) and f = (N - 1) / 3. + pub fn is_byzantine_majority_yes(&self) -> bool { + self.is_byzantine_majority(Ballot::Yes) + } +} From 32c2ba03d784ab0782242142eb0e1fdd0c8ecf28 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 17:43:32 +0100 Subject: [PATCH 30/76] extracting entry points --- .../recovery/impl/canister/canister.rs | 9 ++++----- .../recovery/impl/canister/tests/mod.rs | 9 ++++----- .../canister/tests/proposal_logic_tests.rs | 7 ++++--- .../impl/canister/tests/voting_tests.rs | 5 +++-- .../recovery/impl/src/recovery_proposal.rs | 18 ++++-------------- .../recovery/interface/src/recovery.rs | 11 +++++++++++ 6 files changed, 30 insertions(+), 29 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 91cfefda4f5..601b686359f 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -10,12 +10,11 @@ use ic_cdk::{post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, node_operator_sync::{get_node_operators_in_nns, sync_node_operators, SimpleNodeRecord}, - recovery_proposal::{ - get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner, - NewRecoveryProposal, VoteOnRecoveryProposal, - }, + recovery_proposal::{get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner}, +}; +use ic_nns_handler_recovery_interface::recovery::{ + NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal, }; -use ic_nns_handler_recovery_interface::recovery::RecoveryProposal; fn caller() -> PrincipalId { PrincipalId::from(ic_cdk::caller()) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 5f7e38b7ebc..c25041f2155 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -4,12 +4,11 @@ use candid::Principal; use ed25519_dalek::SigningKey; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; -use ic_nns_handler_recovery::{ - node_operator_sync::SimpleNodeRecord, - recovery_proposal::{NewRecoveryProposal, VoteOnRecoveryProposal}, -}; +use ic_nns_handler_recovery::node_operator_sync::SimpleNodeRecord; use ic_nns_handler_recovery_interface::{ - recovery::RecoveryProposal, security_metadata::SecurityMetadata, Ballot, + recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + security_metadata::SecurityMetadata, + Ballot, }; use ic_protobuf::registry::{ replica_version::v1::{BlessedReplicaVersions, ReplicaVersionRecord}, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index edeb70bdd1c..ee9e61b0b8e 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -1,6 +1,8 @@ use candid::Principal; -use ic_nns_handler_recovery::recovery_proposal::NewRecoveryProposal; -use ic_nns_handler_recovery_interface::{recovery::RecoveryPayload, Ballot}; +use ic_nns_handler_recovery_interface::{ + recovery::{NewRecoveryProposal, RecoveryPayload}, + Ballot, +}; use ic_registry_subnet_type::SubnetType; use pocket_ic::PocketIc; @@ -12,7 +14,6 @@ use super::{ }; // First proposal tests -// TODO: Allow to place multiple recover subnets proposals #[test] fn place_first_proposal() { let mut args = RegistryPreparationArguments::default(); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index b740eef6f23..a293d92969c 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,8 +1,9 @@ use candid::Principal; use ed25519_dalek::SigningKey; -use ic_nns_handler_recovery::recovery_proposal::{NewRecoveryProposal, VoteOnRecoveryProposal}; use ic_nns_handler_recovery_interface::{ - recovery::RecoveryPayload, security_metadata::SecurityMetadata, Ballot, + recovery::{NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal}, + security_metadata::SecurityMetadata, + Ballot, }; use crate::tests::{ diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 757822c630d..b5036018ef1 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -1,14 +1,15 @@ use std::cell::RefCell; -use candid::CandidType; use ic_base_types::PrincipalId; use ic_nns_handler_recovery_interface::{ - recovery::{NodeOperatorBallot, RecoveryPayload, RecoveryProposal}, + recovery::{ + NewRecoveryProposal, NodeOperatorBallot, RecoveryPayload, RecoveryProposal, + VoteOnRecoveryProposal, + }, security_metadata::SecurityMetadata, Ballot, }; use ic_nns_handler_root::now_seconds; -use serde::Deserialize; use crate::node_operator_sync::{get_node_operators_in_nns, SimpleNodeRecord}; @@ -16,17 +17,6 @@ thread_local! { static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; } -#[derive(Debug, CandidType, Deserialize, Clone)] -pub struct NewRecoveryProposal { - pub payload: RecoveryPayload, -} - -#[derive(Debug, CandidType, Deserialize, Clone)] -pub struct VoteOnRecoveryProposal { - pub security_metadata: SecurityMetadata, - pub ballot: Ballot, -} - pub fn get_recovery_proposals() -> Vec { PROPOSALS.with_borrow(|proposals| proposals.clone()) } diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 4212534645f..1525a405199 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -34,6 +34,17 @@ pub struct RecoveryProposal { pub payload: RecoveryPayload, } +#[derive(Debug, CandidType, Deserialize, Clone)] +pub struct NewRecoveryProposal { + pub payload: RecoveryPayload, +} + +#[derive(Debug, CandidType, Deserialize, Clone)] +pub struct VoteOnRecoveryProposal { + pub security_metadata: SecurityMetadata, + pub ballot: Ballot, +} + impl RecoveryProposal { pub fn sign(&self, signing_key: &mut SigningKey) -> Result<[u8; 64]> { let signature = signing_key.sign(&self.signature_payload()?); From 014a92d6814aee968f57a6828d1fa235ae741b67 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 17:50:07 +0100 Subject: [PATCH 31/76] extracting simple node record --- rs/nns/handlers/recovery/impl/canister/canister.rs | 7 ++++--- .../handlers/recovery/impl/canister/tests/mod.rs | 2 +- .../recovery/impl/src/node_operator_sync.rs | 14 +++----------- .../recovery/impl/src/recovery_proposal.rs | 13 +++++++------ rs/nns/handlers/recovery/interface/src/lib.rs | 1 + .../recovery/interface/src/simple_node_record.rs | 8 ++++++++ 6 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/simple_node_record.rs diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 601b686359f..69f72566944 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -9,11 +9,12 @@ use ic_cdk::println; use ic_cdk::{post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, - node_operator_sync::{get_node_operators_in_nns, sync_node_operators, SimpleNodeRecord}, + node_operator_sync::{get_node_operators_in_nns, sync_node_operators}, recovery_proposal::{get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner}, }; -use ic_nns_handler_recovery_interface::recovery::{ - NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal, +use ic_nns_handler_recovery_interface::{ + recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + simple_node_record::SimpleNodeRecord, }; fn caller() -> PrincipalId { diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index c25041f2155..cf28d3f8da4 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -4,10 +4,10 @@ use candid::Principal; use ed25519_dalek::SigningKey; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; -use ic_nns_handler_recovery::node_operator_sync::SimpleNodeRecord; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, + simple_node_record::SimpleNodeRecord, Ballot, }; use ic_protobuf::registry::{ diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs index 46441ca88aa..5e5e8154d4c 100644 --- a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -1,17 +1,9 @@ use std::cell::RefCell; -use candid::CandidType; -use ic_base_types::{NodeId, PrincipalId}; +use ic_nns_handler_recovery_interface::simple_node_record::SimpleNodeRecord; use ic_nns_handler_root::root_proposals::{ get_nns_membership, get_nns_subnet_id, get_node_operator_pid_of_node, }; -use serde::Deserialize; - -#[derive(Debug, Clone, CandidType, Deserialize)] -pub struct SimpleNodeRecord { - pub node_principal: NodeId, - pub operator_principal: PrincipalId, -} thread_local! { static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; @@ -29,8 +21,8 @@ pub async fn sync_node_operators() -> Result<(), String> { get_node_operator_pid_of_node(&node, subnet_membership_registry_version).await?; new_simple_records.push(SimpleNodeRecord { - node_principal: node, - operator_principal: node_operator_id, + node_principal: node.get().0, + operator_principal: node_operator_id.0, }); } diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index b5036018ef1..0a0d8631a76 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -7,11 +7,12 @@ use ic_nns_handler_recovery_interface::{ VoteOnRecoveryProposal, }, security_metadata::SecurityMetadata, + simple_node_record::SimpleNodeRecord, Ballot, }; use ic_nns_handler_root::now_seconds; -use crate::node_operator_sync::{get_node_operators_in_nns, SimpleNodeRecord}; +use crate::node_operator_sync::get_node_operators_in_nns; thread_local! { static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; @@ -30,7 +31,7 @@ pub fn submit_recovery_proposal( // Check if the caller has nodes in nns if !nodes_in_nns .iter() - .any(|node| node.operator_principal == caller) + .any(|node| node.operator_principal == caller.0) { let message = format!( "Caller: {} is not eligible to submit proposals to this canister", @@ -169,16 +170,16 @@ fn initialize_ballots(simple_node_records: &Vec) -> Vec { existing_ballot .nodes_tied_to_ballot - .push(next.node_principal.get().0.clone()); + .push(next.node_principal); } None => acc.push(NodeOperatorBallot { - principal: next.operator_principal.0.clone(), - nodes_tied_to_ballot: vec![next.node_principal.get().0.clone()], + principal: next.operator_principal, + nodes_tied_to_ballot: vec![next.node_principal], ballot: Ballot::Undecided, security_metadata: SecurityMetadata::empty(), }), diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index a06f7aed78a..ff8efaf4cd5 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -3,6 +3,7 @@ use serde::Deserialize; pub mod recovery; pub mod security_metadata; +pub mod simple_node_record; #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] pub enum Ballot { diff --git a/rs/nns/handlers/recovery/interface/src/simple_node_record.rs b/rs/nns/handlers/recovery/interface/src/simple_node_record.rs new file mode 100644 index 00000000000..16422a74af3 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/simple_node_record.rs @@ -0,0 +1,8 @@ +use candid::{CandidType, Principal}; +use serde::Deserialize; + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct SimpleNodeRecord { + pub node_principal: Principal, + pub operator_principal: Principal, +} From f176ebe9fb598d7c9db9371589cf5a2a4257b47b Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 4 Feb 2025 18:33:33 +0100 Subject: [PATCH 32/76] documenting --- .../handlers/recovery/interface/BUILD.bazel | 9 +----- rs/nns/handlers/recovery/interface/src/lib.rs | 14 +++++++++ .../recovery/interface/src/recovery.rs | 29 ++++++++++++++++++- .../interface/src/security_metadata.rs | 19 ++++++++++++ .../interface/src/simple_node_record.rs | 2 ++ 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index 67b318dc026..c6e0b01b9e4 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -4,19 +4,12 @@ package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ # Keep sorted. - "//rs/nervous_system/clients", - "//rs/nns/constants", - "//rs/types/base_types", "@crate_index//:candid", - "@crate_index//:ic-cdk", "@crate_index//:serde", "@crate_index//:ed25519-dalek", ] -MACRO_DEPENDENCIES = [ - # Keep sorted. - "@crate_index//:async-trait", -] +MACRO_DEPENDENCIES = [] DEV_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index ff8efaf4cd5..f9a2f57ebe0 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -6,22 +6,36 @@ pub mod security_metadata; pub mod simple_node_record; #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] +/// Vote types that exist pub enum Ballot { + /// Represents a positive vote on a recovery canister proposal Yes, + /// Represents a vote against a recovery canister proposal No, + /// Represents an undecided state of a vote + /// + /// This is a default value that gets set until the node operator submits its + /// vote on the recovery canister Undecided, } #[derive(Debug)] pub enum RecoveryError { + /// Specifies that the provided bytes couldn't be loaded in a public key InvalidPubKey(String), + /// Specifies that the provided signature bytes couldn't be loaded into + /// signature struct, most likely due to size mismatch InvalidSignatureFormat(String), + /// Provided signature and the resulting signature don't match InvalidSignature(String), + /// Provided principal was not derived from the public key bytes specified PrincipalPublicKeyMismatch(String), + /// Candid error while encoding the recovery canister payload PayloadSerialization(String), } +/// Convenience type to wrap all results in the library type Result = std::result::Result; impl ToString for RecoveryError { diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 1525a405199..70f61b601a4 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -6,17 +6,42 @@ use serde::Deserialize; use crate::{security_metadata::SecurityMetadata, Ballot}; #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +/// Types of acceptable payloads by the recovery canister proposals. pub enum RecoveryPayload { + /// Halt NNS. + /// + /// If adopted, the orchestrator's watching the recovery canister + /// should deem NNS as halted. This proposal maps to a proposal + /// similar to [134605](https://dashboard.internetcomputer.org/proposal/134605). Halt, + /// Do the recovery. + /// + /// If adopted, the orchestrator's watching recovery canister + /// should perform a recovery based on the provided information. + /// This proposal maps to a proposal similar to [134629](https://dashboard.internetcomputer.org/proposal/134629). DoRecovery { height: u64, state_hash: String }, + /// Unhalt NNS. + /// + /// If adopted, the orchestrator's watching the recovery canister + /// should deem NNS as unhalted and working normally. This proposal + /// maps to a proposal similar to [134632](https://dashboard.internetcomputer.org/proposal/134632). Unhalt, } #[derive(Clone, Debug, CandidType, Deserialize)] +/// Represents a vote from one node operator pub struct NodeOperatorBallot { + /// The principal id of the node operator (must be one of the node + /// operators of the NNS subnet according to the registry at + /// time of submission). pub principal: Principal, + /// List of nodes that the node operator controls on the NNS. + /// Each node counts as 1 vote. pub nodes_tied_to_ballot: Vec, + /// The node provider's decision on the observed proposal. pub ballot: Ballot, + /// Metadata used for verifying the user's identity, and integrity of the + /// vote itself. pub security_metadata: SecurityMetadata, } @@ -30,16 +55,18 @@ pub struct RecoveryProposal { pub submission_timestamp_seconds: u64, /// The ballots cast by node operators. pub node_operator_ballots: Vec, - /// Payload for the proposal + /// Payload for the proposal. pub payload: RecoveryPayload, } #[derive(Debug, CandidType, Deserialize, Clone)] +/// Conveniece struct used for submitting a new proposal pub struct NewRecoveryProposal { pub payload: RecoveryPayload, } #[derive(Debug, CandidType, Deserialize, Clone)] +/// Convenience struct used for casting a vote on a proposal pub struct VoteOnRecoveryProposal { pub security_metadata: SecurityMetadata, pub ballot: Ballot, diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 2d2c45c64c1..df1d83fa4dd 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -4,9 +4,24 @@ use ed25519_dalek::{Signature, VerifyingKey}; use serde::Deserialize; #[derive(Clone, Debug, CandidType, Deserialize)] +/// Wrapper struct containing information regarding integrity. pub struct SecurityMetadata { + /// Represents an outcome of a cryptographic operation + /// that includes a private key (also known as signing key) + /// and a payload that is being signed. + /// + /// Should be verified with a corresponding public key (also + /// known as verifying key). pub signature: [[u8; 32]; 2], + /// What is being signed. + /// + /// In context of recovery canister proposal it includes + /// all fields in a proposal except the ballots of node operators + /// serialized as vector of bytes. pub payload: Vec, + /// Verifying key. + /// + /// It is used to verify the authenticity of a signature. pub pub_key: [u8; 32], } @@ -19,11 +34,13 @@ impl SecurityMetadata { } } + /// Verify the authenticity of a whole vote on a recovery canister proposal. pub fn validate_metadata(&self, caller: &Principal) -> Result<()> { self.principal_matches_public_key(caller)?; self.verify() } + /// Verifies the signature authenticity of security metadata. pub fn verify(&self) -> Result<()> { let loaded_public_key = VerifyingKey::from_bytes(&self.pub_key) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; @@ -35,6 +52,8 @@ impl SecurityMetadata { .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) } + /// Verifies if the passed principal is derived from a given public key (also known as + /// verifying key). pub fn principal_matches_public_key(&self, principal: &Principal) -> Result<()> { let loaded_principal = Principal::self_authenticating(self.pub_key); diff --git a/rs/nns/handlers/recovery/interface/src/simple_node_record.rs b/rs/nns/handlers/recovery/interface/src/simple_node_record.rs index 16422a74af3..5332111bd7b 100644 --- a/rs/nns/handlers/recovery/interface/src/simple_node_record.rs +++ b/rs/nns/handlers/recovery/interface/src/simple_node_record.rs @@ -2,6 +2,8 @@ use candid::{CandidType, Principal}; use serde::Deserialize; #[derive(Debug, Clone, CandidType, Deserialize)] +/// Convenience structure for storing information about nodes +/// and their operators coming from NNS on recovery canister. pub struct SimpleNodeRecord { pub node_principal: Principal, pub operator_principal: Principal, From 374e3ee3943eb2186a4716209e2c8a32af566189 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 01:05:01 +0100 Subject: [PATCH 33/76] adding client implementation --- Cargo.lock | 12 ++ Cargo.toml | 1 + rs/nns/handlers/recovery/client/BUILD.bazel | 33 +++++ rs/nns/handlers/recovery/client/Cargo.toml | 15 +++ .../recovery/client/src/implementation.rs | 123 ++++++++++++++++++ rs/nns/handlers/recovery/client/src/lib.rs | 18 +++ .../recovery/impl/canister/tests/mod.rs | 5 +- .../impl/canister/tests/voting_tests.rs | 5 +- .../handlers/recovery/interface/BUILD.bazel | 7 - rs/nns/handlers/recovery/interface/src/lib.rs | 38 +++++- .../recovery/interface/src/recovery.rs | 18 ++- .../interface/src/security_metadata.rs | 4 +- 12 files changed, 256 insertions(+), 23 deletions(-) create mode 100644 rs/nns/handlers/recovery/client/BUILD.bazel create mode 100644 rs/nns/handlers/recovery/client/Cargo.toml create mode 100644 rs/nns/handlers/recovery/client/src/implementation.rs create mode 100644 rs/nns/handlers/recovery/client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3bf282062f8..3cb2c321815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10857,6 +10857,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "ic-nns-handler-recovery-client" +version = "0.9.0" +dependencies = [ + "async-trait", + "candid", + "ed25519-dalek", + "ic-agent", + "ic-nns-handler-recovery-interface", + "serde", +] + [[package]] name = "ic-nns-handler-recovery-interface" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 8699d8bf77c..1d42735fc42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -438,6 +438,7 @@ members = [ "rs/xnet/payload_builder", "rs/xnet/uri", "rs/nns/handlers/recovery/interface", + "rs/nns/handlers/recovery/client", ] resolver = "2" diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel new file mode 100644 index 00000000000..ad6c536be54 --- /dev/null +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -0,0 +1,33 @@ +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +DEPENDENCIES = [ + # Keep sorted. + "//rs/nns/handlers/recovery/interface", + "@crate_index//:candid", + "@crate_index//:serde", + "@crate_index//:ed25519-dalek", + "@crate_index//:ic-agent", +] + +MACRO_DEPENDENCIES = [ + "@crate_index//:async-trait" +] + +DEV_DEPENDENCIES = [] + +MACRO_DEV_DEPENDENCIES = [] + +ALIASES = {} + +rust_library( + name = "client", + srcs = glob(["src/**/*.rs"]), + aliases = ALIASES, + crate_name = "ic_nns_handler_recovery_client", + proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.1.0", + deps = DEPENDENCIES, +) + diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml new file mode 100644 index 00000000000..074c1a742a8 --- /dev/null +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ic-nns-handler-recovery-client" +version.workspace = true +authors.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true + +[dependencies] +candid = { workspace = true } +serde = { workspace = true } +ed25519-dalek.workspace = true +ic-agent.workspace = true +async-trait.workspace = true +ic-nns-handler-recovery-interface.path = "../interface" \ No newline at end of file diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs new file mode 100644 index 00000000000..f022c2349e5 --- /dev/null +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -0,0 +1,123 @@ +use async_trait::async_trait; +use candid::{CandidType, Principal}; +use ed25519_dalek::SigningKey; +use ic_agent::Agent; +use ic_nns_handler_recovery_interface::{ + recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + security_metadata::SecurityMetadata, + simple_node_record::SimpleNodeRecord, + Ballot, RecoveryError, Result, VerifyIntegirty, +}; + +use crate::RecoveryCanister; + +pub struct RecoveryCanisterImpl { + canister_id: Principal, + ic_agent: Agent, + signing_key: SigningKey, +} + +impl RecoveryCanisterImpl { + pub fn new(ic_agent: Agent, canister_id: Principal, signing_key: SigningKey) -> Self { + Self { + ic_agent, + canister_id, + signing_key, + } + } + + async fn query(&self, method: &str, args: P) -> Result + where + T: CandidType + for<'a> candid::Deserialize<'a>, + P: Into>, + { + self.ic_agent + .query(&self.canister_id, method) + .with_arg(args) + .call() + .await + .map(|response| candid::decode_one(&response)) + .map_err(|e| RecoveryError::AgentError(e.to_string()))? + .map_err(|e| RecoveryError::CandidError(e.to_string())) + } + + async fn update(&self, method: &str, args: P) -> Result + where + T: CandidType + for<'a> candid::Deserialize<'a>, + P: Into>, + { + self.ic_agent + .update(&self.canister_id, method) + .with_arg(args) + .call_and_wait() + .await + .map(|response| candid::decode_one(&response)) + .map_err(|e| RecoveryError::AgentError(e.to_string()))? + .map_err(|e| e.into()) + } + + fn ensure_not_anonymous(&self) -> Result<()> { + let principal = self + .ic_agent + .get_principal() + .map_err(|e| RecoveryError::AgentError(e))?; + + match Principal::anonymous().eq(&principal) { + false => Ok(()), + true => Err(RecoveryError::InvalidIdentity( + "Anonymous sender can't proceed with the request action".to_string(), + )), + } + } +} + +#[async_trait] +impl RecoveryCanister for RecoveryCanisterImpl { + async fn get_node_operators_in_nns(&self) -> Result> { + self.query("get_node_operators_in_nns", candid::encode_one(())?) + .await + } + + async fn get_pending_recovery_proposals(&self) -> Result> { + self.query("get_pending_recovery_proposals", candid::encode_one(())?) + .await + } + + async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()> { + self.ensure_not_anonymous()?; + + let proposal_chain = self.get_pending_recovery_proposals().await?; + proposal_chain.iter().verify()?; + + let last_proposal = proposal_chain + .last() + .ok_or(RecoveryError::NoProposalsToVoteOn( + "There are no proposals to be voted in.".to_string(), + ))?; + + let mut signing_key = self.signing_key.clone(); + + self.update( + "vote_on_proposal", + candid::encode_one(VoteOnRecoveryProposal { + security_metadata: SecurityMetadata { + signature: last_proposal.sign(&mut signing_key)?, + payload: last_proposal.signature_payload()?, + pub_key: signing_key.verifying_key().to_bytes(), + }, + ballot, + })?, + ) + .await + } + + async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()> { + self.ensure_not_anonymous()?; + + self.update( + "submit_new_recovery_proposal", + candid::encode_one(new_proposal)?, + ) + .await + } +} diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs new file mode 100644 index 00000000000..0dd14c89fdd --- /dev/null +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -0,0 +1,18 @@ +use async_trait::async_trait; + +use ic_nns_handler_recovery_interface::recovery::{NewRecoveryProposal, RecoveryProposal}; +use ic_nns_handler_recovery_interface::Result; +use ic_nns_handler_recovery_interface::{simple_node_record::SimpleNodeRecord, Ballot}; + +pub mod implementation; + +#[async_trait] +pub trait RecoveryCanister { + async fn get_node_operators_in_nns(&self) -> Result>; + + async fn get_pending_recovery_proposals(&self) -> Result>; + + async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()>; + + async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()>; +} diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index cf28d3f8da4..14acc8e2957 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -323,9 +323,6 @@ fn vote_with_only_ballot( let pending = get_pending(pic, canister); let last = pending.last().unwrap(); let signature = last.sign(&mut sender.signing_key).unwrap(); - let mut parts = [[0; 32]; 2]; - parts[0].copy_from_slice(&signature[..32]); - parts[1].copy_from_slice(&signature[32..]); vote( pic, @@ -336,7 +333,7 @@ fn vote_with_only_ballot( payload: last .signature_payload() .expect("Should be able to fetch payload"), - signature: parts, + signature, pub_key: sender.signing_key.verifying_key().to_bytes(), }, ballot, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index a293d92969c..2817f686689 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -155,9 +155,6 @@ fn disallow_votes_wrong_public_key() { let mut new_key_pair = SigningKey::generate(&mut rand::rngs::OsRng); let signature = last_proposal.sign(&mut new_key_pair).unwrap(); - let mut parts = [[0; 32]; 2]; - parts[0].copy_from_slice(&signature[..32]); - parts[1].copy_from_slice(&signature[32..]); let response = vote( &pic, @@ -168,7 +165,7 @@ fn disallow_votes_wrong_public_key() { payload: last_proposal .signature_payload() .expect("Should be able to serialize payload"), - signature: parts, + signature, pub_key: new_key_pair.verifying_key().to_bytes(), }, ballot: Ballot::Yes, diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index c6e0b01b9e4..2f399653095 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -27,10 +27,3 @@ rust_library( deps = DEPENDENCIES, ) -rust_test( - name = "recovery_interface_test", - aliases = ALIASES, - crate = ":interface", - proc_macro_deps = MACRO_DEPENDENCIES + MACRO_DEV_DEPENDENCIES, - deps = DEPENDENCIES + DEV_DEPENDENCIES, -) diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index f9a2f57ebe0..56c1b139789 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -33,10 +33,23 @@ pub enum RecoveryError { /// Candid error while encoding the recovery canister payload PayloadSerialization(String), + + AgentError(String), + CandidError(String), + + InvalidIdentity(String), + + NoProposalsToVoteOn(String), +} + +impl From for RecoveryError { + fn from(value: candid::Error) -> Self { + Self::CandidError(value.to_string()) + } } /// Convenience type to wrap all results in the library -type Result = std::result::Result; +pub type Result = std::result::Result; impl ToString for RecoveryError { fn to_string(&self) -> String { @@ -45,7 +58,28 @@ impl ToString for RecoveryError { | Self::InvalidSignatureFormat(s) | Self::InvalidSignature(s) | Self::PrincipalPublicKeyMismatch(s) - | Self::PayloadSerialization(s) => s.to_string(), + | Self::PayloadSerialization(s) + | Self::AgentError(s) + | Self::CandidError(s) + | Self::InvalidIdentity(s) + | Self::NoProposalsToVoteOn(s) => s.to_string(), } } } + +pub trait VerifyIntegirty { + fn verify(&self) -> Result<()>; +} + +impl<'a, I, T> VerifyIntegirty for I +where + I: Iterator + Clone, + T: VerifyIntegirty + 'a, +{ + fn verify(&self) -> Result<()> { + self.clone() + .map(|item| item.verify()) + .find(|res| res.is_err()) + .unwrap_or(Ok(())) + } +} diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 70f61b601a4..317b3780c40 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -73,9 +73,9 @@ pub struct VoteOnRecoveryProposal { } impl RecoveryProposal { - pub fn sign(&self, signing_key: &mut SigningKey) -> Result<[u8; 64]> { + pub fn sign(&self, signing_key: &mut SigningKey) -> Result<[[u8; 32]; 2]> { let signature = signing_key.sign(&self.signature_payload()?); - Ok(signature.to_bytes()) + Ok([*signature.r_bytes(), *signature.s_bytes()]) } pub fn signature_payload(&self) -> Result> { @@ -86,9 +86,7 @@ impl RecoveryProposal { candid::encode_one(self_without_ballots) .map_err(|e| RecoveryError::PayloadSerialization(e.to_string())) } -} -impl RecoveryProposal { fn is_byzantine_majority(&self, ballot: Ballot) -> bool { let total_nodes_nodes = self .node_operator_ballots @@ -124,3 +122,15 @@ impl RecoveryProposal { self.is_byzantine_majority(Ballot::Yes) } } + +impl VerifyIntegirty for NodeOperatorBallot { + fn verify(&self) -> Result<()> { + self.security_metadata.validate_metadata(&self.principal) + } +} + +impl VerifyIntegirty for RecoveryProposal { + fn verify(&self) -> Result<()> { + self.node_operator_ballots.iter().verify() + } +} diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index df1d83fa4dd..2ffee777f59 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -37,11 +37,11 @@ impl SecurityMetadata { /// Verify the authenticity of a whole vote on a recovery canister proposal. pub fn validate_metadata(&self, caller: &Principal) -> Result<()> { self.principal_matches_public_key(caller)?; - self.verify() + self.verify_signature() } /// Verifies the signature authenticity of security metadata. - pub fn verify(&self) -> Result<()> { + pub fn verify_signature(&self) -> Result<()> { let loaded_public_key = VerifyingKey::from_bytes(&self.pub_key) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; let signature = Signature::from_slice(self.signature.as_flattened()) From 5c6d44ba3910d83509ce3e18c0de6d86609cd76c Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 11:22:27 +0100 Subject: [PATCH 34/76] changing the node operator api --- .../recovery/client/src/implementation.rs | 4 +- rs/nns/handlers/recovery/client/src/lib.rs | 8 +++- .../handlers/recovery/client/src/tests/mod.rs | 0 .../recovery/impl/canister/canister.rs | 4 +- .../recovery/impl/canister/tests/mod.rs | 7 ++- .../recovery/impl/src/node_operator_sync.rs | 24 ++++++---- .../recovery/impl/src/recovery_proposal.rs | 44 +++++++------------ rs/nns/handlers/recovery/interface/src/lib.rs | 3 +- .../recovery/interface/src/recovery_init.rs | 3 ++ ...cord.rs => simple_node_operator_record.rs} | 6 +-- 10 files changed, 55 insertions(+), 48 deletions(-) create mode 100644 rs/nns/handlers/recovery/client/src/tests/mod.rs create mode 100644 rs/nns/handlers/recovery/interface/src/recovery_init.rs rename rs/nns/handlers/recovery/interface/src/{simple_node_record.rs => simple_node_operator_record.rs} (69%) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index f022c2349e5..1c27a476c2d 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -5,7 +5,7 @@ use ic_agent::Agent; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, - simple_node_record::SimpleNodeRecord, + simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, RecoveryError, Result, VerifyIntegirty, }; @@ -73,7 +73,7 @@ impl RecoveryCanisterImpl { #[async_trait] impl RecoveryCanister for RecoveryCanisterImpl { - async fn get_node_operators_in_nns(&self) -> Result> { + async fn get_node_operators_in_nns(&self) -> Result> { self.query("get_node_operators_in_nns", candid::encode_one(())?) .await } diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index 0dd14c89fdd..3d4ff931748 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -2,13 +2,17 @@ use async_trait::async_trait; use ic_nns_handler_recovery_interface::recovery::{NewRecoveryProposal, RecoveryProposal}; use ic_nns_handler_recovery_interface::Result; -use ic_nns_handler_recovery_interface::{simple_node_record::SimpleNodeRecord, Ballot}; +use ic_nns_handler_recovery_interface::{ + simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, +}; pub mod implementation; +#[cfg(test)] +mod tests; #[async_trait] pub trait RecoveryCanister { - async fn get_node_operators_in_nns(&self) -> Result>; + async fn get_node_operators_in_nns(&self) -> Result>; async fn get_pending_recovery_proposals(&self) -> Result>; diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 69f72566944..4b732f70bb8 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -14,7 +14,7 @@ use ic_nns_handler_recovery::{ }; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, - simple_node_record::SimpleNodeRecord, + simple_node_operator_record::SimpleNodeOperatorRecord, }; fn caller() -> PrincipalId { @@ -47,7 +47,7 @@ fn get_pending_recovery_proposals() -> Vec { } #[query] -fn get_current_nns_node_operators() -> Vec { +fn get_current_nns_node_operators() -> Vec { get_node_operators_in_nns() } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 14acc8e2957..3693c1dbc7e 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -7,7 +7,7 @@ use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, - simple_node_record::SimpleNodeRecord, + simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, }; use ic_protobuf::registry::{ @@ -362,7 +362,10 @@ fn vote( response } -fn get_current_node_operators(pic: &PocketIc, canister: Principal) -> Vec { +fn get_current_node_operators( + pic: &PocketIc, + canister: Principal, +) -> Vec { let response = pic .query_call( canister.into(), diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs index 5e5e8154d4c..93114649cb2 100644 --- a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -1,12 +1,13 @@ -use std::cell::RefCell; +use std::{cell::RefCell, collections::BTreeMap}; -use ic_nns_handler_recovery_interface::simple_node_record::SimpleNodeRecord; +use candid::Principal; +use ic_nns_handler_recovery_interface::simple_node_operator_record::SimpleNodeOperatorRecord; use ic_nns_handler_root::root_proposals::{ get_nns_membership, get_nns_subnet_id, get_node_operator_pid_of_node, }; thread_local! { - static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; + static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; } pub async fn sync_node_operators() -> Result<(), String> { @@ -14,23 +15,28 @@ pub async fn sync_node_operators() -> Result<(), String> { let (nns_nodes, subnet_membership_registry_version) = get_nns_membership(&nns_subnet_id).await?; - let mut new_simple_records = vec![]; + let mut new_simple_records: BTreeMap> = BTreeMap::new(); for node in nns_nodes { let node_operator_id = get_node_operator_pid_of_node(&node, subnet_membership_registry_version).await?; - new_simple_records.push(SimpleNodeRecord { - node_principal: node.get().0, - operator_principal: node_operator_id.0, - }); + if let Some(entry) = new_simple_records.get_mut(&node_operator_id.0) { + entry.push(node.get().0); + } else { + new_simple_records.insert(node_operator_id.0, vec![node.get().0]); + } } + let new_simple_records = new_simple_records + .into_iter() + .map(|(principal, nodes)| SimpleNodeOperatorRecord { principal, nodes }) + .collect(); NODE_OPERATORS_IN_NNS.replace(new_simple_records); Ok(()) } -pub fn get_node_operators_in_nns() -> Vec { +pub fn get_node_operators_in_nns() -> Vec { NODE_OPERATORS_IN_NNS.with_borrow(|records| records.clone()) } diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 0a0d8631a76..c835de1d60a 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -7,7 +7,7 @@ use ic_nns_handler_recovery_interface::{ VoteOnRecoveryProposal, }, security_metadata::SecurityMetadata, - simple_node_record::SimpleNodeRecord, + simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, }; use ic_nns_handler_root::now_seconds; @@ -26,12 +26,12 @@ pub fn submit_recovery_proposal( new_proposal: NewRecoveryProposal, caller: PrincipalId, ) -> Result<(), String> { - let nodes_in_nns = get_node_operators_in_nns(); + let node_operators_in_nns = get_node_operators_in_nns(); // Check if the caller has nodes in nns - if !nodes_in_nns + if !node_operators_in_nns .iter() - .any(|node| node.operator_principal == caller.0) + .any(|node| node.principal == caller.0) { let message = format!( "Caller: {} is not eligible to submit proposals to this canister", @@ -51,7 +51,7 @@ pub fn submit_recovery_proposal( proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), - node_operator_ballots: initialize_ballots(&nodes_in_nns), + node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Halt, }); } @@ -88,7 +88,7 @@ pub fn submit_recovery_proposal( proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), - node_operator_ballots: initialize_ballots(&nodes_in_nns), + node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: new_proposal.payload.clone(), }); } @@ -124,7 +124,7 @@ pub fn submit_recovery_proposal( proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), - node_operator_ballots: initialize_ballots(&nodes_in_nns), + node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Unhalt, }); }, @@ -135,7 +135,7 @@ pub fn submit_recovery_proposal( // Remove the second_one proposals.pop(); - proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&nodes_in_nns), payload: new_proposal.payload.clone() }); + proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: new_proposal.payload.clone() }); }, (_, _) => { let message = format!( @@ -164,28 +164,18 @@ pub fn submit_recovery_proposal( }) } -fn initialize_ballots(simple_node_records: &Vec) -> Vec { +fn initialize_ballots( + simple_node_records: &Vec, +) -> Vec { simple_node_records .iter() - .fold(Vec::new(), |mut acc, next| { - match acc - .iter_mut() - .find(|operator_ballot| operator_ballot.principal == next.operator_principal) - { - Some(existing_ballot) => { - existing_ballot - .nodes_tied_to_ballot - .push(next.node_principal); - } - None => acc.push(NodeOperatorBallot { - principal: next.operator_principal, - nodes_tied_to_ballot: vec![next.node_principal], - ballot: Ballot::Undecided, - security_metadata: SecurityMetadata::empty(), - }), - } - acc + .map(|operator_record| NodeOperatorBallot { + principal: operator_record.principal.clone(), + nodes_tied_to_ballot: operator_record.nodes.clone(), + ballot: Ballot::Undecided, + security_metadata: SecurityMetadata::empty(), }) + .collect() } pub fn vote_on_proposal_inner( diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 56c1b139789..a824f1c4310 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -2,8 +2,9 @@ use candid::CandidType; use serde::Deserialize; pub mod recovery; +pub mod recovery_init; pub mod security_metadata; -pub mod simple_node_record; +pub mod simple_node_operator_record; #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] /// Vote types that exist diff --git a/rs/nns/handlers/recovery/interface/src/recovery_init.rs b/rs/nns/handlers/recovery/interface/src/recovery_init.rs new file mode 100644 index 00000000000..ebab2043df6 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/recovery_init.rs @@ -0,0 +1,3 @@ +pub struct RecoveryInitArgs { + pub initial_node_operator_records: Vec, +} diff --git a/rs/nns/handlers/recovery/interface/src/simple_node_record.rs b/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs similarity index 69% rename from rs/nns/handlers/recovery/interface/src/simple_node_record.rs rename to rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs index 5332111bd7b..34b734d4fe4 100644 --- a/rs/nns/handlers/recovery/interface/src/simple_node_record.rs +++ b/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs @@ -4,7 +4,7 @@ use serde::Deserialize; #[derive(Debug, Clone, CandidType, Deserialize)] /// Convenience structure for storing information about nodes /// and their operators coming from NNS on recovery canister. -pub struct SimpleNodeRecord { - pub node_principal: Principal, - pub operator_principal: Principal, +pub struct SimpleNodeOperatorRecord { + pub principal: Principal, + pub nodes: Vec, } From 9b214946aea0ed086b8cabccb7ea0b69e5cd35ed Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 14:07:08 +0100 Subject: [PATCH 35/76] adding structure for init args --- .../recovery/impl/canister/canister.rs | 19 +++++++- .../impl/canister/tests/initial_args_test.rs | 46 +++++++++++++++++++ .../recovery/impl/canister/tests/mod.rs | 1 + .../recovery/impl/src/node_operator_sync.rs | 26 ++++++++++- .../recovery/impl/src/recovery_proposal.rs | 4 +- .../recovery/interface/src/recovery_init.rs | 8 +++- .../src/simple_node_operator_record.rs | 2 +- 7 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 4b732f70bb8..caf131da7b1 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -1,3 +1,4 @@ +use candid::Decode; use ic_base_types::PrincipalId; use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_cdk_macros::init; @@ -6,14 +7,17 @@ use ic_nervous_system_common::serve_metrics; #[cfg(target_arch = "wasm32")] use ic_cdk::println; -use ic_cdk::{post_upgrade, query, update}; +use ic_cdk::{api::call::arg_data_raw, post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, - node_operator_sync::{get_node_operators_in_nns, sync_node_operators}, + node_operator_sync::{ + get_node_operators_in_nns, set_initial_node_operators, sync_node_operators, + }, recovery_proposal::{get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner}, }; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, }; @@ -91,6 +95,17 @@ fn init() { } async fn setup_node_operator_update() { + let data = arg_data_raw(); + ic_cdk::println!("Received: {:?}", data); + match Decode!(&data, RecoveryInitArgs) { + Ok(args) => { + set_initial_node_operators(args.initial_node_operator_records); + } + Err(e) => { + ic_cdk::println!("Couldn't deseriliaze candid init payload. Skipping setting initial node operators. Error: {}", e.to_string()) + } + } + ic_cdk::println!("Started Sync for new node operators on NNS"); if let Err(e) = sync_node_operators().await { ic_cdk::println!("{}", e); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs new file mode 100644 index 00000000000..990c560f21c --- /dev/null +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -0,0 +1,46 @@ +use candid::Principal; +use ic_base_types::PrincipalId; +use ic_nns_handler_recovery_interface::{ + recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, +}; +use pocket_ic::{PocketIc, PocketIcBuilder}; + +use super::{fetch_canister_wasm, get_current_node_operators}; + +fn setup_and_install_canister(initial_arg: RecoveryInitArgs) -> (PocketIc, Principal) { + let pic = PocketIcBuilder::new() + .with_nns_subnet() + .with_application_subnet() + .build(); + + let app_subnets = pic.topology().get_app_subnets(); + + let subnet_id = app_subnets.first().expect("Should contain one app subnet"); + let canister = pic.create_canister_on_subnet(None, None, *subnet_id); + pic.add_cycles(canister, 100_000_000_000_000); + let encoded = candid::encode_one(initial_arg).unwrap(); + println!("Sending: {:?}", encoded); + pic.install_canister( + canister, + fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), + encoded, + None, + ); + + (pic, canister) +} + +#[test] +fn set_initial_args() { + let initial_arg = RecoveryInitArgs { + initial_node_operator_records: vec![SimpleNodeOperatorRecord { + operator_id: PrincipalId::new_user_test_id(1).0, + nodes: vec![], + }], + }; + let (pic, canister) = setup_and_install_canister(initial_arg); + + let node_operators = get_current_node_operators(&pic, canister); + + assert!(node_operators.len().eq(&1)) +} diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 3693c1dbc7e..24445973fe4 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -34,6 +34,7 @@ use test_helpers::{ prepare_registry_with_nodes_and_node_operator_id, }; +mod initial_args_test; mod node_providers_sync_tests; mod proposal_logic_tests; mod test_helpers; diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs index 93114649cb2..82b21578498 100644 --- a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -8,6 +8,7 @@ use ic_nns_handler_root::root_proposals::{ thread_local! { static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; + static INITIAL_NODE_OPERATORS: RefCell> = const { RefCell::new(Vec::new()) }; } pub async fn sync_node_operators() -> Result<(), String> { @@ -30,13 +31,34 @@ pub async fn sync_node_operators() -> Result<(), String> { let new_simple_records = new_simple_records .into_iter() - .map(|(principal, nodes)| SimpleNodeOperatorRecord { principal, nodes }) + .map(|(operator_id, nodes)| SimpleNodeOperatorRecord { operator_id, nodes }) .collect(); NODE_OPERATORS_IN_NNS.replace(new_simple_records); Ok(()) } +pub fn set_initial_node_operators(initial: Vec) { + INITIAL_NODE_OPERATORS.replace(initial); +} + pub fn get_node_operators_in_nns() -> Vec { - NODE_OPERATORS_IN_NNS.with_borrow(|records| records.clone()) + let initial = INITIAL_NODE_OPERATORS.with_borrow(|records| records.clone()); + let obtained_from_sync = NODE_OPERATORS_IN_NNS.with_borrow(|records| records.clone()); + + let mut merged: Vec<_> = obtained_from_sync + .clone() + .into_iter() + .chain(initial.clone()) + .collect(); + merged.sort_by(|a, b| b.nodes.len().cmp(&a.nodes.len())); + merged.dedup_by(|a, b| a.operator_id == b.operator_id); + + if !initial.is_empty() { + ic_cdk::println!("Initial: {:?}", initial); + ic_cdk::println!("Obtained: {:?}", obtained_from_sync); + ic_cdk::println!("Merged: {:?}", merged); + } + + merged } diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index c835de1d60a..d0fdc60404c 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -31,7 +31,7 @@ pub fn submit_recovery_proposal( // Check if the caller has nodes in nns if !node_operators_in_nns .iter() - .any(|node| node.principal == caller.0) + .any(|node| node.operator_id == caller.0) { let message = format!( "Caller: {} is not eligible to submit proposals to this canister", @@ -170,7 +170,7 @@ fn initialize_ballots( simple_node_records .iter() .map(|operator_record| NodeOperatorBallot { - principal: operator_record.principal.clone(), + principal: operator_record.operator_id.clone(), nodes_tied_to_ballot: operator_record.nodes.clone(), ballot: Ballot::Undecided, security_metadata: SecurityMetadata::empty(), diff --git a/rs/nns/handlers/recovery/interface/src/recovery_init.rs b/rs/nns/handlers/recovery/interface/src/recovery_init.rs index ebab2043df6..5cf62bdf311 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery_init.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery_init.rs @@ -1,3 +1,9 @@ +use candid::CandidType; +use serde::Deserialize; + +use crate::simple_node_operator_record::SimpleNodeOperatorRecord; + +#[derive(CandidType, Clone, Deserialize)] pub struct RecoveryInitArgs { - pub initial_node_operator_records: Vec, + pub initial_node_operator_records: Vec, } diff --git a/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs b/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs index 34b734d4fe4..3178c0b375b 100644 --- a/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs +++ b/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs @@ -5,6 +5,6 @@ use serde::Deserialize; /// Convenience structure for storing information about nodes /// and their operators coming from NNS on recovery canister. pub struct SimpleNodeOperatorRecord { - pub principal: Principal, + pub operator_id: Principal, pub nodes: Vec, } From 8f12a2c5d7b7f890728cea139d255e534a196635 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 16:17:40 +0100 Subject: [PATCH 36/76] add possibility to inject principals --- .../recovery/impl/canister/canister.rs | 27 ++++------ .../impl/canister/tests/initial_args_test.rs | 54 ++++++++++++++++++- .../recovery/impl/canister/tests/mod.rs | 14 ++++- .../recovery/interface/src/recovery.rs | 7 +++ .../recovery/interface/src/recovery_init.rs | 6 +-- .../src/simple_node_operator_record.rs | 6 +-- 6 files changed, 88 insertions(+), 26 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index caf131da7b1..3b60d4e548f 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -1,4 +1,3 @@ -use candid::Decode; use ic_base_types::PrincipalId; use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_cdk_macros::init; @@ -7,7 +6,7 @@ use ic_nervous_system_common::serve_metrics; #[cfg(target_arch = "wasm32")] use ic_cdk::println; -use ic_cdk::{api::call::arg_data_raw, post_upgrade, query, update}; +use ic_cdk::{post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, node_operator_sync::{ @@ -26,9 +25,9 @@ fn caller() -> PrincipalId { } #[post_upgrade] -fn canister_post_upgrade() { +fn canister_post_upgrade(arg: RecoveryInitArgs) { println!("canister_post_upgrade"); - init(); + init(arg); } ic_nervous_system_common_build_metadata::define_get_build_metadata_candid_method_cdk! {} @@ -85,25 +84,19 @@ fn main() { fn main() {} #[init] -fn init() { +fn init(arg: RecoveryInitArgs) { ic_cdk_timers::set_timer(std::time::Duration::from_secs(0), || { - ic_cdk::spawn(setup_node_operator_update()); + ic_cdk::println!("Received: {:?}", arg); + ic_cdk::spawn(setup_node_operator_update(Some(arg))); }); ic_cdk_timers::set_timer_interval(std::time::Duration::from_secs(60 * 60 * 24), || { - ic_cdk::spawn(setup_node_operator_update()); + ic_cdk::spawn(setup_node_operator_update(None)); }); } -async fn setup_node_operator_update() { - let data = arg_data_raw(); - ic_cdk::println!("Received: {:?}", data); - match Decode!(&data, RecoveryInitArgs) { - Ok(args) => { - set_initial_node_operators(args.initial_node_operator_records); - } - Err(e) => { - ic_cdk::println!("Couldn't deseriliaze candid init payload. Skipping setting initial node operators. Error: {}", e.to_string()) - } +async fn setup_node_operator_update(args: Option) { + if let Some(args) = args { + set_initial_node_operators(args.initial_node_operator_records); } ic_cdk::println!("Started Sync for new node operators on NNS"); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index 990c560f21c..4856d659a6e 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -1,11 +1,16 @@ use candid::Principal; use ic_base_types::PrincipalId; use ic_nns_handler_recovery_interface::{ - recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, + recovery::{NewRecoveryProposal, RecoveryPayload}, + recovery_init::RecoveryInitArgs, + simple_node_operator_record::SimpleNodeOperatorRecord, + Ballot, }; use pocket_ic::{PocketIc, PocketIcBuilder}; -use super::{fetch_canister_wasm, get_current_node_operators}; +use crate::tests::{get_pending, vote_with_only_ballot}; + +use super::{fetch_canister_wasm, get_current_node_operators, submit_proposal, NodeOperatorArg}; fn setup_and_install_canister(initial_arg: RecoveryInitArgs) -> (PocketIc, Principal) { let pic = PocketIcBuilder::new() @@ -20,6 +25,7 @@ fn setup_and_install_canister(initial_arg: RecoveryInitArgs) -> (PocketIc, Princ pic.add_cycles(canister, 100_000_000_000_000); let encoded = candid::encode_one(initial_arg).unwrap(); println!("Sending: {:?}", encoded); + println!("Size: {}", encoded.len()); pic.install_canister( canister, fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), @@ -30,6 +36,10 @@ fn setup_and_install_canister(initial_arg: RecoveryInitArgs) -> (PocketIc, Princ (pic, canister) } +fn initialize_node_operators(num: usize) -> Vec { + (0..num).map(|_| NodeOperatorArg::new(0)).collect() +} + #[test] fn set_initial_args() { let initial_arg = RecoveryInitArgs { @@ -44,3 +54,43 @@ fn set_initial_args() { assert!(node_operators.len().eq(&1)) } + +#[test] +fn initial_operators_should_be_able_to_place_proposals_and_vote() { + let mut initial_node_operators = initialize_node_operators(5); + let initial_arg = RecoveryInitArgs { + initial_node_operator_records: initial_node_operators + .iter() + .map(|no| no.clone().into()) + .collect(), + }; + + let (pic, canister) = setup_and_install_canister(initial_arg); + + let first = initial_node_operators.first().unwrap(); + let response = submit_proposal( + &pic, + canister, + first.principal.0.clone(), + NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }, + ); + + assert!(response.is_ok()); + + // All of the operators vote on the proposal + for operator in initial_node_operators.iter_mut() { + let response = vote_with_only_ballot(&pic, canister, operator, Ballot::Yes); + assert!(response.is_ok()); + + // Even if their vote is efectively 0, they cannot + // vote twice + let response = vote_with_only_ballot(&pic, canister, operator, Ballot::Yes); + assert!(response.is_err()) + } + + let pending = get_pending(&pic, canister); + let last = pending.last().unwrap(); + assert!(!last.is_byzantine_majority_yes()) +} diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 24445973fe4..16b0b52fdc3 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -6,6 +6,7 @@ use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery_init::RecoveryInitArgs, security_metadata::SecurityMetadata, simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, @@ -92,6 +93,17 @@ struct NodeOperatorArg { signing_key: SigningKey, } +impl From for SimpleNodeOperatorRecord { + fn from(value: NodeOperatorArg) -> Self { + Self { + operator_id: value.principal.0, + nodes: (0..value.num_nodes) + .map(|i| PrincipalId::new_node_test_id(i as u64).0) + .collect(), + } + } +} + impl NodeOperatorArg { fn new(num_nodes: u8) -> Self { let mut csprng = OsRng; @@ -254,7 +266,7 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr pic.install_canister( canister, fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), - candid::encode_one(()).unwrap(), + candid::encode_one(RecoveryInitArgs::default()).unwrap(), None, ); diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 317b3780c40..53fc0dc65f1 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -93,6 +93,13 @@ impl RecoveryProposal { .iter() .map(|bal| bal.nodes_tied_to_ballot.len()) .sum::(); + // If all node operators in the canister + // were added as initial node operators + // they would have 0 nodes meaning that + // their total sum of nodes would be 0 + if total_nodes_nodes == 0 { + return false; + } let max_faults = (total_nodes_nodes - 1) / 3; let votes_for_ballot = self .node_operator_ballots diff --git a/rs/nns/handlers/recovery/interface/src/recovery_init.rs b/rs/nns/handlers/recovery/interface/src/recovery_init.rs index 5cf62bdf311..35738b21ebb 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery_init.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery_init.rs @@ -1,9 +1,9 @@ -use candid::CandidType; -use serde::Deserialize; +use candid::{CandidType, Deserialize}; +use serde::Serialize; use crate::simple_node_operator_record::SimpleNodeOperatorRecord; -#[derive(CandidType, Clone, Deserialize)] +#[derive(CandidType, Debug, Deserialize, Serialize, Default)] pub struct RecoveryInitArgs { pub initial_node_operator_records: Vec, } diff --git a/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs b/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs index 3178c0b375b..d52fb27741e 100644 --- a/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs +++ b/rs/nns/handlers/recovery/interface/src/simple_node_operator_record.rs @@ -1,7 +1,7 @@ -use candid::{CandidType, Principal}; -use serde::Deserialize; +use candid::{CandidType, Deserialize, Principal}; +use serde::Serialize; -#[derive(Debug, Clone, CandidType, Deserialize)] +#[derive(CandidType, Debug, Deserialize, Serialize, Clone)] /// Convenience structure for storing information about nodes /// and their operators coming from NNS on recovery canister. pub struct SimpleNodeOperatorRecord { From 059bf415379f61ea2e96febbf9311e09c3849454 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 16:59:04 +0100 Subject: [PATCH 37/76] adding convenience methods for proposal validation --- .../impl/canister/tests/initial_args_test.rs | 5 ++-- rs/nns/handlers/recovery/interface/src/lib.rs | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index 4856d659a6e..525044371c3 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -4,7 +4,7 @@ use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload}, recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, - Ballot, + Ballot, VerifyIntegirty, }; use pocket_ic::{PocketIc, PocketIcBuilder}; @@ -92,5 +92,6 @@ fn initial_operators_should_be_able_to_place_proposals_and_vote() { let pending = get_pending(&pic, canister); let last = pending.last().unwrap(); - assert!(!last.is_byzantine_majority_yes()) + assert!(!last.is_byzantine_majority_yes()); + assert!(last.verify().is_ok()) } diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index a824f1c4310..89903901fcb 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -1,4 +1,5 @@ use candid::CandidType; +use recovery::RecoveryProposal; use serde::Deserialize; pub mod recovery; @@ -84,3 +85,28 @@ where .unwrap_or(Ok(())) } } + +/// Convenience method that explicitly checks the authenticity +/// of sent proposal chain. It is equivalent to calling `proposals.iter().verify()` +pub fn verify_signatures_and_authenticity_of_all_proposals_and_votes( + proposals: Vec<&RecoveryProposal>, +) -> Result<()> { + proposals + .clone() + .iter() + .map(|proposal| { + proposal + .node_operator_ballots + .clone() + .iter() + .map(|ballot| { + ballot + .security_metadata + .validate_metadata(&ballot.principal) + }) + .find(|ballot_validation_response| ballot_validation_response.is_err()) + .unwrap_or(Ok(())) + }) + .find(|proposal_validation_response| proposal_validation_response.is_err()) + .unwrap_or(Ok(())) +} From 994c8abfbe1f90133af0000fba18a913330242a2 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 17:52:33 +0100 Subject: [PATCH 38/76] adding transformations --- Cargo.lock | 3 + .../handlers/recovery/interface/BUILD.bazel | 4 +- rs/nns/handlers/recovery/interface/Cargo.toml | 3 + rs/nns/handlers/recovery/interface/src/lib.rs | 13 +- .../recovery/interface/src/recovery.rs | 120 ++++++++++++++++++ 5 files changed, 141 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cb2c321815..219f0297a95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10875,6 +10875,9 @@ version = "0.9.0" dependencies = [ "candid", "ed25519-dalek", + "hex", + "ic-base-types", + "registry-canister", "serde", ] diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index 2f399653095..a6344e39bc0 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -7,6 +7,9 @@ DEPENDENCIES = [ "@crate_index//:candid", "@crate_index//:serde", "@crate_index//:ed25519-dalek", + "@crate_index//:hex", + "//rs/registry/canister", + "//rs/types/base_types", ] MACRO_DEPENDENCIES = [] @@ -26,4 +29,3 @@ rust_library( version = "0.1.0", deps = DEPENDENCIES, ) - diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index c0fb8b53233..f166bd160e2 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -10,3 +10,6 @@ edition.workspace = true candid = { workspace = true } serde = { workspace = true } ed25519-dalek.workspace = true +registry-canister.path = "../../../../registry/canister" +ic-base-types.path = "../../../../types/base_types" +hex.workspace = true diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 89903901fcb..cfb4d32a05b 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -36,12 +36,22 @@ pub enum RecoveryError { /// Candid error while encoding the recovery canister payload PayloadSerialization(String), + /// Errors coming from ic-agent AgentError(String), + /// Candid errors CandidError(String), + /// Identity in use is invalid to fulfil a desired action InvalidIdentity(String), + /// No proposals exist on the canister which could be voted on NoProposalsToVoteOn(String), + + /// Specifies that the recovery proposal payload cannot be + /// mapped to a governance proposal. This can happen if the + /// library versions used for the canister and client don't + /// match + InvalidRecoveryProposalPayload(String), } impl From for RecoveryError { @@ -64,7 +74,8 @@ impl ToString for RecoveryError { | Self::AgentError(s) | Self::CandidError(s) | Self::InvalidIdentity(s) - | Self::NoProposalsToVoteOn(s) => s.to_string(), + | Self::NoProposalsToVoteOn(s) + | Self::InvalidRecoveryProposalPayload(s) => s.to_string(), } } } diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 53fc0dc65f1..04438d65abe 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -1,6 +1,12 @@ +use std::str::FromStr; + use crate::*; use candid::{CandidType, Principal}; use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; +use ic_base_types::{PrincipalId, SubnetId}; +use registry_canister::mutations::{ + do_recover_subnet::RecoverSubnetPayload, do_update_subnet::UpdateSubnetPayload, +}; use serde::Deserialize; use crate::{security_metadata::SecurityMetadata, Ballot}; @@ -141,3 +147,117 @@ impl VerifyIntegirty for RecoveryProposal { self.node_operator_ballots.iter().verify() } } + +fn nns_principal_id() -> PrincipalId { + PrincipalId::from_str("tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe") + .expect("Should be a valid NNS id") +} + +impl TryFrom for UpdateSubnetPayload { + type Error = RecoveryError; + + fn try_from(value: RecoveryProposal) -> std::result::Result { + match value.payload { + RecoveryPayload::Halt => Ok(Self { + chain_key_config: None, + chain_key_signing_disable: None, + chain_key_signing_enable: None, + dkg_dealings_per_block: None, + ecdsa_config: None, + ecdsa_key_signing_disable: None, + ecdsa_key_signing_enable: None, + features: None, + halt_at_cup_height: None, + initial_notary_delay_millis: None, + max_artifact_streams_per_peer: None, + max_block_payload_size: None, + max_chunk_size: None, + max_chunk_wait_ms: None, + max_duplicity: None, + max_ingress_bytes_per_message: None, + max_ingress_messages_per_block: None, + max_number_of_canisters: None, + pfn_evaluation_period_ms: None, + receive_check_cache_size: None, + registry_poll_period_ms: None, + retransmission_request_ms: None, + ssh_backup_access: None, + start_as_nns: None, + subnet_type: None, + unit_delay_millis: None, + dkg_interval_length: None, + + set_gossip_config_to_default: false, + is_halted: Some(true), + subnet_id: SubnetId::from(nns_principal_id()), + // TODO: check if this should be configurable + ssh_readonly_access: Some(vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPiyAbNALyrFb1PAdPCcV5w6GYqILGyRbyqzLEVspFmJ recovery@dfinity.org".to_string()]) + }), + RecoveryPayload::Unhalt => Ok(Self { + chain_key_config: None, + chain_key_signing_disable: None, + chain_key_signing_enable: None, + dkg_dealings_per_block: None, + ecdsa_config: None, + ecdsa_key_signing_disable: None, + ecdsa_key_signing_enable: None, + features: None, + halt_at_cup_height: None, + initial_notary_delay_millis: None, + max_artifact_streams_per_peer: None, + max_block_payload_size: None, + max_chunk_size: None, + max_chunk_wait_ms: None, + max_duplicity: None, + max_ingress_bytes_per_message: None, + max_ingress_messages_per_block: None, + max_number_of_canisters: None, + pfn_evaluation_period_ms: None, + receive_check_cache_size: None, + registry_poll_period_ms: None, + retransmission_request_ms: None, + ssh_backup_access: None, + start_as_nns: None, + subnet_type: None, + unit_delay_millis: None, + dkg_interval_length: None, + + set_gossip_config_to_default: false, + is_halted: Some(false), + subnet_id: SubnetId::from(nns_principal_id()), + ssh_readonly_access: Some(vec![]) + }), + _ => Err(RecoveryError::InvalidRecoveryProposalPayload( + "Cannot map this proposal payload to UpdateSubnetPayload".to_string(), + )), + } + } +} + +impl TryFrom for RecoverSubnetPayload { + type Error = RecoveryError; + + fn try_from(value: RecoveryProposal) -> std::result::Result { + match value.payload { + RecoveryPayload::DoRecovery { height, state_hash } => Ok(Self { + subnet_id: nns_principal_id(), + height, + // TODO: Migrate timestamps to nanoseconds in canister + time_ns: value.submission_timestamp_seconds, + state_hash: hex::decode(state_hash).map_err(|e| { + RecoveryError::PayloadSerialization(format!( + "Cannot deserialize state hash into a byte vector: {}", + e + )) + })?, + replacement_nodes: None, + registry_store_uri: None, + ecdsa_config: None, + chain_key_config: None, + }), + _ => Err(RecoveryError::InvalidRecoveryProposalPayload( + "Cannot map this proposal payload to UpdateSubnetPayload".to_string(), + )), + } + } +} From 967616e3665cf9f014bf7c65834fd49cfbef4503 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Wed, 5 Feb 2025 18:14:01 +0100 Subject: [PATCH 39/76] adding common api's to recovery canister implementation --- .../recovery/client/src/implementation.rs | 18 +++++-------- rs/nns/handlers/recovery/client/src/lib.rs | 26 ++++++++++++++++++- rs/nns/handlers/recovery/interface/src/lib.rs | 6 ++--- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index 1c27a476c2d..ff8ddfe4777 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -79,21 +79,17 @@ impl RecoveryCanister for RecoveryCanisterImpl { } async fn get_pending_recovery_proposals(&self) -> Result> { - self.query("get_pending_recovery_proposals", candid::encode_one(())?) - .await + let response: Vec = self + .query("get_pending_recovery_proposals", candid::encode_one(())?) + .await?; + response.iter().verify()?; + + Ok(response) } async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()> { self.ensure_not_anonymous()?; - - let proposal_chain = self.get_pending_recovery_proposals().await?; - proposal_chain.iter().verify()?; - - let last_proposal = proposal_chain - .last() - .ok_or(RecoveryError::NoProposalsToVoteOn( - "There are no proposals to be voted in.".to_string(), - ))?; + let last_proposal = self.fetch_latest_proposal().await?; let mut signing_key = self.signing_key.clone(); diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index 3d4ff931748..680001a785c 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -1,10 +1,10 @@ use async_trait::async_trait; use ic_nns_handler_recovery_interface::recovery::{NewRecoveryProposal, RecoveryProposal}; -use ic_nns_handler_recovery_interface::Result; use ic_nns_handler_recovery_interface::{ simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, }; +use ic_nns_handler_recovery_interface::{RecoveryError, Result}; pub mod implementation; #[cfg(test)] @@ -19,4 +19,28 @@ pub trait RecoveryCanister { async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()>; async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()>; + + async fn fetch_latest_proposal(&self) -> Result { + let proposal_chain = self.get_pending_recovery_proposals().await?; + + proposal_chain + .last() + .cloned() + .ok_or(RecoveryError::NoProposals( + "There are no proposals to be voted in.".to_string(), + )) + } + + async fn fetch_latest_adopted_proposal(&self) -> Result { + let proposal_chain = self.get_pending_recovery_proposals().await?; + + proposal_chain + .iter() + .rev() + .find(|proposal| proposal.is_byzantine_majority_yes()) + .cloned() + .ok_or(RecoveryError::NoProposals( + "No voted in proposals present at the moment".to_string(), + )) + } } diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index cfb4d32a05b..0470757b909 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -44,8 +44,8 @@ pub enum RecoveryError { /// Identity in use is invalid to fulfil a desired action InvalidIdentity(String), - /// No proposals exist on the canister which could be voted on - NoProposalsToVoteOn(String), + /// No proposals exist on the canister + NoProposals(String), /// Specifies that the recovery proposal payload cannot be /// mapped to a governance proposal. This can happen if the @@ -74,7 +74,7 @@ impl ToString for RecoveryError { | Self::AgentError(s) | Self::CandidError(s) | Self::InvalidIdentity(s) - | Self::NoProposalsToVoteOn(s) + | Self::NoProposals(s) | Self::InvalidRecoveryProposalPayload(s) => s.to_string(), } } From 6a1fb71b2c7b440cacecff143e283a80e3bec70e Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 6 Feb 2025 13:44:07 +0100 Subject: [PATCH 40/76] adding common test logic --- Cargo.lock | 1 + rs/nns/handlers/recovery/client/BUILD.bazel | 23 +++- rs/nns/handlers/recovery/client/Cargo.toml | 5 +- .../recovery/client/src/tests/general.rs | 43 +++++++ .../handlers/recovery/client/src/tests/mod.rs | 110 ++++++++++++++++++ rs/nns/handlers/recovery/impl/BUILD.bazel | 2 +- .../impl/canister/tests/initial_args_test.rs | 2 +- .../recovery/impl/canister/tests/mod.rs | 2 +- 8 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 rs/nns/handlers/recovery/client/src/tests/general.rs diff --git a/Cargo.lock b/Cargo.lock index 219f0297a95..1c6279cbce5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10867,6 +10867,7 @@ dependencies = [ "ic-agent", "ic-nns-handler-recovery-interface", "serde", + "tokio", ] [[package]] diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index ad6c536be54..85a54db3427 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -12,10 +12,13 @@ DEPENDENCIES = [ ] MACRO_DEPENDENCIES = [ - "@crate_index//:async-trait" + "@crate_index//:async-trait", ] -DEV_DEPENDENCIES = [] +DEV_DEPENDENCIES = [ + "//packages/pocket-ic", + "@crate_index//:tokio", +] MACRO_DEV_DEPENDENCIES = [] @@ -31,3 +34,19 @@ rust_library( deps = DEPENDENCIES, ) +rust_test( + name = "client-tests", + srcs = glob(["src/**/*.rs"]), + data = [ + "//rs/nns/handlers/recovery/impl:recovery-canister", + "//rs/pocket_ic_server:pocket-ic-server", + ], + aliases = ALIASES, + proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.1.0", + env = { + "RECOVERY_WASM_PATH": "$(rootpath //rs/nns/handlers/recovery/impl:recovery-canister)", + "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" + }, + deps = DEV_DEPENDENCIES + DEPENDENCIES + [":client"] +) diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 074c1a742a8..891d7028584 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -12,4 +12,7 @@ serde = { workspace = true } ed25519-dalek.workspace = true ic-agent.workspace = true async-trait.workspace = true -ic-nns-handler-recovery-interface.path = "../interface" \ No newline at end of file +ic-nns-handler-recovery-interface.path = "../interface" + +[dev-dependencies] +tokio.workspace = true diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs new file mode 100644 index 00000000000..24072503fc7 --- /dev/null +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -0,0 +1,43 @@ +use ic_nns_handler_recovery_interface::recovery::{NewRecoveryProposal, RecoveryPayload}; + +use crate::{ + tests::{generate_node_operators, preconfigured_recovery_init_args}, + RecoveryCanister, +}; + +use super::{get_client, init_pocket_ic}; + +#[tokio::test] +async fn can_get_node_operators() { + let node_operators_with_keys = generate_node_operators(); + let (mut pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + let client = get_client(&mut pic, canister).await; + + let response = client.get_node_operators_in_nns().await; + + assert!(response.is_ok()); + let current_operators = response.unwrap(); + assert!(current_operators.len().eq(&node_operators_with_keys.len())) +} + +#[tokio::test] +async fn can_place_proposals() { + let node_operators_with_keys = generate_node_operators(); + let (mut pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let mut node_operator_iter = node_operators_with_keys.iter(); + let first = node_operator_iter.next().unwrap(); + let first_client = first + .into_recovery_canister_client(&mut pic, canister) + .await; + + let response = first_client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await; + + assert!(response.is_ok()); +} diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs index e69de29bb2d..ce2a56b8287 100644 --- a/rs/nns/handlers/recovery/client/src/tests/mod.rs +++ b/rs/nns/handlers/recovery/client/src/tests/mod.rs @@ -0,0 +1,110 @@ +use std::path::PathBuf; + +use candid::Principal; +use ed25519_dalek::{ed25519::signature::rand_core::OsRng, SigningKey}; +use ic_agent::{agent::AgentBuilder, identity::BasicIdentity, Agent}; +use ic_nns_handler_recovery_interface::{ + recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, +}; +use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; + +use crate::{implementation::RecoveryCanisterImpl, RecoveryCanister}; +mod general; + +fn fetch_canister_wasm(env: &str) -> Vec { + let path: PathBuf = std::env::var(env) + .expect(&format!("Path should be set in environment variable {env}")) + .try_into() + .unwrap(); + std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) +} + +async fn init_pocket_ic(recovery_init_args: RecoveryInitArgs) -> (PocketIc, Principal) { + let pic = PocketIcBuilder::new() + .with_nns_subnet() + .with_application_subnet() + .build_async() + .await; + + let subnets = pic.topology().await.get_app_subnets(); + let subnet = subnets.first().unwrap(); + let canister = pic.create_canister_on_subnet(None, None, *subnet).await; + pic.add_cycles(canister, 100_000_000_000_000).await; + pic.install_canister( + canister, + fetch_canister_wasm("RECOVERY_WASM_PATH"), + candid::encode_one(recovery_init_args).unwrap(), + None, + ) + .await; + + (pic, canister) +} + +fn get_agent(signing_key: SigningKey, url: &str) -> Agent { + let identity = BasicIdentity::from_signing_key((*signing_key.as_bytes()).into()); + + AgentBuilder::default() + .with_url(url) + .with_boxed_identity(Box::new(identity)) + .build() + .unwrap() +} + +async fn get_client(pic: &mut PocketIc, canister: Principal) -> impl RecoveryCanister { + let signing_key = SigningKey::generate(&mut OsRng); + get_client_with_key(pic, canister, signing_key).await +} + +async fn get_client_with_key( + pic: &mut PocketIc, + canister: Principal, + signing_key: SigningKey, +) -> impl RecoveryCanister { + let url = pic.make_live(None).await; + let agent = get_agent(signing_key.clone(), url.as_str()); + agent.fetch_root_key().await.unwrap(); + + RecoveryCanisterImpl::new(agent, canister, signing_key) +} + +fn preconfigured_recovery_init_args( + operators_with_keys: &Vec, +) -> RecoveryInitArgs { + RecoveryInitArgs { + initial_node_operator_records: operators_with_keys + .iter() + .map(|o| o.record.clone()) + .collect(), + } +} + +struct NodeOperatorWithKey { + record: SimpleNodeOperatorRecord, + key: SigningKey, +} + +impl NodeOperatorWithKey { + async fn into_recovery_canister_client( + &self, + pic: &mut PocketIc, + canister: Principal, + ) -> impl RecoveryCanister { + get_client_with_key(pic, canister, self.key.clone()).await + } +} + +fn generate_node_operators() -> Vec { + (0..10) + .map(|_| { + let key = SigningKey::generate(&mut OsRng); + NodeOperatorWithKey { + record: SimpleNodeOperatorRecord { + operator_id: Principal::self_authenticating(key.verifying_key()), + nodes: (0..4).map(|_| Principal::anonymous()).collect(), + }, + key, + } + }) + .collect() +} diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index 135c0728827..94db2f9bd91 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -162,7 +162,7 @@ rust_test( "//rs/registry/subnet_type" ], env = { - "BACKUP_ROOT_WASM_PATH": "$(rootpath :recovery-canister)", + "RECOVERY_WASM_PATH": "$(rootpath :recovery-canister)", "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" }, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index 525044371c3..dbd61613e48 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -28,7 +28,7 @@ fn setup_and_install_canister(initial_arg: RecoveryInitArgs) -> (PocketIc, Princ println!("Size: {}", encoded.len()); pic.install_canister( canister, - fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), + fetch_canister_wasm("RECOVERY_WASM_PATH"), encoded, None, ); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 16b0b52fdc3..4a2d5b2dd03 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -265,7 +265,7 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr pic.add_cycles(canister, 100_000_000_000_000); pic.install_canister( canister, - fetch_canister_wasm("BACKUP_ROOT_WASM_PATH"), + fetch_canister_wasm("RECOVERY_WASM_PATH"), candid::encode_one(RecoveryInitArgs::default()).unwrap(), None, ); From f220fd53d5f09ead0ebb6070b2f2be3f0840241a Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 6 Feb 2025 14:18:35 +0100 Subject: [PATCH 41/76] use der encoded public key to get principals --- Cargo.lock | 1 + .../recovery/client/src/implementation.rs | 4 ++-- .../recovery/client/src/tests/general.rs | 1 + .../handlers/recovery/client/src/tests/mod.rs | 7 +++++-- .../recovery/impl/canister/tests/mod.rs | 6 ++++-- rs/nns/handlers/recovery/interface/BUILD.bazel | 1 + rs/nns/handlers/recovery/interface/Cargo.toml | 1 + .../interface/src/security_metadata.rs | 18 +++++++++++++++++- 8 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c6279cbce5..7dc8a08b455 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10880,6 +10880,7 @@ dependencies = [ "ic-base-types", "registry-canister", "serde", + "simple_asn1", ] [[package]] diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index ff8ddfe4777..a8b87bda9dc 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -38,7 +38,7 @@ impl RecoveryCanisterImpl { .await .map(|response| candid::decode_one(&response)) .map_err(|e| RecoveryError::AgentError(e.to_string()))? - .map_err(|e| RecoveryError::CandidError(e.to_string())) + .map_err(|e| e.into()) } async fn update(&self, method: &str, args: P) -> Result @@ -74,7 +74,7 @@ impl RecoveryCanisterImpl { #[async_trait] impl RecoveryCanister for RecoveryCanisterImpl { async fn get_node_operators_in_nns(&self) -> Result> { - self.query("get_node_operators_in_nns", candid::encode_one(())?) + self.query("get_current_nns_node_operators", candid::encode_one(())?) .await } diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 24072503fc7..7be3e1d41ae 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -39,5 +39,6 @@ async fn can_place_proposals() { }) .await; + println!("{:?}", response); assert!(response.is_ok()); } diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs index ce2a56b8287..5feadc617ff 100644 --- a/rs/nns/handlers/recovery/client/src/tests/mod.rs +++ b/rs/nns/handlers/recovery/client/src/tests/mod.rs @@ -4,7 +4,8 @@ use candid::Principal; use ed25519_dalek::{ed25519::signature::rand_core::OsRng, SigningKey}; use ic_agent::{agent::AgentBuilder, identity::BasicIdentity, Agent}; use ic_nns_handler_recovery_interface::{ - recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, + recovery_init::RecoveryInitArgs, security_metadata::der_encode_public_key, + simple_node_operator_record::SimpleNodeOperatorRecord, }; use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; @@ -100,7 +101,9 @@ fn generate_node_operators() -> Vec { let key = SigningKey::generate(&mut OsRng); NodeOperatorWithKey { record: SimpleNodeOperatorRecord { - operator_id: Principal::self_authenticating(key.verifying_key()), + operator_id: Principal::self_authenticating(der_encode_public_key( + key.verifying_key().to_bytes().to_vec(), + )), nodes: (0..4).map(|_| Principal::anonymous()).collect(), }, key, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 4a2d5b2dd03..782a8301a4a 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -7,7 +7,7 @@ use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, recovery_init::RecoveryInitArgs, - security_metadata::SecurityMetadata, + security_metadata::{der_encode_public_key, SecurityMetadata}, simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, }; @@ -110,7 +110,9 @@ impl NodeOperatorArg { let signing_key = SigningKey::generate(&mut csprng); Self { - principal: PrincipalId::new_self_authenticating(signing_key.verifying_key().as_bytes()), + principal: PrincipalId::new_self_authenticating(&der_encode_public_key( + signing_key.verifying_key().as_bytes().to_vec(), + )), num_nodes, signing_key, } diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index a6344e39bc0..6dc32a55d83 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -8,6 +8,7 @@ DEPENDENCIES = [ "@crate_index//:serde", "@crate_index//:ed25519-dalek", "@crate_index//:hex", + "@crate_index//:simple_asn1", "//rs/registry/canister", "//rs/types/base_types", ] diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index f166bd160e2..fdcfe54bb1f 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -13,3 +13,4 @@ ed25519-dalek.workspace = true registry-canister.path = "../../../../registry/canister" ic-base-types.path = "../../../../types/base_types" hex.workspace = true +simple_asn1.workspace = true diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 2ffee777f59..c6e984ad998 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -2,6 +2,10 @@ use super::*; use candid::{CandidType, Principal}; use ed25519_dalek::{Signature, VerifyingKey}; use serde::Deserialize; +use simple_asn1::{ + oid, to_der, + ASN1Block::{BitString, ObjectIdentifier, Sequence}, +}; #[derive(Clone, Debug, CandidType, Deserialize)] /// Wrapper struct containing information regarding integrity. @@ -55,7 +59,8 @@ impl SecurityMetadata { /// Verifies if the passed principal is derived from a given public key (also known as /// verifying key). pub fn principal_matches_public_key(&self, principal: &Principal) -> Result<()> { - let loaded_principal = Principal::self_authenticating(self.pub_key); + let loaded_principal = + Principal::self_authenticating(der_encode_public_key(self.pub_key.to_vec())); match loaded_principal.eq(principal) { true => Ok(()), @@ -66,3 +71,14 @@ impl SecurityMetadata { } } } + +// Copied from agent-rs +pub fn der_encode_public_key(public_key: Vec) -> Vec { + // see Section 4 "SubjectPublicKeyInfo" in https://tools.ietf.org/html/rfc8410 + + let id_ed25519 = oid!(1, 3, 101, 112); + let algorithm = Sequence(0, vec![ObjectIdentifier(0, id_ed25519)]); + let subject_public_key = BitString(0, public_key.len() * 8, public_key); + let subject_public_key_info = Sequence(0, vec![algorithm, subject_public_key]); + to_der(&subject_public_key_info).unwrap() +} From da809d094bdc1419589e40130d9efcb5e1cf4c14 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 6 Feb 2025 17:14:29 +0100 Subject: [PATCH 42/76] fixing update call --- rs/nns/handlers/recovery/client/src/implementation.rs | 5 +++-- rs/nns/handlers/recovery/interface/src/lib.rs | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index a8b87bda9dc..a3780603b33 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -51,9 +51,10 @@ impl RecoveryCanisterImpl { .with_arg(args) .call_and_wait() .await - .map(|response| candid::decode_one(&response)) + .map(|response| candid::decode_one::>(&response)) .map_err(|e| RecoveryError::AgentError(e.to_string()))? - .map_err(|e| e.into()) + .map_err(|e| RecoveryError::CandidError(e.to_string()))? + .map_err(|e| RecoveryError::CanisterError(e.to_string())) } fn ensure_not_anonymous(&self) -> Result<()> { diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 0470757b909..7651962fc4d 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -52,6 +52,9 @@ pub enum RecoveryError { /// library versions used for the canister and client don't /// match InvalidRecoveryProposalPayload(String), + + /// Errors received from the canister + CanisterError(String), } impl From for RecoveryError { @@ -75,7 +78,8 @@ impl ToString for RecoveryError { | Self::CandidError(s) | Self::InvalidIdentity(s) | Self::NoProposals(s) - | Self::InvalidRecoveryProposalPayload(s) => s.to_string(), + | Self::InvalidRecoveryProposalPayload(s) + | Self::CanisterError(s) => s.to_string(), } } } From 5f8db37e7a3f880fb100d0b884e3f43a00075551 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 6 Feb 2025 17:35:00 +0100 Subject: [PATCH 43/76] fixing debug prints --- .../recovery/client/src/implementation.rs | 9 +++--- rs/nns/handlers/recovery/client/src/lib.rs | 2 +- .../recovery/client/src/tests/general.rs | 30 ++++++++++++++++++- .../recovery/impl/canister/canister.rs | 1 - .../recovery/impl/src/node_operator_sync.rs | 21 ++++++++++--- .../interface/src/security_metadata.rs | 3 +- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index a3780603b33..5b7397b2a1d 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -88,19 +88,18 @@ impl RecoveryCanister for RecoveryCanisterImpl { Ok(response) } - async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()> { + async fn vote_on_latest_proposal(&mut self, ballot: Ballot) -> Result<()> { self.ensure_not_anonymous()?; let last_proposal = self.fetch_latest_proposal().await?; - - let mut signing_key = self.signing_key.clone(); + let signature = last_proposal.sign(&mut self.signing_key)?; self.update( "vote_on_proposal", candid::encode_one(VoteOnRecoveryProposal { security_metadata: SecurityMetadata { - signature: last_proposal.sign(&mut signing_key)?, + signature, payload: last_proposal.signature_payload()?, - pub_key: signing_key.verifying_key().to_bytes(), + pub_key: self.signing_key.verifying_key().to_bytes(), }, ballot, })?, diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index 680001a785c..35e366fc275 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -16,7 +16,7 @@ pub trait RecoveryCanister { async fn get_pending_recovery_proposals(&self) -> Result>; - async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()>; + async fn vote_on_latest_proposal(&mut self, ballot: Ballot) -> Result<()>; async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()>; diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 7be3e1d41ae..d2c9d63cfef 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,4 +1,7 @@ -use ic_nns_handler_recovery_interface::recovery::{NewRecoveryProposal, RecoveryPayload}; +use ic_nns_handler_recovery_interface::{ + recovery::{NewRecoveryProposal, RecoveryPayload}, + Ballot, +}; use crate::{ tests::{generate_node_operators, preconfigured_recovery_init_args}, @@ -42,3 +45,28 @@ async fn can_place_proposals() { println!("{:?}", response); assert!(response.is_ok()); } + +#[tokio::test] +async fn can_vote_on_proposals() { + let node_operators_with_keys = generate_node_operators(); + let (mut pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let mut node_operator_iter = node_operators_with_keys.iter(); + let first = node_operator_iter.next().unwrap(); + let mut first_client = first + .into_recovery_canister_client(&mut pic, canister) + .await; + + first_client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await + .unwrap(); + + let response = first_client.vote_on_latest_proposal(Ballot::Yes).await; + + println!("{:?}", response); + assert!(response.is_ok()) +} diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 3b60d4e548f..00592510a2c 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -86,7 +86,6 @@ fn main() {} #[init] fn init(arg: RecoveryInitArgs) { ic_cdk_timers::set_timer(std::time::Duration::from_secs(0), || { - ic_cdk::println!("Received: {:?}", arg); ic_cdk::spawn(setup_node_operator_update(Some(arg))); }); ic_cdk_timers::set_timer_interval(std::time::Duration::from_secs(60 * 60 * 24), || { diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs index 82b21578498..688801d7279 100644 --- a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -5,6 +5,7 @@ use ic_nns_handler_recovery_interface::simple_node_operator_record::SimpleNodeOp use ic_nns_handler_root::root_proposals::{ get_nns_membership, get_nns_subnet_id, get_node_operator_pid_of_node, }; +use itertools::Itertools; thread_local! { static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; @@ -55,10 +56,22 @@ pub fn get_node_operators_in_nns() -> Vec { merged.dedup_by(|a, b| a.operator_id == b.operator_id); if !initial.is_empty() { - ic_cdk::println!("Initial: {:?}", initial); - ic_cdk::println!("Obtained: {:?}", obtained_from_sync); - ic_cdk::println!("Merged: {:?}", merged); + ic_cdk::println!("Initial: {}", format_node_operators(&initial)); + ic_cdk::println!("Obtained: {}", format_node_operators(&obtained_from_sync)); } - + ic_cdk::println!("Merged: {}", format_node_operators(&merged)); merged } + +fn format_node_operators(operators: &Vec) -> String { + operators + .iter() + .map(|operator| { + format!( + "Principal: {}, Nodes: [{}]", + operator.operator_id.to_string(), + operator.nodes.iter().map(|n| n.to_string()).join(", ") + ) + }) + .join("\n") +} diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index c6e984ad998..a7b05146b3d 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -40,12 +40,13 @@ impl SecurityMetadata { /// Verify the authenticity of a whole vote on a recovery canister proposal. pub fn validate_metadata(&self, caller: &Principal) -> Result<()> { - self.principal_matches_public_key(caller)?; + // self.principal_matches_public_key(caller)?; self.verify_signature() } /// Verifies the signature authenticity of security metadata. pub fn verify_signature(&self) -> Result<()> { + println!("Loaded key: {:?}", self.pub_key); let loaded_public_key = VerifyingKey::from_bytes(&self.pub_key) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; let signature = Signature::from_slice(self.signature.as_flattened()) From c1926854c77b81f7b8ba01563d25f8000e3bf8a2 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 6 Feb 2025 17:52:24 +0100 Subject: [PATCH 44/76] fixing verification logic --- rs/nns/handlers/recovery/client/src/tests/general.rs | 7 ++++--- rs/nns/handlers/recovery/interface/src/recovery.rs | 5 ++++- .../handlers/recovery/interface/src/security_metadata.rs | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index d2c9d63cfef..65b0052f784 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,6 +1,6 @@ use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload}, - Ballot, + Ballot, VerifyIntegirty, }; use crate::{ @@ -66,7 +66,8 @@ async fn can_vote_on_proposals() { .unwrap(); let response = first_client.vote_on_latest_proposal(Ballot::Yes).await; + assert!(response.is_ok()); - println!("{:?}", response); - assert!(response.is_ok()) + let latest = first_client.get_pending_recovery_proposals().await.unwrap(); + assert!(latest.iter().verify().is_ok()) } diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 04438d65abe..ed2750cbceb 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -144,7 +144,10 @@ impl VerifyIntegirty for NodeOperatorBallot { impl VerifyIntegirty for RecoveryProposal { fn verify(&self) -> Result<()> { - self.node_operator_ballots.iter().verify() + self.node_operator_ballots + .iter() + .filter(|ballot| !ballot.ballot.eq(&Ballot::Undecided)) + .verify() } } diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index a7b05146b3d..b6a6223d675 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -40,7 +40,7 @@ impl SecurityMetadata { /// Verify the authenticity of a whole vote on a recovery canister proposal. pub fn validate_metadata(&self, caller: &Principal) -> Result<()> { - // self.principal_matches_public_key(caller)?; + self.principal_matches_public_key(caller)?; self.verify_signature() } @@ -59,6 +59,7 @@ impl SecurityMetadata { /// Verifies if the passed principal is derived from a given public key (also known as /// verifying key). + /// TODO: This is not possible since not everything has the same oid pub fn principal_matches_public_key(&self, principal: &Principal) -> Result<()> { let loaded_principal = Principal::self_authenticating(der_encode_public_key(self.pub_key.to_vec())); From 038345308f61fe3441f8cc0f9cf4529b75a8be27 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Thu, 6 Feb 2025 18:42:44 +0100 Subject: [PATCH 45/76] adding logic to decode der --- .../recovery/client/src/implementation.rs | 6 ++++-- .../recovery/impl/canister/tests/mod.rs | 4 +++- .../impl/canister/tests/voting_tests.rs | 16 +++++++-------- .../interface/src/security_metadata.rs | 20 +++++++++++-------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index 5b7397b2a1d..3bcef8f207b 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -4,7 +4,7 @@ use ed25519_dalek::SigningKey; use ic_agent::Agent; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, - security_metadata::SecurityMetadata, + security_metadata::{der_encode_public_key, SecurityMetadata}, simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, RecoveryError, Result, VerifyIntegirty, }; @@ -99,7 +99,9 @@ impl RecoveryCanister for RecoveryCanisterImpl { security_metadata: SecurityMetadata { signature, payload: last_proposal.signature_payload()?, - pub_key: self.signing_key.verifying_key().to_bytes(), + pub_key_der: der_encode_public_key( + self.signing_key.verifying_key().to_bytes().to_vec(), + ), }, ballot, })?, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 782a8301a4a..70f9c7948fb 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -349,7 +349,9 @@ fn vote_with_only_ballot( .signature_payload() .expect("Should be able to fetch payload"), signature, - pub_key: sender.signing_key.verifying_key().to_bytes(), + pub_key_der: der_encode_public_key( + sender.signing_key.verifying_key().to_bytes().to_vec(), + ), }, ballot, }, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 2817f686689..9c1a7b644a4 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -2,7 +2,7 @@ use candid::Principal; use ed25519_dalek::SigningKey; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal}, - security_metadata::SecurityMetadata, + security_metadata::{der_encode_public_key, SecurityMetadata}, Ballot, }; @@ -121,7 +121,9 @@ fn disallow_votes_bad_signature() { security_metadata: SecurityMetadata { payload: vec![], signature: [[0; 32]; 2], - pub_key: first.signing_key.verifying_key().to_bytes(), + pub_key_der: der_encode_public_key( + first.signing_key.verifying_key().to_bytes().to_vec(), + ), }, }, ); @@ -166,7 +168,9 @@ fn disallow_votes_wrong_public_key() { .signature_payload() .expect("Should be able to serialize payload"), signature, - pub_key: new_key_pair.verifying_key().to_bytes(), + pub_key_der: der_encode_public_key( + new_key_pair.verifying_key().to_bytes().to_vec(), + ), }, ballot: Ballot::Yes, }, @@ -199,11 +203,7 @@ fn disallow_votes_anonymous() { canister, Principal::anonymous(), VoteOnRecoveryProposal { - security_metadata: SecurityMetadata { - payload: vec![], - signature: [[0; 32]; 2], - pub_key: [0; 32], - }, + security_metadata: SecurityMetadata::empty(), ballot: Ballot::Yes, }, ); diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index b6a6223d675..6f7fcfca4aa 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -23,10 +23,10 @@ pub struct SecurityMetadata { /// all fields in a proposal except the ballots of node operators /// serialized as vector of bytes. pub payload: Vec, - /// Verifying key. + /// Der encoded public key. /// /// It is used to verify the authenticity of a signature. - pub pub_key: [u8; 32], + pub pub_key_der: Vec, } impl SecurityMetadata { @@ -34,7 +34,7 @@ impl SecurityMetadata { Self { signature: [[0; 32]; 2], payload: vec![], - pub_key: [0; 32], + pub_key_der: vec![], } } @@ -46,8 +46,7 @@ impl SecurityMetadata { /// Verifies the signature authenticity of security metadata. pub fn verify_signature(&self) -> Result<()> { - println!("Loaded key: {:?}", self.pub_key); - let loaded_public_key = VerifyingKey::from_bytes(&self.pub_key) + let loaded_public_key = VerifyingKey::from_bytes(&self.decode_der_pub_key()) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; let signature = Signature::from_slice(self.signature.as_flattened()) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; @@ -59,10 +58,8 @@ impl SecurityMetadata { /// Verifies if the passed principal is derived from a given public key (also known as /// verifying key). - /// TODO: This is not possible since not everything has the same oid pub fn principal_matches_public_key(&self, principal: &Principal) -> Result<()> { - let loaded_principal = - Principal::self_authenticating(der_encode_public_key(self.pub_key.to_vec())); + let loaded_principal = Principal::self_authenticating(&self.pub_key_der); match loaded_principal.eq(principal) { true => Ok(()), @@ -72,6 +69,13 @@ impl SecurityMetadata { ))), } } + + fn decode_der_pub_key(&self) -> [u8; 32] { + // TODO: Logic for reversing other keys + let mut key = [0; 32]; + key.copy_from_slice(&self.pub_key_der[self.pub_key_der.len() - 32..]); + key + } } // Copied from agent-rs From d262f7079b416aa7d52de8eda435e275fdb76f67 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 00:38:48 +0100 Subject: [PATCH 46/76] supporting ed25519 and p256 --- Cargo.Bazel.json.lock | 7 +- Cargo.Bazel.toml.lock | 1 + Cargo.lock | 6 +- Cargo.toml | 1 + bazel/external_crates.bzl | 3 + rs/nns/handlers/recovery/client/BUILD.bazel | 1 + rs/nns/handlers/recovery/client/Cargo.toml | 3 + .../recovery/client/src/implementation.rs | 14 ++-- .../recovery/client/src/tests/general.rs | 83 ++++++++++++++++++- .../handlers/recovery/client/src/tests/mod.rs | 10 +-- .../impl/canister/tests/initial_args_test.rs | 2 +- .../recovery/impl/canister/tests/mod.rs | 23 +++-- .../impl/canister/tests/voting_tests.rs | 21 +++-- .../handlers/recovery/interface/BUILD.bazel | 3 +- rs/nns/handlers/recovery/interface/Cargo.toml | 3 +- rs/nns/handlers/recovery/interface/src/lib.rs | 8 +- .../recovery/interface/src/recovery.rs | 8 +- .../interface/src/security_metadata.rs | 68 ++++++++------- 18 files changed, 196 insertions(+), 69 deletions(-) diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index 97325ef3498..6ab2f7ee9e6 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "328a35840f7c103c76777c84791d47b1d1606b7503677e77bb1754e48206dbb4", + "checksum": "c20dba4b4c2367832a7fc85a318808747f93f62195ded75a6a57254d21006eff", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -20377,6 +20377,10 @@ "id": "socket2 0.5.7", "target": "socket2" }, + { + "id": "spki 0.7.3", + "target": "spki" + }, { "id": "ssh2 0.9.4", "target": "ssh2" @@ -91439,6 +91443,7 @@ "slog-scope 4.4.0", "slog-term 2.9.1", "socket2 0.5.7", + "spki 0.7.3", "ssh2 0.9.4", "static_assertions 1.1.0", "strum 0.26.3", diff --git a/Cargo.Bazel.toml.lock b/Cargo.Bazel.toml.lock index ce32f9db713..b02e9ea01c4 100644 --- a/Cargo.Bazel.toml.lock +++ b/Cargo.Bazel.toml.lock @@ -3439,6 +3439,7 @@ dependencies = [ "slog-scope", "slog-term", "socket2 0.5.7", + "spki 0.7.3", "ssh2", "static_assertions", "strum 0.26.3", diff --git a/Cargo.lock b/Cargo.lock index 7dc8a08b455..2fb8ec67da7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10864,8 +10864,11 @@ dependencies = [ "async-trait", "candid", "ed25519-dalek", + "elliptic-curve 0.13.8", "ic-agent", "ic-nns-handler-recovery-interface", + "p256", + "pocket-ic", "serde", "tokio", ] @@ -10878,9 +10881,10 @@ dependencies = [ "ed25519-dalek", "hex", "ic-base-types", + "p256", "registry-canister", "serde", - "simple_asn1", + "spki 0.7.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1d42735fc42..0f743672ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -708,6 +708,7 @@ strum = { version = "0.26.3", features = ["derive"] } strum_macros = "0.26.4" subtle = "2.6.1" syn = { version = "1.0.109", features = ["fold", "full"] } +spki = "0.7.3" tar = "0.4.39" tempfile = "3.12.0" thiserror = "2.0.3" diff --git a/bazel/external_crates.bzl b/bazel/external_crates.bzl index 58fbb24dedb..f505505240c 100644 --- a/bazel/external_crates.bzl +++ b/bazel/external_crates.bzl @@ -444,6 +444,9 @@ def external_crates_repository(name, cargo_lockfile, lockfile, sanitizers_enable version = "^2.1.1", features = ["std", "zeroize", "digest", "batch", "pkcs8", "pem", "hazmat"], ), + "spki": crate.spec( + version = "^0.7.3" + ), "educe": crate.spec( version = "^0.4", ), diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 85a54db3427..3a19536c583 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -18,6 +18,7 @@ MACRO_DEPENDENCIES = [ DEV_DEPENDENCIES = [ "//packages/pocket-ic", "@crate_index//:tokio", + "@crate_index//:p256", ] MACRO_DEV_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 891d7028584..8200c0e534d 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -16,3 +16,6 @@ ic-nns-handler-recovery-interface.path = "../interface" [dev-dependencies] tokio.workspace = true +p256.workspace = true +elliptic-curve.workspace = true +pocket-ic.path = "../../../../../packages/pocket-ic" diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index 3bcef8f207b..f060f7ee9d1 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -1,10 +1,11 @@ use async_trait::async_trait; use candid::{CandidType, Principal}; +use ed25519_dalek::pkcs8::EncodePublicKey; use ed25519_dalek::SigningKey; use ic_agent::Agent; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, - security_metadata::{der_encode_public_key, SecurityMetadata}, + security_metadata::SecurityMetadata, simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, RecoveryError, Result, VerifyIntegirty, }; @@ -83,7 +84,7 @@ impl RecoveryCanister for RecoveryCanisterImpl { let response: Vec = self .query("get_pending_recovery_proposals", candid::encode_one(())?) .await?; - response.iter().verify()?; + response.iter().verify_integrity()?; Ok(response) } @@ -99,9 +100,12 @@ impl RecoveryCanister for RecoveryCanisterImpl { security_metadata: SecurityMetadata { signature, payload: last_proposal.signature_payload()?, - pub_key_der: der_encode_public_key( - self.signing_key.verifying_key().to_bytes().to_vec(), - ), + pub_key_der: self + .signing_key + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), }, ballot, })?, diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 65b0052f784..7a7abedebc3 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,7 +1,17 @@ +use candid::Principal; use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryPayload}, + recovery::{NewRecoveryProposal, RecoveryPayload, RecoveryProposal, VoteOnRecoveryProposal}, + recovery_init::RecoveryInitArgs, + security_metadata::SecurityMetadata, + simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, VerifyIntegirty, }; +use p256::{ + ecdsa::{self, signature::SignerMut, SigningKey}, + elliptic_curve::{rand_core::OsRng, SecretKey}, + pkcs8::EncodePublicKey, + NistP256, +}; use crate::{ tests::{generate_node_operators, preconfigured_recovery_init_args}, @@ -69,5 +79,74 @@ async fn can_vote_on_proposals() { assert!(response.is_ok()); let latest = first_client.get_pending_recovery_proposals().await.unwrap(); - assert!(latest.iter().verify().is_ok()) + assert!(latest.iter().verify_integrity().is_ok()) +} + +#[tokio::test] +async fn can_use_prime256_keys() { + let new_key_pair: SecretKey = SecretKey::random(&mut OsRng); + let pub_key = new_key_pair.public_key(); + let node_operator = SimpleNodeOperatorRecord { + operator_id: Principal::self_authenticating(pub_key.to_public_key_der().unwrap()), + nodes: vec![Principal::anonymous()], + }; + + let (pic, canister) = init_pocket_ic(RecoveryInitArgs { + initial_node_operator_records: vec![node_operator.clone()], + }) + .await; + + pic.update_call( + canister, + node_operator.operator_id, + "submit_new_recovery_proposal", + candid::encode_one(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .unwrap(), + ) + .await + .unwrap(); + + let pending = pic + .query_call( + canister, + Principal::anonymous(), + "get_pending_recovery_proposals", + candid::encode_one(()).unwrap(), + ) + .await + .unwrap(); + + let pending: Vec = candid::decode_one(&pending).unwrap(); + let last = pending.last().unwrap(); + + let mut signing_key: SigningKey = new_key_pair.into(); + let signature: ecdsa::Signature = signing_key + .try_sign(&last.signature_payload().unwrap()) + .unwrap(); + + let mut r = [0; 32]; + let mut s = [0; 32]; + r.copy_from_slice(&signature.r().as_ref().to_bytes()); + s.copy_from_slice(&signature.s().as_ref().to_bytes()); + + let response = pic + .update_call( + canister, + node_operator.operator_id, + "vote_on_proposal", + candid::encode_one(VoteOnRecoveryProposal { + security_metadata: SecurityMetadata { + signature: [r, s], + payload: last.signature_payload().unwrap(), + pub_key_der: pub_key.to_public_key_der().unwrap().into_vec(), + }, + ballot: Ballot::Yes, + }) + .unwrap(), + ) + .await; + + assert!(response.is_ok()) } diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs index 5feadc617ff..4cff7914ee5 100644 --- a/rs/nns/handlers/recovery/client/src/tests/mod.rs +++ b/rs/nns/handlers/recovery/client/src/tests/mod.rs @@ -1,11 +1,11 @@ use std::path::PathBuf; use candid::Principal; +use ed25519_dalek::pkcs8::EncodePublicKey; use ed25519_dalek::{ed25519::signature::rand_core::OsRng, SigningKey}; use ic_agent::{agent::AgentBuilder, identity::BasicIdentity, Agent}; use ic_nns_handler_recovery_interface::{ - recovery_init::RecoveryInitArgs, security_metadata::der_encode_public_key, - simple_node_operator_record::SimpleNodeOperatorRecord, + recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, }; use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; @@ -101,9 +101,9 @@ fn generate_node_operators() -> Vec { let key = SigningKey::generate(&mut OsRng); NodeOperatorWithKey { record: SimpleNodeOperatorRecord { - operator_id: Principal::self_authenticating(der_encode_public_key( - key.verifying_key().to_bytes().to_vec(), - )), + operator_id: Principal::self_authenticating( + key.verifying_key().to_public_key_der().unwrap().into_vec(), + ), nodes: (0..4).map(|_| Principal::anonymous()).collect(), }, key, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index dbd61613e48..cc66a37b52a 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -93,5 +93,5 @@ fn initial_operators_should_be_able_to_place_proposals_and_vote() { let pending = get_pending(&pic, canister); let last = pending.last().unwrap(); assert!(!last.is_byzantine_majority_yes()); - assert!(last.verify().is_ok()) + assert!(last.verify_integrity().is_ok()) } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 70f9c7948fb..8b1ba83440e 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -1,13 +1,13 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; -use ed25519_dalek::SigningKey; +use ed25519_dalek::{pkcs8::EncodePublicKey, SigningKey}; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, recovery_init::RecoveryInitArgs, - security_metadata::{der_encode_public_key, SecurityMetadata}, + security_metadata::SecurityMetadata, simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, }; @@ -110,9 +110,13 @@ impl NodeOperatorArg { let signing_key = SigningKey::generate(&mut csprng); Self { - principal: PrincipalId::new_self_authenticating(&der_encode_public_key( - signing_key.verifying_key().as_bytes().to_vec(), - )), + principal: PrincipalId::new_self_authenticating( + &signing_key + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), + ), num_nodes, signing_key, } @@ -349,9 +353,12 @@ fn vote_with_only_ballot( .signature_payload() .expect("Should be able to fetch payload"), signature, - pub_key_der: der_encode_public_key( - sender.signing_key.verifying_key().to_bytes().to_vec(), - ), + pub_key_der: sender + .signing_key + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), }, ballot, }, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 9c1a7b644a4..bbf76a33e6d 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,8 +1,8 @@ use candid::Principal; -use ed25519_dalek::SigningKey; +use ed25519_dalek::{pkcs8::EncodePublicKey, SigningKey}; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal}, - security_metadata::{der_encode_public_key, SecurityMetadata}, + security_metadata::SecurityMetadata, Ballot, }; @@ -121,9 +121,12 @@ fn disallow_votes_bad_signature() { security_metadata: SecurityMetadata { payload: vec![], signature: [[0; 32]; 2], - pub_key_der: der_encode_public_key( - first.signing_key.verifying_key().to_bytes().to_vec(), - ), + pub_key_der: first + .signing_key + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), }, }, ); @@ -168,9 +171,11 @@ fn disallow_votes_wrong_public_key() { .signature_payload() .expect("Should be able to serialize payload"), signature, - pub_key_der: der_encode_public_key( - new_key_pair.verifying_key().to_bytes().to_vec(), - ), + pub_key_der: new_key_pair + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), }, ballot: Ballot::Yes, }, diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index 6dc32a55d83..f00979db357 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -8,7 +8,8 @@ DEPENDENCIES = [ "@crate_index//:serde", "@crate_index//:ed25519-dalek", "@crate_index//:hex", - "@crate_index//:simple_asn1", + "@crate_index//:spki", + "@crate_index//:p256", "//rs/registry/canister", "//rs/types/base_types", ] diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index fdcfe54bb1f..c191c4eb667 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -13,4 +13,5 @@ ed25519-dalek.workspace = true registry-canister.path = "../../../../registry/canister" ic-base-types.path = "../../../../types/base_types" hex.workspace = true -simple_asn1.workspace = true +spki.workspace = true +p256.workspace = true diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 7651962fc4d..9d65a4f8d8c 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -85,7 +85,7 @@ impl ToString for RecoveryError { } pub trait VerifyIntegirty { - fn verify(&self) -> Result<()>; + fn verify_integrity(&self) -> Result<()>; } impl<'a, I, T> VerifyIntegirty for I @@ -93,16 +93,16 @@ where I: Iterator + Clone, T: VerifyIntegirty + 'a, { - fn verify(&self) -> Result<()> { + fn verify_integrity(&self) -> Result<()> { self.clone() - .map(|item| item.verify()) + .map(|item| item.verify_integrity()) .find(|res| res.is_err()) .unwrap_or(Ok(())) } } /// Convenience method that explicitly checks the authenticity -/// of sent proposal chain. It is equivalent to calling `proposals.iter().verify()` +/// of sent proposal chain. It is equivalent to calling `proposals.iter().verify_integrity()` pub fn verify_signatures_and_authenticity_of_all_proposals_and_votes( proposals: Vec<&RecoveryProposal>, ) -> Result<()> { diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index ed2750cbceb..deb98ded789 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -114,7 +114,7 @@ impl RecoveryProposal { // Each vote has the weight of 1 times // the amount of nodes the node operator // has in the nns subnet - true => 1 * vote.nodes_tied_to_ballot.len(), + true => vote.nodes_tied_to_ballot.len(), false => 0, }) .sum::(); @@ -137,17 +137,17 @@ impl RecoveryProposal { } impl VerifyIntegirty for NodeOperatorBallot { - fn verify(&self) -> Result<()> { + fn verify_integrity(&self) -> Result<()> { self.security_metadata.validate_metadata(&self.principal) } } impl VerifyIntegirty for RecoveryProposal { - fn verify(&self) -> Result<()> { + fn verify_integrity(&self) -> Result<()> { self.node_operator_ballots .iter() .filter(|ballot| !ballot.ballot.eq(&Ballot::Undecided)) - .verify() + .verify_integrity() } } diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 6f7fcfca4aa..240e3b16b69 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -1,11 +1,9 @@ use super::*; use candid::{CandidType, Principal}; -use ed25519_dalek::{Signature, VerifyingKey}; +use p256::ecdsa::signature::Verifier; +use p256::pkcs8::DecodePublicKey; use serde::Deserialize; -use simple_asn1::{ - oid, to_der, - ASN1Block::{BitString, ObjectIdentifier, Sequence}, -}; +use spki::{Document, SubjectPublicKeyInfoRef}; #[derive(Clone, Debug, CandidType, Deserialize)] /// Wrapper struct containing information regarding integrity. @@ -46,14 +44,11 @@ impl SecurityMetadata { /// Verifies the signature authenticity of security metadata. pub fn verify_signature(&self) -> Result<()> { - let loaded_public_key = VerifyingKey::from_bytes(&self.decode_der_pub_key()) - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - let signature = Signature::from_slice(self.signature.as_flattened()) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - loaded_public_key - .verify_strict(&self.payload, &signature) - .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + valid_signature( + &self.pub_key_der, + self.signature.as_flattened(), + &self.payload, + ) } /// Verifies if the passed principal is derived from a given public key (also known as @@ -69,22 +64,39 @@ impl SecurityMetadata { ))), } } - - fn decode_der_pub_key(&self) -> [u8; 32] { - // TODO: Logic for reversing other keys - let mut key = [0; 32]; - key.copy_from_slice(&self.pub_key_der[self.pub_key_der.len() - 32..]); - key - } } -// Copied from agent-rs -pub fn der_encode_public_key(public_key: Vec) -> Vec { - // see Section 4 "SubjectPublicKeyInfo" in https://tools.ietf.org/html/rfc8410 +fn valid_signature(pub_key_der: &Vec, signature: &[u8], payload: &Vec) -> Result<()> { + let document: Document = Document::from_public_key_der(&pub_key_der) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let info: SubjectPublicKeyInfoRef = document.decode_msg().unwrap(); + + let maybe_ed25519: Result = info + .clone() + .try_into() + .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string())); + let maybe_p256: Result = info + .try_into() + .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string())); - let id_ed25519 = oid!(1, 3, 101, 112); - let algorithm = Sequence(0, vec![ObjectIdentifier(0, id_ed25519)]); - let subject_public_key = BitString(0, public_key.len() * 8, public_key); - let subject_public_key_info = Sequence(0, vec![algorithm, subject_public_key]); - to_der(&subject_public_key_info).unwrap() + match (maybe_ed25519, maybe_p256) { + (Ok(k), _) => { + let signature = ed25519_dalek::Signature::from_slice(signature) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + k.verify_strict(&payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } + (_, Ok(k)) => { + let signature = p256::ecdsa::Signature::from_slice(&signature) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + k.verify(payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } + _ => Err(RecoveryError::InvalidPubKey( + "Unknown der format".to_string(), + )), + } } From 49388ae6a7949e48e36d5bf17b99f5ed5ab782b2 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 01:17:27 +0100 Subject: [PATCH 47/76] adding different implementations --- Cargo.lock | 1 - rs/nns/handlers/recovery/client/Cargo.toml | 1 - rs/nns/handlers/recovery/interface/src/lib.rs | 1 + .../interface/src/security_metadata.rs | 10 +-- .../recovery/interface/src/signing/ed25519.rs | 67 +++++++++++++++++ .../recovery/interface/src/signing/mod.rs | 11 +++ .../recovery/interface/src/signing/p256.rs | 73 +++++++++++++++++++ 7 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/signing/ed25519.rs create mode 100644 rs/nns/handlers/recovery/interface/src/signing/mod.rs create mode 100644 rs/nns/handlers/recovery/interface/src/signing/p256.rs diff --git a/Cargo.lock b/Cargo.lock index 2fb8ec67da7..55335ca2c2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10864,7 +10864,6 @@ dependencies = [ "async-trait", "candid", "ed25519-dalek", - "elliptic-curve 0.13.8", "ic-agent", "ic-nns-handler-recovery-interface", "p256", diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 8200c0e534d..ba29e997a8d 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -17,5 +17,4 @@ ic-nns-handler-recovery-interface.path = "../interface" [dev-dependencies] tokio.workspace = true p256.workspace = true -elliptic-curve.workspace = true pocket-ic.path = "../../../../../packages/pocket-ic" diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 9d65a4f8d8c..53f6ae9e2f8 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -5,6 +5,7 @@ use serde::Deserialize; pub mod recovery; pub mod recovery_init; pub mod security_metadata; +pub mod signing; pub mod simple_node_operator_record; #[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 240e3b16b69..367df0cd7b6 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -14,7 +14,7 @@ pub struct SecurityMetadata { /// /// Should be verified with a corresponding public key (also /// known as verifying key). - pub signature: [[u8; 32]; 2], + pub signature: Vec, /// What is being signed. /// /// In context of recovery canister proposal it includes @@ -30,7 +30,7 @@ pub struct SecurityMetadata { impl SecurityMetadata { pub fn empty() -> Self { Self { - signature: [[0; 32]; 2], + signature: vec![], payload: vec![], pub_key_der: vec![], } @@ -44,11 +44,7 @@ impl SecurityMetadata { /// Verifies the signature authenticity of security metadata. pub fn verify_signature(&self) -> Result<()> { - valid_signature( - &self.pub_key_der, - self.signature.as_flattened(), - &self.payload, - ) + valid_signature(&self.pub_key_der, &self.signature, &self.payload) } /// Verifies if the passed principal is derived from a given public key (also known as diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs new file mode 100644 index 00000000000..fa0d2643dca --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -0,0 +1,67 @@ +use ed25519_dalek::{pkcs8::EncodePublicKey, Signature, Signer, SigningKey, VerifyingKey}; +use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; + +use crate::RecoveryError; + +pub struct EdwardsCurve { + signing_key: Option, + verifying_key: VerifyingKey, +} + +impl super::Signer for EdwardsCurve { + fn sign_payload(&self, payload: &Vec) -> crate::Result> { + let signing_key = self + .signing_key + .clone() + .ok_or(RecoveryError::InvalidIdentity( + "Signing key missing".to_string(), + ))?; + + let signature = signing_key.sign(&payload); + Ok(signature.to_vec()) + } +} + +impl TryInto> for EdwardsCurve { + type Error = RecoveryError; + + fn try_into(self) -> Result, Self::Error> { + self.verifying_key + .to_public_key_der() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + .map(|document| document.into_vec()) + } +} + +impl super::Verifier for EdwardsCurve { + fn verify_payload(&self, payload: &Vec, signature: &Vec) -> crate::Result<()> { + let signature = Signature::from_slice(&signature) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + self.verifying_key + .verify_strict(&payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } +} + +impl TryFrom> for EdwardsCurve { + type Error = RecoveryError; + + fn try_from(value: Vec) -> Result { + let document: Document = Document::from_public_key_der(&value) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let info: SubjectPublicKeyInfoRef = document + .decode_msg() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let verifying_key: VerifyingKey = info + .try_into() + .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } +} diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs new file mode 100644 index 00000000000..870ecd6f62a --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -0,0 +1,11 @@ +use super::Result; + +pub mod ed25519; +pub mod p256; + +pub trait Verifier: TryFrom> { + fn verify_payload(&self, payload: &Vec, signature: &Vec) -> Result<()>; +} +pub trait Signer: TryInto> + Verifier { + fn sign_payload(&self, payload: &Vec) -> Result>; +} diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs new file mode 100644 index 00000000000..c5ef7b626f7 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -0,0 +1,73 @@ +use p256::ecdsa::{signature::SignerMut, signature::Verifier, Signature, SigningKey, VerifyingKey}; +use p256::pkcs8::EncodePublicKey; +use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; + +use crate::RecoveryError; + +pub struct Prime256 { + signing_key: Option, + verifying_key: VerifyingKey, +} + +impl super::Signer for Prime256 { + fn sign_payload(&self, payload: &Vec) -> crate::Result> { + let mut signing_key = self + .signing_key + .clone() + .ok_or(RecoveryError::InvalidIdentity( + "Signing key missing".to_string(), + ))?; + + let signature: Signature = signing_key + .try_sign(&payload) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + let r = signature.r().to_bytes().to_vec(); + let s = signature.s().to_bytes().to_vec(); + Ok(r.into_iter().chain(s.into_iter()).collect()) + } +} + +impl TryInto> for Prime256 { + type Error = RecoveryError; + + fn try_into(self) -> Result, Self::Error> { + self.verifying_key + .to_public_key_der() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + .map(|document| document.into_vec()) + } +} + +impl super::Verifier for Prime256 { + fn verify_payload(&self, payload: &Vec, signature: &Vec) -> crate::Result<()> { + let signature = Signature::from_slice(&signature) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + self.verifying_key + .verify(payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } +} + +impl TryFrom> for Prime256 { + type Error = RecoveryError; + + fn try_from(value: Vec) -> Result { + let document: Document = Document::from_public_key_der(&value) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let info: SubjectPublicKeyInfoRef = document + .decode_msg() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let verifying_key: VerifyingKey = info + .try_into() + .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } +} From d344ff2069760d9c56b077aa89e4e4da1a0361d6 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 01:38:17 +0100 Subject: [PATCH 48/76] adapting security metadata --- .../interface/src/security_metadata.rs | 42 ++----------------- .../recovery/interface/src/signing/ed25519.rs | 4 +- .../recovery/interface/src/signing/mod.rs | 33 ++++++++++++++- .../recovery/interface/src/signing/p256.rs | 4 +- 4 files changed, 38 insertions(+), 45 deletions(-) diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 367df0cd7b6..3b819cd27dc 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -1,9 +1,8 @@ +use crate::signing::verify_payload_naive; + use super::*; use candid::{CandidType, Principal}; -use p256::ecdsa::signature::Verifier; -use p256::pkcs8::DecodePublicKey; use serde::Deserialize; -use spki::{Document, SubjectPublicKeyInfoRef}; #[derive(Clone, Debug, CandidType, Deserialize)] /// Wrapper struct containing information regarding integrity. @@ -44,7 +43,7 @@ impl SecurityMetadata { /// Verifies the signature authenticity of security metadata. pub fn verify_signature(&self) -> Result<()> { - valid_signature(&self.pub_key_der, &self.signature, &self.payload) + verify_payload_naive(&self.pub_key_der, &self.signature, &self.payload) } /// Verifies if the passed principal is derived from a given public key (also known as @@ -61,38 +60,3 @@ impl SecurityMetadata { } } } - -fn valid_signature(pub_key_der: &Vec, signature: &[u8], payload: &Vec) -> Result<()> { - let document: Document = Document::from_public_key_der(&pub_key_der) - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - - let info: SubjectPublicKeyInfoRef = document.decode_msg().unwrap(); - - let maybe_ed25519: Result = info - .clone() - .try_into() - .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string())); - let maybe_p256: Result = info - .try_into() - .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string())); - - match (maybe_ed25519, maybe_p256) { - (Ok(k), _) => { - let signature = ed25519_dalek::Signature::from_slice(signature) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - k.verify_strict(&payload, &signature) - .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) - } - (_, Ok(k)) => { - let signature = p256::ecdsa::Signature::from_slice(&signature) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - k.verify(payload, &signature) - .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) - } - _ => Err(RecoveryError::InvalidPubKey( - "Unknown der format".to_string(), - )), - } -} diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index fa0d2643dca..9b96f5889a1 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -9,7 +9,7 @@ pub struct EdwardsCurve { } impl super::Signer for EdwardsCurve { - fn sign_payload(&self, payload: &Vec) -> crate::Result> { + fn sign_payload(&self, payload: &[u8]) -> crate::Result> { let signing_key = self .signing_key .clone() @@ -34,7 +34,7 @@ impl TryInto> for EdwardsCurve { } impl super::Verifier for EdwardsCurve { - fn verify_payload(&self, payload: &Vec, signature: &Vec) -> crate::Result<()> { + fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { let signature = Signature::from_slice(&signature) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index 870ecd6f62a..b116f70997a 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -1,11 +1,40 @@ +use ed25519::EdwardsCurve; +use p256::Prime256; + +use crate::RecoveryError; + use super::Result; pub mod ed25519; pub mod p256; pub trait Verifier: TryFrom> { - fn verify_payload(&self, payload: &Vec, signature: &Vec) -> Result<()>; + fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> Result<()>; } pub trait Signer: TryInto> + Verifier { - fn sign_payload(&self, payload: &Vec) -> Result>; + fn sign_payload(&self, payload: &[u8]) -> Result>; +} + +pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[u8]) -> Result<()> { + match EdwardsCurve::try_from(public_key_der.to_vec()) { + Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), + _ => {} + } + + match Prime256::try_from(public_key_der.to_vec()) { + Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), + _ => {} + } + + Err(RecoveryError::InvalidPubKey( + "Couldn't decode public key der with any known algorithm".to_string(), + )) +} + +fn verify_payload_naive_inner( + verifier: impl Verifier, + payload: &[u8], + signature: &[u8], +) -> Result<()> { + verifier.verify_payload(payload, signature) } diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index c5ef7b626f7..c50066ff879 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -10,7 +10,7 @@ pub struct Prime256 { } impl super::Signer for Prime256 { - fn sign_payload(&self, payload: &Vec) -> crate::Result> { + fn sign_payload(&self, payload: &[u8]) -> crate::Result> { let mut signing_key = self .signing_key .clone() @@ -40,7 +40,7 @@ impl TryInto> for Prime256 { } impl super::Verifier for Prime256 { - fn verify_payload(&self, payload: &Vec, signature: &Vec) -> crate::Result<()> { + fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { let signature = Signature::from_slice(&signature) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; From 3ea5747ba74b071875f41f8ab1da32d8363c57b1 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 02:55:21 +0100 Subject: [PATCH 49/76] refactoring tests --- rs/nns/handlers/recovery/client/BUILD.bazel | 2 +- rs/nns/handlers/recovery/client/Cargo.toml | 2 +- .../recovery/client/src/implementation.rs | 24 +- rs/nns/handlers/recovery/client/src/lib.rs | 2 +- .../recovery/client/src/tests/general.rs | 274 +++++++++--------- .../handlers/recovery/client/src/tests/mod.rs | 78 ++--- .../recovery/interface/src/recovery.rs | 6 - .../recovery/interface/src/signing/ed25519.rs | 34 ++- .../recovery/interface/src/signing/mod.rs | 10 +- .../recovery/interface/src/signing/p256.rs | 33 ++- 10 files changed, 216 insertions(+), 249 deletions(-) diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 3a19536c583..7f02bb624c0 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -7,7 +7,6 @@ DEPENDENCIES = [ "//rs/nns/handlers/recovery/interface", "@crate_index//:candid", "@crate_index//:serde", - "@crate_index//:ed25519-dalek", "@crate_index//:ic-agent", ] @@ -19,6 +18,7 @@ DEV_DEPENDENCIES = [ "//packages/pocket-ic", "@crate_index//:tokio", "@crate_index//:p256", + "@crate_index//:ed25519-dalek", ] MACRO_DEV_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index ba29e997a8d..705a994383c 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -9,7 +9,6 @@ edition.workspace = true [dependencies] candid = { workspace = true } serde = { workspace = true } -ed25519-dalek.workspace = true ic-agent.workspace = true async-trait.workspace = true ic-nns-handler-recovery-interface.path = "../interface" @@ -17,4 +16,5 @@ ic-nns-handler-recovery-interface.path = "../interface" [dev-dependencies] tokio.workspace = true p256.workspace = true +ed25519-dalek.workspace = true pocket-ic.path = "../../../../../packages/pocket-ic" diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index f060f7ee9d1..b359c9b4a4a 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -1,11 +1,12 @@ +use std::sync::Arc; + use async_trait::async_trait; use candid::{CandidType, Principal}; -use ed25519_dalek::pkcs8::EncodePublicKey; -use ed25519_dalek::SigningKey; use ic_agent::Agent; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, + signing::Signer, simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, RecoveryError, Result, VerifyIntegirty, }; @@ -15,15 +16,15 @@ use crate::RecoveryCanister; pub struct RecoveryCanisterImpl { canister_id: Principal, ic_agent: Agent, - signing_key: SigningKey, + signer: Arc, } impl RecoveryCanisterImpl { - pub fn new(ic_agent: Agent, canister_id: Principal, signing_key: SigningKey) -> Self { + pub fn new(ic_agent: Agent, canister_id: Principal, signer: Arc) -> Self { Self { ic_agent, canister_id, - signing_key, + signer, } } @@ -89,10 +90,12 @@ impl RecoveryCanister for RecoveryCanisterImpl { Ok(response) } - async fn vote_on_latest_proposal(&mut self, ballot: Ballot) -> Result<()> { + async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()> { self.ensure_not_anonymous()?; let last_proposal = self.fetch_latest_proposal().await?; - let signature = last_proposal.sign(&mut self.signing_key)?; + let signature = self + .signer + .sign_payload(&last_proposal.signature_payload()?)?; self.update( "vote_on_proposal", @@ -100,12 +103,7 @@ impl RecoveryCanister for RecoveryCanisterImpl { security_metadata: SecurityMetadata { signature, payload: last_proposal.signature_payload()?, - pub_key_der: self - .signing_key - .verifying_key() - .to_public_key_der() - .unwrap() - .into_vec(), + pub_key_der: self.signer.to_public_key_der()?, }, ballot, })?, diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index 35e366fc275..680001a785c 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -16,7 +16,7 @@ pub trait RecoveryCanister { async fn get_pending_recovery_proposals(&self) -> Result>; - async fn vote_on_latest_proposal(&mut self, ballot: Ballot) -> Result<()>; + async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()>; async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()>; diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 7a7abedebc3..82c620f9df2 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,152 +1,156 @@ -use candid::Principal; -use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryPayload, RecoveryProposal, VoteOnRecoveryProposal}, - recovery_init::RecoveryInitArgs, - security_metadata::SecurityMetadata, - simple_node_operator_record::SimpleNodeOperatorRecord, - Ballot, VerifyIntegirty, -}; -use p256::{ - ecdsa::{self, signature::SignerMut, SigningKey}, - elliptic_curve::{rand_core::OsRng, SecretKey}, - pkcs8::EncodePublicKey, - NistP256, -}; +use std::sync::Arc; + +use ed25519_dalek::SigningKey as EdSigningKey; +use ic_agent::identity::BasicIdentity; +use ic_nns_handler_recovery_interface::signing::{ed25519::EdwardsCurve, Verifier}; +use p256::elliptic_curve::rand_core::OsRng; use crate::{ - tests::{generate_node_operators, preconfigured_recovery_init_args}, + implementation::RecoveryCanisterImpl, + tests::{generate_node_operators, get_ic_agent, preconfigured_recovery_init_args}, RecoveryCanister, }; -use super::{get_client, init_pocket_ic}; +use super::init_pocket_ic; #[tokio::test] async fn can_get_node_operators() { - let node_operators_with_keys = generate_node_operators(); - let (mut pic, canister) = - init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let client = get_client(&mut pic, canister).await; - - let response = client.get_node_operators_in_nns().await; - - assert!(response.is_ok()); - let current_operators = response.unwrap(); - assert!(current_operators.len().eq(&node_operators_with_keys.len())) -} + let key = EdSigningKey::generate(&mut OsRng); + let signer = EdwardsCurve::new(key.clone()); -#[tokio::test] -async fn can_place_proposals() { - let node_operators_with_keys = generate_node_operators(); - let (mut pic, canister) = + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let mut node_operator_iter = node_operators_with_keys.iter(); - let first = node_operator_iter.next().unwrap(); - let first_client = first - .into_recovery_canister_client(&mut pic, canister) - .await; - - let response = first_client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) - .await; + let identity = BasicIdentity::from_signing_key((key.to_bytes()).into()); - println!("{:?}", response); - assert!(response.is_ok()); -} - -#[tokio::test] -async fn can_vote_on_proposals() { - let node_operators_with_keys = generate_node_operators(); - let (mut pic, canister) = - init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - - let mut node_operator_iter = node_operators_with_keys.iter(); - let first = node_operator_iter.next().unwrap(); - let mut first_client = first - .into_recovery_canister_client(&mut pic, canister) - .await; + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); - first_client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) - .await - .unwrap(); + let response = client.get_node_operators_in_nns().await; - let response = first_client.vote_on_latest_proposal(Ballot::Yes).await; assert!(response.is_ok()); - - let latest = first_client.get_pending_recovery_proposals().await.unwrap(); - assert!(latest.iter().verify_integrity().is_ok()) + let current_operators = response.unwrap(); + assert!(current_operators.len().eq(&node_operators_with_keys.len())) } -#[tokio::test] -async fn can_use_prime256_keys() { - let new_key_pair: SecretKey = SecretKey::random(&mut OsRng); - let pub_key = new_key_pair.public_key(); - let node_operator = SimpleNodeOperatorRecord { - operator_id: Principal::self_authenticating(pub_key.to_public_key_der().unwrap()), - nodes: vec![Principal::anonymous()], - }; - - let (pic, canister) = init_pocket_ic(RecoveryInitArgs { - initial_node_operator_records: vec![node_operator.clone()], - }) - .await; - - pic.update_call( - canister, - node_operator.operator_id, - "submit_new_recovery_proposal", - candid::encode_one(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) - .unwrap(), - ) - .await - .unwrap(); - - let pending = pic - .query_call( - canister, - Principal::anonymous(), - "get_pending_recovery_proposals", - candid::encode_one(()).unwrap(), - ) - .await - .unwrap(); - - let pending: Vec = candid::decode_one(&pending).unwrap(); - let last = pending.last().unwrap(); - - let mut signing_key: SigningKey = new_key_pair.into(); - let signature: ecdsa::Signature = signing_key - .try_sign(&last.signature_payload().unwrap()) - .unwrap(); - - let mut r = [0; 32]; - let mut s = [0; 32]; - r.copy_from_slice(&signature.r().as_ref().to_bytes()); - s.copy_from_slice(&signature.s().as_ref().to_bytes()); - - let response = pic - .update_call( - canister, - node_operator.operator_id, - "vote_on_proposal", - candid::encode_one(VoteOnRecoveryProposal { - security_metadata: SecurityMetadata { - signature: [r, s], - payload: last.signature_payload().unwrap(), - pub_key_der: pub_key.to_public_key_der().unwrap().into_vec(), - }, - ballot: Ballot::Yes, - }) - .unwrap(), - ) - .await; - - assert!(response.is_ok()) -} +// #[tokio::test] +// async fn can_place_proposals() { +// let node_operators_with_keys = generate_node_operators(); +// let (mut pic, canister) = +// init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + +// let mut node_operator_iter = node_operators_with_keys.iter(); +// let first = node_operator_iter.next().unwrap(); +// let first_client = first +// .into_recovery_canister_client(&mut pic, canister) +// .await; + +// let response = first_client +// .submit_new_recovery_proposal(NewRecoveryProposal { +// payload: RecoveryPayload::Halt, +// }) +// .await; + +// println!("{:?}", response); +// assert!(response.is_ok()); +// } + +// #[tokio::test] +// async fn can_vote_on_proposals() { +// let node_operators_with_keys = generate_node_operators(); +// let (mut pic, canister) = +// init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + +// let mut node_operator_iter = node_operators_with_keys.iter(); +// let first = node_operator_iter.next().unwrap(); +// let mut first_client = first +// .into_recovery_canister_client(&mut pic, canister) +// .await; + +// first_client +// .submit_new_recovery_proposal(NewRecoveryProposal { +// payload: RecoveryPayload::Halt, +// }) +// .await +// .unwrap(); + +// let response = first_client.vote_on_latest_proposal(Ballot::Yes).await; +// assert!(response.is_ok()); + +// let latest = first_client.get_pending_recovery_proposals().await.unwrap(); +// assert!(latest.iter().verify_integrity().is_ok()) +// } + +// #[tokio::test] +// async fn can_use_prime256_keys() { +// let new_key_pair: SecretKey = SecretKey::random(&mut OsRng); +// let pub_key = new_key_pair.public_key(); +// let node_operator = SimpleNodeOperatorRecord { +// operator_id: Principal::self_authenticating(pub_key.to_public_key_der().unwrap()), +// nodes: vec![Principal::anonymous()], +// }; + +// let (pic, canister) = init_pocket_ic(RecoveryInitArgs { +// initial_node_operator_records: vec![node_operator.clone()], +// }) +// .await; + +// pic.update_call( +// canister, +// node_operator.operator_id, +// "submit_new_recovery_proposal", +// candid::encode_one(NewRecoveryProposal { +// payload: RecoveryPayload::Halt, +// }) +// .unwrap(), +// ) +// .await +// .unwrap(); + +// let pending = pic +// .query_call( +// canister, +// Principal::anonymous(), +// "get_pending_recovery_proposals", +// candid::encode_one(()).unwrap(), +// ) +// .await +// .unwrap(); + +// let pending: Vec = candid::decode_one(&pending).unwrap(); +// let last = pending.last().unwrap(); + +// let mut signing_key: SigningKey = new_key_pair.into(); +// let signature: ecdsa::Signature = signing_key +// .try_sign(&last.signature_payload().unwrap()) +// .unwrap(); + +// let mut r = [0; 32]; +// let mut s = [0; 32]; +// r.copy_from_slice(&signature.r().as_ref().to_bytes()); +// s.copy_from_slice(&signature.s().as_ref().to_bytes()); + +// let response = pic +// .update_call( +// canister, +// node_operator.operator_id, +// "vote_on_proposal", +// candid::encode_one(VoteOnRecoveryProposal { +// security_metadata: SecurityMetadata { +// signature: [r, s], +// payload: last.signature_payload().unwrap(), +// pub_key_der: pub_key.to_public_key_der().unwrap().into_vec(), +// }, +// ballot: Ballot::Yes, +// }) +// .unwrap(), +// ) +// .await; + +// assert!(response.is_ok()) +// } diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs index 4cff7914ee5..b5d756ea374 100644 --- a/rs/nns/handlers/recovery/client/src/tests/mod.rs +++ b/rs/nns/handlers/recovery/client/src/tests/mod.rs @@ -1,15 +1,13 @@ use std::path::PathBuf; use candid::Principal; -use ed25519_dalek::pkcs8::EncodePublicKey; -use ed25519_dalek::{ed25519::signature::rand_core::OsRng, SigningKey}; -use ic_agent::{agent::AgentBuilder, identity::BasicIdentity, Agent}; +use ic_agent::agent::AgentBuilder; +use ic_agent::{Agent, Identity}; use ic_nns_handler_recovery_interface::{ recovery_init::RecoveryInitArgs, simple_node_operator_record::SimpleNodeOperatorRecord, }; use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; -use crate::{implementation::RecoveryCanisterImpl, RecoveryCanister}; mod general; fn fetch_canister_wasm(env: &str) -> Vec { @@ -21,7 +19,7 @@ fn fetch_canister_wasm(env: &str) -> Vec { } async fn init_pocket_ic(recovery_init_args: RecoveryInitArgs) -> (PocketIc, Principal) { - let pic = PocketIcBuilder::new() + let mut pic = PocketIcBuilder::new() .with_nns_subnet() .with_application_subnet() .build_async() @@ -39,36 +37,10 @@ async fn init_pocket_ic(recovery_init_args: RecoveryInitArgs) -> (PocketIc, Prin ) .await; + pic.make_live(None).await; (pic, canister) } -fn get_agent(signing_key: SigningKey, url: &str) -> Agent { - let identity = BasicIdentity::from_signing_key((*signing_key.as_bytes()).into()); - - AgentBuilder::default() - .with_url(url) - .with_boxed_identity(Box::new(identity)) - .build() - .unwrap() -} - -async fn get_client(pic: &mut PocketIc, canister: Principal) -> impl RecoveryCanister { - let signing_key = SigningKey::generate(&mut OsRng); - get_client_with_key(pic, canister, signing_key).await -} - -async fn get_client_with_key( - pic: &mut PocketIc, - canister: Principal, - signing_key: SigningKey, -) -> impl RecoveryCanister { - let url = pic.make_live(None).await; - let agent = get_agent(signing_key.clone(), url.as_str()); - agent.fetch_root_key().await.unwrap(); - - RecoveryCanisterImpl::new(agent, canister, signing_key) -} - fn preconfigured_recovery_init_args( operators_with_keys: &Vec, ) -> RecoveryInitArgs { @@ -80,34 +52,28 @@ fn preconfigured_recovery_init_args( } } -struct NodeOperatorWithKey { - record: SimpleNodeOperatorRecord, - key: SigningKey, +async fn get_ic_agent(identity: Box, endpoint: &str) -> Agent { + let agent = AgentBuilder::default() + .with_identity(identity) + .with_url(endpoint) + .build() + .unwrap(); + agent.fetch_root_key().await.unwrap(); + agent } -impl NodeOperatorWithKey { - async fn into_recovery_canister_client( - &self, - pic: &mut PocketIc, - canister: Principal, - ) -> impl RecoveryCanister { - get_client_with_key(pic, canister, self.key.clone()).await - } +struct NodeOperatorWithKey { + record: SimpleNodeOperatorRecord, } -fn generate_node_operators() -> Vec { - (0..10) - .map(|_| { - let key = SigningKey::generate(&mut OsRng); - NodeOperatorWithKey { - record: SimpleNodeOperatorRecord { - operator_id: Principal::self_authenticating( - key.verifying_key().to_public_key_der().unwrap().into_vec(), - ), - nodes: (0..4).map(|_| Principal::anonymous()).collect(), - }, - key, - } +fn generate_node_operators(signers: Vec>) -> Vec { + signers + .iter() + .map(|der_encoded_pub_key| NodeOperatorWithKey { + record: SimpleNodeOperatorRecord { + operator_id: Principal::self_authenticating(der_encoded_pub_key), + nodes: (0..4).map(|_| Principal::anonymous()).collect(), + }, }) .collect() } diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index deb98ded789..055f64ce81f 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use crate::*; use candid::{CandidType, Principal}; -use ed25519_dalek::{ed25519::signature::SignerMut, SigningKey}; use ic_base_types::{PrincipalId, SubnetId}; use registry_canister::mutations::{ do_recover_subnet::RecoverSubnetPayload, do_update_subnet::UpdateSubnetPayload, @@ -79,11 +78,6 @@ pub struct VoteOnRecoveryProposal { } impl RecoveryProposal { - pub fn sign(&self, signing_key: &mut SigningKey) -> Result<[[u8; 32]; 2]> { - let signature = signing_key.sign(&self.signature_payload()?); - Ok([*signature.r_bytes(), *signature.s_bytes()]) - } - pub fn signature_payload(&self) -> Result> { let self_without_ballots = Self { node_operator_ballots: vec![], diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index 9b96f5889a1..83eae24f592 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -3,6 +3,7 @@ use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; use crate::RecoveryError; +#[derive(Clone)] pub struct EdwardsCurve { signing_key: Option, verifying_key: VerifyingKey, @@ -22,17 +23,6 @@ impl super::Signer for EdwardsCurve { } } -impl TryInto> for EdwardsCurve { - type Error = RecoveryError; - - fn try_into(self) -> Result, Self::Error> { - self.verifying_key - .to_public_key_der() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) - .map(|document| document.into_vec()) - } -} - impl super::Verifier for EdwardsCurve { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { let signature = Signature::from_slice(&signature) @@ -42,13 +32,18 @@ impl super::Verifier for EdwardsCurve { .verify_strict(&payload, &signature) .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) } -} -impl TryFrom> for EdwardsCurve { - type Error = RecoveryError; + fn to_public_key_der(&self) -> crate::Result> { + self.verifying_key + .to_public_key_der() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + .map(|document| document.into_vec()) + } +} - fn try_from(value: Vec) -> Result { - let document: Document = Document::from_public_key_der(&value) +impl EdwardsCurve { + pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { + let document: Document = Document::from_public_key_der(public_key_der) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; let info: SubjectPublicKeyInfoRef = document @@ -64,4 +59,11 @@ impl TryFrom> for EdwardsCurve { verifying_key, }) } + + pub fn new(signing_key: SigningKey) -> Self { + Self { + verifying_key: signing_key.verifying_key(), + signing_key: Some(signing_key), + } + } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index b116f70997a..3dbc5d5d9fd 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -8,20 +8,22 @@ use super::Result; pub mod ed25519; pub mod p256; -pub trait Verifier: TryFrom> { +pub trait Verifier { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> Result<()>; + + fn to_public_key_der(&self) -> Result>; } -pub trait Signer: TryInto> + Verifier { +pub trait Signer: Verifier + Send + Sync { fn sign_payload(&self, payload: &[u8]) -> Result>; } pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[u8]) -> Result<()> { - match EdwardsCurve::try_from(public_key_der.to_vec()) { + match EdwardsCurve::from_public_key_der(public_key_der) { Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), _ => {} } - match Prime256::try_from(public_key_der.to_vec()) { + match Prime256::from_public_key_der(public_key_der) { Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), _ => {} } diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index c50066ff879..29af1cdaff5 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -28,17 +28,6 @@ impl super::Signer for Prime256 { } } -impl TryInto> for Prime256 { - type Error = RecoveryError; - - fn try_into(self) -> Result, Self::Error> { - self.verifying_key - .to_public_key_der() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) - .map(|document| document.into_vec()) - } -} - impl super::Verifier for Prime256 { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { let signature = Signature::from_slice(&signature) @@ -48,13 +37,18 @@ impl super::Verifier for Prime256 { .verify(payload, &signature) .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) } -} -impl TryFrom> for Prime256 { - type Error = RecoveryError; + fn to_public_key_der(&self) -> crate::Result> { + self.verifying_key + .to_public_key_der() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + .map(|document| document.into_vec()) + } +} - fn try_from(value: Vec) -> Result { - let document: Document = Document::from_public_key_der(&value) +impl Prime256 { + pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { + let document: Document = Document::from_public_key_der(public_key_der) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; let info: SubjectPublicKeyInfoRef = document @@ -70,4 +64,11 @@ impl TryFrom> for Prime256 { verifying_key, }) } + + pub fn new(signing_key: SigningKey) -> Self { + Self { + verifying_key: signing_key.verifying_key().clone(), + signing_key: Some(signing_key), + } + } } From 3e1b83c0e8d49b847224e74a9b5956cddd0c5f8b Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 03:00:39 +0100 Subject: [PATCH 50/76] adding proposal placement tests --- .../recovery/client/src/tests/general.rs | 79 ++++++++++++++----- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 82c620f9df2..7c0a7228056 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,9 +1,12 @@ use std::sync::Arc; use ed25519_dalek::SigningKey as EdSigningKey; -use ic_agent::identity::BasicIdentity; -use ic_nns_handler_recovery_interface::signing::{ed25519::EdwardsCurve, Verifier}; -use p256::elliptic_curve::rand_core::OsRng; +use ic_agent::identity::{BasicIdentity, Prime256v1Identity}; +use ic_nns_handler_recovery_interface::{ + recovery::{NewRecoveryProposal, RecoveryPayload}, + signing::{ed25519::EdwardsCurve, p256::Prime256, Verifier}, +}; +use p256::{elliptic_curve::rand_core::OsRng, SecretKey}; use crate::{ implementation::RecoveryCanisterImpl, @@ -38,27 +41,61 @@ async fn can_get_node_operators() { assert!(current_operators.len().eq(&node_operators_with_keys.len())) } -// #[tokio::test] -// async fn can_place_proposals() { -// let node_operators_with_keys = generate_node_operators(); -// let (mut pic, canister) = -// init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; +#[tokio::test] +async fn can_place_proposals_edwards() { + let key = EdSigningKey::generate(&mut OsRng); + let signer = EdwardsCurve::new(key.clone()); -// let mut node_operator_iter = node_operators_with_keys.iter(); -// let first = node_operator_iter.next().unwrap(); -// let first_client = first -// .into_recovery_canister_client(&mut pic, canister) -// .await; + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; -// let response = first_client -// .submit_new_recovery_proposal(NewRecoveryProposal { -// payload: RecoveryPayload::Halt, -// }) -// .await; + let identity = BasicIdentity::from_signing_key((key.to_bytes()).into()); -// println!("{:?}", response); -// assert!(response.is_ok()); -// } + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + let response = client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await; + + assert!(response.is_ok()); +} + +#[tokio::test] +async fn can_place_proposals_prime256() { + let secret_key = SecretKey::random(&mut OsRng); + let signing_key = secret_key.clone().into(); + + let signer = Prime256::new(signing_key); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = Prime256v1Identity::from_private_key(secret_key); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + let response = client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await; + + assert!(response.is_ok()); +} // #[tokio::test] // async fn can_vote_on_proposals() { From 21e80c4157358c2da5331a8f71da862de2a042fd Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 03:28:23 +0100 Subject: [PATCH 51/76] rewritten tests --- .../recovery/client/src/implementation.rs | 7 +- .../recovery/client/src/tests/general.rs | 158 +++++++----------- .../recovery/impl/canister/tests/mod.rs | 10 +- .../impl/canister/tests/voting_tests.rs | 14 +- .../interface/src/security_metadata.rs | 2 +- 5 files changed, 81 insertions(+), 110 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index b359c9b4a4a..cfd2897f126 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -93,16 +93,15 @@ impl RecoveryCanister for RecoveryCanisterImpl { async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()> { self.ensure_not_anonymous()?; let last_proposal = self.fetch_latest_proposal().await?; - let signature = self - .signer - .sign_payload(&last_proposal.signature_payload()?)?; + let payload = last_proposal.signature_payload()?; + let signature = self.signer.sign_payload(&payload)?; self.update( "vote_on_proposal", candid::encode_one(VoteOnRecoveryProposal { security_metadata: SecurityMetadata { signature, - payload: last_proposal.signature_payload()?, + payload, pub_key_der: self.signer.to_public_key_der()?, }, ballot, diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 7c0a7228056..41a9ea3acb5 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -5,6 +5,7 @@ use ic_agent::identity::{BasicIdentity, Prime256v1Identity}; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload}, signing::{ed25519::EdwardsCurve, p256::Prime256, Verifier}, + Ballot, }; use p256::{elliptic_curve::rand_core::OsRng, SecretKey}; @@ -97,97 +98,66 @@ async fn can_place_proposals_prime256() { assert!(response.is_ok()); } -// #[tokio::test] -// async fn can_vote_on_proposals() { -// let node_operators_with_keys = generate_node_operators(); -// let (mut pic, canister) = -// init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - -// let mut node_operator_iter = node_operators_with_keys.iter(); -// let first = node_operator_iter.next().unwrap(); -// let mut first_client = first -// .into_recovery_canister_client(&mut pic, canister) -// .await; - -// first_client -// .submit_new_recovery_proposal(NewRecoveryProposal { -// payload: RecoveryPayload::Halt, -// }) -// .await -// .unwrap(); - -// let response = first_client.vote_on_latest_proposal(Ballot::Yes).await; -// assert!(response.is_ok()); - -// let latest = first_client.get_pending_recovery_proposals().await.unwrap(); -// assert!(latest.iter().verify_integrity().is_ok()) -// } - -// #[tokio::test] -// async fn can_use_prime256_keys() { -// let new_key_pair: SecretKey = SecretKey::random(&mut OsRng); -// let pub_key = new_key_pair.public_key(); -// let node_operator = SimpleNodeOperatorRecord { -// operator_id: Principal::self_authenticating(pub_key.to_public_key_der().unwrap()), -// nodes: vec![Principal::anonymous()], -// }; - -// let (pic, canister) = init_pocket_ic(RecoveryInitArgs { -// initial_node_operator_records: vec![node_operator.clone()], -// }) -// .await; - -// pic.update_call( -// canister, -// node_operator.operator_id, -// "submit_new_recovery_proposal", -// candid::encode_one(NewRecoveryProposal { -// payload: RecoveryPayload::Halt, -// }) -// .unwrap(), -// ) -// .await -// .unwrap(); - -// let pending = pic -// .query_call( -// canister, -// Principal::anonymous(), -// "get_pending_recovery_proposals", -// candid::encode_one(()).unwrap(), -// ) -// .await -// .unwrap(); - -// let pending: Vec = candid::decode_one(&pending).unwrap(); -// let last = pending.last().unwrap(); - -// let mut signing_key: SigningKey = new_key_pair.into(); -// let signature: ecdsa::Signature = signing_key -// .try_sign(&last.signature_payload().unwrap()) -// .unwrap(); - -// let mut r = [0; 32]; -// let mut s = [0; 32]; -// r.copy_from_slice(&signature.r().as_ref().to_bytes()); -// s.copy_from_slice(&signature.s().as_ref().to_bytes()); - -// let response = pic -// .update_call( -// canister, -// node_operator.operator_id, -// "vote_on_proposal", -// candid::encode_one(VoteOnRecoveryProposal { -// security_metadata: SecurityMetadata { -// signature: [r, s], -// payload: last.signature_payload().unwrap(), -// pub_key_der: pub_key.to_public_key_der().unwrap().into_vec(), -// }, -// ballot: Ballot::Yes, -// }) -// .unwrap(), -// ) -// .await; - -// assert!(response.is_ok()) -// } +#[tokio::test] +async fn can_vote_on_proposals_edwards() { + let key = EdSigningKey::generate(&mut OsRng); + let signer = EdwardsCurve::new(key.clone()); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = BasicIdentity::from_signing_key((key.to_bytes()).into()); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await + .unwrap(); + + let response = client.vote_on_latest_proposal(Ballot::Yes).await; + println!("{:?}", response); + + assert!(response.is_ok()); +} + +#[tokio::test] +async fn can_vote_on_proposals_prime256() { + let secret_key = SecretKey::random(&mut OsRng); + let signing_key = secret_key.clone().into(); + + let signer = Prime256::new(signing_key); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = Prime256v1Identity::from_private_key(secret_key); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await + .unwrap(); + + let response = client.vote_on_latest_proposal(Ballot::Yes).await; + println!("{:?}", response); + + assert!(response.is_ok()); +} diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 8b1ba83440e..a8f82e596d3 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, path::PathBuf}; use candid::Principal; -use ed25519_dalek::{pkcs8::EncodePublicKey, SigningKey}; +use ed25519_dalek::{ed25519::signature::SignerMut, pkcs8::EncodePublicKey, SigningKey}; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery_interface::{ @@ -341,7 +341,9 @@ fn vote_with_only_ballot( // Add logic for signing so that this is valid let pending = get_pending(pic, canister); let last = pending.last().unwrap(); - let signature = last.sign(&mut sender.signing_key).unwrap(); + let payload = last.signature_payload().unwrap(); + let signature = sender.signing_key.sign(&payload); + let signature = signature.to_vec(); vote( pic, @@ -349,9 +351,7 @@ fn vote_with_only_ballot( sender.principal.0.clone(), VoteOnRecoveryProposal { security_metadata: SecurityMetadata { - payload: last - .signature_payload() - .expect("Should be able to fetch payload"), + payload, signature, pub_key_der: sender .signing_key diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index bbf76a33e6d..4bf034b203e 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,5 +1,5 @@ use candid::Principal; -use ed25519_dalek::{pkcs8::EncodePublicKey, SigningKey}; +use ed25519_dalek::{ed25519::signature::SignerMut, pkcs8::EncodePublicKey, SigningKey}; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, @@ -120,7 +120,7 @@ fn disallow_votes_bad_signature() { ballot: Ballot::Yes, security_metadata: SecurityMetadata { payload: vec![], - signature: [[0; 32]; 2], + signature: vec![], pub_key_der: first .signing_key .verifying_key() @@ -159,7 +159,11 @@ fn disallow_votes_wrong_public_key() { let last_proposal = pending.last().unwrap(); let mut new_key_pair = SigningKey::generate(&mut rand::rngs::OsRng); - let signature = last_proposal.sign(&mut new_key_pair).unwrap(); + let payload = last_proposal + .signature_payload() + .expect("Should be able to serialize payload"); + let signature = new_key_pair.sign(&payload); + let signature = signature.to_vec(); let response = vote( &pic, @@ -167,9 +171,7 @@ fn disallow_votes_wrong_public_key() { first.principal.0.clone(), VoteOnRecoveryProposal { security_metadata: SecurityMetadata { - payload: last_proposal - .signature_payload() - .expect("Should be able to serialize payload"), + payload, signature, pub_key_der: new_key_pair .verifying_key() diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 3b819cd27dc..d41941e652d 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -43,7 +43,7 @@ impl SecurityMetadata { /// Verifies the signature authenticity of security metadata. pub fn verify_signature(&self) -> Result<()> { - verify_payload_naive(&self.pub_key_der, &self.signature, &self.payload) + verify_payload_naive(&self.pub_key_der, &self.payload, &self.signature) } /// Verifies if the passed principal is derived from a given public key (also known as From a7536abaf703efa779cb4c6376eb4d4af2952308 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 13:08:37 +0100 Subject: [PATCH 52/76] adding support for secp256 --- Cargo.lock | 2 + rs/nns/handlers/recovery/client/BUILD.bazel | 1 + rs/nns/handlers/recovery/client/Cargo.toml | 1 + .../recovery/client/src/tests/general.rs | 73 ++++++++++++++++-- .../recovery/impl/src/recovery_proposal.rs | 1 + .../handlers/recovery/interface/BUILD.bazel | 1 + rs/nns/handlers/recovery/interface/Cargo.toml | 1 + .../recovery/interface/src/signing/k256.rs | 75 +++++++++++++++++++ .../recovery/interface/src/signing/mod.rs | 7 ++ 9 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/signing/k256.rs diff --git a/Cargo.lock b/Cargo.lock index 55335ca2c2e..3eeaf4240ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10866,6 +10866,7 @@ dependencies = [ "ed25519-dalek", "ic-agent", "ic-nns-handler-recovery-interface", + "k256 0.13.4", "p256", "pocket-ic", "serde", @@ -10880,6 +10881,7 @@ dependencies = [ "ed25519-dalek", "hex", "ic-base-types", + "k256 0.13.4", "p256", "registry-canister", "serde", diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 7f02bb624c0..35ad80c3e91 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -19,6 +19,7 @@ DEV_DEPENDENCIES = [ "@crate_index//:tokio", "@crate_index//:p256", "@crate_index//:ed25519-dalek", + "@crate_index//:k256", ] MACRO_DEV_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 705a994383c..d8f4d7d2173 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -16,5 +16,6 @@ ic-nns-handler-recovery-interface.path = "../interface" [dev-dependencies] tokio.workspace = true p256.workspace = true +k256.workspace = true ed25519-dalek.workspace = true pocket-ic.path = "../../../../../packages/pocket-ic" diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 41a9ea3acb5..6ad844f93bc 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use ed25519_dalek::SigningKey as EdSigningKey; -use ic_agent::identity::{BasicIdentity, Prime256v1Identity}; +use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload}, - signing::{ed25519::EdwardsCurve, p256::Prime256, Verifier}, + signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Verifier}, Ballot, }; -use p256::{elliptic_curve::rand_core::OsRng, SecretKey}; +use k256::SecretKey as k256SecretKey; +use p256::{elliptic_curve::rand_core::OsRng, SecretKey as p256SecretKey}; use crate::{ implementation::RecoveryCanisterImpl, @@ -71,7 +72,7 @@ async fn can_place_proposals_edwards() { #[tokio::test] async fn can_place_proposals_prime256() { - let secret_key = SecretKey::random(&mut OsRng); + let secret_key = p256SecretKey::random(&mut OsRng); let signing_key = secret_key.clone().into(); let signer = Prime256::new(signing_key); @@ -98,6 +99,35 @@ async fn can_place_proposals_prime256() { assert!(response.is_ok()); } +#[tokio::test] +async fn can_place_proposals_secp256() { + let secret_key = k256SecretKey::random(&mut OsRng); + let signing_key = secret_key.clone().into(); + + let signer = Secp256k1::new(signing_key); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = Secp256k1Identity::from_private_key(secret_key); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + let response = client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await; + + assert!(response.is_ok()); +} + #[tokio::test] async fn can_vote_on_proposals_edwards() { let key = EdSigningKey::generate(&mut OsRng); @@ -131,7 +161,7 @@ async fn can_vote_on_proposals_edwards() { #[tokio::test] async fn can_vote_on_proposals_prime256() { - let secret_key = SecretKey::random(&mut OsRng); + let secret_key = p256SecretKey::random(&mut OsRng); let signing_key = secret_key.clone().into(); let signer = Prime256::new(signing_key); @@ -161,3 +191,36 @@ async fn can_vote_on_proposals_prime256() { assert!(response.is_ok()); } + +#[tokio::test] +async fn can_vote_on_proposals_secp256() { + let secret_key = k256SecretKey::random(&mut OsRng); + let signing_key = secret_key.clone().into(); + + let signer = Secp256k1::new(signing_key); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = Secp256k1Identity::from_private_key(secret_key); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + client + .submit_new_recovery_proposal(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + }) + .await + .unwrap(); + + let response = client.vote_on_latest_proposal(Ballot::Yes).await; + println!("{:?}", response); + + assert!(response.is_ok()); +} diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index d0fdc60404c..d5c8df1f3a8 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -50,6 +50,7 @@ pub fn submit_recovery_proposal( RecoveryPayload::Halt => { proposals.push(RecoveryProposal { proposer: caller.0.clone(), + // TODO: Use nanoseconds submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Halt, diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index f00979db357..d23b3156679 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -10,6 +10,7 @@ DEPENDENCIES = [ "@crate_index//:hex", "@crate_index//:spki", "@crate_index//:p256", + "@crate_index//:k256", "//rs/registry/canister", "//rs/types/base_types", ] diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index c191c4eb667..33cf061a595 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -15,3 +15,4 @@ ic-base-types.path = "../../../../types/base_types" hex.workspace = true spki.workspace = true p256.workspace = true +k256.workspace = true diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs new file mode 100644 index 00000000000..4ca0b639c59 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -0,0 +1,75 @@ +use k256::ecdsa::signature::{Signer, Verifier}; +use k256::ecdsa::{Signature, SigningKey, VerifyingKey}; +use k256::pkcs8::{Document, EncodePublicKey}; +use spki::{DecodePublicKey, SubjectPublicKeyInfoRef}; + +use crate::RecoveryError; + +pub struct Secp256k1 { + signing_key: Option, + verifying_key: VerifyingKey, +} + +impl super::Verifier for Secp256k1 { + fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { + let signature = Signature::from_slice(signature) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + self.verifying_key + .verify(&payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } + + fn to_public_key_der(&self) -> crate::Result> { + self.verifying_key + .to_public_key_der() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + .map(|document| document.into_vec()) + } +} + +impl super::Signer for Secp256k1 { + fn sign_payload(&self, payload: &[u8]) -> crate::Result> { + let signing_key = self + .signing_key + .clone() + .ok_or(RecoveryError::InvalidIdentity( + "Signing key missing".to_string(), + ))?; + + let signature: Signature = signing_key + .try_sign(&payload) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + let r = signature.r().to_bytes().to_vec(); + let s = signature.s().to_bytes().to_vec(); + Ok(r.into_iter().chain(s.into_iter()).collect()) + } +} + +impl Secp256k1 { + pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { + let document: Document = Document::from_public_key_der(public_key_der) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let info: SubjectPublicKeyInfoRef = document + .decode_msg() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + let verifying_key: VerifyingKey = info + .try_into() + .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } + + pub fn new(signing_key: SigningKey) -> Self { + Self { + verifying_key: signing_key.verifying_key().clone(), + signing_key: Some(signing_key), + } + } +} diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index 3dbc5d5d9fd..6f90c72657c 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -1,4 +1,5 @@ use ed25519::EdwardsCurve; +use k256::Secp256k1; use p256::Prime256; use crate::RecoveryError; @@ -6,6 +7,7 @@ use crate::RecoveryError; use super::Result; pub mod ed25519; +pub mod k256; pub mod p256; pub trait Verifier { @@ -28,6 +30,11 @@ pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[ _ => {} } + match Secp256k1::from_public_key_der(public_key_der) { + Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), + _ => {} + } + Err(RecoveryError::InvalidPubKey( "Couldn't decode public key der with any known algorithm".to_string(), )) From c9d0988c8b540dfbaa70a6f9198455f38fd706a9 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 15:58:53 +0100 Subject: [PATCH 53/76] adding proposal signatures --- .../impl/canister/tests/initial_args_test.rs | 17 +- .../recovery/impl/canister/tests/mod.rs | 31 +- .../canister/tests/proposal_logic_tests.rs | 330 +++++------------- .../impl/canister/tests/voting_tests.rs | 60 +--- .../recovery/impl/src/recovery_proposal.rs | 14 +- .../recovery/interface/src/recovery.rs | 6 + 6 files changed, 148 insertions(+), 310 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index cc66a37b52a..93faab34c91 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -1,10 +1,8 @@ use candid::Principal; use ic_base_types::PrincipalId; use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryPayload}, - recovery_init::RecoveryInitArgs, - simple_node_operator_record::SimpleNodeOperatorRecord, - Ballot, VerifyIntegirty, + recovery::RecoveryPayload, recovery_init::RecoveryInitArgs, + simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, VerifyIntegirty, }; use pocket_ic::{PocketIc, PocketIcBuilder}; @@ -67,15 +65,8 @@ fn initial_operators_should_be_able_to_place_proposals_and_vote() { let (pic, canister) = setup_and_install_canister(initial_arg); - let first = initial_node_operators.first().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let first = initial_node_operators.first_mut().unwrap(); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index a8f82e596d3..7480bda1194 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -1,11 +1,11 @@ -use std::{collections::BTreeMap, path::PathBuf}; +use std::{collections::BTreeMap, path::PathBuf, time::SystemTime}; use candid::Principal; use ed25519_dalek::{ed25519::signature::SignerMut, pkcs8::EncodePublicKey, SigningKey}; use ic_base_types::{CanisterId, PrincipalId, SubnetId}; use ic_nns_constants::REGISTRY_CANISTER_ID; use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery::{NewRecoveryProposal, RecoveryPayload, RecoveryProposal, VoteOnRecoveryProposal}, recovery_init::RecoveryInitArgs, security_metadata::SecurityMetadata, simple_node_operator_record::SimpleNodeOperatorRecord, @@ -302,14 +302,33 @@ fn init_pocket_ic(arguments: &mut RegistryPreparationArguments) -> (PocketIc, Pr fn submit_proposal( pic: &PocketIc, canister: Principal, - sender: Principal, - arg: NewRecoveryProposal, + sender: &mut NodeOperatorArg, + arg: RecoveryPayload, ) -> Result<(), String> { + // Duration from epoch + let from_epoch = SystemTime::UNIX_EPOCH.elapsed().unwrap(); + let seconds_payload = from_epoch.as_secs().to_le_bytes().to_vec(); + let signature = sender.signing_key.sign(&seconds_payload); + let signature = signature.to_vec(); + let response = pic.update_call( canister.into(), - sender, + sender.principal.0.clone(), "submit_new_recovery_proposal", - candid::encode_one(arg).unwrap(), + candid::encode_one(NewRecoveryProposal { + payload: arg, + security_metadata: SecurityMetadata { + signature, + payload: seconds_payload, + pub_key_der: sender + .signing_key + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), + }, + }) + .unwrap(), ); let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); println!("{:?}", response); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index ee9e61b0b8e..abd314673b4 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -1,6 +1,7 @@ use candid::Principal; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload}, + security_metadata::SecurityMetadata, Ballot, }; use ic_registry_subnet_type::SubnetType; @@ -22,14 +23,7 @@ fn place_first_proposal() { let mut node_operators = extract_node_operators_from_init_data(&args); let first = node_operators.iter_mut().next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); @@ -64,23 +58,19 @@ fn place_non_halt_first_proposal() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let first = node_operators.iter().next().unwrap(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let first = node_operators.iter_mut().next().unwrap(); let invalid_first_proposals = vec![ - NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, - }, - NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, + RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }, + RecoveryPayload::Unhalt, ]; for proposal in invalid_first_proposals { - let response = submit_proposal(&pic, canister, first.principal.0.clone(), proposal); + let response = submit_proposal(&pic, canister, first, proposal); assert!(response.is_err()); let pending_proposals = get_pending(&pic, canister); @@ -97,25 +87,11 @@ fn replace_first_proposal_after_voting_no_on_the_first() { let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); // Try resubmitting - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_err()); // To achieve byzantine majority in default setup 7 @@ -130,14 +106,7 @@ fn replace_first_proposal_after_voting_no_on_the_first() { } // Resubmit - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); } @@ -146,14 +115,21 @@ fn disallow_unknown_node_operators_from_placing_proposals() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let response = submit_proposal( - &pic, - canister, - Principal::anonymous(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = pic + .update_call( + canister.into(), + Principal::anonymous(), + "submit_new_recovery_proposal", + candid::encode_one(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + security_metadata: SecurityMetadata::empty(), + }) + .unwrap(), + ) + .unwrap(); + + let response: Result<(), String> = candid::decode_one(&response).unwrap(); + assert!(response.is_err()); } @@ -162,7 +138,7 @@ fn disallow_node_operators_from_different_subnets_from_placing_proposals() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let non_system_subnet = args + let mut non_system_subnet = args .subnet_node_operators .iter() .find_map(|subnet| match !subnet.subnet_type.eq(&SubnetType::System) { @@ -170,16 +146,9 @@ fn disallow_node_operators_from_different_subnets_from_placing_proposals() { true => Some(subnet.node_operators.clone()), }) .unwrap(); - let first_node_operator = non_system_subnet.iter().next().unwrap(); - - let response = submit_proposal( - &pic, - canister, - first_node_operator.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let first_node_operator = non_system_subnet.iter_mut().next().unwrap(); + + let response = submit_proposal(&pic, canister, first_node_operator, RecoveryPayload::Halt); assert!(response.is_err()); } @@ -193,14 +162,7 @@ fn place_and_execute_first_proposal( let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); // To achieve byzantine majority in default setup 7 @@ -224,27 +186,20 @@ fn place_second_proposal_recovery() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, + let new_proposal = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); let pending_proposals = get_pending(&pic, canister); let latest_proposal = pending_proposals.last().unwrap(); - assert!(latest_proposal.payload.eq(&new_proposal.payload)); + assert!(latest_proposal.payload.eq(&new_proposal)); assert!( !latest_proposal.is_byzantine_majority_no() && !latest_proposal.is_byzantine_majority_yes() ) @@ -255,24 +210,17 @@ fn place_second_proposal_unhalt() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); let pending_proposals = get_pending(&pic, canister); let latest_proposal = pending_proposals.last().unwrap(); - assert!(latest_proposal.payload.eq(&new_proposal.payload)); + assert!(latest_proposal.payload.eq(&new_proposal)); assert!( !latest_proposal.is_byzantine_majority_no() && !latest_proposal.is_byzantine_majority_yes() ) @@ -283,18 +231,11 @@ fn place_second_proposal_halt() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_err()); } @@ -308,18 +249,11 @@ fn second_proposal_vote_against() { let first = node_operators_iterator.next().unwrap(); // Place the second - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, + let new_proposal = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 votes to vote against this @@ -344,18 +278,11 @@ fn second_proposal_recovery_vote_in() { let first = node_operators_iterator.next().unwrap(); // Place the second - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, + let new_proposal = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 to vote in @@ -382,18 +309,11 @@ fn second_proposal_recovery_vote_in_and_resubmit() { let first = node_operators_iterator.next().unwrap(); // Place the second - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, + let new_proposal = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 to vote in @@ -404,18 +324,11 @@ fn second_proposal_recovery_vote_in_and_resubmit() { assert!(response.is_ok()) } - let resubmitted_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 456, - state_hash: "456".to_string(), - }, + let resubmitted_proposal = RecoveryPayload::DoRecovery { + height: 456, + state_hash: "456".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - resubmitted_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, resubmitted_proposal.clone()); assert!(response.is_ok()); let pending = get_pending(&pic, canister); @@ -423,7 +336,7 @@ fn second_proposal_recovery_vote_in_and_resubmit() { let last = pending.last().unwrap(); assert!(!last.is_byzantine_majority_no() && !last.is_byzantine_majority_yes()); - assert_eq!(last.payload, resubmitted_proposal.payload) + assert_eq!(last.payload, resubmitted_proposal) } #[test] @@ -436,15 +349,8 @@ fn second_proposal_unhalt_vote_in() { let first = node_operators_iterator.next().unwrap(); // Place the second - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 to vote it in @@ -465,35 +371,21 @@ fn submit_first_two_second_not_voted_in_place_third() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_first_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the second - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, + let new_proposal = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // Place the third - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_err()); } @@ -507,18 +399,11 @@ fn place_and_execute_second_proposal( let first = node_operators_iterator.next().unwrap(); // Place the second - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::DoRecovery { - height: 123, - state_hash: "123".to_string(), - }, + let new_proposal = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 to vote it in @@ -540,20 +425,13 @@ fn place_and_execute_second_proposal( fn submit_first_two_second_voted_in_place_third() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_second_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the third - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); let pending = get_pending(&pic, canister); @@ -571,15 +449,8 @@ fn vote_against_last_proposal() { let first = node_operators_iterator.next().unwrap(); // Place the third - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 votes to vote against this proposal @@ -607,15 +478,8 @@ fn vote_in_last_proposal() { let first = node_operators_iterator.next().unwrap(); // Place the third - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); // We need 7 votes to vote for this proposal @@ -637,20 +501,13 @@ fn vote_in_last_proposal() { fn place_any_proposal_after_there_are_three() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = place_and_execute_second_proposal(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); // Place the third - let new_proposal = NewRecoveryProposal { - payload: RecoveryPayload::Unhalt, - }; - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - new_proposal.clone(), - ); + let new_proposal = RecoveryPayload::Unhalt; + let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); let payloads = vec![ @@ -662,12 +519,7 @@ fn place_any_proposal_after_there_are_three() { RecoveryPayload::Unhalt, ]; for payload in payloads { - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { payload }, - ); + let response = submit_proposal(&pic, canister, first, payload); assert!(response.is_err()) } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 4bf034b203e..94df945ab8a 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -1,7 +1,7 @@ use candid::Principal; use ed25519_dalek::{ed25519::signature::SignerMut, pkcs8::EncodePublicKey, SigningKey}; use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryPayload, VoteOnRecoveryProposal}, + recovery::{RecoveryPayload, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, Ballot, }; @@ -22,14 +22,7 @@ fn disallow_double_vote() { let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); @@ -46,18 +39,11 @@ fn disallow_vote_anonymous() { let mut args = RegistryPreparationArguments::default(); let (pic, canister) = init_pocket_ic(&mut args); - let node_operators = extract_node_operators_from_init_data(&args); - let mut node_operators_iterator = node_operators.iter(); + let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); @@ -75,14 +61,7 @@ fn allow_votes_even_if_executed() { let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); @@ -101,14 +80,7 @@ fn disallow_votes_bad_signature() { let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); @@ -145,14 +117,7 @@ fn disallow_votes_wrong_public_key() { let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); let pending = get_pending(&pic, canister); @@ -195,14 +160,7 @@ fn disallow_votes_anonymous() { let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); - let response = submit_proposal( - &pic, - canister, - first.principal.0.clone(), - NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }, - ); + let response = submit_proposal(&pic, canister, first, RecoveryPayload::Halt); assert!(response.is_ok()); let response = vote( diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index d5c8df1f3a8..30be35dbbf1 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -41,6 +41,9 @@ pub fn submit_recovery_proposal( return Err(message); } + // Verify metadata integrity + new_proposal.security_metadata.validate_metadata(&caller.0).map_err(|e| e.to_string())?; + PROPOSALS.with_borrow_mut(|proposals| { match proposals.len() { 0 => { @@ -54,6 +57,7 @@ pub fn submit_recovery_proposal( submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Halt, + security_metadata: new_proposal.security_metadata.clone() }); } _ => { @@ -91,6 +95,7 @@ pub fn submit_recovery_proposal( submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: new_proposal.payload.clone(), + security_metadata: new_proposal.security_metadata.clone() }); } _ => { @@ -127,6 +132,7 @@ pub fn submit_recovery_proposal( submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Unhalt, + security_metadata: new_proposal.security_metadata.clone() }); }, // Allow submitting a new recovery proposal only if the current one @@ -136,7 +142,13 @@ pub fn submit_recovery_proposal( // Remove the second_one proposals.pop(); - proposals.push(RecoveryProposal { proposer: caller.0.clone(), submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: new_proposal.payload.clone() }); + proposals.push(RecoveryProposal { + proposer: caller.0.clone(), + submission_timestamp_seconds: now_seconds(), + security_metadata: new_proposal.security_metadata.clone(), + node_operator_ballots: initialize_ballots(&node_operators_in_nns), + payload: new_proposal.payload.clone() + }); }, (_, _) => { let message = format!( diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index 055f64ce81f..9a2ed551f59 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -62,12 +62,16 @@ pub struct RecoveryProposal { pub node_operator_ballots: Vec, /// Payload for the proposal. pub payload: RecoveryPayload, + /// Metadata used for verifying the user's identity and the integrity + /// of the proposal itself + pub security_metadata: SecurityMetadata, } #[derive(Debug, CandidType, Deserialize, Clone)] /// Conveniece struct used for submitting a new proposal pub struct NewRecoveryProposal { pub payload: RecoveryPayload, + pub security_metadata: SecurityMetadata, } #[derive(Debug, CandidType, Deserialize, Clone)] @@ -81,6 +85,7 @@ impl RecoveryProposal { pub fn signature_payload(&self) -> Result> { let self_without_ballots = Self { node_operator_ballots: vec![], + security_metadata: SecurityMetadata::empty(), ..self.clone() }; candid::encode_one(self_without_ballots) @@ -138,6 +143,7 @@ impl VerifyIntegirty for NodeOperatorBallot { impl VerifyIntegirty for RecoveryProposal { fn verify_integrity(&self) -> Result<()> { + self.security_metadata.validate_metadata(&self.proposer)?; self.node_operator_ballots .iter() .filter(|ballot| !ballot.ballot.eq(&Ballot::Undecided)) From f30e7249913f1142bddfba5abf4e14daf9cbd126 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 16:06:35 +0100 Subject: [PATCH 54/76] adding proposal signatures when submitting a proposal --- .../recovery/client/src/implementation.rs | 18 ++++++++++--- rs/nns/handlers/recovery/client/src/lib.rs | 4 +-- .../recovery/client/src/tests/general.rs | 26 +++++-------------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index cfd2897f126..f2705827b9d 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -1,10 +1,10 @@ -use std::sync::Arc; +use std::{sync::Arc, time::SystemTime}; use async_trait::async_trait; use candid::{CandidType, Principal}; use ic_agent::Agent; use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryProposal, VoteOnRecoveryProposal}, + recovery::{NewRecoveryProposal, RecoveryPayload, RecoveryProposal, VoteOnRecoveryProposal}, security_metadata::SecurityMetadata, signing::Signer, simple_node_operator_record::SimpleNodeOperatorRecord, @@ -110,12 +110,22 @@ impl RecoveryCanister for RecoveryCanisterImpl { .await } - async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()> { + async fn submit_new_recovery_proposal(&self, new_proposal: RecoveryPayload) -> Result<()> { self.ensure_not_anonymous()?; + let epoch = SystemTime::UNIX_EPOCH.elapsed().unwrap(); + let seconds_payload = epoch.as_secs().to_le_bytes().to_vec(); + let signature = self.signer.sign_payload(&seconds_payload)?; self.update( "submit_new_recovery_proposal", - candid::encode_one(new_proposal)?, + candid::encode_one(NewRecoveryProposal { + payload: new_proposal, + security_metadata: SecurityMetadata { + signature, + payload: seconds_payload, + pub_key_der: self.signer.to_public_key_der()?, + }, + })?, ) .await } diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index 680001a785c..ce131dd4a6f 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; -use ic_nns_handler_recovery_interface::recovery::{NewRecoveryProposal, RecoveryProposal}; +use ic_nns_handler_recovery_interface::recovery::{RecoveryPayload, RecoveryProposal}; use ic_nns_handler_recovery_interface::{ simple_node_operator_record::SimpleNodeOperatorRecord, Ballot, }; @@ -18,7 +18,7 @@ pub trait RecoveryCanister { async fn vote_on_latest_proposal(&self, ballot: Ballot) -> Result<()>; - async fn submit_new_recovery_proposal(&self, new_proposal: NewRecoveryProposal) -> Result<()>; + async fn submit_new_recovery_proposal(&self, new_proposal: RecoveryPayload) -> Result<()>; async fn fetch_latest_proposal(&self) -> Result { let proposal_chain = self.get_pending_recovery_proposals().await?; diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 6ad844f93bc..cb83702efd6 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use ed25519_dalek::SigningKey as EdSigningKey; use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; use ic_nns_handler_recovery_interface::{ - recovery::{NewRecoveryProposal, RecoveryPayload}, + recovery::RecoveryPayload, signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Verifier}, Ballot, }; @@ -62,9 +62,7 @@ async fn can_place_proposals_edwards() { ); let response = client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) + .submit_new_recovery_proposal(RecoveryPayload::Halt) .await; assert!(response.is_ok()); @@ -91,9 +89,7 @@ async fn can_place_proposals_prime256() { ); let response = client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) + .submit_new_recovery_proposal(RecoveryPayload::Halt) .await; assert!(response.is_ok()); @@ -120,9 +116,7 @@ async fn can_place_proposals_secp256() { ); let response = client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) + .submit_new_recovery_proposal(RecoveryPayload::Halt) .await; assert!(response.is_ok()); @@ -147,9 +141,7 @@ async fn can_vote_on_proposals_edwards() { ); client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) + .submit_new_recovery_proposal(RecoveryPayload::Halt) .await .unwrap(); @@ -180,9 +172,7 @@ async fn can_vote_on_proposals_prime256() { ); client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) + .submit_new_recovery_proposal(RecoveryPayload::Halt) .await .unwrap(); @@ -213,9 +203,7 @@ async fn can_vote_on_proposals_secp256() { ); client - .submit_new_recovery_proposal(NewRecoveryProposal { - payload: RecoveryPayload::Halt, - }) + .submit_new_recovery_proposal(RecoveryPayload::Halt) .await .unwrap(); From ac79065980c90defd3d5c2cd92a1d548f73d7cc0 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 16:38:01 +0100 Subject: [PATCH 55/76] add time validation --- .../recovery/impl/canister/tests/mod.rs | 7 ++- .../canister/tests/proposal_logic_tests.rs | 45 +++++++++++++++++++ .../recovery/impl/src/recovery_proposal.rs | 25 +++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index 7480bda1194..d2c4e526424 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -305,9 +305,12 @@ fn submit_proposal( sender: &mut NodeOperatorArg, arg: RecoveryPayload, ) -> Result<(), String> { + // Update time so that it doesn't fail the threshold + pic.set_time(SystemTime::now()); + // Duration from epoch - let from_epoch = SystemTime::UNIX_EPOCH.elapsed().unwrap(); - let seconds_payload = from_epoch.as_secs().to_le_bytes().to_vec(); + let now = SystemTime::UNIX_EPOCH.elapsed().unwrap(); + let seconds_payload = now.as_secs().to_le_bytes().to_vec(); let signature = sender.signing_key.sign(&seconds_payload); let signature = signature.to_vec(); diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index abd314673b4..78753ea535a 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -1,4 +1,8 @@ +use std::time::{Duration, SystemTime}; + use candid::Principal; +use ed25519_dalek::pkcs8::EncodePublicKey; +use ed25519_dalek::Signer; use ic_nns_handler_recovery_interface::{ recovery::{NewRecoveryProposal, RecoveryPayload}, security_metadata::SecurityMetadata, @@ -524,3 +528,44 @@ fn place_any_proposal_after_there_are_three() { assert!(response.is_err()) } } + +#[test] +fn submit_proposal_lag_more_than_threshold() { + let mut args = RegistryPreparationArguments::default(); + let (pic, canister) = init_pocket_ic(&mut args); + + let mut node_operators = extract_node_operators_from_init_data(&args); + let first = node_operators.iter_mut().next().unwrap(); + + // Duration from epoch + let from_epoch = SystemTime::UNIX_EPOCH.elapsed().unwrap(); + let before_one_hour = from_epoch + .checked_sub(Duration::from_secs(1 * 60 * 60)) + .unwrap(); + let seconds_payload = before_one_hour.as_secs().to_le_bytes().to_vec(); + let signature = first.signing_key.sign(&seconds_payload); + let signature = signature.to_vec(); + + let response = pic.update_call( + canister.into(), + first.principal.0.clone(), + "submit_new_recovery_proposal", + candid::encode_one(NewRecoveryProposal { + payload: RecoveryPayload::Halt, + security_metadata: SecurityMetadata { + signature, + payload: seconds_payload, + pub_key_der: first + .signing_key + .verifying_key() + .to_public_key_der() + .unwrap() + .into_vec(), + }, + }) + .unwrap(), + ); + let response: Result<(), String> = candid::decode_one(response.unwrap().as_slice()).unwrap(); + + assert!(response.is_err()); +} diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 30be35dbbf1..968fc6a6af3 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -43,6 +43,9 @@ pub fn submit_recovery_proposal( // Verify metadata integrity new_proposal.security_metadata.validate_metadata(&caller.0).map_err(|e| e.to_string())?; + // Ensure that timestamp sent doesn't differ more than + // the threshold + check_secs_difference(&new_proposal.security_metadata.payload)?; PROPOSALS.with_borrow_mut(|proposals| { match proposals.len() { @@ -242,3 +245,25 @@ fn vote_on_last_proposal( Ok(()) } + +const ALLOWED_LAG: u64 = 10 * 60; // 10 minutes + +fn check_secs_difference(seconds_payload: &[u8]) -> Result<(), String> { + let now = now_seconds(); + + if seconds_payload.len() != 8 { + return Err(format!("Incorect signature lenght: {}", seconds_payload.len())); + } + + let mut total_input = [0; 8]; + total_input.copy_from_slice(seconds_payload); + + let payload_seconds = u64::from_le_bytes(total_input); + ic_cdk::println!("NOW {}, Sent {}", now, payload_seconds); + let abs_diff = now.abs_diff(payload_seconds); + + match abs_diff > ALLOWED_LAG { + true => Err(format!("Proposal submittion timestamp lags more than allowed {} seconds", ALLOWED_LAG)), + false => Ok(()) + } +} \ No newline at end of file From 93dabb3b613d1b06f79b4ace5d237662ba283148 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 16:59:12 +0100 Subject: [PATCH 56/76] linting --- .../recovery/client/src/implementation.rs | 2 +- .../handlers/recovery/client/src/tests/mod.rs | 9 +-- .../recovery/impl/canister/tests/mod.rs | 23 +++--- .../canister/tests/proposal_logic_tests.rs | 12 +-- .../impl/canister/tests/voting_tests.rs | 4 +- .../recovery/impl/src/node_operator_sync.rs | 4 +- .../recovery/impl/src/recovery_proposal.rs | 77 +++++++++++-------- rs/nns/handlers/recovery/interface/src/lib.rs | 36 +++++---- .../recovery/interface/src/signing/ed25519.rs | 6 +- .../recovery/interface/src/signing/k256.rs | 8 +- .../recovery/interface/src/signing/mod.rs | 15 ++-- .../recovery/interface/src/signing/p256.rs | 8 +- 12 files changed, 111 insertions(+), 93 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/implementation.rs b/rs/nns/handlers/recovery/client/src/implementation.rs index f2705827b9d..6d464d3c28b 100644 --- a/rs/nns/handlers/recovery/client/src/implementation.rs +++ b/rs/nns/handlers/recovery/client/src/implementation.rs @@ -63,7 +63,7 @@ impl RecoveryCanisterImpl { let principal = self .ic_agent .get_principal() - .map_err(|e| RecoveryError::AgentError(e))?; + .map_err(RecoveryError::AgentError)?; match Principal::anonymous().eq(&principal) { false => Ok(()), diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs index b5d756ea374..c925b18b729 100644 --- a/rs/nns/handlers/recovery/client/src/tests/mod.rs +++ b/rs/nns/handlers/recovery/client/src/tests/mod.rs @@ -12,10 +12,9 @@ mod general; fn fetch_canister_wasm(env: &str) -> Vec { let path: PathBuf = std::env::var(env) - .expect(&format!("Path should be set in environment variable {env}")) - .try_into() - .unwrap(); - std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) + .unwrap_or_else(|_| panic!("Path should be set in environment variable {env}")) + .into(); + std::fs::read(&path).unwrap_or_else(|_| panic!("Failed to read path {}", path.display())) } async fn init_pocket_ic(recovery_init_args: RecoveryInitArgs) -> (PocketIc, Principal) { @@ -42,7 +41,7 @@ async fn init_pocket_ic(recovery_init_args: RecoveryInitArgs) -> (PocketIc, Prin } fn preconfigured_recovery_init_args( - operators_with_keys: &Vec, + operators_with_keys: &[NodeOperatorWithKey], ) -> RecoveryInitArgs { RecoveryInitArgs { initial_node_operator_records: operators_with_keys diff --git a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs index d2c4e526424..8783552f2e9 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/mod.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/mod.rs @@ -43,10 +43,9 @@ mod voting_tests; fn fetch_canister_wasm(env: &str) -> Vec { let path: PathBuf = std::env::var(env) - .expect(&format!("Path should be set in environment variable {env}")) - .try_into() - .unwrap(); - std::fs::read(&path).expect(&format!("Failed to read path {}", path.display())) + .unwrap_or_else(|_| panic!("Path should be set in environment variable {env}")) + .into(); + std::fs::read(&path).unwrap_or_else(|_| panic!("Failed to read path {}", path.display())) } fn add_replica_version_records(total_mutations: &mut Vec) { @@ -180,7 +179,7 @@ fn prepare_registry( let (mutation, nodes) = prepare_registry_with_nodes_and_node_operator_id( operator_mutation_ids, operator_arg.num_nodes as u64, - operator_arg.principal.clone(), + operator_arg.principal, ); operator_mutation_ids += operator_arg.num_nodes; @@ -206,7 +205,7 @@ fn prepare_registry( .subnet_node_operators .iter() .find_map(|arg| match arg.subnet_type { - SubnetType::System => Some(arg.subnet_id.clone()), + SubnetType::System => Some(arg.subnet_id), _ => None, }) .expect("Missing system subnet"), @@ -315,8 +314,8 @@ fn submit_proposal( let signature = signature.to_vec(); let response = pic.update_call( - canister.into(), - sender.principal.0.clone(), + canister, + sender.principal.0, "submit_new_recovery_proposal", candid::encode_one(NewRecoveryProposal { payload: arg, @@ -341,7 +340,7 @@ fn submit_proposal( fn get_pending(pic: &PocketIc, canister: Principal) -> Vec { let response = pic .query_call( - canister.into(), + canister, Principal::anonymous(), "get_pending_recovery_proposals", candid::encode_one(()).unwrap(), @@ -370,7 +369,7 @@ fn vote_with_only_ballot( vote( pic, canister, - sender.principal.0.clone(), + sender.principal.0, VoteOnRecoveryProposal { security_metadata: SecurityMetadata { payload, @@ -395,7 +394,7 @@ fn vote( ) -> Result<(), String> { let response = pic .update_call( - canister.into(), + canister, sender, "vote_on_proposal", candid::encode_one(arg).unwrap(), @@ -414,7 +413,7 @@ fn get_current_node_operators( ) -> Vec { let response = pic .query_call( - canister.into(), + canister, Principal::anonymous(), "get_current_nns_node_operators", candid::encode_one(()).unwrap(), diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 78753ea535a..543fa665b6f 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -121,7 +121,7 @@ fn disallow_unknown_node_operators_from_placing_proposals() { let response = pic .update_call( - canister.into(), + canister, Principal::anonymous(), "submit_new_recovery_proposal", candid::encode_one(NewRecoveryProposal { @@ -162,7 +162,7 @@ fn place_and_execute_first_proposal( ) -> (PocketIc, Principal) { let (pic, canister) = init_pocket_ic(args); - let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators = extract_node_operators_from_init_data(args); let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); @@ -398,7 +398,7 @@ fn place_and_execute_second_proposal( ) -> (PocketIc, Principal) { let (pic, canister) = place_and_execute_first_proposal(args); - let mut node_operators = extract_node_operators_from_init_data(&args); + let mut node_operators = extract_node_operators_from_init_data(args); let mut node_operators_iterator = node_operators.iter_mut(); let first = node_operators_iterator.next().unwrap(); @@ -540,15 +540,15 @@ fn submit_proposal_lag_more_than_threshold() { // Duration from epoch let from_epoch = SystemTime::UNIX_EPOCH.elapsed().unwrap(); let before_one_hour = from_epoch - .checked_sub(Duration::from_secs(1 * 60 * 60)) + .checked_sub(Duration::from_secs(60 * 60)) .unwrap(); let seconds_payload = before_one_hour.as_secs().to_le_bytes().to_vec(); let signature = first.signing_key.sign(&seconds_payload); let signature = signature.to_vec(); let response = pic.update_call( - canister.into(), - first.principal.0.clone(), + canister, + first.principal.0, "submit_new_recovery_proposal", candid::encode_one(NewRecoveryProposal { payload: RecoveryPayload::Halt, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs index 94df945ab8a..6269f253543 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/voting_tests.rs @@ -87,7 +87,7 @@ fn disallow_votes_bad_signature() { let response = vote( &pic, canister, - first.principal.0.clone(), + first.principal.0, VoteOnRecoveryProposal { ballot: Ballot::Yes, security_metadata: SecurityMetadata { @@ -133,7 +133,7 @@ fn disallow_votes_wrong_public_key() { let response = vote( &pic, canister, - first.principal.0.clone(), + first.principal.0, VoteOnRecoveryProposal { security_metadata: SecurityMetadata { payload, diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs index 688801d7279..7c4d3a69870 100644 --- a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -63,13 +63,13 @@ pub fn get_node_operators_in_nns() -> Vec { merged } -fn format_node_operators(operators: &Vec) -> String { +fn format_node_operators(operators: &[SimpleNodeOperatorRecord]) -> String { operators .iter() .map(|operator| { format!( "Principal: {}, Nodes: [{}]", - operator.operator_id.to_string(), + operator.operator_id, operator.nodes.iter().map(|n| n.to_string()).join(", ") ) }) diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 968fc6a6af3..3ddbb65370f 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -42,7 +42,10 @@ pub fn submit_recovery_proposal( } // Verify metadata integrity - new_proposal.security_metadata.validate_metadata(&caller.0).map_err(|e| e.to_string())?; + new_proposal + .security_metadata + .validate_metadata(&caller.0) + .map_err(|e| e.to_string())?; // Ensure that timestamp sent doesn't differ more than // the threshold check_secs_difference(&new_proposal.security_metadata.payload)?; @@ -55,12 +58,12 @@ pub fn submit_recovery_proposal( match &new_proposal.payload { RecoveryPayload::Halt => { proposals.push(RecoveryProposal { - proposer: caller.0.clone(), + proposer: caller.0, // TODO: Use nanoseconds submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Halt, - security_metadata: new_proposal.security_metadata.clone() + security_metadata: new_proposal.security_metadata.clone(), }); } _ => { @@ -80,7 +83,8 @@ pub fn submit_recovery_proposal( // No need to check if it is a majority no because it will be removed if it is if !first.is_byzantine_majority_yes() { - let message = format!("Can't submit a proposal until the previous is decided"); + let message = + "Can't submit a proposal until the previous is decided".to_string(); ic_cdk::println!("{}", message); return Err(message); } @@ -94,11 +98,11 @@ pub fn submit_recovery_proposal( } | RecoveryPayload::Unhalt => { proposals.push(RecoveryProposal { - proposer: caller.0.clone(), + proposer: caller.0, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: new_proposal.payload.clone(), - security_metadata: new_proposal.security_metadata.clone() + security_metadata: new_proposal.security_metadata.clone(), }); } _ => { @@ -113,12 +117,14 @@ pub fn submit_recovery_proposal( } 2 => { // There are two previous options: - // 1. Recovery - if this is previous proposal allow placing of the next only if it is voted in - // 2. Unhalt - if this is previous proposal don't allow placing new proposal + // 1. Recovery - if this is previous proposal allow placing + // of the next only if it is voted in + // 2. Unhalt - if this is previous proposal + // don't allow placing new proposal let second_proposal = proposals.get(1).expect("Must have at least two proposals"); if !second_proposal.is_byzantine_majority_yes() { let message = - format!("Can't submit a proposal until the previous is decided"); + "Can't submit a proposal until the previous is decided".to_string(); ic_cdk::println!("{}", message); return Err(message); } @@ -131,28 +137,37 @@ pub fn submit_recovery_proposal( RecoveryPayload::Unhalt, ) => { proposals.push(RecoveryProposal { - proposer: caller.0.clone(), + proposer: caller.0, submission_timestamp_seconds: now_seconds(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), payload: RecoveryPayload::Unhalt, - security_metadata: new_proposal.security_metadata.clone() + security_metadata: new_proposal.security_metadata.clone(), }); - }, + } // Allow submitting a new recovery proposal only if the current one // is voted in. This could happen if the recovery from this proposal // failed and we need to submit a new one with different args. - (RecoveryPayload::DoRecovery { height: _, state_hash: _ }, RecoveryPayload::DoRecovery { height: _, state_hash: _ }) => { + ( + RecoveryPayload::DoRecovery { + height: _, + state_hash: _, + }, + RecoveryPayload::DoRecovery { + height: _, + state_hash: _, + }, + ) => { // Remove the second_one proposals.pop(); proposals.push(RecoveryProposal { - proposer: caller.0.clone(), - submission_timestamp_seconds: now_seconds(), + proposer: caller.0, + submission_timestamp_seconds: now_seconds(), security_metadata: new_proposal.security_metadata.clone(), node_operator_ballots: initialize_ballots(&node_operators_in_nns), - payload: new_proposal.payload.clone() + payload: new_proposal.payload.clone(), }); - }, + } (_, _) => { let message = format!( "Caller {} tried to place proposal {:?} which is currently not allowed", @@ -172,21 +187,17 @@ pub fn submit_recovery_proposal( ic_cdk::println!("{}", message); return Err(message); } - _ => unreachable!( - "There is an error in the logic since its not possible to have more than 3 proposals" - ), + _ => unreachable!("not possible to have more than 3 proposals"), } Ok(()) }) } -fn initialize_ballots( - simple_node_records: &Vec, -) -> Vec { +fn initialize_ballots(simple_node_records: &[SimpleNodeOperatorRecord]) -> Vec { simple_node_records .iter() .map(|operator_record| NodeOperatorBallot { - principal: operator_record.operator_id.clone(), + principal: operator_record.operator_id, nodes_tied_to_ballot: operator_record.nodes.clone(), ballot: Ballot::Undecided, security_metadata: SecurityMetadata::empty(), @@ -208,7 +219,7 @@ fn vote_on_last_proposal( ) -> Result<(), String> { let last_proposal = proposals .last_mut() - .ok_or(format!("There are no proposals"))?; + .ok_or("There are no proposals".to_string())?; let correlated_ballot = last_proposal .node_operator_ballots @@ -250,9 +261,12 @@ const ALLOWED_LAG: u64 = 10 * 60; // 10 minutes fn check_secs_difference(seconds_payload: &[u8]) -> Result<(), String> { let now = now_seconds(); - + if seconds_payload.len() != 8 { - return Err(format!("Incorect signature lenght: {}", seconds_payload.len())); + return Err(format!( + "Incorect signature lenght: {}", + seconds_payload.len() + )); } let mut total_input = [0; 8]; @@ -263,7 +277,10 @@ fn check_secs_difference(seconds_payload: &[u8]) -> Result<(), String> { let abs_diff = now.abs_diff(payload_seconds); match abs_diff > ALLOWED_LAG { - true => Err(format!("Proposal submittion timestamp lags more than allowed {} seconds", ALLOWED_LAG)), - false => Ok(()) + true => Err(format!( + "Proposal submittion timestamp lags more than allowed {} seconds", + ALLOWED_LAG + )), + false => Ok(()), } -} \ No newline at end of file +} diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 53f6ae9e2f8..ee340c1a60c 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use candid::CandidType; use recovery::RecoveryProposal; use serde::Deserialize; @@ -67,21 +69,25 @@ impl From for RecoveryError { /// Convenience type to wrap all results in the library pub type Result = std::result::Result; -impl ToString for RecoveryError { - fn to_string(&self) -> String { - match self { - Self::InvalidPubKey(s) - | Self::InvalidSignatureFormat(s) - | Self::InvalidSignature(s) - | Self::PrincipalPublicKeyMismatch(s) - | Self::PayloadSerialization(s) - | Self::AgentError(s) - | Self::CandidError(s) - | Self::InvalidIdentity(s) - | Self::NoProposals(s) - | Self::InvalidRecoveryProposalPayload(s) - | Self::CanisterError(s) => s.to_string(), - } +impl Display for RecoveryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::InvalidPubKey(s) + | Self::InvalidSignatureFormat(s) + | Self::InvalidSignature(s) + | Self::PrincipalPublicKeyMismatch(s) + | Self::PayloadSerialization(s) + | Self::AgentError(s) + | Self::CandidError(s) + | Self::InvalidIdentity(s) + | Self::NoProposals(s) + | Self::InvalidRecoveryProposalPayload(s) + | Self::CanisterError(s) => s, + } + ) } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index 83eae24f592..6542c63a6ec 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -18,18 +18,18 @@ impl super::Signer for EdwardsCurve { "Signing key missing".to_string(), ))?; - let signature = signing_key.sign(&payload); + let signature = signing_key.sign(payload); Ok(signature.to_vec()) } } impl super::Verifier for EdwardsCurve { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { - let signature = Signature::from_slice(&signature) + let signature = Signature::from_slice(signature) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; self.verifying_key - .verify_strict(&payload, &signature) + .verify_strict(payload, &signature) .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) } diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs index 4ca0b639c59..8d26da3cdc8 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/k256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -16,7 +16,7 @@ impl super::Verifier for Secp256k1 { .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; self.verifying_key - .verify(&payload, &signature) + .verify(payload, &signature) .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) } @@ -38,12 +38,12 @@ impl super::Signer for Secp256k1 { ))?; let signature: Signature = signing_key - .try_sign(&payload) + .try_sign(payload) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; let r = signature.r().to_bytes().to_vec(); let s = signature.s().to_bytes().to_vec(); - Ok(r.into_iter().chain(s.into_iter()).collect()) + Ok(r.into_iter().chain(s).collect()) } } @@ -68,7 +68,7 @@ impl Secp256k1 { pub fn new(signing_key: SigningKey) -> Self { Self { - verifying_key: signing_key.verifying_key().clone(), + verifying_key: *signing_key.verifying_key(), signing_key: Some(signing_key), } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index 6f90c72657c..d167027141d 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -20,19 +20,16 @@ pub trait Signer: Verifier + Send + Sync { } pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[u8]) -> Result<()> { - match EdwardsCurve::from_public_key_der(public_key_der) { - Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), - _ => {} + if let Ok(verifier) = EdwardsCurve::from_public_key_der(public_key_der) { + return verify_payload_naive_inner(verifier, payload, signature); } - match Prime256::from_public_key_der(public_key_der) { - Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), - _ => {} + if let Ok(verifier) = Prime256::from_public_key_der(public_key_der) { + return verify_payload_naive_inner(verifier, payload, signature); } - match Secp256k1::from_public_key_der(public_key_der) { - Ok(verifier) => return verify_payload_naive_inner(verifier, payload, signature), - _ => {} + if let Ok(verifier) = Secp256k1::from_public_key_der(public_key_der) { + return verify_payload_naive_inner(verifier, payload, signature); } Err(RecoveryError::InvalidPubKey( diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index 29af1cdaff5..fa665e9728d 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -19,18 +19,18 @@ impl super::Signer for Prime256 { ))?; let signature: Signature = signing_key - .try_sign(&payload) + .try_sign(payload) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; let r = signature.r().to_bytes().to_vec(); let s = signature.s().to_bytes().to_vec(); - Ok(r.into_iter().chain(s.into_iter()).collect()) + Ok(r.into_iter().chain(s).collect()) } } impl super::Verifier for Prime256 { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { - let signature = Signature::from_slice(&signature) + let signature = Signature::from_slice(signature) .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; self.verifying_key @@ -67,7 +67,7 @@ impl Prime256 { pub fn new(signing_key: SigningKey) -> Self { Self { - verifying_key: signing_key.verifying_key().clone(), + verifying_key: *signing_key.verifying_key(), signing_key: Some(signing_key), } } From ba85116ad9279131a1edb7119e1155cdf096cbf0 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Fri, 7 Feb 2025 19:41:10 +0100 Subject: [PATCH 57/76] locking --- Cargo.Bazel.Fuzzing.json.lock | 7 ++++++- Cargo.Bazel.Fuzzing.toml.lock | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.Bazel.Fuzzing.json.lock b/Cargo.Bazel.Fuzzing.json.lock index 8fb4c7dadd3..5fe7c0e9081 100644 --- a/Cargo.Bazel.Fuzzing.json.lock +++ b/Cargo.Bazel.Fuzzing.json.lock @@ -1,5 +1,5 @@ { - "checksum": "4ae7b3c97aa8a5afd939403aaa5919e419cc497d77a0fef55b643a99ef0c998b", + "checksum": "68ccb790390da618288579f1fed367dcc7239797ac7fde0b5e06e1e051a14a8a", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -20553,6 +20553,10 @@ "id": "socket2 0.5.7", "target": "socket2" }, + { + "id": "spki 0.7.3", + "target": "spki" + }, { "id": "ssh2 0.9.4", "target": "ssh2" @@ -91581,6 +91585,7 @@ "slog-scope 4.4.0", "slog-term 2.9.1", "socket2 0.5.7", + "spki 0.7.3", "ssh2 0.9.4", "static_assertions 1.1.0", "strum 0.26.3", diff --git a/Cargo.Bazel.Fuzzing.toml.lock b/Cargo.Bazel.Fuzzing.toml.lock index bcdc0c24d2f..dceb261db0f 100644 --- a/Cargo.Bazel.Fuzzing.toml.lock +++ b/Cargo.Bazel.Fuzzing.toml.lock @@ -3451,6 +3451,7 @@ dependencies = [ "slog-scope", "slog-term", "socket2 0.5.7", + "spki 0.7.3", "ssh2", "static_assertions", "strum 0.26.3", From 3c49f180fae8a73ab97b33348e8863e408018e7d Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sat, 8 Feb 2025 16:32:28 +0100 Subject: [PATCH 58/76] refactoring printing --- .../handlers/recovery/impl/canister/canister.rs | 10 ++++------ rs/nns/handlers/recovery/impl/src/lib.rs | 9 +++++++++ .../recovery/impl/src/node_operator_sync.rs | 17 +++++++++++------ .../recovery/impl/src/recovery_proposal.rs | 17 ++++++++--------- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index 00592510a2c..a5da1fe0ac6 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -3,15 +3,13 @@ use ic_canisters_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; use ic_cdk_macros::init; use ic_nervous_system_common::serve_metrics; -#[cfg(target_arch = "wasm32")] -use ic_cdk::println; - use ic_cdk::{post_upgrade, query, update}; use ic_nns_handler_recovery::{ metrics::encode_metrics, node_operator_sync::{ get_node_operators_in_nns, set_initial_node_operators, sync_node_operators, }, + print_with_prefix, recovery_proposal::{get_recovery_proposals, submit_recovery_proposal, vote_on_proposal_inner}, }; use ic_nns_handler_recovery_interface::{ @@ -98,11 +96,11 @@ async fn setup_node_operator_update(args: Option) { set_initial_node_operators(args.initial_node_operator_records); } - ic_cdk::println!("Started Sync for new node operators on NNS"); + print_with_prefix("Started Sync for new node operators on NNS"); if let Err(e) = sync_node_operators().await { - ic_cdk::println!("{}", e); + print_with_prefix(e); } - ic_cdk::println!("Sync completed") + print_with_prefix("Sync completed"); } #[cfg(test)] diff --git a/rs/nns/handlers/recovery/impl/src/lib.rs b/rs/nns/handlers/recovery/impl/src/lib.rs index 899439f4963..9e5d7f694ca 100644 --- a/rs/nns/handlers/recovery/impl/src/lib.rs +++ b/rs/nns/handlers/recovery/impl/src/lib.rs @@ -1,3 +1,12 @@ pub mod metrics; pub mod node_operator_sync; pub mod recovery_proposal; + +#[cfg(target_arch = "wasm32")] +use ic_cdk::println; + +const PREFIX: &str = "[Recovery canister] "; +#[inline] +pub fn print_with_prefix>(message: S) { + println!("{}{}", PREFIX, message.as_ref()) +} diff --git a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs index 7c4d3a69870..3149a8eceab 100644 --- a/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs +++ b/rs/nns/handlers/recovery/impl/src/node_operator_sync.rs @@ -7,6 +7,8 @@ use ic_nns_handler_root::root_proposals::{ }; use itertools::Itertools; +use crate::print_with_prefix; + thread_local! { static NODE_OPERATORS_IN_NNS: RefCell> = const { RefCell::new(Vec::new()) }; static INITIAL_NODE_OPERATORS: RefCell> = const { RefCell::new(Vec::new()) }; @@ -30,16 +32,24 @@ pub async fn sync_node_operators() -> Result<(), String> { } } - let new_simple_records = new_simple_records + let new_simple_records: Vec<_> = new_simple_records .into_iter() .map(|(operator_id, nodes)| SimpleNodeOperatorRecord { operator_id, nodes }) .collect(); + print_with_prefix(format!( + "Updating node operators in nns with: {}", + format_node_operators(&new_simple_records) + )); NODE_OPERATORS_IN_NNS.replace(new_simple_records); Ok(()) } pub fn set_initial_node_operators(initial: Vec) { + print_with_prefix(format!( + "Initial records: {}", + format_node_operators(&initial) + )); INITIAL_NODE_OPERATORS.replace(initial); } @@ -55,11 +65,6 @@ pub fn get_node_operators_in_nns() -> Vec { merged.sort_by(|a, b| b.nodes.len().cmp(&a.nodes.len())); merged.dedup_by(|a, b| a.operator_id == b.operator_id); - if !initial.is_empty() { - ic_cdk::println!("Initial: {}", format_node_operators(&initial)); - ic_cdk::println!("Obtained: {}", format_node_operators(&obtained_from_sync)); - } - ic_cdk::println!("Merged: {}", format_node_operators(&merged)); merged } diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 3ddbb65370f..9a2f8109de2 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -12,7 +12,7 @@ use ic_nns_handler_recovery_interface::{ }; use ic_nns_handler_root::now_seconds; -use crate::node_operator_sync::get_node_operators_in_nns; +use crate::{node_operator_sync::get_node_operators_in_nns, print_with_prefix}; thread_local! { static PROPOSALS: RefCell> = const { RefCell::new(Vec::new()) }; @@ -37,7 +37,7 @@ pub fn submit_recovery_proposal( "Caller: {} is not eligible to submit proposals to this canister", caller ); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } @@ -71,7 +71,7 @@ pub fn submit_recovery_proposal( "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal ); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } } @@ -85,7 +85,7 @@ pub fn submit_recovery_proposal( if !first.is_byzantine_majority_yes() { let message = "Can't submit a proposal until the previous is decided".to_string(); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } @@ -110,7 +110,7 @@ pub fn submit_recovery_proposal( "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal ); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } } @@ -125,7 +125,7 @@ pub fn submit_recovery_proposal( if !second_proposal.is_byzantine_majority_yes() { let message = "Can't submit a proposal until the previous is decided".to_string(); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } match (&second_proposal.payload, &new_proposal.payload) { @@ -173,7 +173,7 @@ pub fn submit_recovery_proposal( "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal ); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } } @@ -184,7 +184,7 @@ pub fn submit_recovery_proposal( "Caller {} tried to place proposal {:?} which is currently not allowed", caller, new_proposal ); - ic_cdk::println!("{}", message); + print_with_prefix(&message); return Err(message); } _ => unreachable!("not possible to have more than 3 proposals"), @@ -273,7 +273,6 @@ fn check_secs_difference(seconds_payload: &[u8]) -> Result<(), String> { total_input.copy_from_slice(seconds_payload); let payload_seconds = u64::from_le_bytes(total_input); - ic_cdk::println!("NOW {}, Sent {}", now, payload_seconds); let abs_diff = now.abs_diff(payload_seconds); match abs_diff > ALLOWED_LAG { From 31bc79d79a86278f47bfef97d5cb487fe32f259a Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sat, 8 Feb 2025 18:39:42 +0100 Subject: [PATCH 59/76] adding anonymous identity --- Cargo.lock | 2 +- rs/nns/handlers/recovery/client/Cargo.toml | 1 + rs/nns/handlers/recovery/impl/Cargo.toml | 2 +- .../impl/canister/tests/initial_args_test.rs | 2 ++ rs/nns/handlers/recovery/interface/src/lib.rs | 2 ++ .../interface/src/signing/anonymous.rs | 19 +++++++++++++++++++ .../recovery/interface/src/signing/mod.rs | 1 + 7 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/signing/anonymous.rs diff --git a/Cargo.lock b/Cargo.lock index f1ec17a3cf7..bcd70b4d4e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10800,7 +10800,7 @@ dependencies = [ "ic-cdk-macros 0.9.0", "ic-cdk-timers", "ic-crypto-sha2", - "ic-management-canister-types", + "ic-management-canister-types-private", "ic-metrics-encoder", "ic-nervous-system-clients", "ic-nervous-system-common", diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index d8f4d7d2173..d3d0445cd30 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -6,6 +6,7 @@ description.workspace = true documentation.workspace = true edition.workspace = true + [dependencies] candid = { workspace = true } serde = { workspace = true } diff --git a/rs/nns/handlers/recovery/impl/Cargo.toml b/rs/nns/handlers/recovery/impl/Cargo.toml index 590de56d219..4455243ce92 100644 --- a/rs/nns/handlers/recovery/impl/Cargo.toml +++ b/rs/nns/handlers/recovery/impl/Cargo.toml @@ -23,7 +23,7 @@ ic-cdk = { workspace = true } ic-cdk-macros = { workspace = true } ic-cdk-timers = { workspace = true } ic-crypto-sha2 = { path = "../../../../crypto/sha2" } -ic-management-canister-types = { path = "../../../../types/management_canister_types" } +ic-management-canister-types-private = { path = "../../../../types/management_canister_types" } ic-metrics-encoder = "1" ic-nervous-system-clients = { path = "../../../../nervous_system/clients" } ic-nervous-system-common = { path = "../../../../nervous_system/common" } diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index 93faab34c91..fdc84038057 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use candid::Principal; use ic_base_types::PrincipalId; use ic_nns_handler_recovery_interface::{ diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index ee340c1a60c..5beba000400 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -4,6 +4,8 @@ use candid::CandidType; use recovery::RecoveryProposal; use serde::Deserialize; +pub const RECOVERY_CANISTER_ID: &str = "23bh6-6iaaa-aaaam-aednq-cai"; + pub mod recovery; pub mod recovery_init; pub mod security_metadata; diff --git a/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs b/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs new file mode 100644 index 00000000000..9fced576217 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs @@ -0,0 +1,19 @@ +use super::{Signer, Verifier}; + +pub struct AnonymousSigner; + +impl Verifier for AnonymousSigner { + fn verify_payload(&self, _: &[u8], _: &[u8]) -> crate::Result<()> { + Ok(()) + } + + fn to_public_key_der(&self) -> crate::Result> { + Ok(vec![]) + } +} + +impl Signer for AnonymousSigner { + fn sign_payload(&self, _: &[u8]) -> crate::Result> { + Ok(vec![]) + } +} diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index d167027141d..d85d5c8bdc0 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -6,6 +6,7 @@ use crate::RecoveryError; use super::Result; +pub mod anonymous; pub mod ed25519; pub mod k256; pub mod p256; From 1f68c516bd774e240263214e49ef2ef6d4bad6ff Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sat, 8 Feb 2025 22:38:48 +0100 Subject: [PATCH 60/76] adding hsm signing methods --- Cargo.Bazel.json.lock | 7 +- Cargo.Bazel.toml.lock | 1 + Cargo.lock | 1 + Cargo.toml | 1 + bazel/external_crates.bzl | 3 + rs/nns/handlers/recovery/client/BUILD.bazel | 1 + rs/nns/handlers/recovery/client/Cargo.toml | 1 + .../recovery/client/src/tests/general.rs | 16 +++- rs/nns/handlers/recovery/interface/src/lib.rs | 2 + .../recovery/interface/src/signing/ed25519.rs | 23 +++++- .../recovery/interface/src/signing/hsm.rs | 74 +++++++++++++++++++ .../recovery/interface/src/signing/k256.rs | 10 +++ .../recovery/interface/src/signing/mod.rs | 9 ++- .../recovery/interface/src/signing/p256.rs | 10 +++ 14 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/signing/hsm.rs diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index fc498f47d3b..90a69f8432a 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "7a42e7625fdea0f5801a145662281da1633fc4609c2a92540ad8ea2bdc1e29b0", + "checksum": "db92f1b23dc7421270036e33322b78b5201037b7c480b910ca77c74f414f5a6f", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -19795,6 +19795,10 @@ "id": "ic-http-gateway 0.1.0", "target": "ic_http_gateway" }, + { + "id": "ic-identity-hsm 0.39.3", + "target": "ic_identity_hsm" + }, { "id": "ic-metrics-encoder 1.1.1", "target": "ic_metrics_encoder" @@ -91346,6 +91350,7 @@ "ic-certified-map 0.3.4", "ic-http-certification 3.0.2", "ic-http-gateway 0.1.0", + "ic-identity-hsm 0.39.3", "ic-metrics-encoder 1.1.1", "ic-response-verification 3.0.2", "ic-sha3 1.0.0", diff --git a/Cargo.Bazel.toml.lock b/Cargo.Bazel.toml.lock index 58b21863772..4d80d8c4b51 100644 --- a/Cargo.Bazel.toml.lock +++ b/Cargo.Bazel.toml.lock @@ -3288,6 +3288,7 @@ dependencies = [ "ic-certified-map", "ic-http-certification", "ic-http-gateway", + "ic-identity-hsm", "ic-metrics-encoder", "ic-response-verification", "ic-sha3", diff --git a/Cargo.lock b/Cargo.lock index bcd70b4d4e6..d02759f233a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10846,6 +10846,7 @@ dependencies = [ "candid", "ed25519-dalek", "ic-agent", + "ic-identity-hsm", "ic-nns-handler-recovery-interface", "k256 0.13.4", "p256", diff --git a/Cargo.toml b/Cargo.toml index 727223a7cf3..f232cfcd9c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -564,6 +564,7 @@ hyper-rustls = { version = "0.27.3", default-features = false, features = [ hyper-socks2 = { version = "0.9.1", default-features = false } hyper-util = { version = "0.1.10", features = ["full"] } ic-agent = { version = "0.39.2", features = ["pem", "ring"] } +ic-identity-hsm = "0.39.2" ic-bn-lib = { git = "https://github.com/dfinity/ic-bn-lib", rev = "d74a6527fbaf8a2c1a7076983cc84f5c5a727923" } ic-btc-interface = "0.2.2" ic-canister-sig-creation = { git = "https://github.com/dfinity/ic-canister-sig-creation", rev = "7f9e931954637526295269155881207f6c832d6d" } diff --git a/bazel/external_crates.bzl b/bazel/external_crates.bzl index 57b36ce0e54..2d0b52a1fc6 100644 --- a/bazel/external_crates.bzl +++ b/bazel/external_crates.bzl @@ -576,6 +576,9 @@ def external_crates_repository(name, cargo_lockfile, lockfile, sanitizers_enable version = "^0.39.2", features = ["pem", "ring"], ), + "ic-identity-hsm": crate.spec( + version = "^0.39.2" + ), "ic-bn-lib": crate.spec( git = "https://github.com/dfinity/ic-bn-lib", rev = "d74a6527fbaf8a2c1a7076983cc84f5c5a727923", diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 35ad80c3e91..15d3f2de3f0 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -8,6 +8,7 @@ DEPENDENCIES = [ "@crate_index//:candid", "@crate_index//:serde", "@crate_index//:ic-agent", + "@crate_index//:ic-identity-hsm", ] MACRO_DEPENDENCIES = [ diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index d3d0445cd30..7cfe4876645 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -13,6 +13,7 @@ serde = { workspace = true } ic-agent.workspace = true async-trait.workspace = true ic-nns-handler-recovery-interface.path = "../interface" +ic-identity-hsm.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index cb83702efd6..21c764f8a70 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -2,12 +2,22 @@ use std::sync::Arc; use ed25519_dalek::SigningKey as EdSigningKey; use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; +use ic_identity_hsm::HardwareIdentity; use ic_nns_handler_recovery_interface::{ recovery::RecoveryPayload, - signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Verifier}, - Ballot, + signing::{ + ed25519::EdwardsCurve, + hsm::{Hsm, SignBytes}, + k256::Secp256k1, + p256::Prime256, + Verifier, + }, + Ballot, RecoveryError, +}; +use k256::ecdsa::SigningKey as SecpSigningKey; +use k256::{ + ecdsa::signature::SignerMut, ecdsa::Signature as k256Signature, SecretKey as k256SecretKey, }; -use k256::SecretKey as k256SecretKey; use p256::{elliptic_curve::rand_core::OsRng, SecretKey as p256SecretKey}; use crate::{ diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 5beba000400..6ddc649815e 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -93,6 +93,8 @@ impl Display for RecoveryError { } } +impl std::error::Error for RecoveryError {} + pub trait VerifyIntegirty { fn verify_integrity(&self) -> Result<()>; } diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index 6542c63a6ec..bb596d74981 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -1,4 +1,6 @@ -use ed25519_dalek::{pkcs8::EncodePublicKey, Signature, Signer, SigningKey, VerifyingKey}; +use ed25519_dalek::{ + pkcs8::EncodePublicKey, Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, +}; use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; use crate::RecoveryError; @@ -60,6 +62,25 @@ impl EdwardsCurve { }) } + pub fn from_public_key(public_key: &[u8]) -> crate::Result { + if public_key.len() != PUBLIC_KEY_LENGTH { + return Err(RecoveryError::InvalidPubKey( + "Invalid public key length".to_string(), + )); + } + + let mut pub_key = [0; 32]; + pub_key.copy_from_slice(public_key); + + let verifying_key = VerifyingKey::from_bytes(&pub_key) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } + pub fn new(signing_key: SigningKey) -> Self { Self { verifying_key: signing_key.verifying_key(), diff --git a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs new file mode 100644 index 00000000000..b2c17fac157 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use crate::RecoveryError; + +use super::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Signer, Verifier}; + +// From ic-admin +pub type SignBytes = + Arc Result, Box> + Send + Sync>; + +pub struct Hsm { + inner_pub_key: Arc, + pub_key: Vec, + sign_func: Option, +} + +impl Verifier for Hsm { + fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { + self.inner_pub_key.verify_payload(payload, signature) + } + + fn to_public_key_der(&self) -> crate::Result> { + Ok(self.pub_key.clone()) + } +} + +impl Signer for Hsm { + fn sign_payload(&self, payload: &[u8]) -> crate::Result> { + if let Some(sign_func) = &self.sign_func { + sign_func(payload).map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } else { + Err(RecoveryError::InvalidIdentity( + "Missing sing func".to_string(), + )) + } + } +} + +impl Hsm { + pub fn from_public_key(pub_key: &[u8]) -> crate::Result { + if let Ok(edwards) = EdwardsCurve::from_public_key(pub_key) { + return Ok(Self::from_verifier(pub_key, Arc::new(edwards))); + } + + if let Ok(prime256) = Prime256::from_public_key(pub_key) { + return Ok(Self::from_verifier(pub_key, Arc::new(prime256))); + } + + if let Ok(secp256) = Secp256k1::from_public_key(pub_key) { + return Ok(Self::from_verifier(pub_key, Arc::new(secp256))); + } + + return Err(RecoveryError::InvalidPubKey( + "Key stored on hsm implements an unknown algorithm".to_string(), + )); + } + + fn from_verifier(pub_key: &[u8], verifier: Arc) -> Self { + Self { + sign_func: None, + pub_key: pub_key.to_vec(), + inner_pub_key: verifier, + } + } + + pub fn new(pub_key: &[u8], sign_func: SignBytes) -> crate::Result { + let from_pub_key = Self::from_public_key(pub_key)?; + + Ok(Self { + sign_func: Some(sign_func), + ..from_pub_key + }) + } +} diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs index 8d26da3cdc8..9b3786e4c6f 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/k256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -66,6 +66,16 @@ impl Secp256k1 { }) } + pub fn from_public_key(public_key: &[u8]) -> crate::Result { + let verifying_key = VerifyingKey::from_sec1_bytes(public_key) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } + pub fn new(signing_key: SigningKey) -> Self { Self { verifying_key: *signing_key.verifying_key(), diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index d85d5c8bdc0..7d75399dac3 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -1,4 +1,5 @@ use ed25519::EdwardsCurve; +use hsm::Hsm; use k256::Secp256k1; use p256::Prime256; @@ -8,10 +9,11 @@ use super::Result; pub mod anonymous; pub mod ed25519; +pub mod hsm; pub mod k256; pub mod p256; -pub trait Verifier { +pub trait Verifier: Send + Sync { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> Result<()>; fn to_public_key_der(&self) -> Result>; @@ -33,6 +35,11 @@ pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[ return verify_payload_naive_inner(verifier, payload, signature); } + // Assume it's an hsm which isn't der encoded + if let Ok(verifier) = Hsm::from_public_key(public_key_der) { + return verify_payload_naive_inner(verifier, payload, signature); + } + Err(RecoveryError::InvalidPubKey( "Couldn't decode public key der with any known algorithm".to_string(), )) diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index fa665e9728d..62d01bb4cfc 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -65,6 +65,16 @@ impl Prime256 { }) } + pub fn from_public_key(public_key: &[u8]) -> crate::Result { + let verifying_key = VerifyingKey::from_sec1_bytes(public_key) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } + pub fn new(signing_key: SigningKey) -> Self { Self { verifying_key: *signing_key.verifying_key(), From 0aedaab7da1b86fcf1240affa2a3bc21c74f4b35 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sat, 8 Feb 2025 23:44:39 +0100 Subject: [PATCH 61/76] implementing builder for the canister client --- rs/nns/handlers/recovery/client/BUILD.bazel | 6 +- rs/nns/handlers/recovery/client/Cargo.toml | 6 +- .../handlers/recovery/client/src/builder.rs | 154 ++++++++++++++++++ rs/nns/handlers/recovery/client/src/lib.rs | 1 + .../recovery/interface/src/signing/ed25519.rs | 8 + .../recovery/interface/src/signing/k256.rs | 9 +- .../recovery/interface/src/signing/p256.rs | 8 + 7 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 rs/nns/handlers/recovery/client/src/builder.rs diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 15d3f2de3f0..03c388201c0 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -9,6 +9,9 @@ DEPENDENCIES = [ "@crate_index//:serde", "@crate_index//:ic-agent", "@crate_index//:ic-identity-hsm", + "@crate_index//:p256", + "@crate_index//:ed25519-dalek", + "@crate_index//:k256", ] MACRO_DEPENDENCIES = [ @@ -18,9 +21,6 @@ MACRO_DEPENDENCIES = [ DEV_DEPENDENCIES = [ "//packages/pocket-ic", "@crate_index//:tokio", - "@crate_index//:p256", - "@crate_index//:ed25519-dalek", - "@crate_index//:k256", ] MACRO_DEV_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 7cfe4876645..ea6e296f255 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -14,10 +14,10 @@ ic-agent.workspace = true async-trait.workspace = true ic-nns-handler-recovery-interface.path = "../interface" ic-identity-hsm.workspace = true - -[dev-dependencies] -tokio.workspace = true p256.workspace = true k256.workspace = true ed25519-dalek.workspace = true + +[dev-dependencies] +tokio.workspace = true pocket-ic.path = "../../../../../packages/pocket-ic" diff --git a/rs/nns/handlers/recovery/client/src/builder.rs b/rs/nns/handlers/recovery/client/src/builder.rs new file mode 100644 index 00000000000..6ed938415cb --- /dev/null +++ b/rs/nns/handlers/recovery/client/src/builder.rs @@ -0,0 +1,154 @@ +use std::sync::Arc; + +use candid::Principal; +use ic_agent::{ + agent::AgentBuilder, + identity::{AnonymousIdentity, BasicIdentity, PemError, Prime256v1Identity, Secp256k1Identity}, + Agent, Identity, +}; +use ic_identity_hsm::HardwareIdentity; +use ic_nns_handler_recovery_interface::{ + signing::{ + anonymous::AnonymousSigner, ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Signer, + }, + RecoveryError, RECOVERY_CANISTER_ID, +}; + +use crate::implementation::RecoveryCanisterImpl; + +#[derive(Clone)] +pub enum SenderOpts { + Pem { + path: String, + }, + Hsm { + slot: usize, + key_id: String, + pin: String, + }, + Anonymous, +} + +pub struct RecoveryCanisterBuilder { + canister_id: String, + url: String, + sender: SenderOpts, +} + +impl Default for RecoveryCanisterBuilder { + fn default() -> Self { + Self { + canister_id: RECOVERY_CANISTER_ID.to_string(), + url: "https://ic0.app".to_string(), + sender: SenderOpts::Anonymous, + } + } +} + +impl RecoveryCanisterBuilder { + pub fn with_url(self, url: &str) -> Self { + Self { + url: url.to_string(), + ..self + } + } + + pub fn with_canister_id(self, canister_id: &str) -> Self { + Self { + canister_id: canister_id.to_string(), + ..self + } + } + + pub fn with_sender(self, sender: SenderOpts) -> Self { + Self { sender, ..self } + } + + pub fn build(self) -> Result { + let signer = self.sender.clone().try_into()?; + let identity: Box = self.sender.try_into()?; + let ic_agent = AgentBuilder::default() + .with_url(self.url) + .with_identity(identity) + .build() + .map_err(|e| RecoveryError::AgentError(e.to_string()))?; + + let canister_id = Principal::from_text(self.canister_id) + .map_err(|e| RecoveryError::AgentError(e.to_string()))?; + Ok(RecoveryCanisterImpl::new(ic_agent, canister_id, signer)) + } +} + +impl TryFrom for Arc { + type Error = RecoveryError; + + fn try_from(value: SenderOpts) -> Result { + match value { + SenderOpts::Pem { path } => { + let maybe_signer: Result, RecoveryError> = + EdwardsCurve::from_pem(&path) + .map(|signer| Arc::new(signer) as Arc) + .or_else(|_| { + Prime256::from_pem(&path) + .map(|signer| Arc::new(signer) as Arc) + }) + .or_else(|_| { + Secp256k1::from_pem(&path) + .map(|signer| Arc::new(signer) as Arc) + }); + + let signer = maybe_signer.map_err(|_| { + RecoveryError::InvalidIdentity( + "Couldn't deserialize identity into any known implementation".to_string(), + ) + })?; + Ok(signer) + } + SenderOpts::Hsm { slot, key_id, pin } => unimplemented!("Ic agent blocks the session"), + SenderOpts::Anonymous => Ok(Arc::new(AnonymousSigner)), + } + } +} + +impl TryFrom for Box { + type Error = RecoveryError; + + fn try_from(value: SenderOpts) -> Result { + match value { + SenderOpts::Pem { path } => { + let maybe_identity: Result, PemError> = + BasicIdentity::from_pem_file(&path) + .map(|identity| Box::new(identity) as Box) + .or_else(|_| { + Prime256v1Identity::from_pem_file(&path) + .map(|identity| Box::new(identity) as Box) + }) + .or_else(|_| { + Secp256k1Identity::from_pem_file(&path) + .map(|identity| Box::new(identity) as Box) + }); + + let identity = + maybe_identity.map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + + Ok(identity) + } + SenderOpts::Hsm { slot, key_id, pin } => { + HardwareIdentity::new("/usr/lib/opensc-pkcs11.so", slot, &key_id, || Ok(pin)) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string())) + .map(|identity| Box::new(identity) as Box) + } + SenderOpts::Anonymous => Ok(Box::new(AnonymousIdentity)), + } + } +} + +fn resolve_signer(sender: &SenderOpts) -> Result, RecoveryError> { + Ok(Arc::new(AnonymousSigner {})) +} + +fn resolve_ic_agent(url: &str, sender: &SenderOpts) -> Result { + AgentBuilder::default() + .build() + .map_err(|e| RecoveryError::AgentError(e.to_string())) +} diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index ce131dd4a6f..53763ea864e 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -6,6 +6,7 @@ use ic_nns_handler_recovery_interface::{ }; use ic_nns_handler_recovery_interface::{RecoveryError, Result}; +pub mod builder; pub mod implementation; #[cfg(test)] mod tests; diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index bb596d74981..44cddc62a41 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -1,6 +1,7 @@ use ed25519_dalek::{ pkcs8::EncodePublicKey, Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, }; +use k256::pkcs8::DecodePrivateKey; use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; use crate::RecoveryError; @@ -87,4 +88,11 @@ impl EdwardsCurve { signing_key: Some(signing_key), } } + + pub fn from_pem(path: &str) -> crate::Result { + let signing_key = SigningKey::from_pkcs8_pem(path) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + + Ok(Self::new(signing_key)) + } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs index 9b3786e4c6f..7ce0e3aca80 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/k256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -1,6 +1,6 @@ use k256::ecdsa::signature::{Signer, Verifier}; use k256::ecdsa::{Signature, SigningKey, VerifyingKey}; -use k256::pkcs8::{Document, EncodePublicKey}; +use k256::pkcs8::{DecodePrivateKey, Document, EncodePublicKey}; use spki::{DecodePublicKey, SubjectPublicKeyInfoRef}; use crate::RecoveryError; @@ -82,4 +82,11 @@ impl Secp256k1 { signing_key: Some(signing_key), } } + + pub fn from_pem(path: &str) -> crate::Result { + let signing_key = SigningKey::from_pkcs8_pem(path) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + + Ok(Self::new(signing_key)) + } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index 62d01bb4cfc..5573635eef5 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -1,3 +1,4 @@ +use k256::pkcs8::DecodePrivateKey; use p256::ecdsa::{signature::SignerMut, signature::Verifier, Signature, SigningKey, VerifyingKey}; use p256::pkcs8::EncodePublicKey; use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; @@ -81,4 +82,11 @@ impl Prime256 { signing_key: Some(signing_key), } } + + pub fn from_pem(path: &str) -> crate::Result { + let signing_key = SigningKey::from_pkcs8_pem(path) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + + Ok(Self::new(signing_key)) + } } From d7f87037cf30b23478d01167e2d5babae867c025 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 00:20:22 +0100 Subject: [PATCH 62/76] adding query functionality to ic-admin --- Cargo.lock | 1 + .../handlers/recovery/client/src/builder.rs | 39 ++++++-------- .../recovery/client/src/tests/general.rs | 16 ++---- rs/nns/handlers/recovery/interface/src/lib.rs | 4 +- .../recovery/interface/src/recovery.rs | 8 +-- .../interface/src/security_metadata.rs | 4 +- rs/registry/admin/BUILD.bazel | 1 + rs/registry/admin/Cargo.toml | 1 + rs/registry/admin/src/main.rs | 18 +++++-- rs/registry/admin/src/recovery_canister.rs | 54 +++++++++++++++++++ 10 files changed, 97 insertions(+), 49 deletions(-) create mode 100644 rs/registry/admin/src/recovery_canister.rs diff --git a/Cargo.lock b/Cargo.lock index d02759f233a..50776725b81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5671,6 +5671,7 @@ dependencies = [ "ic-nns-common", "ic-nns-constants", "ic-nns-governance-api", + "ic-nns-handler-recovery-client", "ic-nns-handler-root", "ic-nns-init", "ic-nns-test-utils", diff --git a/rs/nns/handlers/recovery/client/src/builder.rs b/rs/nns/handlers/recovery/client/src/builder.rs index 6ed938415cb..2933dfea2de 100644 --- a/rs/nns/handlers/recovery/client/src/builder.rs +++ b/rs/nns/handlers/recovery/client/src/builder.rs @@ -4,7 +4,7 @@ use candid::Principal; use ic_agent::{ agent::AgentBuilder, identity::{AnonymousIdentity, BasicIdentity, PemError, Prime256v1Identity, Secp256k1Identity}, - Agent, Identity, + Identity, }; use ic_identity_hsm::HardwareIdentity; use ic_nns_handler_recovery_interface::{ @@ -46,22 +46,19 @@ impl Default for RecoveryCanisterBuilder { } impl RecoveryCanisterBuilder { - pub fn with_url(self, url: &str) -> Self { - Self { - url: url.to_string(), - ..self - } + pub fn with_url(&mut self, url: &str) -> &mut Self { + self.url = url.to_string(); + self } - pub fn with_canister_id(self, canister_id: &str) -> Self { - Self { - canister_id: canister_id.to_string(), - ..self - } + pub fn with_canister_id(&mut self, canister_id: &str) -> &mut Self { + self.canister_id = canister_id.to_string(); + self } - pub fn with_sender(self, sender: SenderOpts) -> Self { - Self { sender, ..self } + pub fn with_sender(&mut self, sender: SenderOpts) -> &mut Self { + self.sender = sender; + self } pub fn build(self) -> Result { @@ -104,7 +101,11 @@ impl TryFrom for Arc { })?; Ok(signer) } - SenderOpts::Hsm { slot, key_id, pin } => unimplemented!("Ic agent blocks the session"), + SenderOpts::Hsm { + slot: _, + key_id: _, + pin: _, + } => unimplemented!("Ic agent blocks the session"), SenderOpts::Anonymous => Ok(Arc::new(AnonymousSigner)), } } @@ -142,13 +143,3 @@ impl TryFrom for Box { } } } - -fn resolve_signer(sender: &SenderOpts) -> Result, RecoveryError> { - Ok(Arc::new(AnonymousSigner {})) -} - -fn resolve_ic_agent(url: &str, sender: &SenderOpts) -> Result { - AgentBuilder::default() - .build() - .map_err(|e| RecoveryError::AgentError(e.to_string())) -} diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 21c764f8a70..cb83702efd6 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -2,22 +2,12 @@ use std::sync::Arc; use ed25519_dalek::SigningKey as EdSigningKey; use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; -use ic_identity_hsm::HardwareIdentity; use ic_nns_handler_recovery_interface::{ recovery::RecoveryPayload, - signing::{ - ed25519::EdwardsCurve, - hsm::{Hsm, SignBytes}, - k256::Secp256k1, - p256::Prime256, - Verifier, - }, - Ballot, RecoveryError, -}; -use k256::ecdsa::SigningKey as SecpSigningKey; -use k256::{ - ecdsa::signature::SignerMut, ecdsa::Signature as k256Signature, SecretKey as k256SecretKey, + signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Verifier}, + Ballot, }; +use k256::SecretKey as k256SecretKey; use p256::{elliptic_curve::rand_core::OsRng, SecretKey as p256SecretKey}; use crate::{ diff --git a/rs/nns/handlers/recovery/interface/src/lib.rs b/rs/nns/handlers/recovery/interface/src/lib.rs index 6ddc649815e..dc2f6ace266 100644 --- a/rs/nns/handlers/recovery/interface/src/lib.rs +++ b/rs/nns/handlers/recovery/interface/src/lib.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use candid::CandidType; use recovery::RecoveryProposal; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; pub const RECOVERY_CANISTER_ID: &str = "23bh6-6iaaa-aaaam-aednq-cai"; @@ -12,7 +12,7 @@ pub mod security_metadata; pub mod signing; pub mod simple_node_operator_record; -#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq, Serialize)] /// Vote types that exist pub enum Ballot { /// Represents a positive vote on a recovery canister proposal diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index f023a362125..a1f6ddf6c4a 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -6,11 +6,11 @@ use ic_base_types::{PrincipalId, SubnetId}; use registry_canister::mutations::{ do_recover_subnet::RecoverSubnetPayload, do_update_subnet::UpdateSubnetPayload, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::{security_metadata::SecurityMetadata, Ballot}; -#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq, Serialize)] /// Types of acceptable payloads by the recovery canister proposals. pub enum RecoveryPayload { /// Halt NNS. @@ -33,7 +33,7 @@ pub enum RecoveryPayload { Unhalt, } -#[derive(Clone, Debug, CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize, Serialize)] /// Represents a vote from one node operator pub struct NodeOperatorBallot { /// The principal id of the node operator (must be one of the node @@ -50,7 +50,7 @@ pub struct NodeOperatorBallot { pub security_metadata: SecurityMetadata, } -#[derive(Clone, Debug, CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize, Serialize)] pub struct RecoveryProposal { /// The principal id of the proposer (must be one of the node /// operators of the NNS subnet according to the registry at diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index d41941e652d..8cf575970a6 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -2,9 +2,9 @@ use crate::signing::verify_payload_naive; use super::*; use candid::{CandidType, Principal}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, CandidType, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize, Serialize)] /// Wrapper struct containing information regarding integrity. pub struct SecurityMetadata { /// Represents an outcome of a cryptographic operation diff --git a/rs/registry/admin/BUILD.bazel b/rs/registry/admin/BUILD.bazel index b4729122aeb..65c257b26b2 100644 --- a/rs/registry/admin/BUILD.bazel +++ b/rs/registry/admin/BUILD.bazel @@ -44,6 +44,7 @@ DEPENDENCIES = [ "//rs/sns/swap", "//rs/types/management_canister_types", "//rs/types/types", + "//rs/nns/handlers/recovery/client", "@crate_index//:anyhow", "@crate_index//:base64", "@crate_index//:candid", diff --git a/rs/registry/admin/Cargo.toml b/rs/registry/admin/Cargo.toml index 9b24f55e9c7..75cc4122a9a 100644 --- a/rs/registry/admin/Cargo.toml +++ b/rs/registry/admin/Cargo.toml @@ -68,6 +68,7 @@ strum_macros = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } url = { workspace = true } +ic-nns-handler-recovery-client.path = "../../nns/handlers/recovery/client" [dev-dependencies] assert_matches = { workspace = true } diff --git a/rs/registry/admin/src/main.rs b/rs/registry/admin/src/main.rs index b14cd8a8a0a..3d43119e549 100644 --- a/rs/registry/admin/src/main.rs +++ b/rs/registry/admin/src/main.rs @@ -181,6 +181,7 @@ extern crate chrono; mod create_subnet; mod helpers; mod recover_subnet; +mod recovery_canister; mod types; mod update_subnet; @@ -368,6 +369,12 @@ enum SubCommand { /// Get the SSH key access lists for unassigned nodes GetUnassignedNodes, + /// Get proposals from recovery canister + GetRecoveryCanisterProposals, + + /// Get node operators enlisted in the recovery canister + GetRecoveryCanisterNodeOperators, + /// Propose to add an API Boundary Node ProposeToAddApiBoundaryNodes(ProposeToAddApiBoundaryNodesCmd), @@ -3716,19 +3723,19 @@ async fn main() { } if opts.secret_key_pem.is_some() { - let secret_key_path = opts.secret_key_pem.unwrap(); + let secret_key_path = opts.secret_key_pem.as_ref().unwrap(); let contents = read_to_string(secret_key_path).expect("Could not read key file"); let sig_keys = SigKeys::from_pem(&contents).expect("Failed to parse pem file"); Sender::SigKeys(sig_keys) } else if opts.use_hsm { make_hsm_sender( - &opts.hsm_slot.expect( + &opts.hsm_slot.as_ref().expect( "HSM slot must also be provided for --use-hsm; use --hsm-slot or see --help.", ), - &opts.hsm_key_id.expect( + &opts.hsm_key_id.as_ref().expect( "HSM key ID must also be provided for --use-hsm; use --key-id or see --help.", ), - &opts.hsm_pin.expect( + &opts.hsm_pin.as_ref().expect( "HSM pin must also be provided for --use-hsm; use --pin or see --help.", ), ) @@ -4951,6 +4958,9 @@ async fn main() { ); propose_action_from_command(cmd, canister_client, proposer).await; } + SubCommand::GetRecoveryCanisterProposals | SubCommand::GetRecoveryCanisterNodeOperators => { + recovery_canister::execute(&opts).await + } } } diff --git a/rs/registry/admin/src/recovery_canister.rs b/rs/registry/admin/src/recovery_canister.rs new file mode 100644 index 00000000000..8e07097a77a --- /dev/null +++ b/rs/registry/admin/src/recovery_canister.rs @@ -0,0 +1,54 @@ +use ic_nns_handler_recovery_client::{ + builder::{RecoveryCanisterBuilder, SenderOpts}, + implementation::RecoveryCanisterImpl, + RecoveryCanister, +}; + +use crate::{Opts, SubCommand}; + +pub async fn execute(opts: &Opts) { + let client = build_client(opts); + + match opts.subcmd { + SubCommand::GetRecoveryCanisterNodeOperators => { + let response = client.get_node_operators_in_nns().await.unwrap(); + + println!("{}", serde_json::to_string_pretty(&response).unwrap()); + } + SubCommand::GetRecoveryCanisterProposals => { + let response = client.get_pending_recovery_proposals().await.unwrap(); + + println!("{}", serde_json::to_string_pretty(&response).unwrap()); + } + _ => panic!("Not a recovery canister subcommand"), + } +} + +pub fn build_client(opts: &Opts) -> RecoveryCanisterImpl { + let mut builder = RecoveryCanisterBuilder::default(); + + let sender = if opts.use_hsm { + match (&opts.hsm_slot, &opts.hsm_pin, &opts.hsm_key_id) { + (Some(slot), Some(pin), Some(key_id)) => SenderOpts::Hsm { + slot: slot.parse().expect("Cannot parse slot"), + key_id: key_id.to_string(), + pin: pin.to_string(), + }, + _ => panic!("Invalid hsm opts"), + } + } else if let Some(path) = &opts.secret_key_pem { + SenderOpts::Pem { + path: path.display().to_string(), + } + } else { + SenderOpts::Anonymous + }; + + builder.with_sender(sender); + + if !opts.nns_urls.is_empty() { + builder.with_url(opts.nns_urls.first().unwrap().as_str()); + } + + builder.build().unwrap() +} From 714061684b18db781eab7d8db2aebd44baad3846 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 01:35:05 +0100 Subject: [PATCH 63/76] migrating to ic implementations of ed and secp --- Cargo.lock | 5 ++ rs/nns/handlers/recovery/client/BUILD.bazel | 2 + rs/nns/handlers/recovery/client/Cargo.toml | 3 + .../handlers/recovery/client/src/builder.rs | 9 ++- .../recovery/client/src/tests/general.rs | 41 ++++++----- .../impl/canister/tests/initial_args_test.rs | 2 - .../handlers/recovery/interface/BUILD.bazel | 2 + rs/nns/handlers/recovery/interface/Cargo.toml | 2 + .../recovery/interface/src/signing/ed25519.rs | 67 ++++++------------ .../recovery/interface/src/signing/k256.rs | 68 +++++++------------ rs/registry/admin/BUILD.bazel | 1 + rs/registry/admin/Cargo.toml | 1 + rs/registry/admin/src/main.rs | 24 ++++++- rs/registry/admin/src/recovery_canister.rs | 49 ++++++++++++- 14 files changed, 162 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50776725b81..254a828a123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5672,6 +5672,7 @@ dependencies = [ "ic-nns-constants", "ic-nns-governance-api", "ic-nns-handler-recovery-client", + "ic-nns-handler-recovery-interface", "ic-nns-handler-root", "ic-nns-init", "ic-nns-test-utils", @@ -10847,8 +10848,10 @@ dependencies = [ "candid", "ed25519-dalek", "ic-agent", + "ic-ed25519", "ic-identity-hsm", "ic-nns-handler-recovery-interface", + "ic-secp256k1", "k256 0.13.4", "p256", "pocket-ic", @@ -10864,6 +10867,8 @@ dependencies = [ "ed25519-dalek", "hex", "ic-base-types", + "ic-ed25519", + "ic-secp256k1", "k256 0.13.4", "p256", "registry-canister", diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 03c388201c0..0f7ed885a62 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -5,6 +5,8 @@ package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ # Keep sorted. "//rs/nns/handlers/recovery/interface", + "//packages/ic-ed25519", + "//packages/ic-secp256k1", "@crate_index//:candid", "@crate_index//:serde", "@crate_index//:ic-agent", diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index ea6e296f255..35113686c9f 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -17,6 +17,9 @@ ic-identity-hsm.workspace = true p256.workspace = true k256.workspace = true ed25519-dalek.workspace = true +ic-secp256k1.path = "../../../../../packages/ic-secp256k1" +ic-ed25519.path = "../../../../../packages/ic-ed25519" + [dev-dependencies] tokio.workspace = true diff --git a/rs/nns/handlers/recovery/client/src/builder.rs b/rs/nns/handlers/recovery/client/src/builder.rs index 2933dfea2de..faa5a917e14 100644 --- a/rs/nns/handlers/recovery/client/src/builder.rs +++ b/rs/nns/handlers/recovery/client/src/builder.rs @@ -85,16 +85,19 @@ impl TryFrom for Arc { let maybe_signer: Result, RecoveryError> = EdwardsCurve::from_pem(&path) .map(|signer| Arc::new(signer) as Arc) - .or_else(|_| { + .or_else(|e| { + eprintln!("Received error: {:?}", e); Prime256::from_pem(&path) .map(|signer| Arc::new(signer) as Arc) }) - .or_else(|_| { + .or_else(|e| { + eprintln!("Received error: {:?}", e); Secp256k1::from_pem(&path) .map(|signer| Arc::new(signer) as Arc) }); - let signer = maybe_signer.map_err(|_| { + let signer = maybe_signer.map_err(|e| { + eprintln!("Received error: {:?}", e); RecoveryError::InvalidIdentity( "Couldn't deserialize identity into any known implementation".to_string(), ) diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index cb83702efd6..933de06e257 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,13 +1,13 @@ use std::sync::Arc; -use ed25519_dalek::SigningKey as EdSigningKey; use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; +use ic_ed25519::PrivateKey as EdwardPrivateKey; use ic_nns_handler_recovery_interface::{ recovery::RecoveryPayload, signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Verifier}, Ballot, }; -use k256::SecretKey as k256SecretKey; +use ic_secp256k1::PrivateKey as SecpPrivateKey; use p256::{elliptic_curve::rand_core::OsRng, SecretKey as p256SecretKey}; use crate::{ @@ -20,7 +20,7 @@ use super::init_pocket_ic; #[tokio::test] async fn can_get_node_operators() { - let key = EdSigningKey::generate(&mut OsRng); + let key = EdwardPrivateKey::generate(); let signer = EdwardsCurve::new(key.clone()); let node_operators_with_keys = @@ -28,7 +28,9 @@ async fn can_get_node_operators() { let (pic, canister) = init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let identity = BasicIdentity::from_signing_key((key.to_bytes()).into()); + let pem = key.serialize_pkcs8_pem(ic_ed25519::PrivateKeyFormat::Pkcs8v1); + let pem = pem.as_bytes(); + let identity = BasicIdentity::from_pem(pem).unwrap(); let client = RecoveryCanisterImpl::new( get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, @@ -45,7 +47,7 @@ async fn can_get_node_operators() { #[tokio::test] async fn can_place_proposals_edwards() { - let key = EdSigningKey::generate(&mut OsRng); + let key = EdwardPrivateKey::generate(); let signer = EdwardsCurve::new(key.clone()); let node_operators_with_keys = @@ -53,7 +55,9 @@ async fn can_place_proposals_edwards() { let (pic, canister) = init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let identity = BasicIdentity::from_signing_key((key.to_bytes()).into()); + let pem = key.serialize_pkcs8_pem(ic_ed25519::PrivateKeyFormat::Pkcs8v1); + let pem = pem.as_bytes(); + let identity = BasicIdentity::from_pem(pem).unwrap(); let client = RecoveryCanisterImpl::new( get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, @@ -97,17 +101,18 @@ async fn can_place_proposals_prime256() { #[tokio::test] async fn can_place_proposals_secp256() { - let secret_key = k256SecretKey::random(&mut OsRng); - let signing_key = secret_key.clone().into(); + let private_key = SecpPrivateKey::generate(); - let signer = Secp256k1::new(signing_key); + let signer = Secp256k1::new(private_key.clone()); let node_operators_with_keys = generate_node_operators(vec![signer.to_public_key_der().unwrap()]); let (pic, canister) = init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let identity = Secp256k1Identity::from_private_key(secret_key); + let pem = private_key.serialize_rfc5915_pem(); + let pem = pem.as_bytes(); + let identity = Secp256k1Identity::from_pem(pem).unwrap(); let client = RecoveryCanisterImpl::new( get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, @@ -124,7 +129,7 @@ async fn can_place_proposals_secp256() { #[tokio::test] async fn can_vote_on_proposals_edwards() { - let key = EdSigningKey::generate(&mut OsRng); + let key: EdwardPrivateKey = EdwardPrivateKey::generate(); let signer = EdwardsCurve::new(key.clone()); let node_operators_with_keys = @@ -132,7 +137,9 @@ async fn can_vote_on_proposals_edwards() { let (pic, canister) = init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let identity = BasicIdentity::from_signing_key((key.to_bytes()).into()); + let pem = key.serialize_pkcs8_pem(ic_ed25519::PrivateKeyFormat::Pkcs8v1); + let pem = pem.as_bytes(); + let identity = BasicIdentity::from_pem(pem).unwrap(); let client = RecoveryCanisterImpl::new( get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, @@ -184,17 +191,17 @@ async fn can_vote_on_proposals_prime256() { #[tokio::test] async fn can_vote_on_proposals_secp256() { - let secret_key = k256SecretKey::random(&mut OsRng); - let signing_key = secret_key.clone().into(); - - let signer = Secp256k1::new(signing_key); + let secret_key = SecpPrivateKey::generate(); + let pem = secret_key.serialize_rfc5915_pem(); + let pem = pem.as_bytes(); + let signer = Secp256k1::new(secret_key); let node_operators_with_keys = generate_node_operators(vec![signer.to_public_key_der().unwrap()]); let (pic, canister) = init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - let identity = Secp256k1Identity::from_private_key(secret_key); + let identity = Secp256k1Identity::from_pem(pem).unwrap(); let client = RecoveryCanisterImpl::new( get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, diff --git a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs index fdc84038057..93faab34c91 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/initial_args_test.rs @@ -1,5 +1,3 @@ -use std::io::Write; - use candid::Principal; use ic_base_types::PrincipalId; use ic_nns_handler_recovery_interface::{ diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index d23b3156679..e51b48899f4 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -13,6 +13,8 @@ DEPENDENCIES = [ "@crate_index//:k256", "//rs/registry/canister", "//rs/types/base_types", + "//packages/ic-ed25519", + "//packages/ic-secp256k1", ] MACRO_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index 33cf061a595..d84f59edf6f 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -16,3 +16,5 @@ hex.workspace = true spki.workspace = true p256.workspace = true k256.workspace = true +ic-secp256k1.path = "../../../../../packages/ic-secp256k1" +ic-ed25519.path = "../../../../../packages/ic-ed25519" diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index 44cddc62a41..931b151a540 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -1,98 +1,71 @@ -use ed25519_dalek::{ - pkcs8::EncodePublicKey, Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, -}; -use k256::pkcs8::DecodePrivateKey; -use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; +use ic_ed25519::{PrivateKey, PublicKey}; use crate::RecoveryError; #[derive(Clone)] pub struct EdwardsCurve { - signing_key: Option, - verifying_key: VerifyingKey, + private_key: Option, + public_key: PublicKey, } impl super::Signer for EdwardsCurve { fn sign_payload(&self, payload: &[u8]) -> crate::Result> { - let signing_key = self - .signing_key + let private_key = self + .private_key .clone() .ok_or(RecoveryError::InvalidIdentity( "Signing key missing".to_string(), ))?; - let signature = signing_key.sign(payload); + let signature = private_key.sign_message(payload); Ok(signature.to_vec()) } } impl super::Verifier for EdwardsCurve { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { - let signature = Signature::from_slice(signature) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - self.verifying_key - .verify_strict(payload, &signature) + self.public_key + .verify_signature(payload, signature) .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) } fn to_public_key_der(&self) -> crate::Result> { - self.verifying_key - .to_public_key_der() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) - .map(|document| document.into_vec()) + Ok(self.public_key.serialize_rfc8410_der()) } } impl EdwardsCurve { pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { - let document: Document = Document::from_public_key_der(public_key_der) - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - - let info: SubjectPublicKeyInfoRef = document - .decode_msg() + let public_key = PublicKey::deserialize_rfc8410_der(public_key_der) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - let verifying_key: VerifyingKey = info - .try_into() - .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string()))?; - Ok(Self { - signing_key: None, - verifying_key, + private_key: None, + public_key, }) } pub fn from_public_key(public_key: &[u8]) -> crate::Result { - if public_key.len() != PUBLIC_KEY_LENGTH { - return Err(RecoveryError::InvalidPubKey( - "Invalid public key length".to_string(), - )); - } - - let mut pub_key = [0; 32]; - pub_key.copy_from_slice(public_key); - - let verifying_key = VerifyingKey::from_bytes(&pub_key) + let public_key = PublicKey::deserialize_raw(public_key) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; Ok(Self { - signing_key: None, - verifying_key, + private_key: None, + public_key, }) } - pub fn new(signing_key: SigningKey) -> Self { + pub fn new(private_key: PrivateKey) -> Self { Self { - verifying_key: signing_key.verifying_key(), - signing_key: Some(signing_key), + public_key: private_key.public_key(), + private_key: Some(private_key), } } pub fn from_pem(path: &str) -> crate::Result { - let signing_key = SigningKey::from_pkcs8_pem(path) + let private_key = PrivateKey::deserialize_pkcs8_pem(path) .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; - Ok(Self::new(signing_key)) + Ok(Self::new(private_key)) } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs index 7ce0e3aca80..444ef1e5d1e 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/k256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -1,92 +1,74 @@ -use k256::ecdsa::signature::{Signer, Verifier}; -use k256::ecdsa::{Signature, SigningKey, VerifyingKey}; -use k256::pkcs8::{DecodePrivateKey, Document, EncodePublicKey}; -use spki::{DecodePublicKey, SubjectPublicKeyInfoRef}; +use ic_secp256k1::{PrivateKey, PublicKey}; use crate::RecoveryError; pub struct Secp256k1 { - signing_key: Option, - verifying_key: VerifyingKey, + private_key: Option, + public_key: PublicKey, } impl super::Verifier for Secp256k1 { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { - let signature = Signature::from_slice(signature) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - self.verifying_key - .verify(payload, &signature) - .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + match self.public_key.verify_ecdsa_signature(payload, signature) { + true => Ok(()), + false => Err(RecoveryError::InvalidSignature( + "Invalid signature".to_string(), + )), + } } fn to_public_key_der(&self) -> crate::Result> { - self.verifying_key - .to_public_key_der() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) - .map(|document| document.into_vec()) + Ok(self.public_key.serialize_der()) } } impl super::Signer for Secp256k1 { fn sign_payload(&self, payload: &[u8]) -> crate::Result> { - let signing_key = self - .signing_key + let private_key = self + .private_key .clone() .ok_or(RecoveryError::InvalidIdentity( "Signing key missing".to_string(), ))?; - let signature: Signature = signing_key - .try_sign(payload) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + let signature = private_key.sign_message_with_ecdsa(payload); - let r = signature.r().to_bytes().to_vec(); - let s = signature.s().to_bytes().to_vec(); - Ok(r.into_iter().chain(s).collect()) + Ok(signature.to_vec()) } } impl Secp256k1 { pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { - let document: Document = Document::from_public_key_der(public_key_der) + let public_key = PublicKey::deserialize_der(public_key_der) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - let info: SubjectPublicKeyInfoRef = document - .decode_msg() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - - let verifying_key: VerifyingKey = info - .try_into() - .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string()))?; - Ok(Self { - signing_key: None, - verifying_key, + private_key: None, + public_key, }) } pub fn from_public_key(public_key: &[u8]) -> crate::Result { - let verifying_key = VerifyingKey::from_sec1_bytes(public_key) + let public_key = PublicKey::deserialize_sec1(public_key) .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; Ok(Self { - signing_key: None, - verifying_key, + private_key: None, + public_key, }) } - pub fn new(signing_key: SigningKey) -> Self { + pub fn new(private_key: PrivateKey) -> Self { Self { - verifying_key: *signing_key.verifying_key(), - signing_key: Some(signing_key), + public_key: private_key.public_key(), + private_key: Some(private_key), } } pub fn from_pem(path: &str) -> crate::Result { - let signing_key = SigningKey::from_pkcs8_pem(path) + let private_key = PrivateKey::deserialize_rfc5915_pem(path) .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; - Ok(Self::new(signing_key)) + Ok(Self::new(private_key.into())) } } diff --git a/rs/registry/admin/BUILD.bazel b/rs/registry/admin/BUILD.bazel index 65c257b26b2..629f9ba70dd 100644 --- a/rs/registry/admin/BUILD.bazel +++ b/rs/registry/admin/BUILD.bazel @@ -45,6 +45,7 @@ DEPENDENCIES = [ "//rs/types/management_canister_types", "//rs/types/types", "//rs/nns/handlers/recovery/client", + "//rs/nns/handlers/recovery/interface", "@crate_index//:anyhow", "@crate_index//:base64", "@crate_index//:candid", diff --git a/rs/registry/admin/Cargo.toml b/rs/registry/admin/Cargo.toml index 75cc4122a9a..e5b4f9fbd97 100644 --- a/rs/registry/admin/Cargo.toml +++ b/rs/registry/admin/Cargo.toml @@ -69,6 +69,7 @@ tempfile = { workspace = true } tokio = { workspace = true } url = { workspace = true } ic-nns-handler-recovery-client.path = "../../nns/handlers/recovery/client" +ic-nns-handler-recovery-interface.path = "../../nns/handlers/recovery/interface" [dev-dependencies] assert_matches = { workspace = true } diff --git a/rs/registry/admin/src/main.rs b/rs/registry/admin/src/main.rs index 3d43119e549..32c79cf0ebb 100644 --- a/rs/registry/admin/src/main.rs +++ b/rs/registry/admin/src/main.rs @@ -126,6 +126,7 @@ use itertools::izip; use maplit::hashmap; use prost::Message; use recover_subnet::ProposeToUpdateRecoveryCupCmd; +use recovery_canister::{RecoveryCanisterBallot, RecoveryCanisterProposeRecoveryCmd}; use registry_canister::mutations::{ complete_canister_migration::CompleteCanisterMigrationPayload, do_add_api_boundary_nodes::AddApiBoundaryNodesPayload, @@ -375,6 +376,18 @@ enum SubCommand { /// Get node operators enlisted in the recovery canister GetRecoveryCanisterNodeOperators, + /// Propose to halt NNS + RecoveryCanisterProposeHalt, + + /// Propose to unhalt NNS + RecoveryCanisterProposeUnhalt, + + /// Propose to do recovery + RecoveryCanisterProposeRecovery(RecoveryCanisterProposeRecoveryCmd), + + /// Submit a vote to the latest recovery canister proposal + RecoveryCanisterVoteOnLatestProposal(RecoveryCanisterBallot), + /// Propose to add an API Boundary Node ProposeToAddApiBoundaryNodes(ProposeToAddApiBoundaryNodesCmd), @@ -3716,6 +3729,10 @@ async fn main() { SubCommand::ProposeToUpdateXdrIcpConversionRate(_) => (), SubCommand::SubmitRootProposalToUpgradeGovernanceCanister(_) => (), SubCommand::VoteOnRootProposalToUpgradeGovernanceCanister(_) => (), + SubCommand::RecoveryCanisterProposeHalt => (), + SubCommand::RecoveryCanisterProposeUnhalt => (), + SubCommand::RecoveryCanisterProposeRecovery(_) => (), + SubCommand::RecoveryCanisterVoteOnLatestProposal(_) => (), _ => panic!( "Specifying a secret key or HSM is only supported for \ methods that interact with NNS handlers." @@ -4958,7 +4975,12 @@ async fn main() { ); propose_action_from_command(cmd, canister_client, proposer).await; } - SubCommand::GetRecoveryCanisterProposals | SubCommand::GetRecoveryCanisterNodeOperators => { + SubCommand::GetRecoveryCanisterProposals + | SubCommand::GetRecoveryCanisterNodeOperators + | SubCommand::RecoveryCanisterProposeHalt + | SubCommand::RecoveryCanisterProposeUnhalt + | SubCommand::RecoveryCanisterProposeRecovery(_) + | SubCommand::RecoveryCanisterVoteOnLatestProposal(_) => { recovery_canister::execute(&opts).await } } diff --git a/rs/registry/admin/src/recovery_canister.rs b/rs/registry/admin/src/recovery_canister.rs index 8e07097a77a..2a81166e3b0 100644 --- a/rs/registry/admin/src/recovery_canister.rs +++ b/rs/registry/admin/src/recovery_canister.rs @@ -1,26 +1,73 @@ +use clap::Args; use ic_nns_handler_recovery_client::{ builder::{RecoveryCanisterBuilder, SenderOpts}, implementation::RecoveryCanisterImpl, RecoveryCanister, }; +use ic_nns_handler_recovery_interface::{recovery::RecoveryPayload, Ballot}; use crate::{Opts, SubCommand}; +#[derive(Args)] +pub struct RecoveryCanisterProposeRecoveryCmd { + height: u64, + state_hash: String, +} + +#[derive(Args)] +pub struct RecoveryCanisterBallot { + ballot: String, +} + pub async fn execute(opts: &Opts) { let client = build_client(opts); - match opts.subcmd { + let response = match &opts.subcmd { SubCommand::GetRecoveryCanisterNodeOperators => { let response = client.get_node_operators_in_nns().await.unwrap(); println!("{}", serde_json::to_string_pretty(&response).unwrap()); + return; } SubCommand::GetRecoveryCanisterProposals => { let response = client.get_pending_recovery_proposals().await.unwrap(); println!("{}", serde_json::to_string_pretty(&response).unwrap()); + return; + } + SubCommand::RecoveryCanisterProposeHalt => { + client + .submit_new_recovery_proposal(RecoveryPayload::Halt) + .await + } + SubCommand::RecoveryCanisterProposeUnhalt => { + client + .submit_new_recovery_proposal(RecoveryPayload::Unhalt) + .await + } + SubCommand::RecoveryCanisterProposeRecovery(cmd) => { + client + .submit_new_recovery_proposal(RecoveryPayload::DoRecovery { + height: cmd.height, + state_hash: cmd.state_hash.clone(), + }) + .await + } + SubCommand::RecoveryCanisterVoteOnLatestProposal(cmd) => { + client + .vote_on_latest_proposal(match cmd.ballot.to_lowercase().as_str() { + "yes" | "y" => Ballot::Yes, + "no" | "n" => Ballot::No, + s => panic!("Unknown ballot: {}", s), + }) + .await } _ => panic!("Not a recovery canister subcommand"), + }; + + match response { + Ok(_) => eprintln!("Operation completed successfully"), + Err(e) => eprintln!("Received error: {:?}", e), } } From fe868692b2c9caaaaeae4097285ce799ae579402 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 02:19:16 +0100 Subject: [PATCH 64/76] removing unsupported p256 --- Cargo.Bazel.json.lock | 7 +- Cargo.Bazel.toml.lock | 1 - Cargo.lock | 2 - Cargo.toml | 1 - bazel/external_crates.bzl | 3 - rs/nns/handlers/recovery/client/BUILD.bazel | 1 - rs/nns/handlers/recovery/client/Cargo.toml | 2 - .../handlers/recovery/client/src/builder.rs | 15 +-- .../recovery/client/src/tests/general.rs | 63 +------------ .../handlers/recovery/interface/BUILD.bazel | 2 - rs/nns/handlers/recovery/interface/Cargo.toml | 1 - .../recovery/interface/src/signing/ed25519.rs | 4 +- .../recovery/interface/src/signing/hsm.rs | 6 +- .../recovery/interface/src/signing/k256.rs | 6 +- .../recovery/interface/src/signing/mod.rs | 6 -- .../recovery/interface/src/signing/p256.rs | 92 ------------------- 16 files changed, 14 insertions(+), 198 deletions(-) delete mode 100644 rs/nns/handlers/recovery/interface/src/signing/p256.rs diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index 90a69f8432a..ffc9c4360e3 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "db92f1b23dc7421270036e33322b78b5201037b7c480b910ca77c74f414f5a6f", + "checksum": "71b5a5e66eaef8dd9b3113fc3de526af7b3199a43d1bc5ed8c2671250a6797f4", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -20385,10 +20385,6 @@ "id": "socket2 0.5.7", "target": "socket2" }, - { - "id": "spki 0.7.3", - "target": "spki" - }, { "id": "ssh2 0.9.4", "target": "ssh2" @@ -91503,7 +91499,6 @@ "slog-scope 4.4.0", "slog-term 2.9.1", "socket2 0.5.7", - "spki 0.7.3", "ssh2 0.9.4", "static_assertions 1.1.0", "strum 0.26.3", diff --git a/Cargo.Bazel.toml.lock b/Cargo.Bazel.toml.lock index 4d80d8c4b51..4ff605bd40e 100644 --- a/Cargo.Bazel.toml.lock +++ b/Cargo.Bazel.toml.lock @@ -3441,7 +3441,6 @@ dependencies = [ "slog-scope", "slog-term", "socket2 0.5.7", - "spki 0.7.3", "ssh2", "static_assertions", "strum 0.26.3", diff --git a/Cargo.lock b/Cargo.lock index 254a828a123..98b08fbd764 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10853,7 +10853,6 @@ dependencies = [ "ic-nns-handler-recovery-interface", "ic-secp256k1", "k256 0.13.4", - "p256", "pocket-ic", "serde", "tokio", @@ -10870,7 +10869,6 @@ dependencies = [ "ic-ed25519", "ic-secp256k1", "k256 0.13.4", - "p256", "registry-canister", "serde", "spki 0.7.3", diff --git a/Cargo.toml b/Cargo.toml index f232cfcd9c4..2ca3901967e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -707,7 +707,6 @@ strum = { version = "0.26.3", features = ["derive"] } strum_macros = "0.26.4" subtle = "2.6.1" syn = { version = "1.0.109", features = ["fold", "full"] } -spki = "0.7.3" tar = "0.4.39" tempfile = "3.12.0" thiserror = "2.0.3" diff --git a/bazel/external_crates.bzl b/bazel/external_crates.bzl index 2d0b52a1fc6..e03896daa9a 100644 --- a/bazel/external_crates.bzl +++ b/bazel/external_crates.bzl @@ -444,9 +444,6 @@ def external_crates_repository(name, cargo_lockfile, lockfile, sanitizers_enable version = "^2.1.1", features = ["std", "zeroize", "digest", "batch", "pkcs8", "pem", "hazmat"], ), - "spki": crate.spec( - version = "^0.7.3" - ), "educe": crate.spec( version = "^0.4", ), diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 0f7ed885a62..1eb7958941b 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -11,7 +11,6 @@ DEPENDENCIES = [ "@crate_index//:serde", "@crate_index//:ic-agent", "@crate_index//:ic-identity-hsm", - "@crate_index//:p256", "@crate_index//:ed25519-dalek", "@crate_index//:k256", ] diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 35113686c9f..0f40d583807 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -14,13 +14,11 @@ ic-agent.workspace = true async-trait.workspace = true ic-nns-handler-recovery-interface.path = "../interface" ic-identity-hsm.workspace = true -p256.workspace = true k256.workspace = true ed25519-dalek.workspace = true ic-secp256k1.path = "../../../../../packages/ic-secp256k1" ic-ed25519.path = "../../../../../packages/ic-ed25519" - [dev-dependencies] tokio.workspace = true pocket-ic.path = "../../../../../packages/pocket-ic" diff --git a/rs/nns/handlers/recovery/client/src/builder.rs b/rs/nns/handlers/recovery/client/src/builder.rs index faa5a917e14..8b183c6093f 100644 --- a/rs/nns/handlers/recovery/client/src/builder.rs +++ b/rs/nns/handlers/recovery/client/src/builder.rs @@ -8,9 +8,7 @@ use ic_agent::{ }; use ic_identity_hsm::HardwareIdentity; use ic_nns_handler_recovery_interface::{ - signing::{ - anonymous::AnonymousSigner, ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Signer, - }, + signing::{anonymous::AnonymousSigner, ed25519::EdwardsCurve, k256::Secp256k1, Signer}, RecoveryError, RECOVERY_CANISTER_ID, }; @@ -85,19 +83,12 @@ impl TryFrom for Arc { let maybe_signer: Result, RecoveryError> = EdwardsCurve::from_pem(&path) .map(|signer| Arc::new(signer) as Arc) - .or_else(|e| { - eprintln!("Received error: {:?}", e); - Prime256::from_pem(&path) - .map(|signer| Arc::new(signer) as Arc) - }) - .or_else(|e| { - eprintln!("Received error: {:?}", e); + .or_else(|_| { Secp256k1::from_pem(&path) .map(|signer| Arc::new(signer) as Arc) }); - let signer = maybe_signer.map_err(|e| { - eprintln!("Received error: {:?}", e); + let signer = maybe_signer.map_err(|_| { RecoveryError::InvalidIdentity( "Couldn't deserialize identity into any known implementation".to_string(), ) diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index 933de06e257..c6a60204613 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,14 +1,13 @@ use std::sync::Arc; -use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; +use ic_agent::identity::{BasicIdentity, Secp256k1Identity}; use ic_ed25519::PrivateKey as EdwardPrivateKey; use ic_nns_handler_recovery_interface::{ recovery::RecoveryPayload, - signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Verifier}, + signing::{ed25519::EdwardsCurve, k256::Secp256k1, Verifier}, Ballot, }; use ic_secp256k1::PrivateKey as SecpPrivateKey; -use p256::{elliptic_curve::rand_core::OsRng, SecretKey as p256SecretKey}; use crate::{ implementation::RecoveryCanisterImpl, @@ -72,33 +71,6 @@ async fn can_place_proposals_edwards() { assert!(response.is_ok()); } -#[tokio::test] -async fn can_place_proposals_prime256() { - let secret_key = p256SecretKey::random(&mut OsRng); - let signing_key = secret_key.clone().into(); - - let signer = Prime256::new(signing_key); - - let node_operators_with_keys = - generate_node_operators(vec![signer.to_public_key_der().unwrap()]); - let (pic, canister) = - init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - - let identity = Prime256v1Identity::from_private_key(secret_key); - - let client = RecoveryCanisterImpl::new( - get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, - canister, - Arc::new(signer), - ); - - let response = client - .submit_new_recovery_proposal(RecoveryPayload::Halt) - .await; - - assert!(response.is_ok()); -} - #[tokio::test] async fn can_place_proposals_secp256() { let private_key = SecpPrivateKey::generate(); @@ -158,37 +130,6 @@ async fn can_vote_on_proposals_edwards() { assert!(response.is_ok()); } -#[tokio::test] -async fn can_vote_on_proposals_prime256() { - let secret_key = p256SecretKey::random(&mut OsRng); - let signing_key = secret_key.clone().into(); - - let signer = Prime256::new(signing_key); - - let node_operators_with_keys = - generate_node_operators(vec![signer.to_public_key_der().unwrap()]); - let (pic, canister) = - init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; - - let identity = Prime256v1Identity::from_private_key(secret_key); - - let client = RecoveryCanisterImpl::new( - get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, - canister, - Arc::new(signer), - ); - - client - .submit_new_recovery_proposal(RecoveryPayload::Halt) - .await - .unwrap(); - - let response = client.vote_on_latest_proposal(Ballot::Yes).await; - println!("{:?}", response); - - assert!(response.is_ok()); -} - #[tokio::test] async fn can_vote_on_proposals_secp256() { let secret_key = SecpPrivateKey::generate(); diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index e51b48899f4..545e3933300 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -8,8 +8,6 @@ DEPENDENCIES = [ "@crate_index//:serde", "@crate_index//:ed25519-dalek", "@crate_index//:hex", - "@crate_index//:spki", - "@crate_index//:p256", "@crate_index//:k256", "//rs/registry/canister", "//rs/types/base_types", diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index d84f59edf6f..a0a28b4718a 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -14,7 +14,6 @@ registry-canister.path = "../../../../registry/canister" ic-base-types.path = "../../../../types/base_types" hex.workspace = true spki.workspace = true -p256.workspace = true k256.workspace = true ic-secp256k1.path = "../../../../../packages/ic-secp256k1" ic-ed25519.path = "../../../../../packages/ic-ed25519" diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index 931b151a540..99419a95d42 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -63,7 +63,9 @@ impl EdwardsCurve { } pub fn from_pem(path: &str) -> crate::Result { - let private_key = PrivateKey::deserialize_pkcs8_pem(path) + let contents = std::fs::read_to_string(path) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + let private_key = PrivateKey::deserialize_pkcs8_pem(&contents) .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; Ok(Self::new(private_key)) diff --git a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs index b2c17fac157..af17c87959d 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::RecoveryError; -use super::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256, Signer, Verifier}; +use super::{ed25519::EdwardsCurve, k256::Secp256k1, Signer, Verifier}; // From ic-admin pub type SignBytes = @@ -42,10 +42,6 @@ impl Hsm { return Ok(Self::from_verifier(pub_key, Arc::new(edwards))); } - if let Ok(prime256) = Prime256::from_public_key(pub_key) { - return Ok(Self::from_verifier(pub_key, Arc::new(prime256))); - } - if let Ok(secp256) = Secp256k1::from_public_key(pub_key) { return Ok(Self::from_verifier(pub_key, Arc::new(secp256))); } diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs index 444ef1e5d1e..1e52ace76f6 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/k256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -66,9 +66,11 @@ impl Secp256k1 { } pub fn from_pem(path: &str) -> crate::Result { - let private_key = PrivateKey::deserialize_rfc5915_pem(path) + let contents = std::fs::read_to_string(path) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + let private_key = PrivateKey::deserialize_rfc5915_pem(&contents) .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; - Ok(Self::new(private_key.into())) + Ok(Self::new(private_key)) } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index 7d75399dac3..f9ff0245397 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -1,7 +1,6 @@ use ed25519::EdwardsCurve; use hsm::Hsm; use k256::Secp256k1; -use p256::Prime256; use crate::RecoveryError; @@ -11,7 +10,6 @@ pub mod anonymous; pub mod ed25519; pub mod hsm; pub mod k256; -pub mod p256; pub trait Verifier: Send + Sync { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> Result<()>; @@ -27,10 +25,6 @@ pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[ return verify_payload_naive_inner(verifier, payload, signature); } - if let Ok(verifier) = Prime256::from_public_key_der(public_key_der) { - return verify_payload_naive_inner(verifier, payload, signature); - } - if let Ok(verifier) = Secp256k1::from_public_key_der(public_key_der) { return verify_payload_naive_inner(verifier, payload, signature); } diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs deleted file mode 100644 index 5573635eef5..00000000000 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ /dev/null @@ -1,92 +0,0 @@ -use k256::pkcs8::DecodePrivateKey; -use p256::ecdsa::{signature::SignerMut, signature::Verifier, Signature, SigningKey, VerifyingKey}; -use p256::pkcs8::EncodePublicKey; -use spki::{DecodePublicKey, Document, SubjectPublicKeyInfoRef}; - -use crate::RecoveryError; - -pub struct Prime256 { - signing_key: Option, - verifying_key: VerifyingKey, -} - -impl super::Signer for Prime256 { - fn sign_payload(&self, payload: &[u8]) -> crate::Result> { - let mut signing_key = self - .signing_key - .clone() - .ok_or(RecoveryError::InvalidIdentity( - "Signing key missing".to_string(), - ))?; - - let signature: Signature = signing_key - .try_sign(payload) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - let r = signature.r().to_bytes().to_vec(); - let s = signature.s().to_bytes().to_vec(); - Ok(r.into_iter().chain(s).collect()) - } -} - -impl super::Verifier for Prime256 { - fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { - let signature = Signature::from_slice(signature) - .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; - - self.verifying_key - .verify(payload, &signature) - .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) - } - - fn to_public_key_der(&self) -> crate::Result> { - self.verifying_key - .to_public_key_der() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) - .map(|document| document.into_vec()) - } -} - -impl Prime256 { - pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { - let document: Document = Document::from_public_key_der(public_key_der) - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - - let info: SubjectPublicKeyInfoRef = document - .decode_msg() - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - - let verifying_key: VerifyingKey = info - .try_into() - .map_err(|e: spki::Error| RecoveryError::InvalidPubKey(e.to_string()))?; - - Ok(Self { - signing_key: None, - verifying_key, - }) - } - - pub fn from_public_key(public_key: &[u8]) -> crate::Result { - let verifying_key = VerifyingKey::from_sec1_bytes(public_key) - .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; - - Ok(Self { - signing_key: None, - verifying_key, - }) - } - - pub fn new(signing_key: SigningKey) -> Self { - Self { - verifying_key: *signing_key.verifying_key(), - signing_key: Some(signing_key), - } - } - - pub fn from_pem(path: &str) -> crate::Result { - let signing_key = SigningKey::from_pkcs8_pem(path) - .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; - - Ok(Self::new(signing_key)) - } -} From 6ee28850435adfdd3604c082eb29196ae55f1be0 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 02:21:30 +0100 Subject: [PATCH 65/76] removing spki --- Cargo.lock | 1 - rs/nns/handlers/recovery/interface/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98b08fbd764..9706dda6e37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10871,7 +10871,6 @@ dependencies = [ "k256 0.13.4", "registry-canister", "serde", - "spki 0.7.3", ] [[package]] diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index a0a28b4718a..2579ac07c32 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -13,7 +13,6 @@ ed25519-dalek.workspace = true registry-canister.path = "../../../../registry/canister" ic-base-types.path = "../../../../types/base_types" hex.workspace = true -spki.workspace = true k256.workspace = true ic-secp256k1.path = "../../../../../packages/ic-secp256k1" ic-ed25519.path = "../../../../../packages/ic-ed25519" From a8918c7e541bfd93779cdcc6df4f74981470a04a Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 15:00:00 +0100 Subject: [PATCH 66/76] adding hsm support --- .../handlers/recovery/client/src/builder.rs | 89 ++++++++++--------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/builder.rs b/rs/nns/handlers/recovery/client/src/builder.rs index 8b183c6093f..1ec93fcd967 100644 --- a/rs/nns/handlers/recovery/client/src/builder.rs +++ b/rs/nns/handlers/recovery/client/src/builder.rs @@ -3,12 +3,14 @@ use std::sync::Arc; use candid::Principal; use ic_agent::{ agent::AgentBuilder, - identity::{AnonymousIdentity, BasicIdentity, PemError, Prime256v1Identity, Secp256k1Identity}, + identity::{AnonymousIdentity, BasicIdentity, PemError, Secp256k1Identity}, Identity, }; use ic_identity_hsm::HardwareIdentity; use ic_nns_handler_recovery_interface::{ - signing::{anonymous::AnonymousSigner, ed25519::EdwardsCurve, k256::Secp256k1, Signer}, + signing::{ + anonymous::AnonymousSigner, ed25519::EdwardsCurve, hsm::Hsm, k256::Secp256k1, Signer, + }, RecoveryError, RECOVERY_CANISTER_ID, }; @@ -60,11 +62,10 @@ impl RecoveryCanisterBuilder { } pub fn build(self) -> Result { - let signer = self.sender.clone().try_into()?; - let identity: Box = self.sender.try_into()?; + let (signer, identity) = self.sender.try_into()?; let ic_agent = AgentBuilder::default() .with_url(self.url) - .with_identity(identity) + .with_arc_identity(identity) .build() .map_err(|e| RecoveryError::AgentError(e.to_string()))?; @@ -74,12 +75,24 @@ impl RecoveryCanisterBuilder { } } -impl TryFrom for Arc { +impl TryFrom for (Arc, Arc) { type Error = RecoveryError; fn try_from(value: SenderOpts) -> Result { match value { + SenderOpts::Anonymous => Ok((Arc::new(AnonymousSigner), Arc::new(AnonymousIdentity))), SenderOpts::Pem { path } => { + let maybe_identity: Result, PemError> = + BasicIdentity::from_pem_file(&path) + .map(|identity| Arc::new(identity) as Arc) + .or_else(|_| { + Secp256k1Identity::from_pem_file(&path) + .map(|identity| Arc::new(identity) as Arc) + }); + + let identity = + maybe_identity.map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + let maybe_signer: Result, RecoveryError> = EdwardsCurve::from_pem(&path) .map(|signer| Arc::new(signer) as Arc) @@ -93,47 +106,37 @@ impl TryFrom for Arc { "Couldn't deserialize identity into any known implementation".to_string(), ) })?; - Ok(signer) - } - SenderOpts::Hsm { - slot: _, - key_id: _, - pin: _, - } => unimplemented!("Ic agent blocks the session"), - SenderOpts::Anonymous => Ok(Arc::new(AnonymousSigner)), - } - } -} - -impl TryFrom for Box { - type Error = RecoveryError; - - fn try_from(value: SenderOpts) -> Result { - match value { - SenderOpts::Pem { path } => { - let maybe_identity: Result, PemError> = - BasicIdentity::from_pem_file(&path) - .map(|identity| Box::new(identity) as Box) - .or_else(|_| { - Prime256v1Identity::from_pem_file(&path) - .map(|identity| Box::new(identity) as Box) - }) - .or_else(|_| { - Secp256k1Identity::from_pem_file(&path) - .map(|identity| Box::new(identity) as Box) - }); - - let identity = - maybe_identity.map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; - Ok(identity) + Ok((signer, identity)) } SenderOpts::Hsm { slot, key_id, pin } => { - HardwareIdentity::new("/usr/lib/opensc-pkcs11.so", slot, &key_id, || Ok(pin)) - .map_err(|e| RecoveryError::InvalidIdentity(e.to_string())) - .map(|identity| Box::new(identity) as Box) + let hardware_identity = + HardwareIdentity::new("/usr/lib/opensc-pkcs11.so", slot, &key_id, || Ok(pin)) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + + let hardware_identity = Arc::new(hardware_identity); + let hardware_identity_clone = hardware_identity.clone(); + let sign_func = move |payload: &[u8]| { + let signature = hardware_identity_clone + .sign_arbitrary(payload) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string()))?; + + let bytes = + signature + .signature + .ok_or(RecoveryError::InvalidSignatureFormat( + "Missing signature bytes".to_ascii_lowercase(), + ))?; + + Ok(bytes) + }; + let sign_func = Arc::new(sign_func); + + let signer = Hsm::new(&hardware_identity.public_key().unwrap(), sign_func)?; + let signer = Arc::new(signer); + + Ok((signer, hardware_identity)) } - SenderOpts::Anonymous => Ok(Box::new(AnonymousIdentity)), } } } From c8a69fa1314e6a0d20a9cfbd04fee82e26939fcf Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 15:37:28 +0100 Subject: [PATCH 67/76] adding back prime support because some hsm's still use it --- Cargo.lock | 2 + rs/nns/handlers/recovery/client/BUILD.bazel | 1 + rs/nns/handlers/recovery/client/Cargo.toml | 1 + .../recovery/client/src/tests/general.rs | 63 +++++++++++++- .../handlers/recovery/interface/BUILD.bazel | 1 + rs/nns/handlers/recovery/interface/Cargo.toml | 1 + .../recovery/interface/src/signing/hsm.rs | 28 +++--- .../recovery/interface/src/signing/mod.rs | 6 +- .../recovery/interface/src/signing/p256.rs | 87 +++++++++++++++++++ 9 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 rs/nns/handlers/recovery/interface/src/signing/p256.rs diff --git a/Cargo.lock b/Cargo.lock index 9706dda6e37..60d075e9f7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10853,6 +10853,7 @@ dependencies = [ "ic-nns-handler-recovery-interface", "ic-secp256k1", "k256 0.13.4", + "p256", "pocket-ic", "serde", "tokio", @@ -10869,6 +10870,7 @@ dependencies = [ "ic-ed25519", "ic-secp256k1", "k256 0.13.4", + "p256", "registry-canister", "serde", ] diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 1eb7958941b..3d6914432db 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -21,6 +21,7 @@ MACRO_DEPENDENCIES = [ DEV_DEPENDENCIES = [ "//packages/pocket-ic", + "@crate_index//:p256", "@crate_index//:tokio", ] diff --git a/rs/nns/handlers/recovery/client/Cargo.toml b/rs/nns/handlers/recovery/client/Cargo.toml index 0f40d583807..8205393879e 100644 --- a/rs/nns/handlers/recovery/client/Cargo.toml +++ b/rs/nns/handlers/recovery/client/Cargo.toml @@ -22,3 +22,4 @@ ic-ed25519.path = "../../../../../packages/ic-ed25519" [dev-dependencies] tokio.workspace = true pocket-ic.path = "../../../../../packages/pocket-ic" +p256.workspace = true diff --git a/rs/nns/handlers/recovery/client/src/tests/general.rs b/rs/nns/handlers/recovery/client/src/tests/general.rs index c6a60204613..24bf36a79cb 100644 --- a/rs/nns/handlers/recovery/client/src/tests/general.rs +++ b/rs/nns/handlers/recovery/client/src/tests/general.rs @@ -1,13 +1,14 @@ use std::sync::Arc; -use ic_agent::identity::{BasicIdentity, Secp256k1Identity}; +use ic_agent::identity::{BasicIdentity, Prime256v1Identity, Secp256k1Identity}; use ic_ed25519::PrivateKey as EdwardPrivateKey; use ic_nns_handler_recovery_interface::{ recovery::RecoveryPayload, - signing::{ed25519::EdwardsCurve, k256::Secp256k1, Verifier}, + signing::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256v1, Verifier}, Ballot, }; use ic_secp256k1::PrivateKey as SecpPrivateKey; +use p256::{elliptic_curve::rand_core::OsRng, SecretKey as p256SecretKey}; use crate::{ implementation::RecoveryCanisterImpl, @@ -160,3 +161,61 @@ async fn can_vote_on_proposals_secp256() { assert!(response.is_ok()); } + +#[tokio::test] +async fn can_place_proposals_prime256() { + let secret_key = p256SecretKey::random(&mut OsRng); + let signing_key = secret_key.clone().into(); + + let signer = Prime256v1::new(signing_key); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = Prime256v1Identity::from_private_key(secret_key); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + let response = client + .submit_new_recovery_proposal(RecoveryPayload::Halt) + .await; + + assert!(response.is_ok()); +} + +#[tokio::test] +async fn can_vote_on_proposals_prime256() { + let secret_key = p256SecretKey::random(&mut OsRng); + let signing_key = secret_key.clone().into(); + + let signer = Prime256v1::new(signing_key); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let identity = Prime256v1Identity::from_private_key(secret_key); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + client + .submit_new_recovery_proposal(RecoveryPayload::Halt) + .await + .unwrap(); + + let response = client.vote_on_latest_proposal(Ballot::Yes).await; + println!("{:?}", response); + + assert!(response.is_ok()); +} diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index 545e3933300..b691d0dc306 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -9,6 +9,7 @@ DEPENDENCIES = [ "@crate_index//:ed25519-dalek", "@crate_index//:hex", "@crate_index//:k256", + "@crate_index//:p256", "//rs/registry/canister", "//rs/types/base_types", "//packages/ic-ed25519", diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index 2579ac07c32..b49d7686b93 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -14,5 +14,6 @@ registry-canister.path = "../../../../registry/canister" ic-base-types.path = "../../../../types/base_types" hex.workspace = true k256.workspace = true +p256.workspace = true ic-secp256k1.path = "../../../../../packages/ic-secp256k1" ic-ed25519.path = "../../../../../packages/ic-ed25519" diff --git a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs index af17c87959d..0d0d2f19f7d 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::RecoveryError; -use super::{ed25519::EdwardsCurve, k256::Secp256k1, Signer, Verifier}; +use super::{ed25519::EdwardsCurve, k256::Secp256k1, p256::Prime256v1, Signer, Verifier}; // From ic-admin pub type SignBytes = @@ -10,7 +10,6 @@ pub type SignBytes = pub struct Hsm { inner_pub_key: Arc, - pub_key: Vec, sign_func: Option, } @@ -20,7 +19,7 @@ impl Verifier for Hsm { } fn to_public_key_der(&self) -> crate::Result> { - Ok(self.pub_key.clone()) + self.inner_pub_key.to_public_key_der() } } @@ -37,13 +36,17 @@ impl Signer for Hsm { } impl Hsm { - pub fn from_public_key(pub_key: &[u8]) -> crate::Result { - if let Ok(edwards) = EdwardsCurve::from_public_key(pub_key) { - return Ok(Self::from_verifier(pub_key, Arc::new(edwards))); + pub fn from_public_key_der(pub_key_der: &[u8]) -> crate::Result { + if let Ok(edwards) = EdwardsCurve::from_public_key_der(pub_key_der) { + return Ok(Self::from_verifier(Arc::new(edwards))); } - if let Ok(secp256) = Secp256k1::from_public_key(pub_key) { - return Ok(Self::from_verifier(pub_key, Arc::new(secp256))); + if let Ok(secp256) = Secp256k1::from_public_key_der(pub_key_der) { + return Ok(Self::from_verifier(Arc::new(secp256))); + } + + if let Ok(prime) = Prime256v1::from_public_key_der(pub_key_der) { + return Ok(Self::from_verifier(Arc::new(prime))); } return Err(RecoveryError::InvalidPubKey( @@ -51,20 +54,19 @@ impl Hsm { )); } - fn from_verifier(pub_key: &[u8], verifier: Arc) -> Self { + fn from_verifier(verifier: Arc) -> Self { Self { sign_func: None, - pub_key: pub_key.to_vec(), inner_pub_key: verifier, } } - pub fn new(pub_key: &[u8], sign_func: SignBytes) -> crate::Result { - let from_pub_key = Self::from_public_key(pub_key)?; + pub fn new(pub_key_der: &[u8], sign_func: SignBytes) -> crate::Result { + let from_pub_key_der = Self::from_public_key_der(pub_key_der)?; Ok(Self { sign_func: Some(sign_func), - ..from_pub_key + ..from_pub_key_der }) } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index f9ff0245397..6bc62a0b90e 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -1,6 +1,6 @@ use ed25519::EdwardsCurve; -use hsm::Hsm; use k256::Secp256k1; +use p256::Prime256v1; use crate::RecoveryError; @@ -10,6 +10,7 @@ pub mod anonymous; pub mod ed25519; pub mod hsm; pub mod k256; +pub mod p256; pub trait Verifier: Send + Sync { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> Result<()>; @@ -29,8 +30,7 @@ pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[ return verify_payload_naive_inner(verifier, payload, signature); } - // Assume it's an hsm which isn't der encoded - if let Ok(verifier) = Hsm::from_public_key(public_key_der) { + if let Ok(verifier) = Prime256v1::from_public_key_der(public_key_der) { return verify_payload_naive_inner(verifier, payload, signature); } diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs new file mode 100644 index 00000000000..82ef1197e02 --- /dev/null +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -0,0 +1,87 @@ +use k256::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePublicKey}; +use p256::ecdsa::signature::Verifier; +use p256::ecdsa::Signature; +use p256::ecdsa::{signature::Signer, SigningKey, VerifyingKey}; + +use crate::RecoveryError; + +pub struct Prime256v1 { + signing_key: Option, + verifying_key: VerifyingKey, +} + +impl super::Verifier for Prime256v1 { + fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> crate::Result<()> { + let signature = Signature::from_slice(signature) + .map_err(|e| RecoveryError::InvalidSignatureFormat(e.to_string()))?; + + self.verifying_key + .verify(payload, &signature) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string())) + } + + fn to_public_key_der(&self) -> crate::Result> { + self.verifying_key + .to_public_key_der() + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + .map(|document| document.into_vec()) + } +} + +impl super::Signer for Prime256v1 { + fn sign_payload(&self, payload: &[u8]) -> crate::Result> { + let signing_key = self + .signing_key + .clone() + .ok_or(RecoveryError::InvalidIdentity( + "Signing key missing".to_string(), + ))?; + + let signature: Signature = signing_key + .try_sign(payload) + .map_err(|e| RecoveryError::InvalidSignature(e.to_string()))?; + + let r = signature.r().to_bytes().to_vec(); + let s = signature.s().to_bytes().to_vec(); + + Ok(r.into_iter().chain(s).collect()) + } +} + +impl Prime256v1 { + pub fn from_public_key_der(public_key_der: &[u8]) -> crate::Result { + let verifying_key = VerifyingKey::from_public_key_der(public_key_der) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } + + pub fn from_public_key(public_key: &[u8]) -> crate::Result { + let verifying_key = VerifyingKey::from_sec1_bytes(public_key) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string()))?; + + Ok(Self { + signing_key: None, + verifying_key, + }) + } + + pub fn new(signing_key: SigningKey) -> Self { + Self { + verifying_key: signing_key.verifying_key().clone(), + signing_key: Some(signing_key), + } + } + + pub fn from_pem(path: &str) -> crate::Result { + let contents = std::fs::read_to_string(path) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + + let signing_key = SigningKey::from_pkcs8_pem(&contents) + .map_err(|e| RecoveryError::InvalidIdentity(e.to_string()))?; + Ok(Self::new(signing_key)) + } +} From f79a95f79012709fcaa0402a245106e5351d83cc Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Sun, 9 Feb 2025 22:30:38 +0100 Subject: [PATCH 68/76] adding prettier serialization --- Cargo.lock | 1 + .../handlers/recovery/interface/BUILD.bazel | 1 + rs/nns/handlers/recovery/interface/Cargo.toml | 1 + .../interface/src/security_metadata.rs | 26 +++++++++++++++++++ .../interface/src/signing/anonymous.rs | 4 +++ .../recovery/interface/src/signing/ed25519.rs | 5 ++++ .../recovery/interface/src/signing/hsm.rs | 4 +++ .../recovery/interface/src/signing/k256.rs | 4 +++ .../recovery/interface/src/signing/mod.rs | 2 ++ .../recovery/interface/src/signing/p256.rs | 6 +++++ 10 files changed, 54 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 60d075e9f7d..135a4762c45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10863,6 +10863,7 @@ dependencies = [ name = "ic-nns-handler-recovery-interface" version = "0.9.0" dependencies = [ + "base64 0.13.1", "candid", "ed25519-dalek", "hex", diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index b691d0dc306..99f8bb5b8b1 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -10,6 +10,7 @@ DEPENDENCIES = [ "@crate_index//:hex", "@crate_index//:k256", "@crate_index//:p256", + "@crate_index//:base64", "//rs/registry/canister", "//rs/types/base_types", "//packages/ic-ed25519", diff --git a/rs/nns/handlers/recovery/interface/Cargo.toml b/rs/nns/handlers/recovery/interface/Cargo.toml index b49d7686b93..3572fab9278 100644 --- a/rs/nns/handlers/recovery/interface/Cargo.toml +++ b/rs/nns/handlers/recovery/interface/Cargo.toml @@ -17,3 +17,4 @@ k256.workspace = true p256.workspace = true ic-secp256k1.path = "../../../../../packages/ic-secp256k1" ic-ed25519.path = "../../../../../packages/ic-ed25519" +base64.workspace = true diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 8cf575970a6..4d70383ba1f 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -13,16 +13,19 @@ pub struct SecurityMetadata { /// /// Should be verified with a corresponding public key (also /// known as verifying key). + #[serde(serialize_with = "base64_serde::serialize")] pub signature: Vec, /// What is being signed. /// /// In context of recovery canister proposal it includes /// all fields in a proposal except the ballots of node operators /// serialized as vector of bytes. + #[serde(serialize_with = "base64_serde::serialize")] pub payload: Vec, /// Der encoded public key. /// /// It is used to verify the authenticity of a signature. + #[serde(serialize_with = "pub_key_serde::serialize")] pub pub_key_der: Vec, } @@ -60,3 +63,26 @@ impl SecurityMetadata { } } } + +mod base64_serde { + use serde::Serializer; + + pub fn serialize(bytes: &Vec, serializer: S) -> core::result::Result + where + S: Serializer, + { + let encoded = base64::encode(bytes); + serializer.serialize_str(&encoded) + } +} + +mod pub_key_serde { + use serde::Serializer; + + pub fn serialize(_bytes: &Vec, serializer: S) -> core::result::Result + where + S: Serializer, + { + serializer.serialize_str("Outout omitted") + } +} diff --git a/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs b/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs index 9fced576217..e912257061f 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/anonymous.rs @@ -10,6 +10,10 @@ impl Verifier for AnonymousSigner { fn to_public_key_der(&self) -> crate::Result> { Ok(vec![]) } + + fn to_public_key_pem(&self) -> crate::Result { + Ok("".to_string()) + } } impl Signer for AnonymousSigner { diff --git a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs index 99419a95d42..cc1052c16b4 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/ed25519.rs @@ -32,6 +32,11 @@ impl super::Verifier for EdwardsCurve { fn to_public_key_der(&self) -> crate::Result> { Ok(self.public_key.serialize_rfc8410_der()) } + + fn to_public_key_pem(&self) -> crate::Result { + String::from_utf8(self.public_key.serialize_rfc8410_pem()) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + } } impl EdwardsCurve { diff --git a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs index 0d0d2f19f7d..8a547309e38 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs @@ -21,6 +21,10 @@ impl Verifier for Hsm { fn to_public_key_der(&self) -> crate::Result> { self.inner_pub_key.to_public_key_der() } + + fn to_public_key_pem(&self) -> crate::Result { + self.inner_pub_key.to_public_key_pem() + } } impl Signer for Hsm { diff --git a/rs/nns/handlers/recovery/interface/src/signing/k256.rs b/rs/nns/handlers/recovery/interface/src/signing/k256.rs index 1e52ace76f6..10af2842a2e 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/k256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/k256.rs @@ -20,6 +20,10 @@ impl super::Verifier for Secp256k1 { fn to_public_key_der(&self) -> crate::Result> { Ok(self.public_key.serialize_der()) } + + fn to_public_key_pem(&self) -> crate::Result { + Ok(self.public_key.serialize_pem()) + } } impl super::Signer for Secp256k1 { diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index 6bc62a0b90e..edfdaff953e 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -16,6 +16,8 @@ pub trait Verifier: Send + Sync { fn verify_payload(&self, payload: &[u8], signature: &[u8]) -> Result<()>; fn to_public_key_der(&self) -> Result>; + + fn to_public_key_pem(&self) -> Result; } pub trait Signer: Verifier + Send + Sync { fn sign_payload(&self, payload: &[u8]) -> Result>; diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index 82ef1197e02..fbaa3d6ab59 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -26,6 +26,12 @@ impl super::Verifier for Prime256v1 { .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) .map(|document| document.into_vec()) } + + fn to_public_key_pem(&self) -> crate::Result { + self.verifying_key + .to_public_key_pem(k256::pkcs8::LineEnding::LF) + .map_err(|e| RecoveryError::InvalidPubKey(e.to_string())) + } } impl super::Signer for Prime256v1 { From a492ec1bcd8678f389de16c602f58624c1b4d217 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 10 Feb 2025 14:27:39 +0100 Subject: [PATCH 69/76] prettier serialization to json --- .../interface/src/security_metadata.rs | 10 ++++++++-- .../recovery/interface/src/signing/mod.rs | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 4d70383ba1f..07ddff21a79 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -79,10 +79,16 @@ mod base64_serde { mod pub_key_serde { use serde::Serializer; - pub fn serialize(_bytes: &Vec, serializer: S) -> core::result::Result + use crate::signing::public_der_to_pem; + + pub fn serialize(bytes: &Vec, serializer: S) -> core::result::Result where S: Serializer, { - serializer.serialize_str("Outout omitted") + let pem = match bytes.is_empty() { + true => "".to_string(), + false => public_der_to_pem(bytes).map_err(serde::ser::Error::custom)?, + }; + serializer.serialize_str(&pem) } } diff --git a/rs/nns/handlers/recovery/interface/src/signing/mod.rs b/rs/nns/handlers/recovery/interface/src/signing/mod.rs index edfdaff953e..42ef40bd389 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/mod.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/mod.rs @@ -41,6 +41,24 @@ pub fn verify_payload_naive(public_key_der: &[u8], payload: &[u8], signature: &[ )) } +pub fn public_der_to_pem(public_key_der: &[u8]) -> Result { + if let Ok(verifier) = EdwardsCurve::from_public_key_der(public_key_der) { + return verifier.to_public_key_pem(); + } + + if let Ok(verifier) = Secp256k1::from_public_key_der(public_key_der) { + return verifier.to_public_key_pem(); + } + + if let Ok(verifier) = Prime256v1::from_public_key_der(public_key_der) { + return verifier.to_public_key_pem(); + } + + Err(RecoveryError::InvalidPubKey( + "Couldn't decode public key der with any known algorithm".to_string(), + )) +} + fn verify_payload_naive_inner( verifier: impl Verifier, payload: &[u8], From 75b945cd8046fb99a76ae8f7e69b77958702eff7 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 10 Feb 2025 14:38:48 +0100 Subject: [PATCH 70/76] adding candid --- .../recovery/impl/canister/canister.rs | 8 ++-- .../recovery/impl/canister/recovery.did | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/rs/nns/handlers/recovery/impl/canister/canister.rs b/rs/nns/handlers/recovery/impl/canister/canister.rs index a5da1fe0ac6..69ae5dd01b3 100644 --- a/rs/nns/handlers/recovery/impl/canister/canister.rs +++ b/rs/nns/handlers/recovery/impl/canister/canister.rs @@ -30,19 +30,19 @@ fn canister_post_upgrade(arg: RecoveryInitArgs) { ic_nervous_system_common_build_metadata::define_get_build_metadata_candid_method_cdk! {} -#[update(hidden = true)] +#[update] async fn submit_new_recovery_proposal( new_recovery_proposal: NewRecoveryProposal, ) -> Result<(), String> { submit_recovery_proposal(new_recovery_proposal, caller()) } -#[update(hidden = true)] +#[update] async fn vote_on_proposal(vote: VoteOnRecoveryProposal) -> Result<(), String> { vote_on_proposal_inner(caller(), vote) } -#[query(hidden = true)] +#[query] fn get_pending_recovery_proposals() -> Vec { get_recovery_proposals() } @@ -105,3 +105,5 @@ async fn setup_node_operator_update(args: Option) { #[cfg(test)] mod tests; + +ic_cdk::export_candid!(); diff --git a/rs/nns/handlers/recovery/impl/canister/recovery.did b/rs/nns/handlers/recovery/impl/canister/recovery.did index e69de29bb2d..97fcc21b986 100644 --- a/rs/nns/handlers/recovery/impl/canister/recovery.did +++ b/rs/nns/handlers/recovery/impl/canister/recovery.did @@ -0,0 +1,47 @@ +type Ballot = variant { No; Yes; Undecided }; +type NewRecoveryProposal = record { + security_metadata : SecurityMetadata; + payload : RecoveryPayload; +}; +type NodeOperatorBallot = record { + "principal" : principal; + ballot : Ballot; + nodes_tied_to_ballot : vec principal; + security_metadata : SecurityMetadata; +}; +type RecoveryInitArgs = record { + initial_node_operator_records : vec SimpleNodeOperatorRecord; +}; +type RecoveryPayload = variant { + DoRecovery : record { height : nat64; state_hash : text }; + Halt; + Unhalt; +}; +type RecoveryProposal = record { + submission_timestamp_seconds : nat64; + proposer : principal; + security_metadata : SecurityMetadata; + payload : RecoveryPayload; + node_operator_ballots : vec NodeOperatorBallot; +}; +type Result = variant { Ok; Err : text }; +type SecurityMetadata = record { + signature : blob; + pub_key_der : blob; + payload : blob; +}; +type SimpleNodeOperatorRecord = record { + operator_id : principal; + nodes : vec principal; +}; +type VoteOnRecoveryProposal = record { + ballot : Ballot; + security_metadata : SecurityMetadata; +}; +service : (RecoveryInitArgs) -> { + get_build_metadata : () -> (text) query; + get_current_nns_node_operators : () -> (vec SimpleNodeOperatorRecord) query; + get_pending_recovery_proposals : () -> (vec RecoveryProposal) query; + submit_new_recovery_proposal : (NewRecoveryProposal) -> (Result); + vote_on_proposal : (VoteOnRecoveryProposal) -> (Result); +} From 093a9e0763ac235c891404316b2ab9a40c53d526 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 10 Feb 2025 15:42:31 +0100 Subject: [PATCH 71/76] adding latest adopted state --- rs/nns/handlers/recovery/client/src/lib.rs | 9 +++++++ .../recovery/impl/canister/recovery.did | 2 +- .../recovery/impl/src/recovery_proposal.rs | 4 +++ .../recovery/interface/src/recovery.rs | 27 ++++++++++++------- rs/registry/admin/src/main.rs | 8 +++--- rs/registry/admin/src/recovery_canister.rs | 15 +++++++++++ 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/lib.rs b/rs/nns/handlers/recovery/client/src/lib.rs index 53763ea864e..9f4810a3cc0 100644 --- a/rs/nns/handlers/recovery/client/src/lib.rs +++ b/rs/nns/handlers/recovery/client/src/lib.rs @@ -44,4 +44,13 @@ pub trait RecoveryCanister { "No voted in proposals present at the moment".to_string(), )) } + + async fn latest_adopted_state(&self) -> RecoveryPayload { + self.fetch_latest_adopted_proposal() + .await + .map(|proposal| proposal.payload) + // If there isn't any voted in proposals look + // at NNS as unhalted. + .unwrap_or(RecoveryPayload::Unhalt) + } } diff --git a/rs/nns/handlers/recovery/impl/canister/recovery.did b/rs/nns/handlers/recovery/impl/canister/recovery.did index 97fcc21b986..853f3b90ca8 100644 --- a/rs/nns/handlers/recovery/impl/canister/recovery.did +++ b/rs/nns/handlers/recovery/impl/canister/recovery.did @@ -13,7 +13,7 @@ type RecoveryInitArgs = record { initial_node_operator_records : vec SimpleNodeOperatorRecord; }; type RecoveryPayload = variant { - DoRecovery : record { height : nat64; state_hash : text }; + DoRecovery : record { height : nat64; state_hash : text; time_ns : nat64 }; Halt; Unhalt; }; diff --git a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs index 9a2f8109de2..15cc012da9d 100644 --- a/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs +++ b/rs/nns/handlers/recovery/impl/src/recovery_proposal.rs @@ -95,6 +95,7 @@ pub fn submit_recovery_proposal( RecoveryPayload::DoRecovery { height: _, state_hash: _, + time_ns: _, } | RecoveryPayload::Unhalt => { proposals.push(RecoveryProposal { @@ -133,6 +134,7 @@ pub fn submit_recovery_proposal( RecoveryPayload::DoRecovery { height: _, state_hash: _, + time_ns: _, }, RecoveryPayload::Unhalt, ) => { @@ -151,10 +153,12 @@ pub fn submit_recovery_proposal( RecoveryPayload::DoRecovery { height: _, state_hash: _, + time_ns: _, }, RecoveryPayload::DoRecovery { height: _, state_hash: _, + time_ns: _, }, ) => { // Remove the second_one diff --git a/rs/nns/handlers/recovery/interface/src/recovery.rs b/rs/nns/handlers/recovery/interface/src/recovery.rs index a1f6ddf6c4a..419343636b8 100644 --- a/rs/nns/handlers/recovery/interface/src/recovery.rs +++ b/rs/nns/handlers/recovery/interface/src/recovery.rs @@ -24,7 +24,11 @@ pub enum RecoveryPayload { /// If adopted, the orchestrator's watching recovery canister /// should perform a recovery based on the provided information. /// This proposal maps to a proposal similar to [134629](https://dashboard.internetcomputer.org/proposal/134629). - DoRecovery { height: u64, state_hash: String }, + DoRecovery { + height: u64, + state_hash: String, + time_ns: u64, + }, /// Unhalt NNS. /// /// If adopted, the orchestrator's watching the recovery canister @@ -156,11 +160,11 @@ fn nns_principal_id() -> PrincipalId { .expect("Should be a valid NNS id") } -impl TryFrom for UpdateSubnetPayload { +impl TryFrom for UpdateSubnetPayload { type Error = RecoveryError; - fn try_from(value: RecoveryProposal) -> std::result::Result { - match value.payload { + fn try_from(value: RecoveryPayload) -> std::result::Result { + match value { RecoveryPayload::Halt => Ok(Self { chain_key_config: None, chain_key_signing_disable: None, @@ -231,16 +235,19 @@ impl TryFrom for UpdateSubnetPayload { } } -impl TryFrom for RecoverSubnetPayload { +impl TryFrom for RecoverSubnetPayload { type Error = RecoveryError; - fn try_from(value: RecoveryProposal) -> std::result::Result { - match value.payload { - RecoveryPayload::DoRecovery { height, state_hash } => Ok(Self { + fn try_from(value: RecoveryPayload) -> std::result::Result { + match value { + RecoveryPayload::DoRecovery { + height, + state_hash, + time_ns, + } => Ok(Self { subnet_id: nns_principal_id(), height, - // TODO: Migrate timestamps to nanoseconds in canister - time_ns: value.submission_timestamp_seconds, + time_ns, state_hash: hex::decode(state_hash).map_err(|e| { RecoveryError::PayloadSerialization(format!( "Cannot deserialize state hash into a byte vector: {}", diff --git a/rs/registry/admin/src/main.rs b/rs/registry/admin/src/main.rs index 32c79cf0ebb..f8b7af3cd09 100644 --- a/rs/registry/admin/src/main.rs +++ b/rs/registry/admin/src/main.rs @@ -376,6 +376,9 @@ enum SubCommand { /// Get node operators enlisted in the recovery canister GetRecoveryCanisterNodeOperators, + /// List the active status of the NNS + GetRecoveryCanisterLatestState, + /// Propose to halt NNS RecoveryCanisterProposeHalt, @@ -4980,9 +4983,8 @@ async fn main() { | SubCommand::RecoveryCanisterProposeHalt | SubCommand::RecoveryCanisterProposeUnhalt | SubCommand::RecoveryCanisterProposeRecovery(_) - | SubCommand::RecoveryCanisterVoteOnLatestProposal(_) => { - recovery_canister::execute(&opts).await - } + | SubCommand::RecoveryCanisterVoteOnLatestProposal(_) + | SubCommand::GetRecoveryCanisterLatestState => recovery_canister::execute(&opts).await, } } diff --git a/rs/registry/admin/src/recovery_canister.rs b/rs/registry/admin/src/recovery_canister.rs index 2a81166e3b0..5e1416a5bcc 100644 --- a/rs/registry/admin/src/recovery_canister.rs +++ b/rs/registry/admin/src/recovery_canister.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + use clap::Args; use ic_nns_handler_recovery_client::{ builder::{RecoveryCanisterBuilder, SenderOpts}, @@ -12,6 +14,12 @@ use crate::{Opts, SubCommand}; pub struct RecoveryCanisterProposeRecoveryCmd { height: u64, state_hash: String, + #[clap(default_value_t = now())] + time_ns: u64, +} + +fn now() -> u64 { + SystemTime::UNIX_EPOCH.elapsed().unwrap().as_secs() } #[derive(Args)] @@ -23,6 +31,12 @@ pub async fn execute(opts: &Opts) { let client = build_client(opts); let response = match &opts.subcmd { + SubCommand::GetRecoveryCanisterLatestState => { + let latest_state = client.latest_adopted_state().await; + + println!("Latest state: {:?}", latest_state); + return; + } SubCommand::GetRecoveryCanisterNodeOperators => { let response = client.get_node_operators_in_nns().await.unwrap(); @@ -50,6 +64,7 @@ pub async fn execute(opts: &Opts) { .submit_new_recovery_proposal(RecoveryPayload::DoRecovery { height: cmd.height, state_hash: cmd.state_hash.clone(), + time_ns: cmd.time_ns, }) .await } From 5e0fc46260d05e1dd376c53ae7a33b68aac46633 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 10 Feb 2025 16:02:26 +0100 Subject: [PATCH 72/76] adding tests --- .../client/src/tests/latest_adopoted_state.rs | 116 ++++++++++++++++++ .../handlers/recovery/client/src/tests/mod.rs | 1 + 2 files changed, 117 insertions(+) create mode 100644 rs/nns/handlers/recovery/client/src/tests/latest_adopoted_state.rs diff --git a/rs/nns/handlers/recovery/client/src/tests/latest_adopoted_state.rs b/rs/nns/handlers/recovery/client/src/tests/latest_adopoted_state.rs new file mode 100644 index 00000000000..6781f57b4de --- /dev/null +++ b/rs/nns/handlers/recovery/client/src/tests/latest_adopoted_state.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use ic_agent::identity::BasicIdentity; +use ic_ed25519::PrivateKey; +use ic_nns_handler_recovery_interface::{ + recovery::RecoveryPayload, + signing::{ed25519::EdwardsCurve, Verifier}, + Ballot, +}; +use pocket_ic::nonblocking::PocketIc; + +use crate::{implementation::RecoveryCanisterImpl, RecoveryCanister}; + +use super::{ + generate_node_operators, get_ic_agent, init_pocket_ic, preconfigured_recovery_init_args, +}; + +async fn prepare_client() -> (PocketIc, RecoveryCanisterImpl) { + let key = PrivateKey::generate(); + let signer = EdwardsCurve::new(key.clone()); + + let node_operators_with_keys = + generate_node_operators(vec![signer.to_public_key_der().unwrap()]); + let (pic, canister) = + init_pocket_ic(preconfigured_recovery_init_args(&node_operators_with_keys)).await; + + let pem = key.serialize_pkcs8_pem(ic_ed25519::PrivateKeyFormat::Pkcs8v1); + let pem = pem.as_bytes(); + let identity = BasicIdentity::from_pem(pem).unwrap(); + + let client = RecoveryCanisterImpl::new( + get_ic_agent(Box::new(identity), pic.url().unwrap().as_str()).await, + canister, + Arc::new(signer), + ); + + (pic, client) +} + +async fn place_proposal(client: &RecoveryCanisterImpl, payload: RecoveryPayload) { + let response = client.submit_new_recovery_proposal(payload).await; + assert!(response.is_ok()); +} + +async fn place_and_vote_in_proposal( + client: &RecoveryCanisterImpl, + payload: RecoveryPayload, + ballot: Ballot, +) { + place_proposal(client, payload).await; + let response = client.vote_on_latest_proposal(ballot).await; + assert!(response.is_ok()) +} + +#[tokio::test] +async fn no_proposals() { + let (_, client) = prepare_client().await; + + let latest_state = client.latest_adopted_state().await; + + assert_eq!(latest_state, RecoveryPayload::Unhalt) +} + +#[tokio::test] +async fn placed_proposal_for_halt() { + let (_, client) = prepare_client().await; + + place_proposal(&client, RecoveryPayload::Halt).await; + let latest_state = client.latest_adopted_state().await; + + assert_eq!(latest_state, RecoveryPayload::Unhalt) +} + +#[tokio::test] +async fn voted_in_halt_proposal() { + let (_, client) = prepare_client().await; + place_and_vote_in_proposal(&client, RecoveryPayload::Halt, Ballot::Yes).await; + + let latest_state = client.latest_adopted_state().await; + assert_eq!(latest_state, RecoveryPayload::Halt); +} + +#[tokio::test] +async fn voted_in_recovery() { + let (_, client) = prepare_client().await; + place_and_vote_in_proposal(&client, RecoveryPayload::Halt, Ballot::Yes).await; + let payload = RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + time_ns: 123, + }; + place_and_vote_in_proposal(&client, payload.clone(), Ballot::Yes).await; + + let latest_state = client.latest_adopted_state().await; + assert_eq!(latest_state, payload); +} + +#[tokio::test] +async fn voted_in_unhalt() { + let (_, client) = prepare_client().await; + place_and_vote_in_proposal(&client, RecoveryPayload::Halt, Ballot::Yes).await; + place_and_vote_in_proposal( + &client, + RecoveryPayload::DoRecovery { + height: 123, + state_hash: "123".to_string(), + time_ns: 123, + }, + Ballot::Yes, + ) + .await; + place_and_vote_in_proposal(&client, RecoveryPayload::Unhalt, Ballot::Yes).await; + + let latest_state = client.latest_adopted_state().await; + assert_eq!(latest_state, RecoveryPayload::Unhalt); +} diff --git a/rs/nns/handlers/recovery/client/src/tests/mod.rs b/rs/nns/handlers/recovery/client/src/tests/mod.rs index c925b18b729..7e2b893f01c 100644 --- a/rs/nns/handlers/recovery/client/src/tests/mod.rs +++ b/rs/nns/handlers/recovery/client/src/tests/mod.rs @@ -9,6 +9,7 @@ use ic_nns_handler_recovery_interface::{ use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder}; mod general; +mod latest_adopoted_state; fn fetch_canister_wasm(env: &str) -> Vec { let path: PathBuf = std::env::var(env) From df2ba5692af9b7bdf7d5df030811a8e2ebc4df25 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 10 Feb 2025 16:29:04 +0100 Subject: [PATCH 73/76] clippy and ownership changes --- .github/CODEOWNERS | 1 + Cargo.Bazel.Fuzzing.json.lock | 12 +++---- Cargo.Bazel.Fuzzing.toml.lock | 2 +- bazel/external_crates.bzl | 2 +- rs/nns/handlers/recovery/client/BUILD.bazel | 16 ++++----- rs/nns/handlers/recovery/impl/BUILD.bazel | 34 +++++++++---------- .../canister/tests/proposal_logic_tests.rs | 9 +++++ .../handlers/recovery/interface/BUILD.bazel | 14 ++++---- .../interface/src/security_metadata.rs | 4 +-- .../recovery/interface/src/signing/hsm.rs | 4 +-- .../recovery/interface/src/signing/p256.rs | 2 +- rs/registry/admin/BUILD.bazel | 4 +-- rs/registry/admin/src/main.rs | 6 ++-- rs/registry/canister/BUILD.bazel | 2 +- 14 files changed, 60 insertions(+), 52 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e8bbe128d01..8ef58a126fb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -164,6 +164,7 @@ go_deps.bzl @dfinity/idx /rs/monitoring/pprof/ @dfinity/consensus @dfinity/ic-message-routing-owners /rs/nervous_system/ @dfinity/nns-team /rs/nns/ @dfinity/nns-team +/rs/nns/handlers/recovery @dfinity/dre /rs/orchestrator/ @dfinity/consensus /rs/orchestrator/src/hostos_upgrade.rs @dfinity/consensus @dfinity/node /rs/p2p/ @dfinity/consensus diff --git a/Cargo.Bazel.Fuzzing.json.lock b/Cargo.Bazel.Fuzzing.json.lock index 5fe7c0e9081..f2df1c83cb6 100644 --- a/Cargo.Bazel.Fuzzing.json.lock +++ b/Cargo.Bazel.Fuzzing.json.lock @@ -1,5 +1,5 @@ { - "checksum": "68ccb790390da618288579f1fed367dcc7239797ac7fde0b5e06e1e051a14a8a", + "checksum": "16d0eb2166458263a56ae3cc3dab0b8ac6dfb6c1d217c06d46674723608d9c0c", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -19967,6 +19967,10 @@ "id": "ic-http-gateway 0.1.0", "target": "ic_http_gateway" }, + { + "id": "ic-identity-hsm 0.39.2", + "target": "ic_identity_hsm" + }, { "id": "ic-metrics-encoder 1.1.1", "target": "ic_metrics_encoder" @@ -20553,10 +20557,6 @@ "id": "socket2 0.5.7", "target": "socket2" }, - { - "id": "spki 0.7.3", - "target": "spki" - }, { "id": "ssh2 0.9.4", "target": "ssh2" @@ -91433,6 +91433,7 @@ "ic-certified-map 0.3.4", "ic-http-certification 3.0.2", "ic-http-gateway 0.1.0", + "ic-identity-hsm 0.39.2", "ic-metrics-encoder 1.1.1", "ic-response-verification 3.0.2", "ic-sha3 1.0.0", @@ -91585,7 +91586,6 @@ "slog-scope 4.4.0", "slog-term 2.9.1", "socket2 0.5.7", - "spki 0.7.3", "ssh2 0.9.4", "static_assertions 1.1.0", "strum 0.26.3", diff --git a/Cargo.Bazel.Fuzzing.toml.lock b/Cargo.Bazel.Fuzzing.toml.lock index dceb261db0f..5a4b76786e8 100644 --- a/Cargo.Bazel.Fuzzing.toml.lock +++ b/Cargo.Bazel.Fuzzing.toml.lock @@ -3299,6 +3299,7 @@ dependencies = [ "ic-certified-map", "ic-http-certification", "ic-http-gateway", + "ic-identity-hsm", "ic-metrics-encoder", "ic-response-verification", "ic-sha3", @@ -3451,7 +3452,6 @@ dependencies = [ "slog-scope", "slog-term", "socket2 0.5.7", - "spki 0.7.3", "ssh2", "static_assertions", "strum 0.26.3", diff --git a/bazel/external_crates.bzl b/bazel/external_crates.bzl index e03896daa9a..4576f47e2f3 100644 --- a/bazel/external_crates.bzl +++ b/bazel/external_crates.bzl @@ -574,7 +574,7 @@ def external_crates_repository(name, cargo_lockfile, lockfile, sanitizers_enable features = ["pem", "ring"], ), "ic-identity-hsm": crate.spec( - version = "^0.39.2" + version = "^0.39.2", ), "ic-bn-lib": crate.spec( git = "https://github.com/dfinity/ic-bn-lib", diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 3d6914432db..258f517b299 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -4,15 +4,15 @@ package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ # Keep sorted. - "//rs/nns/handlers/recovery/interface", "//packages/ic-ed25519", "//packages/ic-secp256k1", + "//rs/nns/handlers/recovery/interface", "@crate_index//:candid", - "@crate_index//:serde", + "@crate_index//:ed25519-dalek", "@crate_index//:ic-agent", "@crate_index//:ic-identity-hsm", - "@crate_index//:ed25519-dalek", "@crate_index//:k256", + "@crate_index//:serde", ] MACRO_DEPENDENCIES = [ @@ -42,16 +42,16 @@ rust_library( rust_test( name = "client-tests", srcs = glob(["src/**/*.rs"]), + aliases = ALIASES, data = [ "//rs/nns/handlers/recovery/impl:recovery-canister", "//rs/pocket_ic_server:pocket-ic-server", ], - aliases = ALIASES, - proc_macro_deps = MACRO_DEPENDENCIES, - version = "0.1.0", env = { "RECOVERY_WASM_PATH": "$(rootpath //rs/nns/handlers/recovery/impl:recovery-canister)", - "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" + "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)", }, - deps = DEV_DEPENDENCIES + DEPENDENCIES + [":client"] + proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.1.0", + deps = DEV_DEPENDENCIES + DEPENDENCIES + [":client"], ) diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index 94db2f9bd91..489f7ca9113 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -1,8 +1,6 @@ load("@rules_rust//cargo:defs.bzl", "cargo_build_script") -load("@rules_rust//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") load("//bazel:canisters.bzl", "rust_canister") -load("//bazel:defs.bzl", "rust_ic_test_suite") -load("//bazel:prost.bzl", "generated_files_check") package(default_visibility = ["//visibility:public"]) @@ -19,6 +17,8 @@ BASE_DEPENDENCIES = [ "//rs/nervous_system/runtime", "//rs/nns/common", "//rs/nns/constants", + "//rs/nns/handlers/recovery/interface", + "//rs/nns/handlers/root/impl:root", "//rs/nns/handlers/root/interface", "//rs/protobuf", "//rs/registry/keys", @@ -29,21 +29,19 @@ BASE_DEPENDENCIES = [ "//rs/rust_canisters/on_wire", "//rs/types/base_types", "//rs/types/management_canister_types", - "//rs/nns/handlers/root/impl:root", - "//rs/nns/handlers/recovery/interface", "@crate_index//:build-info", - "@crate_index//:ic-cdk-timers", "@crate_index//:candid", + "@crate_index//:ed25519-dalek", "@crate_index//:ic-cdk", + "@crate_index//:ic-cdk-timers", "@crate_index//:ic-metrics-encoder", + "@crate_index//:itertools", "@crate_index//:lazy_static", "@crate_index//:maplit", "@crate_index//:prost", + "@crate_index//:rand", "@crate_index//:serde", "@crate_index//:serde_bytes", - "@crate_index//:ed25519-dalek", - "@crate_index//:rand", - "@crate_index//:itertools", ] # Each target declared in this file may choose either these (release-ready) @@ -148,23 +146,23 @@ rust_canister( rust_test( name = "recovery-canister-tests", srcs = glob(["canister/**/*.rs"]), + crate_root = "canister/canister.rs", data = [ ":recovery-canister", "//rs/pocket_ic_server:pocket-ic-server", - "//rs/registry/canister:registry-canister" + "//rs/registry/canister:registry-canister", ], - crate_root = "canister/canister.rs", + env = { + "RECOVERY_WASM_PATH": "$(rootpath :recovery-canister)", + "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", + "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)", + }, proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.9.0", deps = DEPENDENCIES + DEV_DEPENDENCIES + [ ":build_script", ":recovery", "//packages/pocket-ic", - "//rs/registry/subnet_type" + "//rs/registry/subnet_type", ], - env = { - "RECOVERY_WASM_PATH": "$(rootpath :recovery-canister)", - "REGISTRY_WASM_PATH": "$(rootpath //rs/registry/canister:registry-canister)", - "POCKET_IC_BIN": "$(rootpath //rs/pocket_ic_server:pocket-ic-server)" - }, - version = "0.9.0" ) diff --git a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs index 543fa665b6f..9a000495ff1 100644 --- a/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs +++ b/rs/nns/handlers/recovery/impl/canister/tests/proposal_logic_tests.rs @@ -69,6 +69,7 @@ fn place_non_halt_first_proposal() { RecoveryPayload::DoRecovery { height: 123, state_hash: "123".to_string(), + time_ns: 123, }, RecoveryPayload::Unhalt, ]; @@ -197,6 +198,7 @@ fn place_second_proposal_recovery() { let new_proposal = RecoveryPayload::DoRecovery { height: 123, state_hash: "123".to_string(), + time_ns: 123, }; let response = submit_proposal(&pic, canister, first, new_proposal.clone()); assert!(response.is_ok()); @@ -255,6 +257,7 @@ fn second_proposal_vote_against() { // Place the second let new_proposal = RecoveryPayload::DoRecovery { height: 123, + time_ns: 123, state_hash: "123".to_string(), }; let response = submit_proposal(&pic, canister, first, new_proposal.clone()); @@ -284,6 +287,7 @@ fn second_proposal_recovery_vote_in() { // Place the second let new_proposal = RecoveryPayload::DoRecovery { height: 123, + time_ns: 123, state_hash: "123".to_string(), }; let response = submit_proposal(&pic, canister, first, new_proposal.clone()); @@ -315,6 +319,7 @@ fn second_proposal_recovery_vote_in_and_resubmit() { // Place the second let new_proposal = RecoveryPayload::DoRecovery { height: 123, + time_ns: 123, state_hash: "123".to_string(), }; let response = submit_proposal(&pic, canister, first, new_proposal.clone()); @@ -330,6 +335,7 @@ fn second_proposal_recovery_vote_in_and_resubmit() { let resubmitted_proposal = RecoveryPayload::DoRecovery { height: 456, + time_ns: 123, state_hash: "456".to_string(), }; let response = submit_proposal(&pic, canister, first, resubmitted_proposal.clone()); @@ -382,6 +388,7 @@ fn submit_first_two_second_not_voted_in_place_third() { // Place the second let new_proposal = RecoveryPayload::DoRecovery { height: 123, + time_ns: 123, state_hash: "123".to_string(), }; let response = submit_proposal(&pic, canister, first, new_proposal.clone()); @@ -405,6 +412,7 @@ fn place_and_execute_second_proposal( // Place the second let new_proposal = RecoveryPayload::DoRecovery { height: 123, + time_ns: 123, state_hash: "123".to_string(), }; let response = submit_proposal(&pic, canister, first, new_proposal.clone()); @@ -519,6 +527,7 @@ fn place_any_proposal_after_there_are_three() { RecoveryPayload::DoRecovery { height: 123, state_hash: "123".to_string(), + time_ns: 123, }, RecoveryPayload::Unhalt, ]; diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index 99f8bb5b8b1..ac425bd2674 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -1,20 +1,20 @@ -load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") +load("@rules_rust//rust:defs.bzl", "rust_library") package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ # Keep sorted. + "//packages/ic-ed25519", + "//packages/ic-secp256k1", + "//rs/registry/canister", + "//rs/types/base_types", + "@crate_index//:base64", "@crate_index//:candid", - "@crate_index//:serde", "@crate_index//:ed25519-dalek", "@crate_index//:hex", "@crate_index//:k256", "@crate_index//:p256", - "@crate_index//:base64", - "//rs/registry/canister", - "//rs/types/base_types", - "//packages/ic-ed25519", - "//packages/ic-secp256k1", + "@crate_index//:serde", ] MACRO_DEPENDENCIES = [] diff --git a/rs/nns/handlers/recovery/interface/src/security_metadata.rs b/rs/nns/handlers/recovery/interface/src/security_metadata.rs index 07ddff21a79..8f11f68d12e 100644 --- a/rs/nns/handlers/recovery/interface/src/security_metadata.rs +++ b/rs/nns/handlers/recovery/interface/src/security_metadata.rs @@ -67,7 +67,7 @@ impl SecurityMetadata { mod base64_serde { use serde::Serializer; - pub fn serialize(bytes: &Vec, serializer: S) -> core::result::Result + pub fn serialize(bytes: &[u8], serializer: S) -> core::result::Result where S: Serializer, { @@ -81,7 +81,7 @@ mod pub_key_serde { use crate::signing::public_der_to_pem; - pub fn serialize(bytes: &Vec, serializer: S) -> core::result::Result + pub fn serialize(bytes: &[u8], serializer: S) -> core::result::Result where S: Serializer, { diff --git a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs index 8a547309e38..7fcd7bd63ec 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/hsm.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/hsm.rs @@ -53,9 +53,9 @@ impl Hsm { return Ok(Self::from_verifier(Arc::new(prime))); } - return Err(RecoveryError::InvalidPubKey( + Err(RecoveryError::InvalidPubKey( "Key stored on hsm implements an unknown algorithm".to_string(), - )); + )) } fn from_verifier(verifier: Arc) -> Self { diff --git a/rs/nns/handlers/recovery/interface/src/signing/p256.rs b/rs/nns/handlers/recovery/interface/src/signing/p256.rs index fbaa3d6ab59..8e342dcea15 100644 --- a/rs/nns/handlers/recovery/interface/src/signing/p256.rs +++ b/rs/nns/handlers/recovery/interface/src/signing/p256.rs @@ -77,7 +77,7 @@ impl Prime256v1 { pub fn new(signing_key: SigningKey) -> Self { Self { - verifying_key: signing_key.verifying_key().clone(), + verifying_key: *signing_key.verifying_key(), signing_key: Some(signing_key), } } diff --git a/rs/registry/admin/BUILD.bazel b/rs/registry/admin/BUILD.bazel index 629f9ba70dd..46c50a72080 100644 --- a/rs/registry/admin/BUILD.bazel +++ b/rs/registry/admin/BUILD.bazel @@ -22,6 +22,8 @@ DEPENDENCIES = [ "//rs/nns/common", "//rs/nns/constants", "//rs/nns/governance/api", + "//rs/nns/handlers/recovery/client", + "//rs/nns/handlers/recovery/interface", "//rs/nns/handlers/root/impl:root", "//rs/nns/init", "//rs/nns/sns-wasm", @@ -44,8 +46,6 @@ DEPENDENCIES = [ "//rs/sns/swap", "//rs/types/management_canister_types", "//rs/types/types", - "//rs/nns/handlers/recovery/client", - "//rs/nns/handlers/recovery/interface", "@crate_index//:anyhow", "@crate_index//:base64", "@crate_index//:candid", diff --git a/rs/registry/admin/src/main.rs b/rs/registry/admin/src/main.rs index f8b7af3cd09..6dda6f33996 100644 --- a/rs/registry/admin/src/main.rs +++ b/rs/registry/admin/src/main.rs @@ -3749,13 +3749,13 @@ async fn main() { Sender::SigKeys(sig_keys) } else if opts.use_hsm { make_hsm_sender( - &opts.hsm_slot.as_ref().expect( + opts.hsm_slot.as_ref().expect( "HSM slot must also be provided for --use-hsm; use --hsm-slot or see --help.", ), - &opts.hsm_key_id.as_ref().expect( + opts.hsm_key_id.as_ref().expect( "HSM key ID must also be provided for --use-hsm; use --key-id or see --help.", ), - &opts.hsm_pin.as_ref().expect( + opts.hsm_pin.as_ref().expect( "HSM pin must also be provided for --use-hsm; use --pin or see --help.", ), ) diff --git a/rs/registry/canister/BUILD.bazel b/rs/registry/canister/BUILD.bazel index 2de550e129e..c59280706ca 100644 --- a/rs/registry/canister/BUILD.bazel +++ b/rs/registry/canister/BUILD.bazel @@ -132,7 +132,7 @@ rust_library( name = "canister--test_feature", srcs = LIB_SRCS, aliases = ALIASES, - crate_features = [ "test" ], + crate_features = ["test"], crate_name = "registry_canister", proc_macro_deps = MACRO_DEPENDENCIES, version = "0.9.0", From f30bd7163fc6d0c87aecbe33492f0b1be34e9f89 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Mon, 10 Feb 2025 16:36:18 +0100 Subject: [PATCH 74/76] fixing name of bazel target --- rs/nns/handlers/recovery/impl/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index 489f7ca9113..97eae4d04fb 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -128,7 +128,7 @@ rust_library( rust_test( name = "recovery_test", srcs = glob(["src/**/*.rs"]), - deps = [":root--test_feature"] + DEPENDENCIES + DEV_DEPENDENCIES, + deps = [":recovery--test_feature"] + DEPENDENCIES + DEV_DEPENDENCIES, ) rust_canister( From d487f3ea1c68a8ad5467171a41d3f372ac4f9ad6 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 11 Feb 2025 12:16:22 +0100 Subject: [PATCH 75/76] buildifier changes --- rs/nns/handlers/recovery/client/BUILD.bazel | 2 -- rs/nns/handlers/recovery/impl/BUILD.bazel | 2 -- rs/nns/handlers/recovery/interface/BUILD.bazel | 4 ---- 3 files changed, 8 deletions(-) diff --git a/rs/nns/handlers/recovery/client/BUILD.bazel b/rs/nns/handlers/recovery/client/BUILD.bazel index 258f517b299..004b248a0cb 100644 --- a/rs/nns/handlers/recovery/client/BUILD.bazel +++ b/rs/nns/handlers/recovery/client/BUILD.bazel @@ -25,8 +25,6 @@ DEV_DEPENDENCIES = [ "@crate_index//:tokio", ] -MACRO_DEV_DEPENDENCIES = [] - ALIASES = {} rust_library( diff --git a/rs/nns/handlers/recovery/impl/BUILD.bazel b/rs/nns/handlers/recovery/impl/BUILD.bazel index 97eae4d04fb..695eaf2926f 100644 --- a/rs/nns/handlers/recovery/impl/BUILD.bazel +++ b/rs/nns/handlers/recovery/impl/BUILD.bazel @@ -85,8 +85,6 @@ DEV_DEPENDENCIES = [ ], }) -MACRO_DEV_DEPENDENCIES = [] - LIB_SRCS = glob( ["src/**"], exclude = [ diff --git a/rs/nns/handlers/recovery/interface/BUILD.bazel b/rs/nns/handlers/recovery/interface/BUILD.bazel index ac425bd2674..eb10046a67a 100644 --- a/rs/nns/handlers/recovery/interface/BUILD.bazel +++ b/rs/nns/handlers/recovery/interface/BUILD.bazel @@ -19,10 +19,6 @@ DEPENDENCIES = [ MACRO_DEPENDENCIES = [] -DEV_DEPENDENCIES = [] - -MACRO_DEV_DEPENDENCIES = [] - ALIASES = {} rust_library( From 58687da2137595d55bb914a242a66f6dfc7c92b3 Mon Sep 17 00:00:00 2001 From: nikolamilosa Date: Tue, 11 Feb 2025 13:47:32 +0100 Subject: [PATCH 76/76] changing builder --- rs/nns/handlers/recovery/client/src/builder.rs | 6 +++--- rs/registry/admin/src/recovery_canister.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/nns/handlers/recovery/client/src/builder.rs b/rs/nns/handlers/recovery/client/src/builder.rs index 1ec93fcd967..4895c62360a 100644 --- a/rs/nns/handlers/recovery/client/src/builder.rs +++ b/rs/nns/handlers/recovery/client/src/builder.rs @@ -46,17 +46,17 @@ impl Default for RecoveryCanisterBuilder { } impl RecoveryCanisterBuilder { - pub fn with_url(&mut self, url: &str) -> &mut Self { + pub fn with_url(mut self, url: &str) -> Self { self.url = url.to_string(); self } - pub fn with_canister_id(&mut self, canister_id: &str) -> &mut Self { + pub fn with_canister_id(mut self, canister_id: &str) -> Self { self.canister_id = canister_id.to_string(); self } - pub fn with_sender(&mut self, sender: SenderOpts) -> &mut Self { + pub fn with_sender(mut self, sender: SenderOpts) -> Self { self.sender = sender; self } diff --git a/rs/registry/admin/src/recovery_canister.rs b/rs/registry/admin/src/recovery_canister.rs index 5e1416a5bcc..82a77558b5d 100644 --- a/rs/registry/admin/src/recovery_canister.rs +++ b/rs/registry/admin/src/recovery_canister.rs @@ -106,10 +106,10 @@ pub fn build_client(opts: &Opts) -> RecoveryCanisterImpl { SenderOpts::Anonymous }; - builder.with_sender(sender); + builder = builder.with_sender(sender); if !opts.nns_urls.is_empty() { - builder.with_url(opts.nns_urls.first().unwrap().as_str()); + builder = builder.with_url(opts.nns_urls.first().unwrap().as_str()); } builder.build().unwrap()