Skip to content

Commit

Permalink
Generator of Tendermint types for unit, integration, and model-based …
Browse files Browse the repository at this point in the history
…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
andrey-kuprianov and romac committed Jul 29, 2020
1 parent 985c98a commit 877c586
Show file tree
Hide file tree
Showing 13 changed files with 988 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ members = [
"light-node",
"rpc",
"tendermint",
"testgen"
]
2 changes: 1 addition & 1 deletion tendermint/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Duration> for std::time::Duration {
fn from(d: Duration) -> std::time::Duration {
Expand Down
7 changes: 6 additions & 1 deletion tendermint/src/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
18 changes: 18 additions & 0 deletions testgen/Cargo.toml
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"
133 changes: 133 additions & 0 deletions testgen/bin/tendermint-testgen.rs
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),
}
}
199 changes: 199 additions & 0 deletions testgen/src/commit.rs
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),
};
}
}
}
Loading

0 comments on commit 877c586

Please sign in to comment.