From e870d34f8ca201959f430a5a2a705dc5de6ddf11 Mon Sep 17 00:00:00 2001 From: akildemir Date: Fri, 20 Sep 2024 14:45:50 +0300 Subject: [PATCH 1/2] add validator sets pallet tests --- Cargo.lock | 7 + substrate/validator-sets/pallet/Cargo.toml | 23 +- substrate/validator-sets/pallet/src/lib.rs | 19 +- substrate/validator-sets/pallet/src/mock.rs | 210 +++++++ substrate/validator-sets/pallet/src/tests.rs | 573 +++++++++++++++++++ 5 files changed, 828 insertions(+), 4 deletions(-) create mode 100644 substrate/validator-sets/pallet/src/mock.rs create mode 100644 substrate/validator-sets/pallet/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index a950b0c19..59b41c796 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8564,24 +8564,31 @@ dependencies = [ name = "serai-validator-sets-pallet" version = "0.1.0" dependencies = [ + "ciphersuite", "frame-support", "frame-system", + "frost-schnorrkel", "hashbrown 0.14.5", + "modular-frost", "pallet-babe", "pallet-grandpa", + "pallet-timestamp", "parity-scale-codec", + "rand_core", "scale-info", "serai-coins-pallet", "serai-dex-pallet", "serai-primitives", "serai-validator-sets-primitives", "sp-application-crypto", + "sp-consensus-babe", "sp-core", "sp-io", "sp-runtime", "sp-session", "sp-staking", "sp-std", + "zeroize", ] [[package]] diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index dd67d1bc3..528d3a656 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -44,6 +44,18 @@ validator-sets-primitives = { package = "serai-validator-sets-primitives", path coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } +[dev-dependencies] +pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false } + +sp-consensus-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } + +ciphersuite = { path = "../../../crypto/ciphersuite", features = ["ristretto"] } +frost = { package = "modular-frost", path = "../../../crypto/frost", features = ["tests"] } +schnorrkel = { path = "../../../crypto/schnorrkel", package = "frost-schnorrkel" } + +zeroize = "^1.5" +rand_core = "0.6" + [features] std = [ "scale/std", @@ -56,12 +68,15 @@ std = [ "sp-runtime/std", "sp-session/std", "sp-staking/std", + + "sp-consensus-babe/std", "frame-system/std", "frame-support/std", "pallet-babe/std", "pallet-grandpa/std", + "pallet-timestamp/std", "serai-primitives/std", "validator-sets-primitives/std", @@ -70,8 +85,12 @@ std = [ "dex-pallet/std", ] -# TODO -try-runtime = [] +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime", + + "sp-runtime/try-runtime", +] runtime-benchmarks = [ "frame-system/runtime-benchmarks", diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index c2ba80a96..8aa049825 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -1,5 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + use core::marker::PhantomData; use scale::{Encode, Decode}; @@ -303,6 +309,7 @@ pub mod pallet { /// Pending deallocations, keyed by the Session they become unlocked on. #[pallet::storage] + #[pallet::getter(fn pending_deallocations)] type PendingDeallocations = StorageDoubleMap< _, Blake2_128Concat, @@ -391,6 +398,7 @@ pub mod pallet { let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0; let mut participants = vec![]; + let mut total_allocated_stake = 0; { let mut iter = SortedAllocationsIter::::new(network); let mut key_shares = 0; @@ -401,6 +409,7 @@ pub mod pallet { (amount.0 / allocation_per_key_share).min(u64::from(MAX_KEY_SHARES_PER_SET)); participants.push((key, these_key_shares)); + total_allocated_stake += amount.0; key_shares += these_key_shares; } amortize_excess_key_shares(&mut participants); @@ -413,6 +422,12 @@ pub mod pallet { let set = ValidatorSet { network, session }; Pallet::::deposit_event(Event::NewSet { set }); + // other networks set their Session(0) TAS once they set their keys but serai network + // doesn't have that so we set it here. + if network == NetworkId::Serai && session == Session(0) { + TotalAllocatedStake::::set(network, Some(Amount(total_allocated_stake))); + } + Participants::::set(network, Some(participants.try_into().unwrap())); SessionBeginBlock::::set( network, @@ -618,7 +633,7 @@ pub mod pallet { // If we're not removing the entire allocation, yet the allocation is no longer at or above // the threshold for a key share, error let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0; - if (new_allocation != 0) && (new_allocation < allocation_per_key_share) { + if (new_allocation > 0) && (new_allocation < allocation_per_key_share) { Err(Error::::DeallocationWouldRemoveParticipant)?; } @@ -772,7 +787,7 @@ pub mod pallet { PendingDeallocations::::take((network, key), session) } - fn rotate_session() { + pub(crate) fn rotate_session() { // next serai validators that is in the queue. let now_validators = Participants::::get(NetworkId::Serai) .expect("no Serai participants upon rotate_session"); diff --git a/substrate/validator-sets/pallet/src/mock.rs b/substrate/validator-sets/pallet/src/mock.rs new file mode 100644 index 000000000..acc513b2e --- /dev/null +++ b/substrate/validator-sets/pallet/src/mock.rs @@ -0,0 +1,210 @@ +//! Test environment for ValidatorSets pallet. + +use super::*; + +use std::collections::HashMap; + +use frame_support::{ + construct_runtime, + traits::{ConstU16, ConstU32, ConstU64}, +}; + +use sp_core::{ + H256, Pair as PairTrait, + sr25519::{Public, Pair}, +}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +use serai_primitives::*; +use validator_sets::{primitives::MAX_KEY_SHARES_PER_SET, MembershipProof}; + +pub use crate as validator_sets; +pub use coins_pallet as coins; +pub use dex_pallet as dex; +pub use pallet_babe as babe; +pub use pallet_grandpa as grandpa; +pub use pallet_timestamp as timestamp; + +type Block = frame_system::mocking::MockBlock; +// Maximum number of authorities per session. +pub type MaxAuthorities = ConstU32<{ MAX_KEY_SHARES_PER_SET }>; + +pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4); +pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration = + sp_consensus_babe::BabeEpochConfiguration { + c: PRIMARY_PROBABILITY, + allowed_slots: sp_consensus_babe::AllowedSlots::PrimaryAndSecondaryPlainSlots, + }; + +pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Timestamp: timestamp, + Coins: coins, + LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, + ValidatorSets: validator_sets, + Dex: dex, + Babe: babe, + Grandpa: grandpa, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = Public; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = Babe; + type MinimumPeriod = ConstU64<{ (TARGET_BLOCK_TIME * 1000) / 2 }>; + type WeightInfo = (); +} + +impl babe::Config for Test { + type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>; + + type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>; + type EpochChangeTrigger = babe::ExternalTrigger; + type DisabledValidators = ValidatorSets; + + type WeightInfo = (); + type MaxAuthorities = MaxAuthorities; + + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = (); +} + +impl grandpa::Config for Test { + type RuntimeEvent = RuntimeEvent; + + type WeightInfo = (); + type MaxAuthorities = MaxAuthorities; + + type MaxSetIdSessionEntries = ConstU64<0>; + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = (); +} + +impl coins::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AllowMint = ValidatorSets; +} + +impl coins::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AllowMint = (); +} + +impl dex::Config for Test { + type RuntimeEvent = RuntimeEvent; + + type LPFee = ConstU32<3>; // 0.3% + type MintMinLiquidity = ConstU64<10000>; + + type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2 + + type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; + + type WeightInfo = dex::weights::SubstrateWeight; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = Babe; +} + +// For a const we can't define +pub fn genesis_participants() -> Vec { + vec![ + insecure_pair_from_name("Alice"), + insecure_pair_from_name("Bob"), + insecure_pair_from_name("Charlie"), + insecure_pair_from_name("Dave"), + ] +} + +// Amounts for single key share per network +pub fn key_shares() -> HashMap { + HashMap::from([ + (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), + (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))), + (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))), + (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))), + ]) +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let networks: Vec<(NetworkId, Amount)> = key_shares().into_iter().collect::>(); + + coins::GenesisConfig:: { + accounts: genesis_participants() + .clone() + .into_iter() + .map(|a| (a.public(), Balance { coin: Coin::Serai, amount: Amount(1 << 60) })) + .collect(), + _ignore: Default::default(), + } + .assimilate_storage(&mut t) + .unwrap(); + + validator_sets::GenesisConfig:: { + networks, + participants: genesis_participants().into_iter().map(|p| p.public()).collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + + babe::GenesisConfig:: { + authorities: genesis_participants() + .into_iter() + .map(|validator| (validator.public().into(), 1)) + .collect(), + epoch_config: Some(BABE_GENESIS_EPOCH_CONFIG), + _config: PhantomData, + } + .assimilate_storage(&mut t) + .unwrap(); + + grandpa::GenesisConfig:: { + authorities: genesis_participants() + .into_iter() + .map(|validator| (validator.public().into(), 1)) + .collect(), + _config: PhantomData, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(0)); + ext +} diff --git a/substrate/validator-sets/pallet/src/tests.rs b/substrate/validator-sets/pallet/src/tests.rs new file mode 100644 index 000000000..64fc80690 --- /dev/null +++ b/substrate/validator-sets/pallet/src/tests.rs @@ -0,0 +1,573 @@ +use crate::{mock::*, primitives::*}; + +use std::collections::HashMap; + +use ciphersuite::{Ciphersuite, Ristretto}; +use frost::dkg::musig::musig; +use schnorrkel::Schnorrkel; + +use zeroize::Zeroizing; +use rand_core::OsRng; + +use frame_support::{ + assert_noop, assert_ok, + pallet_prelude::{InvalidTransaction, TransactionSource}, + traits::{OnFinalize, OnInitialize}, +}; +use frame_system::RawOrigin; + +use sp_core::{ + sr25519::{Public, Pair, Signature}, + Pair as PairTrait, +}; +use sp_runtime::{traits::ValidateUnsigned, BoundedVec}; + +use serai_primitives::*; + +fn active_network_validators(network: NetworkId) -> Vec<(Public, u64)> { + if network == NetworkId::Serai { + Babe::authorities().into_iter().map(|(id, key_share)| (id.into_inner(), key_share)).collect() + } else { + ValidatorSets::participants_for_latest_decided_set(network).unwrap().into_inner() + } +} + +fn verify_session_and_active_validators(network: NetworkId, participants: &[Public], session: u32) { + let mut validators: Vec = active_network_validators(network) + .into_iter() + .map(|(p, ks)| { + assert_eq!(ks, 1); + p + }) + .collect(); + validators.sort(); + + assert_eq!(ValidatorSets::session(network).unwrap(), Session(session)); + assert_eq!(participants, validators); + + // TODO: how to make sure block finalizations work as usual here? +} + +fn get_session_at_which_changes_activate(network: NetworkId) -> u32 { + let current_session = ValidatorSets::session(network).unwrap().0; + // changes should be active in the next session + if network == NetworkId::Serai { + // it takes 1 extra session for serai net to make the changes active. + current_session + 2 + } else { + current_session + 1 + } +} + +fn set_keys_for_session(network: NetworkId) { + ValidatorSets::set_keys( + RawOrigin::None.into(), + network, + BoundedVec::new(), + KeyPair(insecure_pair_from_name("Alice").public(), vec![].try_into().unwrap()), + Signature([0u8; 64]), + ) + .unwrap(); +} + +fn set_keys_signature(set: &ValidatorSet, key_pair: &KeyPair, pairs: &[Pair]) -> Signature { + let mut pub_keys = vec![]; + for pair in pairs { + let public_key = + ::read_G::<&[u8]>(&mut pair.public().0.as_ref()).unwrap(); + pub_keys.push(public_key); + } + + let mut threshold_keys = vec![]; + for i in 0 .. pairs.len() { + let secret_key = ::read_F::<&[u8]>( + &mut pairs[i].as_ref().secret.to_bytes()[.. 32].as_ref(), + ) + .unwrap(); + assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]); + + threshold_keys.push( + musig::(&musig_context(*set), &Zeroizing::new(secret_key), &pub_keys).unwrap(), + ); + } + + let mut musig_keys = HashMap::new(); + for tk in threshold_keys { + musig_keys.insert(tk.params().i(), tk.into()); + } + + let sig = frost::tests::sign_without_caching( + &mut OsRng, + frost::tests::algorithm_machines(&mut OsRng, &Schnorrkel::new(b"substrate"), &musig_keys), + &set_keys_message(set, &[], key_pair), + ); + + Signature(sig.to_bytes()) +} + +fn get_ordered_keys(network: NetworkId, participants: &[Pair]) -> Vec { + // retrieve the current session validators so that we know the order of the keys + // that is necessary for the correct musig signature. + let validators = ValidatorSets::participants_for_latest_decided_set(network).unwrap(); + + // collect the pairs of the validators + let mut pairs = vec![]; + for (v, _) in validators { + let p = participants.iter().find(|pair| pair.public() == v).unwrap().clone(); + pairs.push(p); + } + + pairs +} + +fn rotate_session_until(network: NetworkId, session: u32) { + let mut current = ValidatorSets::session(network).unwrap().0; + while current < session { + Babe::on_initialize(System::block_number() + 1); + ValidatorSets::rotate_session(); + set_keys_for_session(network); + ValidatorSets::retire_set(ValidatorSet { session: Session(current), network }); + current += 1; + } + assert_eq!(current, session); +} + +#[test] +fn rotate_session() { + new_test_ext().execute_with(|| { + let genesis_participants: Vec = + genesis_participants().into_iter().map(|p| p.public()).collect(); + let key_shares = key_shares(); + + let mut participants = HashMap::from([ + (NetworkId::Serai, genesis_participants.clone()), + (NetworkId::Bitcoin, genesis_participants.clone()), + (NetworkId::Monero, genesis_participants.clone()), + (NetworkId::Ethereum, genesis_participants), + ]); + + // rotate session + for network in NETWORKS { + let participants = participants.get_mut(&network).unwrap(); + + // verify for session 0 + participants.sort(); + set_keys_for_session(network); + verify_session_and_active_validators(network, participants, 0); + + // add 1 participant + let new_participant = insecure_pair_from_name("new-guy").public(); + Coins::mint(new_participant, Balance { coin: Coin::Serai, amount: key_shares[&network] }) + .unwrap(); + ValidatorSets::allocate( + RawOrigin::Signed(new_participant).into(), + network, + key_shares[&network], + ) + .unwrap(); + participants.push(new_participant); + + // move network to the activation session + let activation_session = get_session_at_which_changes_activate(network); + rotate_session_until(network, activation_session); + + // verify + participants.sort(); + verify_session_and_active_validators(network, participants, activation_session); + + // remove 1 participant + let participant_to_remove = participants[0]; + ValidatorSets::deallocate( + RawOrigin::Signed(participant_to_remove).into(), + network, + key_shares[&network], + ) + .unwrap(); + participants + .swap_remove(participants.iter().position(|k| *k == participant_to_remove).unwrap()); + + // check pending deallocations + let pending = ValidatorSets::pending_deallocations( + (network, participant_to_remove), + Session(if network == NetworkId::Serai { + activation_session + 3 + } else { + activation_session + 2 + }), + ); + assert_eq!(pending, Some(key_shares[&network])); + + // move network to the activation session + let activation_session = get_session_at_which_changes_activate(network); + rotate_session_until(network, activation_session); + + // verify + participants.sort(); + verify_session_and_active_validators(network, participants, activation_session); + } + }) +} + +#[test] +fn allocate() { + new_test_ext().execute_with(|| { + let genesis_participants: Vec = + genesis_participants().into_iter().map(|p| p.public()).collect(); + let key_shares = key_shares(); + let participant = insecure_pair_from_name("random1").public(); + let network = NetworkId::Ethereum; + + // check genesis TAS + set_keys_for_session(network); + assert_eq!( + ValidatorSets::total_allocated_stake(network).unwrap().0, + key_shares[&network].0 * u64::try_from(genesis_participants.len()).unwrap() + ); + + // we can't allocate less than a key share + let amount = Amount(key_shares[&network].0 * 3); + Coins::mint(participant, Balance { coin: Coin::Serai, amount }).unwrap(); + assert_noop!( + ValidatorSets::allocate( + RawOrigin::Signed(participant).into(), + network, + Amount(key_shares[&network].0 - 1) + ), + validator_sets::Error::::InsufficientAllocation + ); + + // we can't allocate too much that the net exhibits the ability to handle any single node + // becoming byzantine + assert_noop!( + ValidatorSets::allocate(RawOrigin::Signed(participant).into(), network, amount), + validator_sets::Error::::AllocationWouldRemoveFaultTolerance + ); + + // we should be allocate a proper amount + assert_ok!(ValidatorSets::allocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + )); + assert_eq!(Coins::balance(participant, Coin::Serai).0, amount.0 - key_shares[&network].0); + + // check new amount is reflected on TAS on new session + rotate_session_until(network, 1); + assert_eq!( + ValidatorSets::total_allocated_stake(network).unwrap().0, + key_shares[&network].0 * (u64::try_from(genesis_participants.len()).unwrap() + 1) + ); + + // check that new participants match + let mut active_participants: Vec = + active_network_validators(network).into_iter().map(|(p, _)| p).collect(); + + let mut current_participants = genesis_participants.clone(); + current_participants.push(participant); + + current_participants.sort(); + active_participants.sort(); + assert_eq!(current_participants, active_participants); + }) +} + +#[test] +fn deallocate_pending() { + new_test_ext().execute_with(|| { + let genesis_participants: Vec = + genesis_participants().into_iter().map(|p| p.public()).collect(); + let key_shares = key_shares(); + let participant = insecure_pair_from_name("random1").public(); + let network = NetworkId::Bitcoin; + + // check genesis TAS + set_keys_for_session(network); + assert_eq!( + ValidatorSets::total_allocated_stake(network).unwrap().0, + key_shares[&network].0 * u64::try_from(genesis_participants.len()).unwrap() + ); + + // allocate some amount + Coins::mint(participant, Balance { coin: Coin::Serai, amount: key_shares[&network] }).unwrap(); + assert_ok!(ValidatorSets::allocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + )); + assert_eq!(Coins::balance(participant, Coin::Serai).0, 0); + + // move to next session + let mut current_session = ValidatorSets::session(network).unwrap().0; + current_session += 1; + rotate_session_until(network, current_session); + assert_eq!( + ValidatorSets::total_allocated_stake(network).unwrap().0, + key_shares[&network].0 * (u64::try_from(genesis_participants.len()).unwrap() + 1) + ); + + // we can deallocate all of our allocation + assert_ok!(ValidatorSets::deallocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + )); + + // check pending deallocations + let pending_session = + if network == NetworkId::Serai { current_session + 3 } else { current_session + 2 }; + assert_eq!( + ValidatorSets::pending_deallocations((network, participant), Session(pending_session)), + Some(key_shares[&network]) + ); + + // we can't claim it immediately + assert_noop!( + ValidatorSets::claim_deallocation( + RawOrigin::Signed(participant).into(), + network, + Session(pending_session), + ), + validator_sets::Error::::NonExistentDeallocation + ); + + // we should be able to claim it in the pending session + rotate_session_until(network, pending_session); + assert_ok!(ValidatorSets::claim_deallocation( + RawOrigin::Signed(participant).into(), + network, + Session(pending_session), + )); + }) +} + +#[test] +fn deallocate_immediately() { + new_test_ext().execute_with(|| { + let genesis_participants: Vec = + genesis_participants().into_iter().map(|p| p.public()).collect(); + let key_shares = key_shares(); + let participant = insecure_pair_from_name("random1").public(); + let network = NetworkId::Monero; + + // check genesis TAS + set_keys_for_session(network); + assert_eq!( + ValidatorSets::total_allocated_stake(network).unwrap().0, + key_shares[&network].0 * u64::try_from(genesis_participants.len()).unwrap() + ); + + // we can't deallocate when we don't have an allocation + assert_noop!( + ValidatorSets::deallocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + ), + validator_sets::Error::::NonExistentValidator + ); + + // allocate some amount + Coins::mint(participant, Balance { coin: Coin::Serai, amount: key_shares[&network] }).unwrap(); + assert_ok!(ValidatorSets::allocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + )); + assert_eq!(Coins::balance(participant, Coin::Serai).0, 0); + + // we can't deallocate more than our allocation + assert_noop!( + ValidatorSets::deallocate( + RawOrigin::Signed(participant).into(), + network, + Amount(key_shares[&network].0 + 1) + ), + validator_sets::Error::::NotEnoughAllocated + ); + + // we can't deallocate an amount that would left us less than a key share as long as it isn't 0 + assert_noop!( + ValidatorSets::deallocate( + RawOrigin::Signed(participant).into(), + network, + Amount(key_shares[&network].0 / 2) + ), + validator_sets::Error::::DeallocationWouldRemoveParticipant + ); + + // we can deallocate all of our allocation + assert_ok!(ValidatorSets::deallocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + )); + + // It should be immediately deallocated since we are not yet in an active set + assert_eq!(Coins::balance(participant, Coin::Serai), key_shares[&network]); + assert!(ValidatorSets::pending_deallocations((network, participant), Session(1)).is_none()); + + // allocate again + assert_ok!(ValidatorSets::allocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + )); + assert_eq!(Coins::balance(participant, Coin::Serai).0, 0); + + // make a pool so that we have security oracle value for the coin + let liq_acc = insecure_pair_from_name("liq-acc").public(); + let coin = Coin::Monero; + let balance = Balance { coin, amount: Amount(2 * key_shares[&network].0) }; + Coins::mint(liq_acc, balance).unwrap(); + Coins::mint(liq_acc, Balance { coin: Coin::Serai, amount: balance.amount }).unwrap(); + Dex::add_liquidity( + RawOrigin::Signed(liq_acc).into(), + coin, + balance.amount.0 / 2, + balance.amount.0 / 2, + 1, + 1, + liq_acc, + ) + .unwrap(); + Dex::on_finalize(1); + assert!(Dex::security_oracle_value(coin).unwrap().0 > 0); + + // we can't deallocate if it would break economic security + // The reason we don't have economic security for the network now is that we just set + // the value for coin/SRI to 1:1 when making the pool and we minted 2 * key_share amount + // of coin but we only allocated 1 key_share of SRI for the network although we need more than + // 3 for the same amount of coin. + assert_noop!( + ValidatorSets::deallocate( + RawOrigin::Signed(participant).into(), + network, + key_shares[&network] + ), + validator_sets::Error::::DeallocationWouldRemoveEconomicSecurity + ); + }) +} + +#[test] +fn set_keys_no_serai_network() { + new_test_ext().execute_with(|| { + let call = validator_sets::Call::::set_keys { + network: NetworkId::Serai, + removed_participants: Vec::new().try_into().unwrap(), + key_pair: KeyPair(insecure_pair_from_name("name").public(), Vec::new().try_into().unwrap()), + signature: Signature([0u8; 64]), + }; + + assert_eq!( + ValidatorSets::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Custom(0).into() + ); + }) +} + +#[test] +fn set_keys_keys_exist() { + new_test_ext().execute_with(|| { + let network = NetworkId::Monero; + + // set the keys first + ValidatorSets::set_keys( + RawOrigin::None.into(), + network, + Vec::new().try_into().unwrap(), + KeyPair(insecure_pair_from_name("name").public(), Vec::new().try_into().unwrap()), + Signature([0u8; 64]), + ) + .unwrap(); + + let call = validator_sets::Call::::set_keys { + network, + removed_participants: Vec::new().try_into().unwrap(), + key_pair: KeyPair(insecure_pair_from_name("name").public(), Vec::new().try_into().unwrap()), + signature: Signature([0u8; 64]), + }; + + assert_eq!( + ValidatorSets::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Stale.into() + ); + }) +} + +#[test] +fn set_keys_invalid_signature() { + new_test_ext().execute_with(|| { + let network = NetworkId::Ethereum; + let mut participants = get_ordered_keys(network, &genesis_participants()); + + // we can't have invalid set + let mut set = ValidatorSet { network, session: Session(1) }; + let key_pair = + KeyPair(insecure_pair_from_name("name").public(), Vec::new().try_into().unwrap()); + let signature = set_keys_signature(&set, &key_pair, &participants); + + let call = validator_sets::Call::::set_keys { + network, + removed_participants: Vec::new().try_into().unwrap(), + key_pair: key_pair.clone(), + signature, + }; + assert_eq!( + ValidatorSets::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::BadProof.into() + ); + + // fix the set + set.session = Session(0); + + // participants should match + participants.push(insecure_pair_from_name("random1")); + let signature = set_keys_signature(&set, &key_pair, &participants); + + let call = validator_sets::Call::::set_keys { + network, + removed_participants: Vec::new().try_into().unwrap(), + key_pair: key_pair.clone(), + signature, + }; + assert_eq!( + ValidatorSets::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::BadProof.into() + ); + + // fix the participants + participants.pop(); + + // msg key pair and the key pair to set should match + let key_pair2 = + KeyPair(insecure_pair_from_name("name2").public(), Vec::new().try_into().unwrap()); + let signature = set_keys_signature(&set, &key_pair2, &participants); + + let call = validator_sets::Call::::set_keys { + network, + removed_participants: Vec::new().try_into().unwrap(), + key_pair: key_pair.clone(), + signature, + }; + assert_eq!( + ValidatorSets::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::BadProof.into() + ); + + // use the same key pair + let signature = set_keys_signature(&set, &key_pair, &participants); + let call = validator_sets::Call::::set_keys { + network, + removed_participants: Vec::new().try_into().unwrap(), + key_pair, + signature, + }; + ValidatorSets::validate_unsigned(TransactionSource::External, &call).unwrap(); + + // TODO: removed_participants parameter isn't tested since it will be removed in upcoming + // commits? + }) +} + +// TODO: add report_slashes tests when the feature is complete. From 0bc2343e40822ab5089cbc923afca477ed1c734f Mon Sep 17 00:00:00 2001 From: akildemir Date: Mon, 7 Oct 2024 16:35:44 +0300 Subject: [PATCH 2/2] update tests with new types --- substrate/validator-sets/pallet/src/mock.rs | 6 +- substrate/validator-sets/pallet/src/tests.rs | 64 ++++++++------------ 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/substrate/validator-sets/pallet/src/mock.rs b/substrate/validator-sets/pallet/src/mock.rs index acc513b2e..d6d120507 100644 --- a/substrate/validator-sets/pallet/src/mock.rs +++ b/substrate/validator-sets/pallet/src/mock.rs @@ -155,9 +155,9 @@ pub fn genesis_participants() -> Vec { pub fn key_shares() -> HashMap { HashMap::from([ (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), - (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))), - (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))), - (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))), + (NetworkId::External(ExternalNetworkId::Bitcoin), Amount(1_000_000 * 10_u64.pow(8))), + (NetworkId::External(ExternalNetworkId::Ethereum), Amount(1_000_000 * 10_u64.pow(8))), + (NetworkId::External(ExternalNetworkId::Monero), Amount(100_000 * 10_u64.pow(8))), ]) } diff --git a/substrate/validator-sets/pallet/src/tests.rs b/substrate/validator-sets/pallet/src/tests.rs index 64fc80690..6c407abd7 100644 --- a/substrate/validator-sets/pallet/src/tests.rs +++ b/substrate/validator-sets/pallet/src/tests.rs @@ -59,7 +59,7 @@ fn get_session_at_which_changes_activate(network: NetworkId) -> u32 { } } -fn set_keys_for_session(network: NetworkId) { +fn set_keys_for_session(network: ExternalNetworkId) { ValidatorSets::set_keys( RawOrigin::None.into(), network, @@ -70,7 +70,7 @@ fn set_keys_for_session(network: NetworkId) { .unwrap(); } -fn set_keys_signature(set: &ValidatorSet, key_pair: &KeyPair, pairs: &[Pair]) -> Signature { +fn set_keys_signature(set: &ExternalValidatorSet, key_pair: &KeyPair, pairs: &[Pair]) -> Signature { let mut pub_keys = vec![]; for pair in pairs { let public_key = @@ -87,7 +87,8 @@ fn set_keys_signature(set: &ValidatorSet, key_pair: &KeyPair, pairs: &[Pair]) -> assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]); threshold_keys.push( - musig::(&musig_context(*set), &Zeroizing::new(secret_key), &pub_keys).unwrap(), + musig::(&musig_context((*set).into()), &Zeroizing::new(secret_key), &pub_keys) + .unwrap(), ); } @@ -125,7 +126,9 @@ fn rotate_session_until(network: NetworkId, session: u32) { while current < session { Babe::on_initialize(System::block_number() + 1); ValidatorSets::rotate_session(); - set_keys_for_session(network); + if let NetworkId::External(n) = network { + set_keys_for_session(n); + } ValidatorSets::retire_set(ValidatorSet { session: Session(current), network }); current += 1; } @@ -141,9 +144,9 @@ fn rotate_session() { let mut participants = HashMap::from([ (NetworkId::Serai, genesis_participants.clone()), - (NetworkId::Bitcoin, genesis_participants.clone()), - (NetworkId::Monero, genesis_participants.clone()), - (NetworkId::Ethereum, genesis_participants), + (NetworkId::External(ExternalNetworkId::Bitcoin), genesis_participants.clone()), + (NetworkId::External(ExternalNetworkId::Ethereum), genesis_participants.clone()), + (NetworkId::External(ExternalNetworkId::Monero), genesis_participants), ]); // rotate session @@ -152,7 +155,9 @@ fn rotate_session() { // verify for session 0 participants.sort(); - set_keys_for_session(network); + if let NetworkId::External(n) = network { + set_keys_for_session(n); + } verify_session_and_active_validators(network, participants, 0); // add 1 participant @@ -215,10 +220,10 @@ fn allocate() { genesis_participants().into_iter().map(|p| p.public()).collect(); let key_shares = key_shares(); let participant = insecure_pair_from_name("random1").public(); - let network = NetworkId::Ethereum; + let network = NetworkId::External(ExternalNetworkId::Ethereum); // check genesis TAS - set_keys_for_session(network); + set_keys_for_session(network.try_into().unwrap()); assert_eq!( ValidatorSets::total_allocated_stake(network).unwrap().0, key_shares[&network].0 * u64::try_from(genesis_participants.len()).unwrap() @@ -278,10 +283,10 @@ fn deallocate_pending() { genesis_participants().into_iter().map(|p| p.public()).collect(); let key_shares = key_shares(); let participant = insecure_pair_from_name("random1").public(); - let network = NetworkId::Bitcoin; + let network = NetworkId::External(ExternalNetworkId::Bitcoin); // check genesis TAS - set_keys_for_session(network); + set_keys_for_session(network.try_into().unwrap()); assert_eq!( ValidatorSets::total_allocated_stake(network).unwrap().0, key_shares[&network].0 * u64::try_from(genesis_participants.len()).unwrap() @@ -347,10 +352,10 @@ fn deallocate_immediately() { genesis_participants().into_iter().map(|p| p.public()).collect(); let key_shares = key_shares(); let participant = insecure_pair_from_name("random1").public(); - let network = NetworkId::Monero; + let network = NetworkId::External(ExternalNetworkId::Monero); // check genesis TAS - set_keys_for_session(network); + set_keys_for_session(network.try_into().unwrap()); assert_eq!( ValidatorSets::total_allocated_stake(network).unwrap().0, key_shares[&network].0 * u64::try_from(genesis_participants.len()).unwrap() @@ -416,9 +421,9 @@ fn deallocate_immediately() { // make a pool so that we have security oracle value for the coin let liq_acc = insecure_pair_from_name("liq-acc").public(); - let coin = Coin::Monero; - let balance = Balance { coin, amount: Amount(2 * key_shares[&network].0) }; - Coins::mint(liq_acc, balance).unwrap(); + let coin = ExternalCoin::Monero; + let balance = ExternalBalance { coin, amount: Amount(2 * key_shares[&network].0) }; + Coins::mint(liq_acc, balance.into()).unwrap(); Coins::mint(liq_acc, Balance { coin: Coin::Serai, amount: balance.amount }).unwrap(); Dex::add_liquidity( RawOrigin::Signed(liq_acc).into(), @@ -449,27 +454,10 @@ fn deallocate_immediately() { }) } -#[test] -fn set_keys_no_serai_network() { - new_test_ext().execute_with(|| { - let call = validator_sets::Call::::set_keys { - network: NetworkId::Serai, - removed_participants: Vec::new().try_into().unwrap(), - key_pair: KeyPair(insecure_pair_from_name("name").public(), Vec::new().try_into().unwrap()), - signature: Signature([0u8; 64]), - }; - - assert_eq!( - ValidatorSets::validate_unsigned(TransactionSource::External, &call), - InvalidTransaction::Custom(0).into() - ); - }) -} - #[test] fn set_keys_keys_exist() { new_test_ext().execute_with(|| { - let network = NetworkId::Monero; + let network = ExternalNetworkId::Monero; // set the keys first ValidatorSets::set_keys( @@ -498,11 +486,11 @@ fn set_keys_keys_exist() { #[test] fn set_keys_invalid_signature() { new_test_ext().execute_with(|| { - let network = NetworkId::Ethereum; - let mut participants = get_ordered_keys(network, &genesis_participants()); + let network = ExternalNetworkId::Ethereum; + let mut participants = get_ordered_keys(network.into(), &genesis_participants()); // we can't have invalid set - let mut set = ValidatorSet { network, session: Session(1) }; + let mut set = ExternalValidatorSet { network, session: Session(1) }; let key_pair = KeyPair(insecure_pair_from_name("name").public(), Vec::new().try_into().unwrap()); let signature = set_keys_signature(&set, &key_pair, &participants);