diff --git a/Cargo.lock b/Cargo.lock index 10ac70ff..d3df6c51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10895,6 +10895,7 @@ dependencies = [ "scale-info", "serai-primitives", "serde", + "sp-core", "zeroize", ] diff --git a/substrate/serai/client/src/serai/mod.rs b/substrate/serai/client/src/serai/mod.rs index d8fcf107..2f10873b 100644 --- a/substrate/serai/client/src/serai/mod.rs +++ b/substrate/serai/client/src/serai/mod.rs @@ -30,9 +30,7 @@ use serai_runtime::{ pub mod tokens; pub mod in_instructions; -pub mod validator_sets { - pub use serai_runtime::validator_sets::primitives; -} +pub mod validator_sets; #[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Encode, Decode)] pub struct Tip { diff --git a/substrate/serai/client/src/serai/validator_sets.rs b/substrate/serai/client/src/serai/validator_sets.rs new file mode 100644 index 00000000..dcba4faf --- /dev/null +++ b/substrate/serai/client/src/serai/validator_sets.rs @@ -0,0 +1,59 @@ +use serai_runtime::{validator_sets, ValidatorSets, Runtime}; +pub use validator_sets::primitives; +use primitives::{ValidatorSet, ValidatorSetData, KeyPair}; + +use subxt::tx::{self, DynamicTxPayload}; + +use crate::{primitives::NetworkId, Serai, SeraiError, scale_value, scale_composite}; + +const PALLET: &str = "ValidatorSets"; + +pub type ValidatorSetsEvent = validator_sets::Event; + +impl Serai { + pub async fn get_vote_events( + &self, + block: [u8; 32], + ) -> Result, SeraiError> { + self + .events::(block, |event| matches!(event, ValidatorSetsEvent::Vote { .. })) + .await + } + + pub async fn get_key_gen_events( + &self, + block: [u8; 32], + ) -> Result, SeraiError> { + self + .events::(block, |event| matches!(event, ValidatorSetsEvent::KeyGen { .. })) + .await + } + + pub async fn get_validator_set( + &self, + set: ValidatorSet, + ) -> Result, SeraiError> { + self + .storage( + PALLET, + "ValidatorSets", + Some(vec![scale_value(set)]), + self.get_latest_block_hash().await?, + ) + .await + } + + pub async fn get_keys(&self, set: ValidatorSet) -> Result, SeraiError> { + self + .storage(PALLET, "Keys", Some(vec![scale_value(set)]), self.get_latest_block_hash().await?) + .await + } + + pub fn vote(network: NetworkId, key_pair: KeyPair) -> DynamicTxPayload<'static> { + tx::dynamic( + PALLET, + "vote", + scale_composite(validator_sets::Call::::vote { network, key_pair }), + ) + } +} diff --git a/substrate/serai/client/tests/burn.rs b/substrate/serai/client/tests/burn.rs index 5b0fc0b4..1a7b27a0 100644 --- a/substrate/serai/client/tests/burn.rs +++ b/substrate/serai/client/tests/burn.rs @@ -1,9 +1,5 @@ -use core::time::Duration; - use rand_core::{RngCore, OsRng}; -use tokio::time::sleep; - use sp_core::{sr25519::Signature, Pair}; use subxt::{config::extrinsic_params::BaseExtrinsicParamsBuilder}; @@ -21,7 +17,7 @@ use serai_client::{ }; mod runner; -use runner::{URL, provide_batch}; +use runner::{URL, publish_tx, provide_batch}; serai_test!( async fn burn() { @@ -75,22 +71,15 @@ serai_test!( let burn = Serai::burn(balance, out.clone()); let signer = PairSigner::new(pair); - serai - .publish(&serai.sign(&signer, &burn, 0, BaseExtrinsicParamsBuilder::new()).unwrap()) - .await - .unwrap(); + let block = publish_tx( + &serai, + &serai.sign(&signer, &burn, 0, BaseExtrinsicParamsBuilder::new()).unwrap(), + ) + .await; - loop { - let block = serai.get_latest_block_hash().await.unwrap(); - let events = serai.get_burn_events(block).await.unwrap(); - if events.is_empty() { - sleep(Duration::from_millis(50)).await; - continue; - } - assert_eq!(events, vec![TokensEvent::Burn { address, balance, instruction: out }]); - assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), Amount(0)); - assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), Amount(0)); - break; - } + let events = serai.get_burn_events(block).await.unwrap(); + assert_eq!(events, vec![TokensEvent::Burn { address, balance, instruction: out }]); + assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), Amount(0)); + assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), Amount(0)); } ); diff --git a/substrate/serai/client/tests/runner.rs b/substrate/serai/client/tests/runner.rs index 17e3470c..03bc7fa2 100644 --- a/substrate/serai/client/tests/runner.rs +++ b/substrate/serai/client/tests/runner.rs @@ -5,7 +5,7 @@ use lazy_static::lazy_static; use tokio::{sync::Mutex, time::sleep}; use serai_client::{ - subxt::config::Header, + subxt::{config::Header, utils::Encoded}, in_instructions::{primitives::SignedBatch, InInstructionsEvent}, Serai, }; @@ -17,9 +17,7 @@ lazy_static! { } #[allow(dead_code)] -pub async fn provide_batch(batch: SignedBatch) -> [u8; 32] { - let serai = Serai::new(URL).await.unwrap(); - +pub async fn publish_tx(serai: &Serai, tx: &Encoded) -> [u8; 32] { let mut latest = serai .get_block(serai.get_latest_block_hash().await.unwrap()) .await @@ -28,16 +26,15 @@ pub async fn provide_batch(batch: SignedBatch) -> [u8; 32] { .header .number(); - let execution = serai.execute_batch(batch.clone()).unwrap(); - serai.publish(&execution).await.unwrap(); + serai.publish(tx).await.unwrap(); // Get the block it was included in - let mut block; + // TODO: Add an RPC method for this/check the guarantee on the subscription let mut ticks = 0; - 'get_block: loop { + loop { latest += 1; - block = { + let block = { let mut block; while { block = serai.get_block_by_number(latest).await.unwrap(); @@ -54,12 +51,19 @@ pub async fn provide_batch(batch: SignedBatch) -> [u8; 32] { }; for extrinsic in block.extrinsics { - if extrinsic.0 == execution.0[2 ..] { - break 'get_block; + if extrinsic.0 == tx.0[2 ..] { + return block.header.hash().into(); } } } - let block = block.header.hash().into(); +} + +#[allow(dead_code)] +pub async fn provide_batch(batch: SignedBatch) -> [u8; 32] { + let serai = Serai::new(URL).await.unwrap(); + + let execution = serai.execute_batch(batch.clone()).unwrap(); + let block = publish_tx(&serai, &execution).await; let batches = serai.get_batch_events(block).await.unwrap(); // TODO: impl From for BatchEvent? diff --git a/substrate/serai/client/tests/validator_sets.rs b/substrate/serai/client/tests/validator_sets.rs new file mode 100644 index 00000000..12fcbb81 --- /dev/null +++ b/substrate/serai/client/tests/validator_sets.rs @@ -0,0 +1,67 @@ +use rand_core::{RngCore, OsRng}; + +use sp_core::{sr25519::Public, Pair}; +use subxt::{config::extrinsic_params::BaseExtrinsicParamsBuilder}; + +use serai_client::{ + primitives::{BITCOIN_NET_ID, BITCOIN_NET, insecure_pair_from_name}, + validator_sets::{ + primitives::{Session, ValidatorSet}, + ValidatorSetsEvent, + }, + PairSigner, Serai, +}; + +mod runner; +use runner::{URL, publish_tx}; + +serai_test!( + async fn vote_keys() { + let network = BITCOIN_NET_ID; + let set = ValidatorSet { session: Session(0), network }; + + // Neither of these keys are validated + // The external key is infeasible to validate on-chain, the Ristretto key is feasible + // TODO: Should the Ristretto key be validated? + let mut ristretto_key = [0; 32]; + OsRng.fill_bytes(&mut ristretto_key); + let mut external_key = vec![0; 33]; + OsRng.fill_bytes(&mut external_key); + let key_pair = (Public(ristretto_key), external_key.try_into().unwrap()); + + let pair = insecure_pair_from_name("Alice"); + let public = pair.public(); + + let serai = Serai::new(URL).await.unwrap(); + + // Make sure the genesis is as expected + let set_data = serai.get_validator_set(set).await.unwrap().unwrap(); + assert_eq!(set_data.network, *BITCOIN_NET); + let participants_ref: &[_] = set_data.participants.as_ref(); + assert_eq!(participants_ref, [(public, set_data.bond)].as_ref()); + + // Vote in a key pair + let block = publish_tx( + &serai, + &serai + .sign( + &PairSigner::new(pair), + &Serai::vote(network, key_pair.clone()), + 0, + BaseExtrinsicParamsBuilder::new(), + ) + .unwrap(), + ) + .await; + + assert_eq!( + serai.get_vote_events(block).await.unwrap(), + vec![ValidatorSetsEvent::Vote { voter: public, set, key_pair: key_pair.clone(), votes: 1 }] + ); + assert_eq!( + serai.get_key_gen_events(block).await.unwrap(), + vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] + ); + assert_eq!(serai.get_keys(set).await.unwrap(), Some(key_pair)); + } +); diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index c71beb04..d9118ae3 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -28,8 +28,6 @@ std = [ "scale/std", "scale-info/std", - "sp-core/std", - "frame-system/std", "frame-support/std", diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 450c33b5..f96f9782 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -5,8 +5,6 @@ pub mod pallet { use scale::{Encode, Decode}; use scale_info::TypeInfo; - use sp_core::sr25519; - use frame_system::pallet_prelude::*; use frame_support::pallet_prelude::*; @@ -15,7 +13,7 @@ pub mod pallet { use primitives::*; #[pallet::config] - pub trait Config: frame_system::Config + TypeInfo { + pub trait Config: frame_system::Config + TypeInfo { type RuntimeEvent: IsType<::RuntimeEvent> + From>; } @@ -39,20 +37,6 @@ pub mod pallet { } } - // Support keys up to 96 bytes (BLS12-381 G2) - const MAX_KEY_LEN: u32 = 96; - type MaxKeyLen = ConstU32; - - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] - pub struct ValidatorSetData { - bond: Amount, - network: Network, - - // Participant and their amount bonded to this set - // Limit each set to 100 participants for now - participants: BoundedVec<(T::AccountId, Amount), ConstU32<100>>, - } - #[pallet::pallet] pub struct Pallet(PhantomData); @@ -60,16 +44,11 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn validator_set)] pub type ValidatorSets = - StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>; - - type Key = BoundedVec; - // A validator set's key pair is defined as their Ristretto key, used for signing InInstructions, - // and their key on the external network - type KeyPair = (sr25519::Public, Key); + StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>; /// The key pair for a given validator set instance. #[pallet::storage] - #[pallet::getter(fn key)] + #[pallet::getter(fn keys)] pub type Keys = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>; /// If an account has voted for a specific key pair or not. @@ -153,7 +132,7 @@ pub mod pallet { // Confirm the signer is a validator in the set let data = ValidatorSets::::get(set).ok_or(Error::::NonExistentValidatorSet)?; - if data.participants.iter().any(|participant| participant.0 == signer) { + if !data.participants.iter().any(|participant| participant.0 == signer) { Err(Error::::NotValidator)?; } diff --git a/substrate/validator-sets/primitives/Cargo.toml b/substrate/validator-sets/primitives/Cargo.toml index c09ce14a..f880e3fd 100644 --- a/substrate/validator-sets/primitives/Cargo.toml +++ b/substrate/validator-sets/primitives/Cargo.toml @@ -19,8 +19,10 @@ scale-info = { version = "2", default-features = false, features = ["derive"] } serde = { version = "1", features = ["derive"], optional = true } +sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } + serai-primitives = { path = "../../serai/primitives", default-features = false } [features] -std = ["zeroize", "scale/std", "scale-info/std", "serde", "serai-primitives/std"] +std = ["zeroize", "scale/std", "scale-info/std", "serde", "sp-core/std", "serai-primitives/std"] default = ["std"] diff --git a/substrate/validator-sets/primitives/src/lib.rs b/substrate/validator-sets/primitives/src/lib.rs index 2f1cdef2..43d39ca7 100644 --- a/substrate/validator-sets/primitives/src/lib.rs +++ b/substrate/validator-sets/primitives/src/lib.rs @@ -9,7 +9,12 @@ use scale_info::TypeInfo; #[cfg(feature = "std")] use serde::{Serialize, Deserialize}; -use serai_primitives::NetworkId; +use sp_core::{ConstU32, sr25519, bounded::BoundedVec}; + +use serai_primitives::{NetworkId, Network, Amount}; + +// Support keys up to 96 bytes (BLS12-381 G2). +const MAX_KEY_LEN: u32 = 96; /// The type used to identify a specific session of validators. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] @@ -23,3 +28,22 @@ pub struct ValidatorSet { pub session: Session, pub network: NetworkId, } + +/// The data for a validator set. +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub struct ValidatorSetData { + pub bond: Amount, + pub network: Network, + + // Participant and their amount bonded to this set + // Limit each set to 100 participants for now + pub participants: BoundedVec<(sr25519::Public, Amount), ConstU32<100>>, +} + +type MaxKeyLen = ConstU32; +/// The type representing a Key from an external network. +pub type ExternalKey = BoundedVec; + +/// A Validator Set's Ristretto key, used for signing InInstructions, and their key on the external +/// network. +pub type KeyPair = (sr25519::Public, ExternalKey);