diff --git a/Cargo.lock b/Cargo.lock
index 7c17d76a7ba1e511357fc4b2f93f0c7109d23686..46d4c06f5b6a0892c0e48416c38d4784249ed027 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1536,6 +1536,7 @@ dependencies = [
  "pallet-provide-randomness",
  "pallet-proxy",
  "pallet-quota",
+ "pallet-ring",
  "pallet-scheduler",
  "pallet-session",
  "pallet-smith-members",
@@ -1997,6 +1998,7 @@ dependencies = [
  "curve25519-dalek-derive",
  "digest 0.10.7",
  "fiat-crypto",
+ "rand_core",
  "rustc_version",
  "subtle 2.6.1",
  "zeroize",
@@ -2570,10 +2572,12 @@ dependencies = [
  "notify-debouncer-mini",
  "parity-scale-codec",
  "portpicker",
+ "rand",
  "serde_json",
  "sp-core",
  "sp-core-hashing",
  "sp-keyring",
+ "sp-ring",
  "sp-runtime",
  "subxt",
  "tokio",
@@ -3540,6 +3544,7 @@ dependencies = [
  "pallet-provide-randomness",
  "pallet-proxy",
  "pallet-quota",
+ "pallet-ring",
  "pallet-scheduler",
  "pallet-session",
  "pallet-session-benchmarking",
@@ -3616,6 +3621,7 @@ dependencies = [
  "pallet-provide-randomness",
  "pallet-proxy",
  "pallet-quota",
+ "pallet-ring",
  "pallet-scheduler",
  "pallet-session",
  "pallet-session-benchmarking",
@@ -3950,6 +3956,7 @@ dependencies = [
  "pallet-provide-randomness",
  "pallet-proxy",
  "pallet-quota",
+ "pallet-ring",
  "pallet-scheduler",
  "pallet-session",
  "pallet-session-benchmarking",
@@ -6642,6 +6649,16 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
+[[package]]
+name = "orodruin"
+version = "0.1.0"
+dependencies = [
+ "blake2b_simd",
+ "curve25519-dalek",
+ "rand_core",
+ "zeroize",
+]
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -7079,6 +7096,30 @@ dependencies = [
  "sp-std 14.0.0",
 ]
 
+[[package]]
+name = "pallet-ring"
+version = "1.0.0"
+dependencies = [
+ "blake2b_simd",
+ "curve25519-dalek",
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "log",
+ "orodruin",
+ "pallet-authorship",
+ "pallet-balances",
+ "pallet-session",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-consensus-babe",
+ "sp-core",
+ "sp-io",
+ "sp-ring",
+ "sp-runtime",
+ "sp-std 14.0.0",
+]
+
 [[package]]
 name = "pallet-scheduler"
 version = "29.0.0"
@@ -11349,6 +11390,24 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "sp-ring"
+version = "1.0.0"
+dependencies = [
+ "async-trait",
+ "blake2b_simd",
+ "curve25519-dalek",
+ "frame-support",
+ "orodruin",
+ "parity-scale-codec",
+ "rand_core",
+ "scale-info",
+ "serde",
+ "sp-runtime",
+ "sp-std 14.0.0",
+ "thiserror",
+]
+
 [[package]]
 name = "sp-rpc"
 version = "26.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index ccafea0179677850c7ae08b55f68ca653c4b890e..91d0a1339e04ada053972e97fa2be22119b795dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,11 +17,13 @@ members = [
     'pallets/membership',
     'pallets/oneshot-account',
     'pallets/quota',
+    'pallets/ring',
     'pallets/smith-members',
     'pallets/universal-dividend',
     'pallets/upgrade-origin',
     'primitives/distance',
     'primitives/membership',
+    'primitives/ring',
     'resources/weight_analyzer',
     'runtime/common',
     'runtime/gdev',
@@ -71,7 +73,7 @@ convert_case = { version = "0.6.0", default-features = false }
 subweight-core = { version = "3.3.1", default-features = false }
 version_check = { version = "0.9.4", default-features = false }
 codec = { package = "parity-scale-codec", version = "3.6.9", default-features = false }
-enum-as-inner = { version = "=0.5.1", default-features = false }                        #https://github.com/bluejekyll/trust-dns/issues/1946
+enum-as-inner = { version = "=0.5.1", default-features = false } #https://github.com/bluejekyll/trust-dns/issues/1946
 futures = { version = "0.3.30", default-features = false }
 tera = { version = "1", default-features = false }
 hex = { version = "0.4.3", default-features = false }
@@ -102,6 +104,18 @@ simple_logger = { version = "4.3.3", default-features = false }
 bincode = { version = "1.3.3", default-features = false }
 dubp-wot = { version = "0.11.1", default-features = false }
 flate2 = { version = "1.0.28", default-features = false }
+orodruin = { path = "../blsag", default-features = false, features = [
+    "blake2b",
+] }
+#orodruin = { git = "https://git.duniter.org/tuxmain/orodruin-rs.git", default-features = false, features = ["blake2b"] }
+#orodruin = { version = "0.1.0", default-features = false, features = [
+#    "blake2b",
+#] }
+curve25519-dalek = { version = "4", default-features = false, features = [
+    "digest",
+] }
+rand_core = { version = "0.6.4", default-features = false }
+blake2b_simd = { version = "1.0.2", default-features = false }
 
 # Subxt
 subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.37.0-duniter-substrate-v1.14.0', default-features = false }
@@ -128,12 +142,14 @@ pallet-offences = { path = 'pallets/offences', default-features = false }
 pallet-oneshot-account = { path = 'pallets/oneshot-account', default-features = false }
 pallet-provide-randomness = { path = 'pallets/provide-randomness', default-features = false }
 pallet-quota = { path = 'pallets/quota', default-features = false }
+pallet-ring = { path = 'pallets/ring', default-features = false }
 pallet-session-benchmarking = { path = 'pallets/session-benchmarking', default-features = false }
 pallet-smith-members = { path = 'pallets/smith-members', default-features = false }
 pallet-universal-dividend = { path = 'pallets/universal-dividend', default-features = false }
 pallet-upgrade-origin = { path = 'pallets/upgrade-origin', default-features = false }
 sp-distance = { path = 'primitives/distance', default-features = false }
 sp-membership = { path = 'primitives/membership', default-features = false }
+sp-ring = { path = 'primitives/ring', default-features = false }
 
 # substrate dependencies
 pallet-transaction-payment-rpc = { git = 'https://github.com/duniter/duniter-polkadot-sdk', branch = 'duniter-substrate-v1.14.0', default-features = false }
diff --git a/end2end-tests/Cargo.toml b/end2end-tests/Cargo.toml
index d045afbb890d7429f99f0cb14e798b5ef738795a..c3da2bd8aaf3a603c76d8e3ee505b84bae05a419 100644
--- a/end2end-tests/Cargo.toml
+++ b/end2end-tests/Cargo.toml
@@ -22,6 +22,7 @@ std = [
 	"serde_json/std",
 	"sp-core-hashing/std",
 	"sp-core/std",
+	"sp-ring/std",
 	"sp-runtime/std",
 ]
 standalone = ["distance-oracle/standalone"]
@@ -39,11 +40,14 @@ env_logger = { workspace = true }
 hex = { workspace = true }
 notify = { workspace = true }
 notify-debouncer-mini = { workspace = true }
+#orodruin = { workspace = true, features = ["alloc"] }
 portpicker = { workspace = true }
+rand = "0.8.5"
 serde_json = { workspace = true }
 sp-core = { workspace = true }
 sp-core-hashing = { workspace = true }
 sp-keyring = { workspace = true }
+sp-ring = { workspace = true }
 sp-runtime = { workspace = true }
 subxt = { workspace = true, features = [
 	"substrate-compat",
diff --git a/end2end-tests/cucumber-features/ring.feature b/end2end-tests/cucumber-features/ring.feature
new file mode 100644
index 0000000000000000000000000000000000000000..956e075aa7e7f672adcb530f1ef3b0d8f608dba2
--- /dev/null
+++ b/end2end-tests/cucumber-features/ring.feature
@@ -0,0 +1,17 @@
+@genesis.default
+Feature: Ring anonymous transactions
+
+  Scenario: Simple mix
+    Then alice should have 9 ÄžD
+    Then bob should have 10 ÄžD
+    When alice signs up to mix 5 ÄžD with secret key 42
+    When bob signs up to mix 5 ÄžD with secret key 123
+    Then alice should have 4 ÄžD
+    Then bob should have 5 ÄžD
+    When 2 blocks later
+    When charlie claims mixed 5 ÄžD with secret key 123
+    When dave claims mixed 5 ÄžD with secret key 42
+    Then charlie should have 15 ÄžD
+    Then dave should have 15 ÄžD
+    Then alice should have 4 ÄžD
+    Then bob should have 5 ÄžD
diff --git a/end2end-tests/cucumber-genesis/default.json b/end2end-tests/cucumber-genesis/default.json
index 2912937ff5b82608ade3116bf4fd837bffbe2ea1..552a173544aaa9eea323f31aac677a419e590cf9 100644
--- a/end2end-tests/cucumber-genesis/default.json
+++ b/end2end-tests/cucumber-genesis/default.json
@@ -67,7 +67,8 @@
     "smith_wot_min_cert_for_membership": 2,
     "wot_first_cert_issuable_on": 20,
     "wot_min_cert_for_create_idty_right": 2,
-    "wot_min_cert_for_membership": 2
+    "wot_min_cert_for_membership": 2,
+    "ring_mix_period": 20
   },
   "clique_smiths": [
     {
@@ -93,4 +94,4 @@
     "number": 0,
     "medianTime": 1700000000
   }
-}
+}
\ No newline at end of file
diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs
index 6ed0e8375c02367af9262d01261c2b198474a5a2..12c1fa0665b5176dd4b8aeec2cfcb182987aa1c2 100644
--- a/end2end-tests/tests/common/mod.rs
+++ b/end2end-tests/tests/common/mod.rs
@@ -21,12 +21,20 @@ pub mod cert;
 pub mod distance;
 pub mod identity;
 pub mod oneshot;
+pub mod ring;
 
 #[subxt::subxt(
     runtime_metadata_path = "../resources/metadata.scale",
     derive_for_all_types = "Eq, PartialEq"
 )]
-pub mod gdev {}
+pub mod gdev {
+    //#[subxt::subxt(substitute_type = "sp_ring::RingPubkey")]
+    //use ::runtime_types::pallet_ring::RingPubkey;
+}
+/*substitute_type(
+    path = "sp_ring::RingPubkey",
+    with = "::subxt::utils::Static<::sp_ring::RingPubkey>"
+)*/
 
 use anyhow::anyhow;
 use codec::Encode;
diff --git a/end2end-tests/tests/common/ring.rs b/end2end-tests/tests/common/ring.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ce99abc0b6c82ae18c78178bc6eaec0304fe406b
--- /dev/null
+++ b/end2end-tests/tests/common/ring.rs
@@ -0,0 +1,209 @@
+// Copyright 2024 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+use super::{
+    gdev,
+    gdev::runtime_types::{pallet_identity, pallet_ring},
+    *,
+};
+use crate::{gdev::runtime_types::sp_ring::RingSignature, DuniterWorld};
+use sp_keyring::AccountKeyring;
+use subxt::{
+    backend::rpc::RpcClient,
+    tx::{PairSigner, Signer},
+    utils::AccountId32,
+};
+
+pub async fn signup(
+    client: &FullClient,
+    origin: AccountKeyring,
+    amount: u64,
+    secret_key: sp_ring::SecretKey,
+) -> Result<()> {
+    let origin = PairSigner::new(origin.pair());
+
+    let _events = create_block_with_extrinsic(
+        &client.rpc,
+        client
+            .client
+            .tx()
+            .create_signed(
+                &gdev::tx().ring().signup(
+                    gdev::runtime_types::sp_ring::RingPubkey(secret_key.public_key().0.to_bytes()),
+                    amount,
+                ),
+                &origin,
+                SubstrateExtrinsicParamsBuilder::new().build(),
+            )
+            .await?,
+    )
+    .await?;
+
+    Ok(())
+}
+
+pub async fn cancel(
+    client: &FullClient,
+    origin: AccountKeyring,
+    ring_pubkey: sp_ring::RingPubkey,
+    amount: u64,
+) -> Result<()> {
+    let origin = PairSigner::new(origin.pair());
+
+    let _events = create_block_with_extrinsic(
+        &client.rpc,
+        client
+            .client
+            .tx()
+            .create_signed(
+                &gdev::tx().ring().cancel(
+                    gdev::runtime_types::sp_ring::RingPubkey(ring_pubkey.0),
+                    amount,
+                ),
+                &origin,
+                SubstrateExtrinsicParamsBuilder::new().build(),
+            )
+            .await?,
+    )
+    .await?;
+
+    Ok(())
+}
+
+pub async fn claim(
+    client: &FullClient,
+    origin: AccountKeyring,
+    ring_secret_key: sp_ring::SecretKey,
+    amount: u64,
+) -> Result<()> {
+    let origin: PairSigner<GdevConfig, _> = PairSigner::new(origin.pair());
+
+    let current_ring_session_index = match client
+        .client
+        .storage()
+        .at_latest()
+        .await
+        .unwrap()
+        .fetch(&gdev::storage().ring().current_ring_session_index())
+        .await?
+        .unwrap_or(gdev::runtime_types::pallet_ring::types::RingSessionIndex::One)
+    {
+        gdev::runtime_types::pallet_ring::types::RingSessionIndex::Zero => {
+            gdev::runtime_types::pallet_ring::types::RingSessionIndex::One
+        }
+        gdev::runtime_types::pallet_ring::types::RingSessionIndex::One => {
+            gdev::runtime_types::pallet_ring::types::RingSessionIndex::Zero
+        }
+    };
+
+    /*std::fs::write(
+        "/tmp/log",
+        format!(
+            "{:?}",
+            client
+                .client
+                .storage()
+                .at_latest()
+                .await
+                .unwrap()
+                .iter(gdev::storage().ring().signups_iter())
+                .await?
+                .next()
+                .await
+                .expect("None iter")
+                .expect("Error iter")
+                .value
+                .0
+        ),
+    )
+    .ok();*/
+
+    let ring: Vec<_> = client
+        .client
+        .storage()
+        .at_latest()
+        .await
+        .unwrap()
+        .iter(gdev::storage().ring().signups_iter())
+        .await?
+        .next()
+        .await
+        .expect("None iter")
+        .expect("Error iter")
+        .value
+        .0
+        .into_iter()
+        .map(|(pk, _account_id)| sp_ring::RingPubkey(pk.0))
+        .collect();
+
+    /*let ring: Vec<_> = client
+    .client
+    .storage()
+    .at_latest()
+    .await
+    .unwrap()
+    .fetch(
+        &gdev::storage()
+            .ring()
+            .signups(current_ring_session_index, amount),
+    )
+    .await?
+    .expect("Ring signups not found")
+    .0
+    .into_iter()
+    .map(|(pk, _account_id)| sp_ring::RingPubkey(pk.0))
+    .collect();*/
+
+    let rng = rand::thread_rng();
+
+    let signature = sp_ring::sign(
+        ring_secret_key,
+        &ring,
+        &sp_ring::RingClaimPayload::<AccountId32, u64> {
+            amount,
+            recipient: origin.account_id().clone(),
+        }
+        .encode(),
+        rng,
+    )
+    .expect("Private key is not in ring");
+
+    let _events = create_block_with_extrinsic(
+        &client.rpc,
+        client
+            .client
+            .tx()
+            .create_signed(
+                &gdev::tx().ring().claim(
+                    origin.account_id().clone(),
+                    amount,
+                    RingSignature {
+                        challenge: signature.challenge,
+                        key_image: gdev::runtime_types::sp_ring::RingKeyImage(
+                            signature.key_image.0,
+                        ),
+                        responses: signature.responses,
+                    },
+                ),
+                &origin,
+                SubstrateExtrinsicParamsBuilder::new().build(),
+            )
+            .await?,
+    )
+    .await?;
+
+    Ok(())
+}
diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs
index add3f14a01aa27736e80506b9b70c32019ca299a..fd351e4a2f95eacb4c3248972ea7362892cead49 100644
--- a/end2end-tests/tests/cucumber_tests.rs
+++ b/end2end-tests/tests/cucumber_tests.rs
@@ -366,6 +366,52 @@ async fn run_distance_oracle(world: &mut DuniterWorld, who: String) -> Result<()
     .await
 }
 
+#[allow(clippy::needless_pass_by_ref_mut)]
+#[when(regex = r#"([a-zA-Z]+) signs up to mix (\d+) (ÄžD|cÄžD) with secret key (\d+)"#)]
+async fn ring_signup(
+    world: &mut DuniterWorld,
+    from: String,
+    amount: u64,
+    unit: String,
+    secret_key: u8,
+) -> Result<()> {
+    let from = AccountKeyring::from_str(&from).expect("unknown from");
+    let (amount, is_ud) = parse_amount(amount, &unit);
+
+    assert!(!is_ud);
+
+    common::ring::signup(
+        world.full_client(),
+        from,
+        amount,
+        sp_ring::SecretKey::from_bytes([secret_key; 32]),
+    )
+    .await
+}
+
+#[allow(clippy::needless_pass_by_ref_mut)]
+#[when(regex = r#"([a-zA-Z]+) claims mixed (\d+) (ÄžD|cÄžD) with secret key (\d+)"#)]
+async fn ring_claim(
+    world: &mut DuniterWorld,
+    from: String,
+    amount: u64,
+    unit: String,
+    secret_key: u8,
+) -> Result<()> {
+    let from = AccountKeyring::from_str(&from).expect("unknown from");
+    let (amount, is_ud) = parse_amount(amount, &unit);
+
+    assert!(!is_ud);
+
+    common::ring::claim(
+        world.full_client(),
+        from,
+        sp_ring::SecretKey::from_bytes([secret_key; 32]),
+        amount,
+    )
+    .await
+}
+
 // ===== then ====
 
 #[allow(clippy::needless_pass_by_ref_mut)]
diff --git a/pallets/oneshot-account/src/lib.rs b/pallets/oneshot-account/src/lib.rs
index 3bef8281a38bd21d1acdfb96c39b15f07d99da7b..f151081f87a3372835b1b2f361008d1ea24c20a0 100644
--- a/pallets/oneshot-account/src/lib.rs
+++ b/pallets/oneshot-account/src/lib.rs
@@ -162,12 +162,7 @@ pub mod pallet {
                 Preservation::Preserve,
                 Fortitude::Polite,
             )?;
-            OneshotAccounts::<T>::insert(&dest, value);
-            Self::deposit_event(Event::OneshotAccountCreated {
-                account: dest,
-                balance: value,
-                creator: transactor,
-            });
+            Self::do_create_oneshot_account(transactor.clone(), dest.clone(), value);
 
             Ok(())
         }
@@ -208,12 +203,7 @@ pub mod pallet {
                     OneshotAccounts::<T>::get(&dest).is_none(),
                     Error::<T>::OneshotAccountAlreadyCreated
                 );
-                OneshotAccounts::<T>::insert(&dest, value);
-                Self::deposit_event(Event::OneshotAccountCreated {
-                    account: dest.clone(),
-                    balance: value,
-                    creator: transactor.clone(),
-                });
+                Self::do_create_oneshot_account(transactor.clone(), dest.clone(), value);
             } else if frame_system::Pallet::<T>::providers(&dest) > 0 {
                 let _ = T::Currency::deposit(&dest, value, Precision::Exact)?;
             }
