From f3fac52cc6a4a0922275dbeaa6bc36fa127714af Mon Sep 17 00:00:00 2001 From: librelois <c@elo.tf> Date: Tue, 17 May 2022 02:20:39 +0200 Subject: [PATCH] WIP: feat(tests): allow custom genesis for each cucumber scenario --- end2end-tests/README.md | 25 ++-- .../cucumber-features/monetary_mass.feature | 8 +- .../cucumber-features/transfer_all.feature | 11 +- end2end-tests/cucumber-genesis/default.json | 61 +++++++++ end2end-tests/tests/common/mod.rs | 30 ++++- end2end-tests/tests/cucumber_tests.rs | 103 +++++++++++--- node/src/chain_spec/gdev.rs | 127 ++++++++++++------ node/src/chain_spec/gen_genesis_data.rs | 28 +++- resources/gdev.json | 3 +- 9 files changed, 309 insertions(+), 87 deletions(-) create mode 100644 end2end-tests/cucumber-genesis/default.json diff --git a/end2end-tests/README.md b/end2end-tests/README.md index 543ee0f34..8dfd383e4 100644 --- a/end2end-tests/README.md +++ b/end2end-tests/README.md @@ -58,21 +58,26 @@ Feature: My awesome feature - eve - ferdie -### Currency amounts +### genesis state -Amounts must be expressed as an integer of `ÄžD` or `UD`, decimal numbers are not supported. -If you need more precision, you can express amounts in cents of ÄžD (write `cÄžD`), or in thousandths -of UD (write `mUD`). +Each scenario bootstraps its own blockchain with its own genesis state. + +By default, all scenarios use the same configuration for the genesis, which is located in the file +`/cucumber-genesis/default.json`. -#### Given +You can define a custom genesis state for each scenario with the tag `@genesis.confName`. -You can give any currency balance to each of the test users, so money will be created ex-nihilo for -that user. Note that this created money is not included in the monetary mass used to revalue the UD -amount. +The genesis configuration must then be defined in a json file located at +`/cucumber-genesis/confName.json`. -Usage: `{user} have {amount} {unit}` +You can also define a custom genesis at the feature level, all the scenarios of this feature will +then inherit the genesis configuration. -Example: `alice have 10 ÄžD` +### Currency amounts + +Amounts must be expressed as an integer of `ÄžD` or `UD`, decimal numbers are not supported. +If you need more precision, you can express amounts in cents of ÄžD (write `cÄžD`), or in thousandths +of UD (write `mUD`). #### When diff --git a/end2end-tests/cucumber-features/monetary_mass.feature b/end2end-tests/cucumber-features/monetary_mass.feature index 674f34398..8679d2c23 100644 --- a/end2end-tests/cucumber-features/monetary_mass.feature +++ b/end2end-tests/cucumber-features/monetary_mass.feature @@ -1,10 +1,10 @@ Feature: Balance transfer - Scenario: After 10 blocks, the monetary mass should be 30 ÄžD - Then Monetary mass should be 0.00 ÄžD - Then Current UD amount should be 10.00 ÄžD - When 10 blocks later + Scenario: After 10 blocks, the monetary mass should be 60 ÄžD Then Monetary mass should be 30.00 ÄžD + Then Current UD amount should be 10.00 ÄžD When 10 blocks later Then Monetary mass should be 60.00 ÄžD + When 10 blocks later + Then Monetary mass should be 90.00 ÄžD Then Current UD amount should be 10.00 ÄžD diff --git a/end2end-tests/cucumber-features/transfer_all.feature b/end2end-tests/cucumber-features/transfer_all.feature index 313014ee5..b111bb518 100644 --- a/end2end-tests/cucumber-features/transfer_all.feature +++ b/end2end-tests/cucumber-features/transfer_all.feature @@ -1,6 +1,11 @@ +@genesis.default Feature: Balance transfer all - Scenario: If alice sends all her ÄžDs to Dave, Dave will get 10 ÄžD - Given alice have 10 ÄžD + Scenario: If alice sends all her ÄžDs to Dave, Dave will get 8 ÄžD When alice sends all her ÄžDs to dave - Then dave should have 10 ÄžD + """ + Alice is a smith member, as such she is not allowed to empty her account completely, + if she tries to do so, the existence deposit (2 ÄžD) must remain. + """ + Then alice should have 2 ÄžD + Then dave should have 8 ÄžD diff --git a/end2end-tests/cucumber-genesis/default.json b/end2end-tests/cucumber-genesis/default.json new file mode 100644 index 000000000..7a9780910 --- /dev/null +++ b/end2end-tests/cucumber-genesis/default.json @@ -0,0 +1,61 @@ +{ + "first_ud": 1000, + "first_ud_reeval": 100, + "identities": { + "Alice": { + "balance": 1000, + "certs": ["Bob", "Charlie"], + "pubkey": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + }, + "Bob": { + "balance": 1000, + "certs": ["Alice", "Charlie"], + "pubkey": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + }, + "Charlie": { + "balance": 1000, + "certs": ["Alice", "Bob"], + "pubkey": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" + } + }, + "parameters": { + "babe_epoch_duration": 30, + "cert_period": 15, + "cert_max_by_issuer": 10, + "cert_min_received_cert_to_issue_cert": 2, + "cert_renewable_period": 50, + "cert_validity_period": 1000, + "idty_confirm_period": 40, + "idty_creation_period": 50, + "membership_period": 1000, + "membership_renewable_period": 50, + "pending_membership_period": 500, + "ud_creation_period": 10, + "ud_reeval_period": 100, + "smith_cert_period": 15, + "smith_cert_max_by_issuer": 8, + "smith_cert_min_received_cert_to_issue_cert": 2, + "smith_cert_renewable_period": 50, + "smith_cert_validity_period": 1000, + "smith_membership_period": 1000, + "smith_membership_renewable_period": 20, + "smith_pending_membership_period": 500, + "smiths_wot_first_cert_issuable_on": 20, + "smiths_wot_min_cert_for_membership": 2, + "wot_first_cert_issuable_on": 20, + "wot_min_cert_for_create_idty_right": 2, + "wot_min_cert_for_membership": 2 + }, + "smiths": { + "Alice": { + "certs": ["Bob", "Charlie"] + }, + "Bob": { + "certs": ["Alice", "Charlie"] + }, + "Charlie": { + "certs": ["Alice", "Bob"] + } + }, + "sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" +} diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs index e3aeb4ccc..0701ed3a1 100644 --- a/end2end-tests/tests/common/mod.rs +++ b/end2end-tests/tests/common/mod.rs @@ -24,6 +24,7 @@ pub mod node_runtime {} use serde_json::Value; use sp_keyring::AccountKeyring; use std::io::prelude::*; +use std::path::PathBuf; use std::process::Command; use std::str::FromStr; use subxt::rpc::{rpc_params, ClientT, SubscriptionClientT}; @@ -38,9 +39,8 @@ pub type TransactionProgress<'client> = pub const SUDO_ACCOUNT: AccountKeyring = AccountKeyring::Alice; pub struct Process(std::process::Child); - -impl Drop for Process { - fn drop(&mut self) { +impl Process { + pub fn kill(&mut self) { self.0.kill().expect("node already down"); } } @@ -51,7 +51,8 @@ struct FullNode { ws_port: u16, } -pub async fn spawn_node() -> (Api, Client, Process) { +pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Api, Client, Process) { + println!("maybe_genesis_conf_file={:?}", maybe_genesis_conf_file); let duniter_binary_path = std::env::var("DUNITER_BINARY_PATH") .unwrap_or_else(|_| "../target/debug/duniter".to_owned()); let FullNode { @@ -59,8 +60,9 @@ pub async fn spawn_node() -> (Api, Client, Process) { p2p_port: _, ws_port, } = spawn_full_node( - &duniter_binary_path, &["--dev", "--execution=Native", "--sealing=manual"], + &duniter_binary_path, + maybe_genesis_conf_file, ); let client = ClientBuilder::new() .set_url(format!("ws://127.0.0.1:{}", ws_port)) @@ -110,12 +112,27 @@ pub async fn create_block_with_extrinsic( .map_err(Into::into) } -fn spawn_full_node(duniter_binary_path: &str, args: &[&str]) -> FullNode { +fn spawn_full_node( + args: &[&str], + duniter_binary_path: &str, + maybe_genesis_conf_file: Option<PathBuf>, +) -> FullNode { + // Ports let p2p_port = portpicker::pick_unused_port().expect("No ports free"); let rpc_port = portpicker::pick_unused_port().expect("No ports free"); let ws_port = portpicker::pick_unused_port().expect("No ports free"); + + // Env vars + let mut envs = Vec::new(); + if let Some(genesis_conf_file) = maybe_genesis_conf_file { + envs.push(("DUNITER_GENESIS_CONFIG", genesis_conf_file)); + } + + // Logs let log_file_path = format!("duniter-v2s-{}.log", ws_port); let log_file = std::fs::File::create(&log_file_path).expect("fail to create log file"); + + // Command let process = Process( Command::new(duniter_binary_path) .args( @@ -133,6 +150,7 @@ fn spawn_full_node(duniter_binary_path: &str, args: &[&str]) -> FullNode { .iter() .chain(args), ) + .envs(envs) .stdout(std::process::Stdio::null()) .stderr(log_file) .spawn() diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index d416058b1..d1bfa134e 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -21,13 +21,38 @@ use common::*; use cucumber::{given, then, when, World, WorldInit}; use sp_keyring::AccountKeyring; use std::convert::Infallible; +use std::path::PathBuf; use std::str::FromStr; #[derive(WorldInit)] -pub struct DuniterWorld { - api: Api, - client: Client, - _process: Process, +pub struct DuniterWorld(Option<DuniterWorldInner>); + +impl DuniterWorld { + async fn init(&mut self, maybe_genesis_conf_file: Option<PathBuf>) { + if let Some(ref mut inner) = self.0 { + inner.kill(); + } + self.0 = Some(DuniterWorldInner::new(maybe_genesis_conf_file).await); + } + fn api(&self) -> &Api { + if let Some(ref inner) = self.0 { + &inner.api + } else { + panic!("uninit") + } + } + fn client(&self) -> &Client { + if let Some(ref inner) = self.0 { + &inner.client + } else { + panic!("uninit") + } + } + fn kill(&mut self) { + if let Some(ref mut inner) = self.0 { + inner.kill(); + } + } } impl std::fmt::Debug for DuniterWorld { @@ -42,12 +67,27 @@ impl World for DuniterWorld { type Error = Infallible; async fn new() -> std::result::Result<Self, Infallible> { - let (api, client, _process) = spawn_node().await; - Ok(DuniterWorld { + Ok(DuniterWorld(None)) + } +} + +struct DuniterWorldInner { + api: Api, + client: Client, + process: Process, +} + +impl DuniterWorldInner { + async fn new(maybe_genesis_conf_file: Option<PathBuf>) -> Self { + let (api, client, process) = spawn_node(maybe_genesis_conf_file).await; + DuniterWorldInner { api, client, - _process, - }) + process, + } + } + fn kill(&mut self) { + self.process.kill(); } } @@ -69,7 +109,7 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri if is_ud { let current_ud_amount = world - .api + .api() .storage() .universal_dividend() .current_ud(None) @@ -78,7 +118,7 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri } // Create {amount} ÄžD for {who} - common::balances::set_balance(&world.api, &world.client, who, amount).await?; + common::balances::set_balance(&world.api(), &world.client(), who, amount).await?; Ok(()) } @@ -86,7 +126,7 @@ async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: Stri #[when(regex = r"(\d+) blocks? later")] async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> { for _ in 0..n { - common::create_empty_block(&world.client).await?; + common::create_empty_block(&world.client()).await?; } Ok(()) } @@ -105,9 +145,9 @@ async fn transfer( let (amount, is_ud) = parse_amount(amount, &unit); if is_ud { - common::balances::transfer_ud(&world.api, &world.client, from, amount, to).await + common::balances::transfer_ud(&world.api(), &world.client(), from, amount, to).await } else { - common::balances::transfer(&world.api, &world.client, from, amount, to).await + common::balances::transfer(&world.api(), &world.client(), from, amount, to).await } } @@ -117,7 +157,7 @@ async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Resu let from = AccountKeyring::from_str(&from).expect("unknown from"); let to = AccountKeyring::from_str(&to).expect("unknown to"); - common::balances::transfer_all(&world.api, &world.client, from, to).await + common::balances::transfer_all(&world.api(), &world.client(), from, to).await } #[then(regex = r"([a-zA-Z]+) should have (\d+) ÄžD")] @@ -128,7 +168,7 @@ async fn should_have(world: &mut DuniterWorld, who: String, amount: u64) -> Resu .to_account_id(); let amount = amount * 100; - let who_account = world.api.storage().system().account(who, None).await?; + let who_account = world.api().storage().system().account(who, None).await?; assert_eq!(who_account.data.free, amount); Ok(()) } @@ -141,7 +181,7 @@ async fn current_ud_amount_should_be( ) -> Result<()> { let expected = (amount * 100) + cents; let actual = world - .api + .api() .storage() .universal_dividend() .current_ud(None) @@ -154,7 +194,7 @@ async fn current_ud_amount_should_be( async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u64) -> Result<()> { let expected = (amount * 100) + cents; let actual = world - .api + .api() .storage() .universal_dividend() .monetary_mass(None) @@ -170,6 +210,35 @@ async fn main() { DuniterWorld::cucumber() //.fail_on_skipped() .max_concurrent_scenarios(4) + .before(|feature, _rule, scenario, world| { + let mut genesis_conf_file_path = PathBuf::new(); + genesis_conf_file_path.push("cucumber-genesis"); + genesis_conf_file_path.push(&format!( + "{}.json", + genesis_conf_name(&feature.tags, &scenario.tags) + )); + Box::pin(world.init(Some(genesis_conf_file_path))) + }) + .after(|_feature, _rule, _scenario, maybe_world| { + if let Some(world) = maybe_world { + world.kill(); + } + Box::pin(std::future::ready(())) + }) .run_and_exit("cucumber-features") .await; } + +fn genesis_conf_name(feature_tags: &[String], scenario_tags: &[String]) -> String { + for tag in scenario_tags { + if let Some(("genesis", conf_name)) = tag.split_once(".") { + return conf_name.to_owned(); + } + } + for tag in feature_tags { + if let Some(("genesis", conf_name)) = tag.split_once(".") { + return conf_name.to_owned(); + } + } + "default".to_owned() +} diff --git a/node/src/chain_spec/gdev.rs b/node/src/chain_spec/gdev.rs index e2bf545bf..a28470f68 100644 --- a/node/src/chain_spec/gdev.rs +++ b/node/src/chain_spec/gdev.rs @@ -62,47 +62,91 @@ pub fn get_authority_keys_from_seed(s: &str) -> AuthorityKeys { pub fn development_chain_spec() -> Result<ChainSpec, String> { let wasm_binary = WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?; - Ok(ChainSpec::from_genesis( - // Name - "Development", - // ID - "gdev", - ChainType::Development, - move || { - gen_genesis_conf( - wasm_binary, - // Initial authorities len - 1, - // Initial smiths members len - 3, - // Inital identities len - 4, - // Sudo account - get_account_id_from_seed::<sr25519::Public>("Alice"), - true, - ) - }, - // Bootnodes - vec![], - // Telemetry - None, - // Protocol ID - None, - //Fork ID - None, - // Properties - Some( - serde_json::json!({ - "tokenDecimals": TOKEN_DECIMALS, - "tokenSymbol": TOKEN_SYMBOL, - }) - .as_object() - .expect("must be a map") - .clone(), - ), - // Extensions - None, - )) + if std::env::var("DUNITER_GENESIS_CONFIG").is_ok() { + super::gen_genesis_data::generate_genesis_data( + |genesis_data| { + ChainSpec::from_genesis( + // Name + "Development", + // ID + "gdev", + sc_service::ChainType::Development, + move || genesis_data_to_gdev_genesis_conf(genesis_data.clone(), wasm_binary), + // Bootnodes + vec![], + // Telemetry + None, + // Protocol ID + None, + //Fork ID + None, + // Properties + Some( + serde_json::json!({ + "tokenDecimals": TOKEN_DECIMALS, + "tokenSymbol": TOKEN_SYMBOL, + }) + .as_object() + .expect("must be a map") + .clone(), + ), + // Extensions + None, + ) + }, + Some(get_authority_keys_from_seed("Alice").encode()), + Some(super::gen_genesis_data::ParamsAppliedAtGenesis { + genesis_certs_expire_on: 100_000, + genesis_smith_certs_expire_on: 100_000, + genesis_memberships_expire_on: 100_000, + genesis_memberships_renewable_on: 50, + genesis_smith_memberships_expire_on: 100_000, + genesis_smith_memberships_renewable_on: 50, + }), + ) + } else { + Ok(ChainSpec::from_genesis( + // Name + "Development", + // ID + "gdev", + ChainType::Development, + move || { + gen_genesis_conf( + wasm_binary, + // Initial authorities len + 1, + // Initial smiths members len + 3, + // Inital identities len + 4, + // Sudo account + get_account_id_from_seed::<sr25519::Public>("Alice"), + true, + ) + }, + // Bootnodes + vec![], + // Telemetry + None, + // Protocol ID + None, + //Fork ID + None, + // Properties + Some( + serde_json::json!({ + "tokenDecimals": TOKEN_DECIMALS, + "tokenSymbol": TOKEN_SYMBOL, + }) + .as_object() + .expect("must be a map") + .clone(), + ), + // Extensions + None, + )) + } } pub fn gen_live_conf() -> Result<ChainSpec, String> { @@ -139,6 +183,7 @@ pub fn gen_live_conf() -> Result<ChainSpec, String> { None, ) }, + None, Some(super::gen_genesis_data::ParamsAppliedAtGenesis { genesis_certs_expire_on: 100_000, genesis_smith_certs_expire_on: 100_000, diff --git a/node/src/chain_spec/gen_genesis_data.rs b/node/src/chain_spec/gen_genesis_data.rs index cafbeff1f..73f447778 100644 --- a/node/src/chain_spec/gen_genesis_data.rs +++ b/node/src/chain_spec/gen_genesis_data.rs @@ -85,6 +85,7 @@ struct SmithData { pub fn generate_genesis_data<CS, P, SK, F>( f: F, + maybe_force_authority: Option<Vec<u8>>, params_applied_at_genesis: Option<ParamsAppliedAtGenesis>, ) -> Result<CS, String> where @@ -234,6 +235,7 @@ where // SMITHSÂ SUB-WOT // let mut initial_authorities = BTreeMap::new(); + let mut online_authorities_counter = 0; let mut session_keys_map = BTreeMap::new(); let mut smiths_memberships = BTreeMap::new(); let mut smiths_certs_by_issuer = BTreeMap::new(); @@ -253,15 +255,27 @@ where } // Initial authorities - initial_authorities.insert( - *idty_index, - (identity.pubkey.clone(), smith_data.session_keys.is_some()), - ); + if maybe_force_authority.is_some() { + if smith_data.session_keys.is_some() { + return Err(format!("session_keys field forbidden",)); + } + if *idty_index == 1 { + initial_authorities.insert(1, (identity.pubkey.clone(), true)); + } + } else { + initial_authorities.insert( + *idty_index, + (identity.pubkey.clone(), smith_data.session_keys.is_some()), + ); + } // Session keys let session_keys_bytes = if let Some(ref session_keys) = smith_data.session_keys { + online_authorities_counter += 1; hex::decode(&session_keys[2..]) .map_err(|_| format!("invalid session keys for idty {}", &idty_name))? + } else if let (1, Some(ref session_keys_bytes)) = (*idty_index, &maybe_force_authority) { + session_keys_bytes.clone() } else { // Create fake session keys (must be unique and deterministic) let mut fake_session_keys_bytes = Vec::with_capacity(128); @@ -297,6 +311,12 @@ where ); } + if maybe_force_authority.is_none() && online_authorities_counter == 0 { + return Err(format!( + "The session_keys field must be filled in for at least one smith.", + )); + } + let genesis_data = GenesisData { accounts, certs_by_issuer, diff --git a/resources/gdev.json b/resources/gdev.json index 54da96155..095d8a050 100644 --- a/resources/gdev.json +++ b/resources/gdev.json @@ -32,8 +32,7 @@ "pending_membership_period": 500, "ud_creation_period": 10, "ud_first_reeval": 0, - "ud_reeval_period": 50, - "ud_reeval_period_in_blocks": 500, + "ud_reeval_period": 200, "smith_cert_period": 15, "smith_cert_max_by_issuer": 8, "smith_cert_min_received_cert_to_issue_cert": 2, -- GitLab