diff --git a/end2end-tests/cucumber-features/distance.feature b/end2end-tests/cucumber-features/distance.feature
new file mode 100644
index 0000000000000000000000000000000000000000..9cc18d514ce91ef7d296edfbcb3cab0520b4691c
--- /dev/null
+++ b/end2end-tests/cucumber-features/distance.feature
@@ -0,0 +1,24 @@
+Feature: Distance
+
+    Scenario: Alice certifies Eve
+        When alice sends 6 ÄžD to dave
+        When 15 blocks later
+        When alice creates identity for dave
+        Then dave identity should be created
+        Then dave should be certified by alice
+        When dave confirms his identity with pseudo "dave"
+        Then dave identity should be confirmed
+        When bob certifies dave
+        Then dave should be certified by bob
+        When alice requests distance evaluation for dave
+        Then dave should have distance result in 2 sessions
+        When 30 blocks later
+        Then dave should have distance result in 1 session
+        When distance oracle runs
+        Then dave should have distance result in 1 session
+        When 30 blocks later
+        Then dave should have distance result in 0 session
+        Then dave should have distance ok
+        When alice validates dave identity
+        When 3 blocks later
+        Then dave identity should be validated
diff --git a/end2end-tests/tests/common/distance.rs b/end2end-tests/tests/common/distance.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6c792700d3140c150fbdb218b2aeb93282bc0cac
--- /dev/null
+++ b/end2end-tests/tests/common/distance.rs
@@ -0,0 +1,41 @@
+// 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 super::gdev;
+use super::gdev::runtime_types::pallet_identity;
+use super::*;
+use crate::DuniterWorld;
+use sp_keyring::AccountKeyring;
+use subxt::tx::PairSigner;
+
+pub async fn request_evaluation(client: &Client, from: AccountKeyring, to: u32) -> Result<()> {
+    let from = PairSigner::new(from.pair());
+
+    let _events = create_block_with_extrinsic(
+        client,
+        client
+            .tx()
+            .create_signed(
+                &gdev::tx().distance().evaluate_distance(to),
+                &from,
+                BaseExtrinsicParamsBuilder::new(),
+            )
+            .await?,
+    )
+    .await?;
+
+    Ok(())
+}
diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs
index 56ccdea5deadfb58b23dc74f2b7a818f245e3f97..2ff94d982b7d57b3dfdf982bb0aa019fa6849ddf 100644
--- a/end2end-tests/tests/common/mod.rs
+++ b/end2end-tests/tests/common/mod.rs
@@ -18,6 +18,7 @@
 
 pub mod balances;
 pub mod cert;
+pub mod distance;
 pub mod identity;
 pub mod oneshot;
 
@@ -85,6 +86,7 @@ impl Process {
     }
 }
 
+pub const DISTANCE_ORACLE_LOCAL_PATH: &str = "../target/debug/distance-oracle";
 const DUNITER_DOCKER_PATH: &str = "/usr/local/bin/duniter";
 const DUNITER_LOCAL_PATH: &str = "../target/debug/duniter";
 
@@ -94,7 +96,7 @@ struct FullNode {
     ws_port: u16,
 }
 
-pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Client, Process) {
+pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Client, Process, u16) {
     println!("maybe_genesis_conf_file={:?}", maybe_genesis_conf_file);
     let duniter_binary_path = std::env::var("DUNITER_BINARY_PATH").unwrap_or_else(|_| {
         if std::path::Path::new(DUNITER_DOCKER_PATH).exists() {
@@ -117,7 +119,7 @@ pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Client, Pr
         .await
         .expect("fail to connect to node");
 
-    (client, process)
+    (client, process, ws_port)
 }
 
 pub async fn create_empty_block(client: &Client) -> Result<()> {
@@ -173,6 +175,9 @@ fn spawn_full_node(
     let log_file_path = format!("duniter-v2s-{}.log", ws_port);
     let log_file = std::fs::File::create(&log_file_path).expect("fail to create log file");
 
+    // Clean previous data
+    std::fs::remove_dir_all("/tmp/duniter-cucumber").ok();
+
     // Command
     let process = Process(
         Command::new(duniter_binary_path)
@@ -180,13 +185,14 @@ fn spawn_full_node(
                 [
                     "--no-telemetry",
                     "--no-prometheus",
-                    "--tmp",
                     "--port",
                     &p2p_port.to_string(),
                     "--rpc-port",
                     &rpc_port.to_string(),
                     "--ws-port",
                     &ws_port.to_string(),
+                    "--base-path",
+                    "/tmp/duniter-cucumber",
                 ]
                 .iter()
                 .chain(args),
@@ -249,3 +255,22 @@ fn wait_until_log_line(expected_log_line: &str, log_file_path: &str, timeout: st
         }
     }
 }
+
+pub fn spawn_distance_oracle(distance_oracle_binary_path: &str, duniter_rpc_port: u16) {
+    Command::new(distance_oracle_binary_path)
+        .args(
+            [
+                "-u",
+                &format!("ws://127.0.0.1:{duniter_rpc_port}"),
+                "-d",
+                "/tmp/duniter-cucumber/chains/gdev/distance",
+                "-c",
+                "2",
+            ]
+            .iter(),
+        )
+        .spawn()
+        .expect("failed to spawn distance oracle")
+        .wait()
+        .unwrap();
+}
diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs
index 094932d4bfd8ac137e6bae82f74be7f69d379046..21e0a8be539e0fef56bf37c133238c89f35f621d 100644
--- a/end2end-tests/tests/cucumber_tests.rs
+++ b/end2end-tests/tests/cucumber_tests.rs
@@ -120,12 +120,17 @@ impl World for DuniterWorld {
 struct DuniterWorldInner {
     client: Client,
     process: Process,
+    ws_port: u16,
 }
 
 impl DuniterWorldInner {
     async fn new(maybe_genesis_conf_file: Option<PathBuf>) -> Self {
-        let (client, process) = spawn_node(maybe_genesis_conf_file).await;
-        DuniterWorldInner { client, process }
+        let (client, process, ws_port) = spawn_node(maybe_genesis_conf_file).await;
+        DuniterWorldInner {
+            client,
+            process,
+            ws_port,
+        }
     }
     fn kill(&mut self) {
         self.process.kill();
@@ -322,6 +327,28 @@ async fn validate_identity(world: &mut DuniterWorld, from: String, to: String) -
     common::identity::validate_identity(world.client(), from, to).await
 }
 
+#[when(regex = r#"([a-zA-Z]+) requests distance evaluation for ([a-zA-Z]+)"#)]
+async fn request_distance_evaluation(
+    world: &mut DuniterWorld,
+    from: String,
+    to: String,
+) -> Result<()> {
+    // input names to keyrings
+    let from = AccountKeyring::from_str(&from).expect("unknown from");
+    let to: u32 = common::identity::get_identity_index(world, to).await?;
+
+    common::distance::request_evaluation(world.client(), from, to).await
+}
+
+#[when(regex = r#"distance oracle runs"#)]
+async fn run_distance_oracle(world: &mut DuniterWorld) -> Result<()> {
+    common::spawn_distance_oracle(
+        common::DISTANCE_ORACLE_LOCAL_PATH,
+        world.inner.as_ref().unwrap().ws_port,
+    );
+    Ok(())
+}
+
 // ===== then ====
 
 #[then(regex = r"([a-zA-Z]+) should have (\d+) (ÄžD|cÄžD)")]
@@ -437,6 +464,66 @@ async fn should_be_certified_by(
     }
 }
 
+#[then(regex = r"([a-zA-Z]+) should have distance result in (\d+) sessions?")]
+async fn should_have_distance_result_in_sessions(
+    world: &mut DuniterWorld,
+    who: String,
+    sessions: u32,
+) -> Result<()> {
+    assert!(sessions < 3, "Session number must be < 3");
+
+    let who = AccountKeyring::from_str(&who).unwrap().to_account_id();
+
+    let idty_id = world
+        .read(&gdev::storage().identity().identity_index_of(&who))
+        .await?
+        .unwrap();
+
+    let current_session = world
+        .read(&gdev::storage().session().current_index())
+        .await?
+        .unwrap_or_default();
+
+    let pool = world
+        .read(&match (current_session + sessions) % 3 {
+            0 => gdev::storage().distance().evaluation_pool0(),
+            1 => gdev::storage().distance().evaluation_pool1(),
+            2 => gdev::storage().distance().evaluation_pool2(),
+            _ => unreachable!("n%3<3"),
+        })
+        .await
+        .unwrap()
+        .ok_or_else(|| anyhow::anyhow!("given pool is empty"))?;
+
+    for (sample_idty, _) in pool.0 .0 {
+        if sample_idty == idty_id {
+            return Ok(());
+        }
+    }
+
+    Err(anyhow::anyhow!("no evaluation in given pool").into())
+}
+
+#[then(regex = r"([a-zA-Z]+) should have distance ok")]
+async fn should_have_distance_ok(world: &mut DuniterWorld, who: String) -> Result<()> {
+    let who = AccountKeyring::from_str(&who).unwrap().to_account_id();
+
+    let idty_id = world
+        .read(&gdev::storage().identity().identity_index_of(&who))
+        .await?
+        .unwrap();
+
+    if world
+        .read(&gdev::storage().distance().distance_ok_identities(&idty_id))
+        .await?
+        .unwrap()
+    {
+        Ok(())
+    } else {
+        Err(anyhow::anyhow!("no evaluation in given pool").into())
+    }
+}
+
 use gdev::runtime_types::pallet_identity::types::IdtyStatus;
 
 // status from string