@@ -297,22 +287,12 @@ pub mod pallet {
                     balance2 >= T::Currency::minimum_balance(),
                     Error::<T>::ExistentialDeposit
                 );
-                OneshotAccounts::<T>::insert(&dest2, balance2);
-                Self::deposit_event(Event::OneshotAccountCreated {
-                    account: dest2.clone(),
-                    balance: balance2,
-                    creator: transactor.clone(),
-                });
+                Self::do_create_oneshot_account(transactor.clone(), dest2.clone(), balance2);
             } else if frame_system::Pallet::<T>::providers(&dest2) > 0 {
                 let _ = T::Currency::deposit(&dest2, balance2, Precision::Exact)?;
             }
             if dest1_is_oneshot {
-                OneshotAccounts::<T>::insert(&dest1, balance1);
-                Self::deposit_event(Event::OneshotAccountCreated {
-                    account: dest1.clone(),
-                    balance: balance1,
-                    creator: transactor.clone(),
-                });
+                Self::do_create_oneshot_account(transactor.clone(), dest1.clone(), balance1);
             } else if frame_system::Pallet::<T>::providers(&dest1) > 0 {
                 let _ = T::Currency::deposit(&dest1, balance1, Precision::Exact)?;
             }
@@ -326,6 +306,25 @@ pub mod pallet {
             Ok(())
         }
     }
