Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generator of Tendermint types for unit, integration, and model-based testing #468

Merged
merged 70 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
56396ce
mbt/tendermint-produce validator
andrey-kuprianov Jun 18, 2020
c5d6506
add constructor for ProposerPriority
andrey-kuprianov Jun 18, 2020
19d0a3c
mbt-utils/produce header (not complete yet)
andrey-kuprianov Jun 19, 2020
38c9d80
better error handling
andrey-kuprianov Jun 22, 2020
e6bcaf7
add default_consensus_params(); not clear how to correctly hash it
andrey-kuprianov Jun 22, 2020
ae2c189
mbt-utils-produce: started on commit
andrey-kuprianov Jun 22, 2020
d2ff698
read input using generic function
andrey-kuprianov Jun 22, 2020
53cdb24
mbt-utils: rework produce_validator to accept input id/input JSON/CLI…
andrey-kuprianov Jun 23, 2020
03314db
use serde deserializers for option fields
andrey-kuprianov Jun 23, 2020
d1eb8b1
mbt-utils/produce-header: allow to set next_vals/time via input/cli
andrey-kuprianov Jun 23, 2020
db8fb01
mbt-utils/produce-header: remove debug output
andrey-kuprianov Jun 23, 2020
1884cd6
Merge branch 'master' into andrey/mbt-utils
andrey-kuprianov Jun 23, 2020
a3a9505
mbt-utils: refactor produce validator
andrey-kuprianov Jun 24, 2020
6883b0c
mbt-utils: refactor produce header
andrey-kuprianov Jun 24, 2020
d13ca00
mbt-utils/produce-header: allow to specify header height
andrey-kuprianov Jun 24, 2020
7bfb115
mbt-utils/produce-commit: refactor + add height
andrey-kuprianov Jun 24, 2020
8bdfad6
started refactoring using traits; added --usage
andrey-kuprianov Jun 25, 2020
9b5541c
better help
andrey-kuprianov Jun 25, 2020
457d518
mbt-utils/move produce header into Producer trait
andrey-kuprianov Jun 25, 2020
976c1e1
mbt-utils/move produce commit into Producer trait
andrey-kuprianov Jun 25, 2020
f912290
minor improvements
andrey-kuprianov Jun 25, 2020
9cb876f
mbt-utils: produce commit from header
andrey-kuprianov Jun 26, 2020
b41ee56
mbt-utils: add generator functions for Validator,Header,Commit
andrey-kuprianov Jun 26, 2020
5f93b20
mbt-utils: simplify Producer interface
andrey-kuprianov Jun 26, 2020
21aac34
mbt-utils: make stdin parsing failable
andrey-kuprianov Jun 26, 2020
dd07802
mbt-utils: pull up signer from Validator::produce()
andrey-kuprianov Jun 27, 2020
be1c8aa
mbt-utils: started on producing real signatures for commits
andrey-kuprianov Jun 27, 2020
552d2c9
mbt-utils: small simplification
andrey-kuprianov Jun 27, 2020
b99e1a7
Merge branch 'master' into andrey/mbt-utils
andrey-kuprianov Jul 3, 2020
100a19b
Merge branch 'master' into andrey/mbt-utils
andrey-kuprianov Jul 6, 2020
bbe64d6
mbt-utils: added preliminary support for signatures
andrey-kuprianov Jul 6, 2020
3a46134
Add missing pub modifiers in a few places
romac Jul 6, 2020
096ab7b
Remove unused imports
romac Jul 6, 2020
39ae919
Fix clippy warnings
romac Jul 6, 2020
3666003
Refactor into a library and a binary
romac Jul 6, 2020
b123724
Move tendermint-produce command into bin/ directory
romac Jul 6, 2020
c86fdcd
Remove newline at end of files
romac Jul 6, 2020
19b0762
Merge pull request #411 from informalsystems/romain/mbt-utils
andrey-kuprianov Jul 6, 2020
a406414
mbt-utils: start refactoring, FromStr for Validator, Commit, Header
andrey-kuprianov Jul 16, 2020
edebadf
mbt-utils: move encode_with_stdin out of Producer trait
andrey-kuprianov Jul 16, 2020
56b4aa4
mbt-utils: remove parse_stdin() from Producer/Validator/Commit/Header
andrey-kuprianov Jul 16, 2020
17d49d7
mbt:utils: get rid of unwraps for better error handling
andrey-kuprianov Jul 16, 2020
00b102f
#393: refactor mbt-tendermint-produce into tendermint-typegen
andrey-kuprianov Jul 17, 2020
ffaa039
#393: change in usage mbt-tendermint-produce into tendermint-typegen
andrey-kuprianov Jul 17, 2020
19dd8be
#393: switch to gen_setter macros for setters
andrey-kuprianov Jul 17, 2020
00d1445
#393: start on vote
andrey-kuprianov Jul 17, 2020
280e822
#393: tendermint-typegen -> tendermint-testgen
andrey-kuprianov Jul 20, 2020
ede2e45
#393: more of vote + shorten code
andrey-kuprianov Jul 20, 2020
4991c0d
#393: shorten imports
andrey-kuprianov Jul 20, 2020
56a11d8
#393: add generation of votes; refactor commit
andrey-kuprianov Jul 20, 2020
54651f2
#393: commit: use getters
andrey-kuprianov Jul 20, 2020
ef84f20
#393: finish code restructuring
andrey-kuprianov Jul 21, 2020
61dc01f
#393: validator unit test
andrey-kuprianov Jul 21, 2020
8fed686
#393: header unit test
andrey-kuprianov Jul 21, 2020
959d4ac
Merge branch 'master' into andrey/mbt-utils
andrey-kuprianov Jul 21, 2020
6301089
#393: rename mbt-utils -> tendermint-testgen
andrey-kuprianov Jul 21, 2020
3e31f4a
#393: more tests for validator and header
andrey-kuprianov Jul 21, 2020
7a86b53
#393: unit test for vote
andrey-kuprianov Jul 21, 2020
feffb1b
#393: unit test for commit; factor out sign/verify helpers
andrey-kuprianov Jul 21, 2020
5d648ff
#393: fix clippy warnings
andrey-kuprianov Jul 21, 2020
22f2020
#393: account for suggestions from @shonfeder
andrey-kuprianov Jul 22, 2020
a80629f
#393: cargo fmt
andrey-kuprianov Jul 22, 2020
4178415
Apply suggestions from @romac review
andrey-kuprianov Jul 22, 2020
42b77ec
#393: apply suggestions from @romac review + necessary changes
andrey-kuprianov Jul 22, 2020
0cdad53
#393: apply more suggestions from @romac review
andrey-kuprianov Jul 22, 2020
17b6277
#393: add version for tendermint dep, as per @liamsi suggestion
andrey-kuprianov Jul 27, 2020
ebe13a0
#393: fix clippy warning as suggested by @romac
andrey-kuprianov Jul 27, 2020
f6ee883
Merge branch 'master' into andrey/mbt-utils
andrey-kuprianov Jul 27, 2020
44c3f92
After a bit of afterthought and as a result of @Shivani912 comment,
andrey-kuprianov Jul 28, 2020
500e1f7
#393: move tendermint-testgen -> testgen as suggested by @romac
andrey-kuprianov Jul 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
"tendermint-testgen"
andrey-kuprianov marked this conversation as resolved.
Show resolved Hide resolved
]
18 changes: 18 additions & 0 deletions tendermint-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 <[email protected]>"]
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 tendermint-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),
}
}
190 changes: 190 additions & 0 deletions tendermint-testgen/src/commit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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 {
pub fn new(header: Header) -> Self {
Commit {
header: Some(header),
round: None,
votes: None,
}
}
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 {
andrey-kuprianov marked this conversation as resolved.
Show resolved Hide resolved
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)?),
};
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 => bail!("failed to generate commit: votes are missing"),
Some(vs) => vs,
};
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())
.round(3)
.generate_default_votes();

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),
};
}
}
}
18 changes: 18 additions & 0 deletions tendermint-testgen/src/consensus.rs
Original file line number Diff line number Diff line change
@@ -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],
},
}
}
23 changes: 23 additions & 0 deletions tendermint-testgen/src/generator.rs
Original file line number Diff line number Diff line change
@@ -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<Output: Serialize>: FromStr<Err = SimpleError> + 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<Output, SimpleError>;

/// Generate and serialize the complex object
fn encode(&self) -> Result<String, SimpleError> {
let res = self.generate()?;
Ok(try_with!(
serde_json::to_string_pretty(&res),
"failed to serialize into JSON"
))
}
}
Loading