From fc51c9b71c10e807c8b3838dbd850d8f6d397034 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 3 Aug 2024 01:54:57 -0400 Subject: [PATCH] Add embedded elliptic curve keys to Substrate --- Cargo.lock | 4 + substrate/abi/src/validator_sets.rs | 4 + substrate/client/src/serai/validator_sets.rs | 17 ++++- .../client/tests/common/validator_sets.rs | 18 +++++ substrate/client/tests/validator_sets.rs | 21 +++++- substrate/node/Cargo.toml | 4 + substrate/node/src/chain_spec.rs | 50 ++++++++++--- substrate/primitives/src/networks.rs | 24 ++++++ substrate/runtime/src/abi.rs | 13 ++++ substrate/validator-sets/pallet/Cargo.toml | 2 + substrate/validator-sets/pallet/src/lib.rs | 73 +++++++++++++++++-- 11 files changed, 210 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40987a47..4e7542b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8350,7 +8350,9 @@ dependencies = [ name = "serai-node" version = "0.1.0" dependencies = [ + "ciphersuite", "clap", + "embedwards25519", "frame-benchmarking", "futures-util", "hex", @@ -8376,6 +8378,7 @@ dependencies = [ "sc-transaction-pool", "sc-transaction-pool-api", "schnorrkel", + "secq256k1", "serai-env", "serai-runtime", "sp-api", @@ -8603,6 +8606,7 @@ dependencies = [ "serai-dex-pallet", "serai-primitives", "serai-validator-sets-primitives", + "serde", "sp-application-crypto", "sp-core", "sp-io", diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index 1e1e3359..4552cb8d 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -15,6 +15,10 @@ pub enum Call { key_pair: KeyPair, signature: Signature, }, + set_embedded_elliptic_curve_key { + embedded_elliptic_curve: EmbeddedEllipticCurve, + key: BoundedVec>, + }, report_slashes { network: NetworkId, slashes: BoundedVec<(SeraiAddress, u32), ConstU32<{ MAX_KEY_SHARES_PER_SET / 3 }>>, diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 959f8ee6..c3d881bd 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -1,13 +1,14 @@ use scale::Encode; use sp_core::sr25519::{Public, Signature}; +use sp_runtime::BoundedVec; use serai_abi::primitives::Amount; pub use serai_abi::validator_sets::primitives; -use primitives::{Session, ValidatorSet, KeyPair}; +use primitives::{MAX_KEY_LEN, Session, ValidatorSet, KeyPair}; use crate::{ - primitives::{NetworkId, SeraiAddress}, + primitives::{EmbeddedEllipticCurve, NetworkId, SeraiAddress}, Transaction, Serai, TemporalSerai, SeraiError, }; @@ -195,6 +196,18 @@ impl<'a> SeraiValidatorSets<'a> { })) } + pub fn set_embedded_elliptic_curve_key( + embedded_elliptic_curve: EmbeddedEllipticCurve, + key: BoundedVec>, + ) -> serai_abi::Call { + serai_abi::Call::ValidatorSets( + serai_abi::validator_sets::Call::set_embedded_elliptic_curve_key { + embedded_elliptic_curve, + key, + }, + ) + } + pub fn allocate(network: NetworkId, amount: Amount) -> serai_abi::Call { serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::allocate { network, amount }) } diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs index 3238501a..9bc8454f 100644 --- a/substrate/client/tests/common/validator_sets.rs +++ b/substrate/client/tests/common/validator_sets.rs @@ -82,6 +82,24 @@ pub async fn set_keys( block } +#[allow(dead_code)] +pub async fn set_embedded_elliptic_curve_key( + serai: &Serai, + pair: &Pair, + embedded_elliptic_curve: EmbeddedEllipticCurve, + key: BoundedVec>, + nonce: u32, +) -> [u8; 32] { + // get the call + let tx = serai.sign( + pair, + SeraiValidatorSets::set_embedded_elliptic_curve_key(embedded_elliptic_curve, key), + nonce, + 0, + ); + publish_tx(serai, &tx).await +} + #[allow(dead_code)] pub async fn allocate_stake( serai: &Serai, diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index 8aa8174f..9b092afd 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -221,12 +221,31 @@ async fn validator_set_rotation() { // add 1 participant let last_participant = accounts[4].clone(); + + // If this is the first iteration, set embedded elliptic curve keys + if i == 0 { + for (i, embedded_elliptic_curve) in + [EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1] + .into_iter() + .enumerate() + { + set_embedded_elliptic_curve_key( + &serai, + embedded_elliptic_curve, + vec![0; 32].try_into().unwrap(), + &last_participant, + i.try_into().unwrap(), + ) + .await; + } + } + let hash = allocate_stake( &serai, network, key_shares[&network], &last_participant, - i.try_into().unwrap(), + (2 + i).try_into().unwrap(), ) .await; participants.push(last_participant.public()); diff --git a/substrate/node/Cargo.toml b/substrate/node/Cargo.toml index 0e551c72..5da8ce85 100644 --- a/substrate/node/Cargo.toml +++ b/substrate/node/Cargo.toml @@ -27,6 +27,10 @@ log = "0.4" schnorrkel = "0.11" +ciphersuite = { path = "../../crypto/ciphersuite" } +embedwards25519 = { path = "../../crypto/evrf/embedwards25519" } +secq256k1 = { path = "../../crypto/evrf/secq256k1" } + libp2p = "0.52" sp-core = { git = "https://github.com/serai-dex/substrate" } diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index e66ee4a6..f7e83465 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -1,13 +1,20 @@ use core::marker::PhantomData; -use std::collections::HashSet; -use sp_core::{Decode, Pair as PairTrait, sr25519::Public}; +use sp_core::Pair as PairTrait; use sc_service::ChainType; +use ciphersuite::{ + group::{ff::PrimeField, GroupEncoding}, + Ciphersuite, +}; +use embedwards25519::Embedwards25519; +use secq256k1::Secq256k1; + use serai_runtime::{ - primitives::*, WASM_BINARY, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig, - CoinsConfig, DexConfig, ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, + primitives::*, validator_sets::AllEmbeddedEllipticCurveKeysAtGenesis, WASM_BINARY, + BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig, CoinsConfig, DexConfig, + ValidatorSetsConfig, SignalsConfig, BabeConfig, GrandpaConfig, }; pub type ChainSpec = sc_service::GenericChainSpec; @@ -16,6 +23,15 @@ fn account_from_name(name: &'static str) -> PublicKey { insecure_pair_from_name(name).public() } +// Panics on names which are too long, or ciphersuites with weirdly encoded scalars +fn insecure_ciphersuite_key_from_name(name: &'static str) -> Vec { + let mut repr = ::Repr::default(); + let repr_len = repr.as_ref().len(); + let start = (repr_len / 2) - (name.len() / 2); + repr.as_mut()[start .. (start + name.len())].copy_from_slice(name.as_bytes()); + (C::generator() * C::F::from_repr(repr).unwrap()).to_bytes().as_ref().to_vec() +} + fn wasm_binary() -> Vec { // TODO: Accept a config of runtime path const WASM_PATH: &str = "/runtime/serai.wasm"; @@ -32,7 +48,21 @@ fn devnet_genesis( validators: &[&'static str], endowed_accounts: Vec, ) -> RuntimeGenesisConfig { - let validators = validators.iter().map(|name| account_from_name(name)).collect::>(); + let validators = validators + .iter() + .map(|name| { + ( + account_from_name(name), + AllEmbeddedEllipticCurveKeysAtGenesis { + embedwards25519: insecure_ciphersuite_key_from_name::(name) + .try_into() + .unwrap(), + secq256k1: insecure_ciphersuite_key_from_name::(name).try_into().unwrap(), + }, + ) + }) + .collect::>(); + RuntimeGenesisConfig { system: SystemConfig { code: wasm_binary.to_vec(), _config: PhantomData }, @@ -65,17 +95,18 @@ fn devnet_genesis( }, signals: SignalsConfig::default(), babe: BabeConfig { - authorities: validators.iter().map(|validator| ((*validator).into(), 1)).collect(), + authorities: validators.iter().map(|validator| (validator.0.into(), 1)).collect(), epoch_config: Some(BABE_GENESIS_EPOCH_CONFIG), _config: PhantomData, }, grandpa: GrandpaConfig { - authorities: validators.into_iter().map(|validator| (validator.into(), 1)).collect(), + authorities: validators.into_iter().map(|validator| (validator.0.into(), 1)).collect(), _config: PhantomData, }, } } +/* fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> RuntimeGenesisConfig { let validators = validators .into_iter() @@ -126,6 +157,7 @@ fn testnet_genesis(wasm_binary: &[u8], validators: Vec<&'static str>) -> Runtime }, } } +*/ pub fn development_config() -> ChainSpec { let wasm_binary = wasm_binary(); @@ -204,7 +236,7 @@ pub fn local_config() -> ChainSpec { } pub fn testnet_config() -> ChainSpec { - let wasm_binary = wasm_binary(); + // let wasm_binary = wasm_binary(); ChainSpec::from_genesis( // Name @@ -213,7 +245,7 @@ pub fn testnet_config() -> ChainSpec { "testnet-2", ChainType::Live, move || { - let _ = testnet_genesis(&wasm_binary, vec![]); + // let _ = testnet_genesis(&wasm_binary, vec![]) todo!() }, // Bootnodes diff --git a/substrate/primitives/src/networks.rs b/substrate/primitives/src/networks.rs index fd713ca1..0f30d8de 100644 --- a/substrate/primitives/src/networks.rs +++ b/substrate/primitives/src/networks.rs @@ -14,6 +14,16 @@ use sp_core::{ConstU32, bounded::BoundedVec}; #[cfg(feature = "borsh")] use crate::{borsh_serialize_bounded_vec, borsh_deserialize_bounded_vec}; +/// Identifier for an embedded elliptic curve. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Zeroize))] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum EmbeddedEllipticCurve { + Embedwards25519, + Secq256k1, +} + /// The type used to identify networks. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(Zeroize))] @@ -26,6 +36,20 @@ pub enum NetworkId { Monero, } impl NetworkId { + /// The embedded elliptic curve actively used for this network. + pub fn embedded_elliptic_curves(&self) -> &'static [EmbeddedEllipticCurve] { + match self { + // We don't use any embedded elliptic curves for Serai as we don't perform a DKG for Serai + Self::Serai => &[], + // We need to generate a Ristretto key for oraclizing and a Secp256k1 key for the network + Self::Bitcoin | Self::Ethereum => { + &[EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1] + } + // Since the oraclizing key curve is the same as the network's curve, we only need it + Self::Monero => &[EmbeddedEllipticCurve::Embedwards25519], + } + } + pub fn coins(&self) -> &'static [Coin] { match self { Self::Serai => &[Coin::Serai], diff --git a/substrate/runtime/src/abi.rs b/substrate/runtime/src/abi.rs index b479036d..6f8917ae 100644 --- a/substrate/runtime/src/abi.rs +++ b/substrate/runtime/src/abi.rs @@ -115,6 +115,13 @@ impl From for RuntimeCall { key_pair, signature, }), + serai_abi::validator_sets::Call::set_embedded_elliptic_curve_key { + embedded_elliptic_curve, + key, + } => RuntimeCall::ValidatorSets(validator_sets::Call::set_embedded_elliptic_curve_key { + embedded_elliptic_curve, + key, + }), serai_abi::validator_sets::Call::report_slashes { network, slashes, signature } => { RuntimeCall::ValidatorSets(validator_sets::Call::report_slashes { network, @@ -293,6 +300,12 @@ impl TryInto for RuntimeCall { signature, } } + validator_sets::Call::set_embedded_elliptic_curve_key { embedded_elliptic_curve, key } => { + serai_abi::validator_sets::Call::set_embedded_elliptic_curve_key { + embedded_elliptic_curve, + key, + } + } validator_sets::Call::report_slashes { network, slashes, signature } => { serai_abi::validator_sets::Call::report_slashes { network, diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index dd67d1bc..aff27e3e 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -24,6 +24,8 @@ hashbrown = { version = "0.14", default-features = false, features = ["ahash", " scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc"] } + sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index b89d6596..d7917ff6 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -81,6 +81,12 @@ pub mod pallet { type ShouldEndSession: ShouldEndSession>; } + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, serde::Serialize, serde::Deserialize)] + pub struct AllEmbeddedEllipticCurveKeysAtGenesis { + pub embedwards25519: BoundedVec>, + pub secq256k1: BoundedVec>, + } + #[pallet::genesis_config] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct GenesisConfig { @@ -90,7 +96,7 @@ pub mod pallet { /// This stake cannot be withdrawn however as there's no actual stake behind it. pub networks: Vec<(NetworkId, Amount)>, /// List of participants to place in the initial validator sets. - pub participants: Vec, + pub participants: Vec<(T::AccountId, AllEmbeddedEllipticCurveKeysAtGenesis)>, } impl Default for GenesisConfig { @@ -189,6 +195,18 @@ pub mod pallet { } } + /// A key on an embedded elliptic curve. + #[pallet::storage] + pub type EmbeddedEllipticCurveKeys = StorageDoubleMap< + _, + Blake2_128Concat, + Public, + Identity, + EmbeddedEllipticCurve, + BoundedVec>, + OptionQuery, + >; + /// The total stake allocated to this network by the active set of validators. #[pallet::storage] #[pallet::getter(fn total_allocated_stake)] @@ -398,6 +416,9 @@ pub mod pallet { pub enum Error { /// Validator Set doesn't exist. NonExistentValidatorSet, + /// Trying to perform an operation requiring an embedded elliptic curve key, without an + /// embedded elliptic curve key. + MissingEmbeddedEllipticCurveKey, /// Not enough allocation to obtain a key share in the set. InsufficientAllocation, /// Trying to deallocate more than allocated. @@ -441,10 +462,20 @@ pub mod pallet { fn build(&self) { for (id, stake) in self.networks.clone() { AllocationPerKeyShare::::set(id, Some(stake)); - for participant in self.participants.clone() { - if Pallet::::set_allocation(id, participant, stake) { + for participant in &self.participants { + if Pallet::::set_allocation(id, participant.0, stake) { panic!("participants contained duplicates"); } + EmbeddedEllipticCurveKeys::::set( + participant.0, + EmbeddedEllipticCurve::Embedwards25519, + Some(participant.1.embedwards25519.clone()), + ); + EmbeddedEllipticCurveKeys::::set( + participant.0, + EmbeddedEllipticCurve::Secq256k1, + Some(participant.1.secq256k1.clone()), + ); } Pallet::::new_set(id); } @@ -959,8 +990,33 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight(0)] // TODO + pub fn set_embedded_elliptic_curve_key( + origin: OriginFor, + embedded_elliptic_curve: EmbeddedEllipticCurve, + key: BoundedVec>, + ) -> DispatchResult { + let validator = ensure_signed(origin)?; + // This does allow overwriting an existing key which... is unlikely to be done? + // Yet it isn't an issue as we'll fix to the key as of any set's declaration (uncaring to if + // it's distinct at the latest block) + EmbeddedEllipticCurveKeys::::set(validator, embedded_elliptic_curve, Some(key)); + Ok(()) + } + + #[pallet::call_index(3)] + #[pallet::weight(0)] // TODO pub fn allocate(origin: OriginFor, network: NetworkId, amount: Amount) -> DispatchResult { let validator = ensure_signed(origin)?; + // If this network utilizes an embedded elliptic curve, require the validator to have set the + // appropriate key + for embedded_elliptic_curve in network.embedded_elliptic_curves() { + // Require an Embedwards25519 embedded curve key and a key for the curve for this network + // The Embedwards25519 embedded curve key is required for the DKG for the Substrate key + // used to oraclize events with + if !EmbeddedEllipticCurveKeys::::contains_key(validator, *embedded_elliptic_curve) { + Err(Error::::MissingEmbeddedEllipticCurveKey)?; + } + } Coins::::transfer_internal( validator, Self::account(), @@ -969,7 +1025,7 @@ pub mod pallet { Self::increase_allocation(network, validator, amount) } - #[pallet::call_index(3)] + #[pallet::call_index(4)] #[pallet::weight(0)] // TODO pub fn deallocate(origin: OriginFor, network: NetworkId, amount: Amount) -> DispatchResult { let account = ensure_signed(origin)?; @@ -986,7 +1042,7 @@ pub mod pallet { Ok(()) } - #[pallet::call_index(4)] + #[pallet::call_index(5)] #[pallet::weight((0, DispatchClass::Operational))] // TODO pub fn claim_deallocation( origin: OriginFor, @@ -1114,9 +1170,10 @@ pub mod pallet { .propagate(true) .build() } - Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => { - Err(InvalidTransaction::Call)? - } + Call::set_embedded_elliptic_curve_key { .. } | + Call::allocate { .. } | + Call::deallocate { .. } | + Call::claim_deallocation { .. } => Err(InvalidTransaction::Call)?, Call::__Ignore(_, _) => unreachable!(), } }