add node side unit test

This commit is contained in:
akildemir
2024-02-09 11:22:56 +03:00
parent ad0ecc5185
commit 24ff866684
7 changed files with 235 additions and 12 deletions

View File

@@ -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<Vec<Public>, 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::<Vec<Public>>(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(|_| {

View File

@@ -35,6 +35,23 @@ impl<'a> SeraiValidatorSets<'a> {
.await
}
pub async fn extrinsic_failed(&self) -> Result<Vec<serai_abi::system::Event>, 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<Vec<ValidatorSetsEvent>, 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<

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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::<Vec<_>>(),
);
// 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?
}

View File

@@ -302,7 +302,7 @@ pub type ReportLongevity = <Runtime as pallet_babe::Config>::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;

View File

@@ -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<bool, DispatchError> {
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::<T>::DeallocationWouldRemoveEconomicSecurity)?;
}
print("passed stake req");
let old_allocation =
Self::allocation((network, account)).ok_or(Error::<T>::NonExistentValidator)?.0;
let new_allocation =
@@ -591,6 +596,8 @@ pub mod pallet {
Err(Error::<T>::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::<T>::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::<T>::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::<T>::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::<T>::take(set).unwrap();
PendingSlashReport::<T>::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::<T>::get(NetworkId::Serai).unwrap();
print("next session:");
print(session);
print("next validators: ");
print(next_validators.len());
Babe::<T>::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::<T>::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