+
+    // PUBLIC METHODS //
+    impl<T: Config> Pallet<T> {
+        /// Actually creates a oneshot account
+        ///
+        /// Does not perform any check nor withdraws any amount from the origin.
+        pub fn do_create_oneshot_account(
+            creator: T::AccountId,
+            account: T::AccountId,
+            balance: BalanceOf<T>,
+        ) {
+            OneshotAccounts::<T>::insert(&account, balance);
+            Self::deposit_event(Event::OneshotAccountCreated {
+                account,
+                balance,
+                creator,
+            });
+        }
+    }
 }
 
 impl<T: Config> OnChargeTransaction<T> for Pallet<T>
diff --git a/pallets/ring/Cargo.toml b/pallets/ring/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..0e40846b561968336e466d4219d90b89704574f5
--- /dev/null
+++ b/pallets/ring/Cargo.toml
@@ -0,0 +1,70 @@
+[package]
+authors.workspace = true
+description = "duniter pallet ring"
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+name = "pallet-ring"
+readme = "README.md"
+repository.workspace = true
+version.workspace = true
+
+[features]
+default = ["std"]
+runtime-benchmarks = [
+	"frame-benchmarking/runtime-benchmarks",
+	"frame-support/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+	"pallet-balances/runtime-benchmarks",
+	"sp-runtime/runtime-benchmarks",
+]
+std = [
+	"codec/std",
+	"frame-benchmarking?/std",
+	"frame-support/std",
+	"frame-system/std",
+	"pallet-authorship/std",
+	"pallet-balances/std",
+	"pallet-session/std",
+	"scale-info/std",
+	"sp-consensus-babe/std",
+	"sp-core/std",
+	"sp-io/std",
+	"sp-runtime/std",
+	"sp-ring/std",
+	"sp-std/std",
+]
+try-runtime = [
+	"frame-support/try-runtime",
+	"frame-system/try-runtime",
+	"pallet-authorship/try-runtime",
+	"pallet-balances/try-runtime",
+	"pallet-session/try-runtime",
+	"sp-runtime/try-runtime",
+]
+
+[package.metadata.docs.rs]
+default-features = false
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+blake2b_simd = { workspace = true }
+codec = { workspace = true, features = ["derive"] }
+curve25519-dalek = { workspace = true }
+frame-benchmarking = { workspace = true, optional = true }
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+log = { workspace = true }
+orodruin = { workspace = true }
+pallet-authorship = { workspace = true }
+pallet-balances = { workspace = true }
+pallet-session = { workspace = true }
+scale-info = { workspace = true, features = ["derive"] }
+sp-consensus-babe = { workspace = true }
+sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+sp-ring = { workspace = true }
+sp-std = { workspace = true }
+
+[dev-dependencies]
+sp-io = { workspace = true }
diff --git a/pallets/ring/README.md b/pallets/ring/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..bf89c60f358854b0b2bc32919b785af420b2540b
--- /dev/null
+++ b/pallets/ring/README.md
@@ -0,0 +1,15 @@
+# Pallet ring
+
+## Claiming
+
+## Unclaimed transactions
+
+## Security
+
+"Don't run your own crypto"
+
+This pallet uses the crate `orodruin` which has not been reviewed by experts. The risk is limited however:
+
+* Runtime execution is public and does not contain any secret. Hence no leak is possible here.
+* Client-side (wallet) execution is not part of a low-latency network protocol and should be run on a trusted system.
+* If the signature scheme or its implementation is broken, an attacker can claim all the unclaimed transactions but no more.
diff --git a/pallets/ring/src/lib.rs b/pallets/ring/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cf0243e63ccbf6e930c486655345cc0af9e90141
--- /dev/null
+++ b/pallets/ring/src/lib.rs
@@ -0,0 +1,396 @@
+// Copyright 2024 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod types;
+
+pub use pallet::*;
+
+use frame_support::{
+    pallet_prelude::Weight,
+    traits::{
+        fungible::{self, hold, Mutate, MutateHold},
+        tokens::{Fortitude, Precision, Restriction},
+        StorageVersion,
+    },
+};
+use orodruin::Verifiable;
+use sp_runtime::traits::Zero;
+use sp_std::{convert::TryInto, prelude::*};
+
+pub const MAX_RINGS: u32 = 10;
+pub const RING_SIZE: u32 = 100;
+
+pub trait HandleRingClaim<AccountId, Amount> {
+    /// Handle a valid ring claim
+    ///
+    /// This function should transfer "amount" from "from"'s held funds (reserved amount) to "to".
+    fn handle_ring_claim(from: AccountId, to: AccountId, amount: Amount) -> Weight;
+}
+
+impl<AccountId, Amount> HandleRingClaim<AccountId, Amount> for () {
+    fn handle_ring_claim(_from: AccountId, _to: AccountId, _amount: Amount) -> Weight {
+        Weight::zero()
+    }
+}
+
+#[frame_support::pallet()]
+pub mod pallet {
+    use super::*;
+    use frame_support::pallet_prelude::*;
+    use frame_system::pallet_prelude::*;
+    pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
+    pub type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
+
+    #[pallet::composite_enum]
+    pub enum HoldReason {
+        /// The funds are released because the account quit the ring and won't participate to the next mixing session.
+        RingCancel,
+        /// The funds are claimed anonymously in a ring mixing session.
+        RingClaim,
+        /// The funds are held for the next ring mixing session.
+        RingSignUp,
+        /// Some funds have not been claimed in the last mixing sesion.
+        RingUnclaimed,
+    }
+
+    /// The current storage version.
+    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
+
+    #[pallet::pallet]
+    #[pallet::storage_version(STORAGE_VERSION)]
+    #[pallet::without_storage_info]
+    pub struct Pallet<T>(PhantomData<T>);
+    #[pallet::config]
+    pub trait Config:
+        frame_system::Config + pallet_authorship::Config + pallet_balances::Config
+    {
+        /// Currency type used in this pallet (used for reserve/slash)
+        type Currency: Mutate<Self::AccountId>
+            + MutateHold<Self::AccountId, Reason = <Self as pallet::Config>::RuntimeHoldReason>
+            + hold::Balanced<Self::AccountId>;
+        type RingClaimHandler: HandleRingClaim<Self::AccountId, BalanceOf<Self>>;
+        /// Overarching hold reason.
+        type RuntimeHoldReason: From<HoldReason>;
+        /// The overarching event type.
+        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+        /// Duration of a mixing session in number of blocks.
+        #[pallet::constant]
+        type MixPeriod: Get<u32>;
+    }
+
+    // STORAGE //
+
+    // TODO better name!
+    #[pallet::storage]
+    #[pallet::getter(fn signups)]
+    pub type Signups<T: Config> = StorageMap<
+        _,
+        Twox64Concat,
+        (types::RingSessionIndex, BalanceOf<T>),
+        BoundedBTreeMap<
+            types::RingPubkey,
+            <T as frame_system::Config>::AccountId,
+            ConstU32<RING_SIZE>,
+        >,
+        OptionQuery,
+    >;
+
+    #[pallet::storage]
+    #[pallet::getter(fn claims)]
+    pub type Claims<T: Config> =
+        StorageMap<_, Twox64Concat, (BalanceOf<T>, types::RingKeyImage), (), OptionQuery>;
+
+    ///
+    #[pallet::storage]
+    #[pallet::getter(fn next_claim)]
+    pub type NextClaim<T: Config> =
+        StorageMap<_, Twox64Concat, BalanceOf<T>, types::RingPubkey, OptionQuery>;
+
+    #[pallet::storage]
+    #[pallet::getter(fn current_ring_session_index)]
+    pub type CurrentRingSessionIndex<T: Config> =
+        StorageValue<_, types::RingSessionIndex, ValueQuery>;
+
+    // EVENTS //
+
+    #[pallet::event]
+    #[pallet::generate_deposit(pub(super) fn deposit_event)]
+    pub enum Event<T: Config> {
+        Signup {
+            account: <T as frame_system::Config>::AccountId,
+            amount: BalanceOf<T>,
+        },
+    }
+
+    // ERRORS //
+
+    #[pallet::error]
+    pub enum Error<T> {
+        /// The ring signature is invalid or malformed
+        InvalidRingSignature,
+        /// The ring public key has already signed up for the same amount
+        RingPubkeyAlreadySignedUp,
+        /// The ring public key associated with the given amount is not in the current ring
+        NotInRing,
+        /// The key image already claimed its transaction
+        AlreadyClaimed,
+        /// The maximum number of keys already joined this ring
+        RingFull,
+    }
+
+    // HOOKS //
+
+    #[pallet::hooks]
+    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
+        fn on_initialize(block: BlockNumberFor<T>) -> Weight
+        where
+            BlockNumberFor<T>: From<u32>,
+        {
+            // Sweep unclaimed transactions
+            if (block % BlockNumberFor::<T>::from(T::MixPeriod::get())).is_zero() {
+                let current_ring_session_index = CurrentRingSessionIndex::<T>::mutate(|o| {
+                    o.switch();
+                    o.clone()
+                });
+                let _ = Claims::<T>::clear(RING_SIZE, None);
+                for (amount, ring_pubkey) in NextClaim::<T>::drain() {
+                    if let Some(signups) =
+                        Signups::<T>::take((current_ring_session_index.clone(), amount))
+                    {
+                        for (_source_ring_pubkey, source) in signups.range(ring_pubkey..) {
+                            let _ = <T::Currency as hold::Balanced<_>>::slash(
+                                &HoldReason::RingUnclaimed.into(),
+                                source,
+                                amount,
+                            );
+                        }
+                    }
+                }
+            }
+            Weight::zero()
+        }
+
+        fn on_finalize(_n: BlockNumberFor<T>) {}
+    }
+
+    // CALLS //
+
+    #[pallet::call]
+    impl<T: Config> Pallet<T> {
+        /// Sign up to the next mix
+        ///
+        /// The amount is reserved on the origin account, and can be transfered anonymously during the next mix.
+        /// The ring pubkey **must** be generated from a random, one-time private key.
+        /// Re-using a pubkey will hinder your anonymity.
+        /// The same origin can sign up multiple times (with different pubkeys) in the same ring.
+        #[pallet::call_index(0)]
+        #[pallet::weight(0)]
+        pub fn signup(
+            origin: OriginFor<T>,
+            ring_pubkey: types::RingPubkey,
+            amount: BalanceOf<T>,
+        ) -> DispatchResultWithPostInfo {
+            // TODO check number of rings
+            let who = ensure_signed(origin)?;
+
+            Signups::<T>::try_mutate((CurrentRingSessionIndex::<T>::get(), amount), |entry| {
+                if let Some(signups) = entry {
+                    log::info!("RING: signup if");
+                    if signups.contains_key(&ring_pubkey) {
+                        return Err(Error::<T>::RingPubkeyAlreadySignedUp.into());
+                    }
+                    signups
+                        .try_insert(ring_pubkey, who.clone())
+                        .map_err(|_e| Error::<T>::RingFull)?;
+                } else {
+                    log::info!("RING: signup else");
+                    let mut signups = BoundedBTreeMap::new();
+                    signups
+                        .try_insert(ring_pubkey, who.clone())
+                        .map_err(|_e| Error::<T>::RingFull)?;
+                    *entry = Some(signups);
+                }
+                log::info!("RING: after if");
+                T::Currency::hold(&HoldReason::RingSignUp.into(), &who, amount)?;
+                log::info!("RING: after hold");
+                Self::deposit_event(Event::Signup {
+                    account: who,
+                    amount,
+                });
+                Ok(().into())
+            })
+        }
+
+        /// Cancel a sign up
+        ///
+        /// Unreserve the amount from the origin's account and remove it from the current ring.
+        #[pallet::call_index(1)]
+        #[pallet::weight(0)]
+        pub fn cancel(
+            origin: OriginFor<T>,
+            ring_pubkey: types::RingPubkey,
+            amount: BalanceOf<T>,
+        ) -> DispatchResultWithPostInfo {
+            let who = ensure_signed(origin)?;
+
+            Signups::<T>::mutate_exists((CurrentRingSessionIndex::<T>::get(), amount), |entry| {
+                if let Some(signups) = entry {
+                    if let Some(account) = signups.get(&ring_pubkey) {
+                        ensure!(*account == who, Error::<T>::NotInRing);
+                        signups.remove(&ring_pubkey);
+                        T::Currency::release(
+                            &HoldReason::RingCancel.into(),
+                            &who,
+                            amount,
+                            Precision::Exact,
+                        )?;
+                    } else {
+                        return Err(Error::<T>::NotInRing.into());
+                    }
+                    if signups.is_empty() {
+                        *entry = None;
+                    }
+                    Ok(().into())
+                } else {
+                    Err(Error::<T>::NotInRing.into())
+                }
+            })
+        }
+
+        #[pallet::call_index(2)]
+        #[pallet::weight(0)]
+        pub fn claim(
+            origin: OriginFor<T>,
+            recipient: T::AccountId,
+            amount: BalanceOf<T>,
+            signature: types::RingSignature,
+        ) -> DispatchResultWithPostInfo {
+            ensure_none(origin)?;
+
+            // Has this key claimed already?
+            if Claims::<T>::contains_key((amount, signature.key_image.clone())) {
+                return Err(Error::<T>::AlreadyClaimed.into());
+            }
+            let (current_ring_session_index, key_image, signups) =
+                Self::check_claim(&recipient, &amount, signature)?;
+
+            Claims::<T>::insert((amount, key_image), ());
+            let mut next_claims = signups.range(NextClaim::<T>::get(amount).unwrap_or_default()..);
+            let Some((_source_ring_pubkey, source)) = next_claims.next() else {
+                // This should be unreachable, because:
+                // * the key has not already claimed
+                // * the signature is valid
+                // * hence there still exist unclaimed signups
+                // But in case the signature is broken, we want to avoid unlimited claiming.
+                return Err(Error::<T>::AlreadyClaimed.into());
+            };
+            T::Currency::transfer_on_hold(
+                &HoldReason::RingClaim.into(),
+                source,
+                &recipient,
+                amount,
+                Precision::Exact,
+                Restriction::Free,
+                Fortitude::Force,
+            )?;
+
+            if let Some((k, _v)) = next_claims.next() {
+                NextClaim::<T>::set(amount, Some(k.clone()));
+            } else {
+                NextClaim::<T>::set(amount, None);
+                Signups::<T>::remove((current_ring_session_index.clone(), amount));
+            }
+
+            Ok(().into())
+        }
+    }
+
+    // PUBLIC FUNCTIONS //
+
+    impl<T: Config> Pallet<T> {
+        pub fn check_claim(
+            recipient: &T::AccountId,
+            amount: &BalanceOf<T>,
+            signature: types::RingSignature,
+        ) -> Result<
+            (
+                types::RingSessionIndex,
+                types::RingKeyImage,
+                BoundedBTreeMap<types::RingPubkey, T::AccountId, ConstU32<RING_SIZE>>,
+            ),
+            Error<T>,
+        > {
+            let mut current_ring_session_index = CurrentRingSessionIndex::<T>::get();
+            current_ring_session_index.switch();
+
+            // Get last session's signups
+            let Some(signups) = Signups::<T>::get((current_ring_session_index.clone(), amount))
+            else {
+                return Err(Error::<T>::InvalidRingSignature);
+            };
+
+            // Verify ring size
+            ensure!(
+                signups.len() == signature.responses.len(),
+                Error::<T>::InvalidRingSignature
+            );
+
+            let mut params = blake2b_simd::Params::new();
+            params.hash_length(64);
+            let key_image = signature.key_image.clone();
+
+            // Decode the signature and iterate through all the ring members to verify it
+            TryInto::<orodruin::Signature<Vec<curve25519_dalek::Scalar>>>::try_into(signature)
+                .map_err(|_e| Error::<T>::InvalidRingSignature)?
+                .verify(
+                    signups.keys().map(Into::<orodruin::PublicKey>::into),
+                    &types::RingClaimPayload {
+                        amount,
+                        recipient: recipient.clone(),
+                    }
+                    .encode(),
+                    &mut orodruin::blake2b::Blake2b::from_params(&params),
+                )
+                .map_err(|_e| Error::<T>::InvalidRingSignature)?;
+
+            Ok((current_ring_session_index, key_image, signups))
+        }
+    }
+
+    #[pallet::validate_unsigned]
+    impl<T: Config> ValidateUnsigned for Pallet<T> {
+        type Call = Call<T>;
+
+        fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
+            if let Call::claim {
+                recipient,
+                amount,
+                signature,
+            } = call
+            {
+                /*if <Pallet<T>>::is_online(heartbeat.authority_index) {
+                    // we already received a heartbeat for this authority
+                    return InvalidTransaction::Stale.into();
+                }*/
+                InvalidTransaction::Stale.into()
+            } else {
+                InvalidTransaction::Stale.into()
+            }
+        }
+    }
+}
diff --git a/pallets/ring/src/types.rs b/pallets/ring/src/types.rs
new file mode 100644
index 0000000000000000000000000000000000000000..01b00cf3d85f0f02eac7b5d8a22a6ab70afe8781
--- /dev/null
+++ b/pallets/ring/src/types.rs
@@ -0,0 +1,38 @@
+// Copyright 2024 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+use codec::{Decode, Encode};
+use frame_support::pallet_prelude::*;
+
+pub use sp_ring::{
+    RingClaimPayload, RingKeyImage, RingPubkey, RingSignature, RingSignatureDecodeError,
+};
+
+#[derive(Clone, Debug, Decode, Default, Encode, PartialEq, TypeInfo)]
+pub enum RingSessionIndex {
+    #[default]
+    Zero = 0,
+    One = 1,
+}
+
+impl RingSessionIndex {
+    pub fn switch(&mut self) {
+        *self = match self {
+            Self::Zero => Self::One,
+            Self::One => Self::Zero,
+        }
+    }
+}
diff --git a/primitives/ring/Cargo.toml b/primitives/ring/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..058faf124975fb5675d7bd2c2319a4c33b71f56a
--- /dev/null
+++ b/primitives/ring/Cargo.toml
@@ -0,0 +1,47 @@
+[package]
+authors.workspace = true
+description = "primitives for pallet ring"
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+name = "sp-ring"
+readme = "README.md"
+repository.workspace = true
+version.workspace = true
+
+[package.metadata.docs.rs]
+default-features = false
+targets = ["x86_64-unknown-linux-gnu"]
+
+[features]
+default = ["std"]
+std = [
+	"async-trait",
+	"blake2b_simd",
+	"codec/std",
+	"frame-support/std",
+	"orodruin/alloc",
+	"orodruin/zeroize",
+	"rand_core",
+	"scale-info/std",
+	"serde/std",
+	"sp-runtime/std",
+	"sp-std/std",
+	"thiserror",
+]
+try-runtime = ["frame-support/try-runtime", "sp-runtime/try-runtime"]
+runtime-benchmarks = []
+
+[dependencies]
+async-trait = { workspace = true, optional = true }
+blake2b_simd = { workspace = true, optional = true }
+codec = { workspace = true, features = ["derive"] }
+curve25519-dalek = { workspace = true }
+frame-support = { workspace = true }
+orodruin = { workspace = true }
+rand_core = { workspace = true, optional = true }
+scale-info = { workspace = true, features = ["derive"] }
+serde = { workspace = true, features = ["derive"] }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+thiserror = { workspace = true, optional = true }
diff --git a/primitives/ring/src/lib.rs b/primitives/ring/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..57d026d14bf9761b53c188cccab4a97c482aad77
--- /dev/null
+++ b/primitives/ring/src/lib.rs
@@ -0,0 +1,143 @@
+// Copyright 2024 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+//! Defines types and traits for users of pallet distance.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![allow(clippy::type_complexity)]
+
+use codec::{Decode, Encode};
+use curve25519_dalek::{ristretto::CompressedRistretto, Scalar};
+use scale_info::TypeInfo;
+#[cfg(not(feature = "std"))]
+use sp_std::vec::Vec;
+
+pub use orodruin::SecretKey;
+
+/// Temporary public key for ring signatures
+///
+/// This encodes a compressed Ristretto point.
+#[derive(Clone, Debug, Decode, Default, Encode, Eq, Ord, PartialEq, PartialOrd, TypeInfo)]
+pub struct RingPubkey(pub [u8; 32]);
+
+impl From<orodruin::PublicKey> for RingPubkey {
+    fn from(public_key: orodruin::PublicKey) -> Self {
+        Self(public_key.0 .0)
+    }
+}
+
+impl From<RingPubkey> for orodruin::PublicKey {
+    fn from(ring_pubkey: RingPubkey) -> Self {
+        Self(curve25519_dalek::ristretto::CompressedRistretto(
+            ring_pubkey.0,
+        ))
+    }
+}
+
+impl From<&orodruin::PublicKey> for RingPubkey {
+    fn from(public_key: &orodruin::PublicKey) -> Self {
+        Self(public_key.0 .0)
+    }
+}
+
+impl From<&RingPubkey> for orodruin::PublicKey {
+    fn from(ring_pubkey: &RingPubkey) -> Self {
+        Self(curve25519_dalek::ristretto::CompressedRistretto(
+            ring_pubkey.0,
+        ))
+    }
+}
+
+/// Deterministic image of a ring private key
+#[derive(Clone, Debug, Decode, Default, Encode, PartialEq, TypeInfo)]
+pub struct RingKeyImage(pub [u8; 32]);
+
+#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
+pub struct RingSignature {
+    pub challenge: [u8; 32],
+    pub key_image: RingKeyImage,
+    pub responses: Vec<[u8; 32]>,
+}
+
+#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
+pub enum RingSignatureDecodeError {
+    MalformedResponse,
+    MalformedChallenge,
+}
+
+impl TryInto<orodruin::Signature<Vec<Scalar>>> for RingSignature {
+    type Error = RingSignatureDecodeError;
+
+    fn try_into(self) -> Result<orodruin::Signature<Vec<Scalar>>, RingSignatureDecodeError> {
+        let nb_responses = self.responses.len();
+        let responses: Vec<Scalar> = self
+            .responses
+            .into_iter()
+            .map_while(|r| Scalar::from_canonical_bytes(r).into())
+            .collect();
+        if nb_responses != responses.len() {
+            return Err(RingSignatureDecodeError::MalformedResponse);
+        }
+        Ok(orodruin::Signature {
+            challenge: Option::<_>::from(Scalar::from_canonical_bytes(self.challenge))
+                .ok_or(RingSignatureDecodeError::MalformedChallenge)?,
+            key_image: orodruin::KeyImage(CompressedRistretto(self.key_image.0)),
+            responses,
+        })
+    }
+}
+
+impl Into<RingSignature> for orodruin::Signature<Vec<Scalar>> {
+    fn into(self) -> RingSignature {
+        RingSignature {
+            key_image: RingKeyImage(self.key_image.0 .0),
+            challenge: self.challenge.to_bytes(),
+            responses: self.responses.into_iter().map(|r| r.to_bytes()).collect(),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
+pub struct RingClaimPayload<Address, Balance> {
+    pub amount: Balance,
+    pub recipient: Address,
+}
+
+#[cfg(feature = "std")]
+#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
+pub enum SignError {
+    NotInRing,
+}
+
+#[cfg(feature = "std")]
+pub fn sign(
+    sk: SecretKey,
+    ring: &[RingPubkey],
+    message: &[u8],
+    rng: impl rand_core::CryptoRng + rand_core::RngCore,
+) -> Result<RingSignature, SignError> {
+    let pubkey = sk.public_key();
+    let secret_index = ring
+        .iter()
+        .enumerate()
+        .find_map(|(i, pk)| if pk.0 == pubkey.0 .0 { Some(i) } else { None })
+        .ok_or(SignError::NotInRing)?;
+
+    let mut params = blake2b_simd::Params::new();
+    params.hash_length(64);
+    let mut hasher = orodruin::blake2b::Blake2b::from_params(&params);
+    Ok(orodruin::sign(sk, ring, secret_index, message, rng, &mut hasher).into())
+}
diff --git a/resources/metadata.scale b/resources/metadata.scale
index 7b3fc4e7c424afabf32ed020c7739a4ab0be673f..eafc0c350b694cf769478f32494ea6374fdcf9db 100644
Binary files a/resources/metadata.scale and b/resources/metadata.scale differ
diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml
index 8b401fb4e5bcadd5a32963e4c05aa5cf436993f5..ff49af779a2b88ec6994c1062a3f40289dc3fbc0 100644
--- a/runtime/common/Cargo.toml
+++ b/runtime/common/Cargo.toml
@@ -76,6 +76,7 @@ std = [
 	"pallet-provide-randomness/std",
 	"pallet-proxy/std",
 	"pallet-quota/std",
+	"pallet-ring/std",
 	"pallet-scheduler/std",
 	"pallet-session/std",
 	"pallet-smith-members/std",
@@ -163,6 +164,7 @@ pallet-preimage = { workspace = true }
 pallet-provide-randomness = { workspace = true }
 pallet-proxy = { workspace = true }
 pallet-quota = { workspace = true }
+pallet-ring = { workspace = true }
 pallet-scheduler = { workspace = true }
 pallet-session = { workspace = true }
 pallet-smith-members = { workspace = true }
diff --git a/runtime/common/src/apis.rs b/runtime/common/src/apis.rs
index 7347a6ab6d73d1340838de421925ea33e69c3e36..6d070f2d756885fbe69f7221c89ecd965ce3de80 100644
--- a/runtime/common/src/apis.rs
+++ b/runtime/common/src/apis.rs
@@ -140,6 +140,16 @@ macro_rules! runtime_apis {
                     {
                         return sp_runtime::transaction_validity::InvalidTransaction::Call.into();
                     }
+                    // Since ring signatures are expensive to verify, we verify offline first
+                    if let RuntimeCall::Ring(pallet_ring::Call::claim {
+                        recipient,
+                        amount,
+                        signature,
+                    }) = &tx.function {
+                        if pallet_ring::Pallet::<Runtime>::check_claim(&recipient, &amount, signature.clone()).is_err() {
+                            return sp_runtime::transaction_validity::InvalidTransaction::Call.into();
+                        }
+                    }
                     Executive::validate_transaction(source, tx, block_hash)
                 }
             }
diff --git a/runtime/common/src/handlers.rs b/runtime/common/src/handlers.rs
index fa247d1c5ccd530cd4c749b2bd0f9034ce7c1949..8348c31c77428fc0ff45da45004a86f86704fedf 100644
--- a/runtime/common/src/handlers.rs
+++ b/runtime/common/src/handlers.rs
@@ -165,3 +165,28 @@ where
         }
     }
 }
