263 lines
10 KiB
Rust
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()
|
|
}
|
|
}
|