16 Commits

Author SHA1 Message Date
Luke Parker
44d0eeeb18 Restore key gen message match from develop
It was modified in response to the handover completion bug, which has now been
resolved.
2024-06-10 19:04:22 -04:00
Luke Parker
5a9ebc8cdc Correct detection of handover completion 2024-06-10 18:34:58 -04:00
Luke Parker
cca9e8cf16 fmt 2024-06-10 13:46:57 -04:00
Luke Parker
97ef70fbd7 Correct ThresholdParams assert_eq 2024-06-10 13:44:44 -04:00
Luke Parker
92275988dd Add note on origin of publish_tx function in tests/coordinator 2024-06-10 13:36:32 -04:00
Luke Parker
c8f690e2f8 Minor nits 2024-06-10 08:45:38 -04:00
Luke Parker
70add5b270 Remove EPOCH_INTERVAL 2024-06-10 08:40:31 -04:00
Luke Parker
6a7d803fe7 Merge branch 'develop' into HEAD 2024-06-06 02:46:18 -04:00
akildemir
4dfaf31c58 bug fixes 2024-02-27 17:32:50 +03:00
akildemir
360cd023a0 add coordinator side rotation test 2024-02-26 15:16:44 +03:00
akildemir
5f2e15604c Merge branch 'develop' of https://github.com/serai-dex/serai into vs-rotation-tests 2024-02-25 14:13:28 +03:00
akildemir
2292d2d2af fix pr comments 2024-02-19 18:32:55 +03:00
akildemir
c04afa032f set up the fast-epoch docker file 2024-02-12 16:05:17 +03:00
akildemir
d46e24de8f update to develop latest 2024-02-12 16:02:09 +03:00
akildemir
e93dbedd6a complete rotation test for all networks 2024-02-12 14:36:56 +03:00
akildemir
24ff866684 add node side unit test 2024-02-09 11:22:56 +03:00
10 changed files with 433 additions and 205 deletions

View File

@@ -196,10 +196,10 @@ impl Serai {
} }
async fn active_network_validators(&self, network: NetworkId) -> Result<Vec<Public>, SeraiError> { async fn active_network_validators(&self, network: NetworkId) -> Result<Vec<Public>, SeraiError> {
let hash: String = self let validators: String = self
.call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())]) .call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())])
.await?; .await?;
let bytes = Self::hex_decode(hash)?; let bytes = Self::hex_decode(validators)?;
let r = Vec::<Public>::decode(&mut bytes.as_slice()) let r = Vec::<Public>::decode(&mut bytes.as_slice())
.map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?;
Ok(r) Ok(r)

View File

