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(¶ms), + ) + .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(¶ms); + 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,