-
Notifications
You must be signed in to change notification settings - Fork 224
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generator of Tendermint types for unit, integration, and model-based …
…testing (#468) * mbt/tendermint-produce validator * add constructor for ProposerPriority * mbt-utils/produce header (not complete yet) * better error handling * add default_consensus_params(); not clear how to correctly hash it * mbt-utils-produce: started on commit * read input using generic function * mbt-utils: rework produce_validator to accept input id/input JSON/CLI options * use serde deserializers for option fields * mbt-utils/produce-header: allow to set next_vals/time via input/cli * mbt-utils/produce-header: remove debug output * mbt-utils: refactor produce validator * refactor produce_validator to allow calling it from within Rust code, to easily produce Info struct * add --ignore-stdin option to allow skipping parsing from STDIN * mbt-utils: refactor produce header * mbt-utils/produce-header: allow to specify header height * mbt-utils/produce-commit: refactor + add height * started refactoring using traits; added --usage * better help * mbt-utils/move produce header into Producer trait * mbt-utils/move produce commit into Producer trait * minor improvements * mbt-utils: produce commit from header * mbt-utils: add generator functions for Validator,Header,Commit * mbt-utils: simplify Producer interface * mbt-utils: make stdin parsing failable * mbt-utils: pull up signer from Validator::produce() * mbt-utils: started on producing real signatures for commits * mbt-utils: small simplification * mbt-utils: added preliminary support for signatures * Add missing pub modifiers in a few places * Remove unused imports * Fix clippy warnings * Refactor into a library and a binary * Move tendermint-produce command into bin/ directory * Remove newline at end of files * mbt-utils: start refactoring, FromStr for Validator, Commit, Header * mbt-utils: move encode_with_stdin out of Producer trait * mbt-utils: remove parse_stdin() from Producer/Validator/Commit/Header * mbt:utils: get rid of unwraps for better error handling * #393: refactor mbt-tendermint-produce into tendermint-typegen * #393: change in usage mbt-tendermint-produce into tendermint-typegen * #393: switch to gen_setter macros for setters * #393: start on vote * #393: tendermint-typegen -> tendermint-testgen * #393: more of vote + shorten code * #393: shorten imports * #393: add generation of votes; refactor commit * #393: commit: use getters * #393: finish code restructuring * #393: validator unit test * #393: header unit test * #393: rename mbt-utils -> tendermint-testgen * #393: more tests for validator and header * #393: unit test for vote * #393: unit test for commit; factor out sign/verify helpers * #393: fix clippy warnings * #393: account for suggestions from @shonfeder * #393: cargo fmt * Apply suggestions from @romac review Co-authored-by: Romain Ruetschi <romain@informal.systems> * #393: apply suggestions from @romac review + necessary changes * #393: apply more suggestions from @romac review * #393: add version for tendermint dep, as per @liamsi suggestion * #393: fix clippy warning as suggested by @romac * After a bit of afterthought and as a result of @Shivani912 comment, decided to make `round` an explicit parameter when constructing the commit. The reason for this is that we want to allow generating votes only from the header, but without explicitely given round, the generated votes will point to the default 1. Setting the `round` afterwards in the Commit struct will have no effect on those votes. That's why `round` is now an explicit parameter, and there are two constructors: `new()`, and `new_with_votes()`. * #393: move tendermint-testgen -> testgen as suggested by @romac Co-authored-by: Romain Ruetschi <romain@informal.systems>
- Loading branch information
1 parent
985c98a
commit 877c586
Showing
13 changed files
with
988 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,5 @@ members = [ | |
"light-node", | ||
"rpc", | ||
"tendermint", | ||
"testgen" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "tendermint-testgen" | ||
version = "0.1.0" | ||
authors = ["Andrey Kuprianov <andrey@informal.systems>"] | ||
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Command>, | ||
} | ||
|
||
#[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<Opts: Generator<T> + Options, T: serde::Serialize>( | ||
cli: &Opts, | ||
) -> Result<String, SimpleError> { | ||
let stdin = read_stdin()?; | ||
let default = Opts::from_str(&stdin)?; | ||
let producer = cli.clone().merge_with_default(default); | ||
producer.encode() | ||
} | ||
|
||
fn run_command<Opts, T>(cli: Opts, read_stdin: bool) | ||
where | ||
Opts: Generator<T> + 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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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::<Header>"))] | ||
pub header: Option<Header>, | ||
#[options( | ||
help = "votes in this commit (default: from header)", | ||
parse(try_from_str = "parse_as::<Vec<Vote>>") | ||
)] | ||
pub votes: Option<Vec<Vote>>, | ||
#[options(help = "commit round (default: 1)")] | ||
pub round: Option<u64>, | ||
} | ||
|
||
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<Vote>) -> Self { | ||
Commit { | ||
header: Some(header), | ||
round: Some(round), | ||
votes: Some(votes), | ||
} | ||
} | ||
set_option!(header, Header); | ||
set_option!(votes, Vec<Vote>); | ||
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<Self, Self::Err> { | ||
let commit = match parse_as::<Commit>(s) { | ||
Ok(input) => input, | ||
Err(_) => Commit::new(parse_as::<Header>(s)?, 1), | ||
}; | ||
Ok(commit) | ||
} | ||
} | ||
|
||
impl Generator<block::Commit> 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<block::Commit, SimpleError> { | ||
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<block::CommitSig, SimpleError> { | ||
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::<Result<Vec<block::CommitSig>, 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), | ||
}; | ||
} | ||
} | ||
} |
Oops, something went wrong.