Make Validator Set Network a first-class property

There already should only be one validator set operating per network. This
formalizes that. Then, validator sets used to be able to operate over multiple
networks. That is no longer possible.

This formalization increases validator set flexibility while also allowing the
ability to formalize the definiton of tokens (which is necessary to define a
gas asset).
This commit is contained in:
Luke Parker 2023-03-25 01:30:53 -04:00
parent 397d79040c
commit 6a981dae6e
No known key found for this signature in database
10 changed files with 136 additions and 104 deletions

1
Cargo.lock generated
View file

@ -8514,6 +8514,7 @@ dependencies = [
name = "serai-primitives"
version = "0.1.0"
dependencies = [
"lazy_static",
"parity-scale-codec",
"scale-info",
"serde",

View file

@ -6,15 +6,16 @@ These are the list of types used to represent various properties within the
protocol.
| Alias | Type |
|------------------------|----------------------------------------------|
|-----------------|----------------------------------------------|
| SeraiAddress | sr25519::Public (unchecked [u8; 32] wrapper) |
| Amount | u64 |
| NetworkId | u16 |
| Coin | u32 |
| Network | Vec<Coin> |
| Session | u32 |
| Validator Set Index | u16 |
| Validator Set Instance | (Session, Validator Set Index) |
| Validator Set | (Session, NetworkId) |
| Key | BoundedVec\<u8, 96> |
| ExternalAddress | BoundedVec\<u8, 74> |
| ExternalAddress | BoundedVec\<u8, 128> |
| Data | BoundedVec\<u8, 512> |
### Networks
@ -25,13 +26,11 @@ being isolated, the generated keys are further bound to their respective
networks via an additive offset created by hashing the network's name (among
other properties). The network's key is used for all coins on that network.
Networks are not acknowledged by the Serai network, solely by the processor.
| Network | Curve |
|----------|-----------|
| Bitcoin | Secp256k1 |
| Ethereum | Secp256k1 |
| Monero | Ed25519 |
| Network | Curve | ID |
|----------|-----------|----|
| Bitcoin | Secp256k1 | 0 |
| Ethereum | Secp256k1 | 1 |
| Monero | Ed25519 | 2 |
### Coins

View file

@ -3,29 +3,17 @@
Validator Sets are defined at the protocol level, with the following parameters:
- `bond` (Amount): Amount of bond per key-share.
- `coins` (Vec\<Coin>): List of coins within this set.
- `participants` (Vec\<Coin>): List of participants within this set.
- `network` (Network): The network this validator set operates
over.
- `participants` (Vec\<SeraiAddress>): List of participants within this set.
Validator Sets are referred to by `ValidatorSetIndex` yet have their data
accessible via `ValidatorSetInstance`.
Validator Sets are referred to by `NetworkId` yet have their data accessible via
`ValidatorSetInstance`.
At launch, there will solely be Validator Set 0, managing Bitcoin, Ether, DAI,
and Monero.
### Participation in consensus
### Participation in the BFT process
All Validator Sets participate in the BFT process described under
[Consensus](./Consensus.md). Specifically, a block containing In Instructions
for a coin must be approved by the BFT majority of the Validator Set responsible
for it, along with the BFT majority of the network by bond.
At this time, In Instructions for a coin are only expected to be included when a
validator from the Validator Set managing the coin is the producer of the block
in question.
Since there is currently only one Validator Set, the aforementioned BFT
conditions collapse to simply the BFT majority by bond. Ensuring BFT majority
per responsible Validator Set is accordingly unimplemented for now.
All Validator Sets participate in consensus. In the future, a dedicated group
to order Serai is planned.
### Multisig

View file

@ -30,7 +30,7 @@ sp-consensus = { git = "https://github.com/serai-dex/substrate" }
frame-benchmarking = { git = "https://github.com/serai-dex/substrate" }
frame-benchmarking-cli = { git = "https://github.com/serai-dex/substrate" }
serai-runtime = { path = "../runtime" }
serai-runtime = { path = "../runtime", features = ["std"] }
sc-transaction-pool = { git = "https://github.com/serai-dex/substrate" }
sc-transaction-pool-api = { git = "https://github.com/serai-dex/substrate" }

View file

@ -50,7 +50,11 @@ fn testnet_genesis(
session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() },
validator_sets: ValidatorSetsConfig {
bond: Amount(1_000_000 * 10_u64.pow(8)),
coins: vec![BITCOIN, ETHER, DAI, MONERO],
networks: vec![
(BITCOIN_NET_ID, BITCOIN_NET.clone()),
(ETHEREUM_NET_ID, ETHEREUM_NET.clone()),
(MONERO_NET_ID, MONERO_NET.clone()),
],
participants: validators.iter().map(|name| account_from_name(name)).collect(),
},
}

View file

