From 7ffc600bf99f4dc88c1d668a0d7ddb46606234ba Mon Sep 17 00:00:00 2001 From: cryptoAtwill Date: Fri, 20 Sep 2024 14:47:37 +0800 Subject: [PATCH] vote tally initial commit --- Cargo.lock | 35 ++ Cargo.toml | 2 + fendermint/vm/genesis/Cargo.toml | 2 +- fendermint/vm/genesis/src/lib.rs | 21 ++ fendermint/vm/topdown/Cargo.toml | 3 +- fendermint/vm/topdown/src/vote/mod.rs | 41 ++- fendermint/vm/topdown/src/vote/payload.rs | 143 ++++++++ fendermint/vm/topdown/src/vote/store.rs | 110 +++++++ fendermint/vm/topdown/src/vote/tally.rs | 382 ++++++++++++++++++++++ 9 files changed, 730 insertions(+), 9 deletions(-) create mode 100644 fendermint/vm/topdown/src/vote/payload.rs create mode 100644 fendermint/vm/topdown/src/vote/store.rs create mode 100644 fendermint/vm/topdown/src/vote/tally.rs diff --git a/Cargo.lock b/Cargo.lock index f50cd9396..44e1825dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,6 +803,21 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -3463,6 +3478,7 @@ dependencies = [ "num-traits", "prometheus", "rand", + "secp256k1", "serde", "serde_json", "tendermint-rpc", @@ -8363,6 +8379,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.11.1" diff --git a/Cargo.toml b/Cargo.toml index ccc3db09b..1ebc7c396 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,8 @@ libp2p-mplex = { version = "0.41" } # libp2p-bitswap = "0.25.1" libp2p-bitswap = { git = "https://github.com/consensus-shipyard/libp2p-bitswap.git", branch = "chore-upgrade-libipld" } # Updated to libipld 0.16 libsecp256k1 = "0.7" +secp256k1 = {version = "0.27.0" } + literally = "0.1.3" log = "0.4" lru_time_cache = "0.11" diff --git a/fendermint/vm/genesis/Cargo.toml b/fendermint/vm/genesis/Cargo.toml index c44bfbe96..003fb13b6 100644 --- a/fendermint/vm/genesis/Cargo.toml +++ b/fendermint/vm/genesis/Cargo.toml @@ -23,6 +23,7 @@ multihash = { workspace = true, optional = true } fvm_shared = { workspace = true } ipc-api = { workspace = true } fendermint_actor_eam = { workspace = true } +hex = { workspace = true } fendermint_crypto = { path = "../../crypto" } fendermint_testing = { path = "../../testing", optional = true } @@ -32,7 +33,6 @@ fendermint_vm_encoding = { path = "../encoding" } [dev-dependencies] quickcheck = { workspace = true } quickcheck_macros = { workspace = true } -hex = { workspace = true } serde_json = { workspace = true } # Enable arb on self for tests. diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index ae87b4a1d..98bfb4e86 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -3,6 +3,8 @@ //! A Genesis data structure similar to [genesis.Template](https://github.com/filecoin-project/lotus/blob/v1.20.4/genesis/types.go) //! in Lotus, which is used to [initialize](https://github.com/filecoin-project/lotus/blob/v1.20.4/chain/gen/genesis/genesis.go) the state tree. +use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; use anyhow::anyhow; use fvm_shared::bigint::{BigInt, Integer}; use serde::{Deserialize, Serialize}; @@ -150,6 +152,12 @@ impl ValidatorKey { Self(normalize_public_key(key)) } + pub fn from_compressed_pubkey(compress: &[u8; 33]) -> anyhow::Result { + Ok(Self( + PublicKey::parse_compressed(compress)? + )) + } + pub fn public_key(&self) -> &PublicKey { &self.0 } @@ -246,6 +254,19 @@ pub mod ipc { } } +impl Display for ValidatorKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Validator({})", hex::encode(self.0.serialize())) + } +} + +impl Hash for ValidatorKey { + fn hash(&self, h: &mut H) { + let bytes = self.0.serialize(); + Hash::hash(&bytes, h); + } +} + #[cfg(test)] mod tests { use fvm_shared::{bigint::BigInt, econ::TokenAmount}; diff --git a/fendermint/vm/topdown/Cargo.toml b/fendermint/vm/topdown/Cargo.toml index f24db3051..74cd8cd03 100644 --- a/fendermint/vm/topdown/Cargo.toml +++ b/fendermint/vm/topdown/Cargo.toml @@ -34,8 +34,10 @@ prometheus = { workspace = true } fendermint_vm_genesis = { path = "../genesis" } fendermint_vm_event = { path = "../event" } fendermint_tracing = { path = "../../tracing" } +fendermint_crypto = { path = "../../crypto" } ipc-observability = { workspace = true } +secp256k1 = { workspace = true, features = ["recovery", "bitcoin_hashes"] } [dev-dependencies] arbitrary = { workspace = true } @@ -43,5 +45,4 @@ clap = { workspace = true } rand = { workspace = true } tracing-subscriber = { workspace = true } -fendermint_crypto = { path = "../../crypto" } fendermint_testing = { path = "../../testing", features = ["smt"] } diff --git a/fendermint/vm/topdown/src/vote/mod.rs b/fendermint/vm/topdown/src/vote/mod.rs index d3cf2cab2..61364c305 100644 --- a/fendermint/vm/topdown/src/vote/mod.rs +++ b/fendermint/vm/topdown/src/vote/mod.rs @@ -2,16 +2,19 @@ // SPDX-License-Identifier: Apache-2.0, MIT mod operation; +mod payload; +mod store; +mod tally; use crate::sync::TopDownSyncEvent; use crate::vote::operation::{OperationMetrics, OperationStateMachine}; +use crate::vote::payload::Vote; use crate::BlockHeight; use serde::{Deserialize, Serialize}; use std::time::Duration; use tokio::sync::{broadcast, mpsc}; -#[derive(Clone)] -pub struct VoteRecord {} +pub type Weight = u64; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Config { @@ -31,8 +34,8 @@ pub struct VoteReactorClient { pub fn start_vote_reactor( config: Config, - gossip_rx: broadcast::Receiver, - gossip_tx: mpsc::Sender, + gossip_rx: broadcast::Receiver, + gossip_tx: mpsc::Sender, internal_event_listener: broadcast::Receiver, ) -> VoteReactorClient { let (tx, rx) = mpsc::channel(config.req_channel_buffer_size); @@ -57,6 +60,27 @@ pub fn start_vote_reactor( VoteReactorClient { tx } } +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("the last finalized block has not been set")] + Uninitialized, + + #[error("failed to extend chain; height going backwards, current height {0}, got {1}")] + UnexpectedBlock(BlockHeight, BlockHeight), + + #[error("validator unknown or has no power")] + UnpoweredValidator, + + #[error("equivocation by validator")] + Equivocation, + + #[error("validator vote is invalidated")] + VoteCannotBeValidated, + + #[error("validator cannot sign vote")] + CannotSignVote, +} + enum VoteReactorRequest { QueryOperationMode, QueryVotes(BlockHeight), @@ -67,8 +91,8 @@ struct VotingHandler { /// vote tally status and etc. req_rx: mpsc::Receiver, /// Receiver from gossip pub/sub, mostly listening to incoming votes - gossip_rx: broadcast::Receiver, - gossip_tx: mpsc::Sender, + gossip_rx: broadcast::Receiver, + gossip_tx: mpsc::Sender, /// Listens to internal events and handles the events accordingly internal_event_listener: broadcast::Receiver, config: Config, @@ -77,10 +101,11 @@ struct VotingHandler { impl VotingHandler { fn handle_request(&self, _req: VoteReactorRequest) {} - fn record_vote(&self, _vote: VoteRecord) {} + fn record_vote(&self, _vote: Vote) {} fn handle_event(&self, _event: TopDownSyncEvent) {} + /// Process external request, such as RPC queries for debugging and status tracking. fn process_external_request(&mut self, _metrics: &OperationMetrics) -> usize { let mut n = 0; while n < self.config.req_batch_processing_size { @@ -99,6 +124,7 @@ impl VotingHandler { n } + /// Handles vote tally gossip pab/sub incoming votes from other peers fn process_gossip_subscription_votes(&mut self) -> usize { let mut n = 0; while n < self.config.gossip_req_processing_size { @@ -117,6 +143,7 @@ impl VotingHandler { n } + /// Poll internal topdown syncer event broadcasted. fn poll_internal_event(&mut self) -> Option { match self.internal_event_listener.try_recv() { Ok(event) => Some(event), diff --git a/fendermint/vm/topdown/src/vote/payload.rs b/fendermint/vm/topdown/src/vote/payload.rs new file mode 100644 index 000000000..4e74b7553 --- /dev/null +++ b/fendermint/vm/topdown/src/vote/payload.rs @@ -0,0 +1,143 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::vote::Weight; +use crate::{BlockHash, BlockHeight, Bytes}; +use anyhow::anyhow; +use fendermint_vm_genesis::ValidatorKey; +use secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; + +pub type Signature = Bytes; +pub type RecoverableECDSASignature = (i32, Vec); + +pub type PowerTable = HashMap; + +/// The different versions of vote casted in topdown gossip pub-sub channel +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub enum Vote { + V1 { + validator: ValidatorKey, + payload: CertifiedObservation, + }, +} + +/// The actual content that validators should agree upon, or put in another way the content +/// that a quorum should be formed upon +#[derive(Serialize, Deserialize, Hash, Debug, Clone, Eq, PartialEq)] +pub struct Ballot { + parent_height: u64, + /// The hash of the chain unit at that height. Usually a block hash, but could + /// be another entity (e.g. tipset CID), depending on the parent chain + /// and our interface to it. For example, if the parent is a Filecoin network, + /// this would be a tipset CID coerced into a block hash if queried through + /// the Eth API, or the tipset CID as-is if accessed through the Filecoin API. + parent_hash: Bytes, + /// A rolling/cumulative commitment to topdown effects since the beginning of + /// time, including the ones in this block. + cumulative_effects_comm: Bytes, +} + +/// The content that validators gossip among each other +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct Observation { + /// The hash of the subnet's last committed block when this observation was made. + /// Used to discard stale observations that are, e.g. replayed by an attacker + /// at a later time. Also used to detect nodes that might be wrongly gossiping + /// whilst being out of sync. + local_hash: BlockHash, + ballot: Ballot, +} + +/// A self-certified observation made by a validator. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct CertifiedObservation { + observed: Observation, + /// A "recoverable" ECDSA signature with the validator's secp256k1 private key over the + /// CID of the DAG-CBOR encoded observation using a BLAKE2b-256 multihash. + signature: RecoverableECDSASignature, +} + +impl Vote { + pub fn voter(&self) -> ValidatorKey { + match self { + Self::V1 { validator, .. } => validator.clone(), + } + } + + pub fn ballot(&self) -> &Ballot { + match self { + Self::V1 { payload, .. } => &payload.observed.ballot, + } + } +} + +impl TryFrom<&[u8]> for Vote { + type Error = anyhow::Error; + + fn try_from(bytes: &[u8]) -> Result { + let version = bytes[0]; + + if version == 0 { + let obs = CertifiedObservation::try_from(&bytes[1..])?; + let to_sign = fvm_ipld_encoding::to_vec(&obs.observed)?; + let (validator, _) = recover_ecdsa_sig(&to_sign, obs.signature.0, &obs.signature.1)?; + return Ok(Self::V1 { + validator, + payload: obs, + }); + } + + Err(anyhow!("invalid vote version")) + } +} + +impl TryFrom<&[u8]> for CertifiedObservation { + type Error = anyhow::Error; + + fn try_from(bytes: &[u8]) -> Result { + Ok(fvm_ipld_encoding::from_slice(bytes)?) + } +} + +fn recover_ecdsa_sig( + payload: &[u8], + rec_id: i32, + sig: &[u8], +) -> anyhow::Result<(ValidatorKey, Signature)> { + let secp = secp256k1::Secp256k1::new(); + + let message = secp256k1::Message::from_hashed_data::(payload); + let pubkey = secp.recover_ecdsa( + &message, + &RecoverableSignature::from_compact(sig, RecoveryId::from_i32(rec_id)?)?, + )?; + let signature = secp256k1::ecdsa::Signature::from_compact(sig)? + .serialize_der() + .to_vec(); + + Ok(( + ValidatorKey::from_compressed_pubkey(&pubkey.serialize())?, + signature, + )) +} + +impl Display for Ballot { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Ballot(parent_height={}, parent_hash={}, commitment={})", + self.parent_height, + hex::encode(&self.parent_hash), + hex::encode(&self.cumulative_effects_comm), + ) + } +} + +impl Ballot { + pub fn parent_height(&self) -> BlockHeight { + self.parent_height + } +} diff --git a/fendermint/vm/topdown/src/vote/store.rs b/fendermint/vm/topdown/src/vote/store.rs new file mode 100644 index 000000000..a18bb1d21 --- /dev/null +++ b/fendermint/vm/topdown/src/vote/store.rs @@ -0,0 +1,110 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::vote::payload::{Ballot, PowerTable, Vote}; +use crate::vote::{Error, Weight}; +use crate::BlockHeight; +use fendermint_vm_genesis::ValidatorKey; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, HashMap}; + +pub(crate) trait VoteStore { + /// Get the earliest block height of the votes stored + fn earliest_vote_height(&self) -> Result, Error>; + + /// Get the latest block height of the votes stored + fn latest_vote_height(&self) -> Result, Error>; + + /// Store the vote at the target height + fn store_vote(&mut self, height: BlockHeight, vote: Vote) -> Result<(), Error>; + + /// Checks if the validator has voted at the target block height + fn has_voted(&self, height: &BlockHeight, validator: &ValidatorKey) -> Result; + + /// Get the votes at the height. + fn get_votes_at_height(&self, height: BlockHeight) -> Result; + + /// Purge all votes at the specific height. This could be the target height has reached a + /// quorum and the history is not needed. + fn purge_votes_at_height(&mut self, height: BlockHeight) -> Result<(), Error>; +} + +pub(crate) struct InMemoryVoteStore { + votes: BTreeMap>, +} + +impl VoteStore for InMemoryVoteStore { + fn earliest_vote_height(&self) -> Result, Error> { + Ok(self.votes.first_key_value().map(|(k, _)| *k)) + } + + fn latest_vote_height(&self) -> Result, Error> { + Ok(self.votes.last_key_value().map(|(k, _)| *k)) + } + + fn store_vote(&mut self, height: BlockHeight, vote: Vote) -> Result<(), Error> { + match self.votes.entry(height) { + Entry::Vacant(_) => { + let mut map = HashMap::new(); + map.insert(vote.voter(), vote); + self.votes.insert(height, map); + } + Entry::Occupied(mut v) => { + let key = vote.voter(); + v.get_mut().insert(key, vote); + } + } + Ok(()) + } + + fn has_voted(&self, height: &BlockHeight, validator: &ValidatorKey) -> Result { + let Some(votes) = self.votes.get(height) else { + return Ok(false); + }; + Ok(votes.contains_key(validator)) + } + + fn get_votes_at_height(&self, height: BlockHeight) -> Result { + let votes = self + .votes + .get(&height) + .map(|v| v.values().collect()) + .unwrap_or_default(); + Ok(VoteAgg::new(votes)) + } + + fn purge_votes_at_height(&mut self, height: BlockHeight) -> Result<(), Error> { + self.votes.remove(&height); + Ok(()) + } +} + +/// The aggregated votes from different validators. +pub(crate) struct VoteAgg<'a>(Vec<&'a Vote>); + +impl<'a> VoteAgg<'a> { + pub fn new(votes: Vec<&'a Vote>) -> Self { + Self(votes) + } + + pub fn ballot_weights(&self, power_table: &PowerTable) -> Vec<(&Ballot, Weight)> { + let mut votes: Vec<(&Ballot, Weight)> = Vec::new(); + + for v in self.0.iter() { + let validator = v.voter(); + + let power = power_table.get(&validator).cloned().unwrap_or(0); + if power == 0 { + continue; + } + + if let Some(w) = votes.iter_mut().find(|w| w.0 == v.ballot()) { + w.1 += power; + } else { + votes.push((v.ballot(), power)) + } + } + + votes + } +} diff --git a/fendermint/vm/topdown/src/vote/tally.rs b/fendermint/vm/topdown/src/vote/tally.rs new file mode 100644 index 000000000..b1a881316 --- /dev/null +++ b/fendermint/vm/topdown/src/vote/tally.rs @@ -0,0 +1,382 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::vote::payload::{Ballot, PowerTable, Vote}; +use crate::vote::store::VoteStore; +use crate::vote::{Error, Weight}; +use crate::BlockHeight; +use fendermint_vm_genesis::ValidatorKey; +use std::collections::HashMap; + +/// VoteTally aggregates different votes received from various validators in the network +pub(crate) struct VoteTally { + /// Current validator weights. These are the ones who will vote on the blocks, + /// so these are the weights which need to form a quorum. + power_table: PowerTable, + + /// Index votes received by height and hash, which makes it easy to look up + /// all the votes for a given block hash and also to verify that a validator + /// isn't equivocating by trying to vote for two different things at the + /// same height. + votes: S, + + /// The latest height that was voted to be finalized and committed to child blockchian + last_finalized_height: BlockHeight, +} + +impl VoteTally { + /// Initialize the vote tally from the current power table + /// and the last finalized block from the ledger. + pub fn new( + power_table: Vec<(ValidatorKey, Weight)>, + last_finalized_height: BlockHeight, + mut store: S, + ) -> Result { + // purge votes that already committed, no need keep them + if let Some(h) = store.earliest_vote_height()? { + for i in h..=last_finalized_height { + store.purge_votes_at_height(i)?; + } + } + Ok(Self { + power_table: HashMap::from_iter(power_table), + votes: store, + last_finalized_height, + }) + } + + fn power_table(&self) -> &HashMap { + &self.power_table + } + + /// Check that a validator key is currently part of the power table. + fn has_power(&self, validator_key: &ValidatorKey) -> bool { + // For consistency consider validators without power unknown. + match self.power_table.get(validator_key) { + None => false, + Some(weight) => *weight > 0, + } + } + + /// Calculate the minimum weight needed for a proposal to pass with the current membership. + /// + /// This is inclusive, that is, if the sum of weight is greater or equal to this, it should pass. + /// The equivalent formula can be found in CometBFT [here](https://github.com/cometbft/cometbft/blob/a8991d63e5aad8be82b90329b55413e3a4933dc0/types/vote_set.go#L307). + fn quorum_threshold(&self) -> Weight { + let total_weight: Weight = self.power_table.values().sum(); + total_weight * 2 / 3 + 1 + } + + /// Return the height of the first entry in the chain. + /// + /// This is the block that was finalized *in the ledger*. + fn last_finalized_height(&self) -> BlockHeight { + self.last_finalized_height + } + + /// Add a vote we received. + /// + /// Returns `true` if this vote was added, `false` if it was ignored as a + /// duplicate or a height we already finalized, and an error if it's an + /// equivocation or from a validator we don't know. + pub fn add_vote(&mut self, vote: Vote) -> Result { + let validator = vote.voter(); + let parent_height = vote.ballot().parent_height(); + + if !self.has_power(&validator) { + tracing::error!( + validator = validator.to_string(), + "validator unknown or has no power" + ); + return Err(Error::UnpoweredValidator); + } + + if parent_height < self.last_finalized_height() { + tracing::debug!( + parent_height, + last_finalized_height = self.last_finalized_height(), + validator = validator.to_string(), + "reject vote as parent height finalized" + ); + return Ok(false); + } + + if self.votes.has_voted(&parent_height, &validator)? { + tracing::error!( + parent_height, + validator = validator.to_string(), + "equivocation by validator" + ); + return Err(Error::Equivocation); + } + + self.votes.store_vote(parent_height, vote)?; + + Ok(true) + } + + /// Find a block on the (from our perspective) finalized chain that gathered enough votes from validators. + pub fn find_quorum(&self) -> Result, Error> { + let quorum_threshold = self.quorum_threshold(); + let Some(max_height) = self.votes.latest_vote_height()? else { + tracing::info!("vote store has no vote yet, skip finding quorum"); + return Ok(None); + }; + + for h in ((self.last_finalized_height + 1)..max_height).rev() { + let votes = self.votes.get_votes_at_height(h)?; + + for (ballot, weight) in votes.ballot_weights(&self.power_table) { + tracing::info!( + height = h, + ballot = ballot.to_string(), + weight, + quorum_threshold, + "ballot and weight" + ); + + if weight >= quorum_threshold { + return Ok(Some(ballot.clone())); + } + } + + tracing::info!(height = h, "no quorum found"); + } + + Ok(None) + } + + /// Call when a new finalized block is added to the ledger, to clear out all preceding blocks. + /// + /// After this operation the minimum item in the chain will the new finalized block. + pub fn set_finalized(&mut self, block_height: BlockHeight) -> Result<(), Error> { + self.votes.purge_votes_at_height(block_height)?; + self.last_finalized_height = block_height; + Ok(()) + } + + /// Overwrite the power table after it has changed to a new snapshot. + /// + /// This method expects absolute values, it completely replaces the existing powers. + pub fn set_power_table(&mut self, power_table: Vec<(ValidatorKey, Weight)>) { + let power_table = HashMap::from_iter(power_table); + // We don't actually have to remove the votes of anyone who is no longer a validator, + // we just have to make sure to handle the case when they are not in the power table. + self.power_table = power_table; + } + + /// Update the power table after it has changed with changes. + /// + /// This method expects only the updated values, leaving everyone who isn't in it untouched + pub fn update_power_table(&mut self, power_updates: Vec<(ValidatorKey, Weight)>) { + if power_updates.is_empty() { + return; + } + // We don't actually have to remove the votes of anyone who is no longer a validator, + // we just have to make sure to handle the case when they are not in the power table. + for (vk, w) in power_updates { + if w == 0 { + self.power_table.remove(&vk); + } else { + *self.power_table.entry(vk).or_default() = w; + } + } + } +} + +// #[cfg(test)] +// mod tests { +// use crate::voting::payload::{SignedVote, TopdownVote}; +// use crate::voting::VoteTally; +// use crate::BlockHeight; +// use async_stm::atomically_or_err; +// use ipc_ipld_resolver::ValidatorKey; +// use libp2p::identity::Keypair; +// +// fn convert_key(key: &Keypair) -> libp2p::identity::secp256k1::Keypair { +// let key = key.clone(); +// key.try_into_secp256k1().unwrap() +// } +// +// fn random_validator_key() -> (Keypair, ValidatorKey) { +// let key_pair = Keypair::generate_secp256k1(); +// let public_key = key_pair.public(); +// (key_pair, ValidatorKey::from(public_key)) +// } +// +// fn random_vote(height: BlockHeight) -> TopdownVote { +// let rand_bytes = |u: usize| { +// let mut v = vec![]; +// for _ in 0..u { +// v.push(rand::random::()); +// } +// v +// }; +// let hash = rand_bytes(32); +// let commitment = rand_bytes(64); +// +// TopdownVote::v1(height, hash, commitment) +// } +// +// #[tokio::test] +// async fn simple_3_validators_vote() { +// atomically_or_err(|| { +// let validators = (0..3) +// .map(|_| random_validator_key()) +// .collect::>(); +// let powers = validators +// .iter() +// .map(|v| (v.1.clone(), 1)) +// .collect::>(); +// +// let vote_tally = VoteTally::new(powers.clone(), 0); +// +// let votes = (11..15).map(random_vote).collect::>(); +// +// for v in votes.clone() { +// vote_tally.add_block(v)?; +// } +// +// for validator in validators { +// for v in votes.iter() { +// let signed = SignedVote::signed(&convert_key(&validator.0), v).unwrap(); +// assert!(vote_tally.add_vote(signed)?); +// } +// } +// +// let (vote, cert) = vote_tally.find_quorum()?.unwrap(); +// assert_eq!(vote, votes[votes.len() - 1]); +// cert.validate_power_table::<3, 2>(&vote.ballot().unwrap(), im::HashMap::from(powers)) +// .unwrap(); +// +// Ok(()) +// }) +// .await +// .unwrap(); +// } +// +// #[tokio::test] +// async fn new_validators_joined_void_previous_quorum() { +// atomically_or_err(|| { +// let validators = (0..3) +// .map(|_| random_validator_key()) +// .collect::>(); +// let powers = validators +// .iter() +// .map(|v| (v.1.clone(), 1)) +// .collect::>(); +// +// let vote_tally = VoteTally::new(powers.clone(), 0); +// +// let votes = (11..15).map(random_vote).collect::>(); +// +// for v in votes.clone() { +// vote_tally.add_block(v)?; +// } +// +// for validator in validators { +// for v in votes.iter() { +// let signed = SignedVote::signed(&convert_key(&validator.0), v).unwrap(); +// assert!(vote_tally.add_vote(signed)?); +// } +// } +// +// let (vote, cert) = vote_tally.find_quorum()?.unwrap(); +// assert_eq!(vote, votes[votes.len() - 1]); +// cert.validate_power_table::<3, 2>(&vote.ballot().unwrap(), im::HashMap::from(powers)) +// .unwrap(); +// +// let new_powers = (0..3) +// .map(|_| (random_validator_key().1.clone(), 1)) +// .collect::>(); +// vote_tally.update_power_table(new_powers)?; +// assert_eq!(vote_tally.find_quorum()?, None); +// Ok(()) +// }) +// .await +// .unwrap(); +// } +// +// #[tokio::test] +// async fn new_validators_left_formed_quorum() { +// atomically_or_err(|| { +// let validators = (0..3) +// .map(|_| random_validator_key()) +// .collect::>(); +// let mut powers = validators +// .iter() +// .map(|v| (v.1.clone(), 1)) +// .collect::>(); +// let extra_validators = (0..3) +// .map(|_| random_validator_key()) +// .collect::>(); +// for v in extra_validators.iter() { +// powers.push((v.1.clone(), 1)); +// } +// +// let vote_tally = VoteTally::new(powers.clone(), 0); +// +// let votes = (11..15).map(random_vote).collect::>(); +// +// for v in votes.clone() { +// vote_tally.add_block(v)?; +// } +// +// for validator in validators { +// for v in votes.iter() { +// let signed = SignedVote::signed(&convert_key(&validator.0), v).unwrap(); +// assert!(vote_tally.add_vote(signed)?); +// } +// } +// +// assert_eq!(vote_tally.find_quorum()?, None); +// +// let new_powers = extra_validators +// .into_iter() +// .map(|v| (v.1.clone(), 0)) +// .collect::>(); +// vote_tally.update_power_table(new_powers)?; +// let powers = vote_tally.power_table()?; +// let (vote, cert) = vote_tally.find_quorum()?.unwrap(); +// assert_eq!(vote, votes[votes.len() - 1]); +// cert.validate_power_table::<3, 2>(&vote.ballot().unwrap(), powers) +// .unwrap(); +// +// Ok(()) +// }) +// .await +// .unwrap(); +// } +// +// #[tokio::test] +// async fn simple_3_validators_no_quorum() { +// atomically_or_err(|| { +// let validators = (0..3) +// .map(|_| random_validator_key()) +// .collect::>(); +// let powers = validators +// .iter() +// .map(|v| (v.1.clone(), 1)) +// .collect::>(); +// +// let vote_tally = VoteTally::new(powers.clone(), 0); +// +// let votes = [random_vote(10), random_vote(10)]; +// +// vote_tally.add_block(votes[0].clone())?; +// +// let signed = SignedVote::signed(&convert_key(&validators[0].0), &votes[0]).unwrap(); +// assert!(vote_tally.add_vote(signed)?); +// let signed = SignedVote::signed(&convert_key(&validators[1].0), &votes[0]).unwrap(); +// assert!(vote_tally.add_vote(signed)?); +// let signed = SignedVote::signed(&convert_key(&validators[2].0), &votes[1]).unwrap(); +// assert!(vote_tally.add_vote(signed)?); +// +// assert!(vote_tally.find_quorum()?.is_none()); +// +// Ok(()) +// }) +// .await +// .unwrap(); +// } +// }