diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs index 59a2e763..e44e0507 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -3,7 +3,7 @@ use thiserror::Error; use async_lock::RwLock; use simple_request::{hyper, Request, Client}; -use scale::{Encode, Decode, Compact}; +use scale::{decode_from_bytes, Compact, Decode, Encode}; use serde::{Serialize, Deserialize, de::DeserializeOwned}; pub use sp_core::{ @@ -195,6 +195,19 @@ impl Serai { Ok(()) } + pub async fn active_network_validators( + &self, + network: NetworkId, + ) -> Result, SeraiError> { + let hash: String = self + .call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())]) + .await?; + let bytes = Self::hex_decode(hash)?; + let r = decode_from_bytes::>(bytes.into()) + .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; + Ok(r) + } + pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> { let hash: String = self.call("chain_getFinalizedHead", ()).await?; Self::hex_decode(hash)?.try_into().map_err(|_| { diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 308b072b..0f13d2a8 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -35,6 +35,23 @@ impl<'a> SeraiValidatorSets<'a> { .await } + pub async fn extrinsic_failed(&self) -> Result, SeraiError> { + self + .0 + .events(|event| { + if let serai_abi::Event::System(event) = event { + if matches!(event, serai_abi::system::Event::ExtrinsicFailed { .. }) { + Some(event.clone()) + } else { + None + } + } else { + None + } + }) + .await + } + pub async fn participant_removed_events(&self) -> Result, SeraiError> { self .0 @@ -169,6 +186,14 @@ impl<'a> SeraiValidatorSets<'a> { })) } + pub fn allocate(network: NetworkId, amount: Amount) -> serai_abi::Call { + serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::allocate { network, amount }) + } + + pub fn deallocate(network: NetworkId, amount: Amount) -> serai_abi::Call { + serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::deallocate { network, amount }) + } + pub fn report_slashes( network: NetworkId, slashes: sp_runtime::BoundedVec< diff --git a/substrate/client/tests/common/mod.rs b/substrate/client/tests/common/mod.rs index 73fe52cb..caeed120 100644 --- a/substrate/client/tests/common/mod.rs +++ b/substrate/client/tests/common/mod.rs @@ -13,6 +13,7 @@ macro_rules! serai_test { PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, TestBodySpecification, DockerTest, }; + use std::collections::HashMap; serai_docker_tests::build("serai".to_string()); @@ -28,6 +29,7 @@ macro_rules! serai_test { "--rpc-cors".to_string(), "all".to_string(), ]) + .replace_env(HashMap::from([("RUST_LOG=runtime".to_string(), "debug".to_string())])) .set_publish_all_ports(true) .set_handle(handle) .set_start_policy(StartPolicy::Strict) diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs index 22d0c005..e5ec464c 100644 --- a/substrate/client/tests/common/validator_sets.rs +++ b/substrate/client/tests/common/validator_sets.rs @@ -1,9 +1,13 @@ use std::collections::HashMap; +use serai_abi::primitives::NetworkId; use zeroize::Zeroizing; use rand_core::OsRng; -use sp_core::{Pair, sr25519::Signature}; +use sp_core::{ + sr25519::{Pair, Signature}, + Pair as PairTrait, +}; use ciphersuite::{Ciphersuite, Ristretto}; use frost::dkg::musig::musig; @@ -15,7 +19,7 @@ use serai_client::{ primitives::{ValidatorSet, KeyPair, musig_context, set_keys_message}, ValidatorSetsEvent, }, - SeraiValidatorSets, Serai, + Amount, Serai, SeraiValidatorSets, }; use crate::common::tx::publish_tx; @@ -59,3 +63,29 @@ pub async fn set_keys(serai: &Serai, set: ValidatorSet, key_pair: KeyPair) -> [u block } + +#[allow(dead_code)] +pub async fn allocate_stake( + serai: &Serai, + network: NetworkId, + amount: Amount, + pair: &Pair, + nonce: u32, +) -> [u8; 32] { + // get the call + let tx = serai.sign(&pair, SeraiValidatorSets::allocate(network, amount), nonce, 0); + publish_tx(serai, &tx).await +} + +#[allow(dead_code)] +pub async fn deallocate_stake( + serai: &Serai, + network: NetworkId, + amount: Amount, + pair: &Pair, + nonce: u32, +) -> [u8; 32] { + // get the call + let tx = serai.sign(&pair, SeraiValidatorSets::deallocate(network, amount), nonce, 0); + publish_tx(serai, &tx).await +} diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index a487b51c..c570a992 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -8,11 +8,13 @@ use serai_client::{ primitives::{Session, ValidatorSet, KeyPair}, ValidatorSetsEvent, }, - Serai, + Amount, Serai, }; mod common; -use common::validator_sets::set_keys; +use common::validator_sets::{set_keys, allocate_stake, deallocate_stake}; + +const EPOCH_INTERVAL: u64 = 5; serai_test!( set_keys_test: (|serai: Serai| async move { @@ -73,3 +75,126 @@ serai_test!( assert_eq!(serai.keys(set).await.unwrap(), Some(key_pair)); }) ); + +#[tokio::test] +async fn validator_set_rotation() { + use dockertest::{ + PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image, + TestBodySpecification, DockerTest, + }; + + serai_docker_tests::build("serai".to_string()); + let handle = |name| format!("serai_client-serai_node-{name}"); + let composition = |name| { + TestBodySpecification::with_image( + Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), + ) + .replace_cmd(vec![ + "serai-node".to_string(), + "--unsafe-rpc-external".to_string(), + "--rpc-cors".to_string(), + "all".to_string(), + "--chain".to_string(), + "local".to_string(), + format!("--{name}"), + ]) + .set_publish_all_ports(true) + .set_handle(handle(name)) + .set_start_policy(StartPolicy::Strict) + .set_log_options(Some(LogOptions { + action: LogAction::Forward, + policy: LogPolicy::Always, + source: LogSource::Both, + })) + }; + + let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); + test.provide_container(composition("alice")); + test.provide_container(composition("bob")); + test.provide_container(composition("charlie")); + test.provide_container(composition("dave")); + test + .run_async(|ops| async move { + // Sleep until the Substrate RPC starts + let alice = handle("alice"); + let alice_rpc = ops.handle(&alice).host_port(9944).unwrap(); + let alice_rpc = format!("http://{}:{}", alice_rpc.0, alice_rpc.1); + + // Sleep for a minute + tokio::time::sleep(core::time::Duration::from_secs(60)).await; + let serai = Serai::new(alice_rpc.clone()).await.unwrap(); + + // taken from testnet config + let pair1 = insecure_pair_from_name("Alice"); + let pair2 = insecure_pair_from_name("Bob"); + let pair3 = insecure_pair_from_name("Charlie"); + let pair4 = insecure_pair_from_name("Dave"); + let single_key_share = Amount(50_000 * 10_u64.pow(8)); + + // Make sure the genesis is as expected + let network = NetworkId::Serai; + assert_eq!( + serai + .as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash()) + .validator_sets() + .new_set_events() + .await + .unwrap(), + NETWORKS + .iter() + .copied() + .map(|network| ValidatorSetsEvent::NewSet { + set: ValidatorSet { session: Session(0), network } + }) + .collect::>(), + ); + + // we start the chain with 4 default participants that has a single key share each + let mut participants = vec![pair1.public(), pair2.public(), pair3.public(), pair4.public()]; + participants.sort(); + verfiy_session_and_active_validators(&serai, network, 0, &participants).await; + + // remove 1 participant + let hash = deallocate_stake(&serai, network, single_key_share, &pair2, 0).await; + participants.remove(1); + + // TODO: check pending deallocations + + // verify for 2 epoch later(it takes 1 extra session for serai net to make the changes active) + // and since we removed a participant, we also need 1 extra session for cool down period. + let block_number = serai.block(hash).await.unwrap().unwrap().header.number; + let epoch_number = block_number / EPOCH_INTERVAL; + participants.sort(); + verfiy_session_and_active_validators(&serai, network, epoch_number + 3, &participants).await; + + // TODO: test add valiators + }) + .await; +} + +async fn verfiy_session_and_active_validators( + serai: &Serai, + network: NetworkId, + session: u64, + participants: &[Public], +) { + // wait untill the epoch block finalized + let epoch_block = (session * EPOCH_INTERVAL) + 1; + while serai.finalized_block_by_number(epoch_block).await.unwrap().is_none() { + // sleep 1 block + tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + } + let serai_for_block = + serai.as_of(serai.finalized_block_by_number(epoch_block).await.unwrap().unwrap().hash()); + + // verfiy session + let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap(); + assert_eq!(u64::from(s.0), session); + + // verify participants + let mut validators = serai.active_network_validators(network).await.unwrap(); + validators.sort(); + assert_eq!(validators, participants); + + // TODO: verfiy key shares as well? +} diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index f083befb..7f97ac29 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -302,7 +302,7 @@ pub type ReportLongevity = ::EpochDuration; impl babe::Config for Runtime { #[allow(clippy::identity_op)] - type EpochDuration = ConstU64<{ 1 * DAYS }>; + type EpochDuration = ConstU64<{ DAYS / (24 * 60 * 2) }>; // 30 seconds type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>; type EpochChangeTrigger = babe::ExternalTrigger; type DisabledValidators = ValidatorSets; diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 3c8418bf..1e89064e 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -2,6 +2,8 @@ use core::marker::PhantomData; +use sp_runtime::print; + use scale::{Encode, Decode}; use scale_info::TypeInfo; @@ -568,6 +570,7 @@ pub mod pallet { account: T::AccountId, amount: Amount, ) -> Result { + print("in daellocate"); // Check it's safe to decrease this set's stake by this amount let new_total_staked = Self::total_allocated_stake(network) .unwrap() @@ -579,6 +582,8 @@ pub mod pallet { Err(Error::::DeallocationWouldRemoveEconomicSecurity)?; } + print("passed stake req"); + let old_allocation = Self::allocation((network, account)).ok_or(Error::::NonExistentValidator)?.0; let new_allocation = @@ -591,6 +596,8 @@ pub mod pallet { Err(Error::::DeallocationWouldRemoveParticipant)?; } + print("passed DeallocationWouldRemoveParticipant"); + let decreased_key_shares = (old_allocation / allocation_per_key_share) > (new_allocation / allocation_per_key_share); @@ -613,6 +620,8 @@ pub mod pallet { } } + print("passed bft"); + // If we're not in-set, allow immediate deallocation if !Self::in_set(network, account) { Self::deposit_event(Event::AllocationDecreased { @@ -621,9 +630,12 @@ pub mod pallet { amount, delayed_until: None, }); + print("returning ok true"); return Ok(true); } + print("passed in set"); + // Set it to PendingDeallocations, letting it be released upon a future session // This unwrap should be fine as this account is active, meaning a session has occurred let to_unlock_on = Self::session_to_unlock_on_for_current_set(network).unwrap(); @@ -635,6 +647,8 @@ pub mod pallet { Some(Amount(existing.0 + amount.0)), ); + print("passed PendingDeallocations"); + Self::deposit_event(Event::AllocationDecreased { validator: account, network, @@ -642,6 +656,7 @@ pub mod pallet { delayed_until: Some(to_unlock_on), }); + print("return ok false at the end"); Ok(false) } @@ -688,16 +703,20 @@ pub mod pallet { } pub fn retire_set(set: ValidatorSet) { - let keys = Keys::::take(set).unwrap(); - // If the prior prior set didn't report, emit they're retired now + // If the prior set didn't report, emit they're retired now if PendingSlashReport::::get(set.network).is_some() { Self::deposit_event(Event::SetRetired { set: ValidatorSet { network: set.network, session: Session(set.session.0 - 1) }, }); } - // This overwrites the prior value as the prior to-report set's stake presumably just - // unlocked, making their report unenforceable - PendingSlashReport::::set(set.network, Some(keys.0)); + + // Serai network slashes are handled by BABE/GRANDPA + if set.network != NetworkId::Serai { + // This overwrites the prior value as the prior to-report set's stake presumably just + // unlocked, making their report unenforceable + let keys = Keys::::take(set).unwrap(); + PendingSlashReport::::set(set.network, Some(keys.0)); + } // We're retiring this set because the set after it accepted the handover Self::deposit_event(Event::AcceptedHandover { @@ -726,6 +745,11 @@ pub mod pallet { .expect("no Serai participants upon rotate_session"); let prior_serai_session = Self::session(NetworkId::Serai).unwrap(); + print("now session:"); + print(prior_serai_session.0); + print("now validators: "); + print(now_validators.len()); + // TODO: T::SessionHandler::on_before_session_ending() was here. // end the current serai session. Self::retire_set(ValidatorSet { network: NetworkId::Serai, session: prior_serai_session }); @@ -736,6 +760,10 @@ pub mod pallet { // Update Babe and Grandpa let session = prior_serai_session.0 + 1; let next_validators = Participants::::get(NetworkId::Serai).unwrap(); + print("next session:"); + print(session); + print("next validators: "); + print(next_validators.len()); Babe::::enact_epoch_change( WeakBoundedVec::force_from( now_validators.iter().copied().map(|(id, w)| (BabeAuthorityId::from(id), w)).collect(), @@ -750,7 +778,7 @@ pub mod pallet { Grandpa::::new_session( true, session, - next_validators.into_iter().map(|(id, w)| (GrandpaAuthorityId::from(id), w)).collect(), + now_validators.into_iter().map(|(id, w)| (GrandpaAuthorityId::from(id), w)).collect(), ); // Clear SeraiDisabledIndices, only preserving keys still present in the new session