@ -12,6 +12,8 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
lazy_static = { version = "1", optional = true }
zeroize = { version = "^1.5", features = ["derive"], optional = true }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
@ -23,5 +25,5 @@ sp-core = { git = "https://github.com/serai-dex/substrate", default-features = f
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
[features]
std = ["zeroize", "scale/std", "scale-info/std", "serde", "sp-core/std", "sp-runtime/std"]
std = ["lazy_static", "zeroize", "scale/std", "scale-info/std", "serde", "sp-core/std", "sp-runtime/std"]
default = ["std"]

View file

@ -4,9 +4,25 @@ use zeroize::Zeroize;
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_core::{ConstU32, bounded::BoundedVec};
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
/// The type used to identify networks.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Zeroize, Serialize, Deserialize))]
pub struct NetworkId(pub u16);
impl From<u16> for NetworkId {
fn from(network: u16) -> NetworkId {
NetworkId(network)
}
}
pub const BITCOIN_NET_ID: NetworkId = NetworkId(0);
pub const ETHEREUM_NET_ID: NetworkId = NetworkId(1);
pub const MONERO_NET_ID: NetworkId = NetworkId(2);
/// The type used to identify coins.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Zeroize, Serialize, Deserialize))]
@ -22,3 +38,49 @@ pub const BITCOIN: Coin = Coin(1);
pub const ETHER: Coin = Coin(2);
pub const DAI: Coin = Coin(3);
pub const MONERO: Coin = Coin(4);
// Max of 8 coins per network
// Since Serai isn't interested in listing tokens, as on-chain DEXs will almost certainly have
// more liquidity, the only reason we'd have so many coins from a network is if there's no DEX
// on-chain
// There's probably no chain with so many *worthwhile* coins and no on-chain DEX
// This could probably be just 4, yet 8 is a hedge for the unforseen
// If necessary, this can be increased with a fork
pub const MAX_COINS_PER_NETWORK: u32 = 8;
/// Network definition.
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct Network {
coins: BoundedVec<Coin, ConstU32<{ MAX_COINS_PER_NETWORK }>>,
}
#[cfg(feature = "std")]
impl Zeroize for Network {
fn zeroize(&mut self) {
for coin in self.coins.as_mut() {
coin.zeroize();
}
self.coins.truncate(0);
}
}
impl Network {
#[cfg(feature = "std")]
pub fn new(coins: Vec<Coin>) -> Result<Network, &'static str> {
Ok(Network {
coins: coins.try_into().map_err(|_| "coins length exceeds {MAX_COINS_PER_NETWORK}")?,
})
}
pub fn coins(&self) -> &[Coin] {
&self.coins
}
}
#[cfg(feature = "std")]
lazy_static::lazy_static! {
pub static ref BITCOIN_NET: Network = Network::new(vec![BITCOIN]).unwrap();
pub static ref ETHEREUM_NET: Network = Network::new(vec![ETHER, DAI]).unwrap();
pub static ref MONERO_NET: Network = Network::new(vec![MONERO]).unwrap();
}

View file

