From 73b4bcf144ec6b5b8783089f0ddc0a1a8ec49c49 Mon Sep 17 00:00:00 2001 From: tuxmain <tuxmain@zettascript.org> Date: Sat, 5 Aug 2023 10:04:21 +0200 Subject: [PATCH] feat: distance --- Cargo.lock | 105 ++++++++++++++---- distance-oracle/Cargo.toml | 1 + distance-oracle/src/api.rs | 137 +++++++++++++++-------- distance-oracle/src/lib.rs | 147 +++++++++++++++---------- distance-oracle/src/main.rs | 29 ++++- distance-oracle/src/mock.rs | 101 +++++++++++++---- distance-oracle/src/tests.rs | 95 +++++++++++++++- end2end-tests/tests/common/distance.rs | 14 +-- end2end-tests/tests/cucumber_tests.rs | 6 +- resources/metadata.scale | Bin 133022 -> 133378 bytes 10 files changed, 467 insertions(+), 168 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c5d16da4..1f53e2c90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1571,6 +1571,7 @@ dependencies = [ "clap 4.1.4", "dubp-wot", "flate2", + "fnv", "num-traits", "parity-scale-codec", "rayon", @@ -2035,7 +2036,7 @@ checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "windows-sys 0.42.0", ] @@ -4287,9 +4288,9 @@ checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -5008,11 +5009,11 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" dependencies = [ - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", ] [[package]] @@ -5813,7 +5814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.7", + "parking_lot_core 0.9.8", ] [[package]] @@ -5825,22 +5826,22 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi 0.3.9", ] [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.1", ] [[package]] @@ -6462,6 +6463,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -6469,7 +6479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom 0.2.8", - "redox_syscall", + "redox_syscall 0.2.16", "thiserror", ] @@ -9399,7 +9409,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "remove_dir_all", "winapi 0.3.9", ] @@ -10554,12 +10564,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.1", "windows_aarch64_msvc 0.42.1", "windows_i686_gnu 0.42.1", "windows_i686_msvc 0.42.1", "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.1", "windows_x86_64_msvc 0.42.1", ] @@ -10569,7 +10579,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.1", ] [[package]] @@ -10578,21 +10588,42 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.1", "windows_aarch64_msvc 0.42.1", "windows_i686_gnu 0.42.1", "windows_i686_msvc 0.42.1", "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.1", "windows_x86_64_msvc 0.42.1", ] +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.34.0" @@ -10611,6 +10642,12 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.34.0" @@ -10629,6 +10666,12 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.34.0" @@ -10647,6 +10690,12 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" @@ -10665,12 +10714,24 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" @@ -10689,6 +10750,12 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "winreg" version = "0.10.1" diff --git a/distance-oracle/Cargo.toml b/distance-oracle/Cargo.toml index 5e561323b..5f0b626eb 100644 --- a/distance-oracle/Cargo.toml +++ b/distance-oracle/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" sp-distance = { path = "../primitives/distance" } codec = { package = "parity-scale-codec", version = "3.1.5" } +fnv = "1.0.7" num-traits = "0.2.15" rayon = "1.7.0" sp-core = { git = "https://github.com/duniter/substrate.git", branch = "duniter-substrate-v0.9.32" } diff --git a/distance-oracle/src/api.rs b/distance-oracle/src/api.rs index 71bf77e7a..0b007090d 100644 --- a/distance-oracle/src/api.rs +++ b/distance-oracle/src/api.rs @@ -1,6 +1,22 @@ +// Copyright 2023 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/>. + use crate::{ - gdev, - gdev::runtime_types::{ + runtime, + runtime::runtime_types::{ pallet_distance::median::MedianAcc, sp_arithmetic::per_things::Perbill, sp_core::bounded::{bounded_btree_set::BoundedBTreeSet, bounded_vec::BoundedVec}, @@ -8,8 +24,9 @@ use crate::{ }; use sp_core::H256; +use subxt::storage::StorageKey; -pub type Client = subxt::OnlineClient<crate::GdevConfig>; +pub type Client = subxt::OnlineClient<crate::RuntimeConfig>; pub type AccountId = subxt::ext::sp_runtime::AccountId32; pub type IdtyIndex = u32; pub type EvaluationPool<AccountId, IdtyIndex> = ( @@ -24,7 +41,7 @@ pub async fn client(rpc_url: String) -> Client { pub async fn parent_hash(client: &Client) -> H256 { client .storage() - .fetch(&gdev::storage().system().parent_hash(), None) + .fetch(&runtime::storage().system().parent_hash(), None) .await .unwrap() .unwrap() @@ -34,7 +51,7 @@ pub async fn current_session(client: &Client, parent_hash: H256) -> u32 { client .storage() .fetch( - &gdev::storage().session().current_index(), + &runtime::storage().session().current_index(), Some(parent_hash), ) .await @@ -51,9 +68,9 @@ pub async fn current_pool( .storage() .fetch( &match current_session % 3 { - 0 => gdev::storage().distance().evaluation_pool1(), - 1 => gdev::storage().distance().evaluation_pool2(), - 2 => gdev::storage().distance().evaluation_pool0(), + 0 => runtime::storage().distance().evaluation_pool1(), + 1 => runtime::storage().distance().evaluation_pool2(), + 2 => runtime::storage().distance().evaluation_pool0(), _ => unreachable!("n%3<3"), }, Some(parent_hash), @@ -66,7 +83,7 @@ pub async fn evaluation_block(client: &Client, parent_hash: H256) -> H256 { client .storage() .fetch( - &gdev::storage().distance().evaluation_block(), + &runtime::storage().distance().evaluation_block(), Some(parent_hash), ) .await @@ -74,40 +91,74 @@ pub async fn evaluation_block(client: &Client, parent_hash: H256) -> H256 { .unwrap() } -pub async fn member_iter( - client: &Client, - evaluation_block: H256, -) -> subxt::storage::KeyIter< - crate::GdevConfig, - Client, - subxt::metadata::DecodeStaticType<gdev::runtime_types::sp_membership::MembershipData<u32>>, -> { - client - .storage() - .iter( - gdev::storage().membership().membership(0), - 100, - Some(evaluation_block), - ) - .await - .unwrap() +pub async fn member_iter(client: &Client, evaluation_block: H256) -> MemberIter { + MemberIter( + client + .storage() + .iter( + runtime::storage().membership().membership(0), + 100, + Some(evaluation_block), + ) + .await + .unwrap(), + ) } -pub async fn cert_iter( - client: &Client, - evaluation_block: H256, -) -> subxt::storage::KeyIter< - crate::GdevConfig, - Client, - subxt::metadata::DecodeStaticType<Vec<(u32, u32)>>, -> { - client - .storage() - .iter( - gdev::storage().cert().certs_by_receiver(0), - 100, - Some(evaluation_block), - ) - .await - .unwrap() +pub struct MemberIter( + subxt::storage::KeyIter< + crate::RuntimeConfig, + Client, + subxt::metadata::DecodeStaticType< + runtime::runtime_types::sp_membership::MembershipData<u32>, + >, + >, +); + +impl MemberIter { + pub async fn next(&mut self) -> Result<Option<IdtyIndex>, subxt::error::Error> { + Ok(self + .0 + .next() + .await? + .map(|(storage_key, _membership_data)| idty_id_from_storage_key(&storage_key))) + } +} + +pub async fn cert_iter(client: &Client, evaluation_block: H256) -> CertIter { + CertIter( + client + .storage() + .iter( + runtime::storage().cert().certs_by_receiver(0), + 100, + Some(evaluation_block), + ) + .await + .unwrap(), + ) +} + +pub struct CertIter( + subxt::storage::KeyIter< + crate::RuntimeConfig, + Client, + subxt::metadata::DecodeStaticType<Vec<(IdtyIndex, u32)>>, + >, +); + +impl CertIter { + pub async fn next( + &mut self, + ) -> Result<Option<(IdtyIndex, Vec<(IdtyIndex, u32)>)>, subxt::error::Error> { + Ok(self + .0 + .next() + .await? + .map(|(storage_key, issuers)| (idty_id_from_storage_key(&storage_key), issuers))) + } +} + +fn idty_id_from_storage_key(storage_key: &StorageKey) -> IdtyIndex { + u32::from_le_bytes(storage_key.as_ref()[40..44].try_into().unwrap()) } diff --git a/distance-oracle/src/lib.rs b/distance-oracle/src/lib.rs index 47418cc94..dc5c78b95 100644 --- a/distance-oracle/src/lib.rs +++ b/distance-oracle/src/lib.rs @@ -1,28 +1,44 @@ +// Copyright 2023 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/>. + #[cfg(not(test))] -mod api; +pub mod api; #[cfg(test)] -mod mock; +pub mod mock; #[cfg(test)] mod tests; #[cfg(test)] -use mock as api; +pub use mock as api; use api::{AccountId, IdtyIndex}; use codec::Encode; +use fnv::{FnvHashMap, FnvHashSet}; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; -use std::collections::{HashMap, HashSet}; use std::io::Write; use std::path::PathBuf; -use subxt::storage::StorageKey; +// TODO select metadata file using features #[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] -pub mod gdev {} +pub mod runtime {} -pub enum GdevConfig {} -impl subxt::config::Config for GdevConfig { +pub enum RuntimeConfig {} +impl subxt::config::Config for RuntimeConfig { type Index = u32; type BlockNumber = u32; type Hash = sp_core::H256; @@ -71,8 +87,8 @@ impl Default for Settings { } } -pub async fn run_and_save(settings: Settings) { - let Some((evaluation, current_session, evaluation_result_path)) = run(&settings, true).await else {return}; +pub async fn run_and_save(client: &api::Client, settings: Settings) { + let Some((evaluation, current_session, evaluation_result_path)) = run(client, &settings, true).await else {return}; let mut evaluation_result_file = std::fs::OpenOptions::new() .write(true) @@ -108,17 +124,16 @@ pub async fn run_and_save(settings: Settings) { /// Returns `(evaluation, current_session, evaluation_result_path)` pub async fn run( + client: &api::Client, settings: &Settings, handle_fs: bool, ) -> Option<(Vec<sp_runtime::Perbill>, u32, PathBuf)> { - let client = api::client(settings.rpc_url.clone()).await; - - let parent_hash = api::parent_hash(&client).await; + let parent_hash = api::parent_hash(client).await; - let current_session = api::current_session(&client, parent_hash).await; + let current_session = api::current_session(client, parent_hash).await; // Fetch the pending identities - let Some(evaluation_pool) = api::current_pool(&client, parent_hash, current_session).await + let Some(evaluation_pool) = api::current_pool(client, parent_hash, current_session).await else { println!("Pool does not exist"); return None @@ -144,14 +159,14 @@ pub async fn run( std::fs::create_dir_all(&settings.evaluation_result_dir).unwrap(); } - let evaluation_block = api::evaluation_block(&client, parent_hash).await; + let evaluation_block = api::evaluation_block(client, parent_hash).await; // member idty -> issued certs - let mut members = HashMap::<IdtyIndex, u32>::new(); + let mut members = FnvHashMap::<IdtyIndex, u32>::default(); - let mut members_iter = api::member_iter(&client, evaluation_block).await; - while let Some((member_idty, _membership_expire)) = members_iter.next().await.unwrap() { - members.insert(idty_id_from_storage_key(&member_idty), 0); + let mut members_iter = api::member_iter(client, evaluation_block).await; + while let Some(member_idty) = members_iter.next().await.unwrap() { + members.insert(member_idty, 0); } let min_certs_for_referee = (members.len() as f32) @@ -159,22 +174,19 @@ pub async fn run( .ceil() as u32; // idty -> received certs - let mut received_certs = HashMap::<IdtyIndex, Vec<IdtyIndex>>::new(); + let mut received_certs = FnvHashMap::<IdtyIndex, Vec<IdtyIndex>>::default(); - let mut certs_iter = api::cert_iter(&client, evaluation_block).await; + let mut certs_iter = api::cert_iter(client, evaluation_block).await; while let Some((receiver, issuers)) = certs_iter.next().await.unwrap() { - let receiver = idty_id_from_storage_key(&receiver); - // Update members' issued certs count - if issuers.len() as u32 >= min_certs_for_referee { - for (issuer, _removable_on) in issuers.iter() { - if let Some(issued_certs) = members.get_mut(issuer) { - *issued_certs += 1; - } - } - } else { + if (issuers.len() as u32) < min_certs_for_referee { // This member is not referee members.remove(&receiver); } + for (issuer, _removable_on) in issuers.iter() { + if let Some(issued_certs) = members.get_mut(issuer) { + *issued_certs += 1; + } + } received_certs.insert( receiver, issuers @@ -185,7 +197,6 @@ pub async fn run( } // Only retain referees - // TODO benchmark: can it be faster? (maybe using drain_filter) members.retain(|_idty, issued_certs| *issued_certs >= min_certs_for_referee); let referees = members; @@ -194,66 +205,82 @@ pub async fn run( .0 .as_slice() .par_iter() - .map(|(idty, _)| { - sp_runtime::Perbill::from_rational( - distance_rule(&received_certs, &referees, settings.max_depth, *idty), - referees.len() as u32, - ) - }) + .map(|(idty, _)| distance_rule(&received_certs, &referees, settings.max_depth, *idty)) .collect(); Some((evaluation, current_session, evaluation_result_path)) } fn distance_rule_recursive( - received_certs: &HashMap<IdtyIndex, Vec<IdtyIndex>>, - referees: &HashMap<IdtyIndex, u32>, + received_certs: &FnvHashMap<IdtyIndex, Vec<IdtyIndex>>, + referees: &FnvHashMap<IdtyIndex, u32>, idty: IdtyIndex, - accessible_referees: &mut std::collections::HashSet<IdtyIndex>, + accessible_referees: &mut FnvHashSet<IdtyIndex>, + known_idties: &mut FnvHashMap<IdtyIndex, u32>, depth: u32, ) { + // Do not re-explore identities that have already been explored at least as deeply + match known_idties.entry(idty) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + if *entry.get() >= depth { + return; + } else { + *entry.get_mut() = depth; + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(depth); + } + } + + // If referee, add it to the list if referees.contains_key(&idty) { accessible_referees.insert(idty); } + + // If reached the maximum distance, stop exploring if depth == 0 { return; } - for &certifier in received_certs.get(&idty).expect("unreachable").iter() { + + // Explore certifiers + for &certifier in received_certs.get(&idty).unwrap_or(&vec![]).iter() { distance_rule_recursive( received_certs, referees, certifier, accessible_referees, + known_idties, depth - 1, ); } } +/// Returns `(nb_accessible_referees, nb_referees)` fn distance_rule( - received_certs: &HashMap<IdtyIndex, Vec<IdtyIndex>>, - referees: &HashMap<IdtyIndex, u32>, + received_certs: &FnvHashMap<IdtyIndex, Vec<IdtyIndex>>, + referees: &FnvHashMap<IdtyIndex, u32>, depth: u32, idty: IdtyIndex, -) -> u32 { - let mut accessible_referees = HashSet::<u32>::new(); +) -> sp_runtime::Perbill { + let mut accessible_referees = + FnvHashSet::<IdtyIndex>::with_capacity_and_hasher(referees.len(), Default::default()); + let mut known_idties = + FnvHashMap::<IdtyIndex, u32>::with_capacity_and_hasher(referees.len(), Default::default()); distance_rule_recursive( received_certs, referees, idty, &mut accessible_referees, - depth + 1, + &mut known_idties, + depth, ); - accessible_referees.len() as u32 -} - -fn idty_id_from_storage_key(storage_key: &StorageKey) -> IdtyIndex { - u32::from_le_bytes(storage_key.as_ref()[40..44].try_into().unwrap()) + if referees.contains_key(&idty) { + sp_runtime::Perbill::from_rational( + accessible_referees.len() as u32 - 1, + referees.len() as u32 - 1, + ) + } else { + sp_runtime::Perbill::from_rational(accessible_referees.len() as u32, referees.len() as u32) + } } -/* -impl num_traits::Pow<usize> for Perbill {} -impl std::ops::Div<Perbill> for Perbill {} -impl num_traits::Bounded for Perbill {} -impl Ord for Perbill {} -impl Eq for Perbill {} - -impl PerThing for Perbill {}*/ diff --git a/distance-oracle/src/main.rs b/distance-oracle/src/main.rs index 5d25f621b..9d1c62340 100644 --- a/distance-oracle/src/main.rs +++ b/distance-oracle/src/main.rs @@ -1,3 +1,19 @@ +// Copyright 2023 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/>. + use clap::Parser; #[derive(Debug, clap::Parser)] @@ -15,10 +31,13 @@ struct Cli { async fn main() { let cli = Cli::parse(); - distance_oracle::run_and_save(distance_oracle::Settings { - evaluation_result_dir: cli.evaluation_result_dir.into(), - max_depth: cli.max_depth, - rpc_url: cli.rpc_url, - }) + distance_oracle::run_and_save( + &distance_oracle::api::client(cli.rpc_url.clone()).await, + distance_oracle::Settings { + evaluation_result_dir: cli.evaluation_result_dir.into(), + max_depth: cli.max_depth, + rpc_url: cli.rpc_url, + }, + ) .await; } diff --git a/distance-oracle/src/mock.rs b/distance-oracle/src/mock.rs index faf20d0ef..54798d976 100644 --- a/distance-oracle/src/mock.rs +++ b/distance-oracle/src/mock.rs @@ -1,18 +1,42 @@ -use crate::gdev::runtime_types::{ +// Copyright 2023 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/>. + +use crate::runtime::runtime_types::{ pallet_distance::median::MedianAcc, sp_arithmetic::per_things::Perbill, }; +use dubp_wot::{data::rusty::RustyWebOfTrust, WebOfTrust, WotId}; use sp_core::H256; use std::collections::BTreeSet; -pub struct Client; +pub struct Client { + wot: RustyWebOfTrust, + pub pool_len: usize, +} pub type AccountId = subxt::ext::sp_runtime::AccountId32; pub type IdtyIndex = u32; pub type EvaluationPool<AccountId, IdtyIndex> = ((Vec<(IdtyIndex, MedianAcc<Perbill>)>,), BTreeSet<AccountId>); pub async fn client(_rpc_url: String) -> Client { - Client + unimplemented!() +} + +pub fn client_from_wot(wot: RustyWebOfTrust) -> Client { + Client { wot, pool_len: 1 } } pub async fn parent_hash(_client: &Client) -> H256 { @@ -24,42 +48,71 @@ pub async fn current_session(_client: &Client, _parent_hash: H256) -> u32 { } pub async fn current_pool( - _client: &Client, + client: &Client, _parent_hash: H256, _current_session: u32, ) -> Option<EvaluationPool<AccountId, IdtyIndex>> { - Some(((vec![],), std::collections::BTreeSet::new())) + Some(( + (client + .wot + .get_enabled() + .into_iter() + .chain(client.wot.get_disabled().into_iter()) + .zip(0..client.pool_len) + .map(|(wot_id, _)| { + (wot_id.0 as IdtyIndex, unsafe { + std::mem::transmute((Vec::<()>::new(), Option::<u32>::None, 0)) + }) + }) + .collect(),), + std::collections::BTreeSet::new(), + )) } pub async fn evaluation_block(_client: &Client, _parent_hash: H256) -> H256 { Default::default() } -pub async fn member_iter( - _client: &Client, - _evaluation_block: H256, -) -> KeyIter<(subxt::storage::StorageKey, ())> { - KeyIter::new(Vec::new()) +pub async fn member_iter(client: &Client, _evaluation_block: H256) -> MemberIter { + MemberIter(client.wot.get_enabled().into_iter()) } -pub async fn cert_iter( - _client: &Client, - _evaluation_block: H256, -) -> KeyIter<(subxt::storage::StorageKey, Vec<(u32, u32)>)> { - KeyIter::new(Vec::new()) +pub struct MemberIter(std::vec::IntoIter<WotId>); + +impl MemberIter { + pub async fn next(&mut self) -> Result<Option<IdtyIndex>, subxt::error::Error> { + Ok(self.0.next().map(|wot_id| wot_id.0 as u32)) + } } -pub struct KeyIter<T>(std::vec::IntoIter<T>); +pub async fn cert_iter(client: &Client, _evaluation_block: H256) -> CertIter { + CertIter( + client + .wot + .get_enabled() + .iter() + .chain(client.wot.get_disabled().iter()) + .map(|wot_id| { + ( + wot_id.0 as IdtyIndex, + client + .wot + .get_links_source(*wot_id) + .unwrap_or_default() + .into_iter() + .map(|wot_id| (wot_id.0 as IdtyIndex, 0)) + .collect::<Vec<(IdtyIndex, u32)>>(), + ) + }) + .collect::<Vec<_>>() + .into_iter(), + ) +} -impl<T> KeyIter<T> { - fn new(items: Vec<T>) -> Self { - Self(items.into_iter()) - } +pub struct CertIter(std::vec::IntoIter<(IdtyIndex, Vec<(IdtyIndex, u32)>)>); - pub async fn next(&mut self) -> Result<Option<T>, subxt::error::Error> - where - T: Clone, - { +impl CertIter { + pub async fn next(&mut self) -> Result<Option<(u32, Vec<(u32, u32)>)>, subxt::error::Error> { Ok(self.0.next()) } } diff --git a/distance-oracle/src/tests.rs b/distance-oracle/src/tests.rs index 4c3f90fbe..f2f6d77ed 100644 --- a/distance-oracle/src/tests.rs +++ b/distance-oracle/src/tests.rs @@ -1,12 +1,101 @@ -use dubp_wot::data::rusty::RustyWebOfTrust; +// Copyright 2023 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/>. + +use dubp_wot::{ + data::rusty::RustyWebOfTrust, operations::distance::DistanceCalculator, WebOfTrust, +}; use flate2::read::ZlibDecoder; +use sp_runtime::Perbill; use std::{fs::File, io::Read}; #[tokio::test] +#[ignore = "long to execute"] async fn test_distance_against_v1() { - let _wot = wot_from_v1_file(); + let wot = wot_from_v1_file(); + let n = wot.size(); + let max_depth = 5; + let min_certs_for_referee = (wot.get_enabled().len() as f32).powf(1. / 5.).ceil() as u32; + + // Reference implementation + let ref_calculator = dubp_wot::operations::distance::RustyDistanceCalculator; + let t_a = std::time::Instant::now(); + let ref_results: Vec<Perbill> = wot + .get_enabled() + .into_iter() + .chain(wot.get_disabled().into_iter()) + .zip(0..n) + .map(|(i, _)| { + let result = ref_calculator + .compute_distance( + &wot, + dubp_wot::operations::distance::WotDistanceParameters { + node: i, + sentry_requirement: min_certs_for_referee, + step_max: max_depth, + x_percent: 0.8, + }, + ) + .unwrap(); + Perbill::from_rational(result.success, result.sentries) + }) + .collect(); + println!("ref time: {}", t_a.elapsed().as_millis()); + + // Our implementation + let mut client = crate::api::client_from_wot(wot); + client.pool_len = n; + + let t_a = std::time::Instant::now(); + let results = crate::run( + &client, + &crate::Settings { + max_depth, + ..Default::default() + }, + false, + ) + .await + .unwrap(); + println!("new time: {}", t_a.elapsed().as_millis()); + assert_eq!(results.0.len(), n); + + let mut errors: Vec<_> = results + .0 + .iter() + .zip(ref_results.iter()) + .map(|(r, r_ref)| r.deconstruct() as i64 - r_ref.deconstruct() as i64) + .collect(); + errors.sort_unstable(); + println!( + "Error: {:?} / {:?} / {:?} / {:?} / {:?} (min / 1Q / med / 3Q / max)", + errors[0], + errors[errors.len() / 4], + errors[errors.len() / 2], + errors[errors.len() * 3 / 4], + errors[errors.len() - 1] + ); - crate::run(&Default::default(), false).await; + let correct_results = results + .0 + .iter() + .zip(ref_results.iter()) + .map(|(r, r_ref)| (r == r_ref) as usize) + .sum::<usize>(); + println!("Correct results: {correct_results} / {n}"); + assert_eq!(correct_results, n); } fn wot_from_v1_file() -> RustyWebOfTrust { diff --git a/end2end-tests/tests/common/distance.rs b/end2end-tests/tests/common/distance.rs index 15f9d61ce..5b946ca93 100644 --- a/end2end-tests/tests/common/distance.rs +++ b/end2end-tests/tests/common/distance.rs @@ -46,6 +46,7 @@ pub async fn run_oracle(client: &Client, origin: AccountKeyring, rpc_url: String let account_id: &AccountId32 = origin.account_id(); if let Some((distances, _current_session, _evaluation_result_path)) = distance_oracle::run( + &distance_oracle::api::client(rpc_url.clone()).await, &distance_oracle::Settings { evaluation_result_dir: PathBuf::default(), max_depth: 5, @@ -64,21 +65,16 @@ pub async fn run_oracle(client: &Client, origin: AccountKeyring, rpc_url: String client .tx() .create_signed( - &gdev::tx().sudo().sudo(/*gdev::runtime_types::gdev_runtime::RuntimeCall::UpgradeOrigin(gdev::runtime_types::pallet_upgrade_origin::pallet::Call::dispatch_as_root { - call: Box::new(*/gdev::runtime_types::gdev_runtime::RuntimeCall::Distance( + &gdev::tx().sudo().sudo(gdev::runtime_types::gdev_runtime::RuntimeCall::Distance( gdev::runtime_types::pallet_distance::pallet::Call::force_update_evaluation { evaluator: account_id.clone(), computation_result: gdev::runtime_types::sp_distance::ComputationResult { - distances: unsafe { - std::mem::transmute(distances) - }, + distances: distances.into_iter().map(|res| unsafe{std::mem::transmute(res)}).collect(), }, }, - )/*) - })*/), - //&gdev::tx().upgrade_origin().dispatch_as_root( - //), + ) + ), &origin, BaseExtrinsicParamsBuilder::new(), ) diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index 1712389a7..1c3d02f34 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -513,11 +513,7 @@ async fn should_have_distance_ok(world: &mut DuniterWorld, who: String) -> Resul .unwrap(); match world - .read( - &gdev::storage() - .distance() - .identities_distance_status(idty_id), - ) + .read(&gdev::storage().distance().identity_distance_status(idty_id)) .await? { Some(gdev::runtime_types::pallet_distance::types::DistanceStatus::Valid) => Ok(()), diff --git a/resources/metadata.scale b/resources/metadata.scale index 9582cd2af48d15f0121380317bb240fe5e4527a7..50671f6abfc29105bbfd0d68104bc525fde27b4f 100644 GIT binary patch delta 3375 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 bhYpw*o|>7SQNk!Xy^ftpnpKjugO?QmJ$D8p delta 3221 zcmZpg#4)d*gDp3;B#}>aBb!MQBkN@Aq&mj8lXoS}VthL}G&z-#ar5rvg<LELnHVol z-k2FXd2gON%Sk52onVe@z7oq>CdR#!H)h66KAx|{a*>H~E12V1pvic1^2O}<$(IV$ zSne`0t^^B56w0tXWn$b2=3FV1XZZ`#;Fuk|Ii$#_oRMd9`%6#8$)&HSPyYDCjq&K_ z-EY1#G9I10_OaCDv+rgzp4_be+<=McEYtLZt&Gx}w|x|6W4yTe-1omsj8`|G`NhY{ zcy;obUux4;xEaN^i?c8u<7T`%{T@G~I@3+2>9xI#O4Bt37*{ghoqkz>F_Q7&c3nZn z5D~_Q({%+IowgrQW&Fg*_;C7mbw+ijr%clqPGS_F{#~6>lJVtsK@CO$HpZ9J1vMCz zx4Y^w1~M_e-9FubF_Mw-<@S$;j1^3rFPRt^n6@%9yq#WT!l=Rcar$f%Mhn)jOpJ_- zlNH&8r@uB~G-vtA#3(p<W2Pf#Sbk;-0}}(o$LR&8jM0o=r=KxpY!Lgţ$5TBf1 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 -- GitLab