From c570da10de4c13742d06bd778f5f1974dfef8f14 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pascal=20Eng=C3=A9libert?= <tuxmain@zettascript.org>
Date: Sat, 7 Sep 2024 13:48:51 +0200
Subject: [PATCH] wip

---
 Cargo.lock                                   |  59 +++
 Cargo.toml                                   |  18 +-
 end2end-tests/Cargo.toml                     |   4 +
 end2end-tests/cucumber-features/ring.feature |  17 +
 end2end-tests/cucumber-genesis/default.json  |   5 +-
 end2end-tests/tests/common/mod.rs            |  10 +-
 end2end-tests/tests/common/ring.rs           | 209 ++++++++++
 end2end-tests/tests/cucumber_tests.rs        |  46 +++
 pallets/oneshot-account/src/lib.rs           |  47 ++-
 pallets/ring/Cargo.toml                      |  70 ++++
 pallets/ring/README.md                       |  15 +
 pallets/ring/src/lib.rs                      | 396 +++++++++++++++++++
 pallets/ring/src/types.rs                    |  38 ++
 primitives/ring/Cargo.toml                   |  47 +++
 primitives/ring/src/lib.rs                   | 143 +++++++
 resources/metadata.scale                     | Bin 149703 -> 139074 bytes
 runtime/common/Cargo.toml                    |   2 +
 runtime/common/src/apis.rs                   |  10 +
 runtime/common/src/handlers.rs               |  25 ++
 runtime/common/src/pallets_config.rs         |  10 +
 runtime/g1/Cargo.toml                        |   2 +
 runtime/g1/src/lib.rs                        |   1 +
 runtime/gdev/Cargo.toml                      |   2 +
 runtime/gdev/src/lib.rs                      |   1 +
 runtime/gtest/Cargo.toml                     |   2 +
 runtime/gtest/src/lib.rs                     |   1 +
 26 files changed, 1152 insertions(+), 28 deletions(-)
 create mode 100644 end2end-tests/cucumber-features/ring.feature
 create mode 100644 end2end-tests/tests/common/ring.rs
 create mode 100644 pallets/ring/Cargo.toml
 create mode 100644 pallets/ring/README.md
 create mode 100644 pallets/ring/src/lib.rs
 create mode 100644 pallets/ring/src/types.rs
 create mode 100644 primitives/ring/Cargo.toml
 create mode 100644 primitives/ring/src/lib.rs

diff --git a/Cargo.lock b/Cargo.lock
index 7c17d76a7..46d4c06f5 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 ccafea017..91d0a1339 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 d045afbb8..c3da2bd8a 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 000000000..956e075aa
--- /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 2912937ff..552a17354 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 6ed0e8375..12c1fa066 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 000000000..ce99abc0b
--- /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 add3f14a0..fd351e4a2 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 3bef8281a..f151081f8 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 000000000..0e40846b5
--- /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 000000000..bf89c60f3
--- /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 000000000..cf0243e63
--- /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 000000000..01b00cf3d
--- /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 000000000..058faf124
--- /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 000000000..57d026d14
--- /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
GIT binary patch
delta 9642
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
X2dM~{Jz0CE*R5cbWc6k3abyJmd%l}G

delta 20122
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
Ji~&no8308G(M<pV

diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml
index 8b401fb4e..ff49af779 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 7347a6ab6..6d070f2d7 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 fa247d1c5..8348c31c7 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 b376b12d8..332c2dab4 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 a33d3a134..c8c16cc0d 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 865c524eb..c853c237f 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 4ed92bd07..52763dabe 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 bfd9a714a..69aa81152 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 945a1dee8..3f513e062 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 356b288ac..6e680dc47 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,
-- 
GitLab