@ -20,44 +20,31 @@ pub mod pallet {
#[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
/// Bond requirement to join the initial validator set.
/// Bond requirement to join the initial validator sets.
/// Every participant at genesis will automatically be assumed to have this much bond.
/// This bond cannot be withdrawn however as there's no stake behind it.
pub bond: Amount,
/// Coins to spawn the network with in the initial validator set.
pub coins: Vec<Coin>,
/// List of participants to place in the genesis set.
/// Networks to spawn Serai with.
pub networks: Vec<(NetworkId, Network)>,
/// List of participants to place in the initial validator sets.
pub participants: Vec<T::AccountId>,
}
#[cfg(feature = "std")]
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { bond: Amount(1), coins: vec![], participants: vec![] }
GenesisConfig { bond: Amount(1), networks: vec![], participants: vec![] }
}
}
// Max of 16 coins per validator set
// At launch, we'll have BTC, ETH, DAI, and XMR
// In the future, these will be split into separate validator sets, so we're already not
// planning expansion beyond just a few coins per validator set
// The only case which really makes sense for multiple coins in a validator set is:
// 1) The coins are small, easy to run, and make no sense to be in their own set
// In this case, it's still hard to ask validators to run 16 different nodes
// 2) The coins are all on the same network yet there's no DEX on-chain
// In these cases, it'd be hard to find and justify 16 different coins from that single chain
// This could probably be just 8, yet 16 is a hedge for the unforseen
// If necessary, this can be increased with a fork
type MaxCoinsPerSet = ConstU32<16>;
// Support keys up to 96 bytes (BLS12-381 G2)
const MAX_KEY_LEN: u32 = 96;
type MaxKeyLen = ConstU32<MAX_KEY_LEN>;
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct ValidatorSet<T: Config> {
pub struct ValidatorSetData<T: Config> {
bond: Amount,
coins: BoundedVec<Coin, MaxCoinsPerSet>,
network: Network,
// Participant and their amount bonded to this set
// Limit each set to 100 participants for now
@ -72,15 +59,14 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn validator_set)]
pub type ValidatorSets<T: Config> =
StorageMap<_, Twox64Concat, ValidatorSetInstance, ValidatorSet<T>, OptionQuery>;
StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData<T>, OptionQuery>;
type Key = BoundedVec<u8, MaxKeyLen>;
/// The key for a given validator set instance coin.
/// The key for a given validator set instance.
#[pallet::storage]
#[pallet::getter(fn key)]
pub type Keys<T: Config> =
StorageMap<_, Twox64Concat, (ValidatorSetInstance, Coin), Key, OptionQuery>;
pub type Keys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, Key, OptionQuery>;
/// If an account has voted for a specific key or not. Prevents them from voting multiple times.
#[pallet::storage]
@ -91,7 +77,7 @@ pub mod pallet {
#[pallet::storage]
#[pallet::getter(fn vote_count)]
pub type VoteCount<T: Config> =
StorageMap<_, Blake2_128Concat, (ValidatorSetInstance, Coin, Key), u16, ValueQuery>;
StorageMap<_, Blake2_128Concat, (ValidatorSet, Key), u16, ValueQuery>;
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
@ -100,32 +86,29 @@ pub mod pallet {
for participant in self.participants.clone() {
participants.push((participant, self.bond));
}
let participants = BoundedVec::try_from(participants).unwrap();
for (id, network) in self.networks.clone() {
ValidatorSets::<T>::set(
ValidatorSetInstance { session: Session(0), index: ValidatorSetIndex(0) },
Some(ValidatorSet {
bond: self.bond,
coins: BoundedVec::try_from(self.coins.clone()).unwrap(),
participants: BoundedVec::try_from(participants).unwrap(),
}),
ValidatorSet { session: Session(0), network: id },
Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }),
);
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Vote {
voter: T::AccountId,
instance: ValidatorSetInstance,
coin: Coin,
set: ValidatorSet,
key: Key,
// Amount of votes the key now has
votes: u16,
},
KeyGen {
instance: ValidatorSetInstance,
coin: Coin,
set: ValidatorSet,
key: Key,
},
}
@ -146,12 +129,7 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)] // TODO
pub fn vote(
origin: OriginFor<T>,
index: ValidatorSetIndex,
coin: Coin,
key: Key,
) -> DispatchResult {
pub fn vote(origin: OriginFor<T>, network: NetworkId, key: Key) -> 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
@ -161,15 +139,14 @@ pub mod pallet {
let session: Session = Session(0);
// Confirm a key hasn't been set for this set instance
let instance = ValidatorSetInstance { session, index };
if Keys::<T>::get((instance, coin)).is_some() {
let set = ValidatorSet { session, network };
if Keys::<T>::get(set).is_some() {
Err(Error::<T>::AlreadyGeneratedKeys)?;
}
// Confirm the signer is a validator in the set
let set = ValidatorSets::<T>::get(instance).ok_or(Error::<T>::NonExistentValidatorSet)?;
if set.participants.iter().any(|participant| participant.0 == signer) {
let data = ValidatorSets::<T>::get(set).ok_or(Error::<T>::NonExistentValidatorSet)?;
if data.participants.iter().any(|participant| participant.0 == signer) {
Err(Error::<T>::NotValidator)?;
}
@ -180,17 +157,17 @@ pub mod pallet {
Voted::<T>::set((&signer, &key), Some(()));
// Add their vote
let votes = VoteCount::<T>::mutate((instance, coin, &key), |value| {
let votes = VoteCount::<T>::mutate((set, &key), |value| {
*value += 1;
*value
});
Self::deposit_event(Event::Vote { voter: signer, instance, coin, key: key.clone(), votes });
Self::deposit_event(Event::Vote { voter: signer, set, key: key.clone(), votes });
// If we've reached consensus, set the key
if usize::try_from(votes).unwrap() == set.participants.len() {
Keys::<T>::set((instance, coin), Some(key.clone()));
Self::deposit_event(Event::KeyGen { instance, coin, key });
if usize::try_from(votes).unwrap() == data.participants.len() {
Keys::<T>::set(set, Some(key.clone()));
Self::deposit_event(Event::KeyGen { set, key });
}
Ok(())

View file

@ -19,6 +19,8 @@ scale-info = { version = "2", default-features = false, features = ["derive"] }
serde = { version = "1", features = ["derive"], optional = true }
serai-primitives = { path = "../../serai/primitives", default-features = false }
[features]
std = ["zeroize", "scale/std", "scale-info/std", "serde"]
std = ["zeroize", "scale/std", "scale-info/std", "serde", "serai-primitives/std"]
default = ["std"]

View file

@ -9,20 +9,17 @@ use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use serai_primitives::NetworkId;
/// The type used to identify a specific session of validators.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Zeroize, Serialize, Deserialize))]
pub struct Session(pub u32);
/// The type used to identify a validator set.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Zeroize, Serialize, Deserialize))]
pub struct ValidatorSetIndex(pub u16);
/// The type used to identify a specific validator set during a specific session.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
#[cfg_attr(feature = "std", derive(Zeroize, Serialize, Deserialize))]
pub struct ValidatorSetInstance {
pub struct ValidatorSet {
pub session: Session,
pub index: ValidatorSetIndex,
pub network: NetworkId,
}