Skip to content
Snippets Groups Projects
Commit c570da10 authored by Pascal Engélibert's avatar Pascal Engélibert :bicyclist:
Browse files

wip

parent 6dec13d1
No related branches found
No related tags found
No related merge requests found
Pipeline #37865 waiting for manual action
Showing
with 1143 additions and 28 deletions
...@@ -1536,6 +1536,7 @@ dependencies = [ ...@@ -1536,6 +1536,7 @@ dependencies = [
"pallet-provide-randomness", "pallet-provide-randomness",
"pallet-proxy", "pallet-proxy",
"pallet-quota", "pallet-quota",
"pallet-ring",
"pallet-scheduler", "pallet-scheduler",
"pallet-session", "pallet-session",
"pallet-smith-members", "pallet-smith-members",
...@@ -1997,6 +1998,7 @@ dependencies = [ ...@@ -1997,6 +1998,7 @@ dependencies = [
"curve25519-dalek-derive", "curve25519-dalek-derive",
"digest 0.10.7", "digest 0.10.7",
"fiat-crypto", "fiat-crypto",
"rand_core",
"rustc_version", "rustc_version",
"subtle 2.6.1", "subtle 2.6.1",
"zeroize", "zeroize",
...@@ -2570,10 +2572,12 @@ dependencies = [ ...@@ -2570,10 +2572,12 @@ dependencies = [
"notify-debouncer-mini", "notify-debouncer-mini",
"parity-scale-codec", "parity-scale-codec",
"portpicker", "portpicker",
"rand",
"serde_json", "serde_json",
"sp-core", "sp-core",
"sp-core-hashing", "sp-core-hashing",
"sp-keyring", "sp-keyring",
"sp-ring",
"sp-runtime", "sp-runtime",
"subxt", "subxt",
"tokio", "tokio",
...@@ -3540,6 +3544,7 @@ dependencies = [ ...@@ -3540,6 +3544,7 @@ dependencies = [
"pallet-provide-randomness", "pallet-provide-randomness",
"pallet-proxy", "pallet-proxy",
"pallet-quota", "pallet-quota",
"pallet-ring",
"pallet-scheduler", "pallet-scheduler",
"pallet-session", "pallet-session",
"pallet-session-benchmarking", "pallet-session-benchmarking",
...@@ -3616,6 +3621,7 @@ dependencies = [ ...@@ -3616,6 +3621,7 @@ dependencies = [
"pallet-provide-randomness", "pallet-provide-randomness",
"pallet-proxy", "pallet-proxy",
"pallet-quota", "pallet-quota",
"pallet-ring",
"pallet-scheduler", "pallet-scheduler",
"pallet-session", "pallet-session",
"pallet-session-benchmarking", "pallet-session-benchmarking",
...@@ -3950,6 +3956,7 @@ dependencies = [ ...@@ -3950,6 +3956,7 @@ dependencies = [
"pallet-provide-randomness", "pallet-provide-randomness",
"pallet-proxy", "pallet-proxy",
"pallet-quota", "pallet-quota",
"pallet-ring",
"pallet-scheduler", "pallet-scheduler",
"pallet-session", "pallet-session",
"pallet-session-benchmarking", "pallet-session-benchmarking",
...@@ -6642,6 +6649,16 @@ version = "0.2.0" ...@@ -6642,6 +6649,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orodruin"
version = "0.1.0"
dependencies = [
"blake2b_simd",
"curve25519-dalek",
"rand_core",
"zeroize",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
...@@ -7079,6 +7096,30 @@ dependencies = [ ...@@ -7079,6 +7096,30 @@ dependencies = [
"sp-std 14.0.0", "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]] [[package]]
name = "pallet-scheduler" name = "pallet-scheduler"
version = "29.0.0" version = "29.0.0"
...@@ -11349,6 +11390,24 @@ dependencies = [ ...@@ -11349,6 +11390,24 @@ dependencies = [
"regex", "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]] [[package]]
name = "sp-rpc" name = "sp-rpc"
version = "26.0.0" version = "26.0.0"
......
...@@ -17,11 +17,13 @@ members = [ ...@@ -17,11 +17,13 @@ members = [
'pallets/membership', 'pallets/membership',
'pallets/oneshot-account', 'pallets/oneshot-account',
'pallets/quota', 'pallets/quota',
'pallets/ring',
'pallets/smith-members', 'pallets/smith-members',
'pallets/universal-dividend', 'pallets/universal-dividend',
'pallets/upgrade-origin', 'pallets/upgrade-origin',
'primitives/distance', 'primitives/distance',
'primitives/membership', 'primitives/membership',
'primitives/ring',
'resources/weight_analyzer', 'resources/weight_analyzer',
'runtime/common', 'runtime/common',
'runtime/gdev', 'runtime/gdev',
...@@ -102,6 +104,18 @@ simple_logger = { version = "4.3.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 } bincode = { version = "1.3.3", default-features = false }
dubp-wot = { version = "0.11.1", default-features = false } dubp-wot = { version = "0.11.1", default-features = false }
flate2 = { version = "1.0.28", 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
subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.37.0-duniter-substrate-v1.14.0', default-features = false } 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 } ...@@ -128,12 +142,14 @@ pallet-offences = { path = 'pallets/offences', default-features = false }
pallet-oneshot-account = { path = 'pallets/oneshot-account', default-features = false } pallet-oneshot-account = { path = 'pallets/oneshot-account', default-features = false }
pallet-provide-randomness = { path = 'pallets/provide-randomness', default-features = false } pallet-provide-randomness = { path = 'pallets/provide-randomness', default-features = false }
pallet-quota = { path = 'pallets/quota', 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-session-benchmarking = { path = 'pallets/session-benchmarking', default-features = false }
pallet-smith-members = { path = 'pallets/smith-members', default-features = false } pallet-smith-members = { path = 'pallets/smith-members', default-features = false }
pallet-universal-dividend = { path = 'pallets/universal-dividend', default-features = false } pallet-universal-dividend = { path = 'pallets/universal-dividend', default-features = false }
pallet-upgrade-origin = { path = 'pallets/upgrade-origin', default-features = false } pallet-upgrade-origin = { path = 'pallets/upgrade-origin', default-features = false }
sp-distance = { path = 'primitives/distance', default-features = false } sp-distance = { path = 'primitives/distance', default-features = false }
sp-membership = { path = 'primitives/membership', default-features = false } sp-membership = { path = 'primitives/membership', default-features = false }
sp-ring = { path = 'primitives/ring', default-features = false }
# substrate dependencies # substrate dependencies
pallet-transaction-payment-rpc = { git = 'https://github.com/duniter/duniter-polkadot-sdk', branch = 'duniter-substrate-v1.14.0', default-features = false } pallet-transaction-payment-rpc = { git = 'https://github.com/duniter/duniter-polkadot-sdk', branch = 'duniter-substrate-v1.14.0', default-features = false }
......
...@@ -22,6 +22,7 @@ std = [ ...@@ -22,6 +22,7 @@ std = [
"serde_json/std", "serde_json/std",
"sp-core-hashing/std", "sp-core-hashing/std",
"sp-core/std", "sp-core/std",
"sp-ring/std",
"sp-runtime/std", "sp-runtime/std",
] ]
standalone = ["distance-oracle/standalone"] standalone = ["distance-oracle/standalone"]
...@@ -39,11 +40,14 @@ env_logger = { workspace = true } ...@@ -39,11 +40,14 @@ env_logger = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
notify = { workspace = true } notify = { workspace = true }
notify-debouncer-mini = { workspace = true } notify-debouncer-mini = { workspace = true }
#orodruin = { workspace = true, features = ["alloc"] }
portpicker = { workspace = true } portpicker = { workspace = true }
rand = "0.8.5"
serde_json = { workspace = true } serde_json = { workspace = true }
sp-core = { workspace = true } sp-core = { workspace = true }
sp-core-hashing = { workspace = true } sp-core-hashing = { workspace = true }
sp-keyring = { workspace = true } sp-keyring = { workspace = true }
sp-ring = { workspace = true }
sp-runtime = { workspace = true } sp-runtime = { workspace = true }
subxt = { workspace = true, features = [ subxt = { workspace = true, features = [
"substrate-compat", "substrate-compat",
......
@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
...@@ -67,7 +67,8 @@ ...@@ -67,7 +67,8 @@
"smith_wot_min_cert_for_membership": 2, "smith_wot_min_cert_for_membership": 2,
"wot_first_cert_issuable_on": 20, "wot_first_cert_issuable_on": 20,
"wot_min_cert_for_create_idty_right": 2, "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": [ "clique_smiths": [
{ {
......
...@@ -21,12 +21,20 @@ pub mod cert; ...@@ -21,12 +21,20 @@ pub mod cert;
pub mod distance; pub mod distance;
pub mod identity; pub mod identity;
pub mod oneshot; pub mod oneshot;
pub mod ring;
#[subxt::subxt( #[subxt::subxt(
runtime_metadata_path = "../resources/metadata.scale", runtime_metadata_path = "../resources/metadata.scale",
derive_for_all_types = "Eq, PartialEq" 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 anyhow::anyhow;
use codec::Encode; use codec::Encode;
......
// 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(())
}
...@@ -366,6 +366,52 @@ async fn run_distance_oracle(world: &mut DuniterWorld, who: String) -> Result<() ...@@ -366,6 +366,52 @@ async fn run_distance_oracle(world: &mut DuniterWorld, who: String) -> Result<()
.await .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 ==== // ===== then ====
#[allow(clippy::needless_pass_by_ref_mut)] #[allow(clippy::needless_pass_by_ref_mut)]
......
...@@ -162,12 +162,7 @@ pub mod pallet { ...@@ -162,12 +162,7 @@ pub mod pallet {
Preservation::Preserve, Preservation::Preserve,
Fortitude::Polite, Fortitude::Polite,
)?; )?;
OneshotAccounts::<T>::insert(&dest, value); Self::do_create_oneshot_account(transactor.clone(), dest.clone(), value);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest,
balance: value,
creator: transactor,
});
Ok(()) Ok(())
} }
...@@ -208,12 +203,7 @@ pub mod pallet { ...@@ -208,12 +203,7 @@ pub mod pallet {
OneshotAccounts::<T>::get(&dest).is_none(), OneshotAccounts::<T>::get(&dest).is_none(),
Error::<T>::OneshotAccountAlreadyCreated Error::<T>::OneshotAccountAlreadyCreated
); );
OneshotAccounts::<T>::insert(&dest, value); Self::do_create_oneshot_account(transactor.clone(), dest.clone(), value);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest.clone(),
balance: value,
creator: transactor.clone(),
});
} else if frame_system::Pallet::<T>::providers(&dest) > 0 { } else if frame_system::Pallet::<T>::providers(&dest) > 0 {
let _ = T::Currency::deposit(&dest, value, Precision::Exact)?; let _ = T::Currency::deposit(&dest, value, Precision::Exact)?;
} }
...@@ -297,22 +287,12 @@ pub mod pallet { ...@@ -297,22 +287,12 @@ pub mod pallet {
balance2 >= T::Currency::minimum_balance(), balance2 >= T::Currency::minimum_balance(),
Error::<T>::ExistentialDeposit Error::<T>::ExistentialDeposit
); );
OneshotAccounts::<T>::insert(&dest2, balance2); Self::do_create_oneshot_account(transactor.clone(), dest2.clone(), balance2);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest2.clone(),
balance: balance2,
creator: transactor.clone(),
});
} else if frame_system::Pallet::<T>::providers(&dest2) > 0 { } else if frame_system::Pallet::<T>::providers(&dest2) > 0 {
let _ = T::Currency::deposit(&dest2, balance2, Precision::Exact)?; let _ = T::Currency::deposit(&dest2, balance2, Precision::Exact)?;
} }
if dest1_is_oneshot { if dest1_is_oneshot {
OneshotAccounts::<T>::insert(&dest1, balance1); Self::do_create_oneshot_account(transactor.clone(), dest1.clone(), balance1);
Self::deposit_event(Event::OneshotAccountCreated {
account: dest1.clone(),
balance: balance1,
creator: transactor.clone(),
});
} else if frame_system::Pallet::<T>::providers(&dest1) > 0 { } else if frame_system::Pallet::<T>::providers(&dest1) > 0 {
let _ = T::Currency::deposit(&dest1, balance1, Precision::Exact)?; let _ = T::Currency::deposit(&dest1, balance1, Precision::Exact)?;
} }
...@@ -326,6 +306,25 @@ pub mod pallet { ...@@ -326,6 +306,25 @@ pub mod pallet {
Ok(()) 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> impl<T: Config> OnChargeTransaction<T> for Pallet<T>
......
[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 }
# 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.
// Copyright 2024 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
#![cfg_attr(not(feature = "std"), no_std)]
pub mod types;
pub use pallet::*;
use frame_support::{
pallet_prelude::Weight,
traits::{
fungible::{self, hold, Mutate, MutateHold},
tokens::{Fortitude, Precision, Restriction},
StorageVersion,
},
};
use orodruin::Verifiable;
use sp_runtime::traits::Zero;
use sp_std::{convert::TryInto, prelude::*};
pub const MAX_RINGS: u32 = 10;
pub const RING_SIZE: u32 = 100;
pub trait HandleRingClaim<AccountId, Amount> {
/// Handle a valid ring claim
///
/// This function should transfer "amount" from "from"'s held funds (reserved amount) to "to".
fn handle_ring_claim(from: AccountId, to: AccountId, amount: Amount) -> Weight;
}
impl<AccountId, Amount> HandleRingClaim<AccountId, Amount> for () {
fn handle_ring_claim(_from: AccountId, _to: AccountId, _amount: Amount) -> Weight {
Weight::zero()
}
}
#[frame_support::pallet()]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub type BalanceOf<T> = <<T as Config>::Currency as fungible::Inspect<AccountIdOf<T>>>::Balance;
#[pallet::composite_enum]
pub enum HoldReason {
/// The funds are released because the account quit the ring and won't participate to the next mixing session.
RingCancel,
/// The funds are claimed anonymously in a ring mixing session.
RingClaim,
/// The funds are held for the next ring mixing session.
RingSignUp,
/// Some funds have not been claimed in the last mixing sesion.
RingUnclaimed,
}
/// The current storage version.
const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
#[pallet::without_storage_info]
pub struct Pallet<T>(PhantomData<T>);
#[pallet::config]
pub trait Config:
frame_system::Config + pallet_authorship::Config + pallet_balances::Config
{
/// Currency type used in this pallet (used for reserve/slash)
type Currency: Mutate<Self::AccountId>
+ MutateHold<Self::AccountId, Reason = <Self as pallet::Config>::RuntimeHoldReason>
+ hold::Balanced<Self::AccountId>;
type RingClaimHandler: HandleRingClaim<Self::AccountId, BalanceOf<Self>>;
/// Overarching hold reason.
type RuntimeHoldReason: From<HoldReason>;
/// The overarching event type.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Duration of a mixing session in number of blocks.
#[pallet::constant]
type MixPeriod: Get<u32>;
}
// STORAGE //
// TODO better name!
#[pallet::storage]
#[pallet::getter(fn signups)]
pub type Signups<T: Config> = StorageMap<
_,
Twox64Concat,
(types::RingSessionIndex, BalanceOf<T>),
BoundedBTreeMap<
types::RingPubkey,
<T as frame_system::Config>::AccountId,
ConstU32<RING_SIZE>,
>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn claims)]
pub type Claims<T: Config> =
StorageMap<_, Twox64Concat, (BalanceOf<T>, types::RingKeyImage), (), OptionQuery>;
///
#[pallet::storage]
#[pallet::getter(fn next_claim)]
pub type NextClaim<T: Config> =
StorageMap<_, Twox64Concat, BalanceOf<T>, types::RingPubkey, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn current_ring_session_index)]
pub type CurrentRingSessionIndex<T: Config> =
StorageValue<_, types::RingSessionIndex, ValueQuery>;
// EVENTS //
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
Signup {
account: <T as frame_system::Config>::AccountId,
amount: BalanceOf<T>,
},
}
// ERRORS //
#[pallet::error]
pub enum Error<T> {
/// The ring signature is invalid or malformed
InvalidRingSignature,
/// The ring public key has already signed up for the same amount
RingPubkeyAlreadySignedUp,
/// The ring public key associated with the given amount is not in the current ring
NotInRing,
/// The key image already claimed its transaction
AlreadyClaimed,
/// The maximum number of keys already joined this ring
RingFull,
}
// HOOKS //
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(block: BlockNumberFor<T>) -> Weight
where
BlockNumberFor<T>: From<u32>,
{
// Sweep unclaimed transactions
if (block % BlockNumberFor::<T>::from(T::MixPeriod::get())).is_zero() {
let current_ring_session_index = CurrentRingSessionIndex::<T>::mutate(|o| {
o.switch();
o.clone()
});
let _ = Claims::<T>::clear(RING_SIZE, None);
for (amount, ring_pubkey) in NextClaim::<T>::drain() {
if let Some(signups) =
Signups::<T>::take((current_ring_session_index.clone(), amount))
{
for (_source_ring_pubkey, source) in signups.range(ring_pubkey..) {
let _ = <T::Currency as hold::Balanced<_>>::slash(
&HoldReason::RingUnclaimed.into(),
source,
amount,
);
}
}
}
}
Weight::zero()
}
fn on_finalize(_n: BlockNumberFor<T>) {}
}
// CALLS //
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Sign up to the next mix
///
/// The amount is reserved on the origin account, and can be transfered anonymously during the next mix.
/// The ring pubkey **must** be generated from a random, one-time private key.
/// Re-using a pubkey will hinder your anonymity.
/// The same origin can sign up multiple times (with different pubkeys) in the same ring.
#[pallet::call_index(0)]
#[pallet::weight(0)]
pub fn signup(
origin: OriginFor<T>,
ring_pubkey: types::RingPubkey,
amount: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
// TODO check number of rings
let who = ensure_signed(origin)?;
Signups::<T>::try_mutate((CurrentRingSessionIndex::<T>::get(), amount), |entry| {
if let Some(signups) = entry {
log::info!("RING: signup if");
if signups.contains_key(&ring_pubkey) {
return Err(Error::<T>::RingPubkeyAlreadySignedUp.into());
}
signups
.try_insert(ring_pubkey, who.clone())
.map_err(|_e| Error::<T>::RingFull)?;
} else {
log::info!("RING: signup else");
let mut signups = BoundedBTreeMap::new();
signups
.try_insert(ring_pubkey, who.clone())
.map_err(|_e| Error::<T>::RingFull)?;
*entry = Some(signups);
}
log::info!("RING: after if");
T::Currency::hold(&HoldReason::RingSignUp.into(), &who, amount)?;
log::info!("RING: after hold");
Self::deposit_event(Event::Signup {
account: who,
amount,
});
Ok(().into())
})
}
/// Cancel a sign up
///
/// Unreserve the amount from the origin's account and remove it from the current ring.
#[pallet::call_index(1)]
#[pallet::weight(0)]
pub fn cancel(
origin: OriginFor<T>,
ring_pubkey: types::RingPubkey,
amount: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Signups::<T>::mutate_exists((CurrentRingSessionIndex::<T>::get(), amount), |entry| {
if let Some(signups) = entry {
if let Some(account) = signups.get(&ring_pubkey) {
ensure!(*account == who, Error::<T>::NotInRing);
signups.remove(&ring_pubkey);
T::Currency::release(
&HoldReason::RingCancel.into(),
&who,
amount,
Precision::Exact,
)?;
} else {
return Err(Error::<T>::NotInRing.into());
}
if signups.is_empty() {
*entry = None;
}
Ok(().into())
} else {
Err(Error::<T>::NotInRing.into())
}
})
}
#[pallet::call_index(2)]
#[pallet::weight(0)]
pub fn claim(
origin: OriginFor<T>,
recipient: T::AccountId,
amount: BalanceOf<T>,
signature: types::RingSignature,
) -> DispatchResultWithPostInfo {
ensure_none(origin)?;
// Has this key claimed already?
if Claims::<T>::contains_key((amount, signature.key_image.clone())) {
return Err(Error::<T>::AlreadyClaimed.into());
}
let (current_ring_session_index, key_image, signups) =
Self::check_claim(&recipient, &amount, signature)?;
Claims::<T>::insert((amount, key_image), ());
let mut next_claims = signups.range(NextClaim::<T>::get(amount).unwrap_or_default()..);
let Some((_source_ring_pubkey, source)) = next_claims.next() else {
// This should be unreachable, because:
// * the key has not already claimed
// * the signature is valid
// * hence there still exist unclaimed signups
// But in case the signature is broken, we want to avoid unlimited claiming.
return Err(Error::<T>::AlreadyClaimed.into());
};
T::Currency::transfer_on_hold(
&HoldReason::RingClaim.into(),
source,
&recipient,
amount,
Precision::Exact,
Restriction::Free,
Fortitude::Force,
)?;
if let Some((k, _v)) = next_claims.next() {
NextClaim::<T>::set(amount, Some(k.clone()));
} else {
NextClaim::<T>::set(amount, None);
Signups::<T>::remove((current_ring_session_index.clone(), amount));
}
Ok(().into())
}
}
// PUBLIC FUNCTIONS //
impl<T: Config> Pallet<T> {
pub fn check_claim(
recipient: &T::AccountId,
amount: &BalanceOf<T>,
signature: types::RingSignature,
) -> Result<
(
types::RingSessionIndex,
types::RingKeyImage,
BoundedBTreeMap<types::RingPubkey, T::AccountId, ConstU32<RING_SIZE>>,
),
Error<T>,
> {
let mut current_ring_session_index = CurrentRingSessionIndex::<T>::get();
current_ring_session_index.switch();
// Get last session's signups
let Some(signups) = Signups::<T>::get((current_ring_session_index.clone(), amount))
else {
return Err(Error::<T>::InvalidRingSignature);
};
// Verify ring size
ensure!(
signups.len() == signature.responses.len(),
Error::<T>::InvalidRingSignature
);
let mut params = blake2b_simd::Params::new();
params.hash_length(64);
let key_image = signature.key_image.clone();
// Decode the signature and iterate through all the ring members to verify it
TryInto::<orodruin::Signature<Vec<curve25519_dalek::Scalar>>>::try_into(signature)
.map_err(|_e| Error::<T>::InvalidRingSignature)?
.verify(
signups.keys().map(Into::<orodruin::PublicKey>::into),
&types::RingClaimPayload {
amount,
recipient: recipient.clone(),
}
.encode(),
&mut orodruin::blake2b::Blake2b::from_params(&params),
)
.map_err(|_e| Error::<T>::InvalidRingSignature)?;
Ok((current_ring_session_index, key_image, signups))
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
if let Call::claim {
recipient,
amount,
signature,
} = call
{
/*if <Pallet<T>>::is_online(heartbeat.authority_index) {
// we already received a heartbeat for this authority
return InvalidTransaction::Stale.into();
}*/
InvalidTransaction::Stale.into()
} else {
InvalidTransaction::Stale.into()
}
}
}
}
// 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,
}
}
}
[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 }
// Copyright 2024 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
//! Defines types and traits for users of pallet distance.
#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::type_complexity)]
use codec::{Decode, Encode};
use curve25519_dalek::{ristretto::CompressedRistretto, Scalar};
use scale_info::TypeInfo;
#[cfg(not(feature = "std"))]
use sp_std::vec::Vec;
pub use orodruin::SecretKey;
/// Temporary public key for ring signatures
///
/// This encodes a compressed Ristretto point.
#[derive(Clone, Debug, Decode, Default, Encode, Eq, Ord, PartialEq, PartialOrd, TypeInfo)]
pub struct RingPubkey(pub [u8; 32]);
impl From<orodruin::PublicKey> for RingPubkey {
fn from(public_key: orodruin::PublicKey) -> Self {
Self(public_key.0 .0)
}
}
impl From<RingPubkey> for orodruin::PublicKey {
fn from(ring_pubkey: RingPubkey) -> Self {
Self(curve25519_dalek::ristretto::CompressedRistretto(
ring_pubkey.0,
))
}
}
impl From<&orodruin::PublicKey> for RingPubkey {
fn from(public_key: &orodruin::PublicKey) -> Self {
Self(public_key.0 .0)
}
}
impl From<&RingPubkey> for orodruin::PublicKey {
fn from(ring_pubkey: &RingPubkey) -> Self {
Self(curve25519_dalek::ristretto::CompressedRistretto(
ring_pubkey.0,
))
}
}
/// Deterministic image of a ring private key
#[derive(Clone, Debug, Decode, Default, Encode, PartialEq, TypeInfo)]
pub struct RingKeyImage(pub [u8; 32]);
#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
pub struct RingSignature {
pub challenge: [u8; 32],
pub key_image: RingKeyImage,
pub responses: Vec<[u8; 32]>,
}
#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
pub enum RingSignatureDecodeError {
MalformedResponse,
MalformedChallenge,
}
impl TryInto<orodruin::Signature<Vec<Scalar>>> for RingSignature {
type Error = RingSignatureDecodeError;
fn try_into(self) -> Result<orodruin::Signature<Vec<Scalar>>, RingSignatureDecodeError> {
let nb_responses = self.responses.len();
let responses: Vec<Scalar> = self
.responses
.into_iter()
.map_while(|r| Scalar::from_canonical_bytes(r).into())
.collect();
if nb_responses != responses.len() {
return Err(RingSignatureDecodeError::MalformedResponse);
}
Ok(orodruin::Signature {
challenge: Option::<_>::from(Scalar::from_canonical_bytes(self.challenge))
.ok_or(RingSignatureDecodeError::MalformedChallenge)?,
key_image: orodruin::KeyImage(CompressedRistretto(self.key_image.0)),
responses,
})
}
}
impl Into<RingSignature> for orodruin::Signature<Vec<Scalar>> {
fn into(self) -> RingSignature {
RingSignature {
key_image: RingKeyImage(self.key_image.0 .0),
challenge: self.challenge.to_bytes(),
responses: self.responses.into_iter().map(|r| r.to_bytes()).collect(),
}
}
}
#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
pub struct RingClaimPayload<Address, Balance> {
pub amount: Balance,
pub recipient: Address,
}
#[cfg(feature = "std")]
#[derive(Clone, Debug, Decode, Encode, TypeInfo, PartialEq)]
pub enum SignError {
NotInRing,
}
#[cfg(feature = "std")]
pub fn sign(
sk: SecretKey,
ring: &[RingPubkey],
message: &[u8],
rng: impl rand_core::CryptoRng + rand_core::RngCore,
) -> Result<RingSignature, SignError> {
let pubkey = sk.public_key();
let secret_index = ring
.iter()
.enumerate()
.find_map(|(i, pk)| if pk.0 == pubkey.0 .0 { Some(i) } else { None })
.ok_or(SignError::NotInRing)?;
let mut params = blake2b_simd::Params::new();
params.hash_length(64);
let mut hasher = orodruin::blake2b::Blake2b::from_params(&params);
Ok(orodruin::sign(sk, ring, secret_index, message, rng, &mut hasher).into())
}
No preview for this file type
...@@ -76,6 +76,7 @@ std = [ ...@@ -76,6 +76,7 @@ std = [
"pallet-provide-randomness/std", "pallet-provide-randomness/std",
"pallet-proxy/std", "pallet-proxy/std",
"pallet-quota/std", "pallet-quota/std",
"pallet-ring/std",
"pallet-scheduler/std", "pallet-scheduler/std",
"pallet-session/std", "pallet-session/std",
"pallet-smith-members/std", "pallet-smith-members/std",
...@@ -163,6 +164,7 @@ pallet-preimage = { workspace = true } ...@@ -163,6 +164,7 @@ pallet-preimage = { workspace = true }
pallet-provide-randomness = { workspace = true } pallet-provide-randomness = { workspace = true }
pallet-proxy = { workspace = true } pallet-proxy = { workspace = true }
pallet-quota = { workspace = true } pallet-quota = { workspace = true }
pallet-ring = { workspace = true }
pallet-scheduler = { workspace = true } pallet-scheduler = { workspace = true }
pallet-session = { workspace = true } pallet-session = { workspace = true }
pallet-smith-members = { workspace = true } pallet-smith-members = { workspace = true }
......
...@@ -140,6 +140,16 @@ macro_rules! runtime_apis { ...@@ -140,6 +140,16 @@ macro_rules! runtime_apis {
{ {
return sp_runtime::transaction_validity::InvalidTransaction::Call.into(); 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) Executive::validate_transaction(source, tx, block_hash)
} }
} }
......
...@@ -165,3 +165,28 @@ where ...@@ -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()
}
}
...@@ -210,6 +210,16 @@ macro_rules! pallets_config { ...@@ -210,6 +210,16 @@ macro_rules! pallets_config {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
type WeightInfo = weights::pallet_oneshot_account::WeightInfo<Runtime>; 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 // // CONSENSUS //
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment