From f1f5100bc1aa182ae3a62eb115748716401b0e2a Mon Sep 17 00:00:00 2001 From: akildemir Date: Mon, 7 Oct 2024 16:03:47 +0300 Subject: [PATCH] add economic security pallet tests --- Cargo.lock | 8 + substrate/economic-security/pallet/Cargo.toml | 31 ++- substrate/economic-security/pallet/src/lib.rs | 6 + .../economic-security/pallet/src/mock.rs | 217 ++++++++++++++++++ .../economic-security/pallet/src/tests.rs | 82 +++++++ substrate/validator-sets/pallet/src/lib.rs | 8 + 6 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 substrate/economic-security/pallet/src/mock.rs create mode 100644 substrate/economic-security/pallet/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index a128f88d..dc028a31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8121,11 +8121,19 @@ version = "0.1.0" dependencies = [ "frame-support", "frame-system", + "pallet-babe", + "pallet-grandpa", + "pallet-timestamp", "parity-scale-codec", "scale-info", "serai-coins-pallet", "serai-dex-pallet", "serai-primitives", + "serai-validator-sets-pallet", + "sp-consensus-babe", + "sp-core", + "sp-io", + "sp-runtime", ] [[package]] diff --git a/substrate/economic-security/pallet/Cargo.toml b/substrate/economic-security/pallet/Cargo.toml index cefeee8e..eea14e36 100644 --- a/substrate/economic-security/pallet/Cargo.toml +++ b/substrate/economic-security/pallet/Cargo.toml @@ -30,6 +30,19 @@ coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", de serai-primitives = { path = "../../primitives", default-features = false } + +[dev-dependencies] +pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false } + +validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } + +sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-consensus-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } + [features] std = [ "scale/std", @@ -38,11 +51,27 @@ std = [ "frame-system/std", "frame-support/std", + "sp-io/std", + "sp-core/std", + "sp-consensus-babe/std", + "dex-pallet/std", "coins-pallet/std", + "validator-sets-pallet/std", "serai-primitives/std", + + "pallet-babe/std", + "pallet-grandpa/std", + "pallet-timestamp/std", ] -try-runtime = [] # TODO + +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime", + + "sp-runtime/try-runtime", +] + default = ["std"] diff --git a/substrate/economic-security/pallet/src/lib.rs b/substrate/economic-security/pallet/src/lib.rs index 045297f4..20897aaa 100644 --- a/substrate/economic-security/pallet/src/lib.rs +++ b/substrate/economic-security/pallet/src/lib.rs @@ -1,5 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + #[allow( unreachable_patterns, clippy::cast_possible_truncation, diff --git a/substrate/economic-security/pallet/src/mock.rs b/substrate/economic-security/pallet/src/mock.rs new file mode 100644 index 00000000..ffa7d7fb --- /dev/null +++ b/substrate/economic-security/pallet/src/mock.rs @@ -0,0 +1,217 @@ +//! Test environment for EconomicSecurity pallet. + +use super::*; + +use core::marker::PhantomData; +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 economic_security; +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; +pub use validator_sets_pallet as validator_sets; + +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, + EconomicSecurity: economic_security, + 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 validator_sets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = Babe; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +// 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::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))), + ]) +} + +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/economic-security/pallet/src/tests.rs b/substrate/economic-security/pallet/src/tests.rs new file mode 100644 index 00000000..a6010e71 --- /dev/null +++ b/substrate/economic-security/pallet/src/tests.rs @@ -0,0 +1,82 @@ +use crate::mock::*; + +use frame_support::traits::Hooks; +use frame_system::RawOrigin; + +use sp_core::{sr25519::Signature, Pair as PairTrait}; +use sp_runtime::BoundedVec; + +use validator_sets::primitives::KeyPair; +use serai_primitives::{ + insecure_pair_from_name, Balance, Coin, ExternalBalance, ExternalCoin, ExternalNetworkId, + EXTERNAL_COINS, EXTERNAL_NETWORKS, +}; + +fn set_keys_for_session(network: ExternalNetworkId) { + 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 make_pool_with_liquidity(coin: &ExternalCoin) { + // make a pool so that we have security oracle value for the coin + let liq_acc = insecure_pair_from_name("liq-acc").public(); + let balance = ExternalBalance { coin: *coin, amount: key_shares()[&coin.network().into()] }; + 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(), + *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) +} + +#[test] +fn economic_security() { + new_test_ext().execute_with(|| { + // update the state + EconomicSecurity::on_initialize(1); + + // make sure it is right at the beginning + // this is none at this point since no set has set their keys so TAS isn't up-to-date + for network in EXTERNAL_NETWORKS { + assert_eq!(EconomicSecurity::economic_security_block(network), None); + } + + // set the keys for TAS and have pools for oracle value + for coin in EXTERNAL_COINS { + set_keys_for_session(coin.network()); + make_pool_with_liquidity(&coin); + } + + // update the state + EconomicSecurity::on_initialize(1); + + // check again. The reason we have economic security now is because we stake a key share + // per participant per network(total of 4 key share) in genesis for all networks. + for network in EXTERNAL_NETWORKS { + assert_eq!(EconomicSecurity::economic_security_block(network), Some(1)); + } + + // TODO: Not sure how much sense this test makes since we start from an economically secure + // state. Ideally we should start from not economically secure state and stake the necessary + // amount and then check whether the pallet set the value right since that will be the mainnet + // path. But we cant do that at the moment since vs-pallet genesis build auto stake per network + // to construct the set. This also makes a missing piece of logic explicit. We need genesis + // validators to be in-set but without their stake, or at least its affect on TAS. So this test + // should be updated once that logic is coded. + }); +} diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 383f94a7..909b8a1a 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -393,6 +393,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; @@ -403,6 +404,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); @@ -415,6 +417,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,