+
+pub struct RingClaimHandler<Runtime>(core::marker::PhantomData<Runtime>);
+impl<Runtime> pallet_ring::HandleRingClaim<Runtime::AccountId, Runtime::Balance>
+    for RingClaimHandler<Runtime>
+where
+    Runtime: pallet_ring::Config + pallet_oneshot_account::Config,
+    Runtime::Balance: Into<<<Runtime as pallet_oneshot_account::Config>::Currency as frame_support::traits::fungible::Inspect<Runtime::AccountId>>::Balance>,
+{
+    fn handle_ring_claim(
+        _from: Runtime::AccountId,
+        to: Runtime::AccountId,
+        amount: Runtime::Balance,
+    ) -> Weight {
+        if pallet_oneshot_account::OneshotAccounts::<Runtime>::get(&to).is_none() {
+            pallet_oneshot_account::Pallet::<Runtime>::do_create_oneshot_account(
+                to.clone(),
+                to.clone(),
+                amount.into(),
+            );
+        }
+        // TODO else go to treasury
+
+        Weight::zero()
+    }
+}
diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs
index b376b12d87490c963061e5f1dd8114f476527b36..332c2dab4ceb8940c451c5c2343501fd4f5e78b6 100644
--- a/runtime/common/src/pallets_config.rs
+++ b/runtime/common/src/pallets_config.rs
@@ -210,6 +210,16 @@ macro_rules! pallets_config {
             type RuntimeEvent = RuntimeEvent;
             type WeightInfo = weights::pallet_oneshot_account::WeightInfo<Runtime>;
         }
