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
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

-- 
GitLab