diff --git a/Cargo.lock b/Cargo.lock index 9c55b845..db97c1ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8754,7 +8754,9 @@ version = "0.1.0" dependencies = [ "bitcoin", "ciphersuite", + "frost-schnorrkel", "lazy_static", + "modular-frost", "monero-serai", "parity-scale-codec", "rand_core 0.6.4", @@ -8764,6 +8766,7 @@ dependencies = [ "subxt", "thiserror", "tokio", + "zeroize", ] [[package]] @@ -10995,11 +10998,15 @@ dependencies = [ name = "validator-sets-pallet" version = "0.1.0" dependencies = [ + "ciphersuite", + "dkg", "frame-support", "frame-system", + "hashbrown 0.13.2", "parity-scale-codec", "scale-info", "serai-primitives", + "sp-application-crypto", "sp-core", "validator-sets-primitives", ] diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index 192f6241..4ad62b8e 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -13,6 +13,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +zeroize = "^1.5" thiserror = { version = "1", optional = true } scale = { package = "parity-scale-codec", version = "3" } @@ -28,6 +29,17 @@ bitcoin = { version = "0.30", optional = true } ciphersuite = { path = "../../crypto/ciphersuite", version = "0.3", optional = true } monero-serai = { path = "../../coins/monero", version = "0.1.4-alpha", optional = true } +[dev-dependencies] +lazy_static = "1" + +rand_core = "0.6" + +ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto"] } +frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] } +schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" } + +tokio = "1" + [features] serai = ["thiserror", "scale-info", "subxt"] @@ -38,10 +50,3 @@ monero = ["coins", "ciphersuite/ed25519", "monero-serai"] # Assumes the default usage is to use Serai as a DEX, which doesn't actually # require connecting to a Serai node default = ["bitcoin", "monero"] - -[dev-dependencies] -lazy_static = "1" - -rand_core = "0.6" - -tokio = "1" diff --git a/substrate/client/src/serai/in_instructions.rs b/substrate/client/src/serai/in_instructions.rs index 2c4b2c97..978830f4 100644 --- a/substrate/client/src/serai/in_instructions.rs +++ b/substrate/client/src/serai/in_instructions.rs @@ -33,7 +33,7 @@ impl Serai { .await } - pub fn execute_batch(&self, batch: SignedBatch) -> Encoded { - self.unsigned::(&in_instructions::Call::::execute_batch { batch }) + pub fn execute_batch(batch: SignedBatch) -> Encoded { + Self::unsigned::(&in_instructions::Call::::execute_batch { batch }) } } diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs index 13ee01ad..a7d9eb7b 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -269,7 +269,7 @@ impl Serai { .map_err(SeraiError::RpcError) } - fn unsigned(&self, call: &C) -> Encoded { + fn unsigned(call: &C) -> Encoded { // TODO: Should Serai purge the old transaction code AND set this to 0/1? const TRANSACTION_VERSION: u8 = 4; diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 018cbe75..1321686b 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -1,10 +1,12 @@ +use sp_core::sr25519::Signature; + use serai_runtime::{validator_sets, ValidatorSets, Runtime}; pub use validator_sets::primitives; use primitives::{ValidatorSet, ValidatorSetData, KeyPair}; -use subxt::tx::Payload; +use subxt::utils::Encoded; -use crate::{primitives::NetworkId, Serai, SeraiError, Composite, scale_value, scale_composite}; +use crate::{primitives::NetworkId, Serai, SeraiError, scale_value}; const PALLET: &str = "ValidatorSets"; @@ -20,15 +22,6 @@ impl Serai { .await } - 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], @@ -52,17 +45,35 @@ impl Serai { .await } + pub async fn get_validator_set_musig_key( + &self, + set: ValidatorSet, + ) -> Result, SeraiError> { + self + .storage( + PALLET, + "MuSigKeys", + 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) -> Payload> { - Payload::new( - PALLET, - "vote", - scale_composite(validator_sets::Call::::vote { network, key_pair }), - ) + pub fn set_validator_set_keys( + network: NetworkId, + key_pair: KeyPair, + signature: Signature, + ) -> Encoded { + Self::unsigned::(&validator_sets::Call::::set_keys { + network, + key_pair, + signature, + }) } } diff --git a/substrate/client/tests/common/in_instructions.rs b/substrate/client/tests/common/in_instructions.rs index 7563b96c..85c2e5fd 100644 --- a/substrate/client/tests/common/in_instructions.rs +++ b/substrate/client/tests/common/in_instructions.rs @@ -9,9 +9,10 @@ use serai_client::{ primitives::{Batch, SignedBatch}, InInstructionsEvent, }, + Serai, }; -use crate::common::{serai, tx::publish_tx, validator_sets::vote_in_keys}; +use crate::common::{serai, tx::publish_tx, validator_sets::set_validator_set_keys}; #[allow(dead_code)] pub async fn provide_batch(batch: Batch) -> [u8; 32] { @@ -24,15 +25,15 @@ pub async fn provide_batch(batch: Batch) -> [u8; 32] { keys } else { let keys = (pair.public(), vec![].try_into().unwrap()); - vote_in_keys(set, keys.clone()).await; + set_validator_set_keys(set, keys.clone()).await; keys }; assert_eq!(keys.0, pair.public()); - let block = publish_tx( - &serai - .execute_batch(SignedBatch { batch: batch.clone(), signature: pair.sign(&batch.encode()) }), - ) + let block = publish_tx(&Serai::execute_batch(SignedBatch { + batch: batch.clone(), + signature: pair.sign(&batch.encode()), + })) .await; let batches = serai.get_batch_events(block).await.unwrap(); diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs index 4d5f2cc4..750d932a 100644 --- a/substrate/client/tests/common/validator_sets.rs +++ b/substrate/client/tests/common/validator_sets.rs @@ -1,42 +1,68 @@ -use sp_core::Pair; +use std::collections::HashMap; + +use zeroize::Zeroizing; +use rand_core::OsRng; + +use scale::Encode; + +use sp_core::{Pair, sr25519::Signature}; + +use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; +use frost::dkg::musig::*; +use schnorrkel::Schnorrkel; use serai_client::{ - subxt::config::extrinsic_params::BaseExtrinsicParamsBuilder, - primitives::{SeraiAddress, insecure_pair_from_name}, + primitives::insecure_pair_from_name, validator_sets::{ primitives::{ValidatorSet, KeyPair}, ValidatorSetsEvent, }, - PairSigner, Serai, + Serai, }; use crate::common::{serai, tx::publish_tx}; #[allow(dead_code)] -pub async fn vote_in_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8; 32] { +pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8; 32] { let pair = insecure_pair_from_name("Alice"); let public = pair.public(); let serai = serai().await; + let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); + assert_eq!( + serai.get_validator_set_musig_key(set).await.unwrap().unwrap(), + musig_key::(&[public_key]).unwrap().to_bytes() + ); + + let secret_key = ::read_F::<&[u8]>( + &mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(), + ) + .unwrap(); + assert_eq!(Ristretto::generator() * secret_key, public_key); + let threshold_keys = musig::(&Zeroizing::new(secret_key), &[public_key]).unwrap(); + assert_eq!( + serai.get_validator_set_musig_key(set).await.unwrap().unwrap(), + threshold_keys.group_key().to_bytes() + ); + + let sig = frost::tests::sign_without_caching( + &mut OsRng, + frost::tests::algorithm_machines( + &mut OsRng, + Schnorrkel::new(b"substrate"), + &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), + ), + &key_pair.encode(), + ); // Vote in a key pair - let address = SeraiAddress::from(pair.public()); - let block = publish_tx( - &serai - .sign( - &PairSigner::new(pair), - &Serai::vote(set.network, key_pair.clone()), - serai.get_nonce(&address).await.unwrap(), - BaseExtrinsicParamsBuilder::new(), - ) - .unwrap(), - ) + let block = publish_tx(&Serai::set_validator_set_keys( + set.network, + key_pair.clone(), + Signature(sig.to_bytes()), + )) .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() }] diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index 17385a83..6e4acb9b 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -2,6 +2,9 @@ use rand_core::{RngCore, OsRng}; use sp_core::{sr25519::Public, Pair}; +use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; +use frost::dkg::musig::musig_key; + use serai_client::{ primitives::{NETWORKS, NetworkId, insecure_pair_from_name}, validator_sets::{ @@ -12,10 +15,10 @@ use serai_client::{ }; mod common; -use common::{serai, validator_sets::vote_in_keys}; +use common::{serai, validator_sets::set_validator_set_keys}; serai_test!( - async fn vote_keys() { + async fn set_validator_set_keys_test() { let network = NetworkId::Bitcoin; let set = ValidatorSet { session: Session(0), network }; @@ -51,14 +54,20 @@ serai_test!( assert_eq!(set_data.network, NETWORKS[&NetworkId::Bitcoin]); let participants_ref: &[_] = set_data.participants.as_ref(); assert_eq!(participants_ref, [(public, set_data.bond)].as_ref()); - - let block = vote_in_keys(set, key_pair.clone()).await; - - // While the vote_in_keys function should handle this, it's beneficial to independently test it assert_eq!( - serai.get_vote_events(block).await.unwrap(), - vec![ValidatorSetsEvent::Vote { voter: public, set, key_pair: key_pair.clone(), votes: 1 }] + serai.get_validator_set_musig_key(set).await.unwrap().unwrap(), + musig_key::(&[::read_G::<&[u8]>( + &mut public.0.as_ref() + ) + .unwrap()]) + .unwrap() + .to_bytes() ); + + let block = set_validator_set_keys(set, key_pair.clone()).await; + + // While the set_validator_set_keys function should handle this, it's beneficial to + // independently test it assert_eq!( serai.get_key_gen_events(block).await.unwrap(), vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index 31038901..a1a0eb41 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -114,7 +114,7 @@ pub mod pallet { // Match to be exhaustive let batch = match call { Call::execute_batch { ref batch } => batch, - _ => Err(InvalidTransaction::Call)?, + Call::__Ignore(_, _) => unreachable!(), }; let network = batch.batch.network; diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 5511a36d..6a19be99 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -174,7 +174,7 @@ impl Contains for CallFilter { } if let RuntimeCall::ValidatorSets(call) = call { - return matches!(call, validator_sets::Call::vote { .. }); + return matches!(call, validator_sets::Call::set_keys { .. }); } false diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index 8b29cdfc..d540a463 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -12,19 +12,28 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +hashbrown = { version = "0.13", default-features = false } + +ciphersuite = { version = "0.3", path = "../../../crypto/ciphersuite", default-features = false, features = ["ristretto"] } +dkg = { version = "0.4", path = "../../../crypto/dkg", default-features = false } + scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false } frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } -serai-primitives = { path = "../..//primitives", default-features = false } +serai-primitives = { path = "../../primitives", default-features = false } validator-sets-primitives = { path = "../primitives", default-features = false } [features] std = [ + "ciphersuite/std", + "dkg/std", + "scale/std", "scale-info/std", diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 6fa11477..39ecc57e 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -5,6 +5,9 @@ pub mod pallet { use scale_info::TypeInfo; + use sp_core::sr25519::{Public, Signature}; + use sp_application_crypto::RuntimePublic; + use frame_system::pallet_prelude::*; use frame_support::pallet_prelude::*; @@ -13,7 +16,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>; } @@ -46,59 +49,57 @@ pub mod pallet { pub type ValidatorSets = StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>; + /// The MuSig key for a validator set. + #[pallet::storage] + #[pallet::getter(fn musig_key)] + pub type MuSigKeys = StorageMap<_, Twox64Concat, ValidatorSet, Public, OptionQuery>; + /// The key pair for a given validator set instance. #[pallet::storage] #[pallet::getter(fn keys)] pub type Keys = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>; - /// If an account has voted for a specific key pair or not. - // This prevents a validator from voting multiple times. - #[pallet::storage] - #[pallet::getter(fn voted)] - pub type Voted = - StorageMap<_, Blake2_128Concat, (T::AccountId, KeyPair), (), OptionQuery>; - - /// How many times a key pair has been voted for. Once consensus is reached, the keys will be - /// adopted. - #[pallet::storage] - #[pallet::getter(fn vote_count)] - pub type VoteCount = - StorageMap<_, Blake2_128Concat, (ValidatorSet, KeyPair), u16, ValueQuery>; - #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - NewSet { - set: ValidatorSet, - }, - Vote { - voter: T::AccountId, - set: ValidatorSet, - key_pair: KeyPair, - // Amount of votes the key now has - votes: u16, - }, - KeyGen { - set: ValidatorSet, - key_pair: KeyPair, - }, + NewSet { set: ValidatorSet }, + KeyGen { set: ValidatorSet, key_pair: KeyPair }, } #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { + use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; + + let hash_set = self.participants.iter().map(|key| key.0).collect::>(); + if hash_set.len() != self.participants.len() + { + panic!("participants contained duplicates"); + } + let mut participants = Vec::new(); + let mut keys = Vec::new(); for participant in self.participants.clone() { + keys.push( + ::read_G::<&[u8]>( + &mut participant.0.as_ref(), + ) + .expect("invalid participant"), + ); participants.push((participant, self.bond)); } let participants = BoundedVec::try_from(participants).unwrap(); for (id, network) in self.networks.clone() { let set = ValidatorSet { session: Session(0), network: id }; + // TODO: Should this be split up? Substrate will read this entire struct into mem on every + // read, not just accessed variables ValidatorSets::::set( set, Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }), ); + + MuSigKeys::::set(set, Some(Public(dkg::musig::musig_key::(&keys).unwrap().to_bytes()))); Pallet::::deposit_event(Event::NewSet { set }) } } @@ -108,63 +109,90 @@ pub mod pallet { pub enum Error { /// Validator Set doesn't exist. NonExistentValidatorSet, - /// Non-validator is voting. - NotValidator, /// Validator Set already generated keys. AlreadyGeneratedKeys, - /// Vvalidator has already voted for these keys. - AlreadyVoted, + /// An invalid MuSig signature was provided. + BadSignature, + } + + impl Pallet { + fn verify_signature( + set: ValidatorSet, + key_pair: &KeyPair, + signature: &Signature, + ) -> Result<(), Error> { + if Keys::::get(set).is_some() { + Err(Error::AlreadyGeneratedKeys)? + } + + let Some(musig_key) = MuSigKeys::::get(set) else { + Err(Error::NonExistentValidatorSet)? + }; + if !musig_key.verify(&key_pair.encode(), signature) { + Err(Error::BadSignature)?; + } + + Ok(()) + } } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(0)] // TODO - pub fn vote(origin: OriginFor, network: NetworkId, key_pair: KeyPair) -> DispatchResult { - let signer = ensure_signed(origin)?; - // TODO: Do we need to check the key is within the length bounds? - // The docs suggest the BoundedVec will create/write, yet not read, which could be an issue - // if it can be passed in + pub fn set_keys( + origin: OriginFor, + network: NetworkId, + key_pair: KeyPair, + signature: Signature, + ) -> DispatchResult { + ensure_none(origin)?; // TODO: Get session let session: Session = Session(0); // Confirm a key hasn't been set for this set instance let set = ValidatorSet { session, network }; - if Keys::::get(set).is_some() { - Err(Error::::AlreadyGeneratedKeys)?; - } + Self::verify_signature(set, &key_pair, &signature)?; - // 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) { - Err(Error::::NotValidator)?; - } - - // Confirm this signer hasn't already voted for these keys - if Voted::::get((&signer, &key_pair)).is_some() { - Err(Error::::AlreadyVoted)?; - } - Voted::::set((&signer, &key_pair), Some(())); - - // Add their vote - let votes = VoteCount::::mutate((set, &key_pair), |value| { - *value += 1; - *value - }); - - Self::deposit_event(Event::Vote { voter: signer, set, key_pair: key_pair.clone(), votes }); - - // If we've reached consensus, set the key - if usize::try_from(votes).unwrap() == data.participants.len() { - Keys::::set(set, Some(key_pair.clone())); - Self::deposit_event(Event::KeyGen { set, key_pair }); - } + Keys::::set(set, Some(key_pair.clone())); + Self::deposit_event(Event::KeyGen { set, key_pair }); Ok(()) } } + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { + // Match to be exhaustive + let (network, key_pair, signature) = match call { + Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature), + Call::__Ignore(_, _) => unreachable!(), + }; + + // TODO: Get the latest session + let session = Session(0); + + let set = ValidatorSet { session, network: *network }; + match Self::verify_signature(set, key_pair, signature) { + Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?, + Err(Error::NonExistentValidatorSet) | Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?, + Err(Error::__Ignore(_, _)) => unreachable!(), + Ok(()) => (), + } + + ValidTransaction::with_tag_prefix("validator-sets") + .and_provides(set) + // Set a 10 block longevity, though this should be included in the next block + .longevity(10) + .propagate(true) + .build() + } + } + // TODO: Support session rotation }