From 728d17b382a01019a3a8d0a9f5c6012daf06e29f Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Fri, 10 Nov 2023 13:51:32 +1100 Subject: [PATCH 1/3] feat: max authority count contraction --- .../cf-integration-tests/src/mock_runtime.rs | 6 +++- state-chain/node/src/chain_spec.rs | 9 +++++- state-chain/pallets/cf-validator/src/lib.rs | 28 ++++++++++++++++++- state-chain/pallets/cf-validator/src/mock.rs | 1 + state-chain/pallets/cf-validator/src/tests.rs | 23 +++++++++++++++ state-chain/primitives/src/lib.rs | 6 +++- 6 files changed, 69 insertions(+), 4 deletions(-) diff --git a/state-chain/cf-integration-tests/src/mock_runtime.rs b/state-chain/cf-integration-tests/src/mock_runtime.rs index 866cae89b2..bda65616aa 100644 --- a/state-chain/cf-integration-tests/src/mock_runtime.rs +++ b/state-chain/cf-integration-tests/src/mock_runtime.rs @@ -42,7 +42,10 @@ use crate::{ threshold_signing::{EthKeyComponents, KeyUtils}, GENESIS_KEY_SEED, }; -use cf_primitives::{AccountRole, AuthorityCount, BlockNumber, FlipBalance, GENESIS_EPOCH}; +use cf_primitives::{ + AccountRole, AuthorityCount, BlockNumber, FlipBalance, DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, + GENESIS_EPOCH, +}; pub struct ExtBuilder { pub genesis_accounts: Vec<(AccountId, AccountRole, FlipBalance)>, @@ -163,6 +166,7 @@ impl ExtBuilder { min_size: self.min_authorities, max_size: self.max_authorities, max_expansion: self.max_authorities, + max_authority_set_contraction_percentage: DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, }, ethereum_vault: EthereumVaultConfig { vault_key: Some(ethereum_vault_key), diff --git a/state-chain/node/src/chain_spec.rs b/state-chain/node/src/chain_spec.rs index dc25667192..0011770552 100644 --- a/state-chain/node/src/chain_spec.rs +++ b/state-chain/node/src/chain_spec.rs @@ -2,7 +2,10 @@ use cf_chains::{ dot::{PolkadotAccountId, PolkadotHash}, ChainState, }; -use cf_primitives::{chains::assets, AccountRole, AssetAmount, AuthorityCount, NetworkEnvironment}; +use cf_primitives::{ + chains::assets, AccountRole, AssetAmount, AuthorityCount, NetworkEnvironment, + DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, +}; use cf_chains::{ btc::{BitcoinFeeInfo, BitcoinTrackedData}, @@ -231,6 +234,7 @@ pub fn inner_cf_development_config( testnet::SNOW_WHITE_SR25519.into(), 1, devnet::MAX_AUTHORITIES, + DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, EnvironmentConfig { flip_token_address: flip_token_address.into(), eth_usdc_address: eth_usdc_address.into(), @@ -358,6 +362,7 @@ macro_rules! network_spec { SNOW_WHITE_SR25519.into(), MIN_AUTHORITIES, MAX_AUTHORITIES, + DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, EnvironmentConfig { flip_token_address: flip_token_address.into(), eth_usdc_address: eth_usdc_address.into(), @@ -428,6 +433,7 @@ fn testnet_genesis( root_key: AccountId, min_authorities: AuthorityCount, max_authorities: AuthorityCount, + max_authority_set_contraction_percentage: Percent, config_set: EnvironmentConfig, eth_init_agg_key: [u8; 33], ethereum_deployment_block: u64, @@ -556,6 +562,7 @@ fn testnet_genesis( min_size: min_authorities, max_size: max_authorities, max_expansion: max_authorities, + max_authority_set_contraction_percentage, }, session: SessionConfig { keys: initial_authorities diff --git a/state-chain/pallets/cf-validator/src/lib.rs b/state-chain/pallets/cf-validator/src/lib.rs index 5d60215477..5f67fd7922 100644 --- a/state-chain/pallets/cf-validator/src/lib.rs +++ b/state-chain/pallets/cf-validator/src/lib.rs @@ -17,7 +17,10 @@ mod migrations; mod rotation_state; pub use auction_resolver::*; -use cf_primitives::{AuthorityCount, EpochIndex, NodeCFEVersions, SemVer, FLIPPERINOS_PER_FLIP}; +use cf_primitives::{ + AuthorityCount, EpochIndex, NodeCFEVersions, SemVer, DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, + FLIPPERINOS_PER_FLIP, +}; use cf_traits::{ impl_pallet_safe_mode, offence_reporting::OffenceReporter, AsyncResult, AuthoritiesCfeVersions, @@ -62,6 +65,7 @@ pub enum PalletConfigUpdate { AuthoritySetMinSize { min_size: AuthorityCount }, AuctionParameters { parameters: SetSizeParameters }, MinimumReportedCfeVersion { version: SemVer }, + MaxAuthoritySetContractionPercentage { percentage: Percent }, } type RuntimeRotationState = @@ -286,6 +290,13 @@ pub mod pallet { #[pallet::getter(fn minimum_reported_cfe_version)] pub(super) type MinimumReportedCfeVersion = StorageValue<_, SemVer, ValueQuery>; + /// Determines the maximum allowed reduction of authority set size in percents between two + /// consecutive epochs. + #[pallet::storage] + #[pallet::getter(fn max_authority_set_contraction_percentage)] + pub(super) type MaxAuthoritySetContractionPercentage = + StorageValue<_, Percent, ValueQuery>; + #[pallet::event] #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { @@ -533,6 +544,9 @@ pub mod pallet { PalletConfigUpdate::MinimumReportedCfeVersion { version } => { MinimumReportedCfeVersion::::put(version); }, + PalletConfigUpdate::MaxAuthoritySetContractionPercentage { percentage } => { + MaxAuthoritySetContractionPercentage::::put(percentage); + }, } Self::deposit_event(Event::PalletConfigUpdated { update }); @@ -780,6 +794,7 @@ pub mod pallet { pub min_size: AuthorityCount, pub max_size: AuthorityCount, pub max_expansion: AuthorityCount, + pub max_authority_set_contraction_percentage: Percent, } impl Default for GenesisConfig { @@ -796,6 +811,7 @@ pub mod pallet { min_size: 3, max_size: 15, max_expansion: 5, + max_authority_set_contraction_percentage: DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, } } } @@ -811,6 +827,9 @@ pub mod pallet { BackupRewardNodePercentage::::set(self.backup_reward_node_percentage); AuthoritySetMinSize::::set(self.authority_set_min_size); VanityNames::::put(&self.genesis_vanity_names); + MaxAuthoritySetContractionPercentage::::set( + self.max_authority_set_contraction_percentage, + ); CurrentEpoch::::set(GENESIS_EPOCH); @@ -1091,6 +1110,13 @@ impl Pallet { fn try_start_keygen(rotation_state: RuntimeRotationState) { let candidates = rotation_state.authority_candidates(); let SetSizeParameters { min_size, .. } = AuctionParameters::::get(); + + let min_size = sp_std::cmp::max( + min_size, + (Percent::one().saturating_sub(MaxAuthoritySetContractionPercentage::::get())) * + Self::current_authority_count(), + ); + if (candidates.len() as u32) < min_size { log::warn!( target: "cf-validator", diff --git a/state-chain/pallets/cf-validator/src/mock.rs b/state-chain/pallets/cf-validator/src/mock.rs index cb3fa8fff7..93202fc071 100644 --- a/state-chain/pallets/cf-validator/src/mock.rs +++ b/state-chain/pallets/cf-validator/src/mock.rs @@ -216,6 +216,7 @@ cf_test_utilities::impl_test_helpers! { min_size: MIN_AUTHORITY_SIZE, max_size: MAX_AUTHORITY_SIZE, max_expansion: MAX_AUTHORITY_SET_EXPANSION, + max_authority_set_contraction_percentage: DEFAULT_MAX_AUTHORITY_SET_CONTRACTION, }, }, ||{ diff --git a/state-chain/pallets/cf-validator/src/tests.rs b/state-chain/pallets/cf-validator/src/tests.rs index e08e0d2bc4..36127451c9 100644 --- a/state-chain/pallets/cf-validator/src/tests.rs +++ b/state-chain/pallets/cf-validator/src/tests.rs @@ -788,6 +788,29 @@ mod keygen { assert_rotation_aborted(); }); } + + #[test] + fn rotation_aborts_if_candidates_below_min_percentage() { + new_test_ext().execute_with(|| { + // Ban half of the candidates: + let failing_count = CANDIDATES.count() / 2; + let remaining_count = CANDIDATES.count() - failing_count; + + // We still have enough candidates according to auction resolver parameters: + assert!(remaining_count > MIN_AUTHORITY_SIZE as usize); + + // But the rotation should be aborted since authority count would drop too much + // compared to the previous set: + assert!( + remaining_count < + (Percent::one() - DEFAULT_MAX_AUTHORITY_SET_CONTRACTION) * + AUTHORITIES.count() + ); + + failed_keygen_with_offenders(CANDIDATES.take(failing_count)); + assert_rotation_aborted(); + }); + } } #[cfg(test)] diff --git a/state-chain/primitives/src/lib.rs b/state-chain/primitives/src/lib.rs index 75da9448bb..4077aa8c6e 100644 --- a/state-chain/primitives/src/lib.rs +++ b/state-chain/primitives/src/lib.rs @@ -6,7 +6,7 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::sp_runtime::{ traits::{IdentifyAccount, Verify}, - MultiSignature, RuntimeDebug, + MultiSignature, Percent, RuntimeDebug, }; use scale_info::TypeInfo; use semver::{Error, Version}; @@ -75,6 +75,10 @@ pub const SECONDS_PER_BLOCK: u64 = MILLISECONDS_PER_BLOCK / 1000; pub const STABLE_ASSET: Asset = Asset::Usdc; +/// Determines the default (genesis) maximum allowed reduction of authority set size in +/// between two consecutive epochs. +pub const DEFAULT_MAX_AUTHORITY_SET_CONTRACTION: Percent = Percent::from_percent(30); + // Polkadot extrinsics are uniquely identified by - // https://wiki.polkadot.network/docs/build-protocol-info #[derive(Clone, Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Eq)] From 8a334ad8cce35bc5602f11f208241bc11dc1b7e1 Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Fri, 10 Nov 2023 16:43:52 +1100 Subject: [PATCH 2/3] test: fix apy test --- state-chain/cf-integration-tests/src/funding.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/state-chain/cf-integration-tests/src/funding.rs b/state-chain/cf-integration-tests/src/funding.rs index 80a35139c2..ae998ba7c2 100644 --- a/state-chain/cf-integration-tests/src/funding.rs +++ b/state-chain/cf-integration-tests/src/funding.rs @@ -243,8 +243,8 @@ fn can_calculate_account_apy() { #[test] fn apy_can_be_above_100_percent() { const EPOCH_BLOCKS: u32 = 1_000; - const MAX_AUTHORITIES: u32 = 1; - const NUM_BACKUPS: u32 = 1; + const MAX_AUTHORITIES: u32 = 2; + const NUM_BACKUPS: u32 = 2; super::genesis::default() .blocks_per_epoch(EPOCH_BLOCKS) .max_authorities(MAX_AUTHORITIES) @@ -266,10 +266,11 @@ fn apy_can_be_above_100_percent() { // APY rate of > 100% can be calculated correctly. let total = Flip::balance(&validator); - let reward = Emissions::current_authority_emission_per_block() * YEAR as u128; + let reward = Emissions::current_authority_emission_per_block() * YEAR as u128 / + MAX_AUTHORITIES as u128; let apy_basis_point = FixedU64::from_rational(reward, total).checked_mul_int(10_000u32).unwrap(); - assert_eq!(apy_basis_point, 242_543_802u32); + assert_eq!(apy_basis_point, 241_377_726u32); assert_eq!(calculate_account_apy(&validator), Some(apy_basis_point)); }); } From 3e6920ab6c6394009353e17f09af5b8db0f8e5ce Mon Sep 17 00:00:00 2001 From: Maxim Shishmarev Date: Fri, 10 Nov 2023 18:28:17 +1100 Subject: [PATCH 3/3] test: fix keygen restart test --- state-chain/pallets/cf-validator/src/tests.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/state-chain/pallets/cf-validator/src/tests.rs b/state-chain/pallets/cf-validator/src/tests.rs index 36127451c9..711a97c976 100644 --- a/state-chain/pallets/cf-validator/src/tests.rs +++ b/state-chain/pallets/cf-validator/src/tests.rs @@ -737,7 +737,12 @@ const AUTHORITIES: Range = 0..10; lazy_static::lazy_static! { /// How many candidates can fail without preventing us from re-trying keygen - static ref MAX_ALLOWED_KEYGEN_OFFENDERS: usize = CANDIDATES.count().checked_sub(MIN_AUTHORITY_SIZE as usize).unwrap(); + static ref MAX_ALLOWED_KEYGEN_OFFENDERS: usize = { + + let min_size = std::cmp::max(MIN_AUTHORITY_SIZE, (Percent::one() - DEFAULT_MAX_AUTHORITY_SET_CONTRACTION) * AUTHORITIES.count() as u32); + + CANDIDATES.count().checked_sub(min_size as usize).unwrap() + }; /// How many current authorities can fail to leave enough healthy ones to handover the key static ref MAX_ALLOWED_SHARING_OFFENDERS: usize = {