From 39e9586ed8e35f6cb21ffeeea0fc59b5e62a53c0 Mon Sep 17 00:00:00 2001 From: David Frank Date: Fri, 11 Sep 2026 14:09:52 +0200 Subject: [PATCH 01/21] feat: Fast upgrades: upgrade permit shares, pool, and pool manager First step of the Phase-2 rolling-reboot consensus protocol, which adds the data types and the artifact mechanism. * Add `UpgradePermitAction` (`Request` / `Authorize` / `Return`). `UpgradePermitAuthorizationRequest` is the signed content, `UpgradePermitAuthorizationShare` is the gossiped artifact. * Add the in-memory `UpgradePermitAuthPoolImpl` which is modeled after other pools. * Add the `ic-consensus-upgrade` crate with `UpgradePermitAuthPoolManager`. It signs shares for requests in finalized blocks and validates gossiped shares. * Register the new proto file with the generator and add `UpgradePermitAuthorizationRequest` (signing) and `UpgradePermitAuthorizationShare` (pool-ID hashing) domain separators. --- Cargo.lock | 19 + Cargo.toml | 1 + rs/artifact_pool/src/lib.rs | 1 + .../src/upgrade_permit_auth_pool.rs | 311 ++++++++ rs/consensus/mocks/src/lib.rs | 7 + rs/consensus/src/consensus/payload_builder.rs | 1 + rs/consensus/upgrade/BUILD.bazel | 44 ++ rs/consensus/upgrade/Cargo.toml | 24 + rs/consensus/upgrade/src/lib.rs | 207 ++++++ rs/consensus/upgrade/src/pool_manager.rs | 676 ++++++++++++++++++ rs/consensus/utils/src/crypto.rs | 8 +- rs/interfaces/mocks/src/crypto.rs | 48 +- rs/interfaces/src/consensus.rs | 2 + rs/interfaces/src/crypto.rs | 7 +- rs/interfaces/src/lib.rs | 1 + rs/interfaces/src/p2p/consensus.rs | 2 +- rs/interfaces/src/upgrade.rs | 55 ++ rs/protobuf/def/types/v1/artifact.proto | 5 + rs/protobuf/def/types/v1/consensus.proto | 2 + rs/protobuf/def/types/v1/upgrade.proto | 39 + rs/protobuf/generator/src/lib.rs | 1 + rs/protobuf/src/gen/types/types.v1.rs | 57 ++ rs/state_machine_tests/src/lib.rs | 1 + rs/test_utilities/types/src/batch/payload.rs | 1 + rs/types/types/src/artifact.rs | 57 +- rs/types/types/src/batch.rs | 17 + rs/types/types/src/batch/upgrade.rs | 196 +++++ rs/types/types/src/consensus.rs | 84 +++ rs/types/types/src/consensus/upgrade.rs | 39 + rs/types/types/src/crypto/hash.rs | 25 +- .../types/src/crypto/hash/domain_separator.rs | 16 + rs/types/types/src/crypto/hash/tests.rs | 14 +- rs/types/types/src/crypto/sign.rs | 9 +- 33 files changed, 1962 insertions(+), 15 deletions(-) create mode 100644 rs/artifact_pool/src/upgrade_permit_auth_pool.rs create mode 100644 rs/consensus/upgrade/BUILD.bazel create mode 100644 rs/consensus/upgrade/Cargo.toml create mode 100644 rs/consensus/upgrade/src/lib.rs create mode 100644 rs/consensus/upgrade/src/pool_manager.rs create mode 100644 rs/interfaces/src/upgrade.rs create mode 100644 rs/protobuf/def/types/v1/upgrade.proto create mode 100644 rs/types/types/src/batch/upgrade.rs create mode 100644 rs/types/types/src/consensus/upgrade.rs diff --git a/Cargo.lock b/Cargo.lock index fbbfcb5b434c..7c8bd90ecdce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8322,6 +8322,25 @@ dependencies = [ "slog", ] +[[package]] +name = "ic-consensus-upgrade" +version = "0.9.0" +dependencies = [ + "ic-consensus-utils", + "ic-interfaces", + "ic-interfaces-mocks", + "ic-logger", + "ic-protobuf", + "ic-registry-client-fake", + "ic-registry-proto-data-provider", + "ic-test-utilities-consensus", + "ic-test-utilities-registry", + "ic-test-utilities-types", + "ic-types", + "num-traits", + "slog", +] + [[package]] name = "ic-consensus-utils" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 396f11547947..ebfda237de52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ members = [ "rs/consensus/idkg", "rs/consensus/mocks", "rs/consensus/utils", + "rs/consensus/upgrade", "rs/consensus/chain_key", "rs/criterion_time", "rs/cross-chain/blob_store", diff --git a/rs/artifact_pool/src/lib.rs b/rs/artifact_pool/src/lib.rs index cec57a3ab2fe..e205f7229ead 100644 --- a/rs/artifact_pool/src/lib.rs +++ b/rs/artifact_pool/src/lib.rs @@ -11,6 +11,7 @@ mod metrics; mod pool_common; #[cfg(test)] mod test_utils; +pub mod upgrade_permit_auth_pool; pub mod backup; mod lmdb_iterator; diff --git a/rs/artifact_pool/src/upgrade_permit_auth_pool.rs b/rs/artifact_pool/src/upgrade_permit_auth_pool.rs new file mode 100644 index 000000000000..bae39df1f659 --- /dev/null +++ b/rs/artifact_pool/src/upgrade_permit_auth_pool.rs @@ -0,0 +1,311 @@ +use crate::{ + metrics::{POOL_TYPE_UNVALIDATED, POOL_TYPE_VALIDATED}, + pool_common::{HasLabel, PoolSection}, +}; +use ic_interfaces::{ + p2p::consensus::{ + ArtifactTransmit, ArtifactTransmits, ArtifactWithOpt, MutablePool, UnvalidatedArtifact, + ValidatedPoolReader, + }, + upgrade::{UpgradePermitAuthChangeAction, UpgradePermitAuthChangeSet, UpgradePermitAuthPool}, +}; +use ic_logger::ReplicaLogger; +use ic_metrics::MetricsRegistry; +use ic_types::{ + artifact::{IdentifiableArtifact, UpgradePermitAuthorizationShareId}, + consensus::UpgradePermitAuthorizationShare, +}; +use prometheus::IntCounter; + +const POOL_NAME: &str = "upgrade_permit_auth"; + +type ValidatedSection = + PoolSection; +type UnvalidatedSection = PoolSection< + UpgradePermitAuthorizationShareId, + UnvalidatedArtifact, +>; + +/// Upgrade Permit Authorization Pool implementation. +pub struct UpgradePermitAuthPoolImpl { + validated: ValidatedSection, + unvalidated: UnvalidatedSection, + invalidated_artifacts: IntCounter, + log: ReplicaLogger, +} + +impl UpgradePermitAuthPoolImpl { + pub fn new(metrics: MetricsRegistry, log: ReplicaLogger) -> Self { + Self { + invalidated_artifacts: metrics.int_counter( + "upgrade_permit_auth_invalidated_artifacts", + "The number of invalidated upgrade permit auth artifacts", + ), + validated: PoolSection::new(metrics.clone(), POOL_NAME, POOL_TYPE_VALIDATED), + unvalidated: PoolSection::new(metrics, POOL_NAME, POOL_TYPE_UNVALIDATED), + log, + } + } +} + +impl UpgradePermitAuthPool for UpgradePermitAuthPoolImpl { + fn get_validated_shares( + &self, + ) -> Box + '_> { + Box::new(self.validated.values()) + } + + fn get_unvalidated_shares( + &self, + ) -> Box + '_> { + Box::new(self.unvalidated.values().map(|pa| &pa.message)) + } +} + +impl MutablePool for UpgradePermitAuthPoolImpl { + type Mutations = UpgradePermitAuthChangeSet; + + fn insert(&mut self, artifact: UnvalidatedArtifact) { + let id = artifact.message.id(); + self.unvalidated.insert(id, artifact); + } + + fn remove(&mut self, id: &UpgradePermitAuthorizationShareId) { + self.unvalidated.remove(id); + } + + fn apply( + &mut self, + change_set: UpgradePermitAuthChangeSet, + ) -> ArtifactTransmits { + let changed = !change_set.is_empty(); + let mut transmits = vec![]; + for action in change_set { + match action { + UpgradePermitAuthChangeAction::AddToValidated(share) => { + transmits.push(ArtifactTransmit::Deliver(ArtifactWithOpt { + artifact: share.clone(), + is_latency_sensitive: true, + })); + self.validated.insert(share.id(), share); + } + UpgradePermitAuthChangeAction::MoveToValidated(share) => { + let id = share.id(); + self.unvalidated.remove(&id); + transmits.push(ArtifactTransmit::Deliver(ArtifactWithOpt { + artifact: share.clone(), + is_latency_sensitive: true, + })); + self.validated.insert(id, share); + } + UpgradePermitAuthChangeAction::RemoveValidated(id) => { + if self.validated.remove(&id).is_some() { + transmits.push(ArtifactTransmit::Abort(id)); + } + } + UpgradePermitAuthChangeAction::RemoveUnvalidated(id) => { + self.unvalidated.remove(&id); + } + UpgradePermitAuthChangeAction::HandleInvalid(id, reason) => { + ic_logger::warn!( + self.log, + "Invalidating upgrade permit auth artifact {id:?}: {reason}" + ); + self.invalidated_artifacts.inc(); + self.unvalidated.remove(&id); + } + } + } + ArtifactTransmits { + transmits, + poll_immediately: changed, + } + } +} + +impl ValidatedPoolReader for UpgradePermitAuthPoolImpl { + fn get( + &self, + id: &UpgradePermitAuthorizationShareId, + ) -> Option { + self.validated.get(id).cloned() + } + + fn get_all_for_initial_broadcast( + &self, + ) -> Box + '_> { + // Not persisted — no initial broadcast on restart. + Box::new(std::iter::empty()) + } +} + +impl HasLabel for UpgradePermitAuthorizationShare { + fn label(&self) -> &str { + "upgrade_permit_auth_share" + } +} + +impl HasLabel for UnvalidatedArtifact { + fn label(&self) -> &str { + self.message.label() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ic_logger::replica_logger::no_op_logger; + use ic_test_utilities_types::ids::node_test_id; + use ic_types::Height; + use ic_types::consensus::UpgradePermitAuthorizationRequest; + use ic_types::crypto::{BasicSig, BasicSigOf}; + use ic_types::signature::BasicSignature; + use ic_types::time::UNIX_EPOCH; + + fn fake_share( + signer: u64, + requestor: u64, + request_height: u64, + ) -> UpgradePermitAuthorizationShare { + UpgradePermitAuthorizationShare { + content: UpgradePermitAuthorizationRequest { + requestor: node_test_id(requestor), + request_height: Height::from(request_height), + }, + signature: BasicSignature { + signature: BasicSigOf::new(BasicSig(vec![])), + signer: node_test_id(signer), + }, + } + } + + fn to_unvalidated( + share: UpgradePermitAuthorizationShare, + ) -> UnvalidatedArtifact { + UnvalidatedArtifact { + message: share, + peer_id: node_test_id(0), + timestamp: UNIX_EPOCH, + } + } + + fn pool() -> UpgradePermitAuthPoolImpl { + UpgradePermitAuthPoolImpl::new(MetricsRegistry::new(), no_op_logger()) + } + + #[test] + fn test_insert_and_remove_unvalidated() { + let mut pool = pool(); + let share = fake_share(1, 2, 10); + let id = share.id(); + + pool.insert(to_unvalidated(share.clone())); + assert!(pool.get_unvalidated_shares().eq([&share])); + assert!(pool.get(&id).is_none()); + + pool.remove(&id); + assert_eq!(pool.get_unvalidated_shares().count(), 0); + } + + #[test] + fn test_add_to_validated_broadcasts() { + let mut pool = pool(); + let share = fake_share(1, 2, 10); + + let result = pool.apply(vec![UpgradePermitAuthChangeAction::AddToValidated( + share.clone(), + )]); + + assert!(result.poll_immediately); + assert_eq!(result.transmits.len(), 1); + assert!(matches!( + &result.transmits[0], + ArtifactTransmit::Deliver(a) if a.artifact == share + )); + assert!(pool.get_validated_shares().eq([&share])); + assert_eq!(pool.get(&share.id()).unwrap(), share); + } + + #[test] + fn test_move_to_validated_replaces_unvalidated_and_broadcasts() { + let mut pool = pool(); + let share = fake_share(1, 2, 10); + pool.insert(to_unvalidated(share.clone())); + assert!(pool.get_unvalidated_shares().eq([&share])); + + let result = pool.apply(vec![UpgradePermitAuthChangeAction::MoveToValidated( + share.clone(), + )]); + + assert_eq!(result.transmits.len(), 1); + assert_eq!(pool.get_unvalidated_shares().count(), 0); + assert!(pool.get_validated_shares().eq([&share])); + assert_eq!(pool.get(&share.id()).unwrap(), share); + } + + #[test] + fn test_remove_validated_aborts_broadcast() { + let mut pool = pool(); + let share = fake_share(1, 2, 10); + pool.apply(vec![UpgradePermitAuthChangeAction::AddToValidated( + share.clone(), + )]); + + let result = pool.apply(vec![UpgradePermitAuthChangeAction::RemoveValidated( + share.id(), + )]); + + assert!(result.poll_immediately); + assert!(matches!(&result.transmits[0], ArtifactTransmit::Abort(id) if *id == share.id())); + assert_eq!(pool.get_validated_shares().count(), 0); + + // Removing again is a no-op without a redundant Abort. + let result = pool.apply(vec![UpgradePermitAuthChangeAction::RemoveValidated( + share.id(), + )]); + assert_eq!(result.transmits.len(), 0); + } + + #[test] + fn test_handle_invalid_drops_unvalidated_without_broadcast() { + let mut pool = pool(); + let share = fake_share(1, 2, 10); + pool.insert(to_unvalidated(share.clone())); + + let result = pool.apply(vec![UpgradePermitAuthChangeAction::HandleInvalid( + share.id(), + "bad signature".to_string(), + )]); + + assert!(result.poll_immediately); + assert!(result.transmits.is_empty()); + assert_eq!(pool.get_unvalidated_shares().count(), 0); + } + + #[test] + fn test_empty_change_set_does_not_poll() { + let mut pool = pool(); + let result = pool.apply(vec![]); + assert!(!result.poll_immediately); + assert!(result.transmits.is_empty()); + } + + #[test] + fn test_shares_keyed_by_signer_and_request() { + let mut pool = pool(); + pool.apply(vec![ + // Same signer, different requests: two entries. + UpgradePermitAuthChangeAction::AddToValidated(fake_share(1, 2, 10)), + UpgradePermitAuthChangeAction::AddToValidated(fake_share(1, 2, 11)), + UpgradePermitAuthChangeAction::AddToValidated(fake_share(3, 2, 10)), + UpgradePermitAuthChangeAction::AddToValidated(fake_share(4, 2, 10)), + ]); + assert_eq!(pool.get_validated_shares().count(), 4); + + // Re-adding the same (signer, request) pair overwrites. + pool.apply(vec![UpgradePermitAuthChangeAction::AddToValidated( + fake_share(1, 2, 10), + )]); + assert_eq!(pool.get_validated_shares().count(), 4); + } +} diff --git a/rs/consensus/mocks/src/lib.rs b/rs/consensus/mocks/src/lib.rs index 58194d60eb74..acd72c5518d8 100644 --- a/rs/consensus/mocks/src/lib.rs +++ b/rs/consensus/mocks/src/lib.rs @@ -2,6 +2,7 @@ use ic_artifact_pool::{ canister_http_pool::CanisterHttpPoolImpl, dkg_pool::DkgPoolImpl, idkg_pool::IDkgPoolImpl, + upgrade_permit_auth_pool::UpgradePermitAuthPoolImpl, }; use ic_config::artifact_pool::ArtifactPoolConfig; use ic_consensus_utils::membership::Membership; @@ -114,6 +115,7 @@ pub struct Dependencies { pub dkg_pool: Arc>, pub idkg_pool: Arc>, pub canister_http_pool: Arc>, + pub upgrade_permit_auth_pool: Arc>, } pub struct DependenciesBuilder { @@ -281,6 +283,10 @@ impl DependenciesBuilder { Box::new(IDkgStatsNoOp {}), ))); let canister_http_pool = Arc::new(RwLock::new(CanisterHttpPoolImpl::new( + ic_metrics::MetricsRegistry::new(), + log.clone(), + ))); + let upgrade_permit_auth_pool = Arc::new(RwLock::new(UpgradePermitAuthPoolImpl::new( ic_metrics::MetricsRegistry::new(), log, ))); @@ -325,6 +331,7 @@ impl DependenciesBuilder { dkg_pool, idkg_pool, canister_http_pool, + upgrade_permit_auth_pool, } } } diff --git a/rs/consensus/src/consensus/payload_builder.rs b/rs/consensus/src/consensus/payload_builder.rs index 22ccfe686f34..b8be5bc84817 100644 --- a/rs/consensus/src/consensus/payload_builder.rs +++ b/rs/consensus/src/consensus/payload_builder.rs @@ -547,6 +547,7 @@ pub(crate) mod test { canister_http: settings.http_outcalls_payload_to_return, query_stats: settings.query_stats_payload_to_return, chain_key: settings.chain_key_payload_to_return, + upgrade: vec![], }, dkg: DkgDataPayload::new_empty(Height::from(0)), idkg: None, diff --git a/rs/consensus/upgrade/BUILD.bazel b/rs/consensus/upgrade/BUILD.bazel new file mode 100644 index 000000000000..b92bf577a379 --- /dev/null +++ b/rs/consensus/upgrade/BUILD.bazel @@ -0,0 +1,44 @@ +load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "upgrade", + srcs = glob(["src/**/*.rs"]), + crate_features = select({ + "//conditions:default": [], + }), + crate_name = "ic_consensus_upgrade", + proc_macro_deps = [ + # Keep sorted. + ], + deps = [ + # Keep sorted. + "//rs/consensus/utils", + "//rs/interfaces", + "//rs/monitoring/logger", + "//rs/types/types", + "@crate_index//:num-traits", + "@crate_index//:slog", + ], +) + +rust_doc( + name = "consensus_upgrade_doc", + crate = ":upgrade", +) + +rust_test( + name = "upgrade_test", + crate = ":upgrade", + deps = [ + # Keep sorted. + "//rs/interfaces/mocks", + "//rs/protobuf", + "//rs/registry/fake", + "//rs/registry/proto_data_provider", + "//rs/test_utilities/consensus", + "//rs/test_utilities/registry", + "//rs/test_utilities/types", + ], +) diff --git a/rs/consensus/upgrade/Cargo.toml b/rs/consensus/upgrade/Cargo.toml new file mode 100644 index 000000000000..ba5ed86b6fce --- /dev/null +++ b/rs/consensus/upgrade/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ic-consensus-upgrade" +version.workspace = true +authors.workspace = true +edition.workspace = true +description.workspace = true +documentation.workspace = true + +[dependencies] +ic-consensus-utils = { path = "../utils" } +ic-interfaces = { path = "../../interfaces" } +ic-logger = { path = "../../monitoring/logger" } +ic-types = { path = "../../types/types" } +num-traits = { workspace = true } +slog = { workspace = true } + +[dev-dependencies] +ic-interfaces-mocks = { path = "../../interfaces/mocks" } +ic-protobuf = { path = "../../protobuf" } +ic-registry-client-fake = { path = "../../registry/fake" } +ic-registry-proto-data-provider = { path = "../../registry/proto_data_provider" } +ic-test-utilities-consensus = { path = "../../test_utilities/consensus" } +ic-test-utilities-registry = { path = "../../test_utilities/registry" } +ic-test-utilities-types = { path = "../../test_utilities/types" } diff --git a/rs/consensus/upgrade/src/lib.rs b/rs/consensus/upgrade/src/lib.rs new file mode 100644 index 000000000000..a8a448450bbe --- /dev/null +++ b/rs/consensus/upgrade/src/lib.rs @@ -0,0 +1,207 @@ +//! The upgrade permit protocol for the Phase-2 rolling GuestOS reboots. + +use ic_consensus_utils::crypto::ConsensusCrypto; +use ic_consensus_utils::membership::Membership; +use ic_interfaces::upgrade::InvalidUpgradePayloadReason; +use ic_logger::{ReplicaLogger, warn}; +use ic_types::consensus::UpgradePermitAuthorizationShare; +use ic_types::{Height, NodeId, RegistryVersion}; +use std::collections::BTreeSet; + +pub mod pool_manager; + +pub(crate) struct SubnetMembership { + /// Current members staying even at the new CUP. + pub staying_members: BTreeSet, +} + +impl SubnetMembership { + /// Is the node a current member staying even at the new CUP? + pub fn staying(&self, node: &NodeId) -> bool { + self.staying_members.contains(node) + } +} + +/// Subnet membership at the block height and registry version. +pub(crate) fn subnet_membership( + membership: &Membership, + block_height: Height, + block_registry_version: RegistryVersion, + logger: &ReplicaLogger, +) -> SubnetMembership { + let registry_at_height: BTreeSet = membership + .get_nodes_at_version(block_registry_version) + .map(|nodes| nodes.into_iter().collect()) + .unwrap_or_default(); + let current_members: BTreeSet = match membership.get_nodes(block_height) { + Ok(nodes) => nodes.into_iter().collect(), + Err(e) => { + warn!( + logger, + "upgrade_payload: couldn't determine the committee at height {block_height:?}: {e:?}" + ); + registry_at_height.clone() + } + }; + let staying_members = current_members + .intersection(®istry_at_height) + .cloned() + .collect(); + SubnetMembership { staying_members } +} + +/// Check a share's content, staying signer, and signature. Returns the +/// signer. +pub(crate) fn validate_share( + share: &UpgradePermitAuthorizationShare, + requestor: NodeId, + request_height: Height, + membership: &SubnetMembership, + registry_version: RegistryVersion, + crypto: &dyn ConsensusCrypto, +) -> Result { + let signer = share.signature.signer; + if share.content.requestor != requestor || share.content.request_height != request_height { + return Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { signer }); + } + if !membership.staying(&signer) { + return Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { signer }); + } + crypto + .verify_basic_sig( + &share.signature.signature, + &share.content, + signer, + registry_version, + ) + .map_err(|_| InvalidUpgradePayloadReason::AuthorizeInvalidShare { signer })?; + Ok(signer) +} + +#[cfg(test)] +mod tests { + use super::*; + use ic_interfaces_mocks::crypto::MockCrypto; + use ic_test_utilities_types::ids::node_test_id; + use ic_types::consensus::UpgradePermitAuthorizationRequest; + use ic_types::crypto::{BasicSig, BasicSigOf, CryptoError}; + use ic_types::signature::BasicSignature; + + const REGISTRY_VERSION: RegistryVersion = RegistryVersion::new(1); + + fn share(signer: u64, requestor: u64, request_height: u64) -> UpgradePermitAuthorizationShare { + UpgradePermitAuthorizationShare { + content: UpgradePermitAuthorizationRequest { + requestor: node_test_id(requestor), + request_height: Height::from(request_height), + }, + signature: BasicSignature { + signature: BasicSigOf::new(BasicSig(vec![])), + signer: node_test_id(signer), + }, + } + } + + fn membership(staying: &[NodeId]) -> SubnetMembership { + SubnetMembership { + staying_members: staying.iter().copied().collect(), + } + } + + fn verifying_crypto() -> MockCrypto { + let mut crypto = MockCrypto::new(); + crypto + .expect_verify_basic_sig_upgrade_permit_auth() + .returning(|_, _, _, _| Ok(())); + crypto + } + + #[test] + fn test_rejects_requestor_mismatch() { + let share = share(2, 3, 10); + let membership = membership(&[node_test_id(1), node_test_id(2)]); + // No verify expectation: the share is rejected before verification. + let result = validate_share( + &share, + node_test_id(4), + Height::from(10), + &membership, + REGISTRY_VERSION, + &MockCrypto::new(), + ); + assert_eq!( + result, + Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { + signer: node_test_id(2) + }) + ); + } + + #[test] + fn test_rejects_request_height_mismatch() { + let share = share(2, 3, 10); + let membership = membership(&[node_test_id(1), node_test_id(2)]); + let result = validate_share( + &share, + node_test_id(3), + Height::from(11), + &membership, + REGISTRY_VERSION, + &MockCrypto::new(), + ); + assert_eq!( + result, + Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { + signer: node_test_id(2) + }) + ); + } + + #[test] + fn test_rejects_non_staying_signer() { + let share = share(2, 3, 10); + let membership = membership(&[node_test_id(1)]); + let result = validate_share( + &share, + node_test_id(3), + Height::from(10), + &membership, + REGISTRY_VERSION, + &MockCrypto::new(), + ); + assert_eq!( + result, + Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { + signer: node_test_id(2) + }) + ); + } + + #[test] + fn test_rejects_invalid_signature() { + let share = share(2, 3, 10); + let membership = membership(&[node_test_id(1), node_test_id(2)]); + let mut crypto = MockCrypto::new(); + crypto + .expect_verify_basic_sig_upgrade_permit_auth() + .returning(|_, _, _, _| { + Err(CryptoError::TransientInternalError { + internal_error: "boom".to_string(), + }) + }); + let result = validate_share( + &share, + node_test_id(3), + Height::from(10), + &membership, + REGISTRY_VERSION, + &crypto, + ); + assert_eq!( + result, + Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { + signer: node_test_id(2) + }) + ); + } +} diff --git a/rs/consensus/upgrade/src/pool_manager.rs b/rs/consensus/upgrade/src/pool_manager.rs new file mode 100644 index 000000000000..7e74f7e3dd0d --- /dev/null +++ b/rs/consensus/upgrade/src/pool_manager.rs @@ -0,0 +1,676 @@ +use std::collections::BTreeSet; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use crate::{SubnetMembership, subnet_membership, validate_share}; +use ic_consensus_utils::crypto::ConsensusCrypto; +use ic_consensus_utils::membership::Membership; +use ic_interfaces::consensus_pool::ConsensusBlockCache; +use ic_interfaces::p2p::consensus::{Bouncer, BouncerFactory, BouncerValue, PoolMutationsProducer}; +use ic_interfaces::upgrade::{ + UpgradePermitAuthChangeAction, UpgradePermitAuthChangeSet, UpgradePermitAuthPool, +}; +use ic_logger::{ReplicaLogger, info, warn}; +use ic_types::artifact::IdentifiableArtifact; +use ic_types::batch::bytes_to_upgrade_payload; +use ic_types::consensus::{Block, UpgradePermitAuthorizationShare, upgrade::UpgradePermitAction}; +use ic_types::{Height, NodeId}; +use num_traits::SaturatingSub; + +/// Shares whose request height falls this many blocks below the finalized +/// tip are purged from the pool. +const SHARE_EXPIRY_BLOCKS: Height = Height::new(20); + +/// Signs shares for requests in finalized blocks, validates gossiped shares, +/// and purges expired ones. +pub struct UpgradePermitAuthPoolManager { + node_id: NodeId, + crypto: Arc, + consensus_pool_cache: Arc, + membership: Arc, + /// Requests we've already signed (node, request_height). + signed_requests: Mutex>, + /// Last finalized height we scanned for requests. + last_scanned: Mutex, + logger: ReplicaLogger, +} + +impl UpgradePermitAuthPoolManager { + pub fn new( + node_id: NodeId, + crypto: Arc, + consensus_pool_cache: Arc, + membership: Arc, + logger: ReplicaLogger, + ) -> Self { + Self { + node_id, + crypto, + consensus_pool_cache, + membership, + signed_requests: Mutex::new(BTreeSet::new()), + last_scanned: Mutex::new(Height::from(0)), + logger, + } + } + + /// Membership at the finalized block's own height and registry version. + fn block_membership(&self, block: &Block) -> SubnetMembership { + subnet_membership( + &self.membership, + block.height, + block.context.registry_version, + &self.logger, + ) + } + + /// Scan finalized blocks for new `Request` actions and sign an auth share + /// for each one we haven't signed yet. + fn sign_shares_for_new_requests(&self) -> UpgradePermitAuthChangeSet { + let chain = self.consensus_pool_cache.finalized_chain(); + let tip = chain.tip().height; + let mut last = self.last_scanned.lock().unwrap(); + let start = last.increment(); + if start > tip { + return vec![]; + } + *last = tip; + + let mut signed = self.signed_requests.lock().unwrap(); + let mut change_set = vec![]; + + for height_num in start.get()..=tip.get() { + let height = Height::from(height_num); + let Ok(block) = chain.get_block_by_height(height) else { + continue; + }; + let payload = block.payload.as_ref(); + if payload.is_summary() { + continue; + } + let upgrade_bytes = &payload.as_data().batch.upgrade; + if upgrade_bytes.is_empty() { + continue; + } + let Ok(actions) = bytes_to_upgrade_payload(upgrade_bytes) else { + continue; + }; + let membership = self.block_membership(block); + if !membership.staying(&self.node_id) { + continue; + } + for action in actions { + let UpgradePermitAction::Request(request) = action else { + continue; + }; + let key = (request.requestor, request.request_height); + if signed.contains(&key) { + continue; + } + match self + .crypto + .sign(&request, self.node_id, block.context.registry_version) + { + Ok(signature) => { + signed.insert(key); + info!( + self.logger, + "permit_auth: signed share for node {:?} at height {:?}", + request.requestor, + request.request_height + ); + change_set.push(UpgradePermitAuthChangeAction::AddToValidated( + UpgradePermitAuthorizationShare { + content: request, + signature, + }, + )); + } + Err(e) => { + warn!( + self.logger, + "permit_auth: failed to sign share for node {:?}: {:?}", + request.requestor, + e + ); + } + } + } + } + change_set + } + + /// Validate gossiped shares found in the unvalidated section of the pool. + fn validate_gossiped_shares( + &self, + pool: &dyn UpgradePermitAuthPool, + ) -> UpgradePermitAuthChangeSet { + let chain = self.consensus_pool_cache.finalized_chain(); + let mut change_set = vec![]; + + for share in pool.get_unvalidated_shares() { + let Ok(block) = chain.get_block_by_height(share.content.request_height) else { + change_set.push(UpgradePermitAuthChangeAction::HandleInvalid( + share.id(), + format!( + "block at request_height {:?} not found in finalized chain", + share.content.request_height + ), + )); + continue; + }; + let membership = self.block_membership(block); + match validate_share( + share, + share.content.requestor, + share.content.request_height, + &membership, + block.context.registry_version, + self.crypto.as_ref(), + ) { + Ok(_) => { + change_set.push(UpgradePermitAuthChangeAction::MoveToValidated( + share.clone(), + )); + } + Err(reason) => { + warn!( + self.logger, + "permit_auth: dropping invalid share: {reason:?}", + ); + change_set.push(UpgradePermitAuthChangeAction::HandleInvalid( + share.id(), + format!("invalid share: {reason:?}"), + )); + } + } + } + + change_set + } + + /// Purge shares (both validated and unvalidated) whose request has expired + /// (the request height is older than `REQUEST_TIMEOUT_BLOCKS` below the + /// current finalized height). + fn purge_expired_shares(&self, pool: &dyn UpgradePermitAuthPool) -> UpgradePermitAuthChangeSet { + let current_height = self.consensus_pool_cache.finalized_chain().tip().height; + let expiry_threshold = current_height.saturating_sub(&SHARE_EXPIRY_BLOCKS); + + let expired_validated = pool + .get_validated_shares() + .filter(|share| share.content.request_height < expiry_threshold) + .map(|share| UpgradePermitAuthChangeAction::RemoveValidated(share.into())); + + let expired_unvalidated = pool + .get_unvalidated_shares() + .filter(|share| share.content.request_height < expiry_threshold) + .map(|share| UpgradePermitAuthChangeAction::RemoveUnvalidated(share.into())); + + expired_validated.chain(expired_unvalidated).collect() + } +} + +impl PoolMutationsProducer for UpgradePermitAuthPoolManager { + type Mutations = UpgradePermitAuthChangeSet; + + fn on_state_change(&self, pool: &T) -> Self::Mutations { + let mut change_set = self.sign_shares_for_new_requests(); + change_set.extend(self.validate_gossiped_shares(pool)); + change_set.extend(self.purge_expired_shares(pool)); + change_set + } +} + +/// Bouncer that accepts all upgrade permit auth shares. +pub struct UpgradePermitAuthBouncer; + +impl BouncerFactory + for UpgradePermitAuthBouncer +{ + fn new_bouncer( + &self, + _pool: &Pool, + ) -> Bouncer { + Box::new(|_id| BouncerValue::Wants) + } + + fn refresh_period(&self) -> Duration { + Duration::from_secs(60) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ic_interfaces::consensus_pool::{ConsensusBlockChain, ConsensusBlockChainErr}; + use ic_interfaces_mocks::crypto::MockCrypto; + use ic_logger::replica_logger::no_op_logger; + use ic_protobuf::types::v1 as pb; + use ic_registry_client_fake::FakeRegistryClient; + use ic_registry_proto_data_provider::ProtoRegistryDataProvider; + use ic_test_utilities_consensus::{FakeConsensusPoolCache, fake::Fake, make_genesis}; + use ic_test_utilities_registry::{SubnetRecordBuilder, add_single_subnet_record}; + use ic_test_utilities_types::ids::{node_test_id, subnet_test_id, test_replica_version}; + use ic_types::NumBytes; + use ic_types::RegistryVersion; + use ic_types::artifact::{IdentifiableArtifact, UpgradePermitAuthorizationShareId}; + use ic_types::batch::{BatchPayload, ValidationContext, upgrade_payload_to_bytes}; + use ic_types::consensus::upgrade::UpgradePermitAction; + use ic_types::consensus::{ + BlockPayload, DataPayload, Payload, Rank, UpgradePermitAuthorizationRequest, + dkg::{DkgDataPayload, DkgSummary}, + }; + use ic_types::crypto::{ + BasicSig, BasicSigOf, CryptoError, CryptoHash, CryptoHashOf, crypto_hash, + }; + use ic_types::signature::BasicSignature; + use ic_types::time::UNIX_EPOCH; + use std::collections::BTreeMap; + use std::ops::RangeInclusive; + + fn registry_version() -> RegistryVersion { + RegistryVersion::from(1) + } + + fn members() -> Vec { + vec![node_test_id(1), node_test_id(2), node_test_id(3)] + } + + fn signing_crypto() -> MockCrypto { + let mut crypto = MockCrypto::new(); + crypto + .expect_sign_basic_upgrade_permit_auth() + .returning(|_| Ok(BasicSigOf::new(BasicSig(vec![])))); + crypto + } + + fn verifying_crypto() -> MockCrypto { + let mut crypto = MockCrypto::new(); + crypto + .expect_verify_basic_sig_upgrade_permit_auth() + .returning(|_, _, _, _| Ok(())); + crypto + } + + fn membership_of(members: &[NodeId]) -> Arc { + let data_provider = Arc::new(ProtoRegistryDataProvider::new()); + add_single_subnet_record( + &data_provider, + registry_version().get(), + subnet_test_id(1), + SubnetRecordBuilder::default() + .with_membership(members) + .build(), + ); + let registry = Arc::new(FakeRegistryClient::new(Arc::clone(&data_provider) as Arc<_>)); + registry.update_to_latest_version(); + let cup = make_genesis(DkgSummary::fake()); + let consensus_cache = Arc::new(FakeConsensusPoolCache::new(pb::CatchUpPackage::from(cup))); + Arc::new(Membership::new( + consensus_cache, + registry, + subnet_test_id(1), + )) + } + + fn genesis_block() -> Block { + make_genesis(DkgSummary::fake()).content.block.into_inner() + } + + fn data_block(height: u64, actions: &[UpgradePermitAction]) -> Block { + Block::new( + CryptoHashOf::new(CryptoHash(vec![0; 32])), + Payload::new( + crypto_hash, + BlockPayload::Data(DataPayload { + batch: BatchPayload { + upgrade: upgrade_payload_to_bytes( + actions.to_vec(), + NumBytes::new(u64::MAX), + ), + ..BatchPayload::default() + }, + dkg: DkgDataPayload::new_empty(Height::from(height)), + idkg: None, + }), + ), + Height::from(height), + Rank(0), + ValidationContext { + registry_version: registry_version(), + certified_height: Height::from(height), + time: UNIX_EPOCH, + }, + test_replica_version(), + ) + } + + fn empty_blocks(heights: RangeInclusive) -> Vec { + heights.map(|height| data_block(height, &[])).collect() + } + + struct FakeFinalizedChain { + blocks: Vec, + } + + impl ConsensusBlockChain for FakeFinalizedChain { + fn tip(&self) -> &Block { + self.blocks.last().unwrap() + } + + fn get_block_by_height(&self, height: Height) -> Result<&Block, ConsensusBlockChainErr> { + self.blocks + .iter() + .find(|block| block.height == height) + .ok_or(ConsensusBlockChainErr::BlockNotFound(height)) + } + + fn len(&self) -> usize { + self.blocks.len() + } + + fn iter_above(&self, height: Height) -> Box + '_> { + Box::new(self.blocks.iter().filter(move |b| b.height > height)) + } + } + + struct FakeBlockCache { + chain: Arc, + } + + impl ConsensusBlockCache for FakeBlockCache { + fn finalized_chain(&self) -> Arc { + self.chain.clone() + } + } + + fn pool_manager( + node_id: NodeId, + members: &[NodeId], + blocks: Vec, + crypto: MockCrypto, + ) -> UpgradePermitAuthPoolManager { + let mut chain = vec![genesis_block()]; + chain.extend(blocks); + UpgradePermitAuthPoolManager::new( + node_id, + Arc::new(crypto), + Arc::new(FakeBlockCache { + chain: Arc::new(FakeFinalizedChain { blocks: chain }), + }), + membership_of(members), + no_op_logger(), + ) + } + + struct FakePool { + validated: BTreeMap, + unvalidated: BTreeMap, + } + + impl FakePool { + fn new() -> Self { + Self { + validated: BTreeMap::new(), + unvalidated: BTreeMap::new(), + } + } + + fn with_validated(mut self, share: UpgradePermitAuthorizationShare) -> Self { + self.validated.insert(share.id(), share); + self + } + + fn with_unvalidated(mut self, share: UpgradePermitAuthorizationShare) -> Self { + self.unvalidated.insert(share.id(), share); + self + } + } + + impl UpgradePermitAuthPool for FakePool { + fn get_validated_shares( + &self, + ) -> Box + '_> { + Box::new(self.validated.values()) + } + + fn get_unvalidated_shares( + &self, + ) -> Box + '_> { + Box::new(self.unvalidated.values()) + } + } + + fn share(signer: u64, requestor: u64, request_height: u64) -> UpgradePermitAuthorizationShare { + UpgradePermitAuthorizationShare { + content: UpgradePermitAuthorizationRequest { + requestor: node_test_id(requestor), + request_height: Height::from(request_height), + }, + signature: BasicSignature { + signature: BasicSigOf::new(BasicSig(vec![])), + signer: node_test_id(signer), + }, + } + } + + fn request(requestor: u64, request_height: u64) -> UpgradePermitAction { + UpgradePermitAction::Request(UpgradePermitAuthorizationRequest { + requestor: node_test_id(requestor), + request_height: Height::from(request_height), + }) + } + + fn assert_single_add_to_validated( + change_set: &UpgradePermitAuthChangeSet, + expected: &UpgradePermitAuthorizationShare, + ) { + assert_eq!(change_set.len(), 1); + assert!(matches!( + &change_set[0], + UpgradePermitAuthChangeAction::AddToValidated(s) if s == expected + )); + } + + fn assert_single_handle_invalid( + change_set: &UpgradePermitAuthChangeSet, + expected: &UpgradePermitAuthorizationShare, + ) { + assert_eq!(change_set.len(), 1); + assert!(matches!( + &change_set[0], + UpgradePermitAuthChangeAction::HandleInvalid(id, _) if id == &expected.id() + )); + } + + #[test] + fn test_signs_share_for_request_in_finalized_block() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(1, &[request(2, 1)])], + signing_crypto(), + ); + let change_set = manager.on_state_change(&FakePool::new()); + assert_single_add_to_validated(&change_set, &share(1, 2, 1)); + } + + #[test] + fn test_does_not_sign_when_node_leaving_subnet() { + let manager = pool_manager( + node_test_id(4), + &members(), + vec![data_block(1, &[request(2, 1)])], + signing_crypto(), + ); + assert!(manager.on_state_change(&FakePool::new()).is_empty()); + } + + #[test] + fn test_ignores_blocks_without_requests() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(1, &[]), data_block(2, &[])], + signing_crypto(), + ); + assert!(manager.on_state_change(&FakePool::new()).is_empty()); + } + + #[test] + fn test_does_not_rescan_finalized_blocks() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(1, &[request(2, 1)])], + signing_crypto(), + ); + assert_single_add_to_validated(&manager.on_state_change(&FakePool::new()), &share(1, 2, 1)); + assert!(manager.on_state_change(&FakePool::new()).is_empty()); + } + + #[test] + fn test_deduplicates_repeated_requests() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![ + data_block(1, &[request(2, 1)]), + data_block(2, &[request(2, 1)]), + ], + signing_crypto(), + ); + let change_set = manager.on_state_change(&FakePool::new()); + assert_single_add_to_validated(&change_set, &share(1, 2, 1)); + } + + #[test] + fn test_signing_failure_yields_no_action() { + let mut crypto = MockCrypto::new(); + crypto + .expect_sign_basic_upgrade_permit_auth() + .returning(|_| { + Err(CryptoError::TransientInternalError { + internal_error: "boom".to_string(), + }) + }); + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(1, &[request(2, 1)])], + crypto, + ); + assert!(manager.on_state_change(&FakePool::new()).is_empty()); + } + + #[test] + fn test_validates_gossiped_share() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(5, &[])], + verifying_crypto(), + ); + let gossiped = share(2, 3, 5); + let pool = FakePool::new().with_unvalidated(gossiped.clone()); + let change_set = manager.on_state_change(&pool); + assert_eq!(change_set.len(), 1); + assert!(matches!( + &change_set[0], + UpgradePermitAuthChangeAction::MoveToValidated(s) if s == &gossiped + )); + } + + #[test] + fn test_drops_share_with_invalid_signature() { + let mut crypto = MockCrypto::new(); + crypto + .expect_verify_basic_sig_upgrade_permit_auth() + .returning(|_, _, _, _| { + Err(CryptoError::TransientInternalError { + internal_error: "boom".to_string(), + }) + }); + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(5, &[])], + crypto, + ); + let gossiped = share(2, 3, 5); + let pool = FakePool::new().with_unvalidated(gossiped.clone()); + assert_single_handle_invalid(&manager.on_state_change(&pool), &gossiped); + } + + #[test] + fn test_drops_share_from_non_staying_signer() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(5, &[])], + verifying_crypto(), + ); + let gossiped = share(9, 3, 5); + let pool = FakePool::new().with_unvalidated(gossiped.clone()); + assert_single_handle_invalid(&manager.on_state_change(&pool), &gossiped); + } + + #[test] + fn test_drops_share_without_request_block() { + let manager = pool_manager( + node_test_id(1), + &members(), + vec![data_block(5, &[])], + verifying_crypto(), + ); + let gossiped = share(2, 3, 99); + let pool = FakePool::new().with_unvalidated(gossiped.clone()); + assert_single_handle_invalid(&manager.on_state_change(&pool), &gossiped); + } + + #[test] + fn test_purges_expired_validated_shares() { + // Threshold is tip (100) - SHARE_EXPIRY_BLOCKS (20) = 80: shares with + // request height below 80 are purged, at or above it are kept. + let manager = pool_manager( + node_test_id(1), + &members(), + empty_blocks(1..=100), + MockCrypto::new(), + ); + let expired = share(2, 3, 79); + let pool = FakePool::new() + .with_validated(expired.clone()) + .with_validated(share(3, 2, 80)); + let change_set = manager.on_state_change(&pool); + assert_eq!(change_set.len(), 1); + assert!(matches!( + &change_set[0], + UpgradePermitAuthChangeAction::RemoveValidated(id) if id == &expired.id() + )); + } + + #[test] + fn test_purges_expired_unvalidated_shares() { + // The share is both validated (moved out of unvalidated) and purged + // (removed from unvalidated); applying both leaves it validated only. + let manager = pool_manager( + node_test_id(1), + &members(), + empty_blocks(1..=100), + verifying_crypto(), + ); + let expired = share(2, 3, 79); + let pool = FakePool::new().with_unvalidated(expired.clone()); + let change_set = manager.on_state_change(&pool); + assert_eq!(change_set.len(), 2); + assert!(matches!( + &change_set[0], + UpgradePermitAuthChangeAction::MoveToValidated(s) if s == &expired + )); + assert!(matches!( + &change_set[1], + UpgradePermitAuthChangeAction::RemoveUnvalidated(id) if id == &expired.id() + )); + } +} diff --git a/rs/consensus/utils/src/crypto.rs b/rs/consensus/utils/src/crypto.rs index 39f919823c4f..67dc60816ee2 100644 --- a/rs/consensus/utils/src/crypto.rs +++ b/rs/consensus/utils/src/crypto.rs @@ -4,7 +4,7 @@ use ic_types::{ canister_http::CanisterHttpResponseReceipt, consensus::{ BlockMetadata, CatchUpContent, FinalizationContent, NotarizationContent, - RandomBeaconContent, RandomTapeContent, dkg, + RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, dkg, hashed::Hashed, idkg::{IDkgComplaintContent, IDkgOpeningContent}, }, @@ -425,7 +425,11 @@ pub trait ConsensusCrypto: + SignVerify, RegistryVersion> + SignVerify, RegistryVersion> + SignVerify, RegistryVersion> - + SignVerify, NiDkgId> + + SignVerify< + UpgradePermitAuthorizationRequest, + BasicSignature, + RegistryVersion, + > + SignVerify, NiDkgId> + SignVerify, NiDkgId> + SignVerify, NiDkgId> + SignVerify, RegistryVersion> diff --git a/rs/interfaces/mocks/src/crypto.rs b/rs/interfaces/mocks/src/crypto.rs index efb330083a22..88bb17fb8162 100644 --- a/rs/interfaces/mocks/src/crypto.rs +++ b/rs/interfaces/mocks/src/crypto.rs @@ -30,7 +30,7 @@ use ic_interfaces::crypto::{ use ic_types::canister_http::CanisterHttpResponseReceipt; use ic_types::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, - NotarizationContent, RandomBeaconContent, RandomTapeContent, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, certification::CertificationContent, dkg as consensus_dkg, idkg::{IDkgComplaintContent, IDkgOpeningContent}, @@ -310,6 +310,10 @@ mockall::mock! { &self, message: &CanisterHttpResponseReceipt, ) -> CryptoResult>; + pub fn sign_basic_upgrade_permit_auth( + &self, message: &UpgradePermitAuthorizationRequest, + ) -> CryptoResult>; + pub fn sign_basic_query( &self, message: &QueryResponseHash, ) -> CryptoResult>; @@ -490,6 +494,37 @@ mockall::mock! { )>, ) -> CryptoResult<()>; + // UpgradePermitAuthorizationRequest + pub fn verify_basic_sig_upgrade_permit_auth( + &self, + signature: &BasicSigOf, + message: &UpgradePermitAuthorizationRequest, signer: NodeId, + registry_version: RegistryVersion, + ) -> CryptoResult<()>; + + pub fn combine_basic_sig_upgrade_permit_auth( + &self, + signatures: BTreeMap>, + registry_version: RegistryVersion, + ) -> CryptoResult>; + + pub fn verify_basic_sig_batch_upgrade_permit_auth( + &self, + signature_batch: &BasicSignatureBatch, + message: &UpgradePermitAuthorizationRequest, + registry_version: RegistryVersion, + ) -> CryptoResult<()>; + + pub fn verify_basic_sig_batch_multi_msg_upgrade_permit_auth( + &self, + inputs: Vec<( + NodeId, + BasicSigOf, + UpgradePermitAuthorizationRequest, + RegistryVersion, + )>, + ) -> CryptoResult<()>; + // ── ThresholdSigner ────────────────────────────────────────── pub fn sign_threshold_certification( @@ -786,6 +821,10 @@ impl_basic_signer!(IDkgDealing, sign_basic_idkg_dealing); impl_basic_signer!(IDkgComplaintContent, sign_basic_idkg_complaint); impl_basic_signer!(IDkgOpeningContent, sign_basic_idkg_opening); impl_basic_signer!(CanisterHttpResponseReceipt, sign_basic_http); +impl_basic_signer!( + UpgradePermitAuthorizationRequest, + sign_basic_upgrade_permit_auth +); impl_basic_signer!(QueryResponseHash, sign_basic_query); impl_basic_sig_verifier!( @@ -837,6 +876,13 @@ impl_basic_sig_verifier!( verify_basic_sig_batch_http, verify_basic_sig_batch_multi_msg_http ); +impl_basic_sig_verifier!( + UpgradePermitAuthorizationRequest, + verify_basic_sig_upgrade_permit_auth, + combine_basic_sig_upgrade_permit_auth, + verify_basic_sig_batch_upgrade_permit_auth, + verify_basic_sig_batch_multi_msg_upgrade_permit_auth +); impl_threshold_signer!(CertificationContent, sign_threshold_certification); impl_threshold_signer!(CatchUpContent, sign_threshold_cup); diff --git a/rs/interfaces/src/consensus.rs b/rs/interfaces/src/consensus.rs index 03348807090e..f4d1e46bdef6 100644 --- a/rs/interfaces/src/consensus.rs +++ b/rs/interfaces/src/consensus.rs @@ -15,6 +15,7 @@ use crate::{ InvalidSelfValidatingPayloadReason, SelfValidatingPayloadValidationError, SelfValidatingPayloadValidationFailure, }, + upgrade::InvalidUpgradePayloadReason, validation::{ValidationError, ValidationResult}, }; use ic_base_types::{NumBytes, SubnetId}; @@ -75,6 +76,7 @@ pub enum InvalidPayloadReason { InvalidCanisterHttpPayload(InvalidCanisterHttpPayloadReason), InvalidQueryStatsPayload(InvalidQueryStatsPayloadReason), InvalidChainKeyPayload(InvalidChainKeyPayloadReason), + InvalidUpgradePayload(InvalidUpgradePayloadReason), /// The overall block size is too large, even though the individual payloads are valid PayloadTooBig { expected: NumBytes, diff --git a/rs/interfaces/src/crypto.rs b/rs/interfaces/src/crypto.rs index 30a7dddbe427..a790c9ffc9fe 100644 --- a/rs/interfaces/src/crypto.rs +++ b/rs/interfaces/src/crypto.rs @@ -26,7 +26,7 @@ pub use vetkd::*; use ic_crypto_interfaces_sig_verification::BasicSigVerifierByPublicKey; use ic_types::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, - NotarizationContent, RandomBeaconContent, RandomTapeContent, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, certification::CertificationContent, dkg as consensus_dkg, idkg::{IDkgComplaintContent, IDkgOpeningContent}, @@ -73,6 +73,9 @@ pub trait Crypto: // IDkgOpeningContent + BasicSigner + BasicSigVerifier + // UpgradePermitAuthorizationRequest + + BasicSigner + + BasicSigVerifier + IDkgProtocol + ThresholdEcdsaSigner + ThresholdEcdsaSigVerifier @@ -141,6 +144,8 @@ impl Crypto for T where + BasicSigVerifier + BasicSigner + BasicSigVerifier + + BasicSigner + + BasicSigVerifier + BasicSigner + BasicSigVerifier + BasicSigner diff --git a/rs/interfaces/src/lib.rs b/rs/interfaces/src/lib.rs index be56ff4b6e90..01682b3c21d8 100644 --- a/rs/interfaces/src/lib.rs +++ b/rs/interfaces/src/lib.rs @@ -19,6 +19,7 @@ pub mod p2p; pub mod query_stats; pub mod self_validating_payload; pub mod time_source; +pub mod upgrade; pub mod validation; // Note [Associated Types in Interfaces] diff --git a/rs/interfaces/src/p2p/consensus.rs b/rs/interfaces/src/p2p/consensus.rs index 53b14241aa2e..d107e463dc63 100644 --- a/rs/interfaces/src/p2p/consensus.rs +++ b/rs/interfaces/src/p2p/consensus.rs @@ -52,7 +52,7 @@ pub struct ArtifactTransmits { /// The list of replication transmits returned by the client. Mutations are applied in order by P2P-replication. pub transmits: Vec>, /// The field instructs the polling component (the one that calls `on_state_change` + `apply_changes`) - /// that polling immediately can be benefitial. For example, polling consensus when the field is set to + /// that polling immediately can be beneficial. For example, polling consensus when the field is set to /// true results in lower consensus latencies. pub poll_immediately: bool, } diff --git a/rs/interfaces/src/upgrade.rs b/rs/interfaces/src/upgrade.rs new file mode 100644 index 000000000000..a8551eba2385 --- /dev/null +++ b/rs/interfaces/src/upgrade.rs @@ -0,0 +1,55 @@ +use ic_types::NodeId; +use ic_types::artifact::UpgradePermitAuthorizationShareId; +use ic_types::consensus::UpgradePermitAuthorizationShare; + +#[derive(Debug, Eq, PartialEq)] +pub enum InvalidUpgradePayloadReason { + /// A `Request` was issued for a node other than the block maker. + RequestNodeMismatch { node: NodeId, proposer: NodeId }, + /// A `Return` was issued for a node other than the block maker. + ReturnNodeMismatch { node: NodeId, proposer: NodeId }, + /// The number of outstanding permits (requested or authorized) meets + /// the subnet's maximum number of rebooting nodes. + SlotsExhausted { slots_in_use: usize, permits: usize }, + /// An `Authorize` was issued for a node with no outstanding request. + AuthorizeNoOutstandingRequest { node: NodeId }, + /// An `Authorize` contains an invalid share (bad signature, content + /// mismatch, or signer is not a member). + AuthorizeInvalidShare { signer: NodeId }, + /// An `Authorize` does not carry enough valid shares (≥ the active + /// staying nodes). + AuthorizeInsufficientShares { collected: usize, threshold: usize }, + /// Failed to decode the upgrade payload from protobuf. + DecodeFailed(String), +} + +/// Change actions that can be applied to the [`UpgradePermitAuthPool`]. +#[derive(Debug)] +pub enum UpgradePermitAuthChangeAction { + /// Add a locally-produced share directly to validated. + AddToValidated(UpgradePermitAuthorizationShare), + /// Move a gossiped share from unvalidated to validated (after signature + /// verification). + MoveToValidated(UpgradePermitAuthorizationShare), + /// Remove a validated share (e.g. after the request was authorized or + /// timed out). + RemoveValidated(UpgradePermitAuthorizationShareId), + /// Remove an unvalidated share. + RemoveUnvalidated(UpgradePermitAuthorizationShareId), + /// Handle an invalid share (bad signature, no matching request, etc.). + HandleInvalid(UpgradePermitAuthorizationShareId, String), +} + +pub type UpgradePermitAuthChangeSet = Vec; + +/// Query interface for the upgrade permit authorization pool. +pub trait UpgradePermitAuthPool: Send + Sync { + /// Return an iterator over all validated shares. + fn get_validated_shares( + &self, + ) -> Box + '_>; + /// Return an iterator over all unvalidated shares. + fn get_unvalidated_shares( + &self, + ) -> Box + '_>; +} diff --git a/rs/protobuf/def/types/v1/artifact.proto b/rs/protobuf/def/types/v1/artifact.proto index ba00b48eea40..487e1b886964 100644 --- a/rs/protobuf/def/types/v1/artifact.proto +++ b/rs/protobuf/def/types/v1/artifact.proto @@ -12,6 +12,11 @@ message DkgMessageId { uint64 height = 2; } +message UpgradePermitAuthMessageId { + bytes hash = 1; + uint64 height = 2; +} + message ConsensusMessageId { ConsensusMessageHash hash = 1; uint64 height = 2; diff --git a/rs/protobuf/def/types/v1/consensus.proto b/rs/protobuf/def/types/v1/consensus.proto index 8b25ad0d351a..6ddb0a528950 100644 --- a/rs/protobuf/def/types/v1/consensus.proto +++ b/rs/protobuf/def/types/v1/consensus.proto @@ -10,6 +10,7 @@ import "registry/subnet/v1/subnet.proto"; import "types/v1/artifact.proto"; import "types/v1/dkg.proto"; import "types/v1/idkg.proto"; +import "types/v1/signature.proto"; import "types/v1/types.proto"; message CertificationMessage { @@ -69,6 +70,7 @@ message Block { bytes canister_http_payload_bytes = 15; bytes query_stats_payload_bytes = 16; bytes chain_key_payload_bytes = 17; + bytes upgrade_payload_bytes = 18; bytes payload_hash = 11; } diff --git a/rs/protobuf/def/types/v1/upgrade.proto b/rs/protobuf/def/types/v1/upgrade.proto new file mode 100644 index 000000000000..f5f92595b17c --- /dev/null +++ b/rs/protobuf/def/types/v1/upgrade.proto @@ -0,0 +1,39 @@ +// Protocol buffers for the Phase-2 upgrade permit protocol: permit requests +// in block payloads and gossiped authorization shares. + +syntax = "proto3"; +package types.v1; + +import "types/v1/signature.proto"; +import "types/v1/types.proto"; + +message UpgradeAction { + oneof action { + RequestUpgradePermit request_permit = 1; + AuthorizeUpgradePermit authorize_permit = 2; + ReturnUpgradePermit return_permit = 3; + } +} + +message UpgradePermitRequest { + types.v1.NodeId requestor = 1; + uint64 request_height = 2; +} + +message RequestUpgradePermit { + UpgradePermitRequest request = 1; +} + +message AuthorizeUpgradePermit { + UpgradePermitRequest request = 1; + repeated types.v1.BasicSignature signatures = 2; +} + +message ReturnUpgradePermit { + types.v1.NodeId node = 1; +} + +message UpgradePermitAuthorizationShare { + UpgradePermitRequest request = 1; + types.v1.BasicSignature signature = 2; +} diff --git a/rs/protobuf/generator/src/lib.rs b/rs/protobuf/generator/src/lib.rs index 4faf467e9825..715dc58b52d2 100644 --- a/rs/protobuf/generator/src/lib.rs +++ b/rs/protobuf/generator/src/lib.rs @@ -407,6 +407,7 @@ fn build_types_proto(def: &Path, out: &Path) { def.join("types/v1/canister_http.proto"), def.join("types/v1/artifact.proto"), def.join("types/v1/errors.proto"), + def.join("types/v1/upgrade.proto"), ]; compile_protos(config, def, &files); } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index a0eae50a3ed7..db3df960d9c1 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1317,6 +1317,13 @@ pub struct DkgMessageId { pub height: u64, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UpgradePermitAuthMessageId { + #[prost(bytes = "vec", tag = "1")] + pub hash: ::prost::alloc::vec::Vec, + #[prost(uint64, tag = "2")] + pub height: u64, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ConsensusMessageId { #[prost(message, optional, tag = "1")] pub hash: ::core::option::Option, @@ -1489,6 +1496,8 @@ pub struct Block { pub query_stats_payload_bytes: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "17")] pub chain_key_payload_bytes: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "18")] + pub upgrade_payload_bytes: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "11")] pub payload_hash: ::prost::alloc::vec::Vec, } @@ -1852,3 +1861,51 @@ impl ChainKeyErrorCode { } } } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpgradeAction { + #[prost(oneof = "upgrade_action::Action", tags = "1, 2, 3")] + pub action: ::core::option::Option, +} +/// Nested message and enum types in `UpgradeAction`. +pub mod upgrade_action { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Action { + #[prost(message, tag = "1")] + RequestPermit(super::RequestUpgradePermit), + #[prost(message, tag = "2")] + AuthorizePermit(super::AuthorizeUpgradePermit), + #[prost(message, tag = "3")] + ReturnPermit(super::ReturnUpgradePermit), + } +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UpgradePermitRequest { + #[prost(message, optional, tag = "1")] + pub requestor: ::core::option::Option, + #[prost(uint64, tag = "2")] + pub request_height: u64, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RequestUpgradePermit { + #[prost(message, optional, tag = "1")] + pub request: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AuthorizeUpgradePermit { + #[prost(message, optional, tag = "1")] + pub request: ::core::option::Option, + #[prost(message, repeated, tag = "2")] + pub signatures: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ReturnUpgradePermit { + #[prost(message, optional, tag = "1")] + pub node: ::core::option::Option, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UpgradePermitAuthorizationShare { + #[prost(message, optional, tag = "1")] + pub request: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub signature: ::core::option::Option, +} diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 9dd5b533d7fc..fe7d4cfac623 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -3172,6 +3172,7 @@ impl StateMachine { .map(|p| p.get().to_vec()) .unwrap_or_default(), query_stats: payload.query_stats, + upgrade: vec![], }, chain_key_data: ChainKeyData { master_public_keys: self.chain_key_subnet_public_keys.clone(), diff --git a/rs/test_utilities/types/src/batch/payload.rs b/rs/test_utilities/types/src/batch/payload.rs index aaa5e3f9a9df..d39aefd0e96a 100644 --- a/rs/test_utilities/types/src/batch/payload.rs +++ b/rs/test_utilities/types/src/batch/payload.rs @@ -15,6 +15,7 @@ impl Default for PayloadBuilder { canister_http: vec![], query_stats: vec![], chain_key: vec![], + upgrade: vec![], }, } } diff --git a/rs/types/types/src/artifact.rs b/rs/types/types/src/artifact.rs index 2a7914d58662..a1e588ee0cd5 100644 --- a/rs/types/types/src/artifact.rs +++ b/rs/types/types/src/artifact.rs @@ -4,10 +4,11 @@ use crate::{ canister_http::CanisterHttpResponseShare, consensus::{ ConsensusMessage, ConsensusMessageHash, ConsensusMessageHashable, HasHash, HasHeight, + UpgradePermitAuthorizationShare, certification::{CertificationMessage, CertificationMessageHash}, idkg::IDkgArtifactId, }, - crypto::{CryptoHash, crypto_hash}, + crypto::{CryptoHash, CryptoHashOf, crypto_hash}, messages::{MessageId, SignedIngress}, }; #[cfg(test)] @@ -246,3 +247,57 @@ pub type IDkgMessageId = IDkgArtifactId; // CanisterHttp artifacts pub type CanisterHttpResponseId = CanisterHttpResponseShare; + +// ----------------------------------------------------------------------------- +// Upgrade permit authorization artifacts + +/// Upgrade permit authorization message identifier carries both a message hash +/// and a height, used by the upgrade permit auth pool for lookup. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] +pub struct UpgradePermitAuthorizationShareId { + pub hash: CryptoHashOf, + pub height: Height, +} + +impl HasHeight for UpgradePermitAuthorizationShareId { + fn height(&self) -> Height { + self.height + } +} + +impl IdentifiableArtifact for UpgradePermitAuthorizationShare { + const NAME: &'static str = "upgrade"; + type Id = UpgradePermitAuthorizationShareId; + fn id(&self) -> Self::Id { + UpgradePermitAuthorizationShareId { + hash: crypto_hash(self), + height: self.content.height(), + } + } +} + +impl From<&UpgradePermitAuthorizationShare> for UpgradePermitAuthorizationShareId { + fn from(share: &UpgradePermitAuthorizationShare) -> Self { + share.id() + } +} + +impl From for pb::UpgradePermitAuthMessageId { + fn from(id: UpgradePermitAuthorizationShareId) -> Self { + Self { + hash: id.hash.clone().get().0, + height: id.height.get(), + } + } +} + +impl TryFrom for UpgradePermitAuthorizationShareId { + type Error = ProxyDecodeError; + + fn try_from(id: pb::UpgradePermitAuthMessageId) -> Result { + Ok(Self { + hash: CryptoHash(id.hash.clone()).into(), + height: Height::from(id.height), + }) + } +} diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index 0cf62e711c66..a17ab0acb22b 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -6,6 +6,7 @@ mod chain_key; mod execution_environment; mod ingress; mod self_validating; +mod upgrade; mod xnet; pub use self::{ @@ -24,8 +25,10 @@ pub use self::{ }, ingress::{IngressPayload, IngressPayloadError}, self_validating::{MAX_BITCOIN_PAYLOAD_IN_BYTES, SelfValidatingPayload}, + upgrade::{bytes_to_upgrade_payload, upgrade_payload_to_bytes}, xnet::XNetPayload, }; +use crate::consensus::upgrade::UpgradePermitAction; use crate::{ Height, Randomness, RegistryVersion, ReplicaVersion, SubnetId, Time, consensus::idkg::{IDkgMasterPublicKeyId, PreSigId, common::PreSignature}, @@ -177,6 +180,7 @@ pub struct BatchPayload { pub canister_http: Vec, pub query_stats: Vec, pub chain_key: Vec, + pub upgrade: Vec, } /// Batch properties collected form the last DKG summary block. @@ -202,6 +206,7 @@ pub struct BatchMessages { pub certified_stream_slices: BTreeMap, pub bitcoin_adapter_responses: Vec, pub query_stats: Option, + pub upgrade: Vec, } /// Error type that can occur during an `BatchPayload::into_messages` call @@ -209,6 +214,7 @@ pub struct BatchMessages { pub enum IntoMessagesError { IngressPayloadError(IngressPayloadError), QueryStatsPayloadError(ProxyDecodeError), + UpgradePayloadError(ProxyDecodeError), } impl BatchPayload { @@ -226,6 +232,12 @@ impl BatchPayload { bitcoin_adapter_responses: self.self_validating.0, query_stats: QueryStatsPayload::deserialize(&self.query_stats) .map_err(IntoMessagesError::QueryStatsPayloadError)?, + upgrade: if self.upgrade.is_empty() { + Vec::new() + } else { + bytes_to_upgrade_payload(&self.upgrade) + .map_err(IntoMessagesError::UpgradePayloadError)? + }, }) } @@ -237,6 +249,7 @@ impl BatchPayload { canister_http, query_stats, chain_key, + upgrade, } = &self; ingress.is_empty() @@ -245,6 +258,7 @@ impl BatchPayload { && canister_http.is_empty() && query_stats.is_empty() && chain_key.is_empty() + && upgrade.is_empty() } } @@ -405,6 +419,7 @@ mod tests { canister_http, query_stats, chain_key, + upgrade: _, } = BatchPayload::default(); assert_eq!(ingress.total_ids_size_estimate(), NumBytes::new(0)); @@ -429,6 +444,7 @@ mod tests { canister_http, query_stats, chain_key, + upgrade, } = &payload; assert!(ingress.is_empty()); @@ -437,6 +453,7 @@ mod tests { assert!(canister_http.is_empty()); assert!(query_stats.is_empty()); assert!(chain_key.is_empty()); + assert!(upgrade.is_empty()); } #[test] diff --git a/rs/types/types/src/batch/upgrade.rs b/rs/types/types/src/batch/upgrade.rs new file mode 100644 index 000000000000..45d1feb20d56 --- /dev/null +++ b/rs/types/types/src/batch/upgrade.rs @@ -0,0 +1,196 @@ +use ic_base_types::NumBytes; +use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; +use ic_protobuf::types::v1 as pb; +use pb::upgrade_action::Action; +use std::collections::BTreeMap; + +use super::{iterator_to_bytes, slice_to_messages}; +use crate::consensus::UpgradePermitAuthorizationRequest; +use crate::consensus::upgrade::UpgradePermitAction; +use crate::signature::{BasicSignature, BasicSignatureBatch}; + +/// Serializes a list of [`UpgradePermitAction`]s to a length-delimited protobuf +/// stream, respecting the `max_size` budget. Actions that don't fit are +/// silently dropped. +pub fn upgrade_payload_to_bytes(actions: Vec, max_size: NumBytes) -> Vec { + let message_iterator = actions.into_iter().map(pb::UpgradeAction::from); + iterator_to_bytes(message_iterator, max_size) +} + +/// Deserializes a length-delimited protobuf stream into a list of +/// [`UpgradePermitAction`]s. An empty byte slice yields an empty list. +pub fn bytes_to_upgrade_payload(data: &[u8]) -> Result, ProxyDecodeError> { + let messages: Vec = + slice_to_messages(data).map_err(ProxyDecodeError::DecodeError)?; + messages + .into_iter() + .map(UpgradePermitAction::try_from) + .collect() +} + +impl From for pb::UpgradeAction { + fn from(action: UpgradePermitAction) -> Self { + let proto_action = match action { + UpgradePermitAction::Request(request) => { + Action::RequestPermit(pb::RequestUpgradePermit { + request: Some(pb::UpgradePermitRequest::from(request)), + }) + } + UpgradePermitAction::Authorize { + request, + signatures, + } => Action::AuthorizePermit(pb::AuthorizeUpgradePermit { + request: Some(pb::UpgradePermitRequest::from(request)), + signatures: signatures + .signatures_map + .into_iter() + .map(|(signer, signature)| { + pb::BasicSignature::from(BasicSignature { signature, signer }) + }) + .collect(), + }), + UpgradePermitAction::Return { node } => Action::ReturnPermit(pb::ReturnUpgradePermit { + node: Some(crate::node_id_into_protobuf(node)), + }), + }; + Self { + action: Some(proto_action), + } + } +} + +impl TryFrom for UpgradePermitAction { + type Error = ProxyDecodeError; + + fn try_from(proto: pb::UpgradeAction) -> Result { + let action = proto + .action + .ok_or(ProxyDecodeError::MissingField("UpgradeAction::action"))?; + Ok(match action { + Action::RequestPermit(request) => UpgradePermitAction::Request(try_from_option_field( + request.request, + "RequestUpgradePermit::request", + )?), + Action::AuthorizePermit(authorize) => UpgradePermitAction::Authorize { + request: try_from_option_field( + authorize.request, + "AuthorizeUpgradePermit::request", + )?, + signatures: signature_batch(authorize.signatures)?, + }, + Action::ReturnPermit(return_permit) => UpgradePermitAction::Return { + node: crate::node_id_try_from_option(return_permit.node)?, + }, + }) + } +} + +/// Decodes a list of basic signatures over the same content into a +/// [`BasicSignatureBatch`], rejecting duplicate signers. +fn signature_batch( + signatures: Vec, +) -> Result, ProxyDecodeError> { + let mut signatures_map = BTreeMap::new(); + for signature in signatures { + let signature: BasicSignature = signature.try_into()?; + if signatures_map + .insert(signature.signer, signature.signature) + .is_some() + { + return Err(ProxyDecodeError::DuplicateEntry { + key: format!("{:?}", signature.signer), + v1: "signature".to_string(), + v2: "signature".to_string(), + }); + } + } + Ok(BasicSignatureBatch { signatures_map }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Height; + use crate::NodeId; + use ic_base_types::PrincipalId; + + fn node(node_index: u64) -> NodeId { + NodeId::from(PrincipalId::new_node_test_id(node_index)) + } + + #[test] + fn test_round_trip_request() { + let actions = vec![UpgradePermitAction::Request( + UpgradePermitAuthorizationRequest { + requestor: node(3), + request_height: Height::new(42), + }, + )]; + let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); + let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); + assert_eq!(actions, decoded); + } + + #[test] + fn test_round_trip_authorize() { + let actions = vec![UpgradePermitAction::Authorize { + request: UpgradePermitAuthorizationRequest { + requestor: node(5), + request_height: Height::new(3), + }, + signatures: BasicSignatureBatch { + signatures_map: BTreeMap::new(), + }, + }]; + let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); + let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); + assert_eq!(actions, decoded); + } + + #[test] + fn test_round_trip_return() { + let actions = vec![UpgradePermitAction::Return { node: node(7) }]; + let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); + let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); + assert_eq!(actions, decoded); + } + + #[test] + fn test_round_trip_empty() { + let bytes = upgrade_payload_to_bytes(vec![], NumBytes::new(u64::MAX)); + assert!(bytes.is_empty()); + let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); + assert!(decoded.is_empty()); + } + + #[test] + fn test_round_trip_multiple_actions() { + let actions = vec![ + UpgradePermitAction::Request(UpgradePermitAuthorizationRequest { + requestor: node(1), + request_height: Height::new(10), + }), + UpgradePermitAction::Authorize { + request: UpgradePermitAuthorizationRequest { + requestor: node(2), + request_height: Height::new(4), + }, + signatures: BasicSignatureBatch { + signatures_map: BTreeMap::new(), + }, + }, + UpgradePermitAction::Return { node: node(3) }, + ]; + let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); + let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); + assert_eq!(actions, decoded); + } + + #[test] + fn test_max_size_drops_overflow() { + // With max_size = 0, no actions should be encoded. + let actions = vec![UpgradePermitAction::Return { node: node(1) }]; + let bytes = upgrade_payload_to_bytes(actions, NumBytes::new(0)); + assert!(bytes.is_empty()); + } +} diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index 7e71f52647c8..f23ff9e4e14b 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -32,6 +32,7 @@ pub mod hashed; pub mod idkg; mod payload; pub mod thunk; +pub mod upgrade; pub use catchup::*; use hashed::Hashed; @@ -1298,6 +1299,7 @@ impl From<&Block> for pb::Block { canister_http_payload_bytes, query_stats_payload_bytes, chain_key_payload_bytes, + upgrade_payload_bytes, idkg_payload, ) = if payload.is_summary() { ( @@ -1308,6 +1310,7 @@ impl From<&Block> for pb::Block { vec![], vec![], vec![], + vec![], payload.as_summary().idkg.as_ref().map(|idkg| idkg.into()), ) } else { @@ -1320,6 +1323,7 @@ impl From<&Block> for pb::Block { batch.canister_http.clone(), batch.query_stats.clone(), batch.chain_key.clone(), + batch.upgrade.clone(), payload.as_data().idkg.as_ref().map(|idkg| idkg.into()), ) }; @@ -1338,6 +1342,7 @@ impl From<&Block> for pb::Block { canister_http_payload_bytes, query_stats_payload_bytes, chain_key_payload_bytes, + upgrade_payload_bytes, idkg_payload, payload_hash: block.payload.get_hash().clone().get().0, } @@ -1369,6 +1374,7 @@ impl TryFrom for Block { canister_http: block.canister_http_payload_bytes, query_stats: block.query_stats_payload_bytes, chain_key: block.chain_key_payload_bytes, + upgrade: block.upgrade_payload_bytes, }; let payload = match dkg_payload { @@ -1766,6 +1772,84 @@ impl ConsensusMessageHashable for EquivocationProof { } } +/// UpgradePermitAuthorizationRequest holds the values that are signed in an +/// upgrade permit authorization share. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] +pub struct UpgradePermitAuthorizationRequest { + pub requestor: NodeId, + pub request_height: Height, +} + +impl SignedBytesWithoutDomainSeparator for UpgradePermitAuthorizationRequest { + fn write_signed_bytes_without_domain_separator(&self, bytes: &mut Vec) { + serde_cbor::to_writer(bytes, &self).unwrap(); + } +} + +impl HasHeight for UpgradePermitAuthorizationRequest { + fn height(&self) -> Height { + self.request_height + } +} + +/// An upgrade permit authorization share is a basic signature share on an +/// [`UpgradePermitAuthorizationRequest`]. +pub type UpgradePermitAuthorizationShare = + Signed>; + +impl PbArtifact for UpgradePermitAuthorizationShare { + type PbId = pb::UpgradePermitAuthMessageId; + type PbIdError = ProxyDecodeError; + type PbMessage = pb::UpgradePermitAuthorizationShare; + type PbMessageError = ProxyDecodeError; +} + +impl From for pb::UpgradePermitRequest { + fn from(content: UpgradePermitAuthorizationRequest) -> Self { + pb::UpgradePermitRequest { + requestor: Some(node_id_into_protobuf(content.requestor)), + request_height: content.request_height.get(), + } + } +} + +impl TryFrom for UpgradePermitAuthorizationRequest { + type Error = ProxyDecodeError; + + fn try_from(content: pb::UpgradePermitRequest) -> Result { + Ok(UpgradePermitAuthorizationRequest { + requestor: node_id_try_from_option(content.requestor)?, + request_height: Height::from(content.request_height), + }) + } +} + +impl From for pb::UpgradePermitAuthorizationShare { + fn from(share: UpgradePermitAuthorizationShare) -> Self { + pb::UpgradePermitAuthorizationShare { + request: Some(pb::UpgradePermitRequest::from(share.content)), + signature: Some(pb::BasicSignature::from(share.signature)), + } + } +} + +impl TryFrom for UpgradePermitAuthorizationShare { + type Error = ProxyDecodeError; + + fn try_from(message: pb::UpgradePermitAuthorizationShare) -> Result { + let request = + try_from_option_field(message.request, "UpgradePermitAuthorizationShare::request")?; + let signature = try_from_option_field( + message.signature, + "UpgradePermitAuthorizationShare::signature", + )?; + Ok(UpgradePermitAuthorizationShare { + content: request, + signature, + }) + } +} + impl ConsensusMessageHashable for ConsensusMessage { fn get_id(&self) -> ConsensusMessageId { ConsensusMessageId { diff --git a/rs/types/types/src/consensus/upgrade.rs b/rs/types/types/src/consensus/upgrade.rs new file mode 100644 index 000000000000..761fc4a1f312 --- /dev/null +++ b/rs/types/types/src/consensus/upgrade.rs @@ -0,0 +1,39 @@ +//! Phase-2 quick-upgrade permit types. +//! +//! The permit flow works in three stages: +//! +//! 1. **Request**: A block maker includes `UpgradePermitAction::Request` in +//! its block when it wants to reboot. Validators check outstanding requests +//! the allowed max parallel reboots. +//! +//! 2. **Authorize**: After the request block is finalized, each node gossips an +//! [`crate::consensus::UpgradePermitAuthorizationShare`]. When a block maker +//! collects enough shares, it includes `UpgradePermitAction::Authorize` in its block. +//! +//! 3. **Return**: After rebooting, the node includes +//! `UpgradePermitAction::Return` to release the slot. + +use serde::{Deserialize, Serialize}; + +use crate::NodeId; +use crate::consensus::UpgradePermitAuthorizationRequest; +use crate::signature::BasicSignatureBatch; + +/// A single action in a block's upgrade payload section. A block may carry +/// multiple actions (e.g. `Request` for the block maker and `Authorize` for +/// another node). +#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] +pub enum UpgradePermitAction { + /// Request permission to reboot. The block maker requests for itself. + /// `request_height` is the height of the block containing this request, + /// used for timeout tracking. + Request(UpgradePermitAuthorizationRequest), + /// Authorize a node to reboot — the signed request and the basic + /// signatures over it collected from the staying members. + Authorize { + request: UpgradePermitAuthorizationRequest, + signatures: BasicSignatureBatch, + }, + /// Release a previously authorized permit (reboot complete). + Return { node: NodeId }, +} diff --git a/rs/types/types/src/crypto/hash.rs b/rs/types/types/src/crypto/hash.rs index 9ff78b917d10..fbb10202aacd 100644 --- a/rs/types/types/src/crypto/hash.rs +++ b/rs/types/types/src/crypto/hash.rs @@ -7,7 +7,7 @@ use crate::canister_http::{ use crate::consensus::{ Block, BlockMetadata, BlockPayload, CatchUpContent, CatchUpContentProtobufBytes, CatchUpShareContent, ConsensusMessage, EquivocationProof, FinalizationContent, HashedBlock, - NotarizationContent, RandomBeaconContent, RandomTapeContent, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, certification::{ Certification, CertificationContent, CertificationMessage, CertificationShare, }, @@ -70,6 +70,15 @@ mod private { impl CryptoHashDomainSeal for EquivocationProof {} impl CryptoHashDomainSeal for BlockPayload {} + impl CryptoHashDomainSeal for UpgradePermitAuthorizationRequest {} + impl CryptoHashDomainSeal + for Signed< + UpgradePermitAuthorizationRequest, + BasicSignature, + > + { + } + impl CryptoHashDomainSeal for RandomBeaconContent {} impl CryptoHashDomainSeal for Signed> {} impl CryptoHashDomainSeal @@ -222,6 +231,20 @@ impl CryptoHashDomain for EquivocationProof { } } +impl CryptoHashDomain for UpgradePermitAuthorizationRequest { + fn domain(&self) -> String { + DomainSeparator::UpgradePermitAuthorizationRequest.to_string() + } +} + +impl CryptoHashDomain + for Signed> +{ + fn domain(&self) -> String { + DomainSeparator::UpgradePermitAuthorizationShare.to_string() + } +} + impl CryptoHashDomain for BlockPayload { fn domain(&self) -> String { DomainSeparator::InmemoryPayload.to_string() diff --git a/rs/types/types/src/crypto/hash/domain_separator.rs b/rs/types/types/src/crypto/hash/domain_separator.rs index 419c0d3f4048..9f5659f56876 100644 --- a/rs/types/types/src/crypto/hash/domain_separator.rs +++ b/rs/types/types/src/crypto/hash/domain_separator.rs @@ -16,6 +16,8 @@ pub enum DomainSeparator { BlockMetadata, BlockMetadataProposal, EquivocationProof, + UpgradePermitAuthorizationRequest, + UpgradePermitAuthorizationShare, InmemoryPayload, RandomBeaconContent, RandomBeacon, @@ -80,6 +82,12 @@ impl DomainSeparator { DomainSeparator::BlockMetadata => "block_metadata_domain", DomainSeparator::BlockMetadataProposal => "block_metadata_proposal_domain", DomainSeparator::EquivocationProof => "equivocation_proof_domain", + DomainSeparator::UpgradePermitAuthorizationRequest => { + "upgrade_permit_authorization_request_domain" + } + DomainSeparator::UpgradePermitAuthorizationShare => { + "upgrade_permit_authorization_share_domain" + } DomainSeparator::InmemoryPayload => "inmemory_payload_domain", DomainSeparator::RandomBeaconContent => "random_beacon_content_domain", DomainSeparator::RandomBeacon => "random_beacon_domain", @@ -194,6 +202,14 @@ fn domain_separators_are_stable() { ("BlockMetadata", "block_metadata_domain"), ("BlockMetadataProposal", "block_metadata_proposal_domain"), ("EquivocationProof", "equivocation_proof_domain"), + ( + "UpgradePermitAuthorizationRequest", + "upgrade_permit_authorization_request_domain", + ), + ( + "UpgradePermitAuthorizationShare", + "upgrade_permit_authorization_share_domain", + ), ("InmemoryPayload", "inmemory_payload_domain"), ("RandomBeaconContent", "random_beacon_content_domain"), ("RandomBeacon", "random_beacon_domain"), diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 300fdb8b08f5..da6836af1e18 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -551,7 +551,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "764535296841f3db421a928cfadff3460be406d0182da64034eee623a9a97e99", + "c20a87578beb94df369dabfefc30c0d47d170c75d68236aae3b16335c0f21c4a", "Hash of CatchUpContent changed" ); } @@ -569,7 +569,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "7f183aaeb495159567a340b5bf61233cf3226141268febaee47de3e4c69cbc4b", + "db509a477f3ed01ec251325527e946b2e674f249d013bafc0d061620000a6e0d", "Hash of CatchUpShareContent changed" ); } @@ -617,7 +617,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "31f744bc26627fadbf1d73c66cb54603319a87966a488b6f41c4f0cfc1a30c89", + "33c4f3fb79a8520a4c1d6d814aa5bae53e5aa58ad517dfddec45be7dfd930053", "Hash of CatchUpPackage changed" ); } @@ -647,7 +647,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "bff423705e4cb96b7a391c4cccba8ed1ce441dabf2693ed5b9545a2b57d946bd", + "47648b17b0b80122fa1adc34a6d6e82ae8fb5af4a92b2495c41c91052ace1a10", "Hash of CatchUpPackageShare changed" ); } @@ -990,7 +990,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "b040378bc7d9d2b7c2e9067215eae6380a65316922369a1bc6d8376f31fe5d0a", + "5b8ca671118db0ed4f57939788881d95810b36f8d13a9954ecf2c57067e2b8d9", "Hash of Block changed" ); } @@ -1033,7 +1033,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "d591d695f67c644ddcc5315d96c25f00dede77c725859408ab7f113a18a0bf9a", + "9bb9a7c7dacd7513fc58d13b238740e2f8e282c3d6cb66bd3aef520904583ae9", "Hash of BlockProposal changed" ); } @@ -1070,7 +1070,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "c94d927dd7300814fef610a7560ba5a7775a859bb3511796cf23cfb59c038a4f", + "f289b64bb469c9aab1710c44b0b2fc778de9e5a552858eb10a566b8bc803d930", "Hash of BlockPayload changed" ); } diff --git a/rs/types/types/src/crypto/sign.rs b/rs/types/types/src/crypto/sign.rs index 2f0c2e03042b..0409177185ba 100644 --- a/rs/types/types/src/crypto/sign.rs +++ b/rs/types/types/src/crypto/sign.rs @@ -4,7 +4,7 @@ use super::hash::domain_separator::DomainSeparator; use crate::canister_http::CanisterHttpResponseReceipt; use crate::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, - NotarizationContent, RandomBeaconContent, RandomTapeContent, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, certification::CertificationContent, dkg::DealingContent, idkg::{IDkgComplaintContent, IDkgOpeningContent}, @@ -64,6 +64,7 @@ mod private { impl SignatureDomainSeal for DealingContent {} impl SignatureDomainSeal for NotarizationContent {} impl SignatureDomainSeal for FinalizationContent {} + impl SignatureDomainSeal for UpgradePermitAuthorizationRequest {} impl SignatureDomainSeal for IDkgDealing {} impl SignatureDomainSeal for SignedIDkgDealing {} impl SignatureDomainSeal for IDkgComplaintContent {} @@ -115,6 +116,12 @@ impl SignatureDomain for FinalizationContent { } } +impl SignatureDomain for UpgradePermitAuthorizationRequest { + fn domain(&self) -> Vec { + domain_with_prepended_length(DomainSeparator::UpgradePermitAuthorizationRequest.as_str()) + } +} + impl SignatureDomain for IDkgDealing { fn domain(&self) -> Vec { domain_with_prepended_length(DomainSeparator::IdkgDealing.as_str()) From f8f19104b47d0e377a87545259e6e2417ee39d92 Mon Sep 17 00:00:00 2001 From: David Frank Date: Fri, 11 Sep 2026 17:34:50 +0200 Subject: [PATCH 02/21] lint --- rs/consensus/upgrade/src/lib.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/rs/consensus/upgrade/src/lib.rs b/rs/consensus/upgrade/src/lib.rs index a8a448450bbe..bd8714f70376 100644 --- a/rs/consensus/upgrade/src/lib.rs +++ b/rs/consensus/upgrade/src/lib.rs @@ -108,14 +108,6 @@ mod tests { } } - fn verifying_crypto() -> MockCrypto { - let mut crypto = MockCrypto::new(); - crypto - .expect_verify_basic_sig_upgrade_permit_auth() - .returning(|_, _, _, _| Ok(())); - crypto - } - #[test] fn test_rejects_requestor_mismatch() { let share = share(2, 3, 10); From 6a82887dfc0e3cc82adf4760dbdf52e35d32d7ab Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 12:15:08 +0200 Subject: [PATCH 03/21] Revert some stuff that will be added in a later PR to keep PR small --- Cargo.lock | 19 - Cargo.toml | 1 - rs/artifact_pool/src/lib.rs | 1 - .../src/upgrade_permit_auth_pool.rs | 311 -------- rs/consensus/mocks/src/lib.rs | 7 - rs/consensus/upgrade/BUILD.bazel | 44 -- rs/consensus/upgrade/Cargo.toml | 24 - rs/consensus/upgrade/src/lib.rs | 199 ------ rs/consensus/upgrade/src/pool_manager.rs | 676 ------------------ 9 files changed, 1282 deletions(-) delete mode 100644 rs/artifact_pool/src/upgrade_permit_auth_pool.rs delete mode 100644 rs/consensus/upgrade/BUILD.bazel delete mode 100644 rs/consensus/upgrade/Cargo.toml delete mode 100644 rs/consensus/upgrade/src/lib.rs delete mode 100644 rs/consensus/upgrade/src/pool_manager.rs diff --git a/Cargo.lock b/Cargo.lock index 7c8bd90ecdce..fbbfcb5b434c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8322,25 +8322,6 @@ dependencies = [ "slog", ] -[[package]] -name = "ic-consensus-upgrade" -version = "0.9.0" -dependencies = [ - "ic-consensus-utils", - "ic-interfaces", - "ic-interfaces-mocks", - "ic-logger", - "ic-protobuf", - "ic-registry-client-fake", - "ic-registry-proto-data-provider", - "ic-test-utilities-consensus", - "ic-test-utilities-registry", - "ic-test-utilities-types", - "ic-types", - "num-traits", - "slog", -] - [[package]] name = "ic-consensus-utils" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index ebfda237de52..396f11547947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,6 @@ members = [ "rs/consensus/idkg", "rs/consensus/mocks", "rs/consensus/utils", - "rs/consensus/upgrade", "rs/consensus/chain_key", "rs/criterion_time", "rs/cross-chain/blob_store", diff --git a/rs/artifact_pool/src/lib.rs b/rs/artifact_pool/src/lib.rs index e205f7229ead..cec57a3ab2fe 100644 --- a/rs/artifact_pool/src/lib.rs +++ b/rs/artifact_pool/src/lib.rs @@ -11,7 +11,6 @@ mod metrics; mod pool_common; #[cfg(test)] mod test_utils; -pub mod upgrade_permit_auth_pool; pub mod backup; mod lmdb_iterator; diff --git a/rs/artifact_pool/src/upgrade_permit_auth_pool.rs b/rs/artifact_pool/src/upgrade_permit_auth_pool.rs deleted file mode 100644 index bae39df1f659..000000000000 --- a/rs/artifact_pool/src/upgrade_permit_auth_pool.rs +++ /dev/null @@ -1,311 +0,0 @@ -use crate::{ - metrics::{POOL_TYPE_UNVALIDATED, POOL_TYPE_VALIDATED}, - pool_common::{HasLabel, PoolSection}, -}; -use ic_interfaces::{ - p2p::consensus::{ - ArtifactTransmit, ArtifactTransmits, ArtifactWithOpt, MutablePool, UnvalidatedArtifact, - ValidatedPoolReader, - }, - upgrade::{UpgradePermitAuthChangeAction, UpgradePermitAuthChangeSet, UpgradePermitAuthPool}, -}; -use ic_logger::ReplicaLogger; -use ic_metrics::MetricsRegistry; -use ic_types::{ - artifact::{IdentifiableArtifact, UpgradePermitAuthorizationShareId}, - consensus::UpgradePermitAuthorizationShare, -}; -use prometheus::IntCounter; - -const POOL_NAME: &str = "upgrade_permit_auth"; - -type ValidatedSection = - PoolSection; -type UnvalidatedSection = PoolSection< - UpgradePermitAuthorizationShareId, - UnvalidatedArtifact, ->; - -/// Upgrade Permit Authorization Pool implementation. -pub struct UpgradePermitAuthPoolImpl { - validated: ValidatedSection, - unvalidated: UnvalidatedSection, - invalidated_artifacts: IntCounter, - log: ReplicaLogger, -} - -impl UpgradePermitAuthPoolImpl { - pub fn new(metrics: MetricsRegistry, log: ReplicaLogger) -> Self { - Self { - invalidated_artifacts: metrics.int_counter( - "upgrade_permit_auth_invalidated_artifacts", - "The number of invalidated upgrade permit auth artifacts", - ), - validated: PoolSection::new(metrics.clone(), POOL_NAME, POOL_TYPE_VALIDATED), - unvalidated: PoolSection::new(metrics, POOL_NAME, POOL_TYPE_UNVALIDATED), - log, - } - } -} - -impl UpgradePermitAuthPool for UpgradePermitAuthPoolImpl { - fn get_validated_shares( - &self, - ) -> Box + '_> { - Box::new(self.validated.values()) - } - - fn get_unvalidated_shares( - &self, - ) -> Box + '_> { - Box::new(self.unvalidated.values().map(|pa| &pa.message)) - } -} - -impl MutablePool for UpgradePermitAuthPoolImpl { - type Mutations = UpgradePermitAuthChangeSet; - - fn insert(&mut self, artifact: UnvalidatedArtifact) { - let id = artifact.message.id(); - self.unvalidated.insert(id, artifact); - } - - fn remove(&mut self, id: &UpgradePermitAuthorizationShareId) { - self.unvalidated.remove(id); - } - - fn apply( - &mut self, - change_set: UpgradePermitAuthChangeSet, - ) -> ArtifactTransmits { - let changed = !change_set.is_empty(); - let mut transmits = vec![]; - for action in change_set { - match action { - UpgradePermitAuthChangeAction::AddToValidated(share) => { - transmits.push(ArtifactTransmit::Deliver(ArtifactWithOpt { - artifact: share.clone(), - is_latency_sensitive: true, - })); - self.validated.insert(share.id(), share); - } - UpgradePermitAuthChangeAction::MoveToValidated(share) => { - let id = share.id(); - self.unvalidated.remove(&id); - transmits.push(ArtifactTransmit::Deliver(ArtifactWithOpt { - artifact: share.clone(), - is_latency_sensitive: true, - })); - self.validated.insert(id, share); - } - UpgradePermitAuthChangeAction::RemoveValidated(id) => { - if self.validated.remove(&id).is_some() { - transmits.push(ArtifactTransmit::Abort(id)); - } - } - UpgradePermitAuthChangeAction::RemoveUnvalidated(id) => { - self.unvalidated.remove(&id); - } - UpgradePermitAuthChangeAction::HandleInvalid(id, reason) => { - ic_logger::warn!( - self.log, - "Invalidating upgrade permit auth artifact {id:?}: {reason}" - ); - self.invalidated_artifacts.inc(); - self.unvalidated.remove(&id); - } - } - } - ArtifactTransmits { - transmits, - poll_immediately: changed, - } - } -} - -impl ValidatedPoolReader for UpgradePermitAuthPoolImpl { - fn get( - &self, - id: &UpgradePermitAuthorizationShareId, - ) -> Option { - self.validated.get(id).cloned() - } - - fn get_all_for_initial_broadcast( - &self, - ) -> Box + '_> { - // Not persisted — no initial broadcast on restart. - Box::new(std::iter::empty()) - } -} - -impl HasLabel for UpgradePermitAuthorizationShare { - fn label(&self) -> &str { - "upgrade_permit_auth_share" - } -} - -impl HasLabel for UnvalidatedArtifact { - fn label(&self) -> &str { - self.message.label() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ic_logger::replica_logger::no_op_logger; - use ic_test_utilities_types::ids::node_test_id; - use ic_types::Height; - use ic_types::consensus::UpgradePermitAuthorizationRequest; - use ic_types::crypto::{BasicSig, BasicSigOf}; - use ic_types::signature::BasicSignature; - use ic_types::time::UNIX_EPOCH; - - fn fake_share( - signer: u64, - requestor: u64, - request_height: u64, - ) -> UpgradePermitAuthorizationShare { - UpgradePermitAuthorizationShare { - content: UpgradePermitAuthorizationRequest { - requestor: node_test_id(requestor), - request_height: Height::from(request_height), - }, - signature: BasicSignature { - signature: BasicSigOf::new(BasicSig(vec![])), - signer: node_test_id(signer), - }, - } - } - - fn to_unvalidated( - share: UpgradePermitAuthorizationShare, - ) -> UnvalidatedArtifact { - UnvalidatedArtifact { - message: share, - peer_id: node_test_id(0), - timestamp: UNIX_EPOCH, - } - } - - fn pool() -> UpgradePermitAuthPoolImpl { - UpgradePermitAuthPoolImpl::new(MetricsRegistry::new(), no_op_logger()) - } - - #[test] - fn test_insert_and_remove_unvalidated() { - let mut pool = pool(); - let share = fake_share(1, 2, 10); - let id = share.id(); - - pool.insert(to_unvalidated(share.clone())); - assert!(pool.get_unvalidated_shares().eq([&share])); - assert!(pool.get(&id).is_none()); - - pool.remove(&id); - assert_eq!(pool.get_unvalidated_shares().count(), 0); - } - - #[test] - fn test_add_to_validated_broadcasts() { - let mut pool = pool(); - let share = fake_share(1, 2, 10); - - let result = pool.apply(vec![UpgradePermitAuthChangeAction::AddToValidated( - share.clone(), - )]); - - assert!(result.poll_immediately); - assert_eq!(result.transmits.len(), 1); - assert!(matches!( - &result.transmits[0], - ArtifactTransmit::Deliver(a) if a.artifact == share - )); - assert!(pool.get_validated_shares().eq([&share])); - assert_eq!(pool.get(&share.id()).unwrap(), share); - } - - #[test] - fn test_move_to_validated_replaces_unvalidated_and_broadcasts() { - let mut pool = pool(); - let share = fake_share(1, 2, 10); - pool.insert(to_unvalidated(share.clone())); - assert!(pool.get_unvalidated_shares().eq([&share])); - - let result = pool.apply(vec![UpgradePermitAuthChangeAction::MoveToValidated( - share.clone(), - )]); - - assert_eq!(result.transmits.len(), 1); - assert_eq!(pool.get_unvalidated_shares().count(), 0); - assert!(pool.get_validated_shares().eq([&share])); - assert_eq!(pool.get(&share.id()).unwrap(), share); - } - - #[test] - fn test_remove_validated_aborts_broadcast() { - let mut pool = pool(); - let share = fake_share(1, 2, 10); - pool.apply(vec![UpgradePermitAuthChangeAction::AddToValidated( - share.clone(), - )]); - - let result = pool.apply(vec![UpgradePermitAuthChangeAction::RemoveValidated( - share.id(), - )]); - - assert!(result.poll_immediately); - assert!(matches!(&result.transmits[0], ArtifactTransmit::Abort(id) if *id == share.id())); - assert_eq!(pool.get_validated_shares().count(), 0); - - // Removing again is a no-op without a redundant Abort. - let result = pool.apply(vec![UpgradePermitAuthChangeAction::RemoveValidated( - share.id(), - )]); - assert_eq!(result.transmits.len(), 0); - } - - #[test] - fn test_handle_invalid_drops_unvalidated_without_broadcast() { - let mut pool = pool(); - let share = fake_share(1, 2, 10); - pool.insert(to_unvalidated(share.clone())); - - let result = pool.apply(vec![UpgradePermitAuthChangeAction::HandleInvalid( - share.id(), - "bad signature".to_string(), - )]); - - assert!(result.poll_immediately); - assert!(result.transmits.is_empty()); - assert_eq!(pool.get_unvalidated_shares().count(), 0); - } - - #[test] - fn test_empty_change_set_does_not_poll() { - let mut pool = pool(); - let result = pool.apply(vec![]); - assert!(!result.poll_immediately); - assert!(result.transmits.is_empty()); - } - - #[test] - fn test_shares_keyed_by_signer_and_request() { - let mut pool = pool(); - pool.apply(vec![ - // Same signer, different requests: two entries. - UpgradePermitAuthChangeAction::AddToValidated(fake_share(1, 2, 10)), - UpgradePermitAuthChangeAction::AddToValidated(fake_share(1, 2, 11)), - UpgradePermitAuthChangeAction::AddToValidated(fake_share(3, 2, 10)), - UpgradePermitAuthChangeAction::AddToValidated(fake_share(4, 2, 10)), - ]); - assert_eq!(pool.get_validated_shares().count(), 4); - - // Re-adding the same (signer, request) pair overwrites. - pool.apply(vec![UpgradePermitAuthChangeAction::AddToValidated( - fake_share(1, 2, 10), - )]); - assert_eq!(pool.get_validated_shares().count(), 4); - } -} diff --git a/rs/consensus/mocks/src/lib.rs b/rs/consensus/mocks/src/lib.rs index acd72c5518d8..58194d60eb74 100644 --- a/rs/consensus/mocks/src/lib.rs +++ b/rs/consensus/mocks/src/lib.rs @@ -2,7 +2,6 @@ use ic_artifact_pool::{ canister_http_pool::CanisterHttpPoolImpl, dkg_pool::DkgPoolImpl, idkg_pool::IDkgPoolImpl, - upgrade_permit_auth_pool::UpgradePermitAuthPoolImpl, }; use ic_config::artifact_pool::ArtifactPoolConfig; use ic_consensus_utils::membership::Membership; @@ -115,7 +114,6 @@ pub struct Dependencies { pub dkg_pool: Arc>, pub idkg_pool: Arc>, pub canister_http_pool: Arc>, - pub upgrade_permit_auth_pool: Arc>, } pub struct DependenciesBuilder { @@ -283,10 +281,6 @@ impl DependenciesBuilder { Box::new(IDkgStatsNoOp {}), ))); let canister_http_pool = Arc::new(RwLock::new(CanisterHttpPoolImpl::new( - ic_metrics::MetricsRegistry::new(), - log.clone(), - ))); - let upgrade_permit_auth_pool = Arc::new(RwLock::new(UpgradePermitAuthPoolImpl::new( ic_metrics::MetricsRegistry::new(), log, ))); @@ -331,7 +325,6 @@ impl DependenciesBuilder { dkg_pool, idkg_pool, canister_http_pool, - upgrade_permit_auth_pool, } } } diff --git a/rs/consensus/upgrade/BUILD.bazel b/rs/consensus/upgrade/BUILD.bazel deleted file mode 100644 index b92bf577a379..000000000000 --- a/rs/consensus/upgrade/BUILD.bazel +++ /dev/null @@ -1,44 +0,0 @@ -load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_test") - -package(default_visibility = ["//visibility:public"]) - -rust_library( - name = "upgrade", - srcs = glob(["src/**/*.rs"]), - crate_features = select({ - "//conditions:default": [], - }), - crate_name = "ic_consensus_upgrade", - proc_macro_deps = [ - # Keep sorted. - ], - deps = [ - # Keep sorted. - "//rs/consensus/utils", - "//rs/interfaces", - "//rs/monitoring/logger", - "//rs/types/types", - "@crate_index//:num-traits", - "@crate_index//:slog", - ], -) - -rust_doc( - name = "consensus_upgrade_doc", - crate = ":upgrade", -) - -rust_test( - name = "upgrade_test", - crate = ":upgrade", - deps = [ - # Keep sorted. - "//rs/interfaces/mocks", - "//rs/protobuf", - "//rs/registry/fake", - "//rs/registry/proto_data_provider", - "//rs/test_utilities/consensus", - "//rs/test_utilities/registry", - "//rs/test_utilities/types", - ], -) diff --git a/rs/consensus/upgrade/Cargo.toml b/rs/consensus/upgrade/Cargo.toml deleted file mode 100644 index ba5ed86b6fce..000000000000 --- a/rs/consensus/upgrade/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "ic-consensus-upgrade" -version.workspace = true -authors.workspace = true -edition.workspace = true -description.workspace = true -documentation.workspace = true - -[dependencies] -ic-consensus-utils = { path = "../utils" } -ic-interfaces = { path = "../../interfaces" } -ic-logger = { path = "../../monitoring/logger" } -ic-types = { path = "../../types/types" } -num-traits = { workspace = true } -slog = { workspace = true } - -[dev-dependencies] -ic-interfaces-mocks = { path = "../../interfaces/mocks" } -ic-protobuf = { path = "../../protobuf" } -ic-registry-client-fake = { path = "../../registry/fake" } -ic-registry-proto-data-provider = { path = "../../registry/proto_data_provider" } -ic-test-utilities-consensus = { path = "../../test_utilities/consensus" } -ic-test-utilities-registry = { path = "../../test_utilities/registry" } -ic-test-utilities-types = { path = "../../test_utilities/types" } diff --git a/rs/consensus/upgrade/src/lib.rs b/rs/consensus/upgrade/src/lib.rs deleted file mode 100644 index bd8714f70376..000000000000 --- a/rs/consensus/upgrade/src/lib.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! The upgrade permit protocol for the Phase-2 rolling GuestOS reboots. - -use ic_consensus_utils::crypto::ConsensusCrypto; -use ic_consensus_utils::membership::Membership; -use ic_interfaces::upgrade::InvalidUpgradePayloadReason; -use ic_logger::{ReplicaLogger, warn}; -use ic_types::consensus::UpgradePermitAuthorizationShare; -use ic_types::{Height, NodeId, RegistryVersion}; -use std::collections::BTreeSet; - -pub mod pool_manager; - -pub(crate) struct SubnetMembership { - /// Current members staying even at the new CUP. - pub staying_members: BTreeSet, -} - -impl SubnetMembership { - /// Is the node a current member staying even at the new CUP? - pub fn staying(&self, node: &NodeId) -> bool { - self.staying_members.contains(node) - } -} - -/// Subnet membership at the block height and registry version. -pub(crate) fn subnet_membership( - membership: &Membership, - block_height: Height, - block_registry_version: RegistryVersion, - logger: &ReplicaLogger, -) -> SubnetMembership { - let registry_at_height: BTreeSet = membership - .get_nodes_at_version(block_registry_version) - .map(|nodes| nodes.into_iter().collect()) - .unwrap_or_default(); - let current_members: BTreeSet = match membership.get_nodes(block_height) { - Ok(nodes) => nodes.into_iter().collect(), - Err(e) => { - warn!( - logger, - "upgrade_payload: couldn't determine the committee at height {block_height:?}: {e:?}" - ); - registry_at_height.clone() - } - }; - let staying_members = current_members - .intersection(®istry_at_height) - .cloned() - .collect(); - SubnetMembership { staying_members } -} - -/// Check a share's content, staying signer, and signature. Returns the -/// signer. -pub(crate) fn validate_share( - share: &UpgradePermitAuthorizationShare, - requestor: NodeId, - request_height: Height, - membership: &SubnetMembership, - registry_version: RegistryVersion, - crypto: &dyn ConsensusCrypto, -) -> Result { - let signer = share.signature.signer; - if share.content.requestor != requestor || share.content.request_height != request_height { - return Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { signer }); - } - if !membership.staying(&signer) { - return Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { signer }); - } - crypto - .verify_basic_sig( - &share.signature.signature, - &share.content, - signer, - registry_version, - ) - .map_err(|_| InvalidUpgradePayloadReason::AuthorizeInvalidShare { signer })?; - Ok(signer) -} - -#[cfg(test)] -mod tests { - use super::*; - use ic_interfaces_mocks::crypto::MockCrypto; - use ic_test_utilities_types::ids::node_test_id; - use ic_types::consensus::UpgradePermitAuthorizationRequest; - use ic_types::crypto::{BasicSig, BasicSigOf, CryptoError}; - use ic_types::signature::BasicSignature; - - const REGISTRY_VERSION: RegistryVersion = RegistryVersion::new(1); - - fn share(signer: u64, requestor: u64, request_height: u64) -> UpgradePermitAuthorizationShare { - UpgradePermitAuthorizationShare { - content: UpgradePermitAuthorizationRequest { - requestor: node_test_id(requestor), - request_height: Height::from(request_height), - }, - signature: BasicSignature { - signature: BasicSigOf::new(BasicSig(vec![])), - signer: node_test_id(signer), - }, - } - } - - fn membership(staying: &[NodeId]) -> SubnetMembership { - SubnetMembership { - staying_members: staying.iter().copied().collect(), - } - } - - #[test] - fn test_rejects_requestor_mismatch() { - let share = share(2, 3, 10); - let membership = membership(&[node_test_id(1), node_test_id(2)]); - // No verify expectation: the share is rejected before verification. - let result = validate_share( - &share, - node_test_id(4), - Height::from(10), - &membership, - REGISTRY_VERSION, - &MockCrypto::new(), - ); - assert_eq!( - result, - Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { - signer: node_test_id(2) - }) - ); - } - - #[test] - fn test_rejects_request_height_mismatch() { - let share = share(2, 3, 10); - let membership = membership(&[node_test_id(1), node_test_id(2)]); - let result = validate_share( - &share, - node_test_id(3), - Height::from(11), - &membership, - REGISTRY_VERSION, - &MockCrypto::new(), - ); - assert_eq!( - result, - Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { - signer: node_test_id(2) - }) - ); - } - - #[test] - fn test_rejects_non_staying_signer() { - let share = share(2, 3, 10); - let membership = membership(&[node_test_id(1)]); - let result = validate_share( - &share, - node_test_id(3), - Height::from(10), - &membership, - REGISTRY_VERSION, - &MockCrypto::new(), - ); - assert_eq!( - result, - Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { - signer: node_test_id(2) - }) - ); - } - - #[test] - fn test_rejects_invalid_signature() { - let share = share(2, 3, 10); - let membership = membership(&[node_test_id(1), node_test_id(2)]); - let mut crypto = MockCrypto::new(); - crypto - .expect_verify_basic_sig_upgrade_permit_auth() - .returning(|_, _, _, _| { - Err(CryptoError::TransientInternalError { - internal_error: "boom".to_string(), - }) - }); - let result = validate_share( - &share, - node_test_id(3), - Height::from(10), - &membership, - REGISTRY_VERSION, - &crypto, - ); - assert_eq!( - result, - Err(InvalidUpgradePayloadReason::AuthorizeInvalidShare { - signer: node_test_id(2) - }) - ); - } -} diff --git a/rs/consensus/upgrade/src/pool_manager.rs b/rs/consensus/upgrade/src/pool_manager.rs deleted file mode 100644 index 7e74f7e3dd0d..000000000000 --- a/rs/consensus/upgrade/src/pool_manager.rs +++ /dev/null @@ -1,676 +0,0 @@ -use std::collections::BTreeSet; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -use crate::{SubnetMembership, subnet_membership, validate_share}; -use ic_consensus_utils::crypto::ConsensusCrypto; -use ic_consensus_utils::membership::Membership; -use ic_interfaces::consensus_pool::ConsensusBlockCache; -use ic_interfaces::p2p::consensus::{Bouncer, BouncerFactory, BouncerValue, PoolMutationsProducer}; -use ic_interfaces::upgrade::{ - UpgradePermitAuthChangeAction, UpgradePermitAuthChangeSet, UpgradePermitAuthPool, -}; -use ic_logger::{ReplicaLogger, info, warn}; -use ic_types::artifact::IdentifiableArtifact; -use ic_types::batch::bytes_to_upgrade_payload; -use ic_types::consensus::{Block, UpgradePermitAuthorizationShare, upgrade::UpgradePermitAction}; -use ic_types::{Height, NodeId}; -use num_traits::SaturatingSub; - -/// Shares whose request height falls this many blocks below the finalized -/// tip are purged from the pool. -const SHARE_EXPIRY_BLOCKS: Height = Height::new(20); - -/// Signs shares for requests in finalized blocks, validates gossiped shares, -/// and purges expired ones. -pub struct UpgradePermitAuthPoolManager { - node_id: NodeId, - crypto: Arc, - consensus_pool_cache: Arc, - membership: Arc, - /// Requests we've already signed (node, request_height). - signed_requests: Mutex>, - /// Last finalized height we scanned for requests. - last_scanned: Mutex, - logger: ReplicaLogger, -} - -impl UpgradePermitAuthPoolManager { - pub fn new( - node_id: NodeId, - crypto: Arc, - consensus_pool_cache: Arc, - membership: Arc, - logger: ReplicaLogger, - ) -> Self { - Self { - node_id, - crypto, - consensus_pool_cache, - membership, - signed_requests: Mutex::new(BTreeSet::new()), - last_scanned: Mutex::new(Height::from(0)), - logger, - } - } - - /// Membership at the finalized block's own height and registry version. - fn block_membership(&self, block: &Block) -> SubnetMembership { - subnet_membership( - &self.membership, - block.height, - block.context.registry_version, - &self.logger, - ) - } - - /// Scan finalized blocks for new `Request` actions and sign an auth share - /// for each one we haven't signed yet. - fn sign_shares_for_new_requests(&self) -> UpgradePermitAuthChangeSet { - let chain = self.consensus_pool_cache.finalized_chain(); - let tip = chain.tip().height; - let mut last = self.last_scanned.lock().unwrap(); - let start = last.increment(); - if start > tip { - return vec![]; - } - *last = tip; - - let mut signed = self.signed_requests.lock().unwrap(); - let mut change_set = vec![]; - - for height_num in start.get()..=tip.get() { - let height = Height::from(height_num); - let Ok(block) = chain.get_block_by_height(height) else { - continue; - }; - let payload = block.payload.as_ref(); - if payload.is_summary() { - continue; - } - let upgrade_bytes = &payload.as_data().batch.upgrade; - if upgrade_bytes.is_empty() { - continue; - } - let Ok(actions) = bytes_to_upgrade_payload(upgrade_bytes) else { - continue; - }; - let membership = self.block_membership(block); - if !membership.staying(&self.node_id) { - continue; - } - for action in actions { - let UpgradePermitAction::Request(request) = action else { - continue; - }; - let key = (request.requestor, request.request_height); - if signed.contains(&key) { - continue; - } - match self - .crypto - .sign(&request, self.node_id, block.context.registry_version) - { - Ok(signature) => { - signed.insert(key); - info!( - self.logger, - "permit_auth: signed share for node {:?} at height {:?}", - request.requestor, - request.request_height - ); - change_set.push(UpgradePermitAuthChangeAction::AddToValidated( - UpgradePermitAuthorizationShare { - content: request, - signature, - }, - )); - } - Err(e) => { - warn!( - self.logger, - "permit_auth: failed to sign share for node {:?}: {:?}", - request.requestor, - e - ); - } - } - } - } - change_set - } - - /// Validate gossiped shares found in the unvalidated section of the pool. - fn validate_gossiped_shares( - &self, - pool: &dyn UpgradePermitAuthPool, - ) -> UpgradePermitAuthChangeSet { - let chain = self.consensus_pool_cache.finalized_chain(); - let mut change_set = vec![]; - - for share in pool.get_unvalidated_shares() { - let Ok(block) = chain.get_block_by_height(share.content.request_height) else { - change_set.push(UpgradePermitAuthChangeAction::HandleInvalid( - share.id(), - format!( - "block at request_height {:?} not found in finalized chain", - share.content.request_height - ), - )); - continue; - }; - let membership = self.block_membership(block); - match validate_share( - share, - share.content.requestor, - share.content.request_height, - &membership, - block.context.registry_version, - self.crypto.as_ref(), - ) { - Ok(_) => { - change_set.push(UpgradePermitAuthChangeAction::MoveToValidated( - share.clone(), - )); - } - Err(reason) => { - warn!( - self.logger, - "permit_auth: dropping invalid share: {reason:?}", - ); - change_set.push(UpgradePermitAuthChangeAction::HandleInvalid( - share.id(), - format!("invalid share: {reason:?}"), - )); - } - } - } - - change_set - } - - /// Purge shares (both validated and unvalidated) whose request has expired - /// (the request height is older than `REQUEST_TIMEOUT_BLOCKS` below the - /// current finalized height). - fn purge_expired_shares(&self, pool: &dyn UpgradePermitAuthPool) -> UpgradePermitAuthChangeSet { - let current_height = self.consensus_pool_cache.finalized_chain().tip().height; - let expiry_threshold = current_height.saturating_sub(&SHARE_EXPIRY_BLOCKS); - - let expired_validated = pool - .get_validated_shares() - .filter(|share| share.content.request_height < expiry_threshold) - .map(|share| UpgradePermitAuthChangeAction::RemoveValidated(share.into())); - - let expired_unvalidated = pool - .get_unvalidated_shares() - .filter(|share| share.content.request_height < expiry_threshold) - .map(|share| UpgradePermitAuthChangeAction::RemoveUnvalidated(share.into())); - - expired_validated.chain(expired_unvalidated).collect() - } -} - -impl PoolMutationsProducer for UpgradePermitAuthPoolManager { - type Mutations = UpgradePermitAuthChangeSet; - - fn on_state_change(&self, pool: &T) -> Self::Mutations { - let mut change_set = self.sign_shares_for_new_requests(); - change_set.extend(self.validate_gossiped_shares(pool)); - change_set.extend(self.purge_expired_shares(pool)); - change_set - } -} - -/// Bouncer that accepts all upgrade permit auth shares. -pub struct UpgradePermitAuthBouncer; - -impl BouncerFactory - for UpgradePermitAuthBouncer -{ - fn new_bouncer( - &self, - _pool: &Pool, - ) -> Bouncer { - Box::new(|_id| BouncerValue::Wants) - } - - fn refresh_period(&self) -> Duration { - Duration::from_secs(60) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ic_interfaces::consensus_pool::{ConsensusBlockChain, ConsensusBlockChainErr}; - use ic_interfaces_mocks::crypto::MockCrypto; - use ic_logger::replica_logger::no_op_logger; - use ic_protobuf::types::v1 as pb; - use ic_registry_client_fake::FakeRegistryClient; - use ic_registry_proto_data_provider::ProtoRegistryDataProvider; - use ic_test_utilities_consensus::{FakeConsensusPoolCache, fake::Fake, make_genesis}; - use ic_test_utilities_registry::{SubnetRecordBuilder, add_single_subnet_record}; - use ic_test_utilities_types::ids::{node_test_id, subnet_test_id, test_replica_version}; - use ic_types::NumBytes; - use ic_types::RegistryVersion; - use ic_types::artifact::{IdentifiableArtifact, UpgradePermitAuthorizationShareId}; - use ic_types::batch::{BatchPayload, ValidationContext, upgrade_payload_to_bytes}; - use ic_types::consensus::upgrade::UpgradePermitAction; - use ic_types::consensus::{ - BlockPayload, DataPayload, Payload, Rank, UpgradePermitAuthorizationRequest, - dkg::{DkgDataPayload, DkgSummary}, - }; - use ic_types::crypto::{ - BasicSig, BasicSigOf, CryptoError, CryptoHash, CryptoHashOf, crypto_hash, - }; - use ic_types::signature::BasicSignature; - use ic_types::time::UNIX_EPOCH; - use std::collections::BTreeMap; - use std::ops::RangeInclusive; - - fn registry_version() -> RegistryVersion { - RegistryVersion::from(1) - } - - fn members() -> Vec { - vec![node_test_id(1), node_test_id(2), node_test_id(3)] - } - - fn signing_crypto() -> MockCrypto { - let mut crypto = MockCrypto::new(); - crypto - .expect_sign_basic_upgrade_permit_auth() - .returning(|_| Ok(BasicSigOf::new(BasicSig(vec![])))); - crypto - } - - fn verifying_crypto() -> MockCrypto { - let mut crypto = MockCrypto::new(); - crypto - .expect_verify_basic_sig_upgrade_permit_auth() - .returning(|_, _, _, _| Ok(())); - crypto - } - - fn membership_of(members: &[NodeId]) -> Arc { - let data_provider = Arc::new(ProtoRegistryDataProvider::new()); - add_single_subnet_record( - &data_provider, - registry_version().get(), - subnet_test_id(1), - SubnetRecordBuilder::default() - .with_membership(members) - .build(), - ); - let registry = Arc::new(FakeRegistryClient::new(Arc::clone(&data_provider) as Arc<_>)); - registry.update_to_latest_version(); - let cup = make_genesis(DkgSummary::fake()); - let consensus_cache = Arc::new(FakeConsensusPoolCache::new(pb::CatchUpPackage::from(cup))); - Arc::new(Membership::new( - consensus_cache, - registry, - subnet_test_id(1), - )) - } - - fn genesis_block() -> Block { - make_genesis(DkgSummary::fake()).content.block.into_inner() - } - - fn data_block(height: u64, actions: &[UpgradePermitAction]) -> Block { - Block::new( - CryptoHashOf::new(CryptoHash(vec![0; 32])), - Payload::new( - crypto_hash, - BlockPayload::Data(DataPayload { - batch: BatchPayload { - upgrade: upgrade_payload_to_bytes( - actions.to_vec(), - NumBytes::new(u64::MAX), - ), - ..BatchPayload::default() - }, - dkg: DkgDataPayload::new_empty(Height::from(height)), - idkg: None, - }), - ), - Height::from(height), - Rank(0), - ValidationContext { - registry_version: registry_version(), - certified_height: Height::from(height), - time: UNIX_EPOCH, - }, - test_replica_version(), - ) - } - - fn empty_blocks(heights: RangeInclusive) -> Vec { - heights.map(|height| data_block(height, &[])).collect() - } - - struct FakeFinalizedChain { - blocks: Vec, - } - - impl ConsensusBlockChain for FakeFinalizedChain { - fn tip(&self) -> &Block { - self.blocks.last().unwrap() - } - - fn get_block_by_height(&self, height: Height) -> Result<&Block, ConsensusBlockChainErr> { - self.blocks - .iter() - .find(|block| block.height == height) - .ok_or(ConsensusBlockChainErr::BlockNotFound(height)) - } - - fn len(&self) -> usize { - self.blocks.len() - } - - fn iter_above(&self, height: Height) -> Box + '_> { - Box::new(self.blocks.iter().filter(move |b| b.height > height)) - } - } - - struct FakeBlockCache { - chain: Arc, - } - - impl ConsensusBlockCache for FakeBlockCache { - fn finalized_chain(&self) -> Arc { - self.chain.clone() - } - } - - fn pool_manager( - node_id: NodeId, - members: &[NodeId], - blocks: Vec, - crypto: MockCrypto, - ) -> UpgradePermitAuthPoolManager { - let mut chain = vec![genesis_block()]; - chain.extend(blocks); - UpgradePermitAuthPoolManager::new( - node_id, - Arc::new(crypto), - Arc::new(FakeBlockCache { - chain: Arc::new(FakeFinalizedChain { blocks: chain }), - }), - membership_of(members), - no_op_logger(), - ) - } - - struct FakePool { - validated: BTreeMap, - unvalidated: BTreeMap, - } - - impl FakePool { - fn new() -> Self { - Self { - validated: BTreeMap::new(), - unvalidated: BTreeMap::new(), - } - } - - fn with_validated(mut self, share: UpgradePermitAuthorizationShare) -> Self { - self.validated.insert(share.id(), share); - self - } - - fn with_unvalidated(mut self, share: UpgradePermitAuthorizationShare) -> Self { - self.unvalidated.insert(share.id(), share); - self - } - } - - impl UpgradePermitAuthPool for FakePool { - fn get_validated_shares( - &self, - ) -> Box + '_> { - Box::new(self.validated.values()) - } - - fn get_unvalidated_shares( - &self, - ) -> Box + '_> { - Box::new(self.unvalidated.values()) - } - } - - fn share(signer: u64, requestor: u64, request_height: u64) -> UpgradePermitAuthorizationShare { - UpgradePermitAuthorizationShare { - content: UpgradePermitAuthorizationRequest { - requestor: node_test_id(requestor), - request_height: Height::from(request_height), - }, - signature: BasicSignature { - signature: BasicSigOf::new(BasicSig(vec![])), - signer: node_test_id(signer), - }, - } - } - - fn request(requestor: u64, request_height: u64) -> UpgradePermitAction { - UpgradePermitAction::Request(UpgradePermitAuthorizationRequest { - requestor: node_test_id(requestor), - request_height: Height::from(request_height), - }) - } - - fn assert_single_add_to_validated( - change_set: &UpgradePermitAuthChangeSet, - expected: &UpgradePermitAuthorizationShare, - ) { - assert_eq!(change_set.len(), 1); - assert!(matches!( - &change_set[0], - UpgradePermitAuthChangeAction::AddToValidated(s) if s == expected - )); - } - - fn assert_single_handle_invalid( - change_set: &UpgradePermitAuthChangeSet, - expected: &UpgradePermitAuthorizationShare, - ) { - assert_eq!(change_set.len(), 1); - assert!(matches!( - &change_set[0], - UpgradePermitAuthChangeAction::HandleInvalid(id, _) if id == &expected.id() - )); - } - - #[test] - fn test_signs_share_for_request_in_finalized_block() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(1, &[request(2, 1)])], - signing_crypto(), - ); - let change_set = manager.on_state_change(&FakePool::new()); - assert_single_add_to_validated(&change_set, &share(1, 2, 1)); - } - - #[test] - fn test_does_not_sign_when_node_leaving_subnet() { - let manager = pool_manager( - node_test_id(4), - &members(), - vec![data_block(1, &[request(2, 1)])], - signing_crypto(), - ); - assert!(manager.on_state_change(&FakePool::new()).is_empty()); - } - - #[test] - fn test_ignores_blocks_without_requests() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(1, &[]), data_block(2, &[])], - signing_crypto(), - ); - assert!(manager.on_state_change(&FakePool::new()).is_empty()); - } - - #[test] - fn test_does_not_rescan_finalized_blocks() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(1, &[request(2, 1)])], - signing_crypto(), - ); - assert_single_add_to_validated(&manager.on_state_change(&FakePool::new()), &share(1, 2, 1)); - assert!(manager.on_state_change(&FakePool::new()).is_empty()); - } - - #[test] - fn test_deduplicates_repeated_requests() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![ - data_block(1, &[request(2, 1)]), - data_block(2, &[request(2, 1)]), - ], - signing_crypto(), - ); - let change_set = manager.on_state_change(&FakePool::new()); - assert_single_add_to_validated(&change_set, &share(1, 2, 1)); - } - - #[test] - fn test_signing_failure_yields_no_action() { - let mut crypto = MockCrypto::new(); - crypto - .expect_sign_basic_upgrade_permit_auth() - .returning(|_| { - Err(CryptoError::TransientInternalError { - internal_error: "boom".to_string(), - }) - }); - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(1, &[request(2, 1)])], - crypto, - ); - assert!(manager.on_state_change(&FakePool::new()).is_empty()); - } - - #[test] - fn test_validates_gossiped_share() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(5, &[])], - verifying_crypto(), - ); - let gossiped = share(2, 3, 5); - let pool = FakePool::new().with_unvalidated(gossiped.clone()); - let change_set = manager.on_state_change(&pool); - assert_eq!(change_set.len(), 1); - assert!(matches!( - &change_set[0], - UpgradePermitAuthChangeAction::MoveToValidated(s) if s == &gossiped - )); - } - - #[test] - fn test_drops_share_with_invalid_signature() { - let mut crypto = MockCrypto::new(); - crypto - .expect_verify_basic_sig_upgrade_permit_auth() - .returning(|_, _, _, _| { - Err(CryptoError::TransientInternalError { - internal_error: "boom".to_string(), - }) - }); - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(5, &[])], - crypto, - ); - let gossiped = share(2, 3, 5); - let pool = FakePool::new().with_unvalidated(gossiped.clone()); - assert_single_handle_invalid(&manager.on_state_change(&pool), &gossiped); - } - - #[test] - fn test_drops_share_from_non_staying_signer() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(5, &[])], - verifying_crypto(), - ); - let gossiped = share(9, 3, 5); - let pool = FakePool::new().with_unvalidated(gossiped.clone()); - assert_single_handle_invalid(&manager.on_state_change(&pool), &gossiped); - } - - #[test] - fn test_drops_share_without_request_block() { - let manager = pool_manager( - node_test_id(1), - &members(), - vec![data_block(5, &[])], - verifying_crypto(), - ); - let gossiped = share(2, 3, 99); - let pool = FakePool::new().with_unvalidated(gossiped.clone()); - assert_single_handle_invalid(&manager.on_state_change(&pool), &gossiped); - } - - #[test] - fn test_purges_expired_validated_shares() { - // Threshold is tip (100) - SHARE_EXPIRY_BLOCKS (20) = 80: shares with - // request height below 80 are purged, at or above it are kept. - let manager = pool_manager( - node_test_id(1), - &members(), - empty_blocks(1..=100), - MockCrypto::new(), - ); - let expired = share(2, 3, 79); - let pool = FakePool::new() - .with_validated(expired.clone()) - .with_validated(share(3, 2, 80)); - let change_set = manager.on_state_change(&pool); - assert_eq!(change_set.len(), 1); - assert!(matches!( - &change_set[0], - UpgradePermitAuthChangeAction::RemoveValidated(id) if id == &expired.id() - )); - } - - #[test] - fn test_purges_expired_unvalidated_shares() { - // The share is both validated (moved out of unvalidated) and purged - // (removed from unvalidated); applying both leaves it validated only. - let manager = pool_manager( - node_test_id(1), - &members(), - empty_blocks(1..=100), - verifying_crypto(), - ); - let expired = share(2, 3, 79); - let pool = FakePool::new().with_unvalidated(expired.clone()); - let change_set = manager.on_state_change(&pool); - assert_eq!(change_set.len(), 2); - assert!(matches!( - &change_set[0], - UpgradePermitAuthChangeAction::MoveToValidated(s) if s == &expired - )); - assert!(matches!( - &change_set[1], - UpgradePermitAuthChangeAction::RemoveUnvalidated(id) if id == &expired.id() - )); - } -} From f2a3e65e3419faab5f9562c1de63cfccade87409 Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 13:01:20 +0200 Subject: [PATCH 04/21] "against" --- rs/types/types/src/consensus/upgrade.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/types/types/src/consensus/upgrade.rs b/rs/types/types/src/consensus/upgrade.rs index 761fc4a1f312..dedd484c5d86 100644 --- a/rs/types/types/src/consensus/upgrade.rs +++ b/rs/types/types/src/consensus/upgrade.rs @@ -4,7 +4,7 @@ //! //! 1. **Request**: A block maker includes `UpgradePermitAction::Request` in //! its block when it wants to reboot. Validators check outstanding requests -//! the allowed max parallel reboots. +//! against the allowed max parallel reboots. //! //! 2. **Authorize**: After the request block is finalized, each node gossips an //! [`crate::consensus::UpgradePermitAuthorizationShare`]. When a block maker From 3b60c88699f8b81cd33f1d84b5aed44ac92fa522 Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 13:01:28 +0200 Subject: [PATCH 05/21] Hash tests --- rs/types/types/src/crypto/hash/tests.rs | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index da6836af1e18..b3ff7a3ad6b1 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -79,6 +79,7 @@ mod crypto_hash_stability { EquivocationProof, Finalization, FinalizationContent, FinalizationShare, HashedBlock, HashedRandomBeacon, Notarization, NotarizationContent, NotarizationShare, Payload, RandomBeacon, RandomBeaconContent, RandomTapeContent, Rank, + UpgradePermitAuthorizationRequest, UpgradePermitAuthorizationShare, certification::{ Certification, CertificationContent, CertificationMessage, CertificationShare, }, @@ -452,6 +453,42 @@ mod crypto_hash_stability { ); } + /// Test stability of UpgradePermitAuthorizationRequest hash output + #[test] + fn upgrade_permit_authorization_request_stability() { + let data = UpgradePermitAuthorizationRequest { + requestor: NodeId::from(PrincipalId::new_node_test_id(42)), + request_height: Height::from(42), + }; + let hash = crypto_hash(&data); + assert_eq!( + hex::encode(hash.get_ref().0.as_slice()), + "c01cc8564217818aaedb7d2441000413c44b75e5a7769ad25b8f7f30fe9b15a4", + "Hash of UpgradePermitAuthorizationRequest changed" + ); + } + + /// Test stability of UpgradePermitAuthorizationShare hash output + #[test] + fn upgrade_permit_authorization_share_stability() { + let data: UpgradePermitAuthorizationShare = Signed { + content: UpgradePermitAuthorizationRequest { + requestor: NodeId::from(PrincipalId::new_node_test_id(42)), + request_height: Height::from(42), + }, + signature: BasicSignature { + signature: BasicSigOf::new(BasicSig(vec![0x42; 64])), + signer: NodeId::from(PrincipalId::new_node_test_id(42)), + }, + }; + let hash = crypto_hash(&data); + assert_eq!( + hex::encode(hash.get_ref().0.as_slice()), + "c8468fda9b05e8d21600642039b055bc97fc86226395b84f36ac351c00451bec", + "Hash of UpgradePermitAuthorizationShare changed" + ); + } + /// Test stability of CertificationContent hash output #[test] fn certification_content_stability() { From 85f87cf90314c99669a3aa3c5070fc300b56000d Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 15:12:20 +0200 Subject: [PATCH 06/21] Add UpgradePayload wrapper --- rs/protobuf/def/types/v1/upgrade.proto | 4 + rs/protobuf/src/gen/types/types.v1.rs | 5 + rs/state_machine_tests/src/lib.rs | 4 +- rs/types/types/src/batch.rs | 13 +- rs/types/types/src/batch/upgrade.rs | 213 ++++++++++++++++--------- rs/types/types/src/consensus.rs | 6 +- 6 files changed, 157 insertions(+), 88 deletions(-) diff --git a/rs/protobuf/def/types/v1/upgrade.proto b/rs/protobuf/def/types/v1/upgrade.proto index f5f92595b17c..deedd6d4150a 100644 --- a/rs/protobuf/def/types/v1/upgrade.proto +++ b/rs/protobuf/def/types/v1/upgrade.proto @@ -7,6 +7,10 @@ package types.v1; import "types/v1/signature.proto"; import "types/v1/types.proto"; +message UpgradePayload { + repeated UpgradeAction actions = 1; +} + message UpgradeAction { oneof action { RequestUpgradePermit request_permit = 1; diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index db3df960d9c1..3d677ca7ae23 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1862,6 +1862,11 @@ impl ChainKeyErrorCode { } } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct UpgradePayload { + #[prost(message, repeated, tag = "1")] + pub actions: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpgradeAction { #[prost(oneof = "upgrade_action::Action", tags = "1, 2, 3")] pub action: ::core::option::Option, diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index fe7d4cfac623..868785531237 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -149,7 +149,7 @@ use ic_types::{ batch::{ Batch, BatchContent, BatchMessages, BatchSummary, BlockmakerMetrics, CanisterHttpSpent, ChainKeyData, ConsensusResponse, QueryStatsPayload, SelfValidatingPayload, TotalQueryStats, - ValidationContext, XNetPayload, + UpgradePayload, ValidationContext, XNetPayload, }, canister_http::{ CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpRequestId, @@ -3172,7 +3172,7 @@ impl StateMachine { .map(|p| p.get().to_vec()) .unwrap_or_default(), query_stats: payload.query_stats, - upgrade: vec![], + upgrade: UpgradePayload::default(), }, chain_key_data: ChainKeyData { master_public_keys: self.chain_key_subnet_public_keys.clone(), diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index a17ab0acb22b..47266b3b60c4 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -25,10 +25,9 @@ pub use self::{ }, ingress::{IngressPayload, IngressPayloadError}, self_validating::{MAX_BITCOIN_PAYLOAD_IN_BYTES, SelfValidatingPayload}, - upgrade::{bytes_to_upgrade_payload, upgrade_payload_to_bytes}, + upgrade::UpgradePayload, xnet::XNetPayload, }; -use crate::consensus::upgrade::UpgradePermitAction; use crate::{ Height, Randomness, RegistryVersion, ReplicaVersion, SubnetId, Time, consensus::idkg::{IDkgMasterPublicKeyId, PreSigId, common::PreSignature}, @@ -206,7 +205,7 @@ pub struct BatchMessages { pub certified_stream_slices: BTreeMap, pub bitcoin_adapter_responses: Vec, pub query_stats: Option, - pub upgrade: Vec, + pub upgrade: UpgradePayload, } /// Error type that can occur during an `BatchPayload::into_messages` call @@ -232,12 +231,8 @@ impl BatchPayload { bitcoin_adapter_responses: self.self_validating.0, query_stats: QueryStatsPayload::deserialize(&self.query_stats) .map_err(IntoMessagesError::QueryStatsPayloadError)?, - upgrade: if self.upgrade.is_empty() { - Vec::new() - } else { - bytes_to_upgrade_payload(&self.upgrade) - .map_err(IntoMessagesError::UpgradePayloadError)? - }, + upgrade: UpgradePayload::deserialize(&self.upgrade) + .map_err(IntoMessagesError::UpgradePayloadError)?, }) } diff --git a/rs/types/types/src/batch/upgrade.rs b/rs/types/types/src/batch/upgrade.rs index 45d1feb20d56..30a16622e4f7 100644 --- a/rs/types/types/src/batch/upgrade.rs +++ b/rs/types/types/src/batch/upgrade.rs @@ -2,34 +2,61 @@ use ic_base_types::NumBytes; use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; use ic_protobuf::types::v1 as pb; use pb::upgrade_action::Action; +use prost::Message as _; +use prost::encoding::encoded_len_varint; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -use super::{iterator_to_bytes, slice_to_messages}; use crate::consensus::UpgradePermitAuthorizationRequest; use crate::consensus::upgrade::UpgradePermitAction; use crate::signature::{BasicSignature, BasicSignatureBatch}; -/// Serializes a list of [`UpgradePermitAction`]s to a length-delimited protobuf -/// stream, respecting the `max_size` budget. Actions that don't fit are -/// silently dropped. -pub fn upgrade_payload_to_bytes(actions: Vec, max_size: NumBytes) -> Vec { - let message_iterator = actions.into_iter().map(pb::UpgradeAction::from); - iterator_to_bytes(message_iterator, max_size) +/// The upgrade permit actions of a block's batch payload. +#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize, Serialize)] +pub struct UpgradePayload { + pub actions: Vec, } -/// Deserializes a length-delimited protobuf stream into a list of -/// [`UpgradePermitAction`]s. An empty byte slice yields an empty list. -pub fn bytes_to_upgrade_payload(data: &[u8]) -> Result, ProxyDecodeError> { - let messages: Vec = - slice_to_messages(data).map_err(ProxyDecodeError::DecodeError)?; - messages - .into_iter() - .map(UpgradePermitAction::try_from) - .collect() +impl UpgradePayload { + /// Serialize this payload into a vector. + /// + /// This function will drop actions that do not fit to guarantee that the + /// payload fits into the `byte_limit`. Smaller actions after a dropped + /// action can still be included. + pub fn serialize_with_limit(&self, byte_limit: NumBytes) -> Vec { + let mut proto = pb::UpgradePayload::default(); + let mut remaining = byte_limit.get() as usize; + for action in &self.actions { + let entry = pb::UpgradeAction::from(action); + // One repeated field entry: the key, the varint length, and the + // message bytes. + let entry_len = + 1 + encoded_len_varint(entry.encoded_len() as u64) + entry.encoded_len(); + if entry_len > remaining { + continue; + } + remaining -= entry_len; + proto.actions.push(entry); + } + proto.encode_to_vec() + } + + /// Deserializes an [`UpgradePayload`]. An empty byte slice yields an empty + /// payload. + pub fn deserialize(data: &[u8]) -> Result { + let proto = pb::UpgradePayload::decode(data).map_err(ProxyDecodeError::DecodeError)?; + Ok(Self { + actions: proto + .actions + .into_iter() + .map(UpgradePermitAction::try_from) + .collect::>()?, + }) + } } -impl From for pb::UpgradeAction { - fn from(action: UpgradePermitAction) -> Self { +impl From<&UpgradePermitAction> for pb::UpgradeAction { + fn from(action: &UpgradePermitAction) -> Self { let proto_action = match action { UpgradePermitAction::Request(request) => { Action::RequestPermit(pb::RequestUpgradePermit { @@ -43,14 +70,17 @@ impl From for pb::UpgradeAction { request: Some(pb::UpgradePermitRequest::from(request)), signatures: signatures .signatures_map - .into_iter() + .iter() .map(|(signer, signature)| { - pb::BasicSignature::from(BasicSignature { signature, signer }) + pb::BasicSignature::from(BasicSignature { + signature: signature.clone(), + signer: *signer, + }) }) .collect(), }), UpgradePermitAction::Return { node } => Action::ReturnPermit(pb::ReturnUpgradePermit { - node: Some(crate::node_id_into_protobuf(node)), + node: Some(crate::node_id_into_protobuf(*node)), }), }; Self { @@ -112,85 +142,120 @@ mod tests { use super::*; use crate::Height; use crate::NodeId; + use crate::crypto::{BasicSig, BasicSigOf}; use ic_base_types::PrincipalId; fn node(node_index: u64) -> NodeId { NodeId::from(PrincipalId::new_node_test_id(node_index)) } + fn round_trip(payload: UpgradePayload) { + let bytes = payload.serialize_with_limit(NumBytes::new(u64::MAX)); + let decoded = UpgradePayload::deserialize(&bytes).unwrap(); + assert_eq!(payload, decoded); + } + #[test] fn test_round_trip_request() { - let actions = vec![UpgradePermitAction::Request( - UpgradePermitAuthorizationRequest { - requestor: node(3), - request_height: Height::new(42), - }, - )]; - let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); - let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); - assert_eq!(actions, decoded); + round_trip(UpgradePayload { + actions: vec![UpgradePermitAction::Request( + UpgradePermitAuthorizationRequest { + requestor: node(3), + request_height: Height::new(42), + }, + )], + }); } #[test] fn test_round_trip_authorize() { - let actions = vec![UpgradePermitAction::Authorize { - request: UpgradePermitAuthorizationRequest { - requestor: node(5), - request_height: Height::new(3), - }, - signatures: BasicSignatureBatch { - signatures_map: BTreeMap::new(), - }, - }]; - let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); - let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); - assert_eq!(actions, decoded); + round_trip(UpgradePayload { + actions: vec![UpgradePermitAction::Authorize { + request: UpgradePermitAuthorizationRequest { + requestor: node(5), + request_height: Height::new(3), + }, + signatures: BasicSignatureBatch { + signatures_map: BTreeMap::new(), + }, + }], + }); } #[test] fn test_round_trip_return() { - let actions = vec![UpgradePermitAction::Return { node: node(7) }]; - let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); - let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); - assert_eq!(actions, decoded); + round_trip(UpgradePayload { + actions: vec![UpgradePermitAction::Return { node: node(7) }], + }); } #[test] fn test_round_trip_empty() { - let bytes = upgrade_payload_to_bytes(vec![], NumBytes::new(u64::MAX)); - assert!(bytes.is_empty()); - let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); - assert!(decoded.is_empty()); + round_trip(UpgradePayload { actions: vec![] }); } #[test] - fn test_round_trip_multiple_actions() { - let actions = vec![ - UpgradePermitAction::Request(UpgradePermitAuthorizationRequest { - requestor: node(1), - request_height: Height::new(10), - }), - UpgradePermitAction::Authorize { - request: UpgradePermitAuthorizationRequest { - requestor: node(2), - request_height: Height::new(4), - }, - signatures: BasicSignatureBatch { - signatures_map: BTreeMap::new(), + fn test_serialize_with_limit_drops_overflow() { + // A limit of 0 cannot fit any action, so nothing is serialized. + let payload = UpgradePayload { + actions: vec![UpgradePermitAction::Return { node: node(1) }], + }; + assert!(payload.serialize_with_limit(NumBytes::new(0)).is_empty()); + } + + #[test] + fn test_serialize_with_limit_skips_actions_that_do_not_fit() { + // The authorize action does not fit the limit, but the smaller return + // action after it still does. + let payload = UpgradePayload { + actions: vec![ + UpgradePermitAction::Authorize { + request: UpgradePermitAuthorizationRequest { + requestor: node(1), + request_height: Height::new(4), + }, + signatures: BasicSignatureBatch { + signatures_map: BTreeMap::from([( + node(2), + BasicSigOf::new(BasicSig(vec![0x42; 64])), + )]), + }, }, - }, - UpgradePermitAction::Return { node: node(3) }, - ]; - let bytes = upgrade_payload_to_bytes(actions.clone(), NumBytes::new(u64::MAX)); - let decoded = bytes_to_upgrade_payload(&bytes).unwrap(); - assert_eq!(actions, decoded); + UpgradePermitAction::Return { node: node(3) }, + ], + }; + let return_entry_len = UpgradePayload { + actions: vec![UpgradePermitAction::Return { node: node(3) }], + } + .serialize_with_limit(NumBytes::new(u64::MAX)) + .len(); + let bytes = payload.serialize_with_limit(NumBytes::new(return_entry_len as u64)); + let decoded = UpgradePayload::deserialize(&bytes).unwrap(); + assert_eq!( + decoded.actions, + vec![UpgradePermitAction::Return { node: node(3) }] + ); } #[test] - fn test_max_size_drops_overflow() { - // With max_size = 0, no actions should be encoded. - let actions = vec![UpgradePermitAction::Return { node: node(1) }]; - let bytes = upgrade_payload_to_bytes(actions, NumBytes::new(0)); - assert!(bytes.is_empty()); + fn test_round_trip_multiple_actions() { + round_trip(UpgradePayload { + actions: vec![ + UpgradePermitAction::Request(UpgradePermitAuthorizationRequest { + requestor: node(1), + request_height: Height::new(10), + }), + UpgradePermitAction::Authorize { + request: UpgradePermitAuthorizationRequest { + requestor: node(2), + request_height: Height::new(4), + }, + signatures: BasicSignatureBatch { + signatures_map: BTreeMap::new(), + }, + }, + UpgradePermitAction::Return { node: node(3) }, + ], + }); } } diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index f23ff9e4e14b..54c0f73031fa 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -1804,8 +1804,8 @@ impl PbArtifact for UpgradePermitAuthorizationShare { type PbMessageError = ProxyDecodeError; } -impl From for pb::UpgradePermitRequest { - fn from(content: UpgradePermitAuthorizationRequest) -> Self { +impl From<&UpgradePermitAuthorizationRequest> for pb::UpgradePermitRequest { + fn from(content: &UpgradePermitAuthorizationRequest) -> Self { pb::UpgradePermitRequest { requestor: Some(node_id_into_protobuf(content.requestor)), request_height: content.request_height.get(), @@ -1827,7 +1827,7 @@ impl TryFrom for UpgradePermitAuthorizationRequest { impl From for pb::UpgradePermitAuthorizationShare { fn from(share: UpgradePermitAuthorizationShare) -> Self { pb::UpgradePermitAuthorizationShare { - request: Some(pb::UpgradePermitRequest::from(share.content)), + request: Some(pb::UpgradePermitRequest::from(&share.content)), signature: Some(pb::BasicSignature::from(share.signature)), } } From 433eb1530123f1d20c422db706b461ab1798094e Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 15:14:56 +0200 Subject: [PATCH 07/21] default_batch_payload_is_zero_bytes for upgrade --- rs/types/types/src/batch.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index 47266b3b60c4..a04a5a3dd11b 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -414,7 +414,7 @@ mod tests { canister_http, query_stats, chain_key, - upgrade: _, + upgrade, } = BatchPayload::default(); assert_eq!(ingress.total_ids_size_estimate(), NumBytes::new(0)); @@ -423,6 +423,7 @@ mod tests { assert_eq!(canister_http.len(), 0); assert_eq!(query_stats.len(), 0); assert_eq!(chain_key.len(), 0); + assert_eq!(upgrade.len(), 0); } /// This is a quick test to check the invariant, that the [`Default`] implementation From 71384a74172fca9e5e51f2d6f01fb881f526e567 Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 19:45:52 +0200 Subject: [PATCH 08/21] Do not include upgrade_payload_bytes in the Block yet (will come later) --- rs/consensus/src/consensus/payload_builder.rs | 1 - rs/protobuf/def/types/v1/consensus.proto | 2 -- rs/protobuf/src/gen/types/types.v1.rs | 2 -- rs/state_machine_tests/src/lib.rs | 3 +-- rs/test_utilities/types/src/batch/payload.rs | 1 - rs/types/types/src/batch.rs | 11 ----------- rs/types/types/src/consensus.rs | 5 ----- rs/types/types/src/crypto/hash/tests.rs | 14 +++++++------- 8 files changed, 8 insertions(+), 31 deletions(-) diff --git a/rs/consensus/src/consensus/payload_builder.rs b/rs/consensus/src/consensus/payload_builder.rs index b8be5bc84817..22ccfe686f34 100644 --- a/rs/consensus/src/consensus/payload_builder.rs +++ b/rs/consensus/src/consensus/payload_builder.rs @@ -547,7 +547,6 @@ pub(crate) mod test { canister_http: settings.http_outcalls_payload_to_return, query_stats: settings.query_stats_payload_to_return, chain_key: settings.chain_key_payload_to_return, - upgrade: vec![], }, dkg: DkgDataPayload::new_empty(Height::from(0)), idkg: None, diff --git a/rs/protobuf/def/types/v1/consensus.proto b/rs/protobuf/def/types/v1/consensus.proto index 6ddb0a528950..8b25ad0d351a 100644 --- a/rs/protobuf/def/types/v1/consensus.proto +++ b/rs/protobuf/def/types/v1/consensus.proto @@ -10,7 +10,6 @@ import "registry/subnet/v1/subnet.proto"; import "types/v1/artifact.proto"; import "types/v1/dkg.proto"; import "types/v1/idkg.proto"; -import "types/v1/signature.proto"; import "types/v1/types.proto"; message CertificationMessage { @@ -70,7 +69,6 @@ message Block { bytes canister_http_payload_bytes = 15; bytes query_stats_payload_bytes = 16; bytes chain_key_payload_bytes = 17; - bytes upgrade_payload_bytes = 18; bytes payload_hash = 11; } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 3d677ca7ae23..9520b12182dc 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1496,8 +1496,6 @@ pub struct Block { pub query_stats_payload_bytes: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "17")] pub chain_key_payload_bytes: ::prost::alloc::vec::Vec, - #[prost(bytes = "vec", tag = "18")] - pub upgrade_payload_bytes: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "11")] pub payload_hash: ::prost::alloc::vec::Vec, } diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 868785531237..9dd5b533d7fc 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -149,7 +149,7 @@ use ic_types::{ batch::{ Batch, BatchContent, BatchMessages, BatchSummary, BlockmakerMetrics, CanisterHttpSpent, ChainKeyData, ConsensusResponse, QueryStatsPayload, SelfValidatingPayload, TotalQueryStats, - UpgradePayload, ValidationContext, XNetPayload, + ValidationContext, XNetPayload, }, canister_http::{ CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpRequestId, @@ -3172,7 +3172,6 @@ impl StateMachine { .map(|p| p.get().to_vec()) .unwrap_or_default(), query_stats: payload.query_stats, - upgrade: UpgradePayload::default(), }, chain_key_data: ChainKeyData { master_public_keys: self.chain_key_subnet_public_keys.clone(), diff --git a/rs/test_utilities/types/src/batch/payload.rs b/rs/test_utilities/types/src/batch/payload.rs index d39aefd0e96a..aaa5e3f9a9df 100644 --- a/rs/test_utilities/types/src/batch/payload.rs +++ b/rs/test_utilities/types/src/batch/payload.rs @@ -15,7 +15,6 @@ impl Default for PayloadBuilder { canister_http: vec![], query_stats: vec![], chain_key: vec![], - upgrade: vec![], }, } } diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index a04a5a3dd11b..fd53d8825f53 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -179,7 +179,6 @@ pub struct BatchPayload { pub canister_http: Vec, pub query_stats: Vec, pub chain_key: Vec, - pub upgrade: Vec, } /// Batch properties collected form the last DKG summary block. @@ -205,7 +204,6 @@ pub struct BatchMessages { pub certified_stream_slices: BTreeMap, pub bitcoin_adapter_responses: Vec, pub query_stats: Option, - pub upgrade: UpgradePayload, } /// Error type that can occur during an `BatchPayload::into_messages` call @@ -213,7 +211,6 @@ pub struct BatchMessages { pub enum IntoMessagesError { IngressPayloadError(IngressPayloadError), QueryStatsPayloadError(ProxyDecodeError), - UpgradePayloadError(ProxyDecodeError), } impl BatchPayload { @@ -231,8 +228,6 @@ impl BatchPayload { bitcoin_adapter_responses: self.self_validating.0, query_stats: QueryStatsPayload::deserialize(&self.query_stats) .map_err(IntoMessagesError::QueryStatsPayloadError)?, - upgrade: UpgradePayload::deserialize(&self.upgrade) - .map_err(IntoMessagesError::UpgradePayloadError)?, }) } @@ -244,7 +239,6 @@ impl BatchPayload { canister_http, query_stats, chain_key, - upgrade, } = &self; ingress.is_empty() @@ -253,7 +247,6 @@ impl BatchPayload { && canister_http.is_empty() && query_stats.is_empty() && chain_key.is_empty() - && upgrade.is_empty() } } @@ -414,7 +407,6 @@ mod tests { canister_http, query_stats, chain_key, - upgrade, } = BatchPayload::default(); assert_eq!(ingress.total_ids_size_estimate(), NumBytes::new(0)); @@ -423,7 +415,6 @@ mod tests { assert_eq!(canister_http.len(), 0); assert_eq!(query_stats.len(), 0); assert_eq!(chain_key.len(), 0); - assert_eq!(upgrade.len(), 0); } /// This is a quick test to check the invariant, that the [`Default`] implementation @@ -440,7 +431,6 @@ mod tests { canister_http, query_stats, chain_key, - upgrade, } = &payload; assert!(ingress.is_empty()); @@ -449,7 +439,6 @@ mod tests { assert!(canister_http.is_empty()); assert!(query_stats.is_empty()); assert!(chain_key.is_empty()); - assert!(upgrade.is_empty()); } #[test] diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index 54c0f73031fa..79ddbe42a8f0 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -1299,7 +1299,6 @@ impl From<&Block> for pb::Block { canister_http_payload_bytes, query_stats_payload_bytes, chain_key_payload_bytes, - upgrade_payload_bytes, idkg_payload, ) = if payload.is_summary() { ( @@ -1310,7 +1309,6 @@ impl From<&Block> for pb::Block { vec![], vec![], vec![], - vec![], payload.as_summary().idkg.as_ref().map(|idkg| idkg.into()), ) } else { @@ -1323,7 +1321,6 @@ impl From<&Block> for pb::Block { batch.canister_http.clone(), batch.query_stats.clone(), batch.chain_key.clone(), - batch.upgrade.clone(), payload.as_data().idkg.as_ref().map(|idkg| idkg.into()), ) }; @@ -1342,7 +1339,6 @@ impl From<&Block> for pb::Block { canister_http_payload_bytes, query_stats_payload_bytes, chain_key_payload_bytes, - upgrade_payload_bytes, idkg_payload, payload_hash: block.payload.get_hash().clone().get().0, } @@ -1374,7 +1370,6 @@ impl TryFrom for Block { canister_http: block.canister_http_payload_bytes, query_stats: block.query_stats_payload_bytes, chain_key: block.chain_key_payload_bytes, - upgrade: block.upgrade_payload_bytes, }; let payload = match dkg_payload { diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index b3ff7a3ad6b1..4131de9b9af4 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -588,7 +588,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "c20a87578beb94df369dabfefc30c0d47d170c75d68236aae3b16335c0f21c4a", + "764535296841f3db421a928cfadff3460be406d0182da64034eee623a9a97e99", "Hash of CatchUpContent changed" ); } @@ -606,7 +606,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "db509a477f3ed01ec251325527e946b2e674f249d013bafc0d061620000a6e0d", + "7f183aaeb495159567a340b5bf61233cf3226141268febaee47de3e4c69cbc4b", "Hash of CatchUpShareContent changed" ); } @@ -654,7 +654,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "33c4f3fb79a8520a4c1d6d814aa5bae53e5aa58ad517dfddec45be7dfd930053", + "31f744bc26627fadbf1d73c66cb54603319a87966a488b6f41c4f0cfc1a30c89", "Hash of CatchUpPackage changed" ); } @@ -684,7 +684,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "47648b17b0b80122fa1adc34a6d6e82ae8fb5af4a92b2495c41c91052ace1a10", + "bff423705e4cb96b7a391c4cccba8ed1ce441dabf2693ed5b9545a2b57d946bd", "Hash of CatchUpPackageShare changed" ); } @@ -1027,7 +1027,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "5b8ca671118db0ed4f57939788881d95810b36f8d13a9954ecf2c57067e2b8d9", + "b040378bc7d9d2b7c2e9067215eae6380a65316922369a1bc6d8376f31fe5d0a", "Hash of Block changed" ); } @@ -1070,7 +1070,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "9bb9a7c7dacd7513fc58d13b238740e2f8e282c3d6cb66bd3aef520904583ae9", + "d591d695f67c644ddcc5315d96c25f00dede77c725859408ab7f113a18a0bf9a", "Hash of BlockProposal changed" ); } @@ -1107,7 +1107,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "f289b64bb469c9aab1710c44b0b2fc778de9e5a552858eb10a566b8bc803d930", + "c94d927dd7300814fef610a7560ba5a7775a859bb3511796cf23cfb59c038a4f", "Hash of BlockPayload changed" ); } From 4b590b9b8348fb7199e6c39764dee5e2dbbd856d Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 20:00:53 +0200 Subject: [PATCH 09/21] Revert upgrade.rs --- rs/interfaces/src/consensus.rs | 2 -- rs/interfaces/src/lib.rs | 1 - rs/interfaces/src/upgrade.rs | 55 ---------------------------------- 3 files changed, 58 deletions(-) delete mode 100644 rs/interfaces/src/upgrade.rs diff --git a/rs/interfaces/src/consensus.rs b/rs/interfaces/src/consensus.rs index f4d1e46bdef6..03348807090e 100644 --- a/rs/interfaces/src/consensus.rs +++ b/rs/interfaces/src/consensus.rs @@ -15,7 +15,6 @@ use crate::{ InvalidSelfValidatingPayloadReason, SelfValidatingPayloadValidationError, SelfValidatingPayloadValidationFailure, }, - upgrade::InvalidUpgradePayloadReason, validation::{ValidationError, ValidationResult}, }; use ic_base_types::{NumBytes, SubnetId}; @@ -76,7 +75,6 @@ pub enum InvalidPayloadReason { InvalidCanisterHttpPayload(InvalidCanisterHttpPayloadReason), InvalidQueryStatsPayload(InvalidQueryStatsPayloadReason), InvalidChainKeyPayload(InvalidChainKeyPayloadReason), - InvalidUpgradePayload(InvalidUpgradePayloadReason), /// The overall block size is too large, even though the individual payloads are valid PayloadTooBig { expected: NumBytes, diff --git a/rs/interfaces/src/lib.rs b/rs/interfaces/src/lib.rs index 01682b3c21d8..be56ff4b6e90 100644 --- a/rs/interfaces/src/lib.rs +++ b/rs/interfaces/src/lib.rs @@ -19,7 +19,6 @@ pub mod p2p; pub mod query_stats; pub mod self_validating_payload; pub mod time_source; -pub mod upgrade; pub mod validation; // Note [Associated Types in Interfaces] diff --git a/rs/interfaces/src/upgrade.rs b/rs/interfaces/src/upgrade.rs deleted file mode 100644 index a8551eba2385..000000000000 --- a/rs/interfaces/src/upgrade.rs +++ /dev/null @@ -1,55 +0,0 @@ -use ic_types::NodeId; -use ic_types::artifact::UpgradePermitAuthorizationShareId; -use ic_types::consensus::UpgradePermitAuthorizationShare; - -#[derive(Debug, Eq, PartialEq)] -pub enum InvalidUpgradePayloadReason { - /// A `Request` was issued for a node other than the block maker. - RequestNodeMismatch { node: NodeId, proposer: NodeId }, - /// A `Return` was issued for a node other than the block maker. - ReturnNodeMismatch { node: NodeId, proposer: NodeId }, - /// The number of outstanding permits (requested or authorized) meets - /// the subnet's maximum number of rebooting nodes. - SlotsExhausted { slots_in_use: usize, permits: usize }, - /// An `Authorize` was issued for a node with no outstanding request. - AuthorizeNoOutstandingRequest { node: NodeId }, - /// An `Authorize` contains an invalid share (bad signature, content - /// mismatch, or signer is not a member). - AuthorizeInvalidShare { signer: NodeId }, - /// An `Authorize` does not carry enough valid shares (≥ the active - /// staying nodes). - AuthorizeInsufficientShares { collected: usize, threshold: usize }, - /// Failed to decode the upgrade payload from protobuf. - DecodeFailed(String), -} - -/// Change actions that can be applied to the [`UpgradePermitAuthPool`]. -#[derive(Debug)] -pub enum UpgradePermitAuthChangeAction { - /// Add a locally-produced share directly to validated. - AddToValidated(UpgradePermitAuthorizationShare), - /// Move a gossiped share from unvalidated to validated (after signature - /// verification). - MoveToValidated(UpgradePermitAuthorizationShare), - /// Remove a validated share (e.g. after the request was authorized or - /// timed out). - RemoveValidated(UpgradePermitAuthorizationShareId), - /// Remove an unvalidated share. - RemoveUnvalidated(UpgradePermitAuthorizationShareId), - /// Handle an invalid share (bad signature, no matching request, etc.). - HandleInvalid(UpgradePermitAuthorizationShareId, String), -} - -pub type UpgradePermitAuthChangeSet = Vec; - -/// Query interface for the upgrade permit authorization pool. -pub trait UpgradePermitAuthPool: Send + Sync { - /// Return an iterator over all validated shares. - fn get_validated_shares( - &self, - ) -> Box + '_>; - /// Return an iterator over all unvalidated shares. - fn get_unvalidated_shares( - &self, - ) -> Box + '_>; -} From 1f74dc2f26fce1ffac2a847206ee88ec8808cec8 Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 22:19:05 +0200 Subject: [PATCH 10/21] feat: Fast upgrades: upgrade payload section in blocks Add the upgrade section to block payloads and wire it into the payload builder infrastructure (with a placeholder builder for now). - Add `upgrade_payload_bytes` to the `Block` proto and upgrade to `BatchPayload`/`BatchMessages` - Add an `Upgrade` section to `BatchPayloadSectionBuilder` - Add the `ic-consensus-upgrade` crate with a placeholder `UpgradePayloadBuilder` --- Cargo.lock | 12 +++ Cargo.toml | 1 + rs/consensus/BUILD.bazel | 3 + rs/consensus/Cargo.toml | 1 + rs/consensus/benches/validate_payload.rs | 1 + rs/consensus/src/consensus.rs | 3 + rs/consensus/src/consensus/payload.rs | 59 +++++++++++ rs/consensus/src/consensus/payload_builder.rs | 26 ++++- rs/consensus/tests/framework/runner.rs | 1 + rs/consensus/tests/framework/types.rs | 3 + rs/consensus/tests/payload.rs | 4 + rs/consensus/upgrade/BUILD.bazel | 35 ++++++ rs/consensus/upgrade/Cargo.toml | 14 +++ rs/consensus/upgrade/src/lib.rs | 3 + rs/consensus/upgrade/src/payload_builder.rs | 100 ++++++++++++++++++ rs/interfaces/src/consensus.rs | 2 + rs/interfaces/src/lib.rs | 1 + rs/interfaces/src/upgrade.rs | 6 ++ rs/protobuf/def/types/v1/consensus.proto | 2 + rs/protobuf/src/gen/types/types.v1.rs | 2 + rs/replica/setup_ic_network/BUILD.bazel | 2 + rs/replica/setup_ic_network/Cargo.toml | 1 + rs/replica/setup_ic_network/src/lib.rs | 4 + rs/state_machine_tests/BUILD.bazel | 2 + rs/state_machine_tests/Cargo.toml | 1 + rs/state_machine_tests/src/lib.rs | 8 +- rs/test_utilities/types/src/batch/payload.rs | 1 + rs/types/types/src/batch.rs | 11 ++ rs/types/types/src/consensus.rs | 5 + rs/types/types/src/crypto/hash/tests.rs | 14 +-- 30 files changed, 316 insertions(+), 12 deletions(-) create mode 100644 rs/consensus/upgrade/BUILD.bazel create mode 100644 rs/consensus/upgrade/Cargo.toml create mode 100644 rs/consensus/upgrade/src/lib.rs create mode 100644 rs/consensus/upgrade/src/payload_builder.rs create mode 100644 rs/interfaces/src/upgrade.rs diff --git a/Cargo.lock b/Cargo.lock index fbbfcb5b434c..5dd219d17738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8024,6 +8024,7 @@ dependencies = [ "ic-consensus-dkg", "ic-consensus-idkg", "ic-consensus-mocks", + "ic-consensus-upgrade", "ic-consensus-utils", "ic-crypto-prng", "ic-crypto-temp-crypto", @@ -8322,6 +8323,15 @@ dependencies = [ "slog", ] +[[package]] +name = "ic-consensus-upgrade" +version = "0.9.0" +dependencies = [ + "ic-interfaces", + "ic-types", + "ic-types-test-utils", +] + [[package]] name = "ic-consensus-utils" version = "0.9.0" @@ -13729,6 +13739,7 @@ dependencies = [ "ic-consensus-features", "ic-consensus-idkg", "ic-consensus-manager", + "ic-consensus-upgrade", "ic-consensus-utils", "ic-crypto-interfaces-sig-verification", "ic-crypto-tls-interfaces", @@ -14793,6 +14804,7 @@ dependencies = [ "ic-config", "ic-consensus", "ic-consensus-cup-utils", + "ic-consensus-upgrade", "ic-consensus-utils", "ic-crypto-iccsa", "ic-crypto-test-utils-crypto-returning-ok", diff --git a/Cargo.toml b/Cargo.toml index 396f11547947..93a36be8d4b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ members = [ "rs/consensus/mocks", "rs/consensus/utils", "rs/consensus/chain_key", + "rs/consensus/upgrade", "rs/criterion_time", "rs/cross-chain/blob_store", "rs/cross-chain/proposal-cli", diff --git a/rs/consensus/BUILD.bazel b/rs/consensus/BUILD.bazel index ba419d51ef0a..553f5daaf7fd 100644 --- a/rs/consensus/BUILD.bazel +++ b/rs/consensus/BUILD.bazel @@ -113,6 +113,7 @@ rust_test( "//rs/consensus/dkg", "//rs/consensus/idkg:malicious_idkg", "//rs/consensus/mocks", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/prng", "//rs/crypto/temp_crypto", @@ -333,6 +334,7 @@ rust_test( "//rs/consensus/chain_key", "//rs/consensus/dkg", "//rs/consensus/idkg:malicious_idkg", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/prng", "//rs/crypto/temp_crypto", @@ -460,6 +462,7 @@ rust_ic_bench( ":consensus", "//rs/artifact_pool", "//rs/config", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/temp_crypto", "//rs/crypto/tree_hash", diff --git a/rs/consensus/Cargo.toml b/rs/consensus/Cargo.toml index 8ed60035a754..fb7b909f9e31 100644 --- a/rs/consensus/Cargo.toml +++ b/rs/consensus/Cargo.toml @@ -11,6 +11,7 @@ ic-config = { path = "../config" } ic-consensus-certification = { path = "./certification" } ic-consensus-idkg = { path = "./idkg" } ic-consensus-dkg = { path = "./dkg" } +ic-consensus-upgrade = { path = "./upgrade" } ic-consensus-utils = { path = "./utils" } ic-consensus-chain-key = { path = "./chain_key" } ic-crypto-prng = { path = "../crypto/prng" } diff --git a/rs/consensus/benches/validate_payload.rs b/rs/consensus/benches/validate_payload.rs index 643983ab112d..bce9a4bf989c 100644 --- a/rs/consensus/benches/validate_payload.rs +++ b/rs/consensus/benches/validate_payload.rs @@ -176,6 +176,7 @@ where Arc::new(FakeCanisterHttpPayloadBuilder::new()), Arc::new(MockBatchPayloadBuilder::new().expect_noop()), Arc::new(MockBatchPayloadBuilder::new().expect_noop()), + Arc::new(MockBatchPayloadBuilder::new().expect_noop()), metrics_registry, no_op_logger(), )); diff --git a/rs/consensus/src/consensus.rs b/rs/consensus/src/consensus.rs index da124fc884c2..0183d9070bd1 100644 --- a/rs/consensus/src/consensus.rs +++ b/rs/consensus/src/consensus.rs @@ -136,6 +136,7 @@ impl ConsensusImpl { canister_http_payload_builder: Arc, query_stats_payload_builder: Arc, chain_key_payload_builder: Arc, + upgrade_payload_builder: Arc, dkg_pool: Arc>, idkg_pool: Arc>, dkg_key_manager: Arc>, @@ -166,6 +167,7 @@ impl ConsensusImpl { canister_http_payload_builder, query_stats_payload_builder, chain_key_payload_builder, + upgrade_payload_builder, metrics_registry.clone(), logger.clone(), )); @@ -669,6 +671,7 @@ mod tests { Arc::new(FakeCanisterHttpPayloadBuilder::new()), Arc::new(MockBatchPayloadBuilder::new().expect_noop()), Arc::new(MockBatchPayloadBuilder::new().expect_noop()), + Arc::new(MockBatchPayloadBuilder::new().expect_noop()), dkg_pool, idkg_pool, Arc::new(Mutex::new(DkgKeyManager::new( diff --git a/rs/consensus/src/consensus/payload.rs b/rs/consensus/src/consensus/payload.rs index cada9a1b3c00..f693e053870e 100644 --- a/rs/consensus/src/consensus/payload.rs +++ b/rs/consensus/src/consensus/payload.rs @@ -44,6 +44,7 @@ pub(crate) enum BatchPayloadSectionBuilder { CanisterHttp(Arc), QueryStats(Arc), ChainKey(Arc), + Upgrade(Arc), } impl BatchPayloadSectionBuilder { @@ -94,6 +95,7 @@ impl BatchPayloadSectionBuilder { Self::CanisterHttp(_) => "canister_http", Self::QueryStats(_) => "query_stats", Self::ChainKey(_) => "chain_key", + Self::Upgrade(_) => "upgrade", } } @@ -363,6 +365,44 @@ impl BatchPayloadSectionBuilder { } } } + Self::Upgrade(builder) => { + let past_payloads: Vec = + filter_past_payloads(past_payloads, |_, _, payload| { + if payload.is_summary() { + None + } else { + Some(&payload.as_ref().as_data().batch.upgrade) + } + }); + + let upgrade = builder.build_payload( + height, + max_size, + &past_payloads, + proposal_context.validation_context, + ); + let size = NumBytes::new(upgrade.len() as u64); + + // Check validation as safety measure + match builder.validate_payload(height, proposal_context, &upgrade, &past_payloads) { + Ok(()) => { + payload.upgrade = upgrade; + size + } + Err(err) => { + error!( + logger, + "upgrade payload did not pass validation, this is a bug, {:?} @{}", + err, + CRITICAL_ERROR_VALIDATION_NOT_PASSED + ); + + metrics.critical_error_validation_not_passed.inc(); + payload.upgrade = vec![]; + NumBytes::new(0) + } + } + } } } @@ -472,6 +512,25 @@ impl BatchPayloadSectionBuilder { Ok(NumBytes::new(payload.chain_key.len() as u64)) } + Self::Upgrade(builder) => { + let past_payloads: Vec = + filter_past_payloads(past_payloads, |_, _, payload| { + if payload.is_summary() { + None + } else { + Some(&payload.as_ref().as_data().batch.upgrade) + } + }); + + builder.validate_payload( + height, + proposal_context, + &payload.upgrade, + &past_payloads, + )?; + + Ok(NumBytes::new(payload.upgrade.len() as u64)) + } } } } diff --git a/rs/consensus/src/consensus/payload_builder.rs b/rs/consensus/src/consensus/payload_builder.rs index 22ccfe686f34..7305dd55fd67 100644 --- a/rs/consensus/src/consensus/payload_builder.rs +++ b/rs/consensus/src/consensus/payload_builder.rs @@ -50,10 +50,12 @@ impl PayloadBuilderImpl { canister_http_payload_builder: Arc, query_stats_payload_builder: Arc, chain_key_payload_builder: Arc, + upgrade_payload_builder: Arc, metrics: MetricsRegistry, logger: ReplicaLogger, ) -> Self { let section_builder = vec![ + BatchPayloadSectionBuilder::Upgrade(upgrade_payload_builder), BatchPayloadSectionBuilder::Ingress(ingress_selector), BatchPayloadSectionBuilder::SelfValidating(self_validating_payload_builder), BatchPayloadSectionBuilder::XNet(xnet_payload_builder), @@ -290,6 +292,7 @@ pub(crate) mod test { FakeCanisterHttpPayloadBuilder::new().with_responses(canister_http_responses); let query_stats_payload_builder = MockBatchPayloadBuilder::new().expect_noop(); let chain_key_payload_builder = MockBatchPayloadBuilder::new().expect_noop(); + let upgrade_payload_builder = MockBatchPayloadBuilder::new().expect_noop(); PayloadBuilderImpl::new( subnet_test_id(0), @@ -301,6 +304,7 @@ pub(crate) mod test { Arc::new(canister_http_payload_builder), Arc::new(query_stats_payload_builder), Arc::new(chain_key_payload_builder), + Arc::new(upgrade_payload_builder), MetricsRegistry::new(), no_op_logger(), ) @@ -420,12 +424,13 @@ pub(crate) mod test { #[test] // NOTE: this test is sensitive to the order in which the individual payload builders are executed. // At the time of the writing the test the order for a block at height 1 is: - // 1. chain_key + // 1. upgrade // 2. ingress // 3. bitcoin - // 3. xnet - // 4. canister hhtp - // 5. query_stats + // 4. xnet + // 5. canister http + // 6. query_stats + // 7. chain_key fn test_get_payload_respect_limits() { ic_test_utilities::artifact_pool_config::with_test_pool_config(|pool_config| { let Dependencies { registry, .. } = DependenciesBuilder::new(pool_config, 1).build(); @@ -442,7 +447,9 @@ pub(crate) mod test { registry, MocksSettings { chain_key_payload_to_return: vec![0; CHAIN_KEY_PAYLOAD_SIZE.get() as usize], + upgrade_payload_to_return: vec![], expected_chain_key_payload_size_limit: MAX_BLOCK_SIZE, + expected_upgrade_payload_size_limit: MAX_BLOCK_SIZE - CHAIN_KEY_PAYLOAD_SIZE, ingress_payload_size_to_return: INGRESS_PAYLOAD_SIZE, expected_ingress_payload_size_limit: MAX_BLOCK_SIZE - CHAIN_KEY_PAYLOAD_SIZE, bitcoin_payload_size_to_return: BITCOIN_PAYLOAD_SIZE, @@ -515,10 +522,12 @@ pub(crate) mod test { query_stats_payload_to_return: vec![0; MB as usize], chain_key_payload_to_return: vec![0; 512 * KB as usize], http_outcalls_payload_to_return: vec![0; 256 * KB as usize], + upgrade_payload_to_return: vec![0; 32 * KB as usize], bitcoin_payload_size_to_return: NumBytes::new(128 * KB), xnet_payload_size_to_return: NumBytes::new(64 * KB), // The fields below are irrelevant for the test expected_chain_key_payload_size_limit: ZERO_BYTES, + expected_upgrade_payload_size_limit: ZERO_BYTES, expected_ingress_payload_size_limit: ZERO_BYTES, expected_bitcoin_payload_size_limit: ZERO_BYTES, expected_xnet_payload_size_limit: ZERO_BYTES, @@ -547,6 +556,7 @@ pub(crate) mod test { canister_http: settings.http_outcalls_payload_to_return, query_stats: settings.query_stats_payload_to_return, chain_key: settings.chain_key_payload_to_return, + upgrade: settings.upgrade_payload_to_return, }, dkg: DkgDataPayload::new_empty(Height::from(0)), idkg: None, @@ -574,6 +584,8 @@ pub(crate) mod test { expected_ingress_payload_size_limit: NumBytes, chain_key_payload_to_return: Vec, expected_chain_key_payload_size_limit: NumBytes, + upgrade_payload_to_return: Vec, + expected_upgrade_payload_size_limit: NumBytes, bitcoin_payload_size_to_return: NumBytes, expected_bitcoin_payload_size_limit: NumBytes, xnet_payload_size_to_return: NumBytes, @@ -659,6 +671,11 @@ pub(crate) mod test { settings.expected_query_stats_size_limit, ); + let upgrade_payload_builder = MockBatchPayloadBuilder::new().with_response_and_max_size( + settings.upgrade_payload_to_return, + settings.expected_upgrade_payload_size_limit, + ); + PayloadBuilderImpl::new( subnet_test_id(0), node_test_id(0), @@ -669,6 +686,7 @@ pub(crate) mod test { Arc::new(canister_http_payload_builder), Arc::new(query_stats_payload_builder), Arc::new(chain_key_payload_builder), + Arc::new(upgrade_payload_builder), MetricsRegistry::new(), no_op_logger(), ) diff --git a/rs/consensus/tests/framework/runner.rs b/rs/consensus/tests/framework/runner.rs index d5b3a1b33562..951f3ba2b43c 100644 --- a/rs/consensus/tests/framework/runner.rs +++ b/rs/consensus/tests/framework/runner.rs @@ -155,6 +155,7 @@ impl<'a> ConsensusRunner<'a> { deps.canister_http_payload_builder.clone(), deps.query_stats_payload_builder.clone(), deps.chain_key_payload_builder.clone(), + deps.upgrade_payload_builder.clone(), deps.dkg_pool.clone(), deps.idkg_pool.clone(), dkg_key_manager.clone(), diff --git a/rs/consensus/tests/framework/types.rs b/rs/consensus/tests/framework/types.rs index 92f02a99d1a3..9c89ea3447ac 100644 --- a/rs/consensus/tests/framework/types.rs +++ b/rs/consensus/tests/framework/types.rs @@ -6,6 +6,7 @@ use ic_artifact_pool::{ use ic_config::artifact_pool::ArtifactPoolConfig; use ic_consensus::consensus::{ConsensusBouncer, ConsensusImpl}; use ic_consensus_idkg::IDkgImpl; +use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilder; use ic_consensus_utils::{MAX_CONSENSUS_THREADS, build_thread_pool}; use ic_https_outcalls_consensus::test_utils::FakeCanisterHttpPayloadBuilder; use ic_interfaces::{ @@ -176,6 +177,7 @@ pub struct ConsensusDependencies { pub(crate) canister_http_payload_builder: Arc, pub(crate) query_stats_payload_builder: Arc, pub(crate) chain_key_payload_builder: Arc, + pub(crate) upgrade_payload_builder: Arc, pub consensus_pool: Arc>, pub dkg_pool: Arc>, pub idkg_pool: Arc>, @@ -239,6 +241,7 @@ impl ConsensusDependencies { canister_http_payload_builder: Arc::new(FakeCanisterHttpPayloadBuilder::new()), query_stats_payload_builder: Arc::new(MockBatchPayloadBuilder::new().expect_noop()), chain_key_payload_builder: Arc::new(MockBatchPayloadBuilder::new().expect_noop()), + upgrade_payload_builder: Arc::new(MockBatchPayloadBuilder::new().expect_noop()), state_manager, thread_pool: build_thread_pool(MAX_CONSENSUS_THREADS), metrics_registry, diff --git a/rs/consensus/tests/payload.rs b/rs/consensus/tests/payload.rs index 2821df6e899a..cbe32797d6b5 100644 --- a/rs/consensus/tests/payload.rs +++ b/rs/consensus/tests/payload.rs @@ -66,6 +66,9 @@ fn consensus_produces_expected_batches() { let chain_key_payload_builder = MockBatchPayloadBuilder::new().expect_noop(); let chain_key_payload_builder = Arc::new(chain_key_payload_builder); + let upgrade_payload_builder = MockBatchPayloadBuilder::new().expect_noop(); + let upgrade_payload_builder = Arc::new(upgrade_payload_builder); + let mut state_manager = MockStateManager::new(); state_manager.expect_remove_states_below().return_const(()); state_manager @@ -183,6 +186,7 @@ fn consensus_produces_expected_batches() { Arc::clone(&canister_http_payload_builder) as Arc<_>, query_stats_payload_builder, chain_key_payload_builder, + upgrade_payload_builder, Arc::clone(&dkg_pool) as Arc<_>, Arc::clone(&idkg_pool) as Arc<_>, dkg_key_manager.clone(), diff --git a/rs/consensus/upgrade/BUILD.bazel b/rs/consensus/upgrade/BUILD.bazel new file mode 100644 index 000000000000..399c605e815a --- /dev/null +++ b/rs/consensus/upgrade/BUILD.bazel @@ -0,0 +1,35 @@ +load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "upgrade", + srcs = glob(["src/**/*.rs"]), + crate_features = select({ + "//conditions:default": [], + }), + crate_name = "ic_consensus_upgrade", + proc_macro_deps = [ + # Keep sorted. + ], + version = "0.9.0", + deps = [ + # Keep sorted. + "//rs/interfaces", + "//rs/types/types", + ], +) + +rust_doc( + name = "consensus_upgrade_doc", + crate = ":upgrade", +) + +rust_test( + name = "upgrade_test", + crate = ":upgrade", + deps = [ + # Keep sorted. + "//rs/types/types_test_utils", + ], +) diff --git a/rs/consensus/upgrade/Cargo.toml b/rs/consensus/upgrade/Cargo.toml new file mode 100644 index 000000000000..67cd13974d6f --- /dev/null +++ b/rs/consensus/upgrade/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ic-consensus-upgrade" +version.workspace = true +authors.workspace = true +edition.workspace = true +description.workspace = true +documentation.workspace = true + +[dependencies] +ic-interfaces = { path = "../../interfaces" } +ic-types = { path = "../../types/types" } + +[dev-dependencies] +ic-types-test-utils = { path = "../../types/types_test_utils" } diff --git a/rs/consensus/upgrade/src/lib.rs b/rs/consensus/upgrade/src/lib.rs new file mode 100644 index 000000000000..49523ebce73e --- /dev/null +++ b/rs/consensus/upgrade/src/lib.rs @@ -0,0 +1,3 @@ +//! The upgrade permit protocol for the Phase-2 rolling GuestOS reboots. + +pub mod payload_builder; diff --git a/rs/consensus/upgrade/src/payload_builder.rs b/rs/consensus/upgrade/src/payload_builder.rs new file mode 100644 index 000000000000..6f4a5ded318d --- /dev/null +++ b/rs/consensus/upgrade/src/payload_builder.rs @@ -0,0 +1,100 @@ +use ic_interfaces::batch_payload::{BatchPayloadBuilder, PastPayload, ProposalContext}; +use ic_interfaces::consensus::{InvalidPayloadReason, PayloadValidationError}; +use ic_interfaces::upgrade::InvalidUpgradePayloadReason; +use ic_interfaces::validation::{ValidationError, ValidationResult}; +use ic_types::batch::{UpgradePayload, ValidationContext}; +use ic_types::{Height, NumBytes}; + +pub struct UpgradePayloadBuilder; + +impl BatchPayloadBuilder for UpgradePayloadBuilder { + fn build_payload( + &self, + _height: Height, + _max_size: NumBytes, + _past_payloads: &[PastPayload], + _context: &ValidationContext, + ) -> Vec { + // TODO: implement payload building + vec![] + } + + fn validate_payload( + &self, + _height: Height, + _proposal_context: &ProposalContext, + payload: &[u8], + _past_payloads: &[PastPayload], + ) -> ValidationResult { + // TODO: implement proper validation + UpgradePayload::deserialize(payload) + .map(|_| ()) + .map_err(|e| { + ValidationError::InvalidArtifact(InvalidPayloadReason::InvalidUpgradePayload( + InvalidUpgradePayloadReason::DecodeFailed(format!("{e:?}")), + )) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ic_types::RegistryVersion; + use ic_types::time::UNIX_EPOCH; + use ic_types_test_utils::ids::node_test_id; + + fn validation_context() -> ValidationContext { + ValidationContext { + registry_version: RegistryVersion::from(1), + certified_height: Height::from(0), + time: UNIX_EPOCH, + } + } + + #[test] + fn test_build_payload_is_empty() { + let context = validation_context(); + assert!( + UpgradePayloadBuilder + .build_payload(Height::from(1), NumBytes::new(u64::MAX), &[], &context) + .is_empty() + ); + } + + #[test] + fn test_validate_payload_rejects_undecodable_bytes() { + let context = validation_context(); + let proposal_context = ProposalContext { + proposer: node_test_id(1), + validation_context: &context, + }; + assert!(matches!( + UpgradePayloadBuilder.validate_payload( + Height::from(1), + &proposal_context, + &[0xFF, 0xFF], + &[] + ), + Err(ValidationError::InvalidArtifact( + InvalidPayloadReason::InvalidUpgradePayload( + InvalidUpgradePayloadReason::DecodeFailed(_) + ) + )) + )); + } + + #[test] + fn test_validate_payload_accepts_empty_payload() { + let context = validation_context(); + let proposal_context = ProposalContext { + proposer: node_test_id(1), + validation_context: &context, + }; + assert!( + UpgradePayloadBuilder + .validate_payload(Height::from(1), &proposal_context, &[], &[]) + .is_ok() + ); + } +} diff --git a/rs/interfaces/src/consensus.rs b/rs/interfaces/src/consensus.rs index 03348807090e..f4d1e46bdef6 100644 --- a/rs/interfaces/src/consensus.rs +++ b/rs/interfaces/src/consensus.rs @@ -15,6 +15,7 @@ use crate::{ InvalidSelfValidatingPayloadReason, SelfValidatingPayloadValidationError, SelfValidatingPayloadValidationFailure, }, + upgrade::InvalidUpgradePayloadReason, validation::{ValidationError, ValidationResult}, }; use ic_base_types::{NumBytes, SubnetId}; @@ -75,6 +76,7 @@ pub enum InvalidPayloadReason { InvalidCanisterHttpPayload(InvalidCanisterHttpPayloadReason), InvalidQueryStatsPayload(InvalidQueryStatsPayloadReason), InvalidChainKeyPayload(InvalidChainKeyPayloadReason), + InvalidUpgradePayload(InvalidUpgradePayloadReason), /// The overall block size is too large, even though the individual payloads are valid PayloadTooBig { expected: NumBytes, diff --git a/rs/interfaces/src/lib.rs b/rs/interfaces/src/lib.rs index be56ff4b6e90..01682b3c21d8 100644 --- a/rs/interfaces/src/lib.rs +++ b/rs/interfaces/src/lib.rs @@ -19,6 +19,7 @@ pub mod p2p; pub mod query_stats; pub mod self_validating_payload; pub mod time_source; +pub mod upgrade; pub mod validation; // Note [Associated Types in Interfaces] diff --git a/rs/interfaces/src/upgrade.rs b/rs/interfaces/src/upgrade.rs new file mode 100644 index 000000000000..d8a101014afe --- /dev/null +++ b/rs/interfaces/src/upgrade.rs @@ -0,0 +1,6 @@ +/// The reason why an upgrade payload was determined to be invalid. +#[derive(Debug, Eq, PartialEq)] +pub enum InvalidUpgradePayloadReason { + /// Failed to decode the upgrade payload from protobuf. + DecodeFailed(String), +} diff --git a/rs/protobuf/def/types/v1/consensus.proto b/rs/protobuf/def/types/v1/consensus.proto index 8b25ad0d351a..6ddb0a528950 100644 --- a/rs/protobuf/def/types/v1/consensus.proto +++ b/rs/protobuf/def/types/v1/consensus.proto @@ -10,6 +10,7 @@ import "registry/subnet/v1/subnet.proto"; import "types/v1/artifact.proto"; import "types/v1/dkg.proto"; import "types/v1/idkg.proto"; +import "types/v1/signature.proto"; import "types/v1/types.proto"; message CertificationMessage { @@ -69,6 +70,7 @@ message Block { bytes canister_http_payload_bytes = 15; bytes query_stats_payload_bytes = 16; bytes chain_key_payload_bytes = 17; + bytes upgrade_payload_bytes = 18; bytes payload_hash = 11; } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 9520b12182dc..3d677ca7ae23 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1496,6 +1496,8 @@ pub struct Block { pub query_stats_payload_bytes: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "17")] pub chain_key_payload_bytes: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "18")] + pub upgrade_payload_bytes: ::prost::alloc::vec::Vec, #[prost(bytes = "vec", tag = "11")] pub payload_hash: ::prost::alloc::vec::Vec, } diff --git a/rs/replica/setup_ic_network/BUILD.bazel b/rs/replica/setup_ic_network/BUILD.bazel index 8bc67750be53..a0eeee6c7140 100644 --- a/rs/replica/setup_ic_network/BUILD.bazel +++ b/rs/replica/setup_ic_network/BUILD.bazel @@ -17,6 +17,7 @@ rust_library( "//rs/consensus/dkg", "//rs/consensus/features", "//rs/consensus/idkg", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/interfaces/sig_verification", "//rs/crypto/tls_interfaces", @@ -62,6 +63,7 @@ rust_library( "//rs/consensus/dkg", "//rs/consensus/features", "//rs/consensus/idkg:malicious_idkg", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/interfaces/sig_verification", "//rs/crypto/tls_interfaces", diff --git a/rs/replica/setup_ic_network/Cargo.toml b/rs/replica/setup_ic_network/Cargo.toml index f887c1efd15d..80aa032a1bbe 100644 --- a/rs/replica/setup_ic_network/Cargo.toml +++ b/rs/replica/setup_ic_network/Cargo.toml @@ -17,6 +17,7 @@ ic-consensus-dkg = { path = "../../consensus/dkg" } ic-consensus-features= { path = "../../consensus/features" } ic-consensus-idkg = { path = "../../consensus/idkg" } ic-consensus-manager = { path = "../../p2p/consensus_manager" } +ic-consensus-upgrade = { path = "../../consensus/upgrade" } ic-consensus-utils = { path = "../../consensus/utils" } ic-consensus-chain-key = { path = "../../consensus/chain_key" } ic-crypto-interfaces-sig-verification = { path = "../../crypto/interfaces/sig_verification" } diff --git a/rs/replica/setup_ic_network/src/lib.rs b/rs/replica/setup_ic_network/src/lib.rs index 146fa9666a2b..e45087265394 100644 --- a/rs/replica/setup_ic_network/src/lib.rs +++ b/rs/replica/setup_ic_network/src/lib.rs @@ -16,6 +16,7 @@ use ic_consensus_chain_key::ChainKeyPayloadBuilderImpl; use ic_consensus_dkg::DkgBouncer; use ic_consensus_idkg::{IDkgBouncer, IDkgStatsImpl}; use ic_consensus_manager::{AbortableBroadcastChannel, AbortableBroadcastChannelBuilder}; +use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilder; use ic_consensus_utils::{ MAX_CONSENSUS_THREADS, build_thread_pool, crypto::ConsensusCrypto, pool_reader::PoolReader, }; @@ -543,6 +544,8 @@ fn start_consensus( metrics_registry, log.clone(), )); + + let upgrade_payload_builder = Arc::new(UpgradePayloadBuilder); // ------------------------------------------------------------------------ let replica_config = ReplicaConfig { @@ -572,6 +575,7 @@ fn start_consensus( https_outcalls_payload_builder, Arc::from(query_stats_payload_builder), chain_key_payload_builder, + upgrade_payload_builder, Arc::clone(&artifact_pools.dkg_pool) as Arc<_>, Arc::clone(&artifact_pools.idkg_pool) as Arc<_>, Arc::clone(&dkg_key_manager) as Arc<_>, diff --git a/rs/state_machine_tests/BUILD.bazel b/rs/state_machine_tests/BUILD.bazel index b3ca14ca4c9d..7e207cf2cbb2 100644 --- a/rs/state_machine_tests/BUILD.bazel +++ b/rs/state_machine_tests/BUILD.bazel @@ -22,6 +22,7 @@ rust_library( "//rs/config", "//rs/consensus", "//rs/consensus/cup_utils", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/test_utils/crypto_returning_ok", "//rs/crypto/test_utils/ni-dkg", @@ -133,6 +134,7 @@ rust_ic_test( "//rs/config", "//rs/consensus", "//rs/consensus/cup_utils", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/test_utils/crypto_returning_ok", "//rs/crypto/test_utils/ni-dkg", diff --git a/rs/state_machine_tests/Cargo.toml b/rs/state_machine_tests/Cargo.toml index b46d96434622..0e65f9ede0e8 100644 --- a/rs/state_machine_tests/Cargo.toml +++ b/rs/state_machine_tests/Cargo.toml @@ -20,6 +20,7 @@ ic-btc-consensus = { path = "../bitcoin/consensus" } ic-config = { path = "../config" } ic-consensus = { path = "../consensus" } ic-consensus-cup-utils = { path = "../consensus/cup_utils" } +ic-consensus-upgrade = { path = "../consensus/upgrade" } ic-consensus-utils = { path = "../consensus/utils" } ic-crypto-iccsa = { path = "../crypto/iccsa" } ic-crypto-test-utils-crypto-returning-ok = { path = "../crypto/test_utils/crypto_returning_ok" } diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 9dd5b533d7fc..38f7c57d4cd6 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -13,6 +13,7 @@ use ic_config::{ }; use ic_consensus::consensus::payload_builder::PayloadBuilderImpl; use ic_consensus_cup_utils::make_registry_cup; +use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilder; use ic_consensus_utils::{MAX_CONSENSUS_THREADS, build_thread_pool, crypto::SignVerify}; use ic_crypto_test_utils_crypto_returning_ok::CryptoReturningOk; use ic_crypto_test_utils_ni_dkg::{ @@ -149,7 +150,7 @@ use ic_types::{ batch::{ Batch, BatchContent, BatchMessages, BatchSummary, BlockmakerMetrics, CanisterHttpSpent, ChainKeyData, ConsensusResponse, QueryStatsPayload, SelfValidatingPayload, TotalQueryStats, - ValidationContext, XNetPayload, + UpgradePayload, ValidationContext, XNetPayload, }, canister_http::{ CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpRequestId, @@ -1285,6 +1286,7 @@ pub struct StateMachine { query_stats_payload_builder: Arc, local_query_execution_stats: Arc, chain_key_payload_builder: Arc, + upgrade_payload_builder: Arc, remove_old_states: bool, cycles_account_manager: Arc, } @@ -1859,6 +1861,7 @@ impl StateMachineBuilder { sm.canister_http_payload_builder.clone(), sm.query_stats_payload_builder.clone(), sm.chain_key_payload_builder.clone(), + sm.upgrade_payload_builder.clone(), sm.metrics_registry.clone(), sm.replica_logger.clone(), )); @@ -2213,6 +2216,7 @@ impl StateMachine { )); let chain_key_payload_builder = Arc::new(MockBatchPayloadBuilder::new().expect_noop()); + let upgrade_payload_builder = Arc::new(UpgradePayloadBuilder); let cancellation_token = tokio_util::sync::CancellationToken::new(); let cancellation_token_clone = cancellation_token.clone(); @@ -2479,6 +2483,7 @@ impl StateMachine { query_stats_payload_builder: pocket_query_stats_payload_builder, local_query_execution_stats: execution_services.local_query_execution_stats, chain_key_payload_builder, + upgrade_payload_builder, remove_old_states, cycles_account_manager: execution_services.cycles_account_manager, } @@ -3172,6 +3177,7 @@ impl StateMachine { .map(|p| p.get().to_vec()) .unwrap_or_default(), query_stats: payload.query_stats, + upgrade: UpgradePayload::default(), }, chain_key_data: ChainKeyData { master_public_keys: self.chain_key_subnet_public_keys.clone(), diff --git a/rs/test_utilities/types/src/batch/payload.rs b/rs/test_utilities/types/src/batch/payload.rs index aaa5e3f9a9df..d39aefd0e96a 100644 --- a/rs/test_utilities/types/src/batch/payload.rs +++ b/rs/test_utilities/types/src/batch/payload.rs @@ -15,6 +15,7 @@ impl Default for PayloadBuilder { canister_http: vec![], query_stats: vec![], chain_key: vec![], + upgrade: vec![], }, } } diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index fd53d8825f53..a04a5a3dd11b 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -179,6 +179,7 @@ pub struct BatchPayload { pub canister_http: Vec, pub query_stats: Vec, pub chain_key: Vec, + pub upgrade: Vec, } /// Batch properties collected form the last DKG summary block. @@ -204,6 +205,7 @@ pub struct BatchMessages { pub certified_stream_slices: BTreeMap, pub bitcoin_adapter_responses: Vec, pub query_stats: Option, + pub upgrade: UpgradePayload, } /// Error type that can occur during an `BatchPayload::into_messages` call @@ -211,6 +213,7 @@ pub struct BatchMessages { pub enum IntoMessagesError { IngressPayloadError(IngressPayloadError), QueryStatsPayloadError(ProxyDecodeError), + UpgradePayloadError(ProxyDecodeError), } impl BatchPayload { @@ -228,6 +231,8 @@ impl BatchPayload { bitcoin_adapter_responses: self.self_validating.0, query_stats: QueryStatsPayload::deserialize(&self.query_stats) .map_err(IntoMessagesError::QueryStatsPayloadError)?, + upgrade: UpgradePayload::deserialize(&self.upgrade) + .map_err(IntoMessagesError::UpgradePayloadError)?, }) } @@ -239,6 +244,7 @@ impl BatchPayload { canister_http, query_stats, chain_key, + upgrade, } = &self; ingress.is_empty() @@ -247,6 +253,7 @@ impl BatchPayload { && canister_http.is_empty() && query_stats.is_empty() && chain_key.is_empty() + && upgrade.is_empty() } } @@ -407,6 +414,7 @@ mod tests { canister_http, query_stats, chain_key, + upgrade, } = BatchPayload::default(); assert_eq!(ingress.total_ids_size_estimate(), NumBytes::new(0)); @@ -415,6 +423,7 @@ mod tests { assert_eq!(canister_http.len(), 0); assert_eq!(query_stats.len(), 0); assert_eq!(chain_key.len(), 0); + assert_eq!(upgrade.len(), 0); } /// This is a quick test to check the invariant, that the [`Default`] implementation @@ -431,6 +440,7 @@ mod tests { canister_http, query_stats, chain_key, + upgrade, } = &payload; assert!(ingress.is_empty()); @@ -439,6 +449,7 @@ mod tests { assert!(canister_http.is_empty()); assert!(query_stats.is_empty()); assert!(chain_key.is_empty()); + assert!(upgrade.is_empty()); } #[test] diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index 79ddbe42a8f0..54c0f73031fa 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -1299,6 +1299,7 @@ impl From<&Block> for pb::Block { canister_http_payload_bytes, query_stats_payload_bytes, chain_key_payload_bytes, + upgrade_payload_bytes, idkg_payload, ) = if payload.is_summary() { ( @@ -1309,6 +1310,7 @@ impl From<&Block> for pb::Block { vec![], vec![], vec![], + vec![], payload.as_summary().idkg.as_ref().map(|idkg| idkg.into()), ) } else { @@ -1321,6 +1323,7 @@ impl From<&Block> for pb::Block { batch.canister_http.clone(), batch.query_stats.clone(), batch.chain_key.clone(), + batch.upgrade.clone(), payload.as_data().idkg.as_ref().map(|idkg| idkg.into()), ) }; @@ -1339,6 +1342,7 @@ impl From<&Block> for pb::Block { canister_http_payload_bytes, query_stats_payload_bytes, chain_key_payload_bytes, + upgrade_payload_bytes, idkg_payload, payload_hash: block.payload.get_hash().clone().get().0, } @@ -1370,6 +1374,7 @@ impl TryFrom for Block { canister_http: block.canister_http_payload_bytes, query_stats: block.query_stats_payload_bytes, chain_key: block.chain_key_payload_bytes, + upgrade: block.upgrade_payload_bytes, }; let payload = match dkg_payload { diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 4131de9b9af4..b3ff7a3ad6b1 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -588,7 +588,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "764535296841f3db421a928cfadff3460be406d0182da64034eee623a9a97e99", + "c20a87578beb94df369dabfefc30c0d47d170c75d68236aae3b16335c0f21c4a", "Hash of CatchUpContent changed" ); } @@ -606,7 +606,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "7f183aaeb495159567a340b5bf61233cf3226141268febaee47de3e4c69cbc4b", + "db509a477f3ed01ec251325527e946b2e674f249d013bafc0d061620000a6e0d", "Hash of CatchUpShareContent changed" ); } @@ -654,7 +654,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "31f744bc26627fadbf1d73c66cb54603319a87966a488b6f41c4f0cfc1a30c89", + "33c4f3fb79a8520a4c1d6d814aa5bae53e5aa58ad517dfddec45be7dfd930053", "Hash of CatchUpPackage changed" ); } @@ -684,7 +684,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "bff423705e4cb96b7a391c4cccba8ed1ce441dabf2693ed5b9545a2b57d946bd", + "47648b17b0b80122fa1adc34a6d6e82ae8fb5af4a92b2495c41c91052ace1a10", "Hash of CatchUpPackageShare changed" ); } @@ -1027,7 +1027,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "b040378bc7d9d2b7c2e9067215eae6380a65316922369a1bc6d8376f31fe5d0a", + "5b8ca671118db0ed4f57939788881d95810b36f8d13a9954ecf2c57067e2b8d9", "Hash of Block changed" ); } @@ -1070,7 +1070,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "d591d695f67c644ddcc5315d96c25f00dede77c725859408ab7f113a18a0bf9a", + "9bb9a7c7dacd7513fc58d13b238740e2f8e282c3d6cb66bd3aef520904583ae9", "Hash of BlockProposal changed" ); } @@ -1107,7 +1107,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "c94d927dd7300814fef610a7560ba5a7775a859bb3511796cf23cfb59c038a4f", + "f289b64bb469c9aab1710c44b0b2fc778de9e5a552858eb10a566b8bc803d930", "Hash of BlockPayload changed" ); } From 842400782b7c42ebf46f66d55fcf4feca4bc2aff Mon Sep 17 00:00:00 2001 From: David Frank Date: Tue, 22 Sep 2026 22:32:27 +0200 Subject: [PATCH 11/21] Revert BatchPayload for now --- rs/state_machine_tests/src/lib.rs | 3 +-- rs/types/types/src/batch.rs | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 38f7c57d4cd6..bb5a28c6be42 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -150,7 +150,7 @@ use ic_types::{ batch::{ Batch, BatchContent, BatchMessages, BatchSummary, BlockmakerMetrics, CanisterHttpSpent, ChainKeyData, ConsensusResponse, QueryStatsPayload, SelfValidatingPayload, TotalQueryStats, - UpgradePayload, ValidationContext, XNetPayload, + ValidationContext, XNetPayload, }, canister_http::{ CanisterHttpPaymentReceipt, CanisterHttpRequestContext, CanisterHttpRequestId, @@ -3177,7 +3177,6 @@ impl StateMachine { .map(|p| p.get().to_vec()) .unwrap_or_default(), query_stats: payload.query_stats, - upgrade: UpgradePayload::default(), }, chain_key_data: ChainKeyData { master_public_keys: self.chain_key_subnet_public_keys.clone(), diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index a04a5a3dd11b..9aa4cb17efe6 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -205,7 +205,6 @@ pub struct BatchMessages { pub certified_stream_slices: BTreeMap, pub bitcoin_adapter_responses: Vec, pub query_stats: Option, - pub upgrade: UpgradePayload, } /// Error type that can occur during an `BatchPayload::into_messages` call @@ -213,7 +212,6 @@ pub struct BatchMessages { pub enum IntoMessagesError { IngressPayloadError(IngressPayloadError), QueryStatsPayloadError(ProxyDecodeError), - UpgradePayloadError(ProxyDecodeError), } impl BatchPayload { @@ -231,8 +229,6 @@ impl BatchPayload { bitcoin_adapter_responses: self.self_validating.0, query_stats: QueryStatsPayload::deserialize(&self.query_stats) .map_err(IntoMessagesError::QueryStatsPayloadError)?, - upgrade: UpgradePayload::deserialize(&self.upgrade) - .map_err(IntoMessagesError::UpgradePayloadError)?, }) } From 0bde3a780b3fbf69a920b3dede31fd689c460ac2 Mon Sep 17 00:00:00 2001 From: David Frank Date: Wed, 23 Sep 2026 17:26:30 +0200 Subject: [PATCH 12/21] Address review comments --- rs/interfaces/mocks/src/crypto.rs | 20 +-- rs/interfaces/src/crypto.rs | 10 +- rs/protobuf/def/types/v1/artifact.proto | 2 +- rs/protobuf/def/types/v1/upgrade.proto | 18 +-- rs/protobuf/src/gen/types/types.v1.rs | 28 ++--- rs/types/types/src/artifact.rs | 57 +-------- rs/types/types/src/batch/upgrade.rs | 153 ++++++++++++----------- rs/types/types/src/consensus.rs | 80 +----------- rs/types/types/src/consensus/upgrade.rs | 159 +++++++++++++++++++++--- 9 files changed, 266 insertions(+), 261 deletions(-) diff --git a/rs/interfaces/mocks/src/crypto.rs b/rs/interfaces/mocks/src/crypto.rs index 88bb17fb8162..b6754ea903b7 100644 --- a/rs/interfaces/mocks/src/crypto.rs +++ b/rs/interfaces/mocks/src/crypto.rs @@ -310,7 +310,7 @@ mockall::mock! { &self, message: &CanisterHttpResponseReceipt, ) -> CryptoResult>; - pub fn sign_basic_upgrade_permit_auth( + pub fn sign_basic_upgrade_permit_authorization_request( &self, message: &UpgradePermitAuthorizationRequest, ) -> CryptoResult>; @@ -495,27 +495,27 @@ mockall::mock! { ) -> CryptoResult<()>; // UpgradePermitAuthorizationRequest - pub fn verify_basic_sig_upgrade_permit_auth( + pub fn verify_basic_sig_upgrade_permit_authorization_request( &self, signature: &BasicSigOf, message: &UpgradePermitAuthorizationRequest, signer: NodeId, registry_version: RegistryVersion, ) -> CryptoResult<()>; - pub fn combine_basic_sig_upgrade_permit_auth( + pub fn combine_basic_sig_upgrade_permit_authorization_request( &self, signatures: BTreeMap>, registry_version: RegistryVersion, ) -> CryptoResult>; - pub fn verify_basic_sig_batch_upgrade_permit_auth( + pub fn verify_basic_sig_batch_upgrade_permit_authorization_request( &self, signature_batch: &BasicSignatureBatch, message: &UpgradePermitAuthorizationRequest, registry_version: RegistryVersion, ) -> CryptoResult<()>; - pub fn verify_basic_sig_batch_multi_msg_upgrade_permit_auth( + pub fn verify_basic_sig_batch_multi_msg_upgrade_permit_authorization_request( &self, inputs: Vec<( NodeId, @@ -823,7 +823,7 @@ impl_basic_signer!(IDkgOpeningContent, sign_basic_idkg_opening); impl_basic_signer!(CanisterHttpResponseReceipt, sign_basic_http); impl_basic_signer!( UpgradePermitAuthorizationRequest, - sign_basic_upgrade_permit_auth + sign_basic_upgrade_permit_authorization_request ); impl_basic_signer!(QueryResponseHash, sign_basic_query); @@ -878,10 +878,10 @@ impl_basic_sig_verifier!( ); impl_basic_sig_verifier!( UpgradePermitAuthorizationRequest, - verify_basic_sig_upgrade_permit_auth, - combine_basic_sig_upgrade_permit_auth, - verify_basic_sig_batch_upgrade_permit_auth, - verify_basic_sig_batch_multi_msg_upgrade_permit_auth + verify_basic_sig_upgrade_permit_authorization_request, + combine_basic_sig_upgrade_permit_authorization_request, + verify_basic_sig_batch_upgrade_permit_authorization_request, + verify_basic_sig_batch_multi_msg_upgrade_permit_authorization_request ); impl_threshold_signer!(CertificationContent, sign_threshold_certification); diff --git a/rs/interfaces/src/crypto.rs b/rs/interfaces/src/crypto.rs index a790c9ffc9fe..5cb6aa2058ae 100644 --- a/rs/interfaces/src/crypto.rs +++ b/rs/interfaces/src/crypto.rs @@ -73,15 +73,15 @@ pub trait Crypto: // IDkgOpeningContent + BasicSigner + BasicSigVerifier - // UpgradePermitAuthorizationRequest - + BasicSigner - + BasicSigVerifier + IDkgProtocol + ThresholdEcdsaSigner + ThresholdEcdsaSigVerifier + ThresholdSchnorrSigner + ThresholdSchnorrSigVerifier + VetKdProtocol + // UpgradePermitAuthorizationRequest + + BasicSigner + + BasicSigVerifier // CanisterHttpResponse + BasicSigner + BasicSigVerifier @@ -144,8 +144,6 @@ impl Crypto for T where + BasicSigVerifier + BasicSigner + BasicSigVerifier - + BasicSigner - + BasicSigVerifier + BasicSigner + BasicSigVerifier + BasicSigner @@ -155,6 +153,8 @@ impl Crypto for T where + ThresholdSchnorrSigner + ThresholdSchnorrSigVerifier + VetKdProtocol + + BasicSigner + + BasicSigVerifier + BasicSigVerifierByPublicKey + BasicSigVerifierByPublicKey + ThresholdSigner diff --git a/rs/protobuf/def/types/v1/artifact.proto b/rs/protobuf/def/types/v1/artifact.proto index 487e1b886964..adf644955a7d 100644 --- a/rs/protobuf/def/types/v1/artifact.proto +++ b/rs/protobuf/def/types/v1/artifact.proto @@ -12,7 +12,7 @@ message DkgMessageId { uint64 height = 2; } -message UpgradePermitAuthMessageId { +message UpgradePermitAuthorizationShareId { bytes hash = 1; uint64 height = 2; } diff --git a/rs/protobuf/def/types/v1/upgrade.proto b/rs/protobuf/def/types/v1/upgrade.proto index deedd6d4150a..e0d5668394c7 100644 --- a/rs/protobuf/def/types/v1/upgrade.proto +++ b/rs/protobuf/def/types/v1/upgrade.proto @@ -8,28 +8,28 @@ import "types/v1/signature.proto"; import "types/v1/types.proto"; message UpgradePayload { - repeated UpgradeAction actions = 1; + repeated UpgradePermitAction actions = 1; } -message UpgradeAction { +message UpgradePermitAction { oneof action { - RequestUpgradePermit request_permit = 1; - AuthorizeUpgradePermit authorize_permit = 2; - ReturnUpgradePermit return_permit = 3; + RequestUpgradePermit request_upgrade_permit = 1; + AuthorizeUpgradePermit authorize_upgrade_permit = 2; + ReturnUpgradePermit return_upgrade_permit = 3; } } -message UpgradePermitRequest { +message UpgradePermitAuthorizationRequest { types.v1.NodeId requestor = 1; uint64 request_height = 2; } message RequestUpgradePermit { - UpgradePermitRequest request = 1; + UpgradePermitAuthorizationRequest request = 1; } message AuthorizeUpgradePermit { - UpgradePermitRequest request = 1; + UpgradePermitAuthorizationRequest request = 1; repeated types.v1.BasicSignature signatures = 2; } @@ -38,6 +38,6 @@ message ReturnUpgradePermit { } message UpgradePermitAuthorizationShare { - UpgradePermitRequest request = 1; + UpgradePermitAuthorizationRequest request = 1; types.v1.BasicSignature signature = 2; } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 9520b12182dc..c2048fea3ecf 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1317,7 +1317,7 @@ pub struct DkgMessageId { pub height: u64, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct UpgradePermitAuthMessageId { +pub struct UpgradePermitAuthorizationShareId { #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, #[prost(uint64, tag = "2")] @@ -1862,27 +1862,27 @@ impl ChainKeyErrorCode { #[derive(Clone, PartialEq, ::prost::Message)] pub struct UpgradePayload { #[prost(message, repeated, tag = "1")] - pub actions: ::prost::alloc::vec::Vec, + pub actions: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct UpgradeAction { - #[prost(oneof = "upgrade_action::Action", tags = "1, 2, 3")] - pub action: ::core::option::Option, +pub struct UpgradePermitAction { + #[prost(oneof = "upgrade_permit_action::Action", tags = "1, 2, 3")] + pub action: ::core::option::Option, } -/// Nested message and enum types in `UpgradeAction`. -pub mod upgrade_action { +/// Nested message and enum types in `UpgradePermitAction`. +pub mod upgrade_permit_action { #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Action { #[prost(message, tag = "1")] - RequestPermit(super::RequestUpgradePermit), + RequestUpgradePermit(super::RequestUpgradePermit), #[prost(message, tag = "2")] - AuthorizePermit(super::AuthorizeUpgradePermit), + AuthorizeUpgradePermit(super::AuthorizeUpgradePermit), #[prost(message, tag = "3")] - ReturnPermit(super::ReturnUpgradePermit), + ReturnUpgradePermit(super::ReturnUpgradePermit), } } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct UpgradePermitRequest { +pub struct UpgradePermitAuthorizationRequest { #[prost(message, optional, tag = "1")] pub requestor: ::core::option::Option, #[prost(uint64, tag = "2")] @@ -1891,12 +1891,12 @@ pub struct UpgradePermitRequest { #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RequestUpgradePermit { #[prost(message, optional, tag = "1")] - pub request: ::core::option::Option, + pub request: ::core::option::Option, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct AuthorizeUpgradePermit { #[prost(message, optional, tag = "1")] - pub request: ::core::option::Option, + pub request: ::core::option::Option, #[prost(message, repeated, tag = "2")] pub signatures: ::prost::alloc::vec::Vec, } @@ -1908,7 +1908,7 @@ pub struct ReturnUpgradePermit { #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct UpgradePermitAuthorizationShare { #[prost(message, optional, tag = "1")] - pub request: ::core::option::Option, + pub request: ::core::option::Option, #[prost(message, optional, tag = "2")] pub signature: ::core::option::Option, } diff --git a/rs/types/types/src/artifact.rs b/rs/types/types/src/artifact.rs index a1e588ee0cd5..2a7914d58662 100644 --- a/rs/types/types/src/artifact.rs +++ b/rs/types/types/src/artifact.rs @@ -4,11 +4,10 @@ use crate::{ canister_http::CanisterHttpResponseShare, consensus::{ ConsensusMessage, ConsensusMessageHash, ConsensusMessageHashable, HasHash, HasHeight, - UpgradePermitAuthorizationShare, certification::{CertificationMessage, CertificationMessageHash}, idkg::IDkgArtifactId, }, - crypto::{CryptoHash, CryptoHashOf, crypto_hash}, + crypto::{CryptoHash, crypto_hash}, messages::{MessageId, SignedIngress}, }; #[cfg(test)] @@ -247,57 +246,3 @@ pub type IDkgMessageId = IDkgArtifactId; // CanisterHttp artifacts pub type CanisterHttpResponseId = CanisterHttpResponseShare; - -// ----------------------------------------------------------------------------- -// Upgrade permit authorization artifacts - -/// Upgrade permit authorization message identifier carries both a message hash -/// and a height, used by the upgrade permit auth pool for lookup. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] -pub struct UpgradePermitAuthorizationShareId { - pub hash: CryptoHashOf, - pub height: Height, -} - -impl HasHeight for UpgradePermitAuthorizationShareId { - fn height(&self) -> Height { - self.height - } -} - -impl IdentifiableArtifact for UpgradePermitAuthorizationShare { - const NAME: &'static str = "upgrade"; - type Id = UpgradePermitAuthorizationShareId; - fn id(&self) -> Self::Id { - UpgradePermitAuthorizationShareId { - hash: crypto_hash(self), - height: self.content.height(), - } - } -} - -impl From<&UpgradePermitAuthorizationShare> for UpgradePermitAuthorizationShareId { - fn from(share: &UpgradePermitAuthorizationShare) -> Self { - share.id() - } -} - -impl From for pb::UpgradePermitAuthMessageId { - fn from(id: UpgradePermitAuthorizationShareId) -> Self { - Self { - hash: id.hash.clone().get().0, - height: id.height.get(), - } - } -} - -impl TryFrom for UpgradePermitAuthorizationShareId { - type Error = ProxyDecodeError; - - fn try_from(id: pb::UpgradePermitAuthMessageId) -> Result { - Ok(Self { - hash: CryptoHash(id.hash.clone()).into(), - height: Height::from(id.height), - }) - } -} diff --git a/rs/types/types/src/batch/upgrade.rs b/rs/types/types/src/batch/upgrade.rs index 30a16622e4f7..c1c91edf1e6f 100644 --- a/rs/types/types/src/batch/upgrade.rs +++ b/rs/types/types/src/batch/upgrade.rs @@ -1,18 +1,17 @@ use ic_base_types::NumBytes; use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; use ic_protobuf::types::v1 as pb; -use pb::upgrade_action::Action; +use pb::upgrade_permit_action::Action; use prost::Message as _; -use prost::encoding::encoded_len_varint; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use crate::consensus::UpgradePermitAuthorizationRequest; use crate::consensus::upgrade::UpgradePermitAction; +use crate::crypto::Signed; use crate::signature::{BasicSignature, BasicSignatureBatch}; /// The upgrade permit actions of a block's batch payload. -#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)] pub struct UpgradePayload { pub actions: Vec, } @@ -27,11 +26,11 @@ impl UpgradePayload { let mut proto = pb::UpgradePayload::default(); let mut remaining = byte_limit.get() as usize; for action in &self.actions { - let entry = pb::UpgradeAction::from(action); + let entry = pb::UpgradePermitAction::from(action); // One repeated field entry: the key, the varint length, and the // message bytes. - let entry_len = - 1 + encoded_len_varint(entry.encoded_len() as u64) + entry.encoded_len(); + let len = entry.encoded_len(); + let entry_len = 1 + prost::length_delimiter_len(len) + len; if entry_len > remaining { continue; } @@ -55,33 +54,37 @@ impl UpgradePayload { } } -impl From<&UpgradePermitAction> for pb::UpgradeAction { +impl From<&UpgradePermitAction> for pb::UpgradePermitAction { fn from(action: &UpgradePermitAction) -> Self { let proto_action = match action { - UpgradePermitAction::Request(request) => { - Action::RequestPermit(pb::RequestUpgradePermit { - request: Some(pb::UpgradePermitRequest::from(request)), + UpgradePermitAction::RequestUpgradePermit(request) => { + Action::RequestUpgradePermit(pb::RequestUpgradePermit { + request: Some(pb::UpgradePermitAuthorizationRequest::from(request)), }) } - UpgradePermitAction::Authorize { - request, - signatures, - } => Action::AuthorizePermit(pb::AuthorizeUpgradePermit { - request: Some(pb::UpgradePermitRequest::from(request)), - signatures: signatures - .signatures_map - .iter() - .map(|(signer, signature)| { - pb::BasicSignature::from(BasicSignature { - signature: signature.clone(), - signer: *signer, + UpgradePermitAction::AuthorizeUpgradePermit(authorization) => { + Action::AuthorizeUpgradePermit(pb::AuthorizeUpgradePermit { + request: Some(pb::UpgradePermitAuthorizationRequest::from( + &authorization.content, + )), + signatures: authorization + .signature + .signatures_map + .iter() + .map(|(signer, signature)| { + pb::BasicSignature::from(BasicSignature { + signature: signature.clone(), + signer: *signer, + }) }) - }) - .collect(), - }), - UpgradePermitAction::Return { node } => Action::ReturnPermit(pb::ReturnUpgradePermit { - node: Some(crate::node_id_into_protobuf(*node)), - }), + .collect(), + }) + } + UpgradePermitAction::ReturnUpgradePermit { node } => { + Action::ReturnUpgradePermit(pb::ReturnUpgradePermit { + node: Some(crate::node_id_into_protobuf(*node)), + }) + } }; Self { action: Some(proto_action), @@ -89,28 +92,31 @@ impl From<&UpgradePermitAction> for pb::UpgradeAction { } } -impl TryFrom for UpgradePermitAction { +impl TryFrom for UpgradePermitAction { type Error = ProxyDecodeError; - fn try_from(proto: pb::UpgradeAction) -> Result { - let action = proto - .action - .ok_or(ProxyDecodeError::MissingField("UpgradeAction::action"))?; + fn try_from(proto: pb::UpgradePermitAction) -> Result { + let action = proto.action.ok_or(ProxyDecodeError::MissingField( + "UpgradePermitAction::action", + ))?; Ok(match action { - Action::RequestPermit(request) => UpgradePermitAction::Request(try_from_option_field( - request.request, - "RequestUpgradePermit::request", - )?), - Action::AuthorizePermit(authorize) => UpgradePermitAction::Authorize { - request: try_from_option_field( - authorize.request, - "AuthorizeUpgradePermit::request", - )?, - signatures: signature_batch(authorize.signatures)?, - }, - Action::ReturnPermit(return_permit) => UpgradePermitAction::Return { - node: crate::node_id_try_from_option(return_permit.node)?, - }, + Action::RequestUpgradePermit(request) => UpgradePermitAction::RequestUpgradePermit( + try_from_option_field(request.request, "RequestUpgradePermit::request")?, + ), + Action::AuthorizeUpgradePermit(authorize) => { + UpgradePermitAction::AuthorizeUpgradePermit(Signed { + content: try_from_option_field( + authorize.request, + "AuthorizeUpgradePermit::request", + )?, + signature: signature_batch(authorize.signatures)?, + }) + } + Action::ReturnUpgradePermit(return_permit) => { + UpgradePermitAction::ReturnUpgradePermit { + node: crate::node_id_try_from_option(return_permit.node)?, + } + } }) } } @@ -123,14 +129,13 @@ fn signature_batch( let mut signatures_map = BTreeMap::new(); for signature in signatures { let signature: BasicSignature = signature.try_into()?; - if signatures_map - .insert(signature.signer, signature.signature) - .is_some() - { + if let Some(previous) = signatures_map.insert(signature.signer, signature.signature) { + // Unwrap is fine, entry has just been inserted + let new = signatures_map.get(&signature.signer).unwrap(); return Err(ProxyDecodeError::DuplicateEntry { key: format!("{:?}", signature.signer), - v1: "signature".to_string(), - v2: "signature".to_string(), + v1: format!("{previous:?}"), + v2: format!("{new:?}"), }); } } @@ -158,7 +163,7 @@ mod tests { #[test] fn test_round_trip_request() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::Request( + actions: vec![UpgradePermitAction::RequestUpgradePermit( UpgradePermitAuthorizationRequest { requestor: node(3), request_height: Height::new(42), @@ -170,22 +175,22 @@ mod tests { #[test] fn test_round_trip_authorize() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::Authorize { - request: UpgradePermitAuthorizationRequest { + actions: vec![UpgradePermitAction::AuthorizeUpgradePermit(Signed { + content: UpgradePermitAuthorizationRequest { requestor: node(5), request_height: Height::new(3), }, - signatures: BasicSignatureBatch { + signature: BasicSignatureBatch { signatures_map: BTreeMap::new(), }, - }], + })], }); } #[test] fn test_round_trip_return() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::Return { node: node(7) }], + actions: vec![UpgradePermitAction::ReturnUpgradePermit { node: node(7) }], }); } @@ -198,7 +203,7 @@ mod tests { fn test_serialize_with_limit_drops_overflow() { // A limit of 0 cannot fit any action, so nothing is serialized. let payload = UpgradePayload { - actions: vec![UpgradePermitAction::Return { node: node(1) }], + actions: vec![UpgradePermitAction::ReturnUpgradePermit { node: node(1) }], }; assert!(payload.serialize_with_limit(NumBytes::new(0)).is_empty()); } @@ -209,23 +214,23 @@ mod tests { // action after it still does. let payload = UpgradePayload { actions: vec![ - UpgradePermitAction::Authorize { - request: UpgradePermitAuthorizationRequest { + UpgradePermitAction::AuthorizeUpgradePermit(Signed { + content: UpgradePermitAuthorizationRequest { requestor: node(1), request_height: Height::new(4), }, - signatures: BasicSignatureBatch { + signature: BasicSignatureBatch { signatures_map: BTreeMap::from([( node(2), BasicSigOf::new(BasicSig(vec![0x42; 64])), )]), }, - }, - UpgradePermitAction::Return { node: node(3) }, + }), + UpgradePermitAction::ReturnUpgradePermit { node: node(3) }, ], }; let return_entry_len = UpgradePayload { - actions: vec![UpgradePermitAction::Return { node: node(3) }], + actions: vec![UpgradePermitAction::ReturnUpgradePermit { node: node(3) }], } .serialize_with_limit(NumBytes::new(u64::MAX)) .len(); @@ -233,7 +238,7 @@ mod tests { let decoded = UpgradePayload::deserialize(&bytes).unwrap(); assert_eq!( decoded.actions, - vec![UpgradePermitAction::Return { node: node(3) }] + vec![UpgradePermitAction::ReturnUpgradePermit { node: node(3) }] ); } @@ -241,20 +246,20 @@ mod tests { fn test_round_trip_multiple_actions() { round_trip(UpgradePayload { actions: vec![ - UpgradePermitAction::Request(UpgradePermitAuthorizationRequest { + UpgradePermitAction::RequestUpgradePermit(UpgradePermitAuthorizationRequest { requestor: node(1), request_height: Height::new(10), }), - UpgradePermitAction::Authorize { - request: UpgradePermitAuthorizationRequest { + UpgradePermitAction::AuthorizeUpgradePermit(Signed { + content: UpgradePermitAuthorizationRequest { requestor: node(2), request_height: Height::new(4), }, - signatures: BasicSignatureBatch { + signature: BasicSignatureBatch { signatures_map: BTreeMap::new(), }, - }, - UpgradePermitAction::Return { node: node(3) }, + }), + UpgradePermitAction::ReturnUpgradePermit { node: node(3) }, ], }); } diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index 79ddbe42a8f0..d685bcb9a7d7 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -34,6 +34,8 @@ mod payload; pub mod thunk; pub mod upgrade; +pub use upgrade::{UpgradePermitAuthorizationRequest, UpgradePermitAuthorizationShare}; + pub use catchup::*; use hashed::Hashed; pub use payload::{BlockPayload, DataPayload, Payload, PayloadType, SummaryPayload}; @@ -1767,84 +1769,6 @@ impl ConsensusMessageHashable for EquivocationProof { } } -/// UpgradePermitAuthorizationRequest holds the values that are signed in an -/// upgrade permit authorization share. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] -pub struct UpgradePermitAuthorizationRequest { - pub requestor: NodeId, - pub request_height: Height, -} - -impl SignedBytesWithoutDomainSeparator for UpgradePermitAuthorizationRequest { - fn write_signed_bytes_without_domain_separator(&self, bytes: &mut Vec) { - serde_cbor::to_writer(bytes, &self).unwrap(); - } -} - -impl HasHeight for UpgradePermitAuthorizationRequest { - fn height(&self) -> Height { - self.request_height - } -} - -/// An upgrade permit authorization share is a basic signature share on an -/// [`UpgradePermitAuthorizationRequest`]. -pub type UpgradePermitAuthorizationShare = - Signed>; - -impl PbArtifact for UpgradePermitAuthorizationShare { - type PbId = pb::UpgradePermitAuthMessageId; - type PbIdError = ProxyDecodeError; - type PbMessage = pb::UpgradePermitAuthorizationShare; - type PbMessageError = ProxyDecodeError; -} - -impl From<&UpgradePermitAuthorizationRequest> for pb::UpgradePermitRequest { - fn from(content: &UpgradePermitAuthorizationRequest) -> Self { - pb::UpgradePermitRequest { - requestor: Some(node_id_into_protobuf(content.requestor)), - request_height: content.request_height.get(), - } - } -} - -impl TryFrom for UpgradePermitAuthorizationRequest { - type Error = ProxyDecodeError; - - fn try_from(content: pb::UpgradePermitRequest) -> Result { - Ok(UpgradePermitAuthorizationRequest { - requestor: node_id_try_from_option(content.requestor)?, - request_height: Height::from(content.request_height), - }) - } -} - -impl From for pb::UpgradePermitAuthorizationShare { - fn from(share: UpgradePermitAuthorizationShare) -> Self { - pb::UpgradePermitAuthorizationShare { - request: Some(pb::UpgradePermitRequest::from(&share.content)), - signature: Some(pb::BasicSignature::from(share.signature)), - } - } -} - -impl TryFrom for UpgradePermitAuthorizationShare { - type Error = ProxyDecodeError; - - fn try_from(message: pb::UpgradePermitAuthorizationShare) -> Result { - let request = - try_from_option_field(message.request, "UpgradePermitAuthorizationShare::request")?; - let signature = try_from_option_field( - message.signature, - "UpgradePermitAuthorizationShare::signature", - )?; - Ok(UpgradePermitAuthorizationShare { - content: request, - signature, - }) - } -} - impl ConsensusMessageHashable for ConsensusMessage { fn get_id(&self) -> ConsensusMessageId { ConsensusMessageId { diff --git a/rs/types/types/src/consensus/upgrade.rs b/rs/types/types/src/consensus/upgrade.rs index dedd484c5d86..f87206be1eec 100644 --- a/rs/types/types/src/consensus/upgrade.rs +++ b/rs/types/types/src/consensus/upgrade.rs @@ -2,38 +2,169 @@ //! //! The permit flow works in three stages: //! -//! 1. **Request**: A block maker includes `UpgradePermitAction::Request` in +//! 1. **Request**: A block maker includes `UpgradePermitAction::RequestUpgradePermit` in //! its block when it wants to reboot. Validators check outstanding requests //! against the allowed max parallel reboots. //! //! 2. **Authorize**: After the request block is finalized, each node gossips an -//! [`crate::consensus::UpgradePermitAuthorizationShare`]. When a block maker -//! collects enough shares, it includes `UpgradePermitAction::Authorize` in its block. +//! [`UpgradePermitAuthorizationShare`]. When a block maker collects enough +//! shares, it includes `UpgradePermitAction::AuthorizeUpgradePermit` in its block. //! //! 3. **Return**: After rebooting, the node includes -//! `UpgradePermitAction::Return` to release the slot. +//! `UpgradePermitAction::ReturnUpgradePermit` to release the slot. +use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; +use ic_protobuf::types::v1 as pb; use serde::{Deserialize, Serialize}; -use crate::NodeId; -use crate::consensus::UpgradePermitAuthorizationRequest; -use crate::signature::BasicSignatureBatch; +use crate::artifact::{IdentifiableArtifact, PbArtifact}; +use crate::consensus::HasHeight; +use crate::crypto::{ + CryptoHash, CryptoHashOf, Signed, SignedBytesWithoutDomainSeparator, crypto_hash, +}; +use crate::signature::{BasicSignatureBatch, BasicSigned}; +use crate::{Height, NodeId, node_id_into_protobuf, node_id_try_from_option}; /// A single action in a block's upgrade payload section. A block may carry /// multiple actions (e.g. `Request` for the block maker and `Authorize` for /// another node). -#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)] +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub enum UpgradePermitAction { /// Request permission to reboot. The block maker requests for itself. /// `request_height` is the height of the block containing this request, /// used for timeout tracking. - Request(UpgradePermitAuthorizationRequest), + RequestUpgradePermit(UpgradePermitAuthorizationRequest), /// Authorize a node to reboot — the signed request and the basic /// signatures over it collected from the staying members. - Authorize { - request: UpgradePermitAuthorizationRequest, - signatures: BasicSignatureBatch, - }, + AuthorizeUpgradePermit( + Signed< + UpgradePermitAuthorizationRequest, + BasicSignatureBatch, + >, + ), /// Release a previously authorized permit (reboot complete). - Return { node: NodeId }, + ReturnUpgradePermit { node: NodeId }, +} + +/// UpgradePermitAuthorizationRequest holds the values that are signed in an +/// upgrade permit authorization share. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] +pub struct UpgradePermitAuthorizationRequest { + pub requestor: NodeId, + pub request_height: Height, +} + +impl SignedBytesWithoutDomainSeparator for UpgradePermitAuthorizationRequest { + fn write_signed_bytes_without_domain_separator(&self, bytes: &mut Vec) { + serde_cbor::to_writer(bytes, &self).unwrap(); + } +} + +impl HasHeight for UpgradePermitAuthorizationRequest { + fn height(&self) -> Height { + self.request_height + } +} + +pub type UpgradePermitAuthorizationShare = BasicSigned; + +/// Upgrade permit authorization message identifier carries both a message hash +/// and a height, used by the upgrade permit auth pool for lookup. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] +pub struct UpgradePermitAuthorizationShareId { + pub hash: CryptoHashOf, + pub height: Height, +} + +impl HasHeight for UpgradePermitAuthorizationShareId { + fn height(&self) -> Height { + self.height + } +} + +impl IdentifiableArtifact for UpgradePermitAuthorizationShare { + const NAME: &'static str = "upgrade"; + type Id = UpgradePermitAuthorizationShareId; + fn id(&self) -> Self::Id { + UpgradePermitAuthorizationShareId { + hash: crypto_hash(self), + height: self.content.height(), + } + } +} + +impl From<&UpgradePermitAuthorizationShare> for UpgradePermitAuthorizationShareId { + fn from(share: &UpgradePermitAuthorizationShare) -> Self { + share.id() + } +} + +impl PbArtifact for UpgradePermitAuthorizationShare { + type PbId = pb::UpgradePermitAuthorizationShareId; + type PbIdError = ProxyDecodeError; + type PbMessage = pb::UpgradePermitAuthorizationShare; + type PbMessageError = ProxyDecodeError; +} + +impl From<&UpgradePermitAuthorizationRequest> for pb::UpgradePermitAuthorizationRequest { + fn from(content: &UpgradePermitAuthorizationRequest) -> Self { + pb::UpgradePermitAuthorizationRequest { + requestor: Some(node_id_into_protobuf(content.requestor)), + request_height: content.request_height.get(), + } + } +} + +impl TryFrom for UpgradePermitAuthorizationRequest { + type Error = ProxyDecodeError; + + fn try_from(content: pb::UpgradePermitAuthorizationRequest) -> Result { + Ok(UpgradePermitAuthorizationRequest { + requestor: node_id_try_from_option(content.requestor)?, + request_height: Height::from(content.request_height), + }) + } +} + +impl From for pb::UpgradePermitAuthorizationShare { + fn from(share: UpgradePermitAuthorizationShare) -> Self { + pb::UpgradePermitAuthorizationShare { + request: Some(pb::UpgradePermitAuthorizationRequest::from(&share.content)), + signature: Some(pb::BasicSignature::from(share.signature)), + } + } +} + +impl TryFrom for UpgradePermitAuthorizationShare { + type Error = ProxyDecodeError; + + fn try_from(message: pb::UpgradePermitAuthorizationShare) -> Result { + let content = + try_from_option_field(message.request, "UpgradePermitAuthorizationShare::request")?; + let signature = try_from_option_field( + message.signature, + "UpgradePermitAuthorizationShare::signature", + )?; + Ok(UpgradePermitAuthorizationShare { content, signature }) + } +} + +impl From for pb::UpgradePermitAuthorizationShareId { + fn from(id: UpgradePermitAuthorizationShareId) -> Self { + Self { + hash: id.hash.clone().get().0, + height: id.height.get(), + } + } +} + +impl TryFrom for UpgradePermitAuthorizationShareId { + type Error = ProxyDecodeError; + + fn try_from(id: pb::UpgradePermitAuthorizationShareId) -> Result { + Ok(Self { + hash: CryptoHash(id.hash.clone()).into(), + height: Height::from(id.height), + }) + } } From 705835ac01d95718657c45ad54f62a066e541c5a Mon Sep 17 00:00:00 2001 From: David Frank Date: Wed, 23 Sep 2026 17:45:57 +0200 Subject: [PATCH 13/21] Address review comments --- rs/consensus/Cargo.toml | 2 +- rs/protobuf/def/types/v1/consensus.proto | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/rs/consensus/Cargo.toml b/rs/consensus/Cargo.toml index fb7b909f9e31..1e615f474419 100644 --- a/rs/consensus/Cargo.toml +++ b/rs/consensus/Cargo.toml @@ -11,7 +11,6 @@ ic-config = { path = "../config" } ic-consensus-certification = { path = "./certification" } ic-consensus-idkg = { path = "./idkg" } ic-consensus-dkg = { path = "./dkg" } -ic-consensus-upgrade = { path = "./upgrade" } ic-consensus-utils = { path = "./utils" } ic-consensus-chain-key = { path = "./chain_key" } ic-crypto-prng = { path = "../crypto/prng" } @@ -46,6 +45,7 @@ ic-btc-replica-types = { path = "../bitcoin/replica_types" } ic-config = { path = "../config" } ic-consensus = { path = ".", features = ["malicious_code"] } ic-consensus-mocks = { path = "./mocks" } +ic-consensus-upgrade = { path = "./upgrade" } ic-crypto-temp-crypto = { path = "../crypto/temp_crypto" } ic-crypto-test-utils-crypto-returning-ok = { path = "../crypto/test_utils/crypto_returning_ok" } ic-crypto-test-utils-ni-dkg = { path = "../crypto/test_utils/ni-dkg" } diff --git a/rs/protobuf/def/types/v1/consensus.proto b/rs/protobuf/def/types/v1/consensus.proto index 6ddb0a528950..255fda0d31e8 100644 --- a/rs/protobuf/def/types/v1/consensus.proto +++ b/rs/protobuf/def/types/v1/consensus.proto @@ -10,7 +10,6 @@ import "registry/subnet/v1/subnet.proto"; import "types/v1/artifact.proto"; import "types/v1/dkg.proto"; import "types/v1/idkg.proto"; -import "types/v1/signature.proto"; import "types/v1/types.proto"; message CertificationMessage { From 9ade04f4fae44d078554f68c0401022cd4225d95 Mon Sep 17 00:00:00 2001 From: David Frank Date: Wed, 23 Sep 2026 18:56:47 +0200 Subject: [PATCH 14/21] Address review comments --- rs/protobuf/def/types/v1/upgrade.proto | 6 +-- rs/protobuf/src/gen/types/types.v1.rs | 6 +-- rs/types/types/src/batch/upgrade.rs | 60 ++++++++++++------------- rs/types/types/src/consensus/upgrade.rs | 18 ++++---- rs/types/types/src/crypto/hash.rs | 21 ++------- rs/types/types/src/crypto/hash/tests.rs | 14 +++--- 6 files changed, 54 insertions(+), 71 deletions(-) diff --git a/rs/protobuf/def/types/v1/upgrade.proto b/rs/protobuf/def/types/v1/upgrade.proto index e0d5668394c7..93f82282c8dc 100644 --- a/rs/protobuf/def/types/v1/upgrade.proto +++ b/rs/protobuf/def/types/v1/upgrade.proto @@ -13,9 +13,9 @@ message UpgradePayload { message UpgradePermitAction { oneof action { - RequestUpgradePermit request_upgrade_permit = 1; - AuthorizeUpgradePermit authorize_upgrade_permit = 2; - ReturnUpgradePermit return_upgrade_permit = 3; + RequestUpgradePermit request_permit = 1; + AuthorizeUpgradePermit authorize_permit = 2; + ReturnUpgradePermit return_permit = 3; } } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index c2048fea3ecf..0d22fc051184 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1874,11 +1874,11 @@ pub mod upgrade_permit_action { #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Action { #[prost(message, tag = "1")] - RequestUpgradePermit(super::RequestUpgradePermit), + RequestPermit(super::RequestUpgradePermit), #[prost(message, tag = "2")] - AuthorizeUpgradePermit(super::AuthorizeUpgradePermit), + AuthorizePermit(super::AuthorizeUpgradePermit), #[prost(message, tag = "3")] - ReturnUpgradePermit(super::ReturnUpgradePermit), + ReturnPermit(super::ReturnUpgradePermit), } } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] diff --git a/rs/types/types/src/batch/upgrade.rs b/rs/types/types/src/batch/upgrade.rs index c1c91edf1e6f..74ae031f8c07 100644 --- a/rs/types/types/src/batch/upgrade.rs +++ b/rs/types/types/src/batch/upgrade.rs @@ -57,13 +57,13 @@ impl UpgradePayload { impl From<&UpgradePermitAction> for pb::UpgradePermitAction { fn from(action: &UpgradePermitAction) -> Self { let proto_action = match action { - UpgradePermitAction::RequestUpgradePermit(request) => { - Action::RequestUpgradePermit(pb::RequestUpgradePermit { + UpgradePermitAction::RequestPermit(request) => { + Action::RequestPermit(pb::RequestUpgradePermit { request: Some(pb::UpgradePermitAuthorizationRequest::from(request)), }) } - UpgradePermitAction::AuthorizeUpgradePermit(authorization) => { - Action::AuthorizeUpgradePermit(pb::AuthorizeUpgradePermit { + UpgradePermitAction::AuthorizePermit(authorization) => { + Action::AuthorizePermit(pb::AuthorizeUpgradePermit { request: Some(pb::UpgradePermitAuthorizationRequest::from( &authorization.content, )), @@ -80,8 +80,8 @@ impl From<&UpgradePermitAction> for pb::UpgradePermitAction { .collect(), }) } - UpgradePermitAction::ReturnUpgradePermit { node } => { - Action::ReturnUpgradePermit(pb::ReturnUpgradePermit { + UpgradePermitAction::ReturnPermit { node } => { + Action::ReturnPermit(pb::ReturnUpgradePermit { node: Some(crate::node_id_into_protobuf(*node)), }) } @@ -100,23 +100,19 @@ impl TryFrom for UpgradePermitAction { "UpgradePermitAction::action", ))?; Ok(match action { - Action::RequestUpgradePermit(request) => UpgradePermitAction::RequestUpgradePermit( + Action::RequestPermit(request) => UpgradePermitAction::RequestPermit( try_from_option_field(request.request, "RequestUpgradePermit::request")?, ), - Action::AuthorizeUpgradePermit(authorize) => { - UpgradePermitAction::AuthorizeUpgradePermit(Signed { - content: try_from_option_field( - authorize.request, - "AuthorizeUpgradePermit::request", - )?, - signature: signature_batch(authorize.signatures)?, - }) - } - Action::ReturnUpgradePermit(return_permit) => { - UpgradePermitAction::ReturnUpgradePermit { - node: crate::node_id_try_from_option(return_permit.node)?, - } - } + Action::AuthorizePermit(authorize) => UpgradePermitAction::AuthorizePermit(Signed { + content: try_from_option_field( + authorize.request, + "AuthorizeUpgradePermit::request", + )?, + signature: signature_batch(authorize.signatures)?, + }), + Action::ReturnPermit(return_permit) => UpgradePermitAction::ReturnPermit { + node: crate::node_id_try_from_option(return_permit.node)?, + }, }) } } @@ -163,7 +159,7 @@ mod tests { #[test] fn test_round_trip_request() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::RequestUpgradePermit( + actions: vec![UpgradePermitAction::RequestPermit( UpgradePermitAuthorizationRequest { requestor: node(3), request_height: Height::new(42), @@ -175,7 +171,7 @@ mod tests { #[test] fn test_round_trip_authorize() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::AuthorizeUpgradePermit(Signed { + actions: vec![UpgradePermitAction::AuthorizePermit(Signed { content: UpgradePermitAuthorizationRequest { requestor: node(5), request_height: Height::new(3), @@ -190,7 +186,7 @@ mod tests { #[test] fn test_round_trip_return() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::ReturnUpgradePermit { node: node(7) }], + actions: vec![UpgradePermitAction::ReturnPermit { node: node(7) }], }); } @@ -203,7 +199,7 @@ mod tests { fn test_serialize_with_limit_drops_overflow() { // A limit of 0 cannot fit any action, so nothing is serialized. let payload = UpgradePayload { - actions: vec![UpgradePermitAction::ReturnUpgradePermit { node: node(1) }], + actions: vec![UpgradePermitAction::ReturnPermit { node: node(1) }], }; assert!(payload.serialize_with_limit(NumBytes::new(0)).is_empty()); } @@ -214,7 +210,7 @@ mod tests { // action after it still does. let payload = UpgradePayload { actions: vec![ - UpgradePermitAction::AuthorizeUpgradePermit(Signed { + UpgradePermitAction::AuthorizePermit(Signed { content: UpgradePermitAuthorizationRequest { requestor: node(1), request_height: Height::new(4), @@ -226,11 +222,11 @@ mod tests { )]), }, }), - UpgradePermitAction::ReturnUpgradePermit { node: node(3) }, + UpgradePermitAction::ReturnPermit { node: node(3) }, ], }; let return_entry_len = UpgradePayload { - actions: vec![UpgradePermitAction::ReturnUpgradePermit { node: node(3) }], + actions: vec![UpgradePermitAction::ReturnPermit { node: node(3) }], } .serialize_with_limit(NumBytes::new(u64::MAX)) .len(); @@ -238,7 +234,7 @@ mod tests { let decoded = UpgradePayload::deserialize(&bytes).unwrap(); assert_eq!( decoded.actions, - vec![UpgradePermitAction::ReturnUpgradePermit { node: node(3) }] + vec![UpgradePermitAction::ReturnPermit { node: node(3) }] ); } @@ -246,11 +242,11 @@ mod tests { fn test_round_trip_multiple_actions() { round_trip(UpgradePayload { actions: vec![ - UpgradePermitAction::RequestUpgradePermit(UpgradePermitAuthorizationRequest { + UpgradePermitAction::RequestPermit(UpgradePermitAuthorizationRequest { requestor: node(1), request_height: Height::new(10), }), - UpgradePermitAction::AuthorizeUpgradePermit(Signed { + UpgradePermitAction::AuthorizePermit(Signed { content: UpgradePermitAuthorizationRequest { requestor: node(2), request_height: Height::new(4), @@ -259,7 +255,7 @@ mod tests { signatures_map: BTreeMap::new(), }, }), - UpgradePermitAction::ReturnUpgradePermit { node: node(3) }, + UpgradePermitAction::ReturnPermit { node: node(3) }, ], }); } diff --git a/rs/types/types/src/consensus/upgrade.rs b/rs/types/types/src/consensus/upgrade.rs index f87206be1eec..0a138faa3fb5 100644 --- a/rs/types/types/src/consensus/upgrade.rs +++ b/rs/types/types/src/consensus/upgrade.rs @@ -2,16 +2,16 @@ //! //! The permit flow works in three stages: //! -//! 1. **Request**: A block maker includes `UpgradePermitAction::RequestUpgradePermit` in +//! 1. **Request**: A block maker includes `UpgradePermitAction::RequestPermit` in //! its block when it wants to reboot. Validators check outstanding requests //! against the allowed max parallel reboots. //! -//! 2. **Authorize**: After the request block is finalized, each node gossips an +//! 2. **Authorize**: After the request block is finalized, nodes gossip an //! [`UpgradePermitAuthorizationShare`]. When a block maker collects enough -//! shares, it includes `UpgradePermitAction::AuthorizeUpgradePermit` in its block. +//! shares, it includes `UpgradePermitAction::AuthorizePermit` in its block. //! //! 3. **Return**: After rebooting, the node includes -//! `UpgradePermitAction::ReturnUpgradePermit` to release the slot. +//! `UpgradePermitAction::ReturnPermit` to release the slot. use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; use ic_protobuf::types::v1 as pb; @@ -33,17 +33,17 @@ pub enum UpgradePermitAction { /// Request permission to reboot. The block maker requests for itself. /// `request_height` is the height of the block containing this request, /// used for timeout tracking. - RequestUpgradePermit(UpgradePermitAuthorizationRequest), + RequestPermit(UpgradePermitAuthorizationRequest), /// Authorize a node to reboot — the signed request and the basic /// signatures over it collected from the staying members. - AuthorizeUpgradePermit( + AuthorizePermit( Signed< UpgradePermitAuthorizationRequest, BasicSignatureBatch, >, ), /// Release a previously authorized permit (reboot complete). - ReturnUpgradePermit { node: NodeId }, + ReturnPermit { node: NodeId }, } /// UpgradePermitAuthorizationRequest holds the values that are signed in an @@ -152,7 +152,7 @@ impl TryFrom for UpgradePermitAuthorization impl From for pb::UpgradePermitAuthorizationShareId { fn from(id: UpgradePermitAuthorizationShareId) -> Self { Self { - hash: id.hash.clone().get().0, + hash: id.hash.get().0, height: id.height.get(), } } @@ -163,7 +163,7 @@ impl TryFrom for UpgradePermitAuthorizati fn try_from(id: pb::UpgradePermitAuthorizationShareId) -> Result { Ok(Self { - hash: CryptoHash(id.hash.clone()).into(), + hash: CryptoHash(id.hash).into(), height: Height::from(id.height), }) } diff --git a/rs/types/types/src/crypto/hash.rs b/rs/types/types/src/crypto/hash.rs index fbb10202aacd..82a006852643 100644 --- a/rs/types/types/src/crypto/hash.rs +++ b/rs/types/types/src/crypto/hash.rs @@ -7,7 +7,7 @@ use crate::canister_http::{ use crate::consensus::{ Block, BlockMetadata, BlockPayload, CatchUpContent, CatchUpContentProtobufBytes, CatchUpShareContent, ConsensusMessage, EquivocationProof, FinalizationContent, HashedBlock, - NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationShare, certification::{ Certification, CertificationContent, CertificationMessage, CertificationShare, }, @@ -70,14 +70,7 @@ mod private { impl CryptoHashDomainSeal for EquivocationProof {} impl CryptoHashDomainSeal for BlockPayload {} - impl CryptoHashDomainSeal for UpgradePermitAuthorizationRequest {} - impl CryptoHashDomainSeal - for Signed< - UpgradePermitAuthorizationRequest, - BasicSignature, - > - { - } + impl CryptoHashDomainSeal for UpgradePermitAuthorizationShare {} impl CryptoHashDomainSeal for RandomBeaconContent {} impl CryptoHashDomainSeal for Signed> {} @@ -231,15 +224,7 @@ impl CryptoHashDomain for EquivocationProof { } } -impl CryptoHashDomain for UpgradePermitAuthorizationRequest { - fn domain(&self) -> String { - DomainSeparator::UpgradePermitAuthorizationRequest.to_string() - } -} - -impl CryptoHashDomain - for Signed> -{ +impl CryptoHashDomain for UpgradePermitAuthorizationShare { fn domain(&self) -> String { DomainSeparator::UpgradePermitAuthorizationShare.to_string() } diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 4131de9b9af4..bfbcf211f4ec 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -93,6 +93,7 @@ mod crypto_hash_stability { use crate::consensus::{RandomBeaconShare, RandomTape, RandomTapeShare}; use crate::crypto::AlgorithmId; use crate::crypto::CryptoHashableTestDummy; + use crate::crypto::SignedBytesWithoutDomainSeparator; use crate::crypto::canister_threshold_sig::{ ThresholdEcdsaSigShare, ThresholdSchnorrSigShare, idkg::{ @@ -453,18 +454,19 @@ mod crypto_hash_stability { ); } - /// Test stability of UpgradePermitAuthorizationRequest hash output + /// Test stability of the signed bytes of UpgradePermitAuthorizationRequest #[test] - fn upgrade_permit_authorization_request_stability() { + fn upgrade_permit_authorization_request_signed_bytes_stability() { let data = UpgradePermitAuthorizationRequest { requestor: NodeId::from(PrincipalId::new_node_test_id(42)), request_height: Height::from(42), }; - let hash = crypto_hash(&data); + let mut bytes = vec![]; + data.write_signed_bytes_without_domain_separator(&mut bytes); assert_eq!( - hex::encode(hash.get_ref().0.as_slice()), - "c01cc8564217818aaedb7d2441000413c44b75e5a7769ad25b8f7f30fe9b15a4", - "Hash of UpgradePermitAuthorizationRequest changed" + hex::encode(bytes), + "a269726571756573746f724a2a00000000000000fd016e726571756573745f686569676874182a", + "Signed bytes of UpgradePermitAuthorizationRequest changed" ); } From 547b366948a4f966f4ca50b30d71aa192c64e282 Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 24 Sep 2026 14:00:22 +0200 Subject: [PATCH 15/21] Update rs/types/types/src/consensus/upgrade.rs Co-authored-by: Pierugo Pace --- rs/types/types/src/consensus/upgrade.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/types/types/src/consensus/upgrade.rs b/rs/types/types/src/consensus/upgrade.rs index 0a138faa3fb5..5653ff44b727 100644 --- a/rs/types/types/src/consensus/upgrade.rs +++ b/rs/types/types/src/consensus/upgrade.rs @@ -6,7 +6,7 @@ //! its block when it wants to reboot. Validators check outstanding requests //! against the allowed max parallel reboots. //! -//! 2. **Authorize**: After the request block is finalized, nodes gossip an +//! 2. **Authorize**: After the request block is executed, nodes gossip an //! [`UpgradePermitAuthorizationShare`]. When a block maker collects enough //! shares, it includes `UpgradePermitAction::AuthorizePermit` in its block. //! From 6bfa154e3b487470f5490addf16247dc0fae572d Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 24 Sep 2026 15:16:52 +0200 Subject: [PATCH 16/21] Address review comments + remove unnecessary derives --- rs/consensus/utils/src/crypto.rs | 9 +- rs/interfaces/mocks/src/crypto.rs | 49 +++++----- rs/interfaces/src/crypto.rs | 12 +-- rs/protobuf/def/types/v1/artifact.proto | 2 +- rs/protobuf/def/types/v1/upgrade.proto | 14 +-- rs/protobuf/src/gen/types/types.v1.rs | 17 ++-- rs/types/types/src/batch.rs | 2 +- rs/types/types/src/batch/upgrade.rs | 57 +++++------ rs/types/types/src/consensus.rs | 2 +- rs/types/types/src/consensus/upgrade.rs | 94 +++++++++---------- rs/types/types/src/crypto/hash.rs | 18 ++-- .../types/src/crypto/hash/domain_separator.rs | 21 ++--- rs/types/types/src/crypto/hash/tests.rs | 24 ++--- rs/types/types/src/crypto/sign.rs | 8 +- 14 files changed, 142 insertions(+), 187 deletions(-) diff --git a/rs/consensus/utils/src/crypto.rs b/rs/consensus/utils/src/crypto.rs index 67dc60816ee2..20d69c0ee8e1 100644 --- a/rs/consensus/utils/src/crypto.rs +++ b/rs/consensus/utils/src/crypto.rs @@ -4,7 +4,7 @@ use ic_types::{ canister_http::CanisterHttpResponseReceipt, consensus::{ BlockMetadata, CatchUpContent, FinalizationContent, NotarizationContent, - RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, dkg, + RandomBeaconContent, RandomTapeContent, UpgradePermitRequest, dkg, hashed::Hashed, idkg::{IDkgComplaintContent, IDkgOpeningContent}, }, @@ -425,11 +425,8 @@ pub trait ConsensusCrypto: + SignVerify, RegistryVersion> + SignVerify, RegistryVersion> + SignVerify, RegistryVersion> - + SignVerify< - UpgradePermitAuthorizationRequest, - BasicSignature, - RegistryVersion, - > + SignVerify, NiDkgId> + + SignVerify, RegistryVersion> + + SignVerify, NiDkgId> + SignVerify, NiDkgId> + SignVerify, NiDkgId> + SignVerify, RegistryVersion> diff --git a/rs/interfaces/mocks/src/crypto.rs b/rs/interfaces/mocks/src/crypto.rs index b6754ea903b7..a3ede4c7f485 100644 --- a/rs/interfaces/mocks/src/crypto.rs +++ b/rs/interfaces/mocks/src/crypto.rs @@ -30,7 +30,7 @@ use ic_interfaces::crypto::{ use ic_types::canister_http::CanisterHttpResponseReceipt; use ic_types::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, - NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitRequest, certification::CertificationContent, dkg as consensus_dkg, idkg::{IDkgComplaintContent, IDkgOpeningContent}, @@ -310,9 +310,9 @@ mockall::mock! { &self, message: &CanisterHttpResponseReceipt, ) -> CryptoResult>; - pub fn sign_basic_upgrade_permit_authorization_request( - &self, message: &UpgradePermitAuthorizationRequest, - ) -> CryptoResult>; + pub fn sign_basic_upgrade_permit_request( + &self, message: &UpgradePermitRequest, + ) -> CryptoResult>; pub fn sign_basic_query( &self, message: &QueryResponseHash, @@ -494,33 +494,33 @@ mockall::mock! { )>, ) -> CryptoResult<()>; - // UpgradePermitAuthorizationRequest - pub fn verify_basic_sig_upgrade_permit_authorization_request( + // UpgradePermitRequest + pub fn verify_basic_sig_upgrade_permit_request( &self, - signature: &BasicSigOf, - message: &UpgradePermitAuthorizationRequest, signer: NodeId, + signature: &BasicSigOf, + message: &UpgradePermitRequest, signer: NodeId, registry_version: RegistryVersion, ) -> CryptoResult<()>; - pub fn combine_basic_sig_upgrade_permit_authorization_request( + pub fn combine_basic_sig_upgrade_permit_request( &self, - signatures: BTreeMap>, + signatures: BTreeMap>, registry_version: RegistryVersion, - ) -> CryptoResult>; + ) -> CryptoResult>; - pub fn verify_basic_sig_batch_upgrade_permit_authorization_request( + pub fn verify_basic_sig_batch_upgrade_permit_request( &self, - signature_batch: &BasicSignatureBatch, - message: &UpgradePermitAuthorizationRequest, + signature_batch: &BasicSignatureBatch, + message: &UpgradePermitRequest, registry_version: RegistryVersion, ) -> CryptoResult<()>; - pub fn verify_basic_sig_batch_multi_msg_upgrade_permit_authorization_request( + pub fn verify_basic_sig_batch_multi_msg_upgrade_permit_request( &self, inputs: Vec<( NodeId, - BasicSigOf, - UpgradePermitAuthorizationRequest, + BasicSigOf, + UpgradePermitRequest, RegistryVersion, )>, ) -> CryptoResult<()>; @@ -821,10 +821,7 @@ impl_basic_signer!(IDkgDealing, sign_basic_idkg_dealing); impl_basic_signer!(IDkgComplaintContent, sign_basic_idkg_complaint); impl_basic_signer!(IDkgOpeningContent, sign_basic_idkg_opening); impl_basic_signer!(CanisterHttpResponseReceipt, sign_basic_http); -impl_basic_signer!( - UpgradePermitAuthorizationRequest, - sign_basic_upgrade_permit_authorization_request -); +impl_basic_signer!(UpgradePermitRequest, sign_basic_upgrade_permit_request); impl_basic_signer!(QueryResponseHash, sign_basic_query); impl_basic_sig_verifier!( @@ -877,11 +874,11 @@ impl_basic_sig_verifier!( verify_basic_sig_batch_multi_msg_http ); impl_basic_sig_verifier!( - UpgradePermitAuthorizationRequest, - verify_basic_sig_upgrade_permit_authorization_request, - combine_basic_sig_upgrade_permit_authorization_request, - verify_basic_sig_batch_upgrade_permit_authorization_request, - verify_basic_sig_batch_multi_msg_upgrade_permit_authorization_request + UpgradePermitRequest, + verify_basic_sig_upgrade_permit_request, + combine_basic_sig_upgrade_permit_request, + verify_basic_sig_batch_upgrade_permit_request, + verify_basic_sig_batch_multi_msg_upgrade_permit_request ); impl_threshold_signer!(CertificationContent, sign_threshold_certification); diff --git a/rs/interfaces/src/crypto.rs b/rs/interfaces/src/crypto.rs index 5cb6aa2058ae..e85ec44c1fb8 100644 --- a/rs/interfaces/src/crypto.rs +++ b/rs/interfaces/src/crypto.rs @@ -26,7 +26,7 @@ pub use vetkd::*; use ic_crypto_interfaces_sig_verification::BasicSigVerifierByPublicKey; use ic_types::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, - NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitRequest, certification::CertificationContent, dkg as consensus_dkg, idkg::{IDkgComplaintContent, IDkgOpeningContent}, @@ -79,9 +79,9 @@ pub trait Crypto: + ThresholdSchnorrSigner + ThresholdSchnorrSigVerifier + VetKdProtocol - // UpgradePermitAuthorizationRequest - + BasicSigner - + BasicSigVerifier + // UpgradePermitRequest + + BasicSigner + + BasicSigVerifier // CanisterHttpResponse + BasicSigner + BasicSigVerifier @@ -153,8 +153,8 @@ impl Crypto for T where + ThresholdSchnorrSigner + ThresholdSchnorrSigVerifier + VetKdProtocol - + BasicSigner - + BasicSigVerifier + + BasicSigner + + BasicSigVerifier + BasicSigVerifierByPublicKey + BasicSigVerifierByPublicKey + ThresholdSigner diff --git a/rs/protobuf/def/types/v1/artifact.proto b/rs/protobuf/def/types/v1/artifact.proto index adf644955a7d..56080077df92 100644 --- a/rs/protobuf/def/types/v1/artifact.proto +++ b/rs/protobuf/def/types/v1/artifact.proto @@ -12,7 +12,7 @@ message DkgMessageId { uint64 height = 2; } -message UpgradePermitAuthorizationShareId { +message UpgradeAuthorizationShareId { bytes hash = 1; uint64 height = 2; } diff --git a/rs/protobuf/def/types/v1/upgrade.proto b/rs/protobuf/def/types/v1/upgrade.proto index 93f82282c8dc..f015b6be1599 100644 --- a/rs/protobuf/def/types/v1/upgrade.proto +++ b/rs/protobuf/def/types/v1/upgrade.proto @@ -7,10 +7,6 @@ package types.v1; import "types/v1/signature.proto"; import "types/v1/types.proto"; -message UpgradePayload { - repeated UpgradePermitAction actions = 1; -} - message UpgradePermitAction { oneof action { RequestUpgradePermit request_permit = 1; @@ -19,17 +15,17 @@ message UpgradePermitAction { } } -message UpgradePermitAuthorizationRequest { +message UpgradePermitRequest { types.v1.NodeId requestor = 1; uint64 request_height = 2; } message RequestUpgradePermit { - UpgradePermitAuthorizationRequest request = 1; + UpgradePermitRequest request = 1; } message AuthorizeUpgradePermit { - UpgradePermitAuthorizationRequest request = 1; + UpgradePermitRequest request = 1; repeated types.v1.BasicSignature signatures = 2; } @@ -37,7 +33,7 @@ message ReturnUpgradePermit { types.v1.NodeId node = 1; } -message UpgradePermitAuthorizationShare { - UpgradePermitAuthorizationRequest request = 1; +message UpgradeAuthorizationShare { + UpgradePermitRequest request = 1; types.v1.BasicSignature signature = 2; } diff --git a/rs/protobuf/src/gen/types/types.v1.rs b/rs/protobuf/src/gen/types/types.v1.rs index 0d22fc051184..47af23d5d136 100644 --- a/rs/protobuf/src/gen/types/types.v1.rs +++ b/rs/protobuf/src/gen/types/types.v1.rs @@ -1317,7 +1317,7 @@ pub struct DkgMessageId { pub height: u64, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct UpgradePermitAuthorizationShareId { +pub struct UpgradeAuthorizationShareId { #[prost(bytes = "vec", tag = "1")] pub hash: ::prost::alloc::vec::Vec, #[prost(uint64, tag = "2")] @@ -1860,11 +1860,6 @@ impl ChainKeyErrorCode { } } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct UpgradePayload { - #[prost(message, repeated, tag = "1")] - pub actions: ::prost::alloc::vec::Vec, -} -#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpgradePermitAction { #[prost(oneof = "upgrade_permit_action::Action", tags = "1, 2, 3")] pub action: ::core::option::Option, @@ -1882,7 +1877,7 @@ pub mod upgrade_permit_action { } } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct UpgradePermitAuthorizationRequest { +pub struct UpgradePermitRequest { #[prost(message, optional, tag = "1")] pub requestor: ::core::option::Option, #[prost(uint64, tag = "2")] @@ -1891,12 +1886,12 @@ pub struct UpgradePermitAuthorizationRequest { #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RequestUpgradePermit { #[prost(message, optional, tag = "1")] - pub request: ::core::option::Option, + pub request: ::core::option::Option, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct AuthorizeUpgradePermit { #[prost(message, optional, tag = "1")] - pub request: ::core::option::Option, + pub request: ::core::option::Option, #[prost(message, repeated, tag = "2")] pub signatures: ::prost::alloc::vec::Vec, } @@ -1906,9 +1901,9 @@ pub struct ReturnUpgradePermit { pub node: ::core::option::Option, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct UpgradePermitAuthorizationShare { +pub struct UpgradeAuthorizationShare { #[prost(message, optional, tag = "1")] - pub request: ::core::option::Option, + pub request: ::core::option::Option, #[prost(message, optional, tag = "2")] pub signature: ::core::option::Option, } diff --git a/rs/types/types/src/batch.rs b/rs/types/types/src/batch.rs index fd53d8825f53..17cd234f65e3 100644 --- a/rs/types/types/src/batch.rs +++ b/rs/types/types/src/batch.rs @@ -265,7 +265,7 @@ impl BlockmakerMetrics { } } -/// Given an iterator of [`Message`]s, this function will deserialize the messages +/// Given an iterator of [`Message`]s, this function will serialize the messages /// into a byte vector. /// /// The function is given a `max_size` limit, and guarantees that the buffer will be diff --git a/rs/types/types/src/batch/upgrade.rs b/rs/types/types/src/batch/upgrade.rs index 74ae031f8c07..88def9d6efb3 100644 --- a/rs/types/types/src/batch/upgrade.rs +++ b/rs/types/types/src/batch/upgrade.rs @@ -2,10 +2,10 @@ use ic_base_types::NumBytes; use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; use ic_protobuf::types::v1 as pb; use pb::upgrade_permit_action::Action; -use prost::Message as _; use std::collections::BTreeMap; -use crate::consensus::UpgradePermitAuthorizationRequest; +use super::{iterator_to_bytes, slice_to_messages}; +use crate::consensus::UpgradePermitRequest; use crate::consensus::upgrade::UpgradePermitAction; use crate::crypto::Signed; use crate::signature::{BasicSignature, BasicSignatureBatch}; @@ -23,30 +23,19 @@ impl UpgradePayload { /// payload fits into the `byte_limit`. Smaller actions after a dropped /// action can still be included. pub fn serialize_with_limit(&self, byte_limit: NumBytes) -> Vec { - let mut proto = pb::UpgradePayload::default(); - let mut remaining = byte_limit.get() as usize; - for action in &self.actions { - let entry = pb::UpgradePermitAction::from(action); - // One repeated field entry: the key, the varint length, and the - // message bytes. - let len = entry.encoded_len(); - let entry_len = 1 + prost::length_delimiter_len(len) + len; - if entry_len > remaining { - continue; - } - remaining -= entry_len; - proto.actions.push(entry); - } - proto.encode_to_vec() + iterator_to_bytes( + self.actions.iter().map(pb::UpgradePermitAction::from), + byte_limit, + ) } /// Deserializes an [`UpgradePayload`]. An empty byte slice yields an empty /// payload. pub fn deserialize(data: &[u8]) -> Result { - let proto = pb::UpgradePayload::decode(data).map_err(ProxyDecodeError::DecodeError)?; + let messages: Vec = + slice_to_messages(data).map_err(ProxyDecodeError::DecodeError)?; Ok(Self { - actions: proto - .actions + actions: messages .into_iter() .map(UpgradePermitAction::try_from) .collect::>()?, @@ -59,14 +48,12 @@ impl From<&UpgradePermitAction> for pb::UpgradePermitAction { let proto_action = match action { UpgradePermitAction::RequestPermit(request) => { Action::RequestPermit(pb::RequestUpgradePermit { - request: Some(pb::UpgradePermitAuthorizationRequest::from(request)), + request: Some(pb::UpgradePermitRequest::from(request)), }) } UpgradePermitAction::AuthorizePermit(authorization) => { Action::AuthorizePermit(pb::AuthorizeUpgradePermit { - request: Some(pb::UpgradePermitAuthorizationRequest::from( - &authorization.content, - )), + request: Some(pb::UpgradePermitRequest::from(&authorization.content)), signatures: authorization .signature .signatures_map @@ -121,10 +108,10 @@ impl TryFrom for UpgradePermitAction { /// [`BasicSignatureBatch`], rejecting duplicate signers. fn signature_batch( signatures: Vec, -) -> Result, ProxyDecodeError> { +) -> Result, ProxyDecodeError> { let mut signatures_map = BTreeMap::new(); for signature in signatures { - let signature: BasicSignature = signature.try_into()?; + let signature: BasicSignature = signature.try_into()?; if let Some(previous) = signatures_map.insert(signature.signer, signature.signature) { // Unwrap is fine, entry has just been inserted let new = signatures_map.get(&signature.signer).unwrap(); @@ -159,12 +146,10 @@ mod tests { #[test] fn test_round_trip_request() { round_trip(UpgradePayload { - actions: vec![UpgradePermitAction::RequestPermit( - UpgradePermitAuthorizationRequest { - requestor: node(3), - request_height: Height::new(42), - }, - )], + actions: vec![UpgradePermitAction::RequestPermit(UpgradePermitRequest { + requestor: node(3), + request_height: Height::new(42), + })], }); } @@ -172,7 +157,7 @@ mod tests { fn test_round_trip_authorize() { round_trip(UpgradePayload { actions: vec![UpgradePermitAction::AuthorizePermit(Signed { - content: UpgradePermitAuthorizationRequest { + content: UpgradePermitRequest { requestor: node(5), request_height: Height::new(3), }, @@ -211,7 +196,7 @@ mod tests { let payload = UpgradePayload { actions: vec![ UpgradePermitAction::AuthorizePermit(Signed { - content: UpgradePermitAuthorizationRequest { + content: UpgradePermitRequest { requestor: node(1), request_height: Height::new(4), }, @@ -242,12 +227,12 @@ mod tests { fn test_round_trip_multiple_actions() { round_trip(UpgradePayload { actions: vec![ - UpgradePermitAction::RequestPermit(UpgradePermitAuthorizationRequest { + UpgradePermitAction::RequestPermit(UpgradePermitRequest { requestor: node(1), request_height: Height::new(10), }), UpgradePermitAction::AuthorizePermit(Signed { - content: UpgradePermitAuthorizationRequest { + content: UpgradePermitRequest { requestor: node(2), request_height: Height::new(4), }, diff --git a/rs/types/types/src/consensus.rs b/rs/types/types/src/consensus.rs index d685bcb9a7d7..447f05546637 100644 --- a/rs/types/types/src/consensus.rs +++ b/rs/types/types/src/consensus.rs @@ -34,7 +34,7 @@ mod payload; pub mod thunk; pub mod upgrade; -pub use upgrade::{UpgradePermitAuthorizationRequest, UpgradePermitAuthorizationShare}; +pub use upgrade::{UpgradeAuthorizationShare, UpgradePermitRequest}; pub use catchup::*; use hashed::Hashed; diff --git a/rs/types/types/src/consensus/upgrade.rs b/rs/types/types/src/consensus/upgrade.rs index 5653ff44b727..66bce5bc9656 100644 --- a/rs/types/types/src/consensus/upgrade.rs +++ b/rs/types/types/src/consensus/upgrade.rs @@ -7,7 +7,7 @@ //! against the allowed max parallel reboots. //! //! 2. **Authorize**: After the request block is executed, nodes gossip an -//! [`UpgradePermitAuthorizationShare`]. When a block maker collects enough +//! [`UpgradeAuthorizationShare`]. When a block maker collects enough //! shares, it includes `UpgradePermitAction::AuthorizePermit` in its block. //! //! 3. **Return**: After rebooting, the node includes @@ -15,7 +15,7 @@ use ic_protobuf::proxy::{ProxyDecodeError, try_from_option_field}; use ic_protobuf::types::v1 as pb; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use crate::artifact::{IdentifiableArtifact, PbArtifact}; use crate::consensus::HasHeight; @@ -33,124 +33,116 @@ pub enum UpgradePermitAction { /// Request permission to reboot. The block maker requests for itself. /// `request_height` is the height of the block containing this request, /// used for timeout tracking. - RequestPermit(UpgradePermitAuthorizationRequest), + RequestPermit(UpgradePermitRequest), /// Authorize a node to reboot — the signed request and the basic - /// signatures over it collected from the staying members. - AuthorizePermit( - Signed< - UpgradePermitAuthorizationRequest, - BasicSignatureBatch, - >, - ), + /// signatures over it collected from other nodes. + AuthorizePermit(Signed>), /// Release a previously authorized permit (reboot complete). ReturnPermit { node: NodeId }, } -/// UpgradePermitAuthorizationRequest holds the values that are signed in an +/// UpgradePermitRequest holds the values that are signed in an /// upgrade permit authorization share. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] -pub struct UpgradePermitAuthorizationRequest { +#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize)] +pub struct UpgradePermitRequest { pub requestor: NodeId, pub request_height: Height, } -impl SignedBytesWithoutDomainSeparator for UpgradePermitAuthorizationRequest { +impl SignedBytesWithoutDomainSeparator for UpgradePermitRequest { fn write_signed_bytes_without_domain_separator(&self, bytes: &mut Vec) { serde_cbor::to_writer(bytes, &self).unwrap(); } } -impl HasHeight for UpgradePermitAuthorizationRequest { +impl HasHeight for UpgradePermitRequest { fn height(&self) -> Height { self.request_height } } -pub type UpgradePermitAuthorizationShare = BasicSigned; +pub type UpgradeAuthorizationShare = BasicSigned; /// Upgrade permit authorization message identifier carries both a message hash /// and a height, used by the upgrade permit auth pool for lookup. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] -pub struct UpgradePermitAuthorizationShareId { - pub hash: CryptoHashOf, +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct UpgradeAuthorizationShareId { pub height: Height, + pub hash: CryptoHashOf, } -impl HasHeight for UpgradePermitAuthorizationShareId { +impl HasHeight for UpgradeAuthorizationShareId { fn height(&self) -> Height { self.height } } -impl IdentifiableArtifact for UpgradePermitAuthorizationShare { +impl IdentifiableArtifact for UpgradeAuthorizationShare { const NAME: &'static str = "upgrade"; - type Id = UpgradePermitAuthorizationShareId; + type Id = UpgradeAuthorizationShareId; fn id(&self) -> Self::Id { - UpgradePermitAuthorizationShareId { + UpgradeAuthorizationShareId { hash: crypto_hash(self), height: self.content.height(), } } } -impl From<&UpgradePermitAuthorizationShare> for UpgradePermitAuthorizationShareId { - fn from(share: &UpgradePermitAuthorizationShare) -> Self { +impl From<&UpgradeAuthorizationShare> for UpgradeAuthorizationShareId { + fn from(share: &UpgradeAuthorizationShare) -> Self { share.id() } } -impl PbArtifact for UpgradePermitAuthorizationShare { - type PbId = pb::UpgradePermitAuthorizationShareId; +impl PbArtifact for UpgradeAuthorizationShare { + type PbId = pb::UpgradeAuthorizationShareId; type PbIdError = ProxyDecodeError; - type PbMessage = pb::UpgradePermitAuthorizationShare; + type PbMessage = pb::UpgradeAuthorizationShare; type PbMessageError = ProxyDecodeError; } -impl From<&UpgradePermitAuthorizationRequest> for pb::UpgradePermitAuthorizationRequest { - fn from(content: &UpgradePermitAuthorizationRequest) -> Self { - pb::UpgradePermitAuthorizationRequest { +impl From<&UpgradePermitRequest> for pb::UpgradePermitRequest { + fn from(content: &UpgradePermitRequest) -> Self { + pb::UpgradePermitRequest { requestor: Some(node_id_into_protobuf(content.requestor)), request_height: content.request_height.get(), } } } -impl TryFrom for UpgradePermitAuthorizationRequest { +impl TryFrom for UpgradePermitRequest { type Error = ProxyDecodeError; - fn try_from(content: pb::UpgradePermitAuthorizationRequest) -> Result { - Ok(UpgradePermitAuthorizationRequest { + fn try_from(content: pb::UpgradePermitRequest) -> Result { + Ok(UpgradePermitRequest { requestor: node_id_try_from_option(content.requestor)?, request_height: Height::from(content.request_height), }) } } -impl From for pb::UpgradePermitAuthorizationShare { - fn from(share: UpgradePermitAuthorizationShare) -> Self { - pb::UpgradePermitAuthorizationShare { - request: Some(pb::UpgradePermitAuthorizationRequest::from(&share.content)), +impl From for pb::UpgradeAuthorizationShare { + fn from(share: UpgradeAuthorizationShare) -> Self { + pb::UpgradeAuthorizationShare { + request: Some(pb::UpgradePermitRequest::from(&share.content)), signature: Some(pb::BasicSignature::from(share.signature)), } } } -impl TryFrom for UpgradePermitAuthorizationShare { +impl TryFrom for UpgradeAuthorizationShare { type Error = ProxyDecodeError; - fn try_from(message: pb::UpgradePermitAuthorizationShare) -> Result { - let content = - try_from_option_field(message.request, "UpgradePermitAuthorizationShare::request")?; - let signature = try_from_option_field( - message.signature, - "UpgradePermitAuthorizationShare::signature", - )?; - Ok(UpgradePermitAuthorizationShare { content, signature }) + fn try_from(message: pb::UpgradeAuthorizationShare) -> Result { + let content = try_from_option_field(message.request, "UpgradeAuthorizationShare::request")?; + let signature = + try_from_option_field(message.signature, "UpgradeAuthorizationShare::signature")?; + Ok(UpgradeAuthorizationShare { content, signature }) } } -impl From for pb::UpgradePermitAuthorizationShareId { - fn from(id: UpgradePermitAuthorizationShareId) -> Self { +impl From for pb::UpgradeAuthorizationShareId { + fn from(id: UpgradeAuthorizationShareId) -> Self { Self { hash: id.hash.get().0, height: id.height.get(), @@ -158,10 +150,10 @@ impl From for pb::UpgradePermitAuthorizationS } } -impl TryFrom for UpgradePermitAuthorizationShareId { +impl TryFrom for UpgradeAuthorizationShareId { type Error = ProxyDecodeError; - fn try_from(id: pb::UpgradePermitAuthorizationShareId) -> Result { + fn try_from(id: pb::UpgradeAuthorizationShareId) -> Result { Ok(Self { hash: CryptoHash(id.hash).into(), height: Height::from(id.height), diff --git a/rs/types/types/src/crypto/hash.rs b/rs/types/types/src/crypto/hash.rs index 82a006852643..61f8e2280f6a 100644 --- a/rs/types/types/src/crypto/hash.rs +++ b/rs/types/types/src/crypto/hash.rs @@ -7,7 +7,7 @@ use crate::canister_http::{ use crate::consensus::{ Block, BlockMetadata, BlockPayload, CatchUpContent, CatchUpContentProtobufBytes, CatchUpShareContent, ConsensusMessage, EquivocationProof, FinalizationContent, HashedBlock, - NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationShare, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradeAuthorizationShare, certification::{ Certification, CertificationContent, CertificationMessage, CertificationShare, }, @@ -70,8 +70,6 @@ mod private { impl CryptoHashDomainSeal for EquivocationProof {} impl CryptoHashDomainSeal for BlockPayload {} - impl CryptoHashDomainSeal for UpgradePermitAuthorizationShare {} - impl CryptoHashDomainSeal for RandomBeaconContent {} impl CryptoHashDomainSeal for Signed> {} impl CryptoHashDomainSeal @@ -131,6 +129,8 @@ mod private { impl CryptoHashDomainSeal for CanisterHttpResponseMetadata {} impl CryptoHashDomainSeal for CanisterHttpResponseShare {} + impl CryptoHashDomainSeal for UpgradeAuthorizationShare {} + impl CryptoHashDomainSeal for CryptoHashableTestDummy {} } @@ -224,12 +224,6 @@ impl CryptoHashDomain for EquivocationProof { } } -impl CryptoHashDomain for UpgradePermitAuthorizationShare { - fn domain(&self) -> String { - DomainSeparator::UpgradePermitAuthorizationShare.to_string() - } -} - impl CryptoHashDomain for BlockPayload { fn domain(&self) -> String { DomainSeparator::InmemoryPayload.to_string() @@ -418,6 +412,12 @@ impl CryptoHashDomain for Signed String { + DomainSeparator::UpgradeAuthorizationShare.to_string() + } +} + impl CryptoHashDomain for CryptoHashableTestDummy { fn domain(&self) -> String { "test_struct_domain".to_string() diff --git a/rs/types/types/src/crypto/hash/domain_separator.rs b/rs/types/types/src/crypto/hash/domain_separator.rs index 9f5659f56876..235fd82cf1b6 100644 --- a/rs/types/types/src/crypto/hash/domain_separator.rs +++ b/rs/types/types/src/crypto/hash/domain_separator.rs @@ -16,8 +16,8 @@ pub enum DomainSeparator { BlockMetadata, BlockMetadataProposal, EquivocationProof, - UpgradePermitAuthorizationRequest, - UpgradePermitAuthorizationShare, + UpgradePermitRequest, + UpgradeAuthorizationShare, InmemoryPayload, RandomBeaconContent, RandomBeacon, @@ -82,12 +82,8 @@ impl DomainSeparator { DomainSeparator::BlockMetadata => "block_metadata_domain", DomainSeparator::BlockMetadataProposal => "block_metadata_proposal_domain", DomainSeparator::EquivocationProof => "equivocation_proof_domain", - DomainSeparator::UpgradePermitAuthorizationRequest => { - "upgrade_permit_authorization_request_domain" - } - DomainSeparator::UpgradePermitAuthorizationShare => { - "upgrade_permit_authorization_share_domain" - } + DomainSeparator::UpgradePermitRequest => "upgrade_permit_request_domain", + DomainSeparator::UpgradeAuthorizationShare => "upgrade_authorization_share_domain", DomainSeparator::InmemoryPayload => "inmemory_payload_domain", DomainSeparator::RandomBeaconContent => "random_beacon_content_domain", DomainSeparator::RandomBeacon => "random_beacon_domain", @@ -202,13 +198,10 @@ fn domain_separators_are_stable() { ("BlockMetadata", "block_metadata_domain"), ("BlockMetadataProposal", "block_metadata_proposal_domain"), ("EquivocationProof", "equivocation_proof_domain"), + ("UpgradePermitRequest", "upgrade_permit_request_domain"), ( - "UpgradePermitAuthorizationRequest", - "upgrade_permit_authorization_request_domain", - ), - ( - "UpgradePermitAuthorizationShare", - "upgrade_permit_authorization_share_domain", + "UpgradeAuthorizationShare", + "upgrade_authorization_share_domain", ), ("InmemoryPayload", "inmemory_payload_domain"), ("RandomBeaconContent", "random_beacon_content_domain"), diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index bfbcf211f4ec..20afc1c8b401 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -78,8 +78,8 @@ mod crypto_hash_stability { CatchUpPackage, CatchUpPackageShare, CatchUpShareContent, ConsensusMessage, DataPayload, EquivocationProof, Finalization, FinalizationContent, FinalizationShare, HashedBlock, HashedRandomBeacon, Notarization, NotarizationContent, NotarizationShare, Payload, - RandomBeacon, RandomBeaconContent, RandomTapeContent, Rank, - UpgradePermitAuthorizationRequest, UpgradePermitAuthorizationShare, + RandomBeacon, RandomBeaconContent, RandomTapeContent, Rank, UpgradeAuthorizationShare, + UpgradePermitRequest, certification::{ Certification, CertificationContent, CertificationMessage, CertificationShare, }, @@ -454,10 +454,10 @@ mod crypto_hash_stability { ); } - /// Test stability of the signed bytes of UpgradePermitAuthorizationRequest + /// Test stability of the signed bytes of UpgradePermitRequest #[test] - fn upgrade_permit_authorization_request_signed_bytes_stability() { - let data = UpgradePermitAuthorizationRequest { + fn upgrade_permit_request_signed_bytes_stability() { + let data = UpgradePermitRequest { requestor: NodeId::from(PrincipalId::new_node_test_id(42)), request_height: Height::from(42), }; @@ -466,15 +466,15 @@ mod crypto_hash_stability { assert_eq!( hex::encode(bytes), "a269726571756573746f724a2a00000000000000fd016e726571756573745f686569676874182a", - "Signed bytes of UpgradePermitAuthorizationRequest changed" + "Signed bytes of UpgradePermitRequest changed" ); } - /// Test stability of UpgradePermitAuthorizationShare hash output + /// Test stability of UpgradeAuthorizationShare hash output #[test] - fn upgrade_permit_authorization_share_stability() { - let data: UpgradePermitAuthorizationShare = Signed { - content: UpgradePermitAuthorizationRequest { + fn upgrade_authorization_share_stability() { + let data: UpgradeAuthorizationShare = Signed { + content: UpgradePermitRequest { requestor: NodeId::from(PrincipalId::new_node_test_id(42)), request_height: Height::from(42), }, @@ -486,8 +486,8 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "c8468fda9b05e8d21600642039b055bc97fc86226395b84f36ac351c00451bec", - "Hash of UpgradePermitAuthorizationShare changed" + "1e780154b2467bdf06efa99128aa50b5ad8db4a494a300cbe9d35b9747e85c98", + "Hash of UpgradeAuthorizationShare changed" ); } diff --git a/rs/types/types/src/crypto/sign.rs b/rs/types/types/src/crypto/sign.rs index 0409177185ba..8b3fb2f5a00b 100644 --- a/rs/types/types/src/crypto/sign.rs +++ b/rs/types/types/src/crypto/sign.rs @@ -4,7 +4,7 @@ use super::hash::domain_separator::DomainSeparator; use crate::canister_http::CanisterHttpResponseReceipt; use crate::consensus::{ BlockMetadata, CatchUpContent, CatchUpContentProtobufBytes, FinalizationContent, - NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitAuthorizationRequest, + NotarizationContent, RandomBeaconContent, RandomTapeContent, UpgradePermitRequest, certification::CertificationContent, dkg::DealingContent, idkg::{IDkgComplaintContent, IDkgOpeningContent}, @@ -64,7 +64,7 @@ mod private { impl SignatureDomainSeal for DealingContent {} impl SignatureDomainSeal for NotarizationContent {} impl SignatureDomainSeal for FinalizationContent {} - impl SignatureDomainSeal for UpgradePermitAuthorizationRequest {} + impl SignatureDomainSeal for UpgradePermitRequest {} impl SignatureDomainSeal for IDkgDealing {} impl SignatureDomainSeal for SignedIDkgDealing {} impl SignatureDomainSeal for IDkgComplaintContent {} @@ -116,9 +116,9 @@ impl SignatureDomain for FinalizationContent { } } -impl SignatureDomain for UpgradePermitAuthorizationRequest { +impl SignatureDomain for UpgradePermitRequest { fn domain(&self) -> Vec { - domain_with_prepended_length(DomainSeparator::UpgradePermitAuthorizationRequest.as_str()) + domain_with_prepended_length(DomainSeparator::UpgradePermitRequest.as_str()) } } From ccd9cda3df424ba43d7c3c7fe74d885873205ae2 Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 24 Sep 2026 17:29:40 +0200 Subject: [PATCH 17/21] Address review comments --- rs/consensus/tests/framework/types.rs | 2 +- rs/consensus/upgrade/src/payload_builder.rs | 10 +++--- rs/replica/setup_ic_network/src/lib.rs | 4 +-- rs/state_machine_tests/src/lib.rs | 4 +-- rs/types/types/src/crypto/hash/tests.rs | 34 ++++++++++++++------- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/rs/consensus/tests/framework/types.rs b/rs/consensus/tests/framework/types.rs index 9c89ea3447ac..59921d3beb05 100644 --- a/rs/consensus/tests/framework/types.rs +++ b/rs/consensus/tests/framework/types.rs @@ -6,7 +6,7 @@ use ic_artifact_pool::{ use ic_config::artifact_pool::ArtifactPoolConfig; use ic_consensus::consensus::{ConsensusBouncer, ConsensusImpl}; use ic_consensus_idkg::IDkgImpl; -use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilder; +use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilderImpl; use ic_consensus_utils::{MAX_CONSENSUS_THREADS, build_thread_pool}; use ic_https_outcalls_consensus::test_utils::FakeCanisterHttpPayloadBuilder; use ic_interfaces::{ diff --git a/rs/consensus/upgrade/src/payload_builder.rs b/rs/consensus/upgrade/src/payload_builder.rs index 6f4a5ded318d..7ba5fb2785bb 100644 --- a/rs/consensus/upgrade/src/payload_builder.rs +++ b/rs/consensus/upgrade/src/payload_builder.rs @@ -5,9 +5,9 @@ use ic_interfaces::validation::{ValidationError, ValidationResult}; use ic_types::batch::{UpgradePayload, ValidationContext}; use ic_types::{Height, NumBytes}; -pub struct UpgradePayloadBuilder; +pub struct UpgradePayloadBuilderImpl; -impl BatchPayloadBuilder for UpgradePayloadBuilder { +impl BatchPayloadBuilder for UpgradePayloadBuilderImpl { fn build_payload( &self, _height: Height, @@ -56,7 +56,7 @@ mod tests { fn test_build_payload_is_empty() { let context = validation_context(); assert!( - UpgradePayloadBuilder + UpgradePayloadBuilderImpl .build_payload(Height::from(1), NumBytes::new(u64::MAX), &[], &context) .is_empty() ); @@ -70,7 +70,7 @@ mod tests { validation_context: &context, }; assert!(matches!( - UpgradePayloadBuilder.validate_payload( + UpgradePayloadBuilderImpl.validate_payload( Height::from(1), &proposal_context, &[0xFF, 0xFF], @@ -92,7 +92,7 @@ mod tests { validation_context: &context, }; assert!( - UpgradePayloadBuilder + UpgradePayloadBuilderImpl .validate_payload(Height::from(1), &proposal_context, &[], &[]) .is_ok() ); diff --git a/rs/replica/setup_ic_network/src/lib.rs b/rs/replica/setup_ic_network/src/lib.rs index e45087265394..694d5d536ae5 100644 --- a/rs/replica/setup_ic_network/src/lib.rs +++ b/rs/replica/setup_ic_network/src/lib.rs @@ -16,7 +16,7 @@ use ic_consensus_chain_key::ChainKeyPayloadBuilderImpl; use ic_consensus_dkg::DkgBouncer; use ic_consensus_idkg::{IDkgBouncer, IDkgStatsImpl}; use ic_consensus_manager::{AbortableBroadcastChannel, AbortableBroadcastChannelBuilder}; -use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilder; +use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilderImpl; use ic_consensus_utils::{ MAX_CONSENSUS_THREADS, build_thread_pool, crypto::ConsensusCrypto, pool_reader::PoolReader, }; @@ -545,7 +545,7 @@ fn start_consensus( log.clone(), )); - let upgrade_payload_builder = Arc::new(UpgradePayloadBuilder); + let upgrade_payload_builder = Arc::new(UpgradePayloadBuilderImpl); // ------------------------------------------------------------------------ let replica_config = ReplicaConfig { diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index bb5a28c6be42..148db8efb415 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -13,7 +13,7 @@ use ic_config::{ }; use ic_consensus::consensus::payload_builder::PayloadBuilderImpl; use ic_consensus_cup_utils::make_registry_cup; -use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilder; +use ic_consensus_upgrade::payload_builder::UpgradePayloadBuilderImpl; use ic_consensus_utils::{MAX_CONSENSUS_THREADS, build_thread_pool, crypto::SignVerify}; use ic_crypto_test_utils_crypto_returning_ok::CryptoReturningOk; use ic_crypto_test_utils_ni_dkg::{ @@ -2216,7 +2216,7 @@ impl StateMachine { )); let chain_key_payload_builder = Arc::new(MockBatchPayloadBuilder::new().expect_noop()); - let upgrade_payload_builder = Arc::new(UpgradePayloadBuilder); + let upgrade_payload_builder = Arc::new(UpgradePayloadBuilderImpl); let cancellation_token = tokio_util::sync::CancellationToken::new(); let cancellation_token_clone = cancellation_token.clone(); diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 46b528f388cc..19d0d6508ac5 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -78,12 +78,15 @@ mod crypto_hash_stability { CatchUpPackage, CatchUpPackageShare, CatchUpShareContent, ConsensusMessage, DataPayload, EquivocationProof, Finalization, FinalizationContent, FinalizationShare, HashedBlock, HashedRandomBeacon, Notarization, NotarizationContent, NotarizationShare, Payload, - RandomBeacon, RandomBeaconContent, RandomTapeContent, Rank, UpgradeAuthorizationShare, - UpgradePermitRequest, + RandomBeacon, RandomBeaconContent, RandomTapeContent, Rank, SummaryPayload, + UpgradeAuthorizationShare, UpgradePermitRequest, certification::{ Certification, CertificationContent, CertificationMessage, CertificationShare, }, - dkg::{DealingContent, DkgDataPayload, Message as DkgMessage}, + dkg::{ + DealingContent, DkgDataPayload, DkgSummary, Message as DkgMessage, + SubnetSplittingStatus, + }, hashed::Hashed, idkg::{ EcdsaSigShare, IDkgComplaintContent, IDkgMessage, IDkgOpeningContent, RequestId, @@ -590,7 +593,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "c20a87578beb94df369dabfefc30c0d47d170c75d68236aae3b16335c0f21c4a", + "29390083388965b468a0b4dcf653be560bf4ef0a58150acbf826dcac46890d13", "Hash of CatchUpContent changed" ); } @@ -608,7 +611,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "db509a477f3ed01ec251325527e946b2e674f249d013bafc0d061620000a6e0d", + "8c535dda6ce448077a4983e2153ffd299c3a0caa1652379989c89c4964720aa2", "Hash of CatchUpShareContent changed" ); } @@ -656,7 +659,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "33c4f3fb79a8520a4c1d6d814aa5bae53e5aa58ad517dfddec45be7dfd930053", + "90321b317ee0849ebbfd07e3ca0a3bc1595600debf4e84f8a427414b487dd883", "Hash of CatchUpPackage changed" ); } @@ -686,7 +689,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "47648b17b0b80122fa1adc34a6d6e82ae8fb5af4a92b2495c41c91052ace1a10", + "fd257cf9d018ff22f539008e785ed2a40e055eea5d78b4fa32ffbce14405aee8", "Hash of CatchUpPackageShare changed" ); } @@ -1040,9 +1043,18 @@ mod crypto_hash_stability { test_crypto_hash_of(0x42), Payload::new( crypto_hash, - BlockPayload::Data(DataPayload { - batch: BatchPayload::default(), - dkg: DkgDataPayload::new_empty(Height::from(0)), + BlockPayload::Summary(SummaryPayload { + dkg: DkgSummary::new( + /*configs=*/ Vec::default(), + /*current_transcripts=*/ BTreeMap::default(), + /*next_transcripts=*/ BTreeMap::default(), + /*registry_version=*/RegistryVersion::from(1), + /*interval_length=*/ Height::new(59), + /*next_interval_length=*/ Height::new(59), + /*height=*/ Height::new(0), + /*remote_dkg_attempts=*/ BTreeMap::default(), + /*subnet_splitting_status=*/ SubnetSplittingStatus::default(), + ), idkg: None, }), ), @@ -1072,7 +1084,7 @@ mod crypto_hash_stability { let hash = crypto_hash(&data); assert_eq!( hex::encode(hash.get_ref().0.as_slice()), - "9bb9a7c7dacd7513fc58d13b238740e2f8e282c3d6cb66bd3aef520904583ae9", + "7d7d85b7e8a25a005c6cfe9dd5ca8d2c9eb94193adf20cd46fe028f5696a0fde", "Hash of BlockProposal changed" ); } From e49ead3fb5379624eb2e9419cc33241b4942b737 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Thu, 24 Sep 2026 15:35:22 +0000 Subject: [PATCH 18/21] Automatically fixing code for linting and formatting issues --- rs/types/types/src/crypto/hash/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 19d0d6508ac5..0732991ba4a7 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -1048,7 +1048,7 @@ mod crypto_hash_stability { /*configs=*/ Vec::default(), /*current_transcripts=*/ BTreeMap::default(), /*next_transcripts=*/ BTreeMap::default(), - /*registry_version=*/RegistryVersion::from(1), + /*registry_version=*/ RegistryVersion::from(1), /*interval_length=*/ Height::new(59), /*next_interval_length=*/ Height::new(59), /*height=*/ Height::new(0), From 5324358a5c786943811a5a04ba18326e2be3a61b Mon Sep 17 00:00:00 2001 From: David Frank Date: Thu, 24 Sep 2026 19:16:52 +0200 Subject: [PATCH 19/21] Missing deps --- rs/consensus/BUILD.bazel | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rs/consensus/BUILD.bazel b/rs/consensus/BUILD.bazel index 1eb549e46aab..ad57a67f55b4 100644 --- a/rs/consensus/BUILD.bazel +++ b/rs/consensus/BUILD.bazel @@ -66,6 +66,7 @@ rust_library( "//rs/consensus/cup_utils", "//rs/consensus/dkg", "//rs/consensus/idkg:malicious_idkg", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/prng", "//rs/crypto/test_utils/canister_threshold_sigs", @@ -191,6 +192,7 @@ rust_test( "//rs/consensus/chain_key", "//rs/consensus/dkg", "//rs/consensus/idkg:malicious_idkg", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/prng", "//rs/crypto/temp_crypto", @@ -264,6 +266,7 @@ rust_test( "//rs/consensus/chain_key", "//rs/consensus/dkg", "//rs/consensus/idkg:malicious_idkg", + "//rs/consensus/upgrade", "//rs/consensus/utils", "//rs/crypto/prng", "//rs/crypto/temp_crypto", From 4d1dbeb5ae108c807c38d679af78bea4d83a537c Mon Sep 17 00:00:00 2001 From: David Frank Date: Fri, 25 Sep 2026 11:36:43 +0200 Subject: [PATCH 20/21] Address review comments --- rs/consensus/src/consensus/payload_builder.rs | 46 ++++++++----------- rs/consensus/upgrade/src/payload_builder.rs | 22 ++++----- rs/interfaces/src/upgrade.rs | 3 +- rs/types/types/src/crypto/hash/tests.rs | 12 ++--- 4 files changed, 37 insertions(+), 46 deletions(-) diff --git a/rs/consensus/src/consensus/payload_builder.rs b/rs/consensus/src/consensus/payload_builder.rs index 7305dd55fd67..86dc130544ae 100644 --- a/rs/consensus/src/consensus/payload_builder.rs +++ b/rs/consensus/src/consensus/payload_builder.rs @@ -55,13 +55,13 @@ impl PayloadBuilderImpl { logger: ReplicaLogger, ) -> Self { let section_builder = vec![ - BatchPayloadSectionBuilder::Upgrade(upgrade_payload_builder), BatchPayloadSectionBuilder::Ingress(ingress_selector), BatchPayloadSectionBuilder::SelfValidating(self_validating_payload_builder), BatchPayloadSectionBuilder::XNet(xnet_payload_builder), BatchPayloadSectionBuilder::CanisterHttp(canister_http_payload_builder), BatchPayloadSectionBuilder::QueryStats(query_stats_payload_builder), BatchPayloadSectionBuilder::ChainKey(chain_key_payload_builder), + BatchPayloadSectionBuilder::Upgrade(upgrade_payload_builder), ]; Self { @@ -442,45 +442,39 @@ pub(crate) mod test { const CHAIN_KEY_PAYLOAD_SIZE: NumBytes = NumBytes::new(512 * KB); const QUERY_STATS_PAYLOAD_SIZE: NumBytes = NumBytes::new(MB); const INGRESS_PAYLOAD_SIZE: NumBytes = NumBytes::new(2 * MB); + const UPGRADE_PAYLOAD_SIZE: NumBytes = NumBytes::new(0); + + // The expected budgets follow the height-1 build order. Each + // section gets what remains after the earlier ones produced their + // payloads. + let upgrade_budget = MAX_BLOCK_SIZE; + let ingress_budget = upgrade_budget - UPGRADE_PAYLOAD_SIZE; + let bitcoin_budget = ingress_budget - INGRESS_PAYLOAD_SIZE; + let xnet_budget = bitcoin_budget - BITCOIN_PAYLOAD_SIZE; + let http_budget = xnet_budget - XNET_PAYLOAD_SIZE; + let query_stats_budget = http_budget - CANISTER_HTTP_PAYLOAD_SIZE; + let chain_key_budget = query_stats_budget - QUERY_STATS_PAYLOAD_SIZE; let payload_builder = set_up_payload_builder( registry, MocksSettings { chain_key_payload_to_return: vec![0; CHAIN_KEY_PAYLOAD_SIZE.get() as usize], upgrade_payload_to_return: vec![], - expected_chain_key_payload_size_limit: MAX_BLOCK_SIZE, - expected_upgrade_payload_size_limit: MAX_BLOCK_SIZE - CHAIN_KEY_PAYLOAD_SIZE, + expected_chain_key_payload_size_limit: chain_key_budget, + expected_upgrade_payload_size_limit: upgrade_budget, ingress_payload_size_to_return: INGRESS_PAYLOAD_SIZE, - expected_ingress_payload_size_limit: MAX_BLOCK_SIZE - CHAIN_KEY_PAYLOAD_SIZE, + expected_ingress_payload_size_limit: ingress_budget, bitcoin_payload_size_to_return: BITCOIN_PAYLOAD_SIZE, - expected_bitcoin_payload_size_limit: MAX_BLOCK_SIZE - - CHAIN_KEY_PAYLOAD_SIZE - - INGRESS_PAYLOAD_SIZE, + expected_bitcoin_payload_size_limit: bitcoin_budget, xnet_payload_size_to_return: XNET_PAYLOAD_SIZE, - expected_xnet_payload_size_limit: NumBytes::new( - 95 * (MAX_BLOCK_SIZE - - CHAIN_KEY_PAYLOAD_SIZE - - INGRESS_PAYLOAD_SIZE - - BITCOIN_PAYLOAD_SIZE) - .get() - / 100, - ), + expected_xnet_payload_size_limit: NumBytes::new(95 * xnet_budget.get() / 100), http_outcalls_payload_to_return: vec![ 0; CANISTER_HTTP_PAYLOAD_SIZE.get() as usize ], - expected_http_outcalls_size_limit: MAX_BLOCK_SIZE - - CHAIN_KEY_PAYLOAD_SIZE - - INGRESS_PAYLOAD_SIZE - - BITCOIN_PAYLOAD_SIZE - - XNET_PAYLOAD_SIZE, + expected_http_outcalls_size_limit: http_budget, query_stats_payload_to_return: vec![0; QUERY_STATS_PAYLOAD_SIZE.get() as usize], - expected_query_stats_size_limit: MAX_BLOCK_SIZE - - CHAIN_KEY_PAYLOAD_SIZE - - INGRESS_PAYLOAD_SIZE - - BITCOIN_PAYLOAD_SIZE - - XNET_PAYLOAD_SIZE - - CANISTER_HTTP_PAYLOAD_SIZE, + expected_query_stats_size_limit: query_stats_budget, }, ); diff --git a/rs/consensus/upgrade/src/payload_builder.rs b/rs/consensus/upgrade/src/payload_builder.rs index 7ba5fb2785bb..53216c918061 100644 --- a/rs/consensus/upgrade/src/payload_builder.rs +++ b/rs/consensus/upgrade/src/payload_builder.rs @@ -2,7 +2,7 @@ use ic_interfaces::batch_payload::{BatchPayloadBuilder, PastPayload, ProposalCon use ic_interfaces::consensus::{InvalidPayloadReason, PayloadValidationError}; use ic_interfaces::upgrade::InvalidUpgradePayloadReason; use ic_interfaces::validation::{ValidationError, ValidationResult}; -use ic_types::batch::{UpgradePayload, ValidationContext}; +use ic_types::batch::ValidationContext; use ic_types::{Height, NumBytes}; pub struct UpgradePayloadBuilderImpl; @@ -27,13 +27,13 @@ impl BatchPayloadBuilder for UpgradePayloadBuilderImpl { _past_payloads: &[PastPayload], ) -> ValidationResult { // TODO: implement proper validation - UpgradePayload::deserialize(payload) - .map(|_| ()) - .map_err(|e| { - ValidationError::InvalidArtifact(InvalidPayloadReason::InvalidUpgradePayload( - InvalidUpgradePayloadReason::DecodeFailed(format!("{e:?}")), - )) - }) + if payload.is_empty() { + Ok(()) + } else { + Err(ValidationError::InvalidArtifact( + InvalidPayloadReason::InvalidUpgradePayload(InvalidUpgradePayloadReason::NonEmpty), + )) + } } } @@ -63,7 +63,7 @@ mod tests { } #[test] - fn test_validate_payload_rejects_undecodable_bytes() { + fn test_validate_payload_rejects_non_empty_payload() { let context = validation_context(); let proposal_context = ProposalContext { proposer: node_test_id(1), @@ -77,9 +77,7 @@ mod tests { &[] ), Err(ValidationError::InvalidArtifact( - InvalidPayloadReason::InvalidUpgradePayload( - InvalidUpgradePayloadReason::DecodeFailed(_) - ) + InvalidPayloadReason::InvalidUpgradePayload(InvalidUpgradePayloadReason::NonEmpty) )) )); } diff --git a/rs/interfaces/src/upgrade.rs b/rs/interfaces/src/upgrade.rs index d8a101014afe..748acd4db90d 100644 --- a/rs/interfaces/src/upgrade.rs +++ b/rs/interfaces/src/upgrade.rs @@ -1,6 +1,5 @@ /// The reason why an upgrade payload was determined to be invalid. #[derive(Debug, Eq, PartialEq)] pub enum InvalidUpgradePayloadReason { - /// Failed to decode the upgrade payload from protobuf. - DecodeFailed(String), + NonEmpty, } diff --git a/rs/types/types/src/crypto/hash/tests.rs b/rs/types/types/src/crypto/hash/tests.rs index 0732991ba4a7..dc1a377a548d 100644 --- a/rs/types/types/src/crypto/hash/tests.rs +++ b/rs/types/types/src/crypto/hash/tests.rs @@ -584,7 +584,7 @@ mod crypto_hash_stability { /// Test stability of CatchUpContent hash output #[test] fn catch_up_content_stability() { - let block = test_block(); + let block = test_summary_block(); let hashed_block: HashedBlock = Hashed::new(crypto_hash, block); let beacon = test_random_beacon(); let hashed_beacon: HashedRandomBeacon = Hashed::new(crypto_hash, beacon); @@ -601,7 +601,7 @@ mod crypto_hash_stability { /// Test stability of CatchUpShareContent hash output #[test] fn catch_up_share_content_stability() { - let block = test_block(); + let block = test_summary_block(); let hashed_block: HashedBlock = Hashed::new(crypto_hash, block); let beacon = test_random_beacon(); let hashed_beacon: HashedRandomBeacon = Hashed::new(crypto_hash, beacon); @@ -643,7 +643,7 @@ mod crypto_hash_stability { /// Test stability of CatchUpPackage hash output #[test] fn catch_up_package_stability() { - let block = test_block(); + let block = test_summary_block(); let hashed_block: HashedBlock = Hashed::new(crypto_hash, block); let beacon = test_random_beacon(); let hashed_beacon: HashedRandomBeacon = Hashed::new(crypto_hash, beacon); @@ -667,7 +667,7 @@ mod crypto_hash_stability { /// Test stability of CatchUpPackageShare hash output #[test] fn catch_up_package_share_stability() { - let block = test_block(); + let block = test_summary_block(); let hashed_block: HashedBlock = Hashed::new(crypto_hash, block); let beacon = test_random_beacon(); let hashed_beacon: HashedRandomBeacon = Hashed::new(crypto_hash, beacon); @@ -1038,7 +1038,7 @@ mod crypto_hash_stability { } /// Helper to create a test block for use in other tests - fn test_block() -> Block { + fn test_summary_block() -> Block { Block::new( test_crypto_hash_of(0x42), Payload::new( @@ -1072,7 +1072,7 @@ mod crypto_hash_stability { /// Test stability of BlockProposal hash output #[test] fn block_proposal_stability() { - let block = test_block(); + let block = test_summary_block(); let hashed_block: HashedBlock = Hashed::new(crypto_hash, block); let data: BlockProposal = Signed { content: hashed_block, From b2425342436e252b1d52c06c5e9cdc3e3dc6a4d0 Mon Sep 17 00:00:00 2001 From: David Frank Date: Fri, 25 Sep 2026 15:11:40 +0200 Subject: [PATCH 21/21] Address review comments --- rs/consensus/src/consensus/payload_builder.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/consensus/src/consensus/payload_builder.rs b/rs/consensus/src/consensus/payload_builder.rs index 86dc130544ae..7d6471cfca62 100644 --- a/rs/consensus/src/consensus/payload_builder.rs +++ b/rs/consensus/src/consensus/payload_builder.rs @@ -442,7 +442,7 @@ pub(crate) mod test { const CHAIN_KEY_PAYLOAD_SIZE: NumBytes = NumBytes::new(512 * KB); const QUERY_STATS_PAYLOAD_SIZE: NumBytes = NumBytes::new(MB); const INGRESS_PAYLOAD_SIZE: NumBytes = NumBytes::new(2 * MB); - const UPGRADE_PAYLOAD_SIZE: NumBytes = NumBytes::new(0); + const UPGRADE_PAYLOAD_SIZE: NumBytes = NumBytes::new(32 * KB); // The expected budgets follow the height-1 build order. Each // section gets what remains after the earlier ones produced their @@ -458,9 +458,7 @@ pub(crate) mod test { let payload_builder = set_up_payload_builder( registry, MocksSettings { - chain_key_payload_to_return: vec![0; CHAIN_KEY_PAYLOAD_SIZE.get() as usize], - upgrade_payload_to_return: vec![], - expected_chain_key_payload_size_limit: chain_key_budget, + upgrade_payload_to_return: vec![0; UPGRADE_PAYLOAD_SIZE.get() as usize], expected_upgrade_payload_size_limit: upgrade_budget, ingress_payload_size_to_return: INGRESS_PAYLOAD_SIZE, expected_ingress_payload_size_limit: ingress_budget, @@ -475,6 +473,8 @@ pub(crate) mod test { expected_http_outcalls_size_limit: http_budget, query_stats_payload_to_return: vec![0; QUERY_STATS_PAYLOAD_SIZE.get() as usize], expected_query_stats_size_limit: query_stats_budget, + chain_key_payload_to_return: vec![0; CHAIN_KEY_PAYLOAD_SIZE.get() as usize], + expected_chain_key_payload_size_limit: chain_key_budget, }, );