mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 13:39:25 +00:00
add node side unit test
This commit is contained in:
@@ -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(|_| {
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user