From 9c0dce2e1f85c0d67abc366b1b7544ee726316fc Mon Sep 17 00:00:00 2001 From: librelois <c@elo.tf> Date: Fri, 17 Jun 2022 01:40:35 +0200 Subject: [PATCH] tests(live): add sanity tests against gdev live chain --- .cargo/config | 1 + .gitlab-ci.yml | 8 ++ Cargo.lock | 12 ++ Cargo.toml | 1 + live-tests/Cargo.toml | 17 +++ live-tests/README.md | 36 ++++++ live-tests/src/lib.rs | 15 +++ live-tests/tests/sanity_gdev.rs | 189 ++++++++++++++++++++++++++++++++ 8 files changed, 279 insertions(+) create mode 100644 live-tests/Cargo.toml create mode 100644 live-tests/README.md create mode 100644 live-tests/src/lib.rs create mode 100644 live-tests/tests/sanity_gdev.rs diff --git a/.cargo/config b/.cargo/config index 3527f8783..42f6b4bfc 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,5 +1,6 @@ [alias] cucumber = "test -p duniter-end2end-tests --test cucumber_tests --" +sanity-gdev = "test -p duniter-live-tests --test sanity_gdev --" tu = "test --workspace --exclude duniter-end2end-tests" xtask = "run --package xtask --" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 607431e66..21048896e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -124,6 +124,14 @@ build_release_manual: - build/ expire_in: 3 day +sanity_tests: + extends: .env + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" + - when: never + script: + - cargo sanity-gdev + tests_debug: extends: .env rules: diff --git a/Cargo.lock b/Cargo.lock index 3908137f8..28b06ba2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1532,6 +1532,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "duniter-live-tests" +version = "3.0.0" +dependencies = [ + "anyhow", + "hex-literal", + "parity-scale-codec", + "sp-core", + "subxt", + "tokio", +] + [[package]] name = "duniter-primitives" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index e826d2d80..8b6c7c4de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,6 +123,7 @@ resolver = "2" members = [ 'end2end-tests', + 'live-tests', 'pallets/certification', 'pallets/duniter-test-parameters', 'pallets/duniter-test-parameters/macro', diff --git a/live-tests/Cargo.toml b/live-tests/Cargo.toml new file mode 100644 index 000000000..275d3e3d6 --- /dev/null +++ b/live-tests/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ['Axiom-Team Developers <https://axiom-team.fr>'] +description = 'duniter live tests.' +edition = '2018' +homepage = 'https://substrate.dev' +license = 'AGPL-3.0' +name = 'duniter-live-tests' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[dev-dependencies] +anyhow = "1.0" +hex-literal = "0.3" +parity-scale-codec = "2.3.1" +sp-core = { git = 'https://github.com/librelois/substrate', branch = 'duniter-monthly-2022-02' } +subxt = { git = 'https://github.com/librelois/subxt', branch = 'duniter-monthly-2022-02' } +tokio = { version = "1.15.0", features = ["macros"] } diff --git a/live-tests/README.md b/live-tests/README.md new file mode 100644 index 000000000..67dce1756 --- /dev/null +++ b/live-tests/README.md @@ -0,0 +1,36 @@ +# Duniter live tests + +Kind of tests that run against a live chain! + +## Sanity tests + +Test suite that verifies the consistency of the onchain storage. + +### Run sanity tests + +1. Checkout the git tag of the runtime that you want to check +2. run the tests again the default network of the specified runtime type: `cargo sanity-RUNTIME_TYPE` + +`RUNTIME_TYPE` should be replaced by `gdev`, `gtest` or `g1`. + +#### Custom RPC endpoint + +You can choose to use another RPC endpoint by setting the environment variable `WS_RPC_ENDPOINT`. +This is also the only way to test against a different network that the default one. + +#### run against a specific block + +You can choose to use run the sanity tests against a specific block by setting the environment +variable `AT_BLOCK_NUMBER`. + +**Be careful: this would require to use an archive node.** + +### Contribute to sanity tests + +The code is in the file `live-tests/tests/sanity_RUNTIME_TYPE.rs` + +There is 3 different parts: + +1. Runtime types definitions +2. Collect storage data +3. Verify consistency of collected data diff --git a/live-tests/src/lib.rs b/live-tests/src/lib.rs new file mode 100644 index 000000000..86934326a --- /dev/null +++ b/live-tests/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Substrate-Libre-Currency. +// +// Substrate-Libre-Currency is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Substrate-Libre-Currency is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>. diff --git a/live-tests/tests/sanity_gdev.rs b/live-tests/tests/sanity_gdev.rs new file mode 100644 index 000000000..c25a3c6a8 --- /dev/null +++ b/live-tests/tests/sanity_gdev.rs @@ -0,0 +1,189 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] +pub mod gdev_runtime {} + +use hex_literal::hex; +use sp_core::crypto::AccountId32; +use sp_core::{ByteArray, H256}; +use std::collections::HashMap; +use subxt::{ClientBuilder, DefaultConfig, DefaultExtra}; + +const DEFAULT_ENDPOINT: &str = "wss://gdev.librelois.fr:443/ws"; + +const TREASURY_ACCOUNT_ID: [u8; 32] = + hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000"); + +type Api = gdev_runtime::RuntimeApi<DefaultConfig, DefaultExtra<DefaultConfig>>; +type Client = subxt::Client<DefaultConfig>; + +// define gdev basic types +type Balance = u64; +type BlockNumber = u32; +type Index = u32; + +// Define gdev types +type AccountInfo = gdev_runtime::runtime_types::frame_system::AccountInfo< + Index, + gdev_runtime::runtime_types::pallet_duniter_account::types::AccountData<Balance>, +>; +type IdtyIndex = u32; +type IdtyValue = + gdev_runtime::runtime_types::pallet_identity::types::IdtyValue<BlockNumber, AccountId32>; +use gdev_runtime::runtime_types::pallet_identity::types::IdtyStatus; + +#[tokio::test(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let ws_rpc_endpoint = + std::env::var("WS_RPC_ENDPOINT").unwrap_or_else(|_| DEFAULT_ENDPOINT.to_owned()); + let client: Client = ClientBuilder::new() + .set_url(ws_rpc_endpoint) + .set_page_size(100) + .build() + .await + .expect("fail to connect to node"); + + let maybe_block_hash = if let Ok(block_number) = std::env::var("AT_BLOCK_NUMBER") { + let block_number: BlockNumber = block_number.parse()?; + println!("Run sanity tests against ÄžDev at block #{}", block_number); + client.rpc().block_hash(Some(block_number.into())).await? + } else { + println!("Run sanity tests against ÄžDev at last best block"); + None + }; + + sanity_tests_at(client, maybe_block_hash).await +} + +async fn sanity_tests_at(client: Client, maybe_block_hash: Option<H256>) -> anyhow::Result<()> { + // Get API + let api = client.clone().to_runtime_api::<Api>(); + + // ===== Collect storage ===== // + + // Collect accounts + let mut accounts = HashMap::new(); + let mut account_iter = api + .storage() + .system() + .account_iter(maybe_block_hash) + .await?; + while let Some((key, account_info)) = account_iter.next().await? { + let mut account_id_bytes = [0u8; 32]; + account_id_bytes.copy_from_slice(&key.0[48..]); + accounts.insert(AccountId32::new(account_id_bytes), account_info); + } + + // Collect identities + let mut identities = HashMap::new(); + let mut idty_iter = api + .storage() + .identity() + .identities_iter(maybe_block_hash) + .await?; + while let Some((key, idty_value)) = idty_iter.next().await? { + let mut idty_index_bytes = [0u8; 4]; + idty_index_bytes.copy_from_slice(&key.0[40..]); + identities.insert(IdtyIndex::from_le_bytes(idty_index_bytes), idty_value); + } + + // ===== Verify storage ===== // + + verify_accounts(&accounts).await?; + verify_identities(&accounts, &identities).await?; + + Ok(()) +} + +async fn verify_accounts(accounts: &HashMap<AccountId32, AccountInfo>) -> anyhow::Result<()> { + for (account_id, account_info) in accounts { + if account_info.sufficients == 0 { + // Rule 1: If the account is not sufficient, it should have at least one provider + assert!( + account_info.providers > 0, + "Account {} has no providers nor sufficients", + account_id + ); + // Rule 2: If the account is not sufficient, it should comply to the existential deposit + assert!( + (account_info.data.free + account_info.data.reserved) >= 200, + "Account {} not respect existential deposit rule", + account_id + ); + } + + // Rule 3: If the account have consumers, it shoul have at least one provider + if account_info.consumers > 0 { + // Rule 1: If the account is not s + assert!( + account_info.providers > 0, + "Account {} has no providers nor sufficients", + account_id + ); + } + + if account_id.as_slice() != TREASURY_ACCOUNT_ID { + // Rule 4: If the account is not a "special account", + // it should have a random id or a consumer + assert!( + account_info.data.random_id.is_some() || account_info.consumers > 0, + "Account {} has no random_id nor consumer", + account_id + ); + } + } + Ok(()) +} + +async fn verify_identities( + accounts: &HashMap<AccountId32, AccountInfo>, + identities: &HashMap<IdtyIndex, IdtyValue>, +) -> anyhow::Result<()> { + for (idty_index, idty_value) in identities { + // Rule 1: each identity should have an account + let idty_account = accounts + .get(&idty_value.owner_key) + .unwrap_or_else(|| panic!("Identity {} has no account", idty_index)); + + // Rule 2: each identity account should be sufficient + assert!( + idty_account.sufficients > 0, + "Identity {} is corrupted: idty_account.sufficients == 0", + idty_index + ); + + match idty_value.status { + IdtyStatus::Validated => { + // Rule 3: If the identity is validated, removable_on shoud be zero + assert!( + idty_value.removable_on == 0, + "Identity {} is corrupted: removable_on > 0 on validated idty", + idty_index + ); + } + _ => { + // Rule 4: If the identity is not validated, next_creatable_identity_on shoud be zero + assert!( + idty_value.next_creatable_identity_on == 0, + "Identity {} is corrupted: next_creatable_identity_on > 0 on non-validated idty", + idty_index + ); + } + } + } + Ok(()) +} -- GitLab