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