poa-ballot-stats/src/counter.rs

263 lines
10 KiB
Rust

use crate::contracts::v1::voting::events::{ballot_created as ballot_created_v1, vote as vote_v1};
use crate::contracts::v2::consensus::functions::get_validators as get_validators_fn;
use crate::contracts::v2::key_mgr::events::voting_key_changed;
use crate::contracts::v2::key_mgr::functions::{
get_mining_key_by_voting as get_mining_key_by_voting_fn,
get_voting_by_mining as get_voting_by_mining_fn,
};
use crate::contracts::v2::val_meta::functions::validators as validators_fn;
use crate::contracts::v2::voting::events::{ballot_created, vote};
use crate::contracts::ContractAddresses;
use crate::error::{Error, ErrorKind};
use crate::stats::Stats;
use crate::util::{self, HexList, IntoBallot, TopicFilterExt, Web3LogExt};
use colored::{Color, Colorize};
use ethabi::{Address, Bytes, FunctionOutputDecoder, Uint};
use std::collections::BTreeSet;
use std::default::Default;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use web3;
use web3::futures::Future;
/// The maximum age in seconds of the latest block.
const MAX_BLOCK_AGE: u64 = 60 * 60;
const ERR_BLOCK_NUM: &str = "event is missing block number";
const ERR_EPOCH: &str = "current timestamp is earlier than the Unix epoch";
const ERR_BLOCK: &str = "failed to retrieve block";
/// A vote counter, to read ballot statistics from the blockchain.
pub struct Counter {
verbose: bool,
start_time: SystemTime,
start_block: u64,
addrs: ContractAddresses,
web3: web3::Web3<web3::transports::Http>,
_eloop: web3::transports::EventLoopHandle,
}
impl Counter {
/// Creates a new vote counter.
pub fn new(url: &str, addrs: ContractAddresses) -> Counter {
let (_eloop, transport) = web3::transports::Http::new(url).unwrap();
let web3 = web3::Web3::new(transport);
Counter {
verbose: false,
start_time: UNIX_EPOCH,
start_block: 0,
addrs,
web3,
_eloop,
}
}
/// Enables verbose mode.
pub fn set_verbose(&mut self) {
self.verbose = true;
}
/// Sets the first block to be taken into account, by creation time.
pub fn set_start_time(&mut self, start_time: SystemTime) {
self.start_time = start_time;
}
/// Sets the first block to be taken into account, by number.
pub fn set_start_block(&mut self, start_block: u64) {
self.start_block = start_block;
}
/// Finds all logged ballots and returns statistics about how many were missed by each voter.
pub fn count_votes(&mut self) -> Result<Stats, Error> {
self.check_synced();
// Calls `println!` if `verbose` is `true`.
macro_rules! vprintln { ($($arg:tt)*) => { if self.verbose { println!($($arg)*); } } }
// Find all ballots and voter changes. We don't filter by contract address, so we can make
// a single pass. Contract addresses are checked inside the loop.
let ballot_or_change_filter = ballot_created::filter(None, None, None)
.or(ballot_created_v1::filter(None, None, None))
.or(voting_key_changed::filter(None));
let mut voters: BTreeSet<Address> = BTreeSet::new();
let mut stats = Stats::default();
vprintln!("Collecting events…");
let mut event_found = false;
// Iterate over all ballot and voter change events.
for log in ballot_or_change_filter.logs(&self.web3)? {
let block_num = log.block_number.expect(ERR_BLOCK_NUM).into();
if let Ok(change) = voting_key_changed::parse_log(log.clone().into_raw()) {
if !self.addrs.is_keys_manager(&log.address) {
continue; // Event from another contract instance.
}
event_found = true;
// If it is a `VotingKeyChanged`, update the current set of voters.
vprintln!("• {} {:?}", format!("#{}", block_num).bold(), change);
match change.action.as_str() {
"added" => {
voters.insert(change.key);
}
"removed" => {
voters.remove(&change.key);
}
_ => vprintln!(" Unexpected key change action."),
}
} else if let Ok(ballot) =
ballot_created::parse_log(log.clone().into_raw()).or_else(|_| {
ballot_created_v1::parse_log(log.clone().into_raw()).map(IntoBallot::into)
})
{
if !self.addrs.is_voting(&log.address) {
continue; // Event from another contract instance.
}
event_found = true;
if block_num < self.start_block || self.is_block_too_old(block_num) {
let num = format!("#{}", block_num);
vprintln!("• {} Ballot too old; skipping: {:?}", num.bold(), ballot);
continue;
}
// If it is a `BallotCreated`, find the corresponding votes and update the stats.
vprintln!("• {} {:?}", format!("#{}", block_num).bold(), ballot);
let voted = self.voters_for_ballot(ballot.id)?;
if self.verbose {
self.print_ballot_details(&voters, &voted);
}
voters.extend(voted.iter().cloned());
stats.add_ballot(&voters, &voted);
} else {
return Err(ErrorKind::UnexpectedLogParams.into());
}
}
if !event_found {
return Err(ErrorKind::NoEventsFound.into());
}
// Add all voters we haven't encountered so far.
let mining_keys: Vec<Address> = self.call_poa(get_validators_fn::call())?;
for mining_key in mining_keys {
let voter = self.call_key_mgr(get_voting_by_mining_fn::call(mining_key))?;
if voter.is_zero() {
vprintln!("Voting key for {} is zero. Skipping.", mining_key);
} else if voters.insert(voter) {
eprintln!("Unexpected voter {} (mining key {})", voter, mining_key);
}
}
vprintln!(""); // Add a new line between event log and table.
// Finally, gather the metadata for all voters.
for voter in voters {
let mining_key = match self.call_key_mgr(get_mining_key_by_voting_fn::call(voter)) {
Err(err) => {
eprintln!("Failed to find mining key for voter {}: {:?}", voter, err);
continue;
}
Ok(key) => key,
};
if mining_key.is_zero() {
eprintln!("Mining key for voter {} is zero. Skipping.", voter);
continue;
}
let validator = self.call_val_meta(validators_fn::call(mining_key))?.into();
stats.set_metadata(&voter, mining_key, validator);
}
Ok(stats)
}
fn print_ballot_details(&self, voters: &BTreeSet<Address>, voted: &[Address]) {
let mut unexpected = BTreeSet::new();
let mut expected = BTreeSet::new();
for voter in voted {
if voters.contains(voter) {
expected.insert(*voter);
} else {
unexpected.insert(*voter);
}
}
let missed_filter = |voter: &&Address| !voted.contains(voter);
let missed: BTreeSet<_> = voters.iter().filter(missed_filter).collect();
if !missed.is_empty() {
println!(" Missed: {}", HexList(&missed, Color::Red));
}
if !expected.is_empty() {
println!(" Voted: {}", HexList(voted, Color::Green));
}
if !unexpected.is_empty() {
println!(" Unexpected: {}", HexList(&unexpected, Color::Yellow));
}
}
/// Calls a function of the `ValidatorMetadata` contract and returns the decoded result.
fn call_val_meta<D>(&self, fn_call: (Bytes, D)) -> Result<D::Output, web3::contract::Error>
where
D: FunctionOutputDecoder,
{
util::raw_call(self.addrs.v2.metadata_address, &self.web3.eth(), fn_call)
}
/// Calls a function of the `KeysManager` contract and returns the decoded result.
fn call_key_mgr<D>(&self, fn_call: (Bytes, D)) -> Result<D::Output, web3::contract::Error>
where
D: FunctionOutputDecoder,
{
util::raw_call(
self.addrs.v2.keys_manager_address,
&self.web3.eth(),
fn_call,
)
}
/// Calls a function of the `PoaNetworkConsensus` contract and returns the decoded result.
fn call_poa<D>(&self, fn_call: (Bytes, D)) -> Result<D::Output, web3::contract::Error>
where
D: FunctionOutputDecoder,
{
util::raw_call(self.addrs.v2.poa_address, &self.web3.eth(), fn_call)
}
fn voters_for_ballot(&self, id: Uint) -> Result<Vec<Address>, Error> {
let vote_filter = vote::filter(id, None).or(vote_v1::filter(id, None));
let is_voting = |log: &web3::types::Log| self.addrs.is_voting(&log.address);
vote_filter
.logs(&self.web3)?
.into_iter()
.filter(is_voting)
.map(|vote_log| {
vote::parse_log(vote_log.clone().into_raw())
.map(|vote| vote.voter)
.or_else(|_| vote_v1::parse_log(vote_log.into_raw()).map(|vote| vote.voter))
.map_err(Error::from)
})
.collect()
}
/// Returns `true` if the block with the given number is older than `start_time`.
fn is_block_too_old(&self, block_num: u64) -> bool {
self.is_block_older_than(
web3::types::BlockNumber::Number(block_num),
&self.start_time,
)
}
/// Shows a warning if the node's latest block is outdated.
fn check_synced(&self) {
let min_time = SystemTime::now() - Duration::from_secs(MAX_BLOCK_AGE);
if self.is_block_older_than(web3::types::BlockNumber::Latest, &min_time) {
eprintln!("WARNING: The node is not fully synchronized. Stats may be inaccurate.");
}
}
/// Returns `true` if the block with the given number was created before the given time.
fn is_block_older_than(&self, number: web3::types::BlockNumber, time: &SystemTime) -> bool {
let id = web3::types::BlockId::Number(number);
let block_result = self.web3.eth().block(id).wait();
let block = block_result.expect(ERR_BLOCK).expect(ERR_BLOCK);
let seconds = time.duration_since(UNIX_EPOCH).expect(ERR_EPOCH).as_secs();
block.timestamp < seconds.into()
}
}