diff --git a/Cargo.lock b/Cargo.lock
index ee49fef20c0..622dc7e38ae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -991,7 +991,7 @@ dependencies = [
 
 [[package]]
 name = "client-snapshot"
-version = "0.1.21"
+version = "0.1.22"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -3666,7 +3666,7 @@ dependencies = [
 
 [[package]]
 name = "mithril-client"
-version = "0.10.4"
+version = "0.10.5"
 dependencies = [
  "anyhow",
  "async-recursion",
@@ -3698,7 +3698,7 @@ dependencies = [
 
 [[package]]
 name = "mithril-client-cli"
-version = "0.10.5"
+version = "0.10.6"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -3727,14 +3727,17 @@ dependencies = [
 
 [[package]]
 name = "mithril-client-wasm"
-version = "0.7.2"
+version = "0.7.3"
 dependencies = [
+ "anyhow",
  "async-trait",
+ "chrono",
  "futures",
  "mithril-build-script",
  "mithril-client",
  "serde",
  "serde-wasm-bindgen",
+ "serde_json",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "wasm-bindgen-test",
@@ -3743,7 +3746,7 @@ dependencies = [
 
 [[package]]
 name = "mithril-common"
-version = "0.4.97"
+version = "0.4.98"
 dependencies = [
  "anyhow",
  "async-trait",
diff --git a/examples/client-snapshot/Cargo.toml b/examples/client-snapshot/Cargo.toml
index 616209b6314..bebd268c3dd 100644
--- a/examples/client-snapshot/Cargo.toml
+++ b/examples/client-snapshot/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "client-snapshot"
 description = "Mithril client snapshot example"
-version = "0.1.21"
+version = "0.1.22"
 authors = ["dev@iohk.io", "mithril-dev@iohk.io"]
 documentation = "https://mithril.network/doc"
 edition = "2021"
diff --git a/examples/client-snapshot/src/main.rs b/examples/client-snapshot/src/main.rs
index 3b76b6c0730..64a94a5a978 100644
--- a/examples/client-snapshot/src/main.rs
+++ b/examples/client-snapshot/src/main.rs
@@ -171,6 +171,16 @@ impl FeedbackReceiver for IndicatifFeedbackReceiver {
                     progress_bar.inc(1);
                 }
             }
+            MithrilEvent::CertificateFetchedFromCache {
+                certificate_chain_validation_id: _,
+                certificate_hash,
+            } => {
+                let certificate_validation_pb = self.certificate_validation_pb.read().await;
+                if let Some(progress_bar) = certificate_validation_pb.as_ref() {
+                    progress_bar.set_message(format!("Cached '{certificate_hash}'"));
+                    progress_bar.inc(1);
+                }
+            }
             MithrilEvent::CertificateChainValidated {
                 certificate_chain_validation_id: _,
             } => {
diff --git a/examples/client-wasm-nodejs/package-lock.json b/examples/client-wasm-nodejs/package-lock.json
index f55ebb68521..bd297d1dd16 100644
--- a/examples/client-wasm-nodejs/package-lock.json
+++ b/examples/client-wasm-nodejs/package-lock.json
@@ -16,7 +16,7 @@
     },
     "../../mithril-client-wasm": {
       "name": "@mithril-dev/mithril-client-wasm",
-      "version": "0.7.1",
+      "version": "0.7.3",
       "license": "Apache-2.0"
     },
     "node_modules/@mithril-dev/mithril-client-wasm": {
diff --git a/examples/client-wasm-web/package-lock.json b/examples/client-wasm-web/package-lock.json
index fce143d5b0a..1e6b8dbaeb7 100644
--- a/examples/client-wasm-web/package-lock.json
+++ b/examples/client-wasm-web/package-lock.json
@@ -20,7 +20,7 @@
     },
     "../../mithril-client-wasm": {
       "name": "@mithril-dev/mithril-client-wasm",
-      "version": "0.7.1",
+      "version": "0.7.3",
       "license": "Apache-2.0"
     },
     "node_modules/@discoveryjs/json-ext": {
diff --git a/mithril-client-cli/Cargo.toml b/mithril-client-cli/Cargo.toml
index 7ac0d8853b0..6f6834f3d9c 100644
--- a/mithril-client-cli/Cargo.toml
+++ b/mithril-client-cli/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mithril-client-cli"
-version = "0.10.5"
+version = "0.10.6"
 description = "A Mithril Client"
 authors = { workspace = true }
 edition = { workspace = true }
diff --git a/mithril-client-cli/src/utils/feedback_receiver.rs b/mithril-client-cli/src/utils/feedback_receiver.rs
index b9f9874cf05..ef515c19a8c 100644
--- a/mithril-client-cli/src/utils/feedback_receiver.rs
+++ b/mithril-client-cli/src/utils/feedback_receiver.rs
@@ -92,6 +92,16 @@ impl FeedbackReceiver for IndicatifFeedbackReceiver {
                     progress_bar.inc(1);
                 }
             }
+            MithrilEvent::CertificateFetchedFromCache {
+                certificate_chain_validation_id: _,
+                certificate_hash,
+            } => {
+                let certificate_validation_pb = self.certificate_validation_pb.read().await;
+                if let Some(progress_bar) = certificate_validation_pb.as_ref() {
+                    progress_bar.set_message(format!("Cached '{certificate_hash}'"));
+                    progress_bar.inc(1);
+                }
+            }
             MithrilEvent::CertificateChainValidated {
                 certificate_chain_validation_id: _,
             } => {
diff --git a/mithril-client-wasm/Cargo.toml b/mithril-client-wasm/Cargo.toml
index aacb529584c..bda0cee095d 100644
--- a/mithril-client-wasm/Cargo.toml
+++ b/mithril-client-wasm/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mithril-client-wasm"
-version = "0.7.2"
+version = "0.7.3"
 description = "Mithril client WASM"
 authors = { workspace = true }
 edition = { workspace = true }
@@ -13,14 +13,17 @@ categories = ["cryptography"]
 crate-type = ["cdylib"]
 
 [dependencies]
+anyhow = "1.0.94"
 async-trait = "0.1.83"
+chrono = { version = "0.4.38", features = ["serde"] }
 futures = "0.3.31"
 mithril-client = { path = "../mithril-client", features = ["unstable"] }
 serde = { version = "1.0.215", features = ["derive"] }
 serde-wasm-bindgen = "0.6.5"
+serde_json = "1.0.132"
 wasm-bindgen = "0.2.99"
 wasm-bindgen-futures = "0.4.49"
-web-sys = { version = "0.3.76", features = ["BroadcastChannel"] }
+web-sys = { version = "0.3.76", features = ["BroadcastChannel", "console", "Storage", "Window"] }
 
 [dev-dependencies]
 wasm-bindgen-test = "0.3.49"
diff --git a/mithril-client-wasm/ci-test/package-lock.json b/mithril-client-wasm/ci-test/package-lock.json
index e1ecc779f3b..88f16107717 100644
--- a/mithril-client-wasm/ci-test/package-lock.json
+++ b/mithril-client-wasm/ci-test/package-lock.json
@@ -21,7 +21,7 @@
     },
     "..": {
       "name": "@mithril-dev/mithril-client-wasm",
-      "version": "0.7.2",
+      "version": "0.7.3",
       "license": "Apache-2.0"
     },
     "node_modules/@discoveryjs/json-ext": {
diff --git a/mithril-client-wasm/package.json b/mithril-client-wasm/package.json
index c318ee8a74b..b9600e8105f 100644
--- a/mithril-client-wasm/package.json
+++ b/mithril-client-wasm/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@mithril-dev/mithril-client-wasm",
-  "version": "0.7.2",
+  "version": "0.7.3",
   "description": "Mithril client WASM",
   "license": "Apache-2.0",
   "collaborators": [
diff --git a/mithril-client-wasm/src/certificate_verification_cache.rs b/mithril-client-wasm/src/certificate_verification_cache.rs
new file mode 100644
index 00000000000..bf85e7084d9
--- /dev/null
+++ b/mithril-client-wasm/src/certificate_verification_cache.rs
@@ -0,0 +1,437 @@
+use anyhow::{anyhow, Context};
+use async_trait::async_trait;
+use chrono::{DateTime, TimeDelta, Utc};
+use std::ops::Add;
+use web_sys::{window, Storage};
+
+use mithril_client::certificate_client::CertificateVerifierCache;
+use mithril_client::MithrilResult;
+
+pub type CertificateHash = str;
+pub type PreviousCertificateHash = str;
+
+/// Browser local-storage based cache for the certificate verifier.
+///
+/// Note : as this cache is based on the browser local storage, it can only be used in a browser
+/// (it is not compatible with nodejs or other non-browser environment).
+pub struct LocalStorageCertificateVerifierCache {
+    cache_key_prefix: String,
+    expiration_delay: TimeDelta,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
+struct CachedCertificate {
+    previous_hash: String,
+    expire_at: DateTime<Utc>,
+}
+
+impl CachedCertificate {
+    fn new<TPreviousHash: Into<String>>(
+        previous_hash: TPreviousHash,
+        expire_at: DateTime<Utc>,
+    ) -> Self {
+        CachedCertificate {
+            previous_hash: previous_hash.into(),
+            expire_at,
+        }
+    }
+}
+
+impl LocalStorageCertificateVerifierCache {
+    /// `LocalStorageCertificateVerifierCache` factory
+    pub fn new(cache_key_prefix_seed: &str, expiration_delay: TimeDelta) -> Self {
+        const CACHE_KEY_BASE_PREFIX: &'static str = "certificate_cache";
+
+        LocalStorageCertificateVerifierCache {
+            cache_key_prefix: format!("{CACHE_KEY_BASE_PREFIX}_{cache_key_prefix_seed}_"),
+            expiration_delay,
+        }
+    }
+
+    fn push(
+        &self,
+        certificate_hash: &CertificateHash,
+        previous_certificate_hash: &PreviousCertificateHash,
+        expire_at: DateTime<Utc>,
+    ) -> MithrilResult<()> {
+        let key = self.cache_key(certificate_hash);
+        open_local_storage()?
+            .set_item(
+                &key,
+                &serde_json::to_string(&CachedCertificate::new(
+                    previous_certificate_hash,
+                    expire_at,
+                ))
+                .map_err(|err| anyhow!("Error serializing cache: {err:?}"))?,
+            )
+            .map_err(|err| anyhow!("Error storing key `{key}` in local storage: {err:?}"))?;
+
+        Ok(())
+    }
+
+    fn parse_cached_certificate(value: String) -> MithrilResult<CachedCertificate> {
+        serde_json::from_str(&value)
+            .map_err(|err| anyhow!("Error deserializing cached certificate: {err:?}"))
+    }
+
+    fn cache_key(&self, certificate_hash: &CertificateHash) -> String {
+        format!("{}{}", self.cache_key_prefix, certificate_hash)
+    }
+}
+
+fn open_local_storage() -> MithrilResult<Storage> {
+    let window = window()
+        .with_context(|| "No window object")?
+        .local_storage()
+        .map_err(|err| anyhow!("Error accessing local storage: {err:?}"))?
+        .with_context(|| "No local storage object")?;
+    Ok(window)
+}
+
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
+#[cfg_attr(not(target_family = "wasm"), async_trait)]
+impl CertificateVerifierCache for LocalStorageCertificateVerifierCache {
+    async fn store_validated_certificate(
+        &self,
+        certificate_hash: &CertificateHash,
+        previous_certificate_hash: &PreviousCertificateHash,
+    ) -> MithrilResult<()> {
+        self.push(
+            certificate_hash,
+            previous_certificate_hash,
+            Utc::now().add(self.expiration_delay),
+        )?;
+        Ok(())
+    }
+
+    async fn get_previous_hash(
+        &self,
+        certificate_hash: &CertificateHash,
+    ) -> MithrilResult<Option<String>> {
+        let key = self.cache_key(certificate_hash);
+        match open_local_storage()?
+            .get_item(&key)
+            .map_err(|err| anyhow!("Error accessing key `{key}` from local storage: {err:?}"))?
+        {
+            Some(value) => {
+                let cached = Self::parse_cached_certificate(value)?;
+                if Utc::now() >= cached.expire_at {
+                    Ok(None)
+                } else {
+                    Ok(Some(cached.previous_hash))
+                }
+            }
+            None => Ok(None),
+        }
+    }
+
+    async fn reset(&self) -> MithrilResult<()> {
+        let storage = open_local_storage()?;
+        let len = storage
+            .length()
+            .map_err(|err| anyhow!("Error accessing local storage length: {err:?}"))?;
+        let mut key_to_remove = vec![];
+
+        for i in 0..len {
+            match storage.key(i).map_err(|err| {
+                anyhow!("Error accessing key index `{i}` from local storage: {err:?}")
+            })? {
+                Some(key) if key.starts_with(&self.cache_key_prefix) => key_to_remove.push(key),
+                _ => continue,
+            }
+        }
+
+        for key in key_to_remove {
+            storage
+                .remove_item(&key)
+                .map_err(|err| anyhow!("Error removing key `{key}` from local storage: {err:?}"))?;
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+pub(crate) mod test_tools {
+    use std::collections::HashMap;
+
+    use super::*;
+
+    /// `Test only` Return the raw content of the local storage
+    pub(crate) fn local_storage_content() -> HashMap<String, String> {
+        let storage = open_local_storage().unwrap();
+        let len = storage.length().unwrap();
+        let mut content = HashMap::new();
+
+        for i in 0..len {
+            let key = storage.key(i).unwrap().unwrap();
+            let value = storage.get_item(&key).unwrap().unwrap();
+            content.insert(key, value);
+        }
+
+        content
+    }
+
+    impl LocalStorageCertificateVerifierCache {
+        /// `Test only` Return the number of items in the cache
+        pub(crate) fn len(&self) -> usize {
+            local_storage_content()
+                .into_iter()
+                .filter(|(k, _v)| k.starts_with(&self.cache_key_prefix))
+                .count()
+        }
+
+        /// `Test only` Populate the cache with the given hash and previous hash
+        pub(crate) fn with_items<'a, T>(self, key_values: T) -> Self
+        where
+            T: IntoIterator<Item = (&'a CertificateHash, &'a PreviousCertificateHash)>,
+        {
+            let expire_at = Utc::now() + self.expiration_delay;
+            for (k, v) in key_values {
+                self.push(k, v, expire_at).unwrap();
+            }
+            self
+        }
+
+        /// `Test only` Return the content of the cache (without the expiration date)
+        pub(crate) fn content(&self) -> HashMap<String, String> {
+            local_storage_content()
+                .into_iter()
+                .filter(|(k, _v)| k.starts_with(&self.cache_key_prefix))
+                .map(|(k, v)| {
+                    (
+                        k.trim_start_matches(&self.cache_key_prefix).to_string(),
+                        Self::parse_cached_certificate(v).unwrap().previous_hash,
+                    )
+                })
+                .collect()
+        }
+
+        /// `Test only` Overwrite the expiration date of an entry the given certificate hash.
+        ///
+        /// panic if the key is not found
+        pub(crate) fn overwrite_expiration_date(
+            &self,
+            certificate_hash: &CertificateHash,
+            expire_at: DateTime<Utc>,
+        ) {
+            let storage = open_local_storage().unwrap();
+            let key = self.cache_key(certificate_hash);
+            let existing_value = Self::parse_cached_certificate(
+                storage.get_item(&key).unwrap().expect("Key not found"),
+            )
+            .unwrap();
+            storage
+                .set_item(
+                    &key,
+                    &serde_json::to_string(&CachedCertificate::new(
+                        &existing_value.previous_hash,
+                        expire_at,
+                    ))
+                    .unwrap(),
+                )
+                .unwrap();
+        }
+
+        /// `Test only` Get the cached value for the given certificate hash
+        pub(super) fn get_cached_value(
+            &self,
+            certificate_hash: &CertificateHash,
+        ) -> Option<CachedCertificate> {
+            let storage = open_local_storage().unwrap();
+            storage
+                .get_item(&self.cache_key(certificate_hash))
+                .unwrap()
+                .map(Self::parse_cached_certificate)
+                .transpose()
+                .unwrap()
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::collections::HashMap;
+    use wasm_bindgen_test::*;
+
+    use super::{test_tools::*, *};
+
+    // Note: as those tests are using local storage, they MUST be run in a browser as node doesn't
+    // have support for local storage.
+    wasm_bindgen_test_configure!(run_in_browser);
+
+    #[wasm_bindgen_test]
+    async fn from_str_iterator() {
+        let cache =
+            LocalStorageCertificateVerifierCache::new("from_str_iterator", TimeDelta::hours(1))
+                .with_items([("first", "one"), ("second", "two")]);
+
+        assert_eq!(
+            HashMap::from_iter([
+                ("first".to_string(), "one".to_string()),
+                ("second".to_string(), "two".to_string())
+            ]),
+            cache.content()
+        );
+    }
+
+    mod store_validated_certificate {
+        use super::*;
+
+        #[wasm_bindgen_test]
+        async fn store_in_empty_cache_add_new_item_that_expire_after_parametrized_delay() {
+            let expiration_delay = TimeDelta::hours(1);
+            let start_time = Utc::now();
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "store_in_empty_cache_add_new_item_that_expire_after_parametrized_delay",
+                expiration_delay,
+            );
+            cache
+                .store_validated_certificate("hash", "parent")
+                .await
+                .unwrap();
+
+            let cached = cache
+                .get_cached_value("hash")
+                .expect("Cache should have been populated");
+
+            assert_eq!(1, cache.len());
+            assert_eq!("parent", cached.previous_hash);
+            assert!(cached.expire_at - start_time >= expiration_delay);
+        }
+
+        #[wasm_bindgen_test]
+        async fn store_new_hash_push_new_key_at_end_and_dont_alter_existing_values() {
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "store_new_hash_push_new_key_at_end_and_dont_alter_existing_values",
+                TimeDelta::hours(1),
+            )
+            .with_items([
+                ("existing_hash", "existing_parent"),
+                ("another_hash", "another_parent"),
+            ]);
+            cache
+                .store_validated_certificate("new_hash", "new_parent")
+                .await
+                .unwrap();
+
+            assert_eq!(
+                HashMap::from_iter([
+                    ("existing_hash".to_string(), "existing_parent".to_string()),
+                    ("another_hash".to_string(), "another_parent".to_string()),
+                    ("new_hash".to_string(), "new_parent".to_string()),
+                ]),
+                cache.content()
+            );
+        }
+
+        #[wasm_bindgen_test]
+        async fn storing_same_hash_update_parent_hash_and_expiration_time() {
+            let expiration_delay = TimeDelta::days(2);
+            let start_time = Utc::now();
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "storing_same_hash_update_parent_hash_and_expiration_time",
+                expiration_delay,
+            )
+            .with_items([("hash", "first_parent"), ("another_hash", "another_parent")]);
+
+            let initial_value = cache.get_cached_value("hash").unwrap();
+
+            cache
+                .store_validated_certificate("hash", "updated_parent")
+                .await
+                .unwrap();
+
+            let updated_value = cache.get_cached_value("hash").unwrap();
+
+            assert_eq!(2, cache.len());
+            assert_eq!(
+                Some("another_parent".to_string()),
+                cache.get_previous_hash("another_hash").await.unwrap(),
+                "Existing but not updated value should not have been altered, content: {:#?}, start_time: {:?}",
+                local_storage_content(), start_time,
+            );
+            assert_eq!("updated_parent", updated_value.previous_hash);
+            assert_ne!(initial_value, updated_value);
+            assert!(updated_value.expire_at - start_time >= expiration_delay);
+        }
+    }
+
+    mod get_previous_hash {
+        use super::*;
+
+        #[wasm_bindgen_test]
+        async fn get_previous_hash_when_key_exists() {
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "get_previous_hash_when_key_exists",
+                TimeDelta::hours(1),
+            )
+            .with_items([("hash", "parent"), ("another_hash", "another_parent")]);
+
+            assert_eq!(
+                Some("parent".to_string()),
+                cache.get_previous_hash("hash").await.unwrap()
+            );
+        }
+
+        #[wasm_bindgen_test]
+        async fn get_previous_hash_return_none_if_not_found() {
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "get_previous_hash_return_none_if_not_found",
+                TimeDelta::hours(1),
+            )
+            .with_items([("hash", "parent"), ("another_hash", "another_parent")]);
+
+            assert_eq!(None, cache.get_previous_hash("not_found").await.unwrap());
+        }
+
+        #[wasm_bindgen_test]
+        async fn get_expired_previous_hash_return_none() {
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "get_expired_previous_hash_return_none",
+                TimeDelta::hours(1),
+            )
+            .with_items([("hash", "parent")]);
+            cache.overwrite_expiration_date("hash", Utc::now() - TimeDelta::days(5));
+
+            assert_eq!(None, cache.get_previous_hash("hash").await.unwrap());
+        }
+    }
+
+    mod reset {
+        use super::*;
+
+        #[wasm_bindgen_test]
+        async fn reset_empty_cache_dont_raise_error() {
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "reset_empty_cache_dont_raise_error",
+                TimeDelta::hours(1),
+            );
+
+            cache.reset().await.unwrap();
+
+            assert_eq!(HashMap::new(), cache.content());
+        }
+
+        #[wasm_bindgen_test]
+        async fn reset_not_empty_cache() {
+            let cache = LocalStorageCertificateVerifierCache::new(
+                "reset_not_empty_cache",
+                TimeDelta::hours(1),
+            )
+            .with_items([("hash", "parent"), ("another_hash", "another_parent")]);
+            let storage = open_local_storage().unwrap();
+            storage
+                .set_item("key_from_another_component", "another_value")
+                .unwrap();
+
+            cache.reset().await.unwrap();
+
+            assert_eq!(HashMap::new(), cache.content());
+            assert_eq!(
+                Some(&"another_value".to_string()),
+                local_storage_content().get("key_from_another_component")
+            );
+        }
+    }
+}
diff --git a/mithril-client-wasm/src/client_wasm.rs b/mithril-client-wasm/src/client_wasm.rs
index e0946a492d3..a64e256720d 100644
--- a/mithril-client-wasm/src/client_wasm.rs
+++ b/mithril-client-wasm/src/client_wasm.rs
@@ -1,15 +1,18 @@
 use async_trait::async_trait;
+use chrono::TimeDelta;
 use serde::Serialize;
 use std::sync::Arc;
 use wasm_bindgen::prelude::*;
 
 use mithril_client::{
+    certificate_client::CertificateVerifierCache,
     common::Epoch,
     feedback::{FeedbackReceiver, MithrilEvent},
     CardanoTransactionsProofs, Client, ClientBuilder, ClientOptions, MessageBuilder,
     MithrilCertificate,
 };
 
+use crate::certificate_verification_cache::LocalStorageCertificateVerifierCache;
 use crate::WasmResult;
 
 macro_rules! allow_unstable_dead_code {
@@ -66,7 +69,7 @@ impl From<MithrilEvent> for MithrilEventWasm {
 #[wasm_bindgen(getter_with_clone)]
 pub struct MithrilClient {
     client: Client,
-
+    certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
     unstable: bool,
 }
 
@@ -88,15 +91,58 @@ impl MithrilClient {
                 .map_err(|err| format!("Failed to parse options: {err:?}"))
                 .unwrap()
         };
-        let unstable = client_options.unstable;
+
+        let certificate_verifier_cache = if client_options.unstable
+            && client_options.enable_certificate_chain_verification_cache
+        {
+            Self::build_certifier_cache(
+                aggregator_endpoint,
+                TimeDelta::seconds(
+                    client_options.certificate_chain_verification_cache_duration_in_seconds as i64,
+                ),
+            )
+        } else {
+            None
+        };
+
         let client = ClientBuilder::aggregator(aggregator_endpoint, genesis_verification_key)
             .add_feedback_receiver(feedback_receiver)
-            .with_options(client_options)
+            .with_options(client_options.clone())
+            .with_certificate_verifier_cache(certificate_verifier_cache.clone())
             .build()
             .map_err(|err| format!("{err:?}"))
             .unwrap();
 
-        MithrilClient { client, unstable }
+        MithrilClient {
+            client,
+            certificate_verifier_cache,
+            unstable: client_options.unstable,
+        }
+    }
+
+    fn build_certifier_cache(
+        aggregator_endpoint: &str,
+        expiration_delay: TimeDelta,
+    ) -> Option<Arc<dyn CertificateVerifierCache>> {
+        if web_sys::window().is_none() {
+            web_sys::console::warn_1(
+                &"Can't enable certificate chain verification cache: window object is not available\
+                    (are you running in a browser environment?)"
+                    .into(),
+            );
+            return None;
+        }
+
+        web_sys::console::warn_1(
+            &"Danger: the certificate chain verification cache is enabled.\n\
+            This feature is highly experimental and insecure, and it must not be used in production."
+                .into(),
+        );
+
+        Some(Arc::new(LocalStorageCertificateVerifierCache::new(
+            aggregator_endpoint,
+            expiration_delay,
+        )))
     }
 
     /// Call the client to get a snapshot from a digest
@@ -367,6 +413,18 @@ impl MithrilClient {
 
         Ok(serde_wasm_bindgen::to_value(&result)?)
     }
+
+    /// `unstable` Reset the certificate verifier cache if enabled
+    #[wasm_bindgen]
+    pub async fn reset_certificate_verifier_cache(&self) -> Result<(), JsValue> {
+        self.guard_unstable()?;
+
+        if let Some(cache) = self.certificate_verifier_cache.as_ref() {
+            cache.reset().await.map_err(|err| format!("{err:?}"))?;
+        }
+
+        Ok(())
+    }
 }
 
 allow_unstable_dead_code! {
@@ -402,8 +460,7 @@ mod tests {
     const FAKE_AGGREGATOR_IP: &str = "127.0.0.1";
     const FAKE_AGGREGATOR_PORT: &str = "8000";
 
-    fn get_mithril_client(unstable: bool) -> MithrilClient {
-        let options = ClientOptions::new(None).with_unstable_features(unstable);
+    fn get_mithril_client(options: ClientOptions) -> MithrilClient {
         let options_js_value = serde_wasm_bindgen::to_value(&options).unwrap();
         MithrilClient::new(
             &format!(
@@ -416,7 +473,8 @@ mod tests {
     }
 
     fn get_mithril_client_stable() -> MithrilClient {
-        get_mithril_client(false)
+        let options = ClientOptions::new(None).with_unstable_features(false);
+        get_mithril_client(options)
     }
 
     wasm_bindgen_test_configure!(run_in_browser);
diff --git a/mithril-client-wasm/src/lib.rs b/mithril-client-wasm/src/lib.rs
index ce4b7280f3a..3a29a7b07bd 100644
--- a/mithril-client-wasm/src/lib.rs
+++ b/mithril-client-wasm/src/lib.rs
@@ -2,11 +2,11 @@
 #![cfg(target_family = "wasm")]
 #![cfg_attr(target_family = "wasm", warn(missing_docs))]
 
+mod certificate_verification_cache;
 mod client_wasm;
-
-pub use client_wasm::MithrilClient;
-
 #[cfg(test)]
 mod test_data;
 
+pub use client_wasm::MithrilClient;
+
 pub(crate) type WasmResult = Result<wasm_bindgen::JsValue, wasm_bindgen::JsValue>;
diff --git a/mithril-client/Cargo.toml b/mithril-client/Cargo.toml
index c95fedc7c94..080296e9e33 100644
--- a/mithril-client/Cargo.toml
+++ b/mithril-client/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mithril-client"
-version = "0.10.4"
+version = "0.10.5"
 description = "Mithril client library"
 authors = { workspace = true }
 edition = { workspace = true }
@@ -41,7 +41,7 @@ reqwest = { version = "0.12.9", default-features = false, features = [
 semver = "1.0.23"
 serde = { version = "1.0.215", features = ["derive"] }
 serde_json = "1.0.133"
-slog = "2.7.0"
+slog = { version = "2.7.0", features = ["max_level_trace", "release_max_level_warn"] }
 strum = { version = "0.26.3", features = ["derive"] }
 tar = { version = "0.4.43", optional = true }
 thiserror = "2.0.6"
diff --git a/mithril-client/src/certificate_client.rs b/mithril-client/src/certificate_client.rs
deleted file mode 100644
index de33a81cf82..00000000000
--- a/mithril-client/src/certificate_client.rs
+++ /dev/null
@@ -1,519 +0,0 @@
-//! A client which retrieves and validates certificates from an Aggregator.
-//!
-//! In order to do so it defines a [CertificateClient] exposes the following features:
-//!  - [get][CertificateClient::get]: get a certificate data from its hash
-//!  - [list][CertificateClient::list]: get the list of available certificates
-//!  - [verify_chain][CertificateClient::verify_chain]: verify a certificate chain
-//!
-//! # Get a certificate
-//!
-//! To get a certificate using the [ClientBuilder][crate::client::ClientBuilder].
-//!
-//! ```no_run
-//! # async fn run() -> mithril_client::MithrilResult<()> {
-//! use mithril_client::ClientBuilder;
-//!
-//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
-//! let certificate = client.certificate().get("CERTIFICATE_HASH").await?.unwrap();
-//!
-//! println!("Certificate hash={}, signed_message={}", certificate.hash, certificate.signed_message);
-//! #    Ok(())
-//! # }
-//! ```
-//!
-//! # List available certificates
-//!
-//! To list available certificates using the [ClientBuilder][crate::client::ClientBuilder].
-//!
-//! ```no_run
-//! # async fn run() -> mithril_client::MithrilResult<()> {
-//! use mithril_client::ClientBuilder;
-//!
-//! let client = mithril_client::ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
-//! let certificates = client.certificate().list().await?;
-//!
-//! for certificate in certificates {
-//!     println!("Certificate hash={}, signed_message={}", certificate.hash, certificate.signed_message);
-//! }
-//! #    Ok(())
-//! # }
-//! ```
-//!
-//! # Validate a certificate chain
-//!
-//! To validate a certificate using the [ClientBuilder][crate::client::ClientBuilder].
-//!
-//! ```no_run
-//! # async fn run() -> mithril_client::MithrilResult<()> {
-//! use mithril_client::ClientBuilder;
-//!
-//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
-//! let certificate = client.certificate().verify_chain("CERTIFICATE_HASH").await?;
-//!
-//! println!("Chain of Certificate (hash: {}) is valid", certificate.hash);
-//! #    Ok(())
-//! # }
-//! ```
-
-use std::sync::Arc;
-
-use anyhow::{anyhow, Context};
-use async_trait::async_trait;
-use slog::{crit, Logger};
-
-use mithril_common::{
-    certificate_chain::{
-        CertificateRetriever, CertificateRetrieverError,
-        CertificateVerifier as CommonCertificateVerifier,
-        MithrilCertificateVerifier as CommonMithrilCertificateVerifier,
-    },
-    crypto_helper::ProtocolGenesisVerificationKey,
-    entities::Certificate,
-    logging::LoggerExtensions,
-    messages::CertificateMessage,
-};
-
-use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
-use crate::feedback::{FeedbackSender, MithrilEvent};
-use crate::{MithrilCertificate, MithrilCertificateListItem, MithrilResult};
-
-/// Aggregator client for the Certificate
-pub struct CertificateClient {
-    aggregator_client: Arc<dyn AggregatorClient>,
-    retriever: Arc<InternalCertificateRetriever>,
-    verifier: Arc<dyn CertificateVerifier>,
-}
-
-/// API that defines how to validate certificates.
-#[cfg_attr(test, mockall::automock)]
-#[cfg_attr(target_family = "wasm", async_trait(?Send))]
-#[cfg_attr(not(target_family = "wasm"), async_trait)]
-pub trait CertificateVerifier: Sync + Send {
-    /// Validate the chain starting with the given certificate.
-    async fn verify_chain(&self, certificate: &MithrilCertificate) -> MithrilResult<()>;
-}
-
-impl CertificateClient {
-    /// Constructs a new `CertificateClient`.
-    pub fn new(
-        aggregator_client: Arc<dyn AggregatorClient>,
-        verifier: Arc<dyn CertificateVerifier>,
-        logger: Logger,
-    ) -> Self {
-        let logger = logger.new_with_component_name::<Self>();
-        let retriever = Arc::new(InternalCertificateRetriever {
-            aggregator_client: aggregator_client.clone(),
-            logger,
-        });
-
-        Self {
-            aggregator_client,
-            retriever,
-            verifier,
-        }
-    }
-
-    /// Fetch a list of certificates
-    pub async fn list(&self) -> MithrilResult<Vec<MithrilCertificateListItem>> {
-        let response = self
-            .aggregator_client
-            .get_content(AggregatorRequest::ListCertificates)
-            .await
-            .with_context(|| "CertificateClient can not get the certificate list")?;
-        let items = serde_json::from_str::<Vec<MithrilCertificateListItem>>(&response)
-            .with_context(|| "CertificateClient can not deserialize certificate list")?;
-
-        Ok(items)
-    }
-
-    /// Get a single certificate full information from the aggregator.
-    pub async fn get(&self, certificate_hash: &str) -> MithrilResult<Option<MithrilCertificate>> {
-        self.retriever.get(certificate_hash).await
-    }
-
-    /// Validate the chain starting with the certificate with given `certificate_hash`, return the certificate if
-    /// the chain is valid.
-    ///
-    /// This method will fail if no certificate exists for the given `certificate_hash`.
-    pub async fn verify_chain(&self, certificate_hash: &str) -> MithrilResult<MithrilCertificate> {
-        let certificate = self.retriever.get(certificate_hash).await?.ok_or(anyhow!(
-            "No certificate exist for hash '{certificate_hash}'"
-        ))?;
-
-        self.verifier
-            .verify_chain(&certificate)
-            .await
-            .with_context(|| {
-                format!("Certificate chain of certificate '{certificate_hash}' is invalid")
-            })?;
-
-        Ok(certificate)
-    }
-}
-
-/// Internal type to implement the [InternalCertificateRetriever] trait and avoid a circular
-/// dependency between the [CertificateClient] and the [CommonMithrilCertificateVerifier] that need
-/// a [CertificateRetriever] as a dependency.
-struct InternalCertificateRetriever {
-    aggregator_client: Arc<dyn AggregatorClient>,
-    logger: Logger,
-}
-
-impl InternalCertificateRetriever {
-    async fn get(&self, certificate_hash: &str) -> MithrilResult<Option<MithrilCertificate>> {
-        let response = self
-            .aggregator_client
-            .get_content(AggregatorRequest::GetCertificate {
-                hash: certificate_hash.to_string(),
-            })
-            .await;
-
-        match response {
-            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
-            Err(e) => Err(e.into()),
-            Ok(response) => {
-                let message =
-                    serde_json::from_str::<CertificateMessage>(&response).inspect_err(|e| {
-                        crit!(
-                            self.logger, "Could not create certificate from API message";
-                            "error" => e.to_string(),
-                            "raw_message" => response
-                        );
-                    })?;
-
-                Ok(Some(message))
-            }
-        }
-    }
-}
-
-/// Implementation of a [CertificateVerifier] that can send feedbacks using
-/// the [feedback][crate::feedback] mechanism.
-pub struct MithrilCertificateVerifier {
-    internal_verifier: Arc<dyn CommonCertificateVerifier>,
-    genesis_verification_key: ProtocolGenesisVerificationKey,
-    feedback_sender: FeedbackSender,
-}
-
-impl MithrilCertificateVerifier {
-    /// Constructs a new `MithrilCertificateVerifier`.
-    pub fn new(
-        aggregator_client: Arc<dyn AggregatorClient>,
-        genesis_verification_key: &str,
-        feedback_sender: FeedbackSender,
-        logger: Logger,
-    ) -> MithrilResult<MithrilCertificateVerifier> {
-        let logger = logger.new_with_component_name::<Self>();
-        let retriever = Arc::new(InternalCertificateRetriever {
-            aggregator_client: aggregator_client.clone(),
-            logger: logger.clone(),
-        });
-        let internal_verifier = Arc::new(CommonMithrilCertificateVerifier::new(
-            logger,
-            retriever.clone(),
-        ));
-        let genesis_verification_key =
-            ProtocolGenesisVerificationKey::try_from(genesis_verification_key)
-                .with_context(|| "Invalid genesis verification key")?;
-
-        Ok(Self {
-            internal_verifier,
-            genesis_verification_key,
-            feedback_sender,
-        })
-    }
-}
-
-#[cfg_attr(target_family = "wasm", async_trait(?Send))]
-#[cfg_attr(not(target_family = "wasm"), async_trait)]
-impl CertificateVerifier for MithrilCertificateVerifier {
-    async fn verify_chain(&self, certificate: &MithrilCertificate) -> MithrilResult<()> {
-        // Todo: move most of this code in the `mithril_common` verifier by defining
-        // a new `verify_chain` method that take a callback called when a certificate is
-        // validated.
-        let certificate_chain_validation_id = MithrilEvent::new_certificate_chain_validation_id();
-        self.feedback_sender
-            .send_event(MithrilEvent::CertificateChainValidationStarted {
-                certificate_chain_validation_id: certificate_chain_validation_id.clone(),
-            })
-            .await;
-
-        let mut current_certificate = certificate.clone().try_into()?;
-        loop {
-            let previous_or_none = self
-                .internal_verifier
-                .verify_certificate(&current_certificate, &self.genesis_verification_key)
-                .await?;
-
-            self.feedback_sender
-                .send_event(MithrilEvent::CertificateValidated {
-                    certificate_hash: current_certificate.hash.clone(),
-                    certificate_chain_validation_id: certificate_chain_validation_id.clone(),
-                })
-                .await;
-
-            match previous_or_none {
-                Some(previous_certificate) => current_certificate = previous_certificate,
-                None => break,
-            }
-        }
-
-        self.feedback_sender
-            .send_event(MithrilEvent::CertificateChainValidated {
-                certificate_chain_validation_id,
-            })
-            .await;
-
-        Ok(())
-    }
-}
-
-#[cfg_attr(target_family = "wasm", async_trait(?Send))]
-#[cfg_attr(not(target_family = "wasm"), async_trait)]
-impl CertificateRetriever for InternalCertificateRetriever {
-    async fn get_certificate_details(
-        &self,
-        certificate_hash: &str,
-    ) -> Result<Certificate, CertificateRetrieverError> {
-        self.get(certificate_hash)
-            .await
-            .map_err(CertificateRetrieverError)?
-            .map(|message| message.try_into())
-            .transpose()
-            .map_err(CertificateRetrieverError)?
-            .ok_or(CertificateRetrieverError(anyhow!(format!(
-                "Certificate does not exist: '{}'",
-                certificate_hash
-            ))))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use mithril_common::crypto_helper::tests_setup::setup_certificate_chain;
-    use mithril_common::test_utils::fake_data;
-    use mockall::predicate::eq;
-
-    use crate::aggregator_client::MockAggregatorHTTPClient;
-    use crate::feedback::StackFeedbackReceiver;
-    use crate::test_utils;
-
-    use super::*;
-
-    fn build_client(
-        aggregator_client: Arc<dyn AggregatorClient>,
-        verifier: Option<Arc<dyn CertificateVerifier>>,
-    ) -> CertificateClient {
-        CertificateClient::new(
-            aggregator_client,
-            verifier.unwrap_or(Arc::new(MockCertificateVerifier::new())),
-            test_utils::test_logger(),
-        )
-    }
-
-    #[tokio::test]
-    async fn get_certificate_list() {
-        let expected = vec![
-            MithrilCertificateListItem {
-                hash: "cert-hash-123".to_string(),
-                ..MithrilCertificateListItem::dummy()
-            },
-            MithrilCertificateListItem {
-                hash: "cert-hash-456".to_string(),
-                ..MithrilCertificateListItem::dummy()
-            },
-        ];
-        let message = expected.clone();
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        aggregator_client
-            .expect_get_content()
-            .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
-        let certificate_client = build_client(Arc::new(aggregator_client), None);
-        let items = certificate_client.list().await.unwrap();
-
-        assert_eq!(expected, items);
-    }
-
-    #[tokio::test]
-    async fn get_certificate_empty_list() {
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        aggregator_client
-            .expect_get_content()
-            .return_once(move |_| {
-                Ok(serde_json::to_string::<Vec<MithrilCertificateListItem>>(&vec![]).unwrap())
-            });
-        let certificate_client = build_client(Arc::new(aggregator_client), None);
-        let items = certificate_client.list().await.unwrap();
-
-        assert!(items.is_empty());
-    }
-
-    #[tokio::test]
-    async fn test_show_ok_some() {
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        let certificate_hash = "cert-hash-123".to_string();
-        let certificate = fake_data::certificate(certificate_hash.clone());
-        let expected_certificate = certificate.clone();
-        aggregator_client
-            .expect_get_content()
-            .return_once(move |_| {
-                let message: CertificateMessage = certificate.try_into().unwrap();
-                Ok(serde_json::to_string(&message).unwrap())
-            })
-            .times(1);
-
-        let certificate_client = build_client(Arc::new(aggregator_client), None);
-        let cert = certificate_client
-            .get("cert-hash-123")
-            .await
-            .unwrap()
-            .expect("The certificate should be found")
-            .try_into()
-            .unwrap();
-
-        assert_eq!(expected_certificate, cert);
-    }
-
-    #[tokio::test]
-    async fn test_show_ok_none() {
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        aggregator_client
-            .expect_get_content()
-            .return_once(move |_| {
-                Err(AggregatorClientError::RemoteServerLogical(anyhow!(
-                    "an error"
-                )))
-            })
-            .times(1);
-
-        let certificate_client = build_client(Arc::new(aggregator_client), None);
-        assert!(certificate_client
-            .get("cert-hash-123")
-            .await
-            .unwrap()
-            .is_none());
-    }
-
-    #[tokio::test]
-    async fn test_show_ko() {
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        aggregator_client
-            .expect_get_content()
-            .return_once(move |_| {
-                Err(AggregatorClientError::RemoteServerTechnical(anyhow!(
-                    "an error"
-                )))
-            })
-            .times(1);
-
-        let certificate_client = build_client(Arc::new(aggregator_client), None);
-        certificate_client
-            .get("cert-hash-123")
-            .await
-            .expect_err("The certificate client should fail here.");
-    }
-
-    #[tokio::test]
-    async fn validating_chain_send_feedbacks() {
-        let (chain, verifier) = setup_certificate_chain(3, 1);
-        let verification_key: String = verifier.to_verification_key().try_into().unwrap();
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        let last_certificate_hash = chain.first().unwrap().hash.clone();
-
-        for certificate in chain.clone() {
-            let hash = certificate.hash.clone();
-            let message = serde_json::to_string(
-                &TryInto::<CertificateMessage>::try_into(certificate).unwrap(),
-            )
-            .unwrap();
-            aggregator_client
-                .expect_get_content()
-                .with(eq(AggregatorRequest::GetCertificate { hash }))
-                .returning(move |_| Ok(message.to_owned()));
-        }
-
-        let aggregator_client = Arc::new(aggregator_client);
-        let feedback_receiver = Arc::new(StackFeedbackReceiver::new());
-        let certificate_client = build_client(
-            aggregator_client.clone(),
-            Some(Arc::new(
-                MithrilCertificateVerifier::new(
-                    aggregator_client,
-                    &verification_key,
-                    FeedbackSender::new(&[feedback_receiver.clone()]),
-                    test_utils::test_logger(),
-                )
-                .unwrap(),
-            )),
-        );
-
-        certificate_client
-            .verify_chain(&last_certificate_hash)
-            .await
-            .expect("Chain validation should succeed");
-
-        let actual = feedback_receiver.stacked_events();
-        let id = actual[0].event_id();
-
-        let expected = {
-            let mut vec = vec![MithrilEvent::CertificateChainValidationStarted {
-                certificate_chain_validation_id: id.to_string(),
-            }];
-            vec.extend(
-                chain
-                    .into_iter()
-                    .map(|c| MithrilEvent::CertificateValidated {
-                        certificate_chain_validation_id: id.to_string(),
-                        certificate_hash: c.hash,
-                    }),
-            );
-            vec.push(MithrilEvent::CertificateChainValidated {
-                certificate_chain_validation_id: id.to_string(),
-            });
-            vec
-        };
-
-        assert_eq!(actual, expected);
-    }
-
-    #[tokio::test]
-    async fn verify_chain_return_certificate_with_given_hash() {
-        let (chain, verifier) = setup_certificate_chain(3, 1);
-        let verification_key: String = verifier.to_verification_key().try_into().unwrap();
-        let mut aggregator_client = MockAggregatorHTTPClient::new();
-        let last_certificate_hash = chain.first().unwrap().hash.clone();
-
-        for certificate in chain.clone() {
-            let hash = certificate.hash.clone();
-            let message = serde_json::to_string(
-                &TryInto::<CertificateMessage>::try_into(certificate).unwrap(),
-            )
-            .unwrap();
-            aggregator_client
-                .expect_get_content()
-                .with(eq(AggregatorRequest::GetCertificate { hash }))
-                .returning(move |_| Ok(message.to_owned()));
-        }
-
-        let aggregator_client = Arc::new(aggregator_client);
-        let certificate_client = build_client(
-            aggregator_client.clone(),
-            Some(Arc::new(
-                MithrilCertificateVerifier::new(
-                    aggregator_client,
-                    &verification_key,
-                    FeedbackSender::new(&[]),
-                    test_utils::test_logger(),
-                )
-                .unwrap(),
-            )),
-        );
-
-        let certificate = certificate_client
-            .verify_chain(&last_certificate_hash)
-            .await
-            .expect("Chain validation should succeed");
-
-        assert_eq!(certificate.hash, last_certificate_hash);
-    }
-}
diff --git a/mithril-client/src/certificate_client/api.rs b/mithril-client/src/certificate_client/api.rs
new file mode 100644
index 00000000000..4e8343d0177
--- /dev/null
+++ b/mithril-client/src/certificate_client/api.rs
@@ -0,0 +1,83 @@
+use async_trait::async_trait;
+use mithril_common::logging::LoggerExtensions;
+use std::sync::Arc;
+
+use crate::aggregator_client::AggregatorClient;
+use crate::certificate_client::fetch::InternalCertificateRetriever;
+use crate::certificate_client::{fetch, verify};
+use crate::{MithrilCertificate, MithrilCertificateListItem, MithrilResult};
+
+/// Aggregator client for the Certificate
+pub struct CertificateClient {
+    pub(super) aggregator_client: Arc<dyn AggregatorClient>,
+    pub(super) retriever: Arc<InternalCertificateRetriever>,
+    pub(super) verifier: Arc<dyn CertificateVerifier>,
+}
+
+impl CertificateClient {
+    /// Constructs a new `CertificateClient`.
+    pub fn new(
+        aggregator_client: Arc<dyn AggregatorClient>,
+        verifier: Arc<dyn CertificateVerifier>,
+        logger: slog::Logger,
+    ) -> Self {
+        let logger = logger.new_with_component_name::<Self>();
+        let retriever = Arc::new(InternalCertificateRetriever::new(
+            aggregator_client.clone(),
+            logger,
+        ));
+
+        Self {
+            aggregator_client,
+            retriever,
+            verifier,
+        }
+    }
+
+    /// Fetch a list of certificates
+    pub async fn list(&self) -> MithrilResult<Vec<MithrilCertificateListItem>> {
+        fetch::list(self).await
+    }
+
+    /// Get a single certificate full information from the aggregator.
+    pub async fn get(&self, certificate_hash: &str) -> MithrilResult<Option<MithrilCertificate>> {
+        fetch::get(self, certificate_hash).await
+    }
+
+    /// Validate the chain starting with the certificate with given `certificate_hash`, return the certificate if
+    /// the chain is valid.
+    ///
+    /// This method will fail if no certificate exists for the given `certificate_hash`.
+    pub async fn verify_chain(&self, certificate_hash: &str) -> MithrilResult<MithrilCertificate> {
+        verify::verify_chain(self, certificate_hash).await
+    }
+}
+
+/// API that defines how to validate certificates.
+#[cfg_attr(test, mockall::automock)]
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
+#[cfg_attr(not(target_family = "wasm"), async_trait)]
+pub trait CertificateVerifier: Sync + Send {
+    /// Validate the chain starting with the given certificate.
+    async fn verify_chain(&self, certificate: &MithrilCertificate) -> MithrilResult<()>;
+}
+
+#[cfg(feature = "unstable")]
+/// API that defines how to cache certificates validation results.
+#[cfg_attr(test, mockall::automock)]
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
+#[cfg_attr(not(target_family = "wasm"), async_trait)]
+pub trait CertificateVerifierCache: Sync + Send {
+    /// Store a validated certificate hash and its parent hash in the cache.
+    async fn store_validated_certificate(
+        &self,
+        certificate_hash: &str,
+        previous_certificate_hash: &str,
+    ) -> MithrilResult<()>;
+
+    /// Get the previous hash of the certificate with the given hash if available in the cache.
+    async fn get_previous_hash(&self, certificate_hash: &str) -> MithrilResult<Option<String>>;
+
+    /// Reset the stored values
+    async fn reset(&self) -> MithrilResult<()>;
+}
diff --git a/mithril-client/src/certificate_client/fetch.rs b/mithril-client/src/certificate_client/fetch.rs
new file mode 100644
index 00000000000..8edd8834b1b
--- /dev/null
+++ b/mithril-client/src/certificate_client/fetch.rs
@@ -0,0 +1,220 @@
+use anyhow::{anyhow, Context};
+use async_trait::async_trait;
+use slog::{crit, Logger};
+use std::sync::Arc;
+
+use mithril_common::certificate_chain::{CertificateRetriever, CertificateRetrieverError};
+use mithril_common::entities::Certificate;
+use mithril_common::messages::CertificateMessage;
+
+use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
+use crate::certificate_client::CertificateClient;
+use crate::{MithrilCertificate, MithrilCertificateListItem, MithrilResult};
+
+#[inline]
+pub(super) async fn list(
+    client: &CertificateClient,
+) -> MithrilResult<Vec<MithrilCertificateListItem>> {
+    let response = client
+        .aggregator_client
+        .get_content(AggregatorRequest::ListCertificates)
+        .await
+        .with_context(|| "CertificateClient can not get the certificate list")?;
+    let items = serde_json::from_str::<Vec<MithrilCertificateListItem>>(&response)
+        .with_context(|| "CertificateClient can not deserialize certificate list")?;
+
+    Ok(items)
+}
+
+#[inline]
+pub(super) async fn get(
+    client: &CertificateClient,
+    certificate_hash: &str,
+) -> MithrilResult<Option<MithrilCertificate>> {
+    client.retriever.get(certificate_hash).await
+}
+
+/// Internal type to implement the [InternalCertificateRetriever] trait and avoid a circular
+/// dependency between the [CertificateClient] and the [CommonMithrilCertificateVerifier] that need
+/// a [CertificateRetriever] as a dependency.
+pub(super) struct InternalCertificateRetriever {
+    aggregator_client: Arc<dyn AggregatorClient>,
+    logger: Logger,
+}
+
+impl InternalCertificateRetriever {
+    pub(super) fn new(
+        aggregator_client: Arc<dyn AggregatorClient>,
+        logger: Logger,
+    ) -> InternalCertificateRetriever {
+        InternalCertificateRetriever {
+            aggregator_client,
+            logger,
+        }
+    }
+
+    pub(super) async fn get(
+        &self,
+        certificate_hash: &str,
+    ) -> MithrilResult<Option<MithrilCertificate>> {
+        let response = self
+            .aggregator_client
+            .get_content(AggregatorRequest::GetCertificate {
+                hash: certificate_hash.to_string(),
+            })
+            .await;
+
+        match response {
+            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
+            Err(e) => Err(e.into()),
+            Ok(response) => {
+                let message =
+                    serde_json::from_str::<CertificateMessage>(&response).inspect_err(|e| {
+                        crit!(
+                            self.logger, "Could not create certificate from API message";
+                            "error" => e.to_string(),
+                            "raw_message" => response
+                        );
+                    })?;
+
+                Ok(Some(message))
+            }
+        }
+    }
+}
+
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
+#[cfg_attr(not(target_family = "wasm"), async_trait)]
+impl CertificateRetriever for InternalCertificateRetriever {
+    async fn get_certificate_details(
+        &self,
+        certificate_hash: &str,
+    ) -> Result<Certificate, CertificateRetrieverError> {
+        self.get(certificate_hash)
+            .await
+            .map_err(CertificateRetrieverError)?
+            .map(|message| message.try_into())
+            .transpose()
+            .map_err(CertificateRetrieverError)?
+            .ok_or(CertificateRetrieverError(anyhow!(format!(
+                "Certificate does not exist: '{}'",
+                certificate_hash
+            ))))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use mithril_common::test_utils::fake_data;
+
+    use crate::certificate_client::tests_utils::CertificateClientTestBuilder;
+
+    use super::*;
+
+    #[tokio::test]
+    async fn get_certificate_list() {
+        let expected = vec![
+            MithrilCertificateListItem {
+                hash: "cert-hash-123".to_string(),
+                ..MithrilCertificateListItem::dummy()
+            },
+            MithrilCertificateListItem {
+                hash: "cert-hash-456".to_string(),
+                ..MithrilCertificateListItem::dummy()
+            },
+        ];
+        let message = expected.clone();
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| {
+                mock.expect_get_content()
+                    .return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
+            })
+            .build();
+        let items = certificate_client.list().await.unwrap();
+
+        assert_eq!(expected, items);
+    }
+
+    #[tokio::test]
+    async fn get_certificate_empty_list() {
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| {
+                mock.expect_get_content().return_once(move |_| {
+                    Ok(serde_json::to_string::<Vec<MithrilCertificateListItem>>(&vec![]).unwrap())
+                });
+            })
+            .build();
+        let items = certificate_client.list().await.unwrap();
+
+        assert!(items.is_empty());
+    }
+
+    #[tokio::test]
+    async fn test_show_ok_some() {
+        let certificate_hash = "cert-hash-123".to_string();
+        let certificate = fake_data::certificate(certificate_hash.clone());
+        let expected_certificate = certificate.clone();
+
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| {
+                mock.expect_get_content()
+                    .return_once(move |_| {
+                        let message: CertificateMessage = certificate.try_into().unwrap();
+                        Ok(serde_json::to_string(&message).unwrap())
+                    })
+                    .times(1);
+            })
+            .build();
+
+        let cert = certificate_client
+            .get("cert-hash-123")
+            .await
+            .unwrap()
+            .expect("The certificate should be found")
+            .try_into()
+            .unwrap();
+
+        assert_eq!(expected_certificate, cert);
+    }
+
+    #[tokio::test]
+    async fn test_show_ok_none() {
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| {
+                mock.expect_get_content()
+                    .return_once(move |_| {
+                        Err(AggregatorClientError::RemoteServerLogical(anyhow!(
+                            "an error"
+                        )))
+                    })
+                    .times(1);
+            })
+            .build();
+
+        assert!(certificate_client
+            .get("cert-hash-123")
+            .await
+            .unwrap()
+            .is_none());
+    }
+
+    #[tokio::test]
+    async fn test_show_ko() {
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| {
+                mock.expect_get_content()
+                    .return_once(move |_| {
+                        Err(AggregatorClientError::RemoteServerTechnical(anyhow!(
+                            "an error"
+                        )))
+                    })
+                    .times(1);
+            })
+            .build();
+
+        certificate_client
+            .get("cert-hash-123")
+            .await
+            .expect_err("The certificate client should fail here.");
+    }
+}
diff --git a/mithril-client/src/certificate_client/mod.rs b/mithril-client/src/certificate_client/mod.rs
new file mode 100644
index 00000000000..d7f6dbd461f
--- /dev/null
+++ b/mithril-client/src/certificate_client/mod.rs
@@ -0,0 +1,169 @@
+//! A client which retrieves and validates certificates from an Aggregator.
+//!
+//! In order to do so it defines a [CertificateClient] exposes the following features:
+//!  - [get][CertificateClient::get]: get a certificate data from its hash
+//!  - [list][CertificateClient::list]: get the list of available certificates
+//!  - [verify_chain][CertificateClient::verify_chain]: verify a certificate chain
+//!
+//! # Get a certificate
+//!
+//! To get a certificate using the [ClientBuilder][crate::client::ClientBuilder].
+//!
+//! ```no_run
+//! # async fn run() -> mithril_client::MithrilResult<()> {
+//! use mithril_client::ClientBuilder;
+//!
+//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
+//! let certificate = client.certificate().get("CERTIFICATE_HASH").await?.unwrap();
+//!
+//! println!("Certificate hash={}, signed_message={}", certificate.hash, certificate.signed_message);
+//! #    Ok(())
+//! # }
+//! ```
+//!
+//! # List available certificates
+//!
+//! To list available certificates using the [ClientBuilder][crate::client::ClientBuilder].
+//!
+//! ```no_run
+//! # async fn run() -> mithril_client::MithrilResult<()> {
+//! use mithril_client::ClientBuilder;
+//!
+//! let client = mithril_client::ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
+//! let certificates = client.certificate().list().await?;
+//!
+//! for certificate in certificates {
+//!     println!("Certificate hash={}, signed_message={}", certificate.hash, certificate.signed_message);
+//! }
+//! #    Ok(())
+//! # }
+//! ```
+//!
+//! # Validate a certificate chain
+//!
+//! To validate a certificate using the [ClientBuilder][crate::client::ClientBuilder].
+//!
+//! ```no_run
+//! # async fn run() -> mithril_client::MithrilResult<()> {
+//! use mithril_client::ClientBuilder;
+//!
+//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
+//! let certificate = client.certificate().verify_chain("CERTIFICATE_HASH").await?;
+//!
+//! println!("Chain of Certificate (hash: {}) is valid", certificate.hash);
+//! #    Ok(())
+//! # }
+//! ```
+
+mod api;
+mod fetch;
+mod verify;
+#[cfg(feature = "unstable")]
+mod verify_cache;
+
+pub use api::*;
+pub use verify::MithrilCertificateVerifier;
+#[cfg(feature = "unstable")]
+pub use verify_cache::MemoryCertificateVerifierCache;
+
+#[cfg(test)]
+pub(crate) mod tests_utils {
+    use mithril_common::crypto_helper::ProtocolGenesisVerificationKey;
+    use mithril_common::entities::Certificate;
+    use mithril_common::messages::CertificateMessage;
+    use mockall::predicate::eq;
+    use std::sync::Arc;
+
+    use crate::aggregator_client::{AggregatorRequest, MockAggregatorHTTPClient};
+    use crate::feedback::{FeedbackReceiver, FeedbackSender};
+    use crate::test_utils;
+
+    use super::*;
+
+    #[derive(Default)]
+    pub(crate) struct CertificateClientTestBuilder {
+        aggregator_client: MockAggregatorHTTPClient,
+        genesis_verification_key: Option<String>,
+        feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
+        #[cfg(feature = "unstable")]
+        verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
+    }
+
+    impl CertificateClientTestBuilder {
+        pub fn config_aggregator_client_mock(
+            mut self,
+            config: impl FnOnce(&mut MockAggregatorHTTPClient),
+        ) -> Self {
+            config(&mut self.aggregator_client);
+            self
+        }
+
+        pub fn with_genesis_verification_key(
+            mut self,
+            genesis_verification_key: ProtocolGenesisVerificationKey,
+        ) -> Self {
+            self.genesis_verification_key = Some(genesis_verification_key.try_into().unwrap());
+            self
+        }
+
+        pub fn add_feedback_receiver(
+            mut self,
+            feedback_receiver: Arc<dyn FeedbackReceiver>,
+        ) -> Self {
+            self.feedback_receivers.push(feedback_receiver);
+            self
+        }
+
+        #[cfg(feature = "unstable")]
+        pub fn with_verifier_cache(
+            mut self,
+            verifier_cache: Arc<dyn CertificateVerifierCache>,
+        ) -> Self {
+            self.verifier_cache = Some(verifier_cache);
+            self
+        }
+
+        /// Builds a new [CertificateClient] with the given configuration.
+        ///
+        /// If no genesis verification key is provided, a [MockCertificateVerifier] will be used,
+        /// else a [MithrilCertificateVerifier] will be used.
+        pub fn build(self) -> CertificateClient {
+            let logger = test_utils::test_logger();
+            let aggregator_client = Arc::new(self.aggregator_client);
+
+            let certificate_verifier: Arc<dyn CertificateVerifier> =
+                match self.genesis_verification_key {
+                    None => Arc::new(MockCertificateVerifier::new()),
+                    Some(genesis_verification_key) => Arc::new(
+                        MithrilCertificateVerifier::new(
+                            aggregator_client.clone(),
+                            &genesis_verification_key,
+                            FeedbackSender::new(&self.feedback_receivers),
+                            #[cfg(feature = "unstable")]
+                            self.verifier_cache,
+                            logger.clone(),
+                        )
+                        .unwrap(),
+                    ),
+                };
+
+            CertificateClient::new(aggregator_client.clone(), certificate_verifier, logger)
+        }
+    }
+
+    impl MockAggregatorHTTPClient {
+        pub(crate) fn expect_certificate_chain(&mut self, certificate_chain: Vec<Certificate>) {
+            for certificate in certificate_chain {
+                let hash = certificate.hash.clone();
+                let message = serde_json::to_string(
+                    &TryInto::<CertificateMessage>::try_into(certificate).unwrap(),
+                )
+                .unwrap();
+                self.expect_get_content()
+                    .with(eq(AggregatorRequest::GetCertificate { hash }))
+                    .once()
+                    .returning(move |_| Ok(message.to_owned()));
+            }
+        }
+    }
+}
diff --git a/mithril-client/src/certificate_client/verify.rs b/mithril-client/src/certificate_client/verify.rs
new file mode 100644
index 00000000000..ffaf66652b1
--- /dev/null
+++ b/mithril-client/src/certificate_client/verify.rs
@@ -0,0 +1,544 @@
+use anyhow::{anyhow, Context};
+use async_trait::async_trait;
+use slog::{trace, Logger};
+use std::sync::Arc;
+
+use mithril_common::{
+    certificate_chain::{
+        CertificateRetriever, CertificateVerifier as CommonCertificateVerifier,
+        MithrilCertificateVerifier as CommonMithrilCertificateVerifier,
+    },
+    crypto_helper::ProtocolGenesisVerificationKey,
+    entities::Certificate,
+    logging::LoggerExtensions,
+};
+
+use crate::aggregator_client::AggregatorClient;
+use crate::certificate_client::fetch::InternalCertificateRetriever;
+#[cfg(feature = "unstable")]
+use crate::certificate_client::CertificateVerifierCache;
+use crate::certificate_client::{CertificateClient, CertificateVerifier};
+use crate::feedback::{FeedbackSender, MithrilEvent};
+use crate::{MithrilCertificate, MithrilResult};
+
+#[inline]
+pub(super) async fn verify_chain(
+    client: &CertificateClient,
+    certificate_hash: &str,
+) -> MithrilResult<MithrilCertificate> {
+    let certificate = client
+        .retriever
+        .get(certificate_hash)
+        .await?
+        .ok_or(anyhow!(
+            "No certificate exist for hash '{certificate_hash}'"
+        ))?;
+
+    client
+        .verifier
+        .verify_chain(&certificate)
+        .await
+        .with_context(|| {
+            format!("Certificate chain of certificate '{certificate_hash}' is invalid")
+        })?;
+
+    Ok(certificate)
+}
+
+/// Implementation of a [CertificateVerifier] that can send feedbacks using
+/// the [feedback][crate::feedback] mechanism.
+pub struct MithrilCertificateVerifier {
+    retriever: Arc<InternalCertificateRetriever>,
+    internal_verifier: Arc<dyn CommonCertificateVerifier>,
+    genesis_verification_key: ProtocolGenesisVerificationKey,
+    feedback_sender: FeedbackSender,
+    #[cfg(feature = "unstable")]
+    verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
+    logger: Logger,
+}
+
+impl MithrilCertificateVerifier {
+    /// Constructs a new `MithrilCertificateVerifier`.
+    pub fn new(
+        aggregator_client: Arc<dyn AggregatorClient>,
+        genesis_verification_key: &str,
+        feedback_sender: FeedbackSender,
+        #[cfg(feature = "unstable")] verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
+        logger: Logger,
+    ) -> MithrilResult<MithrilCertificateVerifier> {
+        let logger = logger.new_with_component_name::<Self>();
+        let retriever = Arc::new(InternalCertificateRetriever::new(
+            aggregator_client,
+            logger.clone(),
+        ));
+        let internal_verifier = Arc::new(CommonMithrilCertificateVerifier::new(
+            logger.clone(),
+            retriever.clone(),
+        ));
+        let genesis_verification_key =
+            ProtocolGenesisVerificationKey::try_from(genesis_verification_key)
+                .with_context(|| "Invalid genesis verification key")?;
+
+        Ok(Self {
+            retriever,
+            internal_verifier,
+            genesis_verification_key,
+            feedback_sender,
+            #[cfg(feature = "unstable")]
+            verifier_cache,
+            logger,
+        })
+    }
+
+    #[cfg(feature = "unstable")]
+    async fn fetch_cached_previous_hash(&self, hash: &str) -> MithrilResult<Option<String>> {
+        if let Some(cache) = self.verifier_cache.as_ref() {
+            Ok(cache.get_previous_hash(hash).await?)
+        } else {
+            Ok(None)
+        }
+    }
+
+    #[cfg(not(feature = "unstable"))]
+    async fn fetch_cached_previous_hash(&self, _hash: &str) -> MithrilResult<Option<String>> {
+        Ok(None)
+    }
+
+    async fn verify_with_cache_enabled(
+        &self,
+        certificate_chain_validation_id: &str,
+        certificate: CertificateToVerify,
+    ) -> MithrilResult<Option<CertificateToVerify>> {
+        trace!(self.logger, "Validating certificate"; "hash" => certificate.hash(), "previous_hash" => certificate.hash());
+        if let Some(previous_hash) = self.fetch_cached_previous_hash(certificate.hash()).await? {
+            trace!(self.logger, "Certificate fetched from cache"; "hash" => certificate.hash(), "previous_hash" => &previous_hash);
+            self.feedback_sender
+                .send_event(MithrilEvent::CertificateFetchedFromCache {
+                    certificate_hash: certificate.hash().to_owned(),
+                    certificate_chain_validation_id: certificate_chain_validation_id.to_string(),
+                })
+                .await;
+
+            Ok(Some(CertificateToVerify::ToDownload {
+                hash: previous_hash,
+            }))
+        } else {
+            let certificate = match certificate {
+                CertificateToVerify::Downloaded { certificate } => certificate,
+                CertificateToVerify::ToDownload { hash } => {
+                    self.retriever.get_certificate_details(&hash).await?
+                }
+            };
+
+            let previous_certificate = self
+                .verify_without_cache(certificate_chain_validation_id, certificate)
+                .await?;
+            Ok(previous_certificate.map(Into::into))
+        }
+    }
+
+    async fn verify_without_cache(
+        &self,
+        certificate_chain_validation_id: &str,
+        certificate: Certificate,
+    ) -> MithrilResult<Option<Certificate>> {
+        let previous_certificate = self
+            .internal_verifier
+            .verify_certificate(&certificate, &self.genesis_verification_key)
+            .await?;
+
+        #[cfg(feature = "unstable")]
+        if let Some(cache) = self.verifier_cache.as_ref() {
+            if !certificate.is_genesis() {
+                cache
+                    .store_validated_certificate(&certificate.hash, &certificate.previous_hash)
+                    .await?;
+            }
+        }
+
+        trace!(self.logger, "Certificate validated"; "hash" => &certificate.hash, "previous_hash" => &certificate.previous_hash);
+        self.feedback_sender
+            .send_event(MithrilEvent::CertificateValidated {
+                certificate_hash: certificate.hash,
+                certificate_chain_validation_id: certificate_chain_validation_id.to_string(),
+            })
+            .await;
+
+        Ok(previous_certificate)
+    }
+}
+
+enum CertificateToVerify {
+    /// The certificate is already downloaded.
+    Downloaded { certificate: Certificate },
+    /// The certificate is not downloaded yet (since its parent was cached).
+    ToDownload { hash: String },
+}
+
+impl CertificateToVerify {
+    fn hash(&self) -> &str {
+        match self {
+            CertificateToVerify::Downloaded { certificate } => &certificate.hash,
+            CertificateToVerify::ToDownload { hash } => hash,
+        }
+    }
+}
+
+impl From<Certificate> for CertificateToVerify {
+    fn from(value: Certificate) -> Self {
+        Self::Downloaded { certificate: value }
+    }
+}
+
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
+#[cfg_attr(not(target_family = "wasm"), async_trait)]
+impl CertificateVerifier for MithrilCertificateVerifier {
+    async fn verify_chain(&self, certificate: &MithrilCertificate) -> MithrilResult<()> {
+        // Todo: move most of this code in the `mithril_common` verifier by defining
+        // a new `verify_chain` method that take a callback called when a certificate is
+        // validated.
+        let certificate_chain_validation_id = MithrilEvent::new_certificate_chain_validation_id();
+        self.feedback_sender
+            .send_event(MithrilEvent::CertificateChainValidationStarted {
+                certificate_chain_validation_id: certificate_chain_validation_id.clone(),
+            })
+            .await;
+
+        // Validate certificates without cache until we cross an epoch boundary
+        // This is necessary to ensure that the AVK chaining is correct
+        let start_epoch = certificate.epoch;
+        let mut current_certificate: Option<Certificate> = Some(certificate.clone().try_into()?);
+        loop {
+            match current_certificate {
+                None => break,
+                Some(next) => {
+                    current_certificate = self
+                        .verify_without_cache(&certificate_chain_validation_id, next)
+                        .await?;
+
+                    let has_crossed_epoch_boundary = current_certificate
+                        .as_ref()
+                        .is_some_and(|c| c.epoch != start_epoch);
+                    if has_crossed_epoch_boundary {
+                        break;
+                    }
+                }
+            }
+        }
+
+        let mut current_certificate: Option<CertificateToVerify> =
+            current_certificate.map(Into::into);
+        loop {
+            match current_certificate {
+                None => break,
+                Some(next) => {
+                    current_certificate = self
+                        .verify_with_cache_enabled(&certificate_chain_validation_id, next)
+                        .await?
+                }
+            }
+        }
+
+        self.feedback_sender
+            .send_event(MithrilEvent::CertificateChainValidated {
+                certificate_chain_validation_id,
+            })
+            .await;
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use mithril_common::test_utils::CertificateChainBuilder;
+
+    use crate::certificate_client::tests_utils::CertificateClientTestBuilder;
+    use crate::feedback::StackFeedbackReceiver;
+
+    use super::*;
+
+    #[tokio::test]
+    async fn validating_chain_send_feedbacks() {
+        let (chain, verifier) = CertificateChainBuilder::new()
+            .with_total_certificates(3)
+            .with_certificates_per_epoch(1)
+            .build();
+        let last_certificate_hash = chain.first().unwrap().hash.clone();
+
+        let feedback_receiver = Arc::new(StackFeedbackReceiver::new());
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| mock.expect_certificate_chain(chain.clone()))
+            .with_genesis_verification_key(verifier.to_verification_key())
+            .add_feedback_receiver(feedback_receiver.clone())
+            .build();
+
+        certificate_client
+            .verify_chain(&last_certificate_hash)
+            .await
+            .expect("Chain validation should succeed");
+
+        let actual = feedback_receiver.stacked_events();
+        let id = actual[0].event_id();
+
+        let expected = {
+            let mut vec = vec![MithrilEvent::CertificateChainValidationStarted {
+                certificate_chain_validation_id: id.to_string(),
+            }];
+            vec.extend(
+                chain
+                    .into_iter()
+                    .map(|c| MithrilEvent::CertificateValidated {
+                        certificate_chain_validation_id: id.to_string(),
+                        certificate_hash: c.hash,
+                    }),
+            );
+            vec.push(MithrilEvent::CertificateChainValidated {
+                certificate_chain_validation_id: id.to_string(),
+            });
+            vec
+        };
+
+        assert_eq!(actual, expected);
+    }
+
+    #[tokio::test]
+    async fn verify_chain_return_certificate_with_given_hash() {
+        let (chain, verifier) = CertificateChainBuilder::new()
+            .with_total_certificates(3)
+            .with_certificates_per_epoch(1)
+            .build();
+        let last_certificate_hash = chain.first().unwrap().hash.clone();
+
+        let certificate_client = CertificateClientTestBuilder::default()
+            .config_aggregator_client_mock(|mock| mock.expect_certificate_chain(chain.clone()))
+            .with_genesis_verification_key(verifier.to_verification_key())
+            .build();
+
+        let certificate = certificate_client
+            .verify_chain(&last_certificate_hash)
+            .await
+            .expect("Chain validation should succeed");
+
+        assert_eq!(certificate.hash, last_certificate_hash);
+    }
+
+    #[cfg(feature = "unstable")]
+    mod cache {
+        use chrono::TimeDelta;
+        use mithril_common::test_utils::CertificateChainingMethod;
+        use mockall::predicate::eq;
+
+        use crate::aggregator_client::MockAggregatorHTTPClient;
+        use crate::certificate_client::verify_cache::MemoryCertificateVerifierCache;
+        use crate::certificate_client::MockCertificateVerifierCache;
+        use crate::test_utils;
+
+        use super::*;
+
+        fn build_verifier_with_cache(
+            aggregator_client_mock_config: impl FnOnce(&mut MockAggregatorHTTPClient),
+            genesis_verification_key: ProtocolGenesisVerificationKey,
+            cache: Arc<dyn CertificateVerifierCache>,
+        ) -> MithrilCertificateVerifier {
+            let mut aggregator_client = MockAggregatorHTTPClient::new();
+            aggregator_client_mock_config(&mut aggregator_client);
+            let genesis_verification_key: String = genesis_verification_key.try_into().unwrap();
+
+            MithrilCertificateVerifier::new(
+                Arc::new(aggregator_client),
+                &genesis_verification_key,
+                FeedbackSender::new(&[]),
+                Some(cache),
+                test_utils::test_logger(),
+            )
+            .unwrap()
+        }
+
+        #[tokio::test]
+        async fn genesis_certificates_verification_result_is_not_cached() {
+            let (chain, verifier) = CertificateChainBuilder::new()
+                .with_total_certificates(1)
+                .with_certificates_per_epoch(1)
+                .build();
+            let genesis_certificate = chain.last().unwrap();
+            assert!(genesis_certificate.is_genesis());
+
+            let cache = Arc::new(MemoryCertificateVerifierCache::new(TimeDelta::hours(1)));
+            let verifier = build_verifier_with_cache(
+                |_mock| {},
+                verifier.to_verification_key(),
+                cache.clone(),
+            );
+
+            verifier
+                .verify_with_cache_enabled(
+                    "certificate_chain_validation_id",
+                    CertificateToVerify::Downloaded {
+                        certificate: genesis_certificate.clone(),
+                    },
+                )
+                .await
+                .unwrap();
+
+            assert_eq!(
+                cache
+                    .get_previous_hash(&genesis_certificate.hash)
+                    .await
+                    .unwrap(),
+                None
+            );
+        }
+
+        #[tokio::test]
+        async fn non_genesis_certificates_verification_result_is_cached() {
+            let (chain, verifier) = CertificateChainBuilder::new()
+                .with_total_certificates(2)
+                .with_certificates_per_epoch(1)
+                .build();
+            let certificate = chain.first().unwrap();
+            let genesis_certificate = chain.last().unwrap();
+            assert!(!certificate.is_genesis());
+
+            let cache = Arc::new(MemoryCertificateVerifierCache::new(TimeDelta::hours(1)));
+            let verifier = build_verifier_with_cache(
+                |mock| mock.expect_certificate_chain(vec![genesis_certificate.clone()]),
+                verifier.to_verification_key(),
+                cache.clone(),
+            );
+
+            verifier
+                .verify_with_cache_enabled(
+                    "certificate_chain_validation_id",
+                    CertificateToVerify::Downloaded {
+                        certificate: certificate.clone(),
+                    },
+                )
+                .await
+                .unwrap();
+
+            assert_eq!(
+                cache.get_previous_hash(&certificate.hash).await.unwrap(),
+                Some(certificate.previous_hash.clone())
+            );
+        }
+
+        #[tokio::test]
+        async fn verification_of_first_certificate_of_a_chain_should_always_fetch_it_from_network()
+        {
+            let (chain, verifier) = CertificateChainBuilder::new()
+                .with_total_certificates(2)
+                .with_certificates_per_epoch(1)
+                .build();
+            let first_certificate = chain.first().unwrap();
+
+            let cache = Arc::new(
+                MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
+                    .with_items_from_chain(&vec![first_certificate.clone()]),
+            );
+            let certificate_client = CertificateClientTestBuilder::default()
+                .config_aggregator_client_mock(|mock| {
+                    // Expect to first certificate to be fetched from the network
+                    mock.expect_certificate_chain(chain.clone());
+                })
+                .with_genesis_verification_key(verifier.to_verification_key())
+                .with_verifier_cache(cache.clone())
+                .build();
+
+            certificate_client
+                .verify_chain(&first_certificate.hash)
+                .await
+                .unwrap();
+        }
+
+        #[tokio::test]
+        async fn verification_of_certificates_should_not_use_cache_until_crossing_an_epoch_boundary(
+        ) {
+            // Scenario:
+            // | Certificate | epoch |         Parent | Can use cache to | Should be fully |
+            // |             |       |                | get parent hash  | Verified        |
+            // |------------:|------:|---------------:|------------------|-----------------|
+            // |         n°6 |     3 |            n°5 | No               | Yes             |
+            // |         n°5 |     3 |            n°4 | No               | Yes             |
+            // |         n°4 |     2 |            n°3 | Yes              | Yes             |
+            // |         n°3 |     2 |            n°2 | Yes              | No              |
+            // |         n°2 |     2 |            n°1 | Yes              | No              |
+            // |         n°1 |     1 | None (genesis) | Yes              | Yes             |
+            let (chain, verifier) = CertificateChainBuilder::new()
+                .with_total_certificates(6)
+                .with_certificates_per_epoch(3)
+                .with_certificate_chaining_method(CertificateChainingMethod::Sequential)
+                .build();
+
+            let first_certificate = chain.first().unwrap();
+            let genesis_certificate = chain.last().unwrap();
+            assert!(genesis_certificate.is_genesis());
+
+            let certificates_that_must_be_fully_verified =
+                [chain[..3].to_vec(), vec![genesis_certificate.clone()]].concat();
+            let certificates_which_parents_can_be_fetched_from_cache = chain[2..5].to_vec();
+
+            let cache = {
+                let mut mock = MockCertificateVerifierCache::new();
+
+                for certificate in certificates_which_parents_can_be_fetched_from_cache {
+                    let previous_hash = certificate.previous_hash.clone();
+                    mock.expect_get_previous_hash()
+                        .with(eq(certificate.hash.clone()))
+                        .return_once(|_| Ok(Some(previous_hash)))
+                        .once();
+                }
+                mock.expect_get_previous_hash()
+                    .with(eq(genesis_certificate.hash.clone()))
+                    .returning(|_| Ok(None));
+                mock.expect_store_validated_certificate()
+                    .returning(|_, _| Ok(()));
+
+                Arc::new(mock)
+            };
+
+            let certificate_client = CertificateClientTestBuilder::default()
+                .config_aggregator_client_mock(|mock| {
+                    mock.expect_certificate_chain(certificates_that_must_be_fully_verified);
+                })
+                .with_genesis_verification_key(verifier.to_verification_key())
+                .with_verifier_cache(cache)
+                .build();
+
+            certificate_client
+                .verify_chain(&first_certificate.hash)
+                .await
+                .unwrap();
+        }
+
+        #[tokio::test]
+        async fn verify_chain_return_certificate_with_cache() {
+            let (chain, verifier) = CertificateChainBuilder::new()
+                .with_total_certificates(5)
+                .with_certificates_per_epoch(1)
+                .build();
+            let last_certificate_hash = chain.first().unwrap().hash.clone();
+
+            // All certificates are cached except the last two (to cross an epoch boundary) and the genesis
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
+                .with_items_from_chain(&chain[2..4]);
+
+            let certificate_client = CertificateClientTestBuilder::default()
+                .config_aggregator_client_mock(|mock| {
+                    mock.expect_certificate_chain(
+                        [chain[0..3].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
+                    )
+                })
+                .with_genesis_verification_key(verifier.to_verification_key())
+                .with_verifier_cache(Arc::new(cache))
+                .build();
+
+            let certificate = certificate_client
+                .verify_chain(&last_certificate_hash)
+                .await
+                .unwrap();
+
+            assert_eq!(certificate.hash, last_certificate_hash);
+        }
+    }
+}
diff --git a/mithril-client/src/certificate_client/verify_cache/memory_cache.rs b/mithril-client/src/certificate_client/verify_cache/memory_cache.rs
new file mode 100644
index 00000000000..2db890fbdaf
--- /dev/null
+++ b/mithril-client/src/certificate_client/verify_cache/memory_cache.rs
@@ -0,0 +1,337 @@
+use async_trait::async_trait;
+use chrono::{DateTime, TimeDelta, Utc};
+use std::collections::HashMap;
+use std::ops::Add;
+use tokio::sync::RwLock;
+
+use crate::certificate_client::CertificateVerifierCache;
+use crate::MithrilResult;
+
+pub type CertificateHash = str;
+pub type PreviousCertificateHash = str;
+
+/// A in-memory cache for the certificate verifier.
+pub struct MemoryCertificateVerifierCache {
+    expiration_delay: TimeDelta,
+    cache: RwLock<HashMap<String, CachedCertificate>>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+struct CachedCertificate {
+    previous_hash: String,
+    expire_at: DateTime<Utc>,
+}
+
+impl CachedCertificate {
+    fn new<TPreviousHash: Into<String>>(
+        previous_hash: TPreviousHash,
+        expire_at: DateTime<Utc>,
+    ) -> Self {
+        CachedCertificate {
+            previous_hash: previous_hash.into(),
+            expire_at,
+        }
+    }
+}
+
+impl MemoryCertificateVerifierCache {
+    /// `MemoryCertificateVerifierCache` factory
+    pub fn new(expiration_delay: TimeDelta) -> Self {
+        MemoryCertificateVerifierCache {
+            expiration_delay,
+            cache: RwLock::new(HashMap::new()),
+        }
+    }
+
+    /// Get the number of elements in the cache
+    pub async fn len(&self) -> usize {
+        self.cache.read().await.len()
+    }
+
+    /// Return true if the cache is empty
+    pub async fn is_empty(&self) -> bool {
+        self.cache.read().await.is_empty()
+    }
+}
+
+#[cfg_attr(target_family = "wasm", async_trait(?Send))]
+#[cfg_attr(not(target_family = "wasm"), async_trait)]
+impl CertificateVerifierCache for MemoryCertificateVerifierCache {
+    async fn store_validated_certificate(
+        &self,
+        certificate_hash: &CertificateHash,
+        previous_certificate_hash: &PreviousCertificateHash,
+    ) -> MithrilResult<()> {
+        // todo: should we raise an error if an empty string is given for previous_certificate_hash ? (or any other kind of validation)
+        let mut cache = self.cache.write().await;
+        cache.insert(
+            certificate_hash.to_string(),
+            CachedCertificate::new(
+                previous_certificate_hash,
+                Utc::now().add(self.expiration_delay),
+            ),
+        );
+        Ok(())
+    }
+
+    async fn get_previous_hash(
+        &self,
+        certificate_hash: &CertificateHash,
+    ) -> MithrilResult<Option<String>> {
+        let cache = self.cache.read().await;
+        Ok(cache
+            .get(certificate_hash)
+            .filter(|cached| cached.expire_at >= Utc::now())
+            .map(|cached| cached.previous_hash.clone()))
+    }
+
+    async fn reset(&self) -> MithrilResult<()> {
+        let mut cache = self.cache.write().await;
+        cache.clear();
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+pub(crate) mod test_tools {
+    use mithril_common::entities::Certificate;
+
+    use super::*;
+
+    impl MemoryCertificateVerifierCache {
+        /// `Test only` Populate the cache with the given hash and previous hash
+        pub(crate) fn with_items<'a, T>(mut self, key_values: T) -> Self
+        where
+            T: IntoIterator<Item = (&'a CertificateHash, &'a PreviousCertificateHash)>,
+        {
+            let expire_at = Utc::now() + self.expiration_delay;
+            self.cache = RwLock::new(
+                key_values
+                    .into_iter()
+                    .map(|(k, v)| (k.to_string(), CachedCertificate::new(v, expire_at)))
+                    .collect(),
+            );
+            self
+        }
+
+        /// `Test only` Populate the cache with the given hash and previous hash from given certificates
+        pub(crate) fn with_items_from_chain<'a, T>(self, chain: T) -> Self
+        where
+            T: IntoIterator<Item = &'a Certificate>,
+        {
+            self.with_items(
+                chain
+                    .into_iter()
+                    .map(|cert| (cert.hash.as_str(), cert.previous_hash.as_str())),
+            )
+        }
+
+        /// `Test only` Return the content of the cache (without the expiration date)
+        pub(crate) async fn content(&self) -> HashMap<String, String> {
+            self.cache
+                .read()
+                .await
+                .iter()
+                .map(|(hash, cached)| (hash.clone(), cached.previous_hash.clone()))
+                .collect()
+        }
+
+        /// `Test only` Overwrite the expiration date of an entry the given certificate hash.
+        ///
+        /// panic if the key is not found
+        pub(crate) async fn overwrite_expiration_date(
+            &self,
+            certificate_hash: &CertificateHash,
+            expire_at: DateTime<Utc>,
+        ) {
+            let mut cache = self.cache.write().await;
+            cache
+                .get_mut(certificate_hash)
+                .expect("Key not found")
+                .expire_at = expire_at;
+        }
+
+        /// `Test only` Get the cached value for the given certificate hash
+        pub(super) async fn get_cached_value(
+            &self,
+            certificate_hash: &CertificateHash,
+        ) -> Option<CachedCertificate> {
+            self.cache.read().await.get(certificate_hash).cloned()
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use mithril_common::entities::Certificate;
+    use mithril_common::test_utils::fake_data;
+
+    use super::*;
+
+    #[tokio::test]
+    async fn from_str_iterator() {
+        let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1))
+            .with_items([("first", "one"), ("second", "two")]);
+
+        assert_eq!(
+            HashMap::from_iter([
+                ("first".to_string(), "one".to_string()),
+                ("second".to_string(), "two".to_string())
+            ]),
+            cache.content().await
+        );
+    }
+
+    #[tokio::test]
+    async fn from_certificate_iterator() {
+        let chain = vec![
+            Certificate {
+                previous_hash: "first_parent".to_string(),
+                ..fake_data::certificate("first")
+            },
+            Certificate {
+                previous_hash: "second_parent".to_string(),
+                ..fake_data::certificate("second")
+            },
+        ];
+        let cache =
+            MemoryCertificateVerifierCache::new(TimeDelta::hours(1)).with_items_from_chain(&chain);
+
+        assert_eq!(
+            HashMap::from_iter([
+                ("first".to_string(), "first_parent".to_string()),
+                ("second".to_string(), "second_parent".to_string())
+            ]),
+            cache.content().await
+        );
+    }
+
+    mod store_validated_certificate {
+        use super::*;
+
+        #[tokio::test]
+        async fn store_in_empty_cache_add_new_item_that_expire_after_parametrized_delay() {
+            let expiration_delay = TimeDelta::hours(1);
+            let start_time = Utc::now();
+            let cache = MemoryCertificateVerifierCache::new(expiration_delay);
+            cache
+                .store_validated_certificate("hash", "parent")
+                .await
+                .unwrap();
+
+            let cached = cache
+                .get_cached_value("hash")
+                .await
+                .expect("Cache should have been populated");
+
+            assert_eq!(1, cache.len().await);
+            assert_eq!("parent", cached.previous_hash);
+            assert!(cached.expire_at - start_time >= expiration_delay);
+        }
+
+        #[tokio::test]
+        async fn store_new_hash_push_new_key_at_end_and_dont_alter_existing_values() {
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1)).with_items([
+                ("existing_hash", "existing_parent"),
+                ("another_hash", "another_parent"),
+            ]);
+            cache
+                .store_validated_certificate("new_hash", "new_parent")
+                .await
+                .unwrap();
+
+            assert_eq!(
+                HashMap::from_iter([
+                    ("existing_hash".to_string(), "existing_parent".to_string()),
+                    ("another_hash".to_string(), "another_parent".to_string()),
+                    ("new_hash".to_string(), "new_parent".to_string()),
+                ]),
+                cache.content().await
+            );
+        }
+
+        #[tokio::test]
+        async fn storing_same_hash_update_parent_hash_and_expiration_time() {
+            let expiration_delay = TimeDelta::days(2);
+            let start_time = Utc::now();
+            let cache = MemoryCertificateVerifierCache::new(expiration_delay)
+                .with_items([("hash", "first_parent"), ("another_hash", "another_parent")]);
+
+            let initial_value = cache.get_cached_value("hash").await.unwrap();
+
+            cache
+                .store_validated_certificate("hash", "updated_parent")
+                .await
+                .unwrap();
+
+            let updated_value = cache.get_cached_value("hash").await.unwrap();
+
+            assert_eq!(2, cache.len().await);
+            assert_eq!(
+                Some("another_parent".to_string()),
+                cache.get_previous_hash("another_hash").await.unwrap(),
+                "Existing but not updated value should not have been altered"
+            );
+            assert_ne!(initial_value, updated_value);
+            assert_eq!("updated_parent", updated_value.previous_hash);
+            assert!(updated_value.expire_at - start_time >= expiration_delay);
+        }
+    }
+
+    mod get_previous_hash {
+        use super::*;
+
+        #[tokio::test]
+        async fn get_previous_hash_when_key_exists() {
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1))
+                .with_items([("hash", "parent"), ("another_hash", "another_parent")]);
+
+            assert_eq!(
+                Some("parent".to_string()),
+                cache.get_previous_hash("hash").await.unwrap()
+            );
+        }
+
+        #[tokio::test]
+        async fn get_previous_hash_return_none_if_not_found() {
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1))
+                .with_items([("hash", "parent"), ("another_hash", "another_parent")]);
+
+            assert_eq!(None, cache.get_previous_hash("not_found").await.unwrap());
+        }
+
+        #[tokio::test]
+        async fn get_expired_previous_hash_return_none() {
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1))
+                .with_items([("hash", "parent")]);
+            cache
+                .overwrite_expiration_date("hash", Utc::now() - TimeDelta::days(5))
+                .await;
+
+            assert_eq!(None, cache.get_previous_hash("hash").await.unwrap());
+        }
+    }
+
+    mod reset {
+        use super::*;
+
+        #[tokio::test]
+        async fn reset_empty_cache_dont_raise_error() {
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1));
+
+            cache.reset().await.unwrap();
+
+            assert_eq!(HashMap::new(), cache.content().await);
+        }
+
+        #[tokio::test]
+        async fn reset_not_empty_cache() {
+            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(1))
+                .with_items([("hash", "parent"), ("another_hash", "another_parent")]);
+
+            cache.reset().await.unwrap();
+
+            assert_eq!(HashMap::new(), cache.content().await);
+        }
+    }
+}
diff --git a/mithril-client/src/certificate_client/verify_cache/mod.rs b/mithril-client/src/certificate_client/verify_cache/mod.rs
new file mode 100644
index 00000000000..6a4fdfba0c1
--- /dev/null
+++ b/mithril-client/src/certificate_client/verify_cache/mod.rs
@@ -0,0 +1,3 @@
+mod memory_cache;
+
+pub use memory_cache::*;
diff --git a/mithril-client/src/client.rs b/mithril-client/src/client.rs
index 6f52dab99d2..b093778104a 100644
--- a/mithril-client/src/client.rs
+++ b/mithril-client/src/client.rs
@@ -10,6 +10,8 @@ use mithril_common::api_version::APIVersionProvider;
 use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
 use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
 use crate::cardano_transaction_client::CardanoTransactionClient;
+#[cfg(feature = "unstable")]
+use crate::certificate_client::CertificateVerifierCache;
 use crate::certificate_client::{
     CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
 };
@@ -20,8 +22,13 @@ use crate::snapshot_client::SnapshotClient;
 use crate::snapshot_downloader::{HttpSnapshotDownloader, SnapshotDownloader};
 use crate::MithrilResult;
 
+#[cfg(target_family = "wasm")]
+const fn one_week_in_seconds() -> u32 {
+    604800
+}
+
 /// Options that can be used to configure the client.
-#[derive(Debug, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
 pub struct ClientOptions {
     /// HTTP headers to include in the client requests.
     pub http_headers: Option<HashMap<String, String>>,
@@ -30,6 +37,25 @@ pub struct ClientOptions {
     #[cfg(target_family = "wasm")]
     #[cfg_attr(target_family = "wasm", serde(default))]
     pub unstable: bool,
+
+    /// Whether to enable certificate chain verification caching in the WASM client.
+    ///
+    /// `unstable` must be set to `true` for this option to have any effect.
+    ///
+    /// DANGER: This feature is highly experimental and insecure, and it must not be used in production
+    #[cfg(target_family = "wasm")]
+    #[cfg_attr(target_family = "wasm", serde(default))]
+    pub enable_certificate_chain_verification_cache: bool,
+
+    /// Duration in seconds of certificate chain verification cache in the WASM client.
+    ///
+    /// Default to one week (604800 seconds).
+    ///
+    /// `enable_certificate_chain_verification_cache` and `unstable` must both be set to `true`
+    /// for this option to have any effect.
+    #[cfg(target_family = "wasm")]
+    #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
+    pub certificate_chain_verification_cache_duration_in_seconds: u32,
 }
 
 impl ClientOptions {
@@ -39,6 +65,10 @@ impl ClientOptions {
             http_headers,
             #[cfg(target_family = "wasm")]
             unstable: false,
+            #[cfg(target_family = "wasm")]
+            enable_certificate_chain_verification_cache: false,
+            #[cfg(target_family = "wasm")]
+            certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
         }
     }
 
@@ -94,6 +124,8 @@ pub struct ClientBuilder {
     genesis_verification_key: String,
     aggregator_client: Option<Arc<dyn AggregatorClient>>,
     certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
+    #[cfg(feature = "unstable")]
+    certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
     #[cfg(feature = "fs")]
     snapshot_downloader: Option<Arc<dyn SnapshotDownloader>>,
     logger: Option<Logger>,
@@ -110,6 +142,8 @@ impl ClientBuilder {
             genesis_verification_key: genesis_verification_key.to_string(),
             aggregator_client: None,
             certificate_verifier: None,
+            #[cfg(feature = "unstable")]
+            certificate_verifier_cache: None,
             #[cfg(feature = "fs")]
             snapshot_downloader: None,
             logger: None,
@@ -128,6 +162,8 @@ impl ClientBuilder {
             genesis_verification_key: genesis_verification_key.to_string(),
             aggregator_client: None,
             certificate_verifier: None,
+            #[cfg(feature = "unstable")]
+            certificate_verifier_cache: None,
             #[cfg(feature = "fs")]
             snapshot_downloader: None,
             logger: None,
@@ -188,6 +224,8 @@ impl ClientBuilder {
                     aggregator_client.clone(),
                     &self.genesis_verification_key,
                     feedback_sender.clone(),
+                    #[cfg(feature = "unstable")]
+                    self.certificate_verifier_cache,
                     logger.clone(),
                 )
                 .with_context(|| "Building certificate verifier failed")?,
@@ -243,6 +281,19 @@ impl ClientBuilder {
         self
     }
 
+    cfg_unstable! {
+    /// Set the [CertificateVerifierCache] that will be used to cache certificate validation results.
+    ///
+    /// Passing a `None` value will disable the cache if any was previously set.
+    pub fn with_certificate_verifier_cache(
+        mut self,
+        certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
+    ) -> ClientBuilder {
+        self.certificate_verifier_cache = certificate_verifier_cache;
+        self
+    }
+    }
+
     cfg_fs! {
     /// Set the [SnapshotDownloader] that will be used to download snapshots.
     pub fn with_snapshot_downloader(
diff --git a/mithril-client/src/feedback.rs b/mithril-client/src/feedback.rs
index f81c8cee2a4..ab5d4b9f0a2 100644
--- a/mithril-client/src/feedback.rs
+++ b/mithril-client/src/feedback.rs
@@ -91,13 +91,20 @@ pub enum MithrilEvent {
         /// Unique identifier used to track this specific certificate chain validation
         certificate_chain_validation_id: String,
     },
-    /// A individual certificate of a chain have been validated.
+    /// An individual certificate of a chain have been validated.
     CertificateValidated {
         /// Unique identifier used to track this specific certificate chain validation
         certificate_chain_validation_id: String,
         /// The validated certificate hash
         certificate_hash: String,
     },
+    /// An individual certificate of a chain have been fetched from the cache.
+    CertificateFetchedFromCache {
+        /// Unique identifier used to track this specific certificate chain validation
+        certificate_chain_validation_id: String,
+        /// The fetched certificate hash
+        certificate_hash: String,
+    },
     /// The whole certificate chain is valid.
     CertificateChainValidated {
         /// Unique identifier used to track this specific certificate chain validation
@@ -129,6 +136,10 @@ impl MithrilEvent {
                 certificate_chain_validation_id,
                 ..
             } => certificate_chain_validation_id,
+            MithrilEvent::CertificateFetchedFromCache {
+                certificate_chain_validation_id,
+                ..
+            } => certificate_chain_validation_id,
             MithrilEvent::CertificateChainValidated {
                 certificate_chain_validation_id,
             } => certificate_chain_validation_id,
@@ -226,6 +237,16 @@ impl FeedbackReceiver for SlogFeedbackReceiver {
                     "certificate_chain_validation_id" => certificate_chain_validation_id,
                 );
             }
+            MithrilEvent::CertificateFetchedFromCache {
+                certificate_hash,
+                certificate_chain_validation_id,
+            } => {
+                info!(
+                    self.logger, "Cached";
+                    "certificate_hash" => certificate_hash,
+                    "certificate_chain_validation_id" => certificate_chain_validation_id,
+                );
+            }
             MithrilEvent::CertificateChainValidated {
                 certificate_chain_validation_id,
             } => {
diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml
index 460cfc49609..613e403a91a 100644
--- a/mithril-common/Cargo.toml
+++ b/mithril-common/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "mithril-common"
-version = "0.4.97"
+version = "0.4.98"
 description = "Common types, interfaces, and utilities for Mithril nodes."
 authors = { workspace = true }
 edition = { workspace = true }
diff --git a/mithril-common/src/test_utils/certificate_chain_builder.rs b/mithril-common/src/test_utils/certificate_chain_builder.rs
index 9a5fc7f9952..5650704fc08 100644
--- a/mithril-common/src/test_utils/certificate_chain_builder.rs
+++ b/mithril-common/src/test_utils/certificate_chain_builder.rs
@@ -82,6 +82,20 @@ impl<'a> CertificateChainBuilderContext<'a> {
     }
 }
 
+/// Chaining method to use when building a certificate chain with the [CertificateChainBuilder]. For tests only.
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub enum CertificateChainingMethod {
+    /// `default` Chain certificates to the 'master' certificate of the epoch or if it's the 'master'
+    /// certificate, chain it to the 'master' certificate of the previous epoch.
+    ///
+    /// The 'master' certificate of an epoch is the first certificate of the epoch.
+    #[default]
+    ToMasterCertificate,
+
+    /// Chain certificates sequentially.
+    Sequential,
+}
+
 /// A builder for creating a certificate chain. For tests only.
 ///
 /// # Simple example usage for building a fully valid certificate chain
@@ -149,6 +163,7 @@ pub struct CertificateChainBuilder<'a> {
     total_signers_per_epoch_processor: &'a TotalSignersPerEpochProcessorFunc,
     genesis_certificate_processor: &'a GenesisCertificateProcessorFunc,
     standard_certificate_processor: &'a StandardCertificateProcessorFunc,
+    certificate_chaining_method: CertificateChainingMethod,
 }
 
 impl<'a> CertificateChainBuilder<'a> {
@@ -166,6 +181,7 @@ impl<'a> CertificateChainBuilder<'a> {
             total_signers_per_epoch_processor: &|epoch| min(2 + *epoch as usize, 5),
             genesis_certificate_processor: &|certificate, _, _| certificate,
             standard_certificate_processor: &|certificate, _| certificate,
+            certificate_chaining_method: Default::default(),
         }
     }
 
@@ -220,6 +236,16 @@ impl<'a> CertificateChainBuilder<'a> {
         self
     }
 
+    /// Set the chaining method to use when building the certificate chain.
+    pub fn with_certificate_chaining_method(
+        mut self,
+        certificate_chaining_method: CertificateChainingMethod,
+    ) -> Self {
+        self.certificate_chaining_method = certificate_chaining_method;
+
+        self
+    }
+
     /// Build the certificate chain.
     pub fn build(self) -> (Vec<Certificate>, ProtocolGenesisVerifier) {
         let (genesis_signer, genesis_verifier) = CertificateChainBuilder::setup_genesis();
@@ -438,26 +464,31 @@ impl<'a> CertificateChainBuilder<'a> {
         certificate: &Certificate,
         certificates_chained: &'b [Certificate],
     ) -> Option<&'b Certificate> {
-        let is_certificate_first_of_epoch = certificates_chained
-            .last()
-            .map(|c| c.epoch != certificate.epoch)
-            .unwrap_or(true);
-
-        certificates_chained
-            .iter()
-            .rev()
-            .filter(|c| {
-                if is_certificate_first_of_epoch {
-                    // The previous certificate of the first certificate of an epoch
-                    // is the first certificate of the previous epoch
-                    c.epoch == certificate.epoch.previous().unwrap()
-                } else {
-                    // The previous certificate of not the first certificate of an epoch
-                    // is the first certificate of the epoch
-                    c.epoch == certificate.epoch
-                }
-            })
-            .last()
+        match self.certificate_chaining_method {
+            CertificateChainingMethod::ToMasterCertificate => {
+                let is_certificate_first_of_epoch = certificates_chained
+                    .last()
+                    .map(|c| c.epoch != certificate.epoch)
+                    .unwrap_or(true);
+
+                certificates_chained
+                    .iter()
+                    .rev()
+                    .filter(|c| {
+                        if is_certificate_first_of_epoch {
+                            // The previous certificate of the first certificate of an epoch
+                            // is the first certificate of the previous epoch
+                            c.epoch == certificate.epoch.previous().unwrap()
+                        } else {
+                            // The previous certificate of not the first certificate of an epoch
+                            // is the first certificate of the epoch
+                            c.epoch == certificate.epoch
+                        }
+                    })
+                    .last()
+            }
+            CertificateChainingMethod::Sequential => certificates_chained.last(),
+        }
     }
 
     // Returns the chained certificates in reverse order
@@ -788,7 +819,7 @@ mod test {
     }
 
     #[test]
-    fn builds_certificate_chain_correctly_chained() {
+    fn builds_certificate_chain_chained_by_default_to_master_certificates() {
         fn create_fake_certificate(epoch: Epoch, index_in_epoch: u64) -> Certificate {
             Certificate {
                 epoch,
@@ -845,6 +876,65 @@ mod test {
         );
     }
 
+    #[test]
+    fn builds_certificate_chain_chained_sequentially() {
+        fn create_fake_certificate(epoch: Epoch, index_in_epoch: u64) -> Certificate {
+            Certificate {
+                epoch,
+                signed_message: format!("certificate-{}-{index_in_epoch}", *epoch),
+                ..fake_data::certificate("cert-fake".to_string())
+            }
+        }
+
+        let certificates = vec![
+            create_fake_certificate(Epoch(1), 1),
+            create_fake_certificate(Epoch(2), 1),
+            create_fake_certificate(Epoch(2), 2),
+            create_fake_certificate(Epoch(3), 1),
+            create_fake_certificate(Epoch(4), 1),
+            create_fake_certificate(Epoch(4), 2),
+            create_fake_certificate(Epoch(4), 3),
+        ];
+
+        let mut certificates_chained = CertificateChainBuilder::default()
+            .with_certificate_chaining_method(CertificateChainingMethod::Sequential)
+            .compute_chained_certificates(certificates);
+        certificates_chained.reverse();
+
+        let certificate_chained_1_1 = &certificates_chained[0];
+        let certificate_chained_2_1 = &certificates_chained[1];
+        let certificate_chained_2_2 = &certificates_chained[2];
+        let certificate_chained_3_1 = &certificates_chained[3];
+        let certificate_chained_4_1 = &certificates_chained[4];
+        let certificate_chained_4_2 = &certificates_chained[5];
+        let certificate_chained_4_3 = &certificates_chained[6];
+        assert_eq!("", certificate_chained_1_1.previous_hash);
+        assert_eq!(
+            certificate_chained_2_1.previous_hash,
+            certificate_chained_1_1.hash
+        );
+        assert_eq!(
+            certificate_chained_2_2.previous_hash,
+            certificate_chained_2_1.hash
+        );
+        assert_eq!(
+            certificate_chained_3_1.previous_hash,
+            certificate_chained_2_2.hash
+        );
+        assert_eq!(
+            certificate_chained_4_1.previous_hash,
+            certificate_chained_3_1.hash
+        );
+        assert_eq!(
+            certificate_chained_4_2.previous_hash,
+            certificate_chained_4_1.hash
+        );
+        assert_eq!(
+            certificate_chained_4_3.previous_hash,
+            certificate_chained_4_2.hash
+        );
+    }
+
     #[test]
     fn builds_certificate_chain_with_alteration_on_genesis_certificate() {
         let (certificates, _) = CertificateChainBuilder::new()
diff --git a/mithril-common/src/test_utils/mod.rs b/mithril-common/src/test_utils/mod.rs
index 2a0de475583..baadb5da5eb 100644
--- a/mithril-common/src/test_utils/mod.rs
+++ b/mithril-common/src/test_utils/mod.rs
@@ -25,7 +25,9 @@ mod temp_dir;
 pub mod test_http_server;
 
 pub use cardano_transactions_builder::CardanoTransactionsBuilder;
-pub use certificate_chain_builder::{CertificateChainBuilder, CertificateChainBuilderContext};
+pub use certificate_chain_builder::{
+    CertificateChainBuilder, CertificateChainBuilderContext, CertificateChainingMethod,
+};
 pub use fixture_builder::{MithrilFixtureBuilder, StakeDistributionGenerationMethod};
 pub use mithril_fixture::{MithrilFixture, SignerFixture};
 pub use temp_dir::*;
diff --git a/mithril-explorer/package-lock.json b/mithril-explorer/package-lock.json
index 96036bbefdc..05aafec36d6 100644
--- a/mithril-explorer/package-lock.json
+++ b/mithril-explorer/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "mithril-explorer",
-  "version": "0.7.23",
+  "version": "0.7.24",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "mithril-explorer",
-      "version": "0.7.23",
+      "version": "0.7.24",
       "dependencies": {
         "@mithril-dev/mithril-client-wasm": "file:../mithril-client-wasm/dist/web",
         "@popperjs/core": "^2.11.8",
@@ -36,7 +36,7 @@
     },
     "../mithril-client-wasm/dist/web": {
       "name": "mithril-client-wasm",
-      "version": "0.7.2",
+      "version": "0.7.3",
       "license": "Apache-2.0"
     },
     "node_modules/@aashutoshrathi/word-wrap": {
@@ -7693,14 +7693,15 @@
       "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "funding": [
         {
           "type": "github",
           "url": "https://github.com/sponsors/ai"
         }
       ],
-      "license": "MIT",
       "bin": {
         "nanoid": "bin/nanoid.cjs"
       },
diff --git a/mithril-explorer/package.json b/mithril-explorer/package.json
index b82cfd213db..6f85cf84274 100644
--- a/mithril-explorer/package.json
+++ b/mithril-explorer/package.json
@@ -1,6 +1,6 @@
 {
   "name": "mithril-explorer",
-  "version": "0.7.23",
+  "version": "0.7.24",
   "private": true,
   "scripts": {
     "dev": "next dev",
diff --git a/mithril-explorer/src/components/VerifyCertificate/CertificateVerifier.js b/mithril-explorer/src/components/VerifyCertificate/CertificateVerifier.js
index 2b6db60e148..01d67b714d3 100644
--- a/mithril-explorer/src/components/VerifyCertificate/CertificateVerifier.js
+++ b/mithril-explorer/src/components/VerifyCertificate/CertificateVerifier.js
@@ -19,6 +19,7 @@ export const certificateValidationSteps = {
 const certificateChainValidationEvents = {
   started: "CertificateChainValidationStarted",
   certificateValidated: "CertificateValidated",
+  certificateFetchedFromCache: "CertificateFetchedFromCache",
   done: "CertificateChainValidated",
 };
 
@@ -51,6 +52,7 @@ export default function CertificateVerifier({
   certificate,
   hideSpinner = false,
   showCertificateLinks = false,
+  isCacheEnabled = false,
   onStepChange = (step) => {},
   onChainValidationError = (error) => {},
   onCertificateClick = (hash) => {},
@@ -113,7 +115,11 @@ export default function CertificateVerifier({
         break;
       case certificateChainValidationEvents.certificateValidated:
         position = eventPosition.inTable;
-        message = { certificateHash: event.payload.certificate_hash };
+        message = { certificateHash: event.payload.certificate_hash, cached: false };
+        break;
+      case certificateChainValidationEvents.certificateFetchedFromCache:
+        position = eventPosition.inTable;
+        message = { certificateHash: event.payload.certificate_hash, cached: true };
         break;
       case certificateChainValidationEvents.done:
         message = (
@@ -133,6 +139,10 @@ export default function CertificateVerifier({
     ]);
   }
 
+  async function onCacheResetClick() {
+    await client.reset_certificate_verifier_cache();
+  }
+
   return (
     <>
       {Object.entries(certificate).length > 0 && (
@@ -192,7 +202,11 @@ export default function CertificateVerifier({
                           />
                         </td>
                         <td>
-                          <IconBadge tooltip="yes" variant="success" icon="check-circle-fill" />
+                          {evt.message.cached ? (
+                            <IconBadge tooltip="cached" variant="warning" icon="clock-fill" />
+                          ) : (
+                            <IconBadge tooltip="yes" variant="success" icon="check-circle-fill" />
+                          )}
                         </td>
                       </tr>
                     ))}
@@ -203,6 +217,14 @@ export default function CertificateVerifier({
                 .map((evt) => (
                   <div key={evt.id}>{evt.message}</div>
                 ))}
+              {isCacheEnabled && currentStep === certificateValidationSteps.done && (
+                <>
+                  Cache enabled:{" "}
+                  <a href="#" onClick={onCacheResetClick}>
+                    reset cache
+                  </a>
+                </>
+              )}
               {validationError !== undefined && (
                 <Alert variant="danger" className="mt-2">
                   <Alert.Heading>
diff --git a/mithril-explorer/src/components/VerifyCertificate/VerifyCertificateModal.js b/mithril-explorer/src/components/VerifyCertificate/VerifyCertificateModal.js
index ebae7a9079f..4b261fc8732 100644
--- a/mithril-explorer/src/components/VerifyCertificate/VerifyCertificateModal.js
+++ b/mithril-explorer/src/components/VerifyCertificate/VerifyCertificateModal.js
@@ -11,6 +11,7 @@ export default function VerifyCertificateModal({ show, onClose, certificateHash
   const [showLoadingWarning, setShowLoadingWarning] = useState(false);
   const [client, setClient] = useState(undefined);
   const [certificate, setCertificate] = useState(undefined);
+  const [isCacheEnabled, setIsCacheEnabled] = useState(false);
 
   useEffect(() => {
     if (show) {
@@ -30,11 +31,19 @@ export default function VerifyCertificateModal({ show, onClose, certificateHash
 
   async function init(aggregator, certificateHash) {
     const genesisVerificationKey = await fetchGenesisVerificationKey(aggregator);
-    const client = new MithrilClient(aggregator, genesisVerificationKey);
+    const isCacheEnabled = process.env.UNSTABLE === true;
+    const client_options = process.env.UNSTABLE
+      ? {
+          unstable: true,
+          enable_certificate_chain_verification_cache: isCacheEnabled,
+        }
+      : {};
+    const client = new MithrilClient(aggregator, genesisVerificationKey, client_options);
     const certificate = await client.get_mithril_certificate(certificateHash);
 
     setClient(client);
     setCertificate(certificate);
+    setIsCacheEnabled(isCacheEnabled);
   }
 
   function handleModalClose() {
@@ -69,6 +78,7 @@ export default function VerifyCertificateModal({ show, onClose, certificateHash
               <CertificateVerifier
                 client={client}
                 certificate={certificate}
+                isCacheEnabled={isCacheEnabled}
                 onStepChange={(step) =>
                   setLoading(step === certificateValidationSteps.validationInProgress)
                 }