diff --git a/testgen/src/header.rs b/testgen/src/header.rs index 17bba4afe..0c7b19760 100644 --- a/testgen/src/header.rs +++ b/testgen/src/header.rs @@ -49,6 +49,22 @@ impl Header { set_option!(height, u64); set_option!(time, u64); set_option!(proposer, usize); + + pub fn next(&self) -> Self { + let height = self.height.expect("Missing previous header's height"); + let time = self.time.unwrap_or(height); // if no time is found, then we simple correspond it to the header height + let validators = self.validators.clone().expect("Missing validators"); + let next_validators = self.next_validators.clone().unwrap_or(validators); + + Self { + validators: Some(next_validators.clone()), + next_validators: Some(next_validators), + chain_id: self.chain_id.clone(), + height: Some(height + 1), + time: Some(time + 1), + proposer: self.proposer, // TODO: proposer must be incremented + } + } } impl std::str::FromStr for Header { diff --git a/testgen/src/lib.rs b/testgen/src/lib.rs index 8e6606a99..a359172ab 100644 --- a/testgen/src/lib.rs +++ b/testgen/src/lib.rs @@ -6,6 +6,8 @@ pub mod commit; pub mod consensus; pub mod generator; pub mod header; +pub mod light_block; +pub mod light_chain; pub mod time; pub mod validator; pub mod vote; @@ -13,6 +15,8 @@ pub mod vote; pub use commit::Commit; pub use generator::Generator; pub use header::Header; +pub use light_block::LightBlock; +pub use light_chain::LightChain; pub use time::Time; pub use validator::Validator; pub use vote::Vote; diff --git a/testgen/src/light_block.rs b/testgen/src/light_block.rs new file mode 100644 index 000000000..edb33d4ad --- /dev/null +++ b/testgen/src/light_block.rs @@ -0,0 +1,273 @@ +use gumdrop::Options; +use serde::{Deserialize, Serialize}; +use simple_error::*; + +use crate::helpers::parse_as; +use crate::validator::generate_validators; +use crate::{Commit, Generator, Header, Validator}; +use tendermint::block::signed_header::SignedHeader; +use tendermint::node::Id as PeerId; +use tendermint::validator; +use tendermint::validator::Set as ValidatorSet; + +/// A light block is the core data structure used by the light client. +/// It records everything the light client needs to know about a block. +/// NOTE: This struct & associated `impl` below are a copy of light-client's `LightBlock`. +/// The copy is necessary here to avoid a circular dependency. +/// Cf. https://github.com/informalsystems/tendermint-rs/issues/605 +/// TODO: fix redundant code without introducing cyclic dependency. +/// +/// To convert `TMLightBlock` to the Domain type `LightBlock` used in light-client crate +/// You'll need to implement the `From` trait like below: +/// +/// impl From for LightBlock { +/// fn from(tm_lb: TMLightBlock) -> Self { +/// Self { +/// signed_header: tm_lb.signed_header, +/// validators: tm_lb.validators, +/// next_validators: tm_lb.next_validators, +/// provider: tm_lb.provider, +/// } +/// } +/// } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct TMLightBlock { + /// Header and commit of this block + pub signed_header: SignedHeader, + /// Validator set at the block height + pub validators: ValidatorSet, + /// Validator set at the next block height + pub next_validators: ValidatorSet, + /// The peer ID of the node that provided this block + pub provider: PeerId, +} + +/// We use this data structure as a simplistic representation of LightClient's LightBlock +#[derive(Debug, Options, Deserialize, Clone)] +pub struct LightBlock { + #[options(help = "header (required)", parse(try_from_str = "parse_as::
"))] + pub header: Option
, + #[options(help = "commit (required)", parse(try_from_str = "parse_as::"))] + pub commit: Option, + #[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 = "peer id (default: default_peer_id())")] + pub provider: Option, +} + +impl LightBlock { + /// Constructs a new Testgen-specific light block + pub fn new(header: Header, commit: Commit) -> Self { + Self { + header: Some(header), + commit: Some(commit), + validators: None, + next_validators: None, + provider: None, + } + } + set_option!(validators, &[Validator], Some(validators.to_vec())); + set_option!( + next_validators, + &[Validator], + Some(next_validators.to_vec()) + ); + + pub fn new_default(height: u64) -> Self { + let validators = [Validator::new("1"), Validator::new("2")]; + let header = Header::new(&validators) + .height(height) + .chain_id("test-chain") + .next_validators(&validators) + .time(height); // just wanted to initialize time with some value + + let commit = Commit::new(header.clone(), 1); + + Self { + header: Some(header), + commit: Some(commit), + validators: Some(validators.to_vec()), + next_validators: Some(validators.to_vec()), + provider: Some(default_peer_id()), + } + } + + /// Produces a subsequent, i.e. at (height+1), light block to the supplied one + // TODO: figure how to represent the currently ignored details in header + // TODO: and commit like last_block_id and other hashes + pub fn next(&self) -> Self { + let header = self.header.as_ref().expect("header is missing").next(); + + let commit = Commit::new(header.clone(), 1); + + Self { + header: Some(header), + commit: Some(commit), + validators: self.next_validators.clone(), + next_validators: self.next_validators.clone(), + provider: self.provider, + } + } + + /// returns the height of LightBlock's header + pub fn height(&self) -> u64 { + self.header + .as_ref() + .expect("header is missing") + .height + .expect("header height is missing") + } + + /// returns the chain_id of LightBlock's header + pub fn chain_id(&self) -> String { + self.header + .as_ref() + .expect("header is missing") + .chain_id + .as_ref() + .expect("chain_id is missing") + .to_string() + } +} + +impl std::str::FromStr for LightBlock { + type Err = SimpleError; + fn from_str(s: &str) -> Result { + let light_block = match parse_as::(s) { + Ok(input) => input, + Err(_) => LightBlock::new(parse_as::
(s)?, Commit::from_str(s)?), + }; + Ok(light_block) + } +} + +impl Generator for LightBlock { + fn merge_with_default(self, default: Self) -> Self { + Self { + header: self.header.or(default.header), + commit: self.commit.or(default.commit), + validators: self.validators.or(default.validators), + next_validators: self.next_validators.or(default.next_validators), + provider: self.provider.or(default.provider), + } + } + + fn generate(&self) -> Result { + let header = match &self.header { + None => bail!("header is missing"), + Some(h) => h, + }; + let commit = match &self.commit { + None => bail!("commit is missing"), + Some(c) => c, + }; + let signed_header = + generate_signed_header(header, commit).expect("Could not generate signed header"); + + let validators = match &self.validators { + None => validator::Set::new(generate_validators( + header + .validators + .as_ref() + .expect("missing validators in header"), + )?), + Some(vals) => validator::Set::new(generate_validators(vals)?), + }; + + let next_validators = match &self.next_validators { + Some(next_vals) => validator::Set::new(generate_validators(next_vals)?), + None => validators.clone(), + }; + + let provider = default_peer_id(); + + let light_block = TMLightBlock { + signed_header, + validators, + next_validators, + provider, + }; + + Ok(light_block) + } +} + +/// A helper function to generate SignedHeader used by TMLightBlock +pub fn generate_signed_header( + raw_header: &Header, + raw_commit: &Commit, +) -> Result { + let header = match raw_header.generate() { + Err(e) => bail!("Failed to generate header with error: {}", e), + Ok(h) => h, + }; + + let commit = match raw_commit.generate() { + Err(e) => bail!("Failed to generate commit with error: {}", e), + Ok(c) => c, + }; + + Ok(SignedHeader { header, commit }) +} + +pub fn default_peer_id() -> PeerId { + "BADFADAD0BEFEEDC0C0ADEADBEEFC0FFEEFACADE".parse().unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_light_block() { + let light_block_1 = LightBlock::new_default(1); + let light_block_2 = LightBlock::new_default(1); + + assert_eq!(light_block_1.generate(), light_block_2.generate()); + + let validators = [Validator::new("10"), Validator::new("20")]; + + let light_block_3 = LightBlock::new_default(1).validators(&validators); + + assert_ne!(light_block_2.generate(), light_block_3.generate()); + + let light_block_4 = LightBlock::new_default(4).validators(&validators); + + assert_ne!(light_block_3.generate(), light_block_4.generate()); + + let light_block_5 = light_block_3.next(); + let lb_5_height: u64 = light_block_5 + .generate() + .unwrap() + .signed_header + .header + .height + .into(); + + assert_eq!(2, lb_5_height); + + let header_6 = Header::new(&validators) + .next_validators(&validators) + .height(10) + .time(10) + .chain_id("test-chain"); + let commit_6 = Commit::new(header_6.clone(), 1); + let light_block_6 = LightBlock::new(header_6.clone(), commit_6); + + let header_7 = header_6.next(); + let commit_7 = Commit::new(header_7.clone(), 1); + let light_block_7 = LightBlock::new(header_7, commit_7); + + assert_eq!(light_block_7.height(), 11); + assert_eq!(light_block_7.chain_id(), "test-chain"); + assert_ne!(light_block_6.generate(), light_block_7.generate()); + } +} diff --git a/testgen/src/light_chain.rs b/testgen/src/light_chain.rs new file mode 100644 index 000000000..0f8f4bffc --- /dev/null +++ b/testgen/src/light_chain.rs @@ -0,0 +1,115 @@ +use crate::light_block::LightBlock; +use tendermint::block::Height; +use tendermint::chain::Info; + +use std::convert::TryFrom; + +pub struct LightChain { + pub info: Info, + pub light_blocks: Vec, +} + +impl LightChain { + pub fn new(info: Info, light_blocks: Vec) -> Self { + LightChain { info, light_blocks } + } + + // TODO: make this fn more usable + // TODO: like how does someone generate a chain with different validators at each height + pub fn default_with_length(num: u64) -> Self { + let testgen_light_block = LightBlock::new_default(1); + let mut light_blocks: Vec = vec![testgen_light_block.clone()]; + + for _i in 2..num { + // add "next" light block to the vector + light_blocks.push(testgen_light_block.next()); + } + + let id = light_blocks[0].chain_id().parse().unwrap(); + let height = Height::try_from(num).expect("failed to convert from u64 to Height"); + + let info = Info { + id, + height, + // TODO: figure how to add this + last_block_id: None, + // TODO: Not sure yet what this time means + time: None, + }; + Self::new(info, light_blocks) + } + + /// expects at least one LightBlock in the Chain + pub fn advance_chain(&mut self) -> LightBlock { + let last_light_block = self + .light_blocks + .last() + .expect("Cannot find testgen light block"); + + let new_light_block = last_light_block.next(); + self.light_blocks.push(new_light_block.clone()); + + self.info.height = Height::try_from(new_light_block.height()) + .expect("failed to convert from u64 to Height"); + + new_light_block + } + + /// fetches a block from LightChain at a certain height + /// it returns None if a block does not exist for the target_height + pub fn block(&self, target_height: u64) -> Option { + self.light_blocks + .clone() + .into_iter() + .find(|lb| lb.height() == target_height) + } + + /// fetches the latest block from LightChain + pub fn latest_block(&self) -> LightBlock { + self.light_blocks + .last() + .expect("cannot find last light block") + .clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_advance_chain() { + let mut light_chain = LightChain::default_with_length(1); + let advance_1 = light_chain.advance_chain(); + + assert_eq!(2, advance_1.height()); + assert_eq!(2, light_chain.info.height.value()); + + let advance_2 = light_chain.advance_chain(); + + assert_eq!(3, advance_2.height()); + assert_eq!(3, light_chain.info.height.value()); + } + + #[test] + fn test_block() { + let mut light_chain = LightChain::default_with_length(1); + let first_block = light_chain.block(1); + assert_eq!(1, first_block.unwrap().height()); + + light_chain.advance_chain(); + let second_block = light_chain.block(2); + assert_eq!(2, second_block.unwrap().height()); + } + + #[test] + fn test_latest_block() { + let mut light_chain = LightChain::default_with_length(1); + let first_block = light_chain.latest_block(); + assert_eq!(1, first_block.height()); + + light_chain.advance_chain(); + let second_block = light_chain.latest_block(); + assert_eq!(2, second_block.height()); + } +} diff --git a/testgen/src/validator.rs b/testgen/src/validator.rs index 7226ecc23..afc54fcdd 100644 --- a/testgen/src/validator.rs +++ b/testgen/src/validator.rs @@ -27,6 +27,7 @@ impl Validator { proposer_priority: None, } } + // Question: Why do we need this option since we're already initializing id with fn new()?? set_option!(id, &str, Some(id.to_string())); set_option!(voting_power, u64); set_option!(proposer_priority, i64);