+        parameter_types! {
+            pub const RingMixPeriod: BlockNumber = 7 * DAYS;
+        }
+        impl pallet_ring::Config for Runtime {
+            type Currency = Balances;
+            type MixPeriod = RingMixPeriod;
+            type RingClaimHandler = ();
+            type RuntimeEvent = RuntimeEvent;
+            type RuntimeHoldReason = RuntimeHoldReason; // 1 week
+        }
 
         // CONSENSUS  //
 
diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml
index a33d3a13485f0c8aff00dbb186f83ccaa081fe2f..c8c16cc0d45ab2f1d29f18bca50b5c358114ad85 100644
--- a/runtime/g1/Cargo.toml
+++ b/runtime/g1/Cargo.toml
@@ -85,6 +85,7 @@ std = [
 	"pallet-provide-randomness/std",
 	"pallet-proxy/std",
 	"pallet-quota/std",
+	"pallet-ring/std",
 	"pallet-scheduler/std",
 	"pallet-session-benchmarking/std",
 	"pallet-session/std",
@@ -206,6 +207,7 @@ pallet-preimage = { workspace = true }
 pallet-provide-randomness = { workspace = true }
 pallet-proxy = { workspace = true }
 pallet-quota = { workspace = true }
+pallet-ring = { workspace = true }
 pallet-scheduler = { workspace = true }
 pallet-session = { workspace = true }
 pallet-smith-members = { workspace = true }
diff --git a/runtime/g1/src/lib.rs b/runtime/g1/src/lib.rs
index 865c524eb7546a81bc45ed65b89f226acc79343d..c853c237f52ee217b2a9f963d72e170cb9f2b08b 100644
--- a/runtime/g1/src/lib.rs
+++ b/runtime/g1/src/lib.rs
@@ -265,6 +265,7 @@ construct_runtime!(
         TransactionPayment: pallet_transaction_payment = 32,
         OneshotAccount: pallet_oneshot_account = 7,
         Quota: pallet_quota = 66,
+        Ring: pallet_ring = 67,
 
         // Consensus support
         SmithMembers: pallet_smith_members = 10,
diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml
index 4ed92bd07a90f1a17825b47ac577788234368ea1..52763dabed66f3b3f912f89dfb7ca50e81e73bd6 100644
--- a/runtime/gdev/Cargo.toml
+++ b/runtime/gdev/Cargo.toml
@@ -86,6 +86,7 @@ std = [
 	"pallet-provide-randomness/std",
 	"pallet-proxy/std",
 	"pallet-quota/std",
+	"pallet-ring/std",
 	"pallet-scheduler/std",
 	"pallet-session-benchmarking/std",
 	"pallet-session/std",
@@ -212,6 +213,7 @@ pallet-preimage = { workspace = true }
 pallet-provide-randomness = { workspace = true }
 pallet-proxy = { workspace = true }
 pallet-quota = { workspace = true }
+pallet-ring = { workspace = true }
 pallet-scheduler = { workspace = true }
 pallet-session = { workspace = true }
 pallet-session-benchmarking = { workspace = true }
diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs
index bfd9a714a52eddd03faad229dd626ea0e8de6325..69aa81152a3047d3ef7a7c1ad7888c2c6b7e6b9a 100644
--- a/runtime/gdev/src/lib.rs
+++ b/runtime/gdev/src/lib.rs
@@ -300,6 +300,7 @@ construct_runtime!(
         TransactionPayment: pallet_transaction_payment = 32,
         OneshotAccount: pallet_oneshot_account = 7,
         Quota: pallet_quota = 66,
+        Ring: pallet_ring = 67,
 
         // Consensus support
         SmithMembers: pallet_smith_members = 10,
diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml
index 945a1dee8dbf9bb75e61f0400ba66e3017a61549..3f513e0626b36d177cf3ea6bb9cba6cb59e71839 100644
--- a/runtime/gtest/Cargo.toml
+++ b/runtime/gtest/Cargo.toml
@@ -84,6 +84,7 @@ std = [
 	"pallet-provide-randomness/std",
 	"pallet-proxy/std",
 	"pallet-quota/std",
+	"pallet-ring/std",
 	"pallet-scheduler/std",
 	"pallet-session-benchmarking/std",
 	"pallet-session/std",
@@ -204,6 +205,7 @@ pallet-preimage = { workspace = true }
 pallet-provide-randomness = { workspace = true }
 pallet-proxy = { workspace = true }
 pallet-quota = { workspace = true }
+pallet-ring = { workspace = true }
 pallet-scheduler = { workspace = true }
 pallet-session = { workspace = true }
 pallet-smith-members = { workspace = true }
diff --git a/runtime/gtest/src/lib.rs b/runtime/gtest/src/lib.rs
index 356b288acdbac2d079f48ddf109415584fe51327..6e680dc47a9f60ad083882b3cdbde878024e1af6 100644
--- a/runtime/gtest/src/lib.rs
+++ b/runtime/gtest/src/lib.rs
@@ -264,6 +264,7 @@ construct_runtime!(
         TransactionPayment: pallet_transaction_payment = 32,
         OneshotAccount: pallet_oneshot_account = 7,
         Quota: pallet_quota = 66,
+        Ring: pallet_ring = 67,
 
         // Consensus support
         SmithMembers: pallet_smith_members = 10,