@@ -14,8 +14,6 @@ use serai_client::{
mod common; mod common;
use common::validator_sets::{set_keys, allocate_stake, deallocate_stake}; use common::validator_sets::{set_keys, allocate_stake, deallocate_stake};
const EPOCH_INTERVAL: u64 = 5;
serai_test!( serai_test!(
set_keys_test: (|serai: Serai| async move { set_keys_test: (|serai: Serai| async move {
let network = NetworkId::Bitcoin; let network = NetworkId::Bitcoin;
@@ -223,20 +221,38 @@ async fn validator_set_rotation() {
.await; .await;
} }
async fn epoch_for_block(serai: &Serai, block: [u8; 32]) -> u64 {
let epoch: String = serai
.call("state_call", ["BabeApi_current_epoch".to_string(), String::new(), hex::encode(block)])
.await
.unwrap();
<u64 as scale::Decode>::decode(
&mut hex::decode(epoch.strip_prefix("0x").unwrap()).unwrap().as_slice(),
)
.unwrap()
}
async fn verify_session_and_active_validators( async fn verify_session_and_active_validators(
serai: &Serai, serai: &Serai,
network: NetworkId, network: NetworkId,
session: u64, session: u64,
participants: &[Public], participants: &[Public],
) { ) {
// wait untill the epoch block finalized // wait untill the epoch block finalizes
let epoch_block = (session * EPOCH_INTERVAL) + 1; let block = loop {
while serai.finalized_block_by_number(epoch_block).await.unwrap().is_none() { let mut block = serai.latest_finalized_block_hash().await.unwrap();
// sleep 1 block if epoch_for_block(serai, block).await < session {
// Sleep a block
tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; tokio::time::sleep(tokio::time::Duration::from_secs(6)).await;
continue;
} }
let serai_for_block = while epoch_for_block(serai, block).await > session {
serai.as_of(serai.finalized_block_by_number(epoch_block).await.unwrap().unwrap().hash()); block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0;
}
assert_eq!(epoch_for_block(serai, block).await, session);
break block;
};
let serai_for_block = serai.as_of(block);
// verify session // verify session
let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap(); let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap();
@@ -249,9 +265,10 @@ async fn verify_session_and_active_validators(
assert_eq!(validators, participants); assert_eq!(validators, participants);
// make sure finalization continues as usual after the changes // make sure finalization continues as usual after the changes
let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
tokio::time::timeout(tokio::time::Duration::from_secs(60), async move { tokio::time::timeout(tokio::time::Duration::from_secs(60), async move {
let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number; let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
while finalized_block <= epoch_block + 2 { while finalized_block <= current_finalized_block + 2 {
tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; tokio::time::sleep(tokio::time::Duration::from_secs(6)).await;
finalized_block = serai.latest_finalized_block().await.unwrap().header.number; finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
} }
@@ -263,8 +280,7 @@ async fn verify_session_and_active_validators(
} }
async fn get_active_session(serai: &Serai, network: NetworkId, hash: [u8; 32]) -> u64 { async fn get_active_session(serai: &Serai, network: NetworkId, hash: [u8; 32]) -> u64 {
let block_number = serai.block(hash).await.unwrap().unwrap().header.number; let epoch = epoch_for_block(serai, hash).await;
let epoch = block_number / EPOCH_INTERVAL;
// changes should be active in the next session // changes should be active in the next session
if network == NetworkId::Serai { if network == NetworkId::Serai {

View File

@@ -643,8 +643,9 @@ pub mod pallet {
// Checks if this session has completed the handover from the prior session. // Checks if this session has completed the handover from the prior session.
fn handover_completed(network: NetworkId, session: Session) -> bool { fn handover_completed(network: NetworkId, session: Session) -> bool {
let Some(current_session) = Self::session(network) else { return false }; let Some(current_session) = Self::session(network) else { return false };
// No handover occurs on genesis
if current_session.0 == 0 { // If the session we've been queried about is old, it must have completed its handover
if current_session.0 > session.0 {
return true; return true;
} }
// If the session we've been queried about has yet to start, it can't have completed its // If the session we've been queried about has yet to start, it can't have completed its
@@ -652,19 +653,21 @@ pub mod pallet {
if current_session.0 < session.0 { if current_session.0 < session.0 {
return false; return false;
} }
if current_session.0 == session.0 {
// Handover is automatically complete for Serai as it doesn't have a handover protocol // Handover is automatically complete for Serai as it doesn't have a handover protocol
// If not Serai, check the prior session had its keys cleared, which happens once its if network == NetworkId::Serai {
// retired return true;
return (network == NetworkId::Serai) ||
(!Keys::<T>::contains_key(ValidatorSet {
network,
session: Session(current_session.0 - 1),
}));
} }
// We're currently in a future session, meaning this session definitely performed itself
// handover // The current session must have set keys for its handover to be completed
true if !Keys::<T>::contains_key(ValidatorSet { network, session }) {
return false;
}
// This must be the first session (which has set keys) OR the prior session must have been
// retired (signified by its keys no longer being present)
(session.0 == 0) ||
(!Keys::<T>::contains_key(ValidatorSet { network, session: Session(session.0 - 1) }))
} }
fn new_session() { fn new_session() {
@@ -682,6 +685,8 @@ pub mod pallet {
} }
} }
// TODO: This is called retire_set, yet just starts retiring the set
// Update the nomenclature within this function
pub fn retire_set(set: ValidatorSet) { pub fn retire_set(set: ValidatorSet) {
// If the prior prior set didn't report, emit they're retired now // If the prior prior set didn't report, emit they're retired now
if PendingSlashReport::<T>::get(set.network).is_some() { if PendingSlashReport::<T>::get(set.network).is_some() {

View File

@@ -60,12 +60,18 @@ pub fn coordinator_instance(
) )
} }
pub fn serai_composition(name: &str) -> TestBodySpecification { pub fn serai_composition(name: &str, fast_epoch: bool) -> TestBodySpecification {
(if fast_epoch {
serai_docker_tests::build("serai-fast-epoch".to_string());
TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
)
} else {
serai_docker_tests::build("serai".to_string()); serai_docker_tests::build("serai".to_string());
TestBodySpecification::with_image( TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never),
) )
})
.replace_env( .replace_env(
[("SERAI_NAME".to_string(), name.to_lowercase()), ("KEY".to_string(), " ".to_string())].into(), [("SERAI_NAME".to_string(), name.to_lowercase()), ("KEY".to_string(), " ".to_string())].into(),
) )

View File

@@ -260,8 +260,14 @@ pub async fn batch(
#[tokio::test] #[tokio::test]
async fn batch_test() { async fn batch_test() {
new_test(|mut processors: Vec<Processor>| async move { new_test(
let (processor_is, substrate_key, _) = key_gen::<Secp256k1>(&mut processors).await; |mut processors: Vec<Processor>| async move {
// pop the last participant since genesis keygen has only 4 participants
processors.pop().unwrap();
assert_eq!(processors.len(), COORDINATORS);
let (processor_is, substrate_key, _) =
key_gen::<Secp256k1>(&mut processors, Session(0)).await;
batch( batch(
&mut processors, &mut processors,
&processor_is, &processor_is,
@@ -275,6 +281,8 @@ async fn batch_test() {
}, },
) )
.await; .await;
}) },
false,
)
.await; .await;
} }

View File

@@ -23,10 +23,12 @@ use crate::tests::*;
pub async fn key_gen<C: Ciphersuite>( pub async fn key_gen<C: Ciphersuite>(
processors: &mut [Processor], processors: &mut [Processor],
session: Session,
) -> (Vec<u8>, Zeroizing<<Ristretto as Ciphersuite>::F>, Zeroizing<C::F>) { ) -> (Vec<u8>, Zeroizing<<Ristretto as Ciphersuite>::F>, Zeroizing<C::F>) {
let coordinators = processors.len();
let mut participant_is = vec![]; let mut participant_is = vec![];
let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin }; let set = ValidatorSet { session, network: NetworkId::Bitcoin };
let id = KeyGenId { session: set.session, attempt: 0 }; let id = KeyGenId { session: set.session, attempt: 0 };
for (i, processor) in processors.iter_mut().enumerate() { for (i, processor) in processors.iter_mut().enumerate() {
@@ -46,8 +48,8 @@ pub async fn key_gen<C: Ciphersuite>(
CoordinatorMessage::KeyGen(messages::key_gen::CoordinatorMessage::GenerateKey { CoordinatorMessage::KeyGen(messages::key_gen::CoordinatorMessage::GenerateKey {
id, id,
params: ThresholdParams::new( params: ThresholdParams::new(
u16::try_from(((COORDINATORS * 2) / 3) + 1).unwrap(), u16::try_from(((coordinators * 2) / 3) + 1).unwrap(),
u16::try_from(COORDINATORS).unwrap(), u16::try_from(coordinators).unwrap(),
participant_is[i], participant_is[i],
) )
.unwrap(), .unwrap(),
@@ -65,7 +67,7 @@ pub async fn key_gen<C: Ciphersuite>(
wait_for_tributary().await; wait_for_tributary().await;
for (i, processor) in processors.iter_mut().enumerate() { for (i, processor) in processors.iter_mut().enumerate() {
let mut commitments = (0 .. u8::try_from(COORDINATORS).unwrap()) let mut commitments = (0 .. u8::try_from(coordinators).unwrap())
.map(|l| { .map(|l| {
( (
participant_is[usize::from(l)], participant_is[usize::from(l)],
@@ -83,7 +85,7 @@ pub async fn key_gen<C: Ciphersuite>(
); );
// Recipient it's for -> (Sender i, Recipient i) // Recipient it's for -> (Sender i, Recipient i)
let mut shares = (0 .. u8::try_from(COORDINATORS).unwrap()) let mut shares = (0 .. u8::try_from(coordinators).unwrap())
.map(|l| { .map(|l| {
( (
participant_is[usize::from(l)], participant_is[usize::from(l)],
@@ -118,7 +120,7 @@ pub async fn key_gen<C: Ciphersuite>(
CoordinatorMessage::KeyGen(messages::key_gen::CoordinatorMessage::Shares { CoordinatorMessage::KeyGen(messages::key_gen::CoordinatorMessage::Shares {
id, id,
shares: { shares: {
let mut shares = (0 .. u8::try_from(COORDINATORS).unwrap()) let mut shares = (0 .. u8::try_from(coordinators).unwrap())
.map(|l| { .map(|l| {
( (
participant_is[usize::from(l)], participant_is[usize::from(l)],
@@ -182,14 +184,14 @@ pub async fn key_gen<C: Ciphersuite>(
.unwrap() .unwrap()
.as_secs() .as_secs()
.abs_diff(context.serai_time) < .abs_diff(context.serai_time) <
70 (60 * 60 * 3) // 3 hours, which should exceed the length of any test we run
); );
assert_eq!(context.network_latest_finalized_block.0, [0; 32]); assert_eq!(context.network_latest_finalized_block.0, [0; 32]);
assert_eq!(set.session, session); assert_eq!(set.session, session);
assert_eq!(key_pair.0 .0, substrate_key); assert_eq!(key_pair.0 .0, substrate_key);
assert_eq!(&key_pair.1, &network_key); assert_eq!(&key_pair.1, &network_key);
} }
_ => panic!("coordinator didn't respond with ConfirmKeyPair"), _ => panic!("coordinator didn't respond with ConfirmKeyPair. msg: {msg:?}"),
} }
message = Some(msg); message = Some(msg);
} else { } else {
@@ -220,8 +222,15 @@ pub async fn key_gen<C: Ciphersuite>(
#[tokio::test] #[tokio::test]
async fn key_gen_test() { async fn key_gen_test() {
new_test(|mut processors: Vec<Processor>| async move { new_test(
key_gen::<Secp256k1>(&mut processors).await; |mut processors: Vec<Processor>| async move {
}) // pop the last participant since genesis keygen has only 4 participants
processors.pop().unwrap();
assert_eq!(processors.len(), COORDINATORS);
key_gen::<Secp256k1>(&mut processors, Session(0)).await;
},
false,
)
.await; .await;
} }

View File

@@ -22,6 +22,8 @@ mod sign;
#[allow(unused_imports)] #[allow(unused_imports)]
pub use sign::sign; pub use sign::sign;
mod rotation;
pub(crate) const COORDINATORS: usize = 4; pub(crate) const COORDINATORS: usize = 4;
pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1; pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1;
@@ -39,13 +41,15 @@ impl<F: Send + Future, TB: 'static + Send + Sync + Fn(Vec<Processor>) -> F> Test
} }
} }
pub(crate) async fn new_test(test_body: impl TestBody) { pub(crate) async fn new_test(test_body: impl TestBody, fast_epoch: bool) {
let mut unique_id_lock = UNIQUE_ID.get_or_init(|| Mutex::new(0)).lock().await; let mut unique_id_lock = UNIQUE_ID.get_or_init(|| Mutex::new(0)).lock().await;
let mut coordinators = vec![]; let mut coordinators = vec![];
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
let mut coordinator_compositions = vec![]; let mut coordinator_compositions = vec![];
for i in 0 .. COORDINATORS { // Spawn one extra coordinator which isn't in-set
#[allow(clippy::range_plus_one)]
for i in 0 .. (COORDINATORS + 1) {
let name = match i { let name = match i {
0 => "Alice", 0 => "Alice",
1 => "Bob", 1 => "Bob",
@@ -55,7 +59,7 @@ pub(crate) async fn new_test(test_body: impl TestBody) {
5 => "Ferdie", 5 => "Ferdie",
_ => panic!("needed a 7th name for a serai node"), _ => panic!("needed a 7th name for a serai node"),
}; };
let serai_composition = serai_composition(name); let serai_composition = serai_composition(name, fast_epoch);
let (processor_key, message_queue_keys, message_queue_composition) = let (processor_key, message_queue_keys, message_queue_composition) =
serai_message_queue_tests::instance(); serai_message_queue_tests::instance();

View File

@@ -0,0 +1,169 @@
use tokio::time::{sleep, Duration};
use ciphersuite::Secp256k1;
use serai_client::{
primitives::{insecure_pair_from_name, NetworkId},
validator_sets::{
self,
primitives::{Session, ValidatorSet},
ValidatorSetsEvent,
},
Amount, Pair, Transaction,
};
use crate::{*, tests::*};
// TODO: This is duplicated with serai-client's tests
async fn publish_tx(serai: &Serai, tx: &Transaction) -> [u8; 32] {
let mut latest = serai
.block(serai.latest_finalized_block_hash().await.unwrap())
.await
.unwrap()
.unwrap()
.number();
serai.publish(tx).await.unwrap();
// Get the block it was included in
// TODO: Add an RPC method for this/check the guarantee on the subscription
let mut ticks = 0;
loop {
latest += 1;
let block = {
let mut block;
while {
block = serai.finalized_block_by_number(latest).await.unwrap();
block.is_none()
} {
sleep(Duration::from_secs(1)).await;
ticks += 1;
if ticks > 60 {
panic!("60 seconds without inclusion in a finalized block");
}
}
block.unwrap()
};
for transaction in &block.transactions {
if transaction == tx {
return block.hash();
}
}
}
}
#[allow(dead_code)]
async fn allocate_stake(
serai: &Serai,
network: NetworkId,
amount: Amount,
pair: &Pair,
nonce: u32,
) -> [u8; 32] {
// get the call
let tx =
serai.sign(pair, validator_sets::SeraiValidatorSets::allocate(network, amount), nonce, 0);
publish_tx(serai, &tx).await
}
#[allow(dead_code)]
async fn deallocate_stake(
serai: &Serai,
network: NetworkId,
amount: Amount,
pair: &Pair,
nonce: u32,
) -> [u8; 32] {
// get the call
let tx =
serai.sign(pair, validator_sets::SeraiValidatorSets::deallocate(network, amount), nonce, 0);
publish_tx(serai, &tx).await
}
async fn get_session(serai: &Serai, network: NetworkId) -> Session {
serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.session(network)
.await
.unwrap()
.unwrap()
}
async fn wait_till_next_epoch(serai: &Serai) -> Session {
let starting_session = get_session(serai, NetworkId::Serai).await;
let mut session = starting_session;
while session == starting_session {
sleep(Duration::from_secs(6)).await;
session = get_session(serai, NetworkId::Serai).await;
}
session
}
async fn most_recent_new_set_event(serai: &Serai, network: NetworkId) -> ValidatorSetsEvent {
let mut current_block = serai.latest_finalized_block().await.unwrap();
loop {
let events = serai.as_of(current_block.hash()).validator_sets().new_set_events().await.unwrap();
for event in events {
match event {
ValidatorSetsEvent::NewSet { set } => {
if set.network == network {
return event;
}
}
_ => panic!("new_set_events gave non-NewSet event: {event:?}"),
}
}
current_block = serai.block(current_block.header.parent_hash.0).await.unwrap().unwrap();
}
}
#[tokio::test]
async fn set_rotation_test() {
new_test(
|mut processors: Vec<Processor>| async move {
// exclude the last processor from keygen since we will add him later
let mut excluded = processors.pop().unwrap();
assert_eq!(processors.len(), COORDINATORS);
let pair5 = insecure_pair_from_name("Eve");
let network = NetworkId::Bitcoin;
let amount = Amount(1_000_000 * 10_u64.pow(8));
let serai = processors[0].serai().await;
// add the last participant into validator set for btc network
allocate_stake(&serai, network, amount, &pair5, 0).await;
// genesis keygen
let _ = key_gen::<Secp256k1>(&mut processors, Session(0)).await;
// Even the excluded processor should receive the key pair confirmation
match excluded.recv_message().await {
CoordinatorMessage::Substrate(
messages::substrate::CoordinatorMessage::ConfirmKeyPair { session, .. },
) => assert_eq!(session, Session(0)),
_ => panic!("excluded got message other than ConfirmKeyPair"),
}
// wait until next session to see the effect on coordinator
wait_till_next_epoch(&serai).await;
// verfiy that coordinator received new_set
assert_eq!(
most_recent_new_set_event(&serai, network).await,
ValidatorSetsEvent::NewSet { set: ValidatorSet { session: Session(1), network } },
);
// add the last participant & do the keygen
processors.push(excluded);
let _ = key_gen::<Secp256k1>(&mut processors, Session(1)).await;
},
true,
)
.await;
}

View File

@@ -168,8 +168,14 @@ pub async fn sign(
#[tokio::test] #[tokio::test]
async fn sign_test() { async fn sign_test() {
new_test(|mut processors: Vec<Processor>| async move { new_test(
let (participant_is, substrate_key, _) = key_gen::<Secp256k1>(&mut processors).await; |mut processors: Vec<Processor>| async move {
// pop the last participant since genesis keygen has only 4 participant.
processors.pop().unwrap();
assert_eq!(processors.len(), COORDINATORS);
let (participant_is, substrate_key, _) =
key_gen::<Secp256k1>(&mut processors, Session(0)).await;
// 'Send' external coins into Serai // 'Send' external coins into Serai
let serai = processors[0].serai().await; let serai = processors[0].serai().await;
@@ -222,7 +228,10 @@ async fn sign_test() {
let serai = serai.as_of(block_included_in_hash); let serai = serai.as_of(block_included_in_hash);
let serai = serai.coins(); let serai = serai.coins();
assert_eq!(serai.coin_balance(Coin::Serai, serai_addr).await.unwrap(), Amount(1_000_000_000)); assert_eq!(
serai.coin_balance(Coin::Serai, serai_addr).await.unwrap(),
Amount(1_000_000_000)
);
// Verify the mint occurred as expected // Verify the mint occurred as expected
assert_eq!( assert_eq!(
@@ -323,6 +332,8 @@ async fn sign_test() {
} }
sign(&mut processors, &participant_is, Session(0), plan_id).await; sign(&mut processors, &participant_is, Session(0), plan_id).await;
}) },
false,
)
.await; .await;
} }

View File

@@ -69,7 +69,7 @@ pub(crate) async fn new_test(test_body: impl TestBody) {
let monero_processor_composition = monero_processor_composition.swap_remove(0); let monero_processor_composition = monero_processor_composition.swap_remove(0);
let coordinator_composition = coordinator_instance(name, coord_key); let coordinator_composition = coordinator_instance(name, coord_key);
let serai_composition = serai_composition(name); let serai_composition = serai_composition(name, false);
// Give every item in this stack a unique ID // Give every item in this stack a unique ID
// Uses a Mutex as we can't generate a 8-byte random ID without hitting hostname length limits // Uses a Mutex as we can't generate a 8-byte random ID without hitting hostname length limits