-
Hugo Trentesaux authored
* update live tests * wip add position of duplicate * wip add counter for owner key * wip add coherence test * wip
Hugo Trentesaux authored* update live tests * wip add position of duplicate * wip add counter for owner key * wip add coherence test * wip
sanity_gdev.rs 14.50 KiB
// 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 {}
use countmap::CountMap;
use hex_literal::hex;
use sp_core::crypto::AccountId32;
use sp_core::{blake2_128, ByteArray, H256};
use std::collections::{HashMap, HashSet};
use subxt::config::SubstrateConfig as GdevConfig;
const DEFAULT_ENDPOINT: &str = "ws://localhost:9944";
const EXISTENTIAL_DEPOSIT: u64 = 100;
const TREASURY_ACCOUNT_ID: [u8; 32] =
hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000");
type Client = subxt::OnlineClient<GdevConfig>;
// define gdev basic types
type Balance = u64;
type BlockNumber = u32;
type Index = u32;
// Define gdev types
type AccountInfo = gdev::runtime_types::frame_system::AccountInfo<
Index,
gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance, IdtyIndex>,
>;
type IdtyData = gdev::runtime_types::common_runtime::entities::IdtyData;
type IdtyIndex = u32;
type IdtyValue =
gdev::runtime_types::pallet_identity::types::IdtyValue<BlockNumber, AccountId32, IdtyData>;
use gdev::runtime_types::pallet_identity::types::IdtyStatus;
struct Storage {
accounts: HashMap<AccountId32, AccountInfo>,
identities: HashMap<IdtyIndex, IdtyValue>,
identity_index_of: HashMap<[u8; 16], IdtyIndex>,
}
#[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::from_url(ws_rpc_endpoint)
.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<()> {
// ===== Collect storage ===== //
// Collect accounts
let mut accounts: HashMap<AccountId32, AccountInfo> = HashMap::new();
let mut account_iter = client
.storage()
.at_latest()
.await
.unwrap()
.iter(gdev::storage().system().account_root(), 100)
.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);
}
println!("accounts.len(): {}.", accounts.len());
// Collect identities
let mut identities: HashMap<IdtyIndex, IdtyValue> = HashMap::new();
let mut idty_iter = client
.storage()
.at_latest()
.await
.unwrap()
.iter(gdev::storage().identity().identities_root(), 100)
.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..]);
let idty_val = IdtyValue {
data: idty_value.data,
next_creatable_identity_on: idty_value.next_creatable_identity_on,
old_owner_key: None, // Not used in the live test, skip the conversion
owner_key: AccountId32::from(idty_value.owner_key.0),
removable_on: idty_value.removable_on,
status: idty_value.status,
};
identities.insert(IdtyIndex::from_le_bytes(idty_index_bytes), idty_val);
}
println!("identities.len(): {}.", identities.len());
// Collect identity_index_of
let mut identity_index_of: HashMap<[u8; 16], IdtyIndex> = HashMap::new();
let mut idty_index_of_iter = client
.storage()
.at_latest()
.await
.unwrap()
.iter(gdev::storage().identity().identity_index_of_root(), 100)
.await?;
while let Some((key, idty_index)) = idty_index_of_iter.next().await? {
let mut blake2_128_bytes = [0u8; 16];
blake2_128_bytes.copy_from_slice(&key.0[32..48]);
identity_index_of.insert(blake2_128_bytes, idty_index);
}
println!("identity_index_of.len(): {}.", identity_index_of.len());
let storage = Storage {
accounts,
identities,
identity_index_of,
};
// ===== Verify storage ===== //
verifier::Verifier::new().verify_storage(&storage).await
}
mod verifier {
use super::*;
pub(super) struct Verifier {
errors: Vec<String>,
}
impl Verifier {
pub(super) fn new() -> Self {
Self { errors: Vec::new() }
}
// FIXME why async functions when called with await?
/// method to run all storage tests
pub(super) async fn verify_storage(&mut self, storage: &Storage) -> anyhow::Result<()> {
self.verify_accounts(&storage.accounts).await;
self.verify_identities(&storage.accounts, &storage.identities)
.await;
self.verify_identity_index_of(&storage.identities, &storage.identity_index_of)
.await;
self.verify_identity_coherence(&storage.identities, &storage.identity_index_of)
.await;
if self.errors.is_empty() {
Ok(())
} else {
for error in &self.errors {
println!("{}", error);
}
Err(anyhow::anyhow!(
"Storage corrupted: {} errors.",
self.errors.len()
))
}
}
/// assert method to collect errors
fn assert(&mut self, assertion: bool, error: String) {
if !assertion {
self.errors.push(error);
}
}
/// like assert but just push error
fn error(&mut self, error: String) {
self.errors.push(error);
}
/// check accounts sufficients and consumers (specific to duniter-account pallet)
async fn verify_accounts(&mut self, accounts: &HashMap<AccountId32, AccountInfo>) {
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
self.assert(
account_info.providers > 0,
format!("Account {} has no providers nor sufficients.", account_id),
);
// Rule 2: If the account is not sufficient, it should comply to the existential deposit
self.assert(
(account_info.data.free + account_info.data.reserved)
>= EXISTENTIAL_DEPOSIT,
format!(
"Account {} not respect existential deposit rule.",
account_id
),
);
}
// Rule 3: If the account have consumers, it should have at least one provider
if account_info.consumers > 0 {
// Rule 1: If the account is not sufficient [...]
self.assert(
account_info.providers > 0,
format!("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
self.assert(
account_info.data.random_id.is_some() || account_info.consumers > 0,
format!("Account {} has no random_id nor consumer.", account_id),
);
}
}
}
/// check list of identities (account existence, sufficient)
async fn verify_identities(
&mut self,
accounts: &HashMap<AccountId32, AccountInfo>,
identities: &HashMap<IdtyIndex, IdtyValue>,
) {
// counts occurence of owner key
let mut countmap = CountMap::<AccountId32, u8>::new();
// list owner key with multiple occurences
let mut duplicates = HashSet::new();
for (idty_index, idty_value) in identities {
countmap.insert_or_increment(idty_value.owner_key.clone());
if let Some(count) = countmap.get_count(&idty_value.owner_key) {
if count > 1 {
self.error(format!(
"address {} is the owner_key of {count} identities",
idty_value.owner_key
));
if count == 2 {
duplicates.insert(idty_value.owner_key.clone());
}
}
}
// Rule 1: each identity should have an account
let maybe_account = accounts.get(&idty_value.owner_key);
self.assert(
maybe_account.is_some(),
format!("Identity {} has no account.", idty_index),
);
if let Some(account) = maybe_account {
// Rule 2: each identity account should be sufficient
self.assert(
account.sufficients > 0,
format!(
"Identity {} is corrupted: idty_account.sufficients == 0",
idty_index
),
);
}
match idty_value.status {
IdtyStatus::Validated => {
// Rule 3: If the identity is validated, removable_on should be zero
self.assert(
idty_value.removable_on == 0,
format!(
"Identity {} is corrupted: removable_on > 0 on validated idty",
idty_index
),
);
}
_ => {
// Rule 4: If the identity is not validated, next_creatable_identity_on should be zero
self.assert(
idty_value.next_creatable_identity_on == 0,
format!("Identity {} is corrupted: next_creatable_identity_on > 0 on non-validated idty",
idty_index)
);
}
}
}
for (idty_index, idty_value) in identities {
if duplicates.contains(&idty_value.owner_key) {
self.error(format!(
"duplicate key {} at position {idty_index}",
idty_value.owner_key
));
}
}
}
/// check the identity hashmap (length, identity existence, hash matches owner key)
async fn verify_identity_index_of(
&mut self,
identities: &HashMap<IdtyIndex, IdtyValue>,
identity_index_of: &HashMap<[u8; 16], IdtyIndex>,
) {
// Rule1: identity_index_of should have the same lenght as identities
self.assert(
identities.len() == identity_index_of.len(),
format!(
"identities.len({}) != identity_index_of.len({}).",
identities.len(),
identity_index_of.len()
),
);
for (blake2_128_owner_key, idty_index) in identity_index_of {
let maybe_idty_value = identities.get(idty_index);
// Rule2: Each identity_index_of should point to an existing identity
self.assert(
maybe_idty_value.is_some(),
format!(
"Identity {} not exist, but still referenced in IdentityIndexOf.",
idty_index
),
);
if let Some(idty_value) = maybe_idty_value {
// Rule3: identity_index_of key should correspond to the blake2_12- hash of
// identity owner key
self.assert(
blake2_128_owner_key == &blake2_128(idty_value.owner_key.as_slice()),
format!(
"Identity {} is referenced in IdentityIndexOf with an invalid key hash.",
idty_index
),
);
}
}
}
/// check coherence between identity list and identity index hashmap
async fn verify_identity_coherence(
&mut self,
identities: &HashMap<IdtyIndex, IdtyValue>,
identity_index_of: &HashMap<[u8; 16], IdtyIndex>,
) {
// each identity should be correcly referenced in the hashmap
for (idty_index, idty_value) in identities {
// hash owner key to get key
let blake2_128_owner_key = &blake2_128(idty_value.owner_key.as_slice());
// get identity index from hashmap
if let Some(index_of) = identity_index_of.get(blake2_128_owner_key) {
self.assert(idty_index == index_of,
format!("identity number {idty_index} with owner key {0} is mapped to identity index {index_of}", idty_value.owner_key));
} else {
self.error(format!(
"identity with owner key {} is not present in hashmap",
idty_value.owner_key
));
}
}
}
}
}