Merge pull request #3 from poanetwork/afck--logging-json

Improve logging, add contract files.
This commit is contained in:
Andreas Fackler 2018-05-15 17:52:12 +02:00 committed by GitHub
commit ac7e6e80d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 875 additions and 607 deletions

65
.travis.yml Normal file
View File

@ -0,0 +1,65 @@
# Based on the "trust" template v0.1.2
# https://github.com/japaric/trust/tree/v0.1.2
dist: trusty
language: rust
services: docker
sudo: required
rust: nightly-2018-04-19
cache: cargo
env:
global:
- CRATE_NAME=poa-ballot-stats
- RUST_BACKTRACE=1
- RUSTFLAGS="-D warnings"
matrix:
include:
- env: TARGET=x86_64-unknown-linux-gnu
- env: TARGET=x86_64-apple-darwin
os: osx
before_install:
- set -e
- rustup self update
- rustup component add rustfmt-preview
# - cargo install clippy -f --vers=0.0.195
install:
- sh ci/install.sh
- source ~/.cargo/env || true
script:
- bash ci/script.sh
after_script: set +e
before_deploy:
- sh ci/before_deploy.sh
deploy:
api_key:
secure: mj9bnqoc/lbsLv5Wiek37YY7BvOp+CN/Ier/uXC32uSRsJp5ue5/aru181oEhMI+6yeRPQyCrzzpoqMHcM5ARAp0GFW1YOkwqsnzaaiTMYTLGdLxtYBVNCrcLlUU474fvw92GCjhX2Ag8NpaQRYAaD1DMrkBTJ2qYJfm5zaXwByVu1bP1JX2zQl5mx6+/5j2DtrzQwMzRNBGzrJDuodRaeZ+/+cZvTLKkP4JaV7/iSuQ19NptSkLfflvB28J/XOnZ6m4mHbqGAfeNOB9YtH2ag70bvM1qfz4Y1fbphh8NJ4tngslxptOQ2oktAglebmthjiUAu8rhE1V63YioY2e/GaLIq3GSWOCleU6/1IvAac6VjjPBpaw6RqQgCVUCc+w6+CxmaY1CZZII2whM5FqE9AC22oTvK2SUfIzRXolYqUVYZsHI+75DBqExyE3QwW1T7IMTzqK25uokdSZBHdvC5yxZQVjDsuD+TukCOyPDmKW8Dlnn6B5XVTWruF+qALzQA1T8qy++QPz1O5i2lM61SR+iYlQQ/+IEr/je0rw1cpC9zp2/FFb/FAOyXFrGk5UGRSsewBz7j0gsqDin5zrqqBuRv+/hlEgXBJ7xFpOQVPJDk7n/Wz86whQLu7VUcKB3w/3+MO20i0DhXI4Rx8otrVWgDMq8cJ1mGN8bZhl0k8=
file_glob: true
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
on:
condition: $TRAVIS_RUST_VERSION = nightly-2018-04-19
tags: true
provider: releases
skip_cleanup: true
cache: cargo
before_cache:
# Travis can't cache files that are not readable by "others"
- chmod -R a+r $HOME/.cargo
branches:
only:
# release tags
- /^v\d+\.\d+\.\d+.*$/
- master
notifications:
email:
on_success: never

437
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,8 @@ clap = "2.31.2"
colored = "1.6.0"
error-chain = { version = "0.11", default-features = false }
ethabi = "5.1.1"
ethabi-contract = "5.1.0"
ethabi-derive = "5.1.2"
serde = "1.0.36"
serde_derive = "1.0.36"
serde_json = "1.0.13"

View File

