From 3da57cd691f8071b372b5c17633bf79c803f1950 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Wed, 11 Oct 2023 17:29:12 +0200 Subject: [PATCH] Add blockchain crate defining basic emulated blockchain structure (#9) The big missing parat are rewards and slashing. For rewards we probably need to keep track who generated signatures (including valid signatures submitted within some period after quorum has been reached). With slashing there are many questions to answer: do we and if so how do we remove slashed validator from validators set. --- common/blockchain/Cargo.toml | 19 + common/blockchain/src/block.rs | 243 ++++++++++++ common/blockchain/src/candidates.rs | 376 +++++++++++++++++++ common/blockchain/src/candidates/tests.rs | 432 ++++++++++++++++++++++ common/blockchain/src/chain.rs | 74 ++++ common/blockchain/src/common.rs | 77 ++++ common/blockchain/src/epoch.rs | 120 ++++++ common/blockchain/src/height.rs | 126 +++++++ common/blockchain/src/lib.rs | 14 + common/blockchain/src/manager.rs | 249 +++++++++++++ common/blockchain/src/validators.rs | 140 +++++++ common/lib/Cargo.toml | 2 + common/lib/src/hash.rs | 29 ++ common/lib/src/lib.rs | 1 + 14 files changed, 1902 insertions(+) create mode 100644 common/blockchain/Cargo.toml create mode 100644 common/blockchain/src/block.rs create mode 100644 common/blockchain/src/candidates.rs create mode 100644 common/blockchain/src/candidates/tests.rs create mode 100644 common/blockchain/src/chain.rs create mode 100644 common/blockchain/src/common.rs create mode 100644 common/blockchain/src/epoch.rs create mode 100644 common/blockchain/src/height.rs create mode 100644 common/blockchain/src/lib.rs create mode 100644 common/blockchain/src/manager.rs create mode 100644 common/blockchain/src/validators.rs diff --git a/common/blockchain/Cargo.toml b/common/blockchain/Cargo.toml new file mode 100644 index 00000000..b3233a9c --- /dev/null +++ b/common/blockchain/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "blockchain" +authors = ["Michal Nazarewicz "] +version = "0.0.0" +edition = "2021" + +[dependencies] +borsh.workspace = true +derive_more.workspace = true + +lib = { workspace = true, features = ["borsh"] } + +[dev-dependencies] +lib = { workspace = true, features = ["borsh", "test_utils"] } +rand.workspace = true +stdx.workspace = true + +[features] +std = [] diff --git a/common/blockchain/src/block.rs b/common/blockchain/src/block.rs new file mode 100644 index 00000000..2b11aa48 --- /dev/null +++ b/common/blockchain/src/block.rs @@ -0,0 +1,243 @@ +use lib::hash::CryptoHash; + +use crate::epoch; +use crate::height::{BlockHeight, HostHeight}; +use crate::validators::PubKey; + +type Result = core::result::Result; + +/// A single block of the emulated blockchain. +/// +/// Emulated block’s height and timestamp are taken directly from the host +/// chain. Emulated blocks don’t have their own timestamps. +/// +/// A block is uniquely identified by its hash which can be obtained via +/// [`Block::calc_hash`]. +/// +/// Each block belongs to an epoch (identifier by `epoch_id`) which describes +/// set of validators which can sign the block. A new epoch is introduced by +/// setting `next_epoch` field; epoch becomes current one starting from the +/// following block. +#[derive( + Clone, Debug, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize, +)] +pub struct Block { + /// Version of the structure. At the moment always zero byte. + version: crate::common::VersionZero, + + /// Hash of the previous block. + pub prev_block_hash: CryptoHash, + /// Height of the emulated blockchain’s block. + pub block_height: BlockHeight, + /// Height of the host blockchain’s block in which this block was created. + pub host_height: HostHeight, + /// Timestamp of the host blockchani’s block in which this block was created. + pub host_timestamp: u64, + /// Hash of the root node of the state trie, i.e. the commitment + /// of the state. + pub state_root: CryptoHash, + + /// Hash of the block in which current epoch has been defined. + /// + /// Epoch determines validators set signing each block. If epoch is about + /// to change, the new epoch is defined in `next_epoch` field. Then, the + /// very next block will use current’s block hash as `epoch_id`. + pub epoch_id: CryptoHash, + + /// If present, epoch *the next* block will belong to. + pub next_epoch: Option>, +} + +/// Error while generating new block. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GenerateError { + /// Host height went backwards. + BadHostHeight, + /// Host timestamp went backwards. + BadHostTimestamp, +} + +impl Block { + /// Returns whether the block is a valid genesis block. + pub fn is_genesis(&self) -> bool { + self.prev_block_hash == CryptoHash::DEFAULT && + self.epoch_id == CryptoHash::DEFAULT + } + + /// Calculates hash of the block. + pub fn calc_hash(&self) -> CryptoHash { + let mut builder = CryptoHash::builder(); + borsh::to_writer(&mut builder, self).unwrap(); + builder.build() + } + + /// Sign the block using provided signer function. + pub fn sign( + &self, + // TODO(mina86): Consider using signature::Signer. + signer: impl FnOnce(&[u8]) -> Result, + ) -> Result { + borsh::to_vec(self).and_then(|vec| signer(vec.as_slice())) + } + + #[cfg(test)] + fn verify(&self, pk: &PK, signature: &PK::Signature) -> bool { + crate::validators::Signature::verify(signature, &self.calc_hash(), pk) + } + + /// Constructs next block. + /// + /// Returns a new block with `self` as the previous block. Verifies that + /// `host_height` and `host_timestamp` don’t go backwards but otherwise they + /// can increase by any amount. The new block will have `block_height` + /// incremented by one. + pub fn generate_next( + &self, + host_height: HostHeight, + host_timestamp: u64, + state_root: CryptoHash, + next_epoch: Option>, + ) -> Result { + if host_height <= self.host_height { + return Err(GenerateError::BadHostHeight); + } else if host_timestamp <= self.host_timestamp { + return Err(GenerateError::BadHostTimestamp); + } + + let prev_block_hash = self.calc_hash(); + // If self defines a new epoch than the new block starts a new epoch + // with epoch id equal to self’s block hash. Otherwise, epoch doesn’t + // change and the new block uses the same epoch id as self. + let epoch_id = match self.next_epoch.is_some() { + false => self.epoch_id.clone(), + true => prev_block_hash.clone(), + }; + Ok(Self { + version: crate::common::VersionZero, + prev_block_hash, + block_height: self.block_height.next(), + host_height, + host_timestamp, + state_root, + epoch_id, + next_epoch, + }) + } + + /// Constructs a new genesis block. + /// + /// A genesis block is identified by previous block hash and epoch id both + /// being all-zero hash. + pub fn generate_genesis( + block_height: BlockHeight, + host_height: HostHeight, + host_timestamp: u64, + state_root: CryptoHash, + next_epoch: epoch::Epoch, + ) -> Result { + Ok(Self { + version: crate::common::VersionZero, + prev_block_hash: CryptoHash::DEFAULT, + block_height, + host_height, + host_timestamp, + state_root, + epoch_id: CryptoHash::DEFAULT, + next_epoch: Some(next_epoch), + }) + } +} + +#[test] +fn test_block_generation() { + use crate::validators::{MockPubKey, MockSignature}; + + // Generate a genesis block and test it’s behaviour. + let genesis_hash = "Zq3s+b7x6R8tKV1iQtByAWqlDMXVVD9tSDOlmuLH7wI="; + let genesis_hash = CryptoHash::from_base64(genesis_hash).unwrap(); + + let genesis = Block::generate_genesis( + BlockHeight::from(0), + HostHeight::from(42), + 24, + CryptoHash::test(66), + epoch::Epoch::test(&[(0, 10), (1, 10)]), + ) + .unwrap(); + + assert!(genesis.is_genesis()); + + let mut block = genesis.clone(); + block.prev_block_hash = genesis_hash.clone(); + assert!(!block.is_genesis()); + + let mut block = genesis.clone(); + block.epoch_id = genesis_hash.clone(); + assert!(!block.is_genesis()); + + assert_eq!(genesis_hash, genesis.calc_hash()); + assert_ne!(genesis_hash, block.calc_hash()); + + let pk = MockPubKey(77); + let signature = + genesis.sign(|msg| Ok(MockSignature::new(msg, pk))).unwrap(); + assert_eq!(MockSignature(1722674425, pk), signature); + assert!(genesis.verify(&pk, &signature)); + assert!(!genesis.verify(&MockPubKey(88), &signature)); + assert!(!genesis.verify(&pk, &MockSignature(0, pk))); + + let mut block = genesis.clone(); + block.host_timestamp += 1; + assert_ne!(genesis_hash, block.calc_hash()); + assert!(!block.verify(&pk, &signature)); + + // Try creating invalid next block. + assert_eq!( + Err(GenerateError::BadHostHeight), + genesis.generate_next( + HostHeight::from(42), + 100, + CryptoHash::test(99), + None + ) + ); + assert_eq!( + Err(GenerateError::BadHostTimestamp), + genesis.generate_next( + HostHeight::from(43), + 24, + CryptoHash::test(99), + None + ) + ); + + // Create next block and test its behaviour. + let block = genesis + .generate_next(HostHeight::from(50), 50, CryptoHash::test(99), None) + .unwrap(); + assert!(!block.is_genesis()); + assert_eq!(BlockHeight::from(1), block.block_height); + assert_eq!(genesis_hash, block.prev_block_hash); + assert_eq!(genesis_hash, block.epoch_id); + let hash = "uv7IaNMkac36VYAD/RNtDF14wY/DXxlxzsS2Qi+d4uw="; + let hash = CryptoHash::from_base64(hash).unwrap(); + assert_eq!(hash, block.calc_hash()); + + // Create next block within and introduce a new epoch. + let epoch = Some(epoch::Epoch::test(&[(0, 20), (1, 10)])); + let block = block + .generate_next(HostHeight::from(60), 60, CryptoHash::test(99), epoch) + .unwrap(); + assert_eq!(hash, block.prev_block_hash); + assert_eq!(genesis_hash, block.epoch_id); + let hash = "JWVBe5GotaDzyClzBuArPLjcAQTRElMCxvstyZ0bMtM="; + let hash = CryptoHash::from_base64(hash).unwrap(); + assert_eq!(hash, block.calc_hash()); + + // Create next block which belongs to the new epoch. + let block = block + .generate_next(HostHeight::from(65), 65, CryptoHash::test(99), None) + .unwrap(); + assert_eq!(hash, block.prev_block_hash); + assert_eq!(hash, block.epoch_id); +} diff --git a/common/blockchain/src/candidates.rs b/common/blockchain/src/candidates.rs new file mode 100644 index 00000000..31f2795d --- /dev/null +++ b/common/blockchain/src/candidates.rs @@ -0,0 +1,376 @@ +use alloc::vec::Vec; +use core::num::{NonZeroU128, NonZeroU16}; + +use crate::chain; +use crate::validators::{PubKey, Validator}; + +#[cfg(test)] +mod tests; + +/// Set of candidate validators to consider when creating a new epoch. +/// +/// Whenever epoch changes, candidates with most stake are included in +/// validators set. +#[derive(Clone, PartialEq, Eq)] +pub struct Candidates { + /// Maximum number of validators in a validator set. + max_validators: NonZeroU16, + + /// Set of validators which are interested in participating in the + /// blockchain. + /// + /// The vector is kept sorted with candidates with most stake first. + candidates: Vec>, + + /// Whether the set changed in a way which affects the epoch. + /// + /// If this is true, as soon as possible a new epoch will be started. + changed: bool, + + /// Sum of the top `max_validators` stakes. + head_stake: u128, +} + +/// A candidate to become a validator. +#[derive(Clone, PartialEq, Eq)] +struct Candidate { + /// Public key of the candidate. + pubkey: PK, + + /// Candidate’s stake. + stake: NonZeroU128, +} + +/// Error while updating candidate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum UpdateCandidateError { + /// Candidate’s stake is below required minimum. + NotEnoughValidatorStake, + + /// After removing a candidate or reducing candidate’s stake, the total + /// stake would fall below required minimum. + NotEnoughTotalStake, + + /// After removing a candidate, the total number of validators would fall + /// below required minimum. + NotEnoughValidators, +} + +impl Candidates { + /// Creates a new candidates set from the given list. + /// + /// If the list is longer than `max_validators` marks the set as `changed`. + /// I.e. on the next epoch change, the validators set will be changed to + /// only the top `max_validators`. + /// + /// Note that the value of `max_validators` is preserved. All methods which + /// take `cfg: &chain::Config` as an argument ignore `cfg.max_validators` + /// value and use value of this `max_validators` argument instead. + pub fn new( + max_validators: NonZeroU16, + validators: &[Validator], + ) -> Self { + Self::from_candidates( + max_validators, + validators.iter().map(Candidate::from).collect::>(), + ) + } + + fn from_candidates( + max_validators: NonZeroU16, + mut candidates: Vec>, + ) -> Self { + candidates.sort_unstable(); + // If validator set in the genesis block is larger than maximum size + // specified in configuration, than we need to reduce the number on next + // epoch change. + let changed = candidates.len() > usize::from(max_validators.get()); + let head_stake = Self::sum_head_stake(max_validators, &candidates); + let this = Self { max_validators, candidates, changed, head_stake }; + this.debug_verify_state(); + this + } + + /// Sums stake of the first `count` candidates. + fn sum_head_stake(count: NonZeroU16, candidates: &[Candidate]) -> u128 { + let count = usize::from(count.get()).min(candidates.len()); + candidates[..count] + .iter() + .fold(0, |sum, c| sum.checked_add(c.stake.get()).unwrap()) + } + + /// Returns top validators together with their total stake if changed since + /// last call. + pub fn maybe_get_head(&mut self) -> Option<(Vec>, u128)> { + if !self.changed { + return None; + } + let mut total: u128 = 0; + let validators = self + .candidates + .iter() + .take(self.max_validators()) + .map(|candidate| { + total = total.checked_add(candidate.stake.get())?; + Some(Validator::from(candidate)) + }) + .collect::>>() + .unwrap(); + self.changed = false; + self.debug_verify_state(); + Some((validators, total)) + } + + /// Adds a new candidates or updates existing candidate’s stake. + pub fn update( + &mut self, + cfg: &chain::Config, + pubkey: PK, + stake: u128, + ) -> Result<(), UpdateCandidateError> { + let stake = NonZeroU128::new(stake) + .filter(|stake| *stake >= cfg.min_validator_stake) + .ok_or(UpdateCandidateError::NotEnoughValidatorStake)?; + let candidate = Candidate { pubkey, stake }; + let old_pos = + self.candidates.iter().position(|el| el.pubkey == candidate.pubkey); + let mut new_pos = + self.candidates.binary_search(&candidate).map_or_else(|p| p, |p| p); + let res = match old_pos { + None => Ok(self.add_impl(new_pos, candidate)), + Some(old_pos) => { + if new_pos > old_pos { + new_pos -= 1; + } + self.update_impl(cfg, old_pos, new_pos, candidate) + } + }; + self.debug_verify_state(); + res + } + + /// Removes an existing candidate. + pub fn remove( + &mut self, + cfg: &chain::Config, + pubkey: &PK, + ) -> Result<(), UpdateCandidateError> { + let pos = self.candidates.iter().position(|el| &el.pubkey == pubkey); + if let Some(pos) = pos { + if self.candidates.len() <= cfg.min_validators.get().into() { + return Err(UpdateCandidateError::NotEnoughValidators); + } + self.update_stake_for_remove(cfg, pos)?; + self.candidates.remove(pos); + self.debug_verify_state(); + } + Ok(()) + } + + fn max_validators(&self) -> usize { usize::from(self.max_validators.get()) } + + /// Adds a new candidate at given position. + /// + /// It’s caller’s responsibility to guarantee that `new_pos` is correct + /// position for the `candidate` to be added and that there’s no candidate + /// with the same public key already on the list. + fn add_impl(&mut self, new_pos: usize, candidate: Candidate) { + let new = candidate.stake.get(); + let max = self.max_validators(); + self.candidates.insert(new_pos, candidate); + if new_pos < max { + let old = self.candidates.get(max).map_or(0, |c| c.stake.get()); + self.add_head_stake(new - old); + } + } + + /// Updates a candidate by changing its position and stake. + fn update_impl( + &mut self, + cfg: &chain::Config, + old_pos: usize, + new_pos: usize, + candidate: Candidate, + ) -> Result<(), UpdateCandidateError> { + let max = self.max_validators(); + if new_pos >= max { + // Candidate’s new position is outside of the first max_validators. + // Verify it the same way we verify removal of a candidate since + // in next epoch they won’t be in the validators set. + self.update_stake_for_remove(cfg, old_pos)?; + } else if old_pos >= max { + // The candidate graduates to the top max_validators. This may + // change head_stake but never by decreasing it. + let new = candidate.stake.get(); + let old = self.candidates.get(max - 1).map_or(0, |c| c.stake.get()); + self.add_head_stake(new - old); + } else { + // The candidate moves within the top max_validators. We need to + // update head_stake. + let old_stake = self.candidates[old_pos].stake.get(); + let new_stake = candidate.stake.get(); + if old_stake < new_stake { + self.add_head_stake(new_stake - old_stake); + } else if old_stake > new_stake { + self.sub_head_stake(cfg, old_stake - new_stake)?; + } else { + return Ok(()); + }; + } + rotate(self.candidates.as_mut_slice(), old_pos, new_pos).stake = + candidate.stake; + Ok(()) + } + + /// Verifies whether removing candidate at given position adheres to + /// configuration and, if it does, updates head stake if necessary. + /// + /// Returns an error, if removing validator at given position would reduce + /// number of candidates or stake of the head candidates below minimums from + /// the configuration. + /// + /// Otherwise, acts as if the candidate at the position got removed and + /// updates `self.head_stake` and `self.changed` if necessary. + fn update_stake_for_remove( + &mut self, + cfg: &chain::Config, + pos: usize, + ) -> Result<(), UpdateCandidateError> { + let max = self.max_validators(); + if pos >= max { + return Ok(()); + } + let old = self.candidates[pos].stake.get(); + let new = self.candidates.get(max).map_or(0, |c| c.stake.get()); + self.sub_head_stake(cfg, old - new) + } + + /// Adds given amount of stake to `head_stake`. + fn add_head_stake(&mut self, stake: u128) { + self.head_stake = self.head_stake.checked_add(stake).unwrap(); + self.changed = true; + } + + /// Subtracts given amount of stake from `head_stake`. + fn sub_head_stake( + &mut self, + cfg: &chain::Config, + stake: u128, + ) -> Result<(), UpdateCandidateError> { + let head_stake = self.head_stake.checked_sub(stake).unwrap(); + if head_stake < cfg.min_total_stake.get() { + return Err(UpdateCandidateError::NotEnoughTotalStake); + } + self.head_stake = head_stake; + self.changed = true; + Ok(()) + } + + /// If debug assertions are enabled, checks whether all invariants are held. + /// + /// Verifies that a) candidates are sorted, b) contain no duplicates and c) + /// `self.head_stake` is sum of stake of first `self.max_validators` + /// candidates. + #[track_caller] + fn debug_verify_state(&self) { + if !cfg!(debug_assertions) { + return; + } + for (idx, wnd) in self.candidates.windows(2).enumerate() { + assert!(wnd[0] < wnd[1], "{idx}"); + } + + let mut pks = self + .candidates + .iter() + .map(|c| c.pubkey.clone()) + .collect::>(); + pks.sort_unstable(); + for wnd in pks.windows(2) { + assert!(wnd[0] != wnd[1]); + } + + let got = Self::sum_head_stake(self.max_validators, &self.candidates); + assert_eq!(self.head_stake, got); + } +} + +/// Rotates subslice such that element at `old_pos` moves to `new_pos`. +/// +/// Depending whether `old_pos` is less than or greater than `new_pos`, performs +/// a left or right rotation of a `min(old_pos, new_pos)..=max(old_pos, +/// new_pos)` subslice. +/// +/// Returns reference to the element at `new_pos`. +fn rotate(slice: &mut [T], old_pos: usize, new_pos: usize) -> &mut T { + use core::cmp::Ordering; + match old_pos.cmp(&new_pos) { + Ordering::Less => slice[old_pos..=new_pos].rotate_left(1), + Ordering::Equal => (), + Ordering::Greater => slice[new_pos..=old_pos].rotate_right(1), + } + &mut slice[new_pos] +} + +impl core::cmp::PartialOrd> for Candidate { + /// Compares two candidates sorting by `(-stake, pubkey)` pair. + /// + /// That is orders candidates by their stake in descending order and (in + /// case of equal stakes) by public key in ascending order. + fn partial_cmp(&self, rhs: &Self) -> Option { + match rhs.stake.cmp(&self.stake) { + core::cmp::Ordering::Equal => self.pubkey.partial_cmp(&rhs.pubkey), + ord => Some(ord), + } + } +} + +impl core::cmp::Ord for Candidate { + /// Compares two candidates sorting by `(-stake, pubkey)` pair. + /// + /// That is orders candidates by their stake in descending order and (in + /// case of equal stakes) by public key in ascending order. + fn cmp(&self, rhs: &Self) -> core::cmp::Ordering { + rhs.stake.cmp(&self.stake).then_with(|| self.pubkey.cmp(&rhs.pubkey)) + } +} + +impl From<&Candidate> for Validator { + fn from(candidate: &Candidate) -> Self { + Self::new(candidate.pubkey.clone(), candidate.stake) + } +} + +impl From<&Validator> for Candidate { + fn from(validator: &Validator) -> Self { + Self { pubkey: validator.pubkey().clone(), stake: validator.stake() } + } +} + +impl core::fmt::Debug for Candidate { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(fmt, "{{{:?} staking {}}}", self.pubkey, self.stake.get()) + } +} + +impl core::fmt::Debug for Candidates { + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if self.candidates.is_empty() { + fmtr.write_str("{[]")?; + } else { + let max = usize::from(self.max_validators.get()); + for (index, candidate) in self.candidates.iter().enumerate() { + let sep = if index == 0 { + "{[" + } else if index == max { + " | " + } else { + ", " + }; + write!(fmtr, "{sep}{candidate:?}")?; + } + } + let changed = ["", " (changed)"][usize::from(self.changed)]; + write!(fmtr, "]; head_stake: {}{changed}}}", self.head_stake) + } +} diff --git a/common/blockchain/src/candidates/tests.rs b/common/blockchain/src/candidates/tests.rs new file mode 100644 index 00000000..7078781c --- /dev/null +++ b/common/blockchain/src/candidates/tests.rs @@ -0,0 +1,432 @@ +use super::*; +use crate::validators::test_utils::MockPubKey; + +fn candidate(pubkey: char, stake: u128) -> Candidate { + Candidate { + pubkey: MockPubKey(pubkey as u32), + stake: NonZeroU128::new(stake).unwrap(), + } +} + +#[test] +fn test_rotate() { + #[track_caller] + fn run(old_pos: usize, new_pos: usize) -> [u8; 8] { + let mut arr = [0, 1, 2, 3, 4, 5, 6, 7]; + *rotate(&mut arr[..], old_pos, new_pos) = 42; + arr + } + + assert_eq!([42, 1, 2, 3, 4, 5, 6, 7], run(0, 0)); + assert_eq!([0, 1, 2, 3, 4, 5, 6, 42], run(7, 7)); + assert_eq!([1, 2, 3, 4, 5, 6, 7, 42], run(0, 7)); + assert_eq!([42, 0, 1, 2, 3, 4, 5, 6], run(7, 0)); + assert_eq!([0, 1, 3, 4, 5, 42, 6, 7], run(2, 5)); + assert_eq!([0, 1, 42, 2, 3, 4, 6, 7], run(5, 2)); +} + +#[test] +fn test_ord() { + use core::cmp::Ordering::*; + + fn test(want: core::cmp::Ordering, rhs: (char, u128), lhs: (char, u128)) { + let rhs = candidate(rhs.0, rhs.1); + let lhs = candidate(lhs.0, lhs.1); + if want == Equal { + assert_eq!(rhs, lhs); + } else { + assert_ne!(rhs, lhs); + } + assert_eq!(want, rhs.cmp(&lhs)); + } + + test(Less, ('C', 20), ('A', 10)); + test(Less, ('C', 20), ('C', 10)); + test(Less, ('C', 20), ('A', 10)); + test(Greater, ('C', 20), ('A', 20)); + test(Equal, ('C', 20), ('C', 20)); +} + +struct Cfg { + min_validators: u16, + min_validator_stake: u128, + min_total_stake: u128, +} + +impl Default for Cfg { + fn default() -> Self { + Self { min_validators: 1, min_validator_stake: 1, min_total_stake: 1 } + } +} + +impl From for chain::Config { + fn from(cfg: Cfg) -> Self { + chain::Config { + max_validators: NonZeroU16::MAX, + min_validators: NonZeroU16::new(cfg.min_validators).unwrap(), + min_validator_stake: NonZeroU128::new(cfg.min_validator_stake) + .unwrap(), + min_total_stake: NonZeroU128::new(cfg.min_total_stake).unwrap(), + min_quorum_stake: NonZeroU128::MIN, + min_block_length: crate::height::HostDelta::from(1), + min_epoch_length: crate::height::HostDelta::from(1), + } + } +} + +fn cfg_with_min_validators(min_validators: u16) -> chain::Config { + Cfg { min_validators, ..Default::default() }.into() +} + +fn cfg_with_min_validator_stake(min_validator_stake: u128) -> chain::Config { + Cfg { min_validator_stake, ..Default::default() }.into() +} + +fn cfg_with_min_total_stake(min_total_stake: u128) -> chain::Config { + Cfg { min_total_stake, ..Default::default() }.into() +} + +#[track_caller] +fn check( + want_candidates: [(char, u128); N], + candidates: &Candidates, +) { + let max = usize::from(candidates.max_validators.get()); + let want_stake = + want_candidates.iter().take(max).map(|(_, stake)| stake).sum::(); + let want_candidates = want_candidates + .into_iter() + .map(|(pubkey, stake)| candidate(pubkey, stake)) + .collect::>(); + assert_eq!( + (want_stake, want_candidates.as_slice()), + (candidates.head_stake, candidates.candidates.as_slice()) + ) +} + +#[test] +fn test_candidates_0() { + use candidate as c; + use UpdateCandidateError::*; + + fn pk(pubkey: char) -> MockPubKey { MockPubKey(pubkey as u32) } + + // Create candidates set + let mut candidates = Candidates::from_candidates( + NonZeroU16::new(3).unwrap(), + [c('A', 1), c('B', 2), c('C', 3), c('D', 4), c('E', 5)].to_vec(), + ); + check([('E', 5), ('D', 4), ('C', 3), ('B', 2), ('A', 1)], &candidates); + + // Check minimum total stake and count are checked + assert_eq!( + Err(NotEnoughTotalStake), + candidates.remove(&cfg_with_min_total_stake(10), &pk('E')), + ); + assert_eq!( + Err(NotEnoughValidators), + candidates.remove(&cfg_with_min_validators(5), &pk('E')), + ); + + // Removal is idempotent + for _ in 0..2 { + candidates.remove(&cfg_with_min_validators(2), &pk('E')).unwrap(); + check([('D', 4), ('C', 3), ('B', 2), ('A', 1)], &candidates); + } + + // Go below max_validators of candidates. + candidates.remove(&cfg_with_min_validators(1), &pk('C')).unwrap(); + candidates.remove(&cfg_with_min_validators(1), &pk('B')).unwrap(); + candidates.remove(&cfg_with_min_validators(1), &pk('A')).unwrap(); + check([('D', 4)], &candidates); + + // Minimum validator stake is checked + assert_eq!( + Err(NotEnoughValidatorStake), + candidates.update(&cfg_with_min_validator_stake(4), pk('C'), 3), + ); + + // Add back to have over max. Minimums are not checked since we’re + // adding candidates and stake. This theoretically may be a situation + // after chain configuration change so we need to support it. + candidates.update(&cfg_with_min_total_stake(20), pk('A'), 3).unwrap(); + candidates.update(&cfg_with_min_total_stake(20), pk('B'), 2).unwrap(); + candidates.update(&cfg_with_min_total_stake(20), pk('C'), 3).unwrap(); + check([('D', 4), ('A', 3), ('C', 3), ('B', 2)], &candidates); + + // Increase stake. Again, minimums are not checked. + candidates.update(&cfg_with_min_total_stake(20), pk('C'), 4).unwrap(); + check([('C', 4), ('D', 4), ('A', 3), ('B', 2)], &candidates); + + // Reduce stake. Now, minimums are checked. + assert_eq!( + Err(NotEnoughValidatorStake), + candidates.update(&cfg_with_min_validator_stake(3), pk('C'), 2), + ); + assert_eq!( + Err(NotEnoughTotalStake), + candidates.update(&cfg_with_min_total_stake(10), pk('C'), 2), + ); + check([('C', 4), ('D', 4), ('A', 3), ('B', 2)], &candidates); + + candidates.update(&cfg_with_min_total_stake(10), pk('B'), 3).unwrap(); + check([('C', 4), ('D', 4), ('A', 3), ('B', 3)], &candidates); + // `C` is moved out of validators but incoming `B` candidate has enough + // stake to meet min total stake limit. + candidates.update(&cfg_with_min_total_stake(10), pk('C'), 2).unwrap(); + check([('D', 4), ('A', 3), ('B', 3), ('C', 2)], &candidates); +} + +#[test] +fn test_candidiates_1() { + use candidate as c; + + let mut candidates = Candidates::from_candidates( + NonZeroU16::new(4).unwrap(), + [c('F', 168), c('D', 95), c('E', 81), c('C', 68), c('I', 63)].to_vec(), + ); + + let cfg = TestCtx::make_config(); + candidates.update(&cfg, MockPubKey('I' as u32), 254).unwrap(); + check( + [('I', 254), ('F', 168), ('D', 95), ('E', 81), ('C', 68)], + &candidates, + ); +} + +struct TestCtx { + config: chain::Config, + candidates: Candidates, + by_key: alloc::collections::BTreeMap, +} + +impl TestCtx { + /// Generates a new candidates set with random set of candidiates. + fn new(rng: &mut impl rand::Rng) -> Self { + let config = Self::make_config(); + + let candidates = (0..150) + .map(|idx| { + let pubkey = MockPubKey(idx); + let stake = NonZeroU128::new(rng.gen_range(100..255)).unwrap(); + Candidate { pubkey, stake } + }) + .collect::>(); + + let by_key = candidates + .iter() + .map(|c| (c.pubkey, c.stake.get())) + .collect::>(); + + let candidates = + Candidates::from_candidates(config.max_validators, candidates); + + Self { config, candidates, by_key } + } + + /// Generates a test config. + fn make_config() -> chain::Config { + let mut config = chain::Config::from(Cfg { + min_validators: 64, + min_validator_stake: 128, + min_total_stake: 16000, + }); + config.max_validators = NonZeroU16::new(128).unwrap(); + config + } + + /// Checks that total stake and number of validators respect the limits from + /// configuration file. + fn check(&self) { + assert!( + self.candidates.candidates.len() >= + self.config.min_validators.get().into(), + "Violated min validators constraint: {} < {}", + self.candidates.candidates.len(), + self.config.min_validators.get(), + ); + assert!( + self.candidates.head_stake >= self.config.min_total_stake.get(), + "Violated min total stake constraint: {} < {}", + self.candidates.head_stake, + self.config.min_total_stake.get(), + ); + } + + /// Attempts to removes a candidate from candidates set and verifies result + /// of the operation. + fn test_remove(&mut self, pubkey: MockPubKey) { + use super::UpdateCandidateError::*; + + let count = self.candidates.candidates.len(); + let head_stake = self.candidates.head_stake; + + let res = self.candidates.remove(&self.config, &pubkey); + self.check(); + + if let Err(err) = res { + let old_stake = self.by_key.get(&pubkey).unwrap().clone(); + assert_eq!(count, self.candidates.candidates.len()); + assert_eq!(head_stake, self.candidates.head_stake); + + match err { + NotEnoughValidatorStake => unreachable!(), + NotEnoughTotalStake => { + // What would be promoted candidate’s stake after + // removal. + let new_stake = self + .candidates + .candidates + .get(usize::from(self.config.max_validators.get())) + .map_or(0, |c: &Candidate<_>| c.stake.get()); + assert!( + head_stake - old_stake + new_stake < + self.config.min_total_stake.get() + ); + } + NotEnoughValidators => { + assert!( + self.candidates.candidates.len() <= + self.config.min_validators.get().into() + ); + } + } + } else if self.by_key.remove(&pubkey).is_some() { + assert_eq!(count - 1, self.candidates.candidates.len()); + assert!(head_stake >= self.candidates.head_stake); + } else { + assert_eq!(count, self.candidates.candidates.len()); + assert_eq!(head_stake, self.candidates.head_stake); + } + } + + /// Attempts to update candidate’s stake and verifies result of the + /// operation. + fn test_update(&mut self, pubkey: MockPubKey, new_stake: u128) { + use alloc::collections::btree_map::Entry; + + let count = self.candidates.candidates.len(); + let head_stake = self.candidates.head_stake; + + let res = + self.candidates.update(&self.config, pubkey.clone(), new_stake); + self.check(); + + if let Err(err) = res { + assert_eq!(count, self.candidates.candidates.len()); + assert_eq!(head_stake, self.candidates.head_stake); + self.verify_update_error(err, pubkey, new_stake); + } else { + let entry = self.by_key.entry(pubkey.clone()); + let new = matches!(&entry, Entry::Vacant(_)); + assert_eq!( + count + usize::from(new), + self.candidates.candidates.len() + ); + if new { + assert!(head_stake <= self.candidates.head_stake); + } + *entry.or_default() = new_stake; + } + } + + /// Verifies failed attempt at updating candidate’s stake. + fn verify_update_error( + &self, + err: UpdateCandidateError, + pubkey: MockPubKey, + new_stake: u128, + ) { + use super::UpdateCandidateError::*; + + match err { + NotEnoughValidatorStake => { + assert!(new_stake < self.config.min_validator_stake.get()); + return; + } + NotEnoughValidators => unreachable!(), + NotEnoughTotalStake => (), + } + + let old_stake = self.by_key.get(&pubkey).unwrap(); + + // There are two possibilities. We are in head and would stay there or + // we would be moved outside of it (replaced by whoever is just past the + // head). We can determine those cases by comparing updated state to + // the state just outside the head. + let last = self + .candidates + .candidates + .get(usize::from(self.config.max_validators.get())); + let kicked_out = last.clone().map_or(false, |candidiate| { + candidiate < + &Candidate { + pubkey, + stake: NonZeroU128::new(new_stake).unwrap(), + } + }); + + let new_stake = + if kicked_out { last.unwrap().stake.get() } else { new_stake }; + + assert!( + self.candidates.head_stake - old_stake + new_stake < + self.config.min_total_stake.get() + ); + } + + /// Performs a random test. `data` must be a three-element slice. The + /// random test is determined from values in the slice. + fn test(&mut self, data: &[u8]) { + let old_state = self.candidates.clone(); + let pubkey = MockPubKey((data[0]).into()); + let op = if data[2] % 2 == 0 { + Ok(pubkey) + } else { + Err((pubkey, u128::from(data[1]))) + }; + + let this = self as *mut TestCtx; + let res = std::panic::catch_unwind(|| { + // SAFETY: It’s test code. I don’t care. ;) It’s probably safe but + // self.candidates may be in inconsistent state. This is fine since + // we’re panicking anyway. + let this = unsafe { &mut *this }; + match &op { + Ok(pubkey) => this.test_remove(pubkey.clone()), + Err((pubkey, stake)) => { + this.test_update(pubkey.clone(), *stake) + } + } + }); + + if let Err(err) = res { + std::eprintln!("{:?}", old_state); + match op { + Ok(pubkey) => std::eprintln!(" Remove {pubkey:?}"), + Err((pubkey, stake)) => { + std::eprintln!(" Update {pubkey:?} staking {stake}") + } + } + std::eprintln!("{:?}", self.candidates); + std::panic::resume_unwind(err); + } + } +} + +#[test] +fn stress_test() { + use rand::Rng; + + let mut rng = rand::thread_rng(); + let mut ctx = TestCtx::new(&mut rng); + let mut n = lib::test_utils::get_iteration_count(1); + let mut buf = [0u8; 3 * 1024]; + while n > 0 { + rng.fill(&mut buf[..]); + for data in buf.chunks_exact(3).take(n) { + ctx.test(data); + } + n = n.saturating_sub(1024); + } +} diff --git a/common/blockchain/src/chain.rs b/common/blockchain/src/chain.rs new file mode 100644 index 00000000..a25e263a --- /dev/null +++ b/common/blockchain/src/chain.rs @@ -0,0 +1,74 @@ +use core::num::{NonZeroU128, NonZeroU16}; + +/// Chain policies configuration. +/// +/// Those are not encoded within a blockchain and only matter when generating +/// a new block. +// TODO(mina86): Do those configuration options make sense? +pub struct Config { + /// Minimum number of validators allowed in an epoch. + /// + /// The purpose of the minimum is to make sure that the blockchain isn’t + /// controlled by a small group of validators. + pub min_validators: NonZeroU16, + + /// Maximum number of validators allowed in an epoch. + /// + /// The purpose of the maximum is to bound size of the validators set. + /// Large sets may impact performance of the blockchain as epoch definition + /// becomes larger and iterating through all validators becomes slower. + pub max_validators: NonZeroU16, + + /// Minimum stake allowed for a single validator. + /// + /// The purpose of the minimum is to prevent large validators from taking + /// validator seats by splitting their stake into many small stakes as well + /// as limit for only entities with small stake from unnecessarily enlarging + /// the candidates set. + pub min_validator_stake: NonZeroU128, + + /// Minimum total stake allowed for an epoch. + /// + /// The purpose of the minimum is to make sure that there’s always + /// a significant stake guaranteeing each block. Since quorum is defined at + /// over half stake, this also defines a lower bound on quorum stake. + /// + /// Note that `min_validators * min_validator_stake` imposes a lower bound + /// on the minimum total stake. This field allows to raise the total stake + /// minimum above value coming from that calculation. If this is not + /// necessary, this may be set to `1`. + pub min_total_stake: NonZeroU128, + + /// Minimum quorum for an epoch. + /// + /// The purpose of the minimum is to make sure that there’s always + /// a significant stake guaranteeing each block. + /// + /// Note that in contrast to `min_total_stake` and other minimums, this + /// value doesn’t limit what kind of stake validators can have. Instead, it + /// affects `quorum_stake` value for an epoch by making it at least this + /// value. + /// + /// Note that `min_total_stake` imposes additional requirement for minimum + /// quorum stake, i.e. it must be greater than `min_total_stake / 2`. With + /// `min_quorum_stake` it’s possible to configure dynamic quorum ratio: if + /// there’s not enough total stake, the ratio will be increased making it + /// necessary for more validators to sign the blocks. If that feature is + /// not necessary, this may be set to `1`. + pub min_quorum_stake: NonZeroU128, + + /// Minimum number of host blocks before new emulated block can be created. + /// + /// The purpose of the minimum is to limit speed in which emulated blocks + /// are generated. Typically generating them as fast as host block’s isn’t + /// necessary and may even degrade performance when many blocks with small + /// changes are introduced rather bundling them together. + pub min_block_length: crate::height::HostDelta, + + /// Minimum length of an epoch. + /// + /// The purpose of the minimum is to make it possible for light clients to + /// catch up verification by only having to verify blocks at end of each + /// epoch. + pub min_epoch_length: crate::height::HostDelta, +} diff --git a/common/blockchain/src/common.rs b/common/blockchain/src/common.rs new file mode 100644 index 00000000..b98ae77a --- /dev/null +++ b/common/blockchain/src/common.rs @@ -0,0 +1,77 @@ +use borsh::maybestd::io; + +/// A discriminant to include at the beginning of data structures which need to +/// be versioned for forwards compatibility. +/// +/// It’s serialised as a single byte zero and when deserialising it fails if the +/// single read byte isn’t zero. Since at the moment all structures in the code +/// base are at version zero, no other versions are supported. +/// +/// In the future, the zero byte can be used to distinguish versions of data +/// structures. One idea is to provide `VersionUpTo` type which will +/// serialise as specified version and verify version ≤ `MAX` when +/// deserialising: +/// +/// ```ignore +/// #[derive(borsh::BorshSerialize, borsh::BorshDeserialize)] +/// struct Foo { +/// version: VersionZero, +/// pub count: usize, +/// } +/// +/// struct Bar { +/// version: VersionUpTo<1>, +/// pub drinks: Vec, +/// pub dishes: Vec, +/// } +/// ``` +/// +/// With that scheme, borsh serialisation and deserialisation will need to be +/// implemented manually and will have to take into account the version. +/// Another approach is to use an enum with variants for each version: +/// +/// ```ignore +/// #[derive(borsh::BorshSerialize, borsh::BorshDeserialize)] +/// enum Bar { +/// V1(v1::Bar), +/// V2(v2::Bar), +/// } +/// +/// mod v1 { struct Bar { pub drinks: Vec } } +/// mod v2 { struct Bar { pub drinks: Vec, pub dishes: Vec } } +/// ``` +/// +/// Whatever the case, having `version: VersionZero` field as the first one in +/// a structure allows it to be versioned in the future. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct VersionZero; + +impl borsh::BorshSerialize for VersionZero { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&[0]) + } +} + +impl borsh::BorshDeserialize for VersionZero { + fn deserialize_reader(reader: &mut R) -> io::Result { + u8::deserialize_reader(reader).and_then(|byte| { + if byte == 0 { + Ok(Self) + } else { + let msg = alloc::format!("Invalid version: {byte}"); + Err(io::Error::new(io::ErrorKind::InvalidData, msg)) + } + }) + } +} + +#[test] +fn test_version_zero() { + use borsh::BorshDeserialize; + + assert_eq!(&[0], borsh::to_vec(&VersionZero).unwrap().as_slice()); + VersionZero::try_from_slice(&[0]).unwrap(); + VersionZero::try_from_slice(&[1]).unwrap_err(); + VersionZero::try_from_slice(&[]).unwrap_err(); + VersionZero::try_from_slice(&[0, 0]).unwrap_err(); +} diff --git a/common/blockchain/src/epoch.rs b/common/blockchain/src/epoch.rs new file mode 100644 index 00000000..6b6355ab --- /dev/null +++ b/common/blockchain/src/epoch.rs @@ -0,0 +1,120 @@ +use alloc::vec::Vec; +use core::num::NonZeroU128; + +use crate::validators::{PubKey, Validator}; + +/// An epoch describing configuration applying to all blocks within an epoch. +/// +/// An epoch is identified by hash of the block it was introduced in. As such, +/// epoch’s identifier is unknown until block which defines it in +/// [`crate::block::Block::next_blok`] field is created. +#[derive( + Clone, Debug, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize, +)] +pub struct Epoch { + /// Version of the structure. Used to support forward-compatibility. At + /// the moment this is always zero. + version: crate::common::VersionZero, + + /// Validators set. + validators: Vec>, + + /// Minimum stake to consider block signed. + quorum_stake: NonZeroU128, +} + +impl Epoch { + /// Creates a new epoch. + /// + /// Returns `None` if the epoch is invalid, i.e. if quorum stake is greater + /// than total stake of all validators. An invalid epoch leads to + /// a blockchain which cannot generate new blocks since signing them is no + /// longer possible. + pub fn new( + validators: Vec>, + quorum_stake: NonZeroU128, + ) -> Option { + let version = crate::common::VersionZero; + let this = Self { version, validators, quorum_stake }; + Some(this).filter(Self::is_valid) + } + + /// Creates a new epoch without checking whether it’s valid. + /// + /// It’s caller’s responsibility to guarantee that total stake of all + /// validators is no more than quorum stake. + /// + /// In debug builds panics if the result is an invalid epoch. + pub(crate) fn new_unchecked( + validators: Vec>, + quorum_stake: NonZeroU128, + ) -> Self { + let version = crate::common::VersionZero; + let this = Self { version, validators, quorum_stake }; + debug_assert!(this.is_valid()); + this + } + + /// Checks whether the epoch is valid. + fn is_valid(&self) -> bool { + let mut left = self.quorum_stake.get(); + for validator in self.validators.iter() { + left = left.saturating_sub(validator.stake().get()); + if left == 0 { + return true; + } + } + false + } + + /// Returns list of all validators in the epoch. + pub fn validators(&self) -> &[Validator] { self.validators.as_slice() } + + /// Returns stake needed to reach quorum. + pub fn quorum_stake(&self) -> NonZeroU128 { self.quorum_stake } + + /// Finds a validator by their public key. + pub fn validator(&self, pk: &PK) -> Option<&Validator> { + self.validators.iter().find(|validator| validator.pubkey() == pk) + } +} + +#[cfg(test)] +impl Epoch { + /// Creates an epoch calculating quorum as >50% of total stake. + /// + /// Panics if `validators` is empty or any of the stake is zero. + pub fn test(validators: &[(u32, u128)]) -> Self { + let mut total: u128 = 0; + let validators = validators + .iter() + .copied() + .map(|(pk, stake)| { + total += stake; + Validator::new(pk.into(), NonZeroU128::new(stake).unwrap()) + }) + .collect(); + Self::new(validators, NonZeroU128::new(total / 2 + 1).unwrap()).unwrap() + } +} + +#[test] +fn test_creation() { + use crate::validators::MockPubKey; + + let validators = [ + Validator::new(MockPubKey(0), NonZeroU128::new(5).unwrap()), + Validator::new(MockPubKey(1), NonZeroU128::new(5).unwrap()), + ]; + + assert_eq!(None, Epoch::::new(Vec::new(), NonZeroU128::MIN)); + assert_eq!( + None, + Epoch::new(validators.to_vec(), NonZeroU128::new(11).unwrap()) + ); + + let epoch = + Epoch::new(validators.to_vec(), NonZeroU128::new(10).unwrap()).unwrap(); + assert_eq!(Some(&validators[0]), epoch.validator(&MockPubKey(0))); + assert_eq!(None, epoch.validator(&MockPubKey(2))); +} diff --git a/common/blockchain/src/height.rs b/common/blockchain/src/height.rs new file mode 100644 index 00000000..20f5f1dc --- /dev/null +++ b/common/blockchain/src/height.rs @@ -0,0 +1,126 @@ +/// Block height. +/// +/// The generic argument allows the value to be tagged to distinguish it from +/// host blockchain height and emulated blockchain height. +#[derive( + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, +)] +pub struct Height(u64, core::marker::PhantomData<*const T>); + +/// Delta between two host heights. +/// +/// Always expressed as positive value. +/// +/// The generic argument allows the value to be tagged to distinguish it from +/// host blockchain height and emulated blockchain height. +#[derive( + Clone, + Copy, + Default, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, +)] +pub struct Delta(u64, core::marker::PhantomData<*const T>); + +/// Tag for use with [`Height`] and [`Delta`] to indicate it’s host blockchain +/// height. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Host; + +/// Tag for use with [`Height`] and [`Delta`] to indicate it’s emulated +/// blockchain height. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Block; + +pub type HostHeight = Height; +pub type HostDelta = Delta; +pub type BlockHeight = Height; +pub type BlockDelta = Delta; + +impl Height { + /// Returns the next height, i.e. `self + 1`. + pub fn next(self) -> Self { Self(self.0.checked_add(1).unwrap(), self.1) } + + /// Checks whether delta between two heights is at least `min`. + /// + /// In essence, returns `self - past_height >= min`. + pub fn check_delta_from(self, past_height: Self, min: Delta) -> bool { + self.0.checked_sub(past_height.0).map_or(false, |age| age >= min.0) + } +} + +impl From for Height { + fn from(value: u64) -> Self { Self(value, Default::default()) } +} + +impl From for Delta { + fn from(value: u64) -> Self { Self(value, Default::default()) } +} + +impl From> for u64 { + fn from(value: Height) -> u64 { value.0 } +} + +impl From> for u64 { + fn from(value: Delta) -> u64 { value.0 } +} + +impl core::fmt::Display for Height { + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(fmtr) + } +} + +impl core::fmt::Debug for Height { + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(fmtr) + } +} + +impl core::fmt::Display for Delta { + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(fmtr) + } +} + +impl core::fmt::Debug for Delta { + fn fmt(&self, fmtr: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(fmtr) + } +} + +#[test] +fn test_sanity() { + assert!(HostHeight::from(42) == HostHeight::from(42)); + assert!(HostHeight::from(42) <= HostHeight::from(42)); + assert!(HostHeight::from(42) != HostHeight::from(24)); + assert!(HostHeight::from(42) > HostHeight::from(24)); + + assert!(HostDelta::from(42) == HostDelta::from(42)); + assert!(HostDelta::from(42) <= HostDelta::from(42)); + assert!(HostDelta::from(42) != HostDelta::from(24)); + assert!(HostDelta::from(42) > HostDelta::from(24)); + + assert_eq!(HostHeight::from(43), HostHeight::from(42).next()); + + let old = HostHeight::from(24); + let new = HostHeight::from(42); + assert!(new.check_delta_from(old, HostDelta::from(17))); + assert!(new.check_delta_from(old, HostDelta::from(18))); + assert!(!new.check_delta_from(old, HostDelta::from(19))); + assert!(!old.check_delta_from(new, HostDelta::from(0))); +} diff --git a/common/blockchain/src/lib.rs b/common/blockchain/src/lib.rs new file mode 100644 index 00000000..6dd3a1d3 --- /dev/null +++ b/common/blockchain/src/lib.rs @@ -0,0 +1,14 @@ +#![allow(clippy::unit_arg, clippy::comparison_chain)] +#![no_std] +extern crate alloc; +#[cfg(any(feature = "std", test))] +extern crate std; + +pub mod block; +mod candidates; +pub mod chain; +mod common; +pub mod epoch; +pub mod height; +pub mod manager; +pub mod validators; diff --git a/common/blockchain/src/manager.rs b/common/blockchain/src/manager.rs new file mode 100644 index 00000000..a0139d7c --- /dev/null +++ b/common/blockchain/src/manager.rs @@ -0,0 +1,249 @@ +#[cfg(not(feature = "std"))] +use alloc::collections::BTreeSet as Set; +use core::num::NonZeroU128; +#[cfg(feature = "std")] +use std::collections::HashSet as Set; + +use lib::hash::CryptoHash; + +use crate::candidates::Candidates; +pub use crate::candidates::UpdateCandidateError; +use crate::height::HostHeight; +use crate::validators::{PubKey, Signature}; +use crate::{block, chain, epoch}; + +pub struct ChainManager { + /// Configuration specifying limits for block generation. + config: chain::Config, + + /// Current latest block which has been signed by quorum of validators. + block: block::Block, + + /// Epoch of the next block. + /// + /// If `block` defines new epoch, this is copy of `block.next_epoch` + /// otherwise this is epoch of the current block. In other words, this is + /// epoch which specifies validators set for `pending_block`. + next_epoch: epoch::Epoch, + + /// Next block which is waiting for quorum of validators to sign. + pending_block: Option>, + + /// Height at which current epoch was defined. + epoch_height: HostHeight, + + /// Current state root. + state_root: CryptoHash, + + /// Set of validator candidates to consider for the next epoch. + candidates: Candidates, +} + +/// Pending block waiting for signatures. +/// +/// Once quorum of validators sign the block it’s promoted to the current block. +struct PendingBlock { + /// The block that waits for signatures. + next_block: block::Block, + /// Hash of the block. + /// + /// This is what validators are signing. It equals `next_block.calc_hash()` + /// and we’re keeping it as a field to avoid having to hash the block each + /// time. + hash: CryptoHash, + /// Validators who so far submitted valid signatures for the block. + signers: Set, + /// Sum of stake of validators who have signed the block. + signing_stake: u128, +} + +/// Provided genesis block is invalid. +#[derive(Clone, PartialEq, Eq)] +pub struct BadGenesis; + +/// Error while generating a new block. +#[derive(derive_more::From)] +pub enum GenerateError { + /// Last block hasn’t been signed by enough validators yet. + HasPendingBlock, + /// Block isn’t old enough (see [`chain::config::min_block_length`] field). + BlockTooYoung, + Inner(block::GenerateError), +} + +/// Error while accepting a signature from a validator. +pub enum AddSignatureError { + /// There’s no pending block. + NoPendingBlock, + /// The signature is invalid. + BadSignature, + /// The validator is not known. + BadValidator, +} + +impl ChainManager { + pub fn new( + config: chain::Config, + genesis: block::Block, + ) -> Result { + if !genesis.is_genesis() { + return Err(BadGenesis); + } + let next_epoch = genesis.next_epoch.clone().ok_or(BadGenesis)?; + let candidates = + Candidates::new(config.max_validators, next_epoch.validators()); + let state_root = genesis.state_root.clone(); + let epoch_height = genesis.host_height; + Ok(Self { + config, + block: genesis, + next_epoch, + pending_block: None, + epoch_height, + state_root, + candidates, + }) + } + + /// Sets value of state root to use in the next block. + pub fn update_state_root(&mut self, state_root: CryptoHash) { + self.state_root = state_root; + } + + /// Generates a new block and sets it as pending. + /// + /// Returns an error if there’s already a pending block. Previous pending + /// block must first be signed by quorum of validators before next block is + /// generated. + pub fn generate_next( + &mut self, + host_height: HostHeight, + host_timestamp: u64, + ) -> Result<(), GenerateError> { + if self.pending_block.is_some() { + return Err(GenerateError::HasPendingBlock); + } + if !host_height.check_delta_from( + self.block.host_height, + self.config.min_block_length, + ) { + return Err(GenerateError::BlockTooYoung); + } + + let next_epoch = self.maybe_generate_next_epoch(host_height); + let next_block = self.block.generate_next( + host_height, + host_timestamp, + self.state_root.clone(), + next_epoch, + )?; + self.pending_block = Some(PendingBlock { + hash: next_block.calc_hash(), + next_block, + signers: Set::new(), + signing_stake: 0, + }); + Ok(()) + } + + /// Generates a new epoch with the top validators from the candidates set if + /// necessary. + /// + /// Returns `None` if the current epoch is too short to change to new epoch + /// or the validators set hasn’t changed. Otherwise constructs and returns + /// a new epoch by picking top validators from `self.candidates` as the + /// validators set in the new epoch. + /// + /// Panics if there are no candidates, i.e. will always return a valid + /// epoch. However, it doesn’t check minimum number of validators (other + /// than non-zero) or minimum quorum stake (again, other than non-zero). + /// Those conditions are assumed to hold by construction of + /// `self.candidates`. + fn maybe_generate_next_epoch( + &mut self, + host_height: HostHeight, + ) -> Option> { + if !host_height + .check_delta_from(self.epoch_height, self.config.min_epoch_length) + { + return None; + } + let (validators, total) = self.candidates.maybe_get_head()?; + // 1. We validate that genesis has a valid epoch (at least 1 stake). + // 2. We never allow fewer than config.min_validators candidates. + // 3. We never allow candidates with zero stake. + // Therefore, total should always be positive. + let total = NonZeroU128::new(total).unwrap(); + // SAFETY: anything_unsigned + 1 > 0 + let quorum = unsafe { NonZeroU128::new_unchecked(total.get() / 2 + 1) } + .clamp(self.config.min_quorum_stake, total); + Some(epoch::Epoch::new_unchecked(validators, quorum)) + } + + /// Adds a signature to pending block. + /// + /// Returns `true` if quorum has been reached and the pending block has + /// graduated to the current block. + pub fn add_signature( + &mut self, + pubkey: PK, + signature: &PK::Signature, + ) -> Result { + let pending = self + .pending_block + .as_mut() + .ok_or(AddSignatureError::NoPendingBlock)?; + if pending.signers.contains(&pubkey) { + return Ok(false); + } + if !signature.verify(&pending.hash, &pubkey) { + return Err(AddSignatureError::BadSignature); + } + + pending.signing_stake += self + .next_epoch + .validator(&pubkey) + .ok_or(AddSignatureError::BadValidator)? + .stake() + .get(); + assert!(pending.signers.insert(pubkey)); + + if pending.signing_stake < self.next_epoch.quorum_stake().get() { + return Ok(false); + } + + self.block = self.pending_block.take().unwrap().next_block; + if let Some(ref epoch) = self.block.next_epoch { + self.next_epoch = epoch.clone(); + self.epoch_height = self.block.host_height; + } + Ok(true) + } + + /// Adds a new validator candidate or updates existing candidate’s stake. + /// + /// Reducing candidates stake may fail if that would result in quorum or + /// total stake among the top `self.config.max_validators` to drop below + /// limits configured in `self.config`. + pub fn update_candidate( + &mut self, + pubkey: PK, + stake: u128, + ) -> Result<(), UpdateCandidateError> { + self.candidates.update(&self.config, pubkey, stake) + } + + /// Removes an existing validator candidate. + /// + /// Note that removing a candidate may fail if the result candidate set + /// would no longer satisfy minimums in the chain configuration. See also + /// [`Self::update_candidate`]. + /// + /// Does nothing if the candidate is not found. + pub fn remove_candidate( + &mut self, + pubkey: &PK, + ) -> Result<(), UpdateCandidateError> { + self.candidates.remove(&self.config, pubkey) + } +} diff --git a/common/blockchain/src/validators.rs b/common/blockchain/src/validators.rs new file mode 100644 index 00000000..845cabc3 --- /dev/null +++ b/common/blockchain/src/validators.rs @@ -0,0 +1,140 @@ +use core::num::NonZeroU128; + +/// A cryptographic public key used to identify validators and verify block +/// signatures. +pub trait PubKey: + Clone + + Eq + + Ord + + core::hash::Hash + + borsh::BorshSerialize + + borsh::BorshDeserialize +{ + /// Signature corresponding to this public key type. + type Signature: Signature; +} + +/// A cryptographic signature. +pub trait Signature: + Clone + borsh::BorshSerialize + borsh::BorshDeserialize +{ + /// Public key type which can verify the signature. + type PubKey: PubKey; + + /// Verifies that the signature of a given hash is correct. + fn verify( + &self, + message: &lib::hash::CryptoHash, + pk: &Self::PubKey, + ) -> bool; +} + +/// A validator +#[derive( + Clone, Debug, PartialEq, Eq, borsh::BorshSerialize, borsh::BorshDeserialize, +)] +pub struct Validator { + /// Version of the structure. Used to support forward-compatibility. At + /// the moment this is always zero. + version: crate::common::VersionZero, + + /// Public key of the validator. + pubkey: PK, + + /// Validator’s stake. + stake: NonZeroU128, +} + +impl Validator { + pub fn new(pubkey: PK, stake: NonZeroU128) -> Self { + Self { version: crate::common::VersionZero, pubkey, stake } + } + + pub fn pubkey(&self) -> &PK { &self.pubkey } + + pub fn stake(&self) -> NonZeroU128 { self.stake } +} + +#[cfg(test)] +pub(crate) mod test_utils { + + use super::*; + + /// A mock implementation of a PubKey. Offers no security; intended for + /// tests only. + #[derive( + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, + derive_more::From, + )] + pub struct MockPubKey(pub u32); + + /// A mock implementation of a Signature. Offers no security; intended for + /// tests only. + #[derive( + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + borsh::BorshSerialize, + borsh::BorshDeserialize, + )] + pub struct MockSignature(pub u32, pub MockPubKey); + + impl core::fmt::Debug for MockPubKey { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(fmt, "⚷{}", self.0) + } + } + + impl core::fmt::Debug for MockSignature { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(fmt, "Sig({} by {:?})", self.0, self.1) + } + } + + impl super::PubKey for MockPubKey { + type Signature = MockSignature; + } + + impl MockSignature { + pub fn new(message: &[u8], pk: MockPubKey) -> Self { + Self(Self::hash_message(message), pk) + } + + fn hash_message(message: &[u8]) -> u32 { + Self::cut_hash(&lib::hash::CryptoHash::digest(message)) + } + + fn cut_hash(hash: &lib::hash::CryptoHash) -> u32 { + let hash = hash.into(); + let (head, _) = stdx::split_array_ref::<4, 28, 32>(&hash); + u32::from_be_bytes(*head) + } + } + + impl Signature for MockSignature { + type PubKey = MockPubKey; + + fn verify( + &self, + message: &lib::hash::CryptoHash, + pk: &Self::PubKey, + ) -> bool { + self.0 == Self::cut_hash(message) && &self.1 == pk + } + } +} + +#[cfg(test)] +pub(crate) use test_utils::{MockPubKey, MockSignature}; diff --git a/common/lib/Cargo.toml b/common/lib/Cargo.toml index 46a0204c..6469f421 100644 --- a/common/lib/Cargo.toml +++ b/common/lib/Cargo.toml @@ -10,6 +10,8 @@ borsh = { workspace = true, optional = true } derive_more.workspace = true sha2.workspace = true +stdx.workspace = true + [dev-dependencies] rand.workspace = true diff --git a/common/lib/src/hash.rs b/common/lib/src/hash.rs index 26eda41a..8e375f4d 100644 --- a/common/lib/src/hash.rs +++ b/common/lib/src/hash.rs @@ -1,5 +1,7 @@ use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; use base64::Engine; +#[cfg(feature = "borsh")] +use borsh::maybestd::io; use sha2::Digest; /// A cryptographic hash. @@ -54,6 +56,19 @@ impl CryptoHash { builder.build() } + /// Decodes a base64 string representation of the hash. + pub fn from_base64(base64: &str) -> Option { + // base64 API is kind of garbage. In certain situations the output + // buffer must be larger than the size of the decoded data or else + // decoding will fail. + let mut buf = [0; 34]; + match BASE64_ENGINE.decode_slice(base64.as_bytes(), &mut buf[..]) { + Ok(CryptoHash::LENGTH) => { + Some(Self(*stdx::split_array_ref::<32, 2, 34>(&buf).0)) + } + _ => None, + } + } /// Creates a new hash with given number encoded in its first bytes. /// @@ -176,6 +191,20 @@ impl Builder { pub fn build(self) -> CryptoHash { CryptoHash(self.0.finalize().into()) } } +#[cfg(feature = "borsh")] +impl io::Write for Builder { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.update(buf); + Ok(buf.len()) + } + + fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + Ok(self.update(buf)) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } +} + #[test] fn test_new_hash() { assert_eq!(CryptoHash::from([0; 32]), CryptoHash::default()); diff --git a/common/lib/src/lib.rs b/common/lib/src/lib.rs index 0d58dc38..ca82c495 100644 --- a/common/lib/src/lib.rs +++ b/common/lib/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::unit_arg, clippy::comparison_chain)] #![no_std] extern crate alloc; #[cfg(any(feature = "test_utils", test))]