diff --git a/Cargo.toml b/Cargo.toml index b372ea097..98fa0c129 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,5 @@ members = [ "light-node", "rpc", "tendermint", + "testgen" ] diff --git a/tendermint/src/evidence.rs b/tendermint/src/evidence.rs index 248083481..f697d2819 100644 --- a/tendermint/src/evidence.rs +++ b/tendermint/src/evidence.rs @@ -101,7 +101,7 @@ pub struct Params { /// essentially, to keep the usages look cleaner /// i.e. you can avoid using serde annotations everywhere #[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] -pub struct Duration(#[serde(with = "serializers::time_duration")] std::time::Duration); +pub struct Duration(#[serde(with = "serializers::time_duration")] pub std::time::Duration); impl From for std::time::Duration { fn from(d: Duration) -> std::time::Duration { diff --git a/tendermint/src/validator.rs b/tendermint/src/validator.rs index cf4e34f18..1d6d77830 100644 --- a/tendermint/src/validator.rs +++ b/tendermint/src/validator.rs @@ -156,7 +156,12 @@ impl Info { pub struct ProposerPriority(i64); impl ProposerPriority { - /// Get the current voting power + /// Create a new Priority + pub fn new(p: i64) -> ProposerPriority { + ProposerPriority(p) + } + + /// Get the current proposer priority pub fn value(self) -> i64 { self.0 } diff --git a/testgen/Cargo.toml b/testgen/Cargo.toml new file mode 100644 index 000000000..e3130f43b --- /dev/null +++ b/testgen/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "tendermint-testgen" +version = "0.1.0" +authors = ["Andrey Kuprianov "] +edition = "2018" + +[dependencies] +tendermint = { version = "0.15.0", path = "../tendermint" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +gumdrop = "0.8.0" +signatory = { version = "0.20", features = ["ed25519", "ecdsa"] } +signatory-dalek = "0.20" +simple-error = "0.2.1" + +[[bin]] +name = "tendermint-testgen" +path = "bin/tendermint-testgen.rs" diff --git a/testgen/bin/tendermint-testgen.rs b/testgen/bin/tendermint-testgen.rs new file mode 100644 index 000000000..af29fe8c4 --- /dev/null +++ b/testgen/bin/tendermint-testgen.rs @@ -0,0 +1,133 @@ +use gumdrop::Options; +use simple_error::SimpleError; +use tendermint_testgen::{helpers::*, Commit, Generator, Header, Validator, Vote}; + +const USAGE: &str = r#" +This is a small utility for producing tendermint datastructures +from minimal input (for testing purposes only). + +For example, a tendermint validator can be produced only from an identifier, +or a tendermint header only from a set of validators. + +To get an idea which input is needed for each datastructure, try '--help CMD': +it will list the required and optional parameters. + +The parameters can be supplied in two ways: + - via STDIN: in that case they are expected to be a valid JSON object, + with each parameter being a field of this object + - via command line arguments to the specific command. + +If a parameter is supplied both via STDIN and CLI, the latter is given preference. + +In case a particular datastructure can be produced from a single parameter +(like validator), there is a shortcut that allows to provide this parameter +directly via STDIN, without wrapping it into JSON object. +E.g., in the validator case, the following commands are all equivalent: + + tendermint-testgen validator --id a --voting-power 3 + echo -n '{"id": "a", "voting_power": 3}' | tendermint-testgen --stdin validator + echo -n a | tendermint-testgen --stdin validator --voting-power 3 + echo -n '{"id": "a"}' | tendermint-testgen --stdin validator --voting-power 3 + echo -n '{"id": "a", "voting_power": 100}' | tendermint-testgen --stdin validator --voting-power 3 + +The result is: + { + "address": "730D3D6B2E9F4F0F23879458F2D02E0004F0F241", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "YnT69eNDaRaNU7teDTcyBedSD0B/Ziqx+sejm0wQba0=" + }, + "voting_power": "3", + "proposer_priority": null + } +"#; + +#[derive(Debug, Options)] +struct CliOptions { + #[options(help = "print this help and exit (--help CMD for command-specific help)")] + help: bool, + #[options(help = "provide detailed usage instructions")] + usage: bool, + #[options(help = "read input from STDIN (default: no)")] + stdin: bool, + + #[options(command)] + command: Option, +} + +#[derive(Debug, Options)] +enum Command { + #[options(help = "produce validator from identifier and other parameters")] + Validator(Validator), + #[options(help = "produce header from validator array and other parameters")] + Header(Header), + #[options(help = "produce vote from validator and other parameters")] + Vote(Vote), + #[options(help = "produce commit from validator array and other parameters")] + Commit(Commit), +} + +fn encode_with_stdin + Options, T: serde::Serialize>( + cli: &Opts, +) -> Result { + let stdin = read_stdin()?; + let default = Opts::from_str(&stdin)?; + let producer = cli.clone().merge_with_default(default); + producer.encode() +} + +fn run_command(cli: Opts, read_stdin: bool) +where + Opts: Generator + Options, + T: serde::Serialize, +{ + let res = if read_stdin { + encode_with_stdin(&cli) + } else { + cli.encode() + }; + match res { + Ok(res) => println!("{}", res), + Err(e) => { + eprintln!("Error: {}\n", e); + eprintln!("Supported parameters for this command are: "); + print_params(cli.self_usage()); + std::process::exit(1); + } + } +} + +fn print_params(options: &str) { + for line in options.lines().skip(1) { + eprintln!("{}", line); + } +} + +fn main() { + let opts = CliOptions::parse_args_default_or_exit(); + if opts.usage { + eprintln!("{}", USAGE); + std::process::exit(1); + } + match opts.command { + None => { + eprintln!("Produce tendermint datastructures for testing from minimal input\n"); + eprintln!("Please specify a command:"); + eprintln!("{}\n", CliOptions::command_list().unwrap()); + eprintln!("{}\n", CliOptions::usage()); + for cmd in CliOptions::command_list() + .unwrap() + .split('\n') + .map(|s| s.split_whitespace().next().unwrap()) + { + eprintln!("\n{} parameters:", cmd); + print_params(CliOptions::command_usage(cmd).unwrap()) + } + std::process::exit(1); + } + Some(Command::Validator(cli)) => run_command(cli, opts.stdin), + Some(Command::Header(cli)) => run_command(cli, opts.stdin), + Some(Command::Vote(cli)) => run_command(cli, opts.stdin), + Some(Command::Commit(cli)) => run_command(cli, opts.stdin), + } +} diff --git a/testgen/src/commit.rs b/testgen/src/commit.rs new file mode 100644 index 000000000..162aa0d6a --- /dev/null +++ b/testgen/src/commit.rs @@ -0,0 +1,199 @@ +use gumdrop::Options; +use serde::Deserialize; +use simple_error::*; +use tendermint::{block, lite}; + +use crate::{helpers::*, Generator, Header, Validator, Vote}; + +#[derive(Debug, Options, Deserialize, Clone)] +pub struct Commit { + #[options(help = "header (required)", parse(try_from_str = "parse_as::
"))] + pub header: Option
, + #[options( + help = "votes in this commit (default: from header)", + parse(try_from_str = "parse_as::>") + )] + pub votes: Option>, + #[options(help = "commit round (default: 1)")] + pub round: Option, +} + +impl Commit { + /// Make a new commit using default votes produced from the header. + pub fn new(header: Header, round: u64) -> Self { + let commit = Commit { + header: Some(header), + round: Some(round), + votes: None, + }; + commit.generate_default_votes() + } + /// Make a new commit using explicit votes. + pub fn new_with_votes(header: Header, round: u64, votes: Vec) -> Self { + Commit { + header: Some(header), + round: Some(round), + votes: Some(votes), + } + } + set_option!(header, Header); + set_option!(votes, Vec); + set_option!(round, u64); + + /// Generate commit votes from all validators in the header. + /// This function will panic if the header is not present + pub fn generate_default_votes(mut self) -> Self { + let header = self.header.as_ref().unwrap(); + let val_to_vote = |(i, v): (usize, &Validator)| -> Vote { + Vote::new(v.clone(), header.clone()) + .index(i as u64) + .round(self.round.unwrap_or(1)) + }; + let votes = header + .validators + .as_ref() + .unwrap() + .iter() + .enumerate() + .map(val_to_vote) + .collect(); + self.votes = Some(votes); + self + } + + /// Get a mutable reference to the vote of the given validator. + /// This function will panic if the votes or the validator vote is not present + pub fn vote_of_validator(&mut self, id: &str) -> &mut Vote { + self.votes + .as_mut() + .unwrap() + .iter_mut() + .find(|v| *v.validator.as_ref().unwrap() == Validator::new(id)) + .unwrap() + } + + /// Get a mutable reference to the vote at the given index + /// This function will panic if the votes or the vote at index is not present + pub fn vote_at_index(&mut self, index: usize) -> &mut Vote { + self.votes.as_mut().unwrap().get_mut(index).unwrap() + } +} + +impl std::str::FromStr for Commit { + type Err = SimpleError; + fn from_str(s: &str) -> Result { + let commit = match parse_as::(s) { + Ok(input) => input, + Err(_) => Commit::new(parse_as::
(s)?, 1), + }; + Ok(commit) + } +} + +impl Generator for Commit { + fn merge_with_default(self, other: Self) -> Self { + Commit { + header: self.header.or(other.header), + round: self.round.or(other.round), + votes: self.votes.or(other.votes), + } + } + + fn generate(&self) -> Result { + let header = match &self.header { + None => bail!("failed to generate commit: header is missing"), + Some(h) => h, + }; + let votes = match &self.votes { + None => self.clone().generate_default_votes().votes.unwrap(), + Some(vs) => vs.to_vec(), + }; + let block_header = header.generate()?; + let block_id = block::Id::new(lite::Header::hash(&block_header), None); + + let vote_to_sig = |v: &Vote| -> Result { + let vote = v.generate()?; + Ok(block::CommitSig::BlockIDFlagCommit { + validator_address: vote.validator_address, + timestamp: vote.timestamp, + signature: vote.signature, + }) + }; + let sigs = votes + .iter() + .map(vote_to_sig) + .collect::, SimpleError>>()?; + let commit = block::Commit { + height: block_header.height, + round: self.round.unwrap_or(1), + block_id, // TODO do we need at least one part? //block::Id::new(hasher.hash_header(&block_header), None), // + signatures: block::CommitSigs::new(sigs), + }; + Ok(commit) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tendermint::Time; + + #[test] + fn test_commit() { + let valset1 = [ + Validator::new("a"), + Validator::new("b"), + Validator::new("c"), + ]; + let valset2 = [ + Validator::new("b"), + Validator::new("c"), + Validator::new("d"), + ]; + + let now = Time::now(); + let header = Header::new(&valset1) + .next_validators(&valset2) + .height(10) + .time(now); + + let commit = Commit::new(header.clone(), 3); + + let block_header = header.generate().unwrap(); + let block_commit = commit.generate().unwrap(); + + assert_eq!(block_commit.round, 3); + assert_eq!(block_commit.height, block_header.height); + + let mut commit = commit; + assert_eq!(commit.vote_at_index(1).round, Some(3)); + assert_eq!(commit.vote_of_validator("a").index, Some(0)); + + let votes = commit.votes.as_ref().unwrap(); + + for (i, sig) in block_commit.signatures.iter().enumerate() { + match sig { + block::CommitSig::BlockIDFlagCommit { + validator_address: _, + timestamp: _, + signature, + } => { + let block_vote = votes[i].generate().unwrap(); + let sign_bytes = + get_vote_sign_bytes(block_header.chain_id.as_str(), &block_vote); + assert!(!verify_signature( + &valset2[i].get_verifier().unwrap(), + &sign_bytes, + signature + )); + assert!(verify_signature( + &valset1[i].get_verifier().unwrap(), + &sign_bytes, + signature + )); + } + _ => assert!(false), + }; + } + } +} diff --git a/testgen/src/consensus.rs b/testgen/src/consensus.rs new file mode 100644 index 000000000..ad63104bc --- /dev/null +++ b/testgen/src/consensus.rs @@ -0,0 +1,18 @@ +use tendermint::{block, consensus, evidence, public_key::Algorithm}; + +/// Default consensus params modeled after Go code; but it's not clear how to go to a valid hash from here +pub fn default_consensus_params() -> consensus::Params { + consensus::Params { + block: block::Size { + max_bytes: 22020096, + max_gas: -1, // Tendetmint-go also has TimeIotaMs: 1000, // 1s + }, + evidence: evidence::Params { + max_age_num_blocks: 100000, + max_age_duration: evidence::Duration(std::time::Duration::new(48 * 3600, 0)), + }, + validator: consensus::params::ValidatorParams { + pub_key_types: vec![Algorithm::Ed25519], + }, + } +} diff --git a/testgen/src/generator.rs b/testgen/src/generator.rs new file mode 100644 index 000000000..8fe3e554a --- /dev/null +++ b/testgen/src/generator.rs @@ -0,0 +1,23 @@ +use serde::Serialize; +use simple_error::*; +use std::str::FromStr; + +/// A trait that allows to generate complex objects from simple companion objects. +/// A companion type should have a simple API, leaving most fields optional. +pub trait Generator: FromStr + Clone { + /// Merge this companion with the another, default one. + /// The options present in this object will override those in the default one. + fn merge_with_default(self, default: Self) -> Self; + + /// Generate the complex object from this companion object. + fn generate(&self) -> Result; + + /// Generate and serialize the complex object + fn encode(&self) -> Result { + let res = self.generate()?; + Ok(try_with!( + serde_json::to_string_pretty(&res), + "failed to serialize into JSON" + )) + } +} diff --git a/testgen/src/header.rs b/testgen/src/header.rs new file mode 100644 index 000000000..64199efb1 --- /dev/null +++ b/testgen/src/header.rs @@ -0,0 +1,171 @@ +use crate::{helpers::*, validator::generate_validators, Generator, Validator}; +use gumdrop::Options; +use serde::Deserialize; +use simple_error::*; +use std::str::FromStr; +use tendermint::{block, chain, lite::ValidatorSet, validator, Time}; + +#[derive(Debug, Options, Deserialize, Clone)] +pub struct Header { + #[options( + help = "validators (required), encoded as array of 'validator' parameters", + parse(try_from_str = "parse_as::>") + )] + pub validators: Option>, + #[options( + help = "next validators (default: same as validators), encoded as array of 'validator' parameters", + parse(try_from_str = "parse_as::>") + )] + pub next_validators: Option>, + #[options(help = "chain id (default: test-chain)")] + pub chain_id: Option, + #[options(help = "block height (default: 1)")] + pub height: Option, + #[options(help = "time (default: now)")] + pub time: Option