@ -1,20 +1,28 @@
# POA ballot stats
**Note**: This is still work in progress. It doesn't yet correctly determine the initial set of
validators.
A command line tool that displays voting statistics for the [POA network](https://poa.network/).
It requires a recent version of [Rust](https://www.rust-lang.org/), and needs to communicate with a
fully synchronized node that is connected to the network:
[POA installation](https://github.com/poanetwork/wiki/wiki/POA-Installation).
Note that `poa-ballot-stats` needs access to the network's full logs, so the node must run with
`--pruning=archive --no-warp`.
You can view the command line options with `-h`, and specify a different endpoint if your node e.g.
uses a non-standard port. The `-c` option takes a map with the POA contracts' addresses in JSON
format. You can find the current maps for the main and test network in
[poa-chain-spec](https://github.com/poanetwork/poa-chain-spec)'s `core` and `sokol` branches.
uses a non-standard port. By default, it tries to connect to a local node `http://127.0.0.1:8545`.
In verbose mode, with `-v`, the complete list of collected events is displayed.
The `-c` option takes a map with the POA contracts' addresses in JSON format. You can find the
current maps for the main and test network the `contracts` folder. By default, it uses `core.json`,
for the main network.
Examples:
```bash
$ cargo run -- -h
$ cargo run -- -c ../poa-chain-spec/contracts.json http://127.0.0.1:8545
$ cargo run
$ cargo run -- -c contracts/sokol.json https://sokol.poa.network -v
```
## Screenshot
![Screenshot](screenshot.png)

View File

@ -3,7 +3,7 @@
"constant": true,
"inputs": [
{
"name": "",
"name": "_miningKey",
"type": "address"
}
],
@ -31,7 +31,7 @@
},
{
"name": "zipcode",
"type": "uint256"
"type": "bytes32"
},
{
"name": "expirationDate",
@ -58,7 +58,7 @@
"constant": true,
"inputs": [
{
"name": "",
"name": "_newProxyAddress",
"type": "address"
}
],
@ -67,12 +67,54 @@
{
"name": "count",
"type": "uint256"
},
{
"name": "voters",
"type": "address[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_firstName",
"type": "bytes32"
},
{
"name": "_lastName",
"type": "bytes32"
},
{
"name": "_licenseId",
"type": "bytes32"
},
{
"name": "_fullAddress",
"type": "string"
},
{
"name": "_state",
"type": "bytes32"
},
{
"name": "_zipcode",
"type": "bytes32"
},
{
"name": "_expirationDate",
"type": "uint256"
}
],
"name": "createMetadata",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -105,7 +147,7 @@
"constant": true,
"inputs": [
{
"name": "",
"name": "_miningKey",
"type": "address"
}
],
@ -114,6 +156,10 @@
{
"name": "count",
"type": "uint256"
},
{
"name": "voters",
"type": "address[]"
}
],
"payable": false,
@ -134,6 +180,20 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "version",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -149,50 +209,17 @@
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_firstName",
"type": "bytes32"
},
{
"name": "_lastName",
"type": "bytes32"
},
{
"name": "_licenseId",
"type": "bytes32"
},
{
"name": "_fullAddress",
"type": "string"
},
{
"name": "_state",
"type": "bytes32"
},
{
"name": "_zipcode",
"type": "uint256"
},
{
"name": "_expirationDate",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "changeRequestForValidator",
"constant": true,
"inputs": [],
"name": "implementation",
"outputs": [
{
"name": "",
"type": "bool"
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"stateMutability": "view",
"type": "function"
},
{
@ -239,20 +266,31 @@
},
{
"name": "_zipcode",
"type": "uint256"
"type": "bytes32"
},
{
"name": "_expirationDate",
"type": "uint256"
}
],
"name": "changeRequest",
"outputs": [
},
{
"name": "",
"type": "bool"
"name": "_createdDate",
"type": "uint256"
},
{
"name": "_updatedDate",
"type": "uint256"
},
{
"name": "_minThreshold",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "initMetadata",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
@ -313,6 +351,96 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_firstName",
"type": "bytes32"
},
{
"name": "_lastName",
"type": "bytes32"
},
{
"name": "_licenseId",
"type": "bytes32"
},
{
"name": "_fullAddress",
"type": "string"
},
{
"name": "_state",
"type": "bytes32"
},
{
"name": "_zipcode",
"type": "bytes32"
},
{
"name": "_expirationDate",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "changeRequestForValidator",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_firstName",
"type": "bytes32"
},
{
"name": "_lastName",
"type": "bytes32"
},
{
"name": "_licenseId",
"type": "bytes32"
},
{
"name": "_fullAddress",
"type": "string"
},
{
"name": "_state",
"type": "bytes32"
},
{
"name": "_zipcode",
"type": "bytes32"
},
{
"name": "_expirationDate",
"type": "uint256"
}
],
"name": "changeRequest",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -408,37 +536,8 @@
},
{
"constant": false,
"inputs": [
{
"name": "_firstName",
"type": "bytes32"
},
{
"name": "_lastName",
"type": "bytes32"
},
{
"name": "_licenseId",
"type": "bytes32"
},
{
"name": "_fullAddress",
"type": "string"
},
{
"name": "_state",
"type": "bytes32"
},
{
"name": "_zipcode",
"type": "uint256"
},
{
"name": "_expirationDate",
"type": "uint256"
}
],
"name": "createMetadata",
"inputs": [],
"name": "initMetadataDisable",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
@ -462,7 +561,7 @@
"constant": true,
"inputs": [
{
"name": "",
"name": "_miningKey",
"type": "address"
}
],
@ -490,7 +589,7 @@
},
{
"name": "zipcode",
"type": "uint256"
"type": "bytes32"
},
{
"name": "expirationDate",
@ -513,17 +612,6 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_proxyStorage",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [

32
ci/before_deploy.sh Normal file
View File

@ -0,0 +1,32 @@
# This script takes care of building your crate and packaging it for release
set -ex
main() {
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
test -f Cargo.lock || cargo generate-lockfile
cross rustc --bin poa-ballot-stats --target $TARGET --release -- -C lto
cp target/$TARGET/release/poa-ballot-stats $stage/
cp -r contracts $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main

27
ci/install.sh Normal file
View File

@ -0,0 +1,27 @@
set -ex
main() {
local target=
if [ $TRAVIS_OS_NAME = linux ]; then
target=x86_64-unknown-linux-musl
sort=sort
else
target=x86_64-apple-darwin
sort=gsort # for `sort --sort-version`, from brew's coreutils.
fi
# This fetches latest stable release
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
| cut -d/ -f3 \
| grep -E '^v[0.1.0-9.]+$' \
| $sort --version-sort \
| tail -n1)
curl -LSfs https://japaric.github.io/trust/install.sh | \
sh -s -- \
--force \
--git japaric/cross \
--tag $tag \
--target $target
}
main

22
ci/script.sh Normal file
View File

@ -0,0 +1,22 @@
set -ex
main() {
cargo fmt -- --write-mode=diff
cross build --target $TARGET
cross build --target $TARGET --release
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --target $TARGET
cross test --target $TARGET --release
cross build --target $TARGET --release
}
# we don't run the "test phase" when doing deploys
if [ -z $TRAVIS_TAG ]; then
main
fi

11
contracts/core.json Normal file
View File

@ -0,0 +1,11 @@
{
"VOTING_TO_CHANGE_KEYS_ADDRESS": "0x215794efe4b86a2fbcbf706bc9ade63663f1eae1",
"VOTING_TO_CHANGE_MIN_THRESHOLD_ADDRESS": "0xca863b0d12193a87b5173fd51fa4aa1703fb8a32",
"VOTING_TO_CHANGE_PROXY_ADDRESS": "0x9c8a06f0197ee718cd820adeb48a88ea2a9b5c48",
"BALLOTS_STORAGE_ADDRESS": "0x3a28ecc276d222829f78c98d43d719eafda0a6fe",
"KEYS_MANAGER_ADDRESS": "0x2b1dbc7390a65dc40f7d64d67ea11b4d627dd1bf",
"METADATA_ADDRESS": "0x4c0eb450d8dfa6e89eb14ac154867bc86b3c559c",
"PROXY_ADDRESS": "0x6f4aadbb17789b4f5e9e97d456dc4e01b117ccb3",
"POA_ADDRESS": "0x83451c8bc04d4ee9745ccc58edfab88037bc48cc",
"MOC": "0xCf260eA317555637C55F70e55dbA8D5ad8414Cb0"
}

11
contracts/sokol.json Normal file
View File

@ -0,0 +1,11 @@
{
"VOTING_TO_CHANGE_KEYS_ADDRESS": "0xc40cdf254a4a35498aa84f35e9842c110729a2a0",
"VOTING_TO_CHANGE_MIN_THRESHOLD_ADDRESS": "0x700db8ba3128087f3b23f60de4bc3179bafa467d",
"VOTING_TO_CHANGE_PROXY_ADDRESS": "0x0aa4a75549757a90f62f88b3b96b69bead2db0ff",
"BALLOTS_STORAGE_ADDRESS": "0x27e7d2572aa37bec2ed30795f2fabccda4781f86",
"KEYS_MANAGER_ADDRESS": "0x1aa02bd52fe418ac70263351282f66f1dacf898c",
"METADATA_ADDRESS": "0xf71dd3797e4f173c2c08f2cebe8a6801d8191b42",
"PROXY_ADDRESS": "0x3f918617a055d48e90f9fe06c168a75134565190",
"POA_ADDRESS": "0x03048F666359CFD3C74a1A5b9a97848BF71d5038",
"MOC": "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

View File

@ -12,5 +12,9 @@ error_chain! {
UnexpectedLogParams {
description("Unexpected parameter types in log"),
}
NoEventsFound {
description("No events found. \
Make sure your node is running in 'full' mode, not 'light'."),
}
}
}

View File

@ -1,119 +0,0 @@
use error::{ErrorKind, Result};
use ethabi::{Address, FixedBytes, Log, RawTopicFilter, Token, Topic, Uint};
use util::LogExt;
/// An event that is logged when the current set of validators has changed.
#[derive(Debug)]
pub struct ChangeFinalized {
/// The new set of validators.
pub new_set: Vec<Address>,
}
impl ChangeFinalized {
/// Parses the log and returns a `ChangeFinalized`, if the log corresponded to such an event.
pub fn from_log(log: &Log) -> Result<ChangeFinalized> {
log.param(0, "newSet")
.cloned()
.and_then(Token::to_array)
.map(|tokens| ChangeFinalized {
new_set: tokens.into_iter().filter_map(Token::to_address).collect(),
})
.ok_or_else(|| ErrorKind::UnexpectedLogParams.into())
}
}
#[derive(Debug)]
pub struct InitiateChange {
/// The previous voter set's hash.
parent_hash: FixedBytes,
/// The new set of validators.
pub new_set: Vec<Address>,
}
impl InitiateChange {
/// Parses the log and returns a `InitiateChange`, if the log corresponded to such an event.
pub fn from_log(log: &Log) -> Result<InitiateChange> {
match (
log.param(0, "parentHash")
.cloned()
.and_then(Token::to_fixed_bytes),
log.param(1, "newSet").cloned().and_then(Token::to_array),
) {
(Some(parent_hash), Some(tokens)) => Ok(InitiateChange {
parent_hash,
new_set: tokens.into_iter().filter_map(Token::to_address).collect(),
}),
_ => Err(ErrorKind::UnexpectedLogParams.into()),
}
}
}
/// An event that is logged when a new ballot is started.
#[derive(Debug)]
pub struct BallotCreated {
/// The ballot ID.
pub id: Uint,
/// The ballot type.
ballot_type: Uint,
/// The creator's voting key.
creator: Address,
}
impl BallotCreated {
/// Parses the log and returns a `BallotCreated`, if the log corresponded to such an event.
pub fn from_log(log: &Log) -> Result<BallotCreated> {
match (
log.uint_param(0, "id"),
log.uint_param(1, "ballotType"),
log.address_param(2, "creator"),
) {
(Some(&id), Some(&ballot_type), Some(&creator)) => Ok(BallotCreated {
id,
ballot_type,
creator,
}),
_ => Err(ErrorKind::UnexpectedLogParams.into()),
}
}
/// Returns a topic filter to find the votes corresponding to this ballot.
pub fn vote_topic_filter(&self) -> RawTopicFilter {
RawTopicFilter {
topic0: Topic::This(Token::Uint(self.id)),
..RawTopicFilter::default()
}
}
}
/// An event that is logged whenever someone casts a vote in a ballot.
#[derive(Debug)]
pub struct Vote {
/// The ballot ID.
id: Uint,
/// The decision this vote is for.
decision: Uint,
/// The voter's voting key.
pub voter: Address,
/// The timestamp of this vote.
time: Uint,
}
impl Vote {
/// Parses the log and returns a `Vote`, if the log corresponded to such an event.
pub fn from_log(log: &Log) -> Result<Vote> {
match (
log.uint_param(0, "id"),
log.uint_param(1, "decision"),
log.address_param(2, "voter"),
log.uint_param(3, "time"),
) {
(Some(&id), Some(&decision), Some(&voter), Some(&time)) => Ok(Vote {
id,
decision,
voter,
time,
}),
_ => Err(ErrorKind::UnexpectedLogParams.into()),
}
}
}

View File

@ -3,33 +3,50 @@ extern crate colored;
#[macro_use]
extern crate error_chain;
extern crate ethabi;
#[macro_use(EthabiContract)]
extern crate ethabi_derive;
#[macro_use(use_contract)]
extern crate ethabi_contract;
extern crate serde;
#[macro_use]
#[macro_use(Deserialize)]
extern crate serde_derive;
extern crate serde_json;
extern crate web3;
mod cli;
mod error;
mod events;
mod stats;
mod util;
mod validator;
use error::{Error, ErrorKind};
use events::{BallotCreated, ChangeFinalized, InitiateChange, Vote};
use ethabi::Address;
use stats::Stats;
use std::default::Default;
use std::fs::File;
use util::{ContractExt, TopicFilterExt, Web3LogExt};
use std::time::{SystemTime, UNIX_EPOCH};
use util::{HexBytes, HexList, TopicFilterExt, Web3LogExt};
use web3::futures::Future;
// TODO: `ethabi_derive` produces unparseable tokens.
// mod voting_to_change_keys {
// #[derive(EthabiContract)]
// #[ethabi_contract_options(name = "VotingToChangeKeys", path = "abi/VotingToChangeKeys.json")]
// struct _Dummy;
// }
/// The maximum age in seconds of the latest block.
const MAX_BLOCK_AGE: u64 = 60 * 60;
use_contract!(
net_con,
"NetworkConsensus",
"abi/PoaNetworkConsensus.abi.json"
);
use_contract!(
voting,
"VotingToChangeKeys",
"abi/VotingToChangeKeys.abi.json"
);
use_contract!(
val_meta,
"ValidatorMetadata",
"abi/ValidatorMetadata.abi.json"
);
use_contract!(key_mgr, "KeysManager", "abi/KeysManager.abi.json");
#[derive(Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
@ -38,12 +55,15 @@ struct ContractAddresses {
keys_manager_address: String,
}
impl Default for ContractAddresses {
fn default() -> ContractAddresses {
ContractAddresses {
metadata_address: "0x4c0eb450d8dfa6e89eb14ac154867bc86b3c559c".to_string(),
keys_manager_address: "0x2b1dbc7390a65dc40f7d64d67ea11b4d627dd1bf".to_string(),
}
/// Shows a warning if the node's latest block is outdated.
fn check_synced<T: web3::Transport>(web3: &web3::Web3<T>) {
let id = web3::types::BlockId::Number(web3::types::BlockNumber::Latest);
let block = web3.eth().block(id).wait().expect("get latest block");
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Current timestamp is earlier than the Unix epoch!");
if block.timestamp < (now.as_secs() - MAX_BLOCK_AGE).into() {
eprintln!("WARNING: The node is not fully synchronized. Stats may be inaccurate.");
}
}
@ -53,105 +73,112 @@ fn count_votes(
verbose: bool,
contract_addrs: &ContractAddresses,
) -> Result<Stats, Error> {
// Calls `println!` if `verbose` is `true`.
macro_rules! vprintln { ($($arg:tt)*) => { if verbose { println!($($arg)*); } } }
let (_eloop, transport) = web3::transports::Http::new(url).unwrap();
let web3 = web3::Web3::new(transport);
let voting_abi = File::open("abi/VotingToChangeKeys.abi.json").expect("read voting abi");
let net_con_abi = File::open("abi/PoaNetworkConsensus.abi.json").expect("read consensus abi");
let val_meta_abi = File::open("abi/ValidatorMetadata.abi.json").expect("read val meta abi");
let key_mgr_abi = File::open("abi/KeysManager.abi.json").expect("read key mgr abi");
check_synced(&web3);
let voting_contract = ethabi::Contract::load(voting_abi)?;
let net_con_contract = ethabi::Contract::load(net_con_abi)?;
let val_meta_contract = ethabi::Contract::load(val_meta_abi)?;
let key_mgr_contract = ethabi::Contract::load(key_mgr_abi)?;
let voting_contract = voting::VotingToChangeKeys::default();
let net_con_contract = net_con::NetworkConsensus::default();
let val_meta_contract = val_meta::ValidatorMetadata::default();
let key_mgr_contract = key_mgr::KeysManager::default();
let val_meta_addr = util::parse_address(&contract_addrs.metadata_address).unwrap();
let web3_val_meta = web3::contract::Contract::new(web3.eth(), val_meta_addr, val_meta_contract);
let key_mgr_addr = util::parse_address(&contract_addrs.keys_manager_address).unwrap();
let web3_key_mgr = web3::contract::Contract::new(web3.eth(), key_mgr_addr, key_mgr_contract);
let val_meta_addr =
util::parse_address(&contract_addrs.metadata_address).expect("parse contract address");
let key_mgr_addr =
util::parse_address(&contract_addrs.keys_manager_address).expect("parse contract address");
let ballot_event = voting_contract.event("BallotCreated")?;
let vote_event = voting_contract.event("Vote")?;
let change_event = net_con_contract.event("ChangeFinalized")?;
let init_change_event = net_con_contract.event("InitiateChange")?;
let ballot_event = voting_contract.events().ballot_created();
let vote_event = voting_contract.events().vote();
let change_event = net_con_contract.events().change_finalized();
let init_change_event = net_con_contract.events().initiate_change();
// Find all ballots and voter changes.
let ballot_or_change_filter = ethabi::TopicFilter {
topic0: ethabi::Topic::OneOf(vec![
ballot_event.signature(),
change_event.signature(),
init_change_event.signature(),
]),
..ethabi::TopicFilter::default()
}.to_filter_builder()
.build();
let ballot_change_logs_filter = web3.eth_filter()
.create_logs_filter(ballot_or_change_filter)
.wait()?;
let ballot_or_change_filter = (ballot_event.create_filter(None, None, None))
.or(change_event.create_filter())
.or(init_change_event.create_filter(None));
// FIXME: Find out why we see no `ChangeFinalized` events, and how to obtain the initial voters.
let mut voters: Vec<ethabi::Address> = Vec::new();
let mut voters: Vec<Address> = Vec::new();
let mut stats = Stats::default();
let mut prev_init_change: Option<InitiateChange> = None;
let mut prev_init_change: Option<net_con::logs::InitiateChange> = None;
vprintln!("Collecting events…");
let mut event_found = false;
// Iterate over all ballot and voter change events.
for log in ballot_change_logs_filter.logs().wait()? {
if let Ok(change_log) = change_event.parse_log(log.clone().into_raw()) {
for log in ballot_or_change_filter.logs(&web3)? {
event_found = true;
if let Ok(change) = change_event.parse_log(log.clone().into_raw()) {
// If it is a `ChangeFinalized`, update the current set of voters.
let change = ChangeFinalized::from_log(&change_log)?;
if verbose {
println!("{:?}", change);
}
vprintln!(
"• ChangeFinalized {{ new_set: {} }}",
HexList(&change.new_set)
);
voters = change.new_set;
} else if let Ok(init_change_log) = init_change_event.parse_log(log.clone().into_raw()) {
} else if let Ok(init_change) = init_change_event.parse_log(log.clone().into_raw()) {
// If it is an `InitiateChange`, update the current set of voters.
let init_change = InitiateChange::from_log(&init_change_log)?;
if verbose {
println!("{:?}", init_change);
}
vprintln!(
"• InitiateChange {{ parent_hash: {}, new_set: {} }}",
HexBytes(&init_change.parent_hash),
HexList(&init_change.new_set)
);
if let Some(prev) = prev_init_change.take() {
let raw_call = util::raw_call(key_mgr_addr, web3.eth());
let get_voting_by_mining_fn = key_mgr_contract.functions().get_voting_by_mining();
voters = vec![];
for mining_key in prev.new_set {
let voter = web3_key_mgr.simple_query("getVotingByMining", mining_key)?;
if voter != ethabi::Address::zero() {
let voter = get_voting_by_mining_fn.call(mining_key, &*raw_call)?;
if voter != Address::zero() {
voters.push(voter);
}
}
}
prev_init_change = Some(init_change);
} else if let Ok(ballot_log) = ballot_event.parse_log(log.into_raw()) {
} else if let Ok(ballot) = ballot_event.parse_log(log.into_raw()) {
// If it is a `BallotCreated`, find the corresponding votes and update the stats.
let ballot = BallotCreated::from_log(&ballot_log)?;
if verbose {
println!("{:?}", ballot);
}
let vote_filter = vote_event
.create_filter(ballot.vote_topic_filter())?
.to_filter_builder()
.build();
let vote_logs_filter = web3.eth_filter().create_logs_filter(vote_filter).wait()?;
let mut votes: Vec<Vote> = Vec::new();
for vote_log in vote_logs_filter.logs().wait()? {
let vote = Vote::from_log(&vote_event.parse_log(vote_log.into_raw())?)?;
if !voters.contains(&vote.voter) {
if verbose {
eprintln!("Unexpected voter {} for ballot {}", vote.voter, ballot.id);
vprintln!("• {:?}", ballot);
let votes = vote_event
.create_filter(ballot.id, None)
.logs(&web3)?
.into_iter()
.map(|vote_log| {
let vote = vote_event.parse_log(vote_log.into_raw())?;
if !voters.contains(&vote.voter) {
vprintln!(" Unexpected voter {}", vote.voter);
voters.push(vote.voter);
}
voters.push(vote.voter);
}
votes.push(vote);
}
Ok(vote)
})
.collect::<Result<Vec<_>, Error>>()?;
stats.add_ballot(&voters, &votes);
} else {
return Err(ErrorKind::UnexpectedLogParams.into());
}
}
if !event_found {
return Err(ErrorKind::NoEventsFound.into());
}
vprintln!(""); // Add a new line between event log and table.
// Finally, gather the metadata for all voters.
let raw_call = util::raw_call(val_meta_addr, web3.eth());
let get_mining_by_voting_key_fn = val_meta_contract.functions().get_mining_by_voting_key();
let validators_fn = val_meta_contract.functions().validators();
for voter in voters {
let mining_key = web3_val_meta.simple_query("getMiningByVotingKey", voter)?;
let validator = web3_val_meta.simple_query("validators", mining_key)?;
let mining_key = match get_mining_by_voting_key_fn.call(voter, &*raw_call) {
Err(err) => {
eprintln!("Failed to find mining key for voter {}: {:?}", voter, err);
continue;
}
Ok(key) => key,
};
let validator = validators_fn.call(mining_key, &*raw_call)?.into();
stats.set_metadata(&voter, mining_key, validator);
}
Ok(stats)
@ -161,13 +188,11 @@ fn main() {
let matches = cli::get_matches();
let url = matches.value_of("url").unwrap_or("http://127.0.0.1:8545");
let verbose = matches.is_present("verbose");
let contract_addrs = matches
let contract_file = matches
.value_of("contracts")
.map(|filename| {
let file = File::open(filename).expect("open contracts file");
serde_json::from_reader(file).expect("parse contracts file")
})
.unwrap_or_default();
.unwrap_or("contracts/core.json");
let file = File::open(contract_file).expect("open contracts file");
let contract_addrs = serde_json::from_reader(file).expect("parse contracts file");
let stats = count_votes(url, verbose, &contract_addrs).expect("count votes");
println!("{}", stats);
}

View File

@ -1,9 +1,9 @@
use colored::{Color, Colorize};
use ethabi::Address;
use events::Vote;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use validator::Validator;
use voting;
/// The count of ballots and cast votes, as well as metadata for a particular voter.
#[derive(Clone, Default)]
@ -27,7 +27,7 @@ pub struct Stats {
impl Stats {
/// Adds a ballot: `voters` are the voting keys of everyone who was allowed to cast a vote, and
/// `votes` are the ones that were actually cast.
pub fn add_ballot(&mut self, voters: &[Address], votes: &[Vote]) {
pub fn add_ballot(&mut self, voters: &[Address], votes: &[voting::logs::Vote]) {
for voter in voters {
let mut vs = self.voter_stats
.entry(voter.clone())

View File

@ -1,75 +1,124 @@
use ethabi;
use std::u8;
use ethabi::{self, Address, Bytes};
use std::str::FromStr;
use std::{fmt, u8};
use web3;
use web3::futures::Future;
// TODO: Evaluate whether any of these would make sense to include in `web3`.
/// Converts the bytes to a string, interpreting them as null-terminated UTF-8.
pub fn bytes_to_string(bytes: &[u8]) -> String {
let zero = bytes
.iter()
.position(|b| *b == 0)
.unwrap_or_else(|| bytes.len());
String::from_utf8_lossy(&bytes[..zero]).to_string()
}
/// Parses the string as a 40-digit hexadecimal number, and returns the corresponding `Address`.
pub fn parse_address(mut s: &str) -> Option<ethabi::Address> {
let mut bytes = [0u8; 20];
pub fn parse_address(mut s: &str) -> Option<Address> {
if &s[..2] == "0x" {
s = &s[2..];
}
for i in 0..20 {
match u8::from_str_radix(&s[(2 * i)..(2 * i + 2)], 16) {
Ok(b) => bytes[i] = b,
Err(_) => return None,
Address::from_str(s).ok()
}
/// Returns a wrapper of a contract address, to make function calls using the latest block.
pub fn raw_call<T: web3::Transport + 'static>(
to: Address,
eth: web3::api::Eth<T>,
) -> Box<Fn(Bytes) -> Result<Bytes, String>> {
Box::new(move |bytes: Bytes| -> Result<Bytes, String> {
let req = web3::types::CallRequest {
from: None,
to,
gas: None,
gas_price: None,
value: None,
data: Some(bytes.into()),
};
eth.call(req, Some(web3::types::BlockNumber::Latest))
.wait()
.map(|bytes| bytes.0)
.map_err(|err| err.to_string())
})
}
trait TopicExt<T> {
/// Returns the union of the two topics.
fn or(self, other: Self) -> Self;
/// Converts this topic into an `Option<Vec<T>>`, where `Any` corresponds to `None`,
/// `This` to a vector with one element, and `OneOf` to any vector.
fn to_opt_vec(self) -> Option<Vec<T>>;
}
impl<T: Ord> TopicExt<T> for ethabi::Topic<T> {
fn or(self, other: Self) -> Self {
match (self.to_opt_vec(), other.to_opt_vec()) {
(Some(mut v0), Some(v1)) => {
for e in v1 {
if !v0.contains(&e) {
v0.push(e);
}
}
if v0.len() == 1 {
ethabi::Topic::This(v0.into_iter().next().expect("has a single element; qed"))
} else {
ethabi::Topic::OneOf(v0)
}
}
(_, _) => ethabi::Topic::Any,
}
}
Some(ethabi::Address::from_slice(&bytes))
}
pub trait ContractExt {
fn simple_query<P, R>(&self, func: &str, params: P) -> Result<R, web3::contract::Error>
where
R: web3::contract::tokens::Detokenize,
P: web3::contract::tokens::Tokenize;
}
impl ContractExt for web3::contract::Contract<web3::transports::Http> {
/// Calls a constant function with the latest block and default parameters.
fn simple_query<P, R>(&self, func: &str, params: P) -> Result<R, web3::contract::Error>
where
R: web3::contract::tokens::Detokenize,
P: web3::contract::tokens::Tokenize,
{
self.query(
func,
params,
None,
web3::contract::Options::default(),
web3::types::BlockNumber::Latest,
).wait()
fn to_opt_vec(self) -> Option<Vec<T>> {
match self {
ethabi::Topic::Any => None,
ethabi::Topic::OneOf(v) => Some(v),
ethabi::Topic::This(t) => Some(vec![t]),
}
}
}
pub trait TopicFilterExt {
/// Returns a `web3::types::FilterBuilder` with these topics, starting from the first block.
fn to_filter_builder(self) -> web3::types::FilterBuilder;
/// Returns the "disjunction" of the two filters, i.e. it filters for everything that matches
/// at least one of the two in every topic.
fn or(self, other: ethabi::TopicFilter) -> ethabi::TopicFilter;
/// Returns the vector of logs that match this filter.
fn logs<T: web3::Transport>(
self,
web3: &web3::Web3<T>,
) -> Result<Vec<web3::types::Log>, web3::error::Error>;
}
impl TopicFilterExt for ethabi::TopicFilter {
fn to_filter_builder(self) -> web3::types::FilterBuilder {
web3::types::FilterBuilder::default()
.topics(
to_topic(self.topic0),
to_topic(self.topic1),
to_topic(self.topic2),
to_topic(self.topic3),
self.topic0.to_opt_vec(),
self.topic1.to_opt_vec(),
self.topic2.to_opt_vec(),
self.topic3.to_opt_vec(),
)
.from_block(web3::types::BlockNumber::Earliest)
.to_block(web3::types::BlockNumber::Latest)
}
fn or(self, other: ethabi::TopicFilter) -> ethabi::TopicFilter {
ethabi::TopicFilter {
topic0: self.topic0.or(other.topic0),
topic1: self.topic1.or(other.topic1),
topic2: self.topic2.or(other.topic2),
topic3: self.topic3.or(other.topic3),
}
}
fn logs<T: web3::Transport>(
self,
web3: &web3::Web3<T>,
) -> Result<Vec<web3::types::Log>, web3::error::Error> {
web3.eth_filter()
.create_logs_filter(self.to_filter_builder().build())
.wait()?
.logs()
.wait()
}
}
pub trait Web3LogExt {
@ -82,50 +131,51 @@ impl Web3LogExt for web3::types::Log {
}
}
/// Converts an `ethabi::Topic<T>` into an `Option<Vec<T>>`, where `Any` corresponds to `None`,
/// `This` to a vector with one element, and `OneOf` to any vector.
fn to_topic<T>(topic: ethabi::Topic<T>) -> Option<Vec<T>> {
match topic {
ethabi::Topic::Any => None,
ethabi::Topic::OneOf(v) => Some(v),
ethabi::Topic::This(t) => Some(vec![t]),
/// Wrapper for a byte array, whose `Display` implementation outputs shortened hexadecimal strings.
pub struct HexBytes<'a>(pub &'a [u8]);
impl<'a> fmt::Display for HexBytes<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "0x")?;
for i in &self.0[..2] {
write!(f, "{:02x}", i)?;
}
write!(f, "")?;
for i in &self.0[(self.0.len() - 2)..] {
write!(f, "{:02x}", i)?;
}
Ok(())
}
}
pub trait LogExt {
/// Returns the `i`-th parameter, if it has the given name, otherwise `None`.
fn param(&self, i: usize, name: &str) -> Option<&ethabi::Token>;
/// Wrapper for a list of byte arrays, whose `Display` implementation outputs shortened hexadecimal
/// strings.
pub struct HexList<'a, T: 'a>(pub &'a [T]);
/// Returns the `i`-th parameter, if it is an `Address` and has the given name, otherwise
/// `None`.
fn address_param(&self, i: usize, name: &str) -> Option<&ethabi::Address>;
/// Returns the `i`-th parameter, if it is a `Uint` and has the given name, otherwise `None`.
fn uint_param(&self, i: usize, name: &str) -> Option<&ethabi::Uint>;
}
impl LogExt for ethabi::Log {
fn param(&self, i: usize, name: &str) -> Option<&ethabi::Token> {
self.params.get(i).and_then(|param| {
if param.name == name {
Some(&param.value)
} else {
None
impl<'a, T: 'a> fmt::Display for HexList<'a, T>
where
T: AsRef<[u8]>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[")?;
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
})
}
fn address_param(&self, i: usize, name: &str) -> Option<&ethabi::Address> {
match self.param(i, name) {
Some(&ethabi::Token::Address(ref address)) => Some(address),
_ => None,
}
}
fn uint_param(&self, i: usize, name: &str) -> Option<&ethabi::Uint> {
match self.param(i, name) {
Some(&ethabi::Token::Uint(ref i)) => Some(i),
_ => None,
write!(f, "{}", HexBytes(item.as_ref()))?;
}
write!(f, "]")
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_parse_address() {
let addr_str = "0x2b1dbc7390a65dc40f7d64d67ea11b4d627dd1bf";
let addr = super::parse_address(addr_str).expect("parse address with 0x");
let addr2 = super::parse_address(&addr_str[2..]).expect("parse address without 0x");
assert_eq!(addr, addr2);
assert_eq!(addr_str, &format!("{:?}", addr));
}
}

View File

@ -1,6 +1,4 @@
use ethabi::Token;
use util;
use web3::contract::{tokens, Error, ErrorKind};
use ethabi;
/// Validator metadata.
#[derive(Clone, Debug)]
@ -17,17 +15,28 @@ pub struct Validator {
// uint256 minThreshold,
}
impl tokens::Detokenize for Validator {
/// Returns a `Validator` if the token's types match the fields.
fn from_tokens(tokens: Vec<Token>) -> Result<Validator, Error> {
match (tokens.get(0), tokens.get(1)) {
(Some(&Token::FixedBytes(ref first)), Some(&Token::FixedBytes(ref last))) => {
Ok(Validator {
first_name: util::bytes_to_string(first),
last_name: util::bytes_to_string(last),
})
}
_ => Err(ErrorKind::InvalidOutputType("Validator".to_string()).into()),
type ValidatorTuple = (
ethabi::Hash,
ethabi::Hash,
ethabi::Hash,
String,
ethabi::Hash,
ethabi::Hash,
ethabi::Uint,
ethabi::Uint,
ethabi::Uint,
ethabi::Uint,
);
impl From<ValidatorTuple> for Validator {
fn from((first_name_h, last_name_h, ..): ValidatorTuple) -> Validator {
Validator {
first_name: String::from_utf8_lossy(&*first_name_h)
.to_owned()
.to_string(),
last_name: String::from_utf8_lossy(&*last_name_h)
.to_owned()
.to_string(),
}
}
}