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(¶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 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(¶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 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