diff --git a/.vscode/settings.json b/.vscode/settings.json index 3070b8018c7c4e188342e3ca960c949140efd8c5..f6e785cae54a157c5013b596877b512387c97924 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,6 @@ "port_p2p": 19931, "port_rpc": 19932, "port_ws": 19933 - } + }, + "rust-analyzer.showUnlinkedFileNotification": false } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e4699eb8721ab4d035b58961fe0b7a81e6bd5fb2..2c5d16da462af2dd947e304887bd6de4ee131bc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -836,6 +836,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1558,7 +1567,11 @@ dependencies = [ name = "distance-oracle" version = "0.1.0" dependencies = [ + "bincode", "clap 4.1.4", + "dubp-wot", + "flate2", + "num-traits", "parity-scale-codec", "rayon", "sp-core", @@ -1596,6 +1609,19 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c00704156a7de8df8da0911424e30c2049957b0a714542a44e05fe693dd85313" +[[package]] +name = "dubp-wot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f62bdc1abf2da65794c9fd7fe46f7328834a97a2346048d46b0b6676302de0" +dependencies = [ + "log", + "once_cell", + "parking_lot 0.11.2", + "rayon", + "serde", +] + [[package]] name = "duniter" version = "0.3.0" @@ -1683,6 +1709,7 @@ dependencies = [ "clap 3.2.23", "ctrlc", "cucumber", + "distance-oracle", "env_logger 0.9.3", "hex", "notify", @@ -4206,6 +4233,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" dependencies = [ "cc", + "cmake", + "libc", "pkg-config", "vcpkg", ] @@ -9800,7 +9829,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "digest 0.10.6", "rand 0.8.5", "static_assertions", diff --git a/client/distance/src/lib.rs b/client/distance/src/lib.rs index 87a973c7c21687c2d0072f1fa7d7f7ae80f12e6d..a89ef6bc14c65818da0e93053163cd35f5cf5556 100644 --- a/client/distance/src/lib.rs +++ b/client/distance/src/lib.rs @@ -78,12 +78,9 @@ where ), )? .map_or_else(Default::default, |raw| { - pallet_distance::EvaluationPool::< - AccountId32, - IdtyIndex, - ConstU32<100>, - ConstU32<100>, - >::decode(&mut &raw.0[..]) + pallet_distance::EvaluationPool::<AccountId32, IdtyIndex, ConstU32<100>, 100>::decode( + &mut &raw.0[..], + ) .expect("cannot decode EvaluationPool") }); diff --git a/distance-oracle/Cargo.toml b/distance-oracle/Cargo.toml index 8869a5fc6b1901d53eb0bd49904822139890f497..5e561323bed4466dca42e5f292d073a8636b988e 100644 --- a/distance-oracle/Cargo.toml +++ b/distance-oracle/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" sp-distance = { path = "../primitives/distance" } codec = { package = "parity-scale-codec", version = "3.1.5" } +num-traits = "0.2.15" rayon = "1.7.0" sp-core = { git = "https://github.com/duniter/substrate.git", branch = "duniter-substrate-v0.9.32" } sp-runtime = { git = "https://github.com/duniter/substrate.git", branch = "duniter-substrate-v0.9.32" } @@ -22,6 +23,13 @@ tokio = { version = "1.15.0", features = [ "macros", ], optional = true } +[dev-dependencies] +bincode = "1.3.3" +dubp-wot = "0.11.1" +flate2 = { version = "1.0", features = [ + "zlib-ng-compat", +], default-features = false } + [features] default = ["standalone"] standalone = ["clap", "tokio"] diff --git a/distance-oracle/src/api.rs b/distance-oracle/src/api.rs new file mode 100644 index 0000000000000000000000000000000000000000..71bf77e7aa08aff93621ec5c98246d0484e6849a --- /dev/null +++ b/distance-oracle/src/api.rs @@ -0,0 +1,113 @@ +use crate::{ + gdev, + gdev::runtime_types::{ + pallet_distance::median::MedianAcc, + sp_arithmetic::per_things::Perbill, + sp_core::bounded::{bounded_btree_set::BoundedBTreeSet, bounded_vec::BoundedVec}, + }, +}; + +use sp_core::H256; + +pub type Client = subxt::OnlineClient<crate::GdevConfig>; +pub type AccountId = subxt::ext::sp_runtime::AccountId32; +pub type IdtyIndex = u32; +pub type EvaluationPool<AccountId, IdtyIndex> = ( + BoundedVec<(IdtyIndex, MedianAcc<Perbill>)>, + BoundedBTreeSet<AccountId>, +); + +pub async fn client(rpc_url: String) -> Client { + Client::from_url(rpc_url).await.unwrap() +} + +pub async fn parent_hash(client: &Client) -> H256 { + client + .storage() + .fetch(&gdev::storage().system().parent_hash(), None) + .await + .unwrap() + .unwrap() +} + +pub async fn current_session(client: &Client, parent_hash: H256) -> u32 { + client + .storage() + .fetch( + &gdev::storage().session().current_index(), + Some(parent_hash), + ) + .await + .unwrap() + .unwrap_or_default() +} + +pub async fn current_pool( + client: &Client, + parent_hash: H256, + current_session: u32, +) -> Option<EvaluationPool<AccountId, IdtyIndex>> { + client + .storage() + .fetch( + &match current_session % 3 { + 0 => gdev::storage().distance().evaluation_pool1(), + 1 => gdev::storage().distance().evaluation_pool2(), + 2 => gdev::storage().distance().evaluation_pool0(), + _ => unreachable!("n%3<3"), + }, + Some(parent_hash), + ) + .await + .unwrap() +} + +pub async fn evaluation_block(client: &Client, parent_hash: H256) -> H256 { + client + .storage() + .fetch( + &gdev::storage().distance().evaluation_block(), + Some(parent_hash), + ) + .await + .unwrap() + .unwrap() +} + +pub async fn member_iter( + client: &Client, + evaluation_block: H256, +) -> subxt::storage::KeyIter< + crate::GdevConfig, + Client, + subxt::metadata::DecodeStaticType<gdev::runtime_types::sp_membership::MembershipData<u32>>, +> { + client + .storage() + .iter( + gdev::storage().membership().membership(0), + 100, + Some(evaluation_block), + ) + .await + .unwrap() +} + +pub async fn cert_iter( + client: &Client, + evaluation_block: H256, +) -> subxt::storage::KeyIter< + crate::GdevConfig, + Client, + subxt::metadata::DecodeStaticType<Vec<(u32, u32)>>, +> { + client + .storage() + .iter( + gdev::storage().cert().certs_by_receiver(0), + 100, + Some(evaluation_block), + ) + .await + .unwrap() +} diff --git a/distance-oracle/src/lib.rs b/distance-oracle/src/lib.rs index 62fc97b5d772ef81d36ce479684702360ec02e22..47418cc941771306158d9146b22cae2a8326c125 100644 --- a/distance-oracle/src/lib.rs +++ b/distance-oracle/src/lib.rs @@ -1,39 +1,33 @@ +#[cfg(not(test))] +mod api; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(test)] +use mock as api; + +use api::{AccountId, IdtyIndex}; + use codec::Encode; -use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use rayon::iter::IntoParallelRefIterator; +use rayon::iter::ParallelIterator; use std::collections::{HashMap, HashSet}; use std::io::Write; use std::path::PathBuf; -use subxt::ext::sp_runtime::Perbill; use subxt::storage::StorageKey; -pub struct Settings { - pub evaluation_result_dir: PathBuf, - pub max_depth: u32, - pub rpc_url: String, -} - -impl Default for Settings { - fn default() -> Self { - Self { - evaluation_result_dir: PathBuf::from("/tmp/duniter/chains/gdev/distance"), - max_depth: 5, - rpc_url: String::from("ws://127.0.0.1:9944"), - } - } -} - #[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")] pub mod gdev {} -pub type Client = subxt::OnlineClient<GdevConfig>; - pub enum GdevConfig {} impl subxt::config::Config for GdevConfig { type Index = u32; type BlockNumber = u32; type Hash = sp_core::H256; type Hashing = subxt::ext::sp_runtime::traits::BlakeTwo256; - type AccountId = subxt::ext::sp_runtime::AccountId32; + type AccountId = AccountId; type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>; type Header = subxt::ext::sp_runtime::generic::Header< Self::BlockNumber, @@ -61,86 +55,101 @@ impl From<u64> for Tip { } } -type IdtyIndex = u32; +pub struct Settings { + pub evaluation_result_dir: PathBuf, + pub max_depth: u32, + pub rpc_url: String, +} + +impl Default for Settings { + fn default() -> Self { + Self { + evaluation_result_dir: PathBuf::from("/tmp/duniter/chains/gdev/distance"), + max_depth: 5, + rpc_url: String::from("ws://127.0.0.1:9944"), + } + } +} -pub async fn run(settings: Settings) { - let client = Client::from_url(settings.rpc_url).await.unwrap(); +pub async fn run_and_save(settings: Settings) { + let Some((evaluation, current_session, evaluation_result_path)) = run(&settings, true).await else {return}; - let parent_hash = client - .storage() - .fetch(&gdev::storage().system().parent_hash(), None) - .await - .unwrap() + let mut evaluation_result_file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(evaluation_result_path) .unwrap(); - - let current_session = client - .storage() - .fetch( - &gdev::storage().session().current_index(), - Some(parent_hash), + evaluation_result_file + .write_all( + &sp_distance::ComputationResult { + distances: evaluation, + } + .encode(), ) - .await - .unwrap() - .unwrap_or_default(); + .unwrap(); + + // Remove old results + let mut files_to_remove = Vec::new(); + for entry in settings.evaluation_result_dir.read_dir().unwrap().flatten() { + if let Ok(entry_name) = entry.file_name().into_string() { + if let Ok(entry_session) = entry_name.parse::<isize>() { + if current_session as isize - entry_session > 3 { + files_to_remove.push(entry.path()); + } + } + } + } + files_to_remove.into_iter().for_each(|f| { + std::fs::remove_file(&f).unwrap_or_else(move |e| { + eprintln!("Warning: Cannot remove old result file `{f:?}`: {e:?}") + }); + }); +} + +/// Returns `(evaluation, current_session, evaluation_result_path)` +pub async fn run( + settings: &Settings, + handle_fs: bool, +) -> Option<(Vec<sp_runtime::Perbill>, u32, PathBuf)> { + let client = api::client(settings.rpc_url.clone()).await; + + let parent_hash = api::parent_hash(&client).await; + + let current_session = api::current_session(&client, parent_hash).await; // Fetch the pending identities - let Some(evaluation_pool) = client - .storage() - .fetch( - &match current_session % 3 { - 0 => gdev::storage().distance().evaluation_pool1(), - 1 => gdev::storage().distance().evaluation_pool2(), - 2 => gdev::storage().distance().evaluation_pool0(), - _ => unreachable!("n%3<3"), - }, - Some(parent_hash), - ) - .await - .unwrap() else { + let Some(evaluation_pool) = api::current_pool(&client, parent_hash, current_session).await + else { println!("Pool does not exist"); - return + return None }; // Stop if nothing to evaluate if evaluation_pool.0 .0.is_empty() { println!("Pool is empty"); - return; + return None; } let evaluation_result_path = settings .evaluation_result_dir .join((current_session + 1).to_string()); - // Stop if already evaluated - if evaluation_result_path.try_exists().unwrap() { - println!("File already exists"); - return; - } + if handle_fs { + // Stop if already evaluated + if evaluation_result_path.try_exists().unwrap() { + println!("File already exists"); + return None; + } - let evaluation_block = client - .storage() - .fetch( - &gdev::storage().distance().evaluation_block(), - Some(parent_hash), - ) - .await - .unwrap() - .unwrap(); + std::fs::create_dir_all(&settings.evaluation_result_dir).unwrap(); + } - std::fs::create_dir_all(&settings.evaluation_result_dir).unwrap(); + let evaluation_block = api::evaluation_block(&client, parent_hash).await; // member idty -> issued certs let mut members = HashMap::<IdtyIndex, u32>::new(); - let mut members_iter = client - .storage() - .iter( - gdev::storage().membership().membership(0), - 100, - Some(evaluation_block), - ) - .await - .unwrap(); + let mut members_iter = api::member_iter(&client, evaluation_block).await; while let Some((member_idty, _membership_expire)) = members_iter.next().await.unwrap() { members.insert(idty_id_from_storage_key(&member_idty), 0); } @@ -152,15 +161,7 @@ pub async fn run(settings: Settings) { // idty -> received certs let mut received_certs = HashMap::<IdtyIndex, Vec<IdtyIndex>>::new(); - let mut certs_iter = client - .storage() - .iter( - gdev::storage().cert().certs_by_receiver(0), - 100, - Some(evaluation_block), - ) - .await - .unwrap(); + let mut certs_iter = api::cert_iter(&client, evaluation_block).await; while let Some((receiver, issuers)) = certs_iter.next().await.unwrap() { let receiver = idty_id_from_storage_key(&receiver); // Update members' issued certs count @@ -188,48 +189,20 @@ pub async fn run(settings: Settings) { members.retain(|_idty, issued_certs| *issued_certs >= min_certs_for_referee); let referees = members; - let evaluation: Vec<Perbill> = evaluation_pool + let evaluation = evaluation_pool .0 .0 - .into_par_iter() + .as_slice() + .par_iter() .map(|(idty, _)| { - Perbill::from_rational( - distance_rule(&received_certs, &referees, settings.max_depth, idty), + sp_runtime::Perbill::from_rational( + distance_rule(&received_certs, &referees, settings.max_depth, *idty), referees.len() as u32, ) }) .collect(); - let mut evaluation_result_file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(evaluation_result_path) - .unwrap(); - evaluation_result_file - .write_all( - &sp_distance::ComputationResult { - distances: evaluation, - } - .encode(), - ) - .unwrap(); - - // Remove old results - let mut files_to_remove = Vec::new(); - for entry in settings.evaluation_result_dir.read_dir().unwrap().flatten() { - if let Ok(entry_name) = entry.file_name().into_string() { - if let Ok(entry_session) = entry_name.parse::<isize>() { - if current_session as isize - entry_session > 3 { - files_to_remove.push(entry.path()); - } - } - } - } - files_to_remove.into_iter().for_each(|f| { - std::fs::remove_file(&f).unwrap_or_else(move |e| { - eprintln!("Warning: Cannot remove old result file `{f:?}`: {e:?}") - }); - }); + Some((evaluation, current_session, evaluation_result_path)) } fn distance_rule_recursive( @@ -276,3 +249,11 @@ fn distance_rule( fn idty_id_from_storage_key(storage_key: &StorageKey) -> IdtyIndex { u32::from_le_bytes(storage_key.as_ref()[40..44].try_into().unwrap()) } +/* +impl num_traits::Pow<usize> for Perbill {} +impl std::ops::Div<Perbill> for Perbill {} +impl num_traits::Bounded for Perbill {} +impl Ord for Perbill {} +impl Eq for Perbill {} + +impl PerThing for Perbill {}*/ diff --git a/distance-oracle/src/main.rs b/distance-oracle/src/main.rs index af83eb398eec59a18f9ec49d6ebc1c80d3f31913..5d25f621b95cd49673016f73d80952ff08a22e42 100644 --- a/distance-oracle/src/main.rs +++ b/distance-oracle/src/main.rs @@ -15,7 +15,7 @@ struct Cli { async fn main() { let cli = Cli::parse(); - distance_oracle::run(distance_oracle::Settings { + distance_oracle::run_and_save(distance_oracle::Settings { evaluation_result_dir: cli.evaluation_result_dir.into(), max_depth: cli.max_depth, rpc_url: cli.rpc_url, diff --git a/distance-oracle/src/mock.rs b/distance-oracle/src/mock.rs new file mode 100644 index 0000000000000000000000000000000000000000..faf20d0ef41b73e271c7c76606fae04282069754 --- /dev/null +++ b/distance-oracle/src/mock.rs @@ -0,0 +1,65 @@ +use crate::gdev::runtime_types::{ + pallet_distance::median::MedianAcc, sp_arithmetic::per_things::Perbill, +}; + +use sp_core::H256; +use std::collections::BTreeSet; + +pub struct Client; +pub type AccountId = subxt::ext::sp_runtime::AccountId32; +pub type IdtyIndex = u32; +pub type EvaluationPool<AccountId, IdtyIndex> = + ((Vec<(IdtyIndex, MedianAcc<Perbill>)>,), BTreeSet<AccountId>); + +pub async fn client(_rpc_url: String) -> Client { + Client +} + +pub async fn parent_hash(_client: &Client) -> H256 { + Default::default() +} + +pub async fn current_session(_client: &Client, _parent_hash: H256) -> u32 { + 0 +} + +pub async fn current_pool( + _client: &Client, + _parent_hash: H256, + _current_session: u32, +) -> Option<EvaluationPool<AccountId, IdtyIndex>> { + Some(((vec![],), std::collections::BTreeSet::new())) +} + +pub async fn evaluation_block(_client: &Client, _parent_hash: H256) -> H256 { + Default::default() +} + +pub async fn member_iter( + _client: &Client, + _evaluation_block: H256, +) -> KeyIter<(subxt::storage::StorageKey, ())> { + KeyIter::new(Vec::new()) +} + +pub async fn cert_iter( + _client: &Client, + _evaluation_block: H256, +) -> KeyIter<(subxt::storage::StorageKey, Vec<(u32, u32)>)> { + KeyIter::new(Vec::new()) +} + +pub struct KeyIter<T>(std::vec::IntoIter<T>); + +impl<T> KeyIter<T> { + fn new(items: Vec<T>) -> Self { + Self(items.into_iter()) + } + + pub async fn next(&mut self) -> Result<Option<T>, subxt::error::Error> + where + T: Clone, + { + Ok(self.0.next()) + } +} diff --git a/distance-oracle/src/tests.rs b/distance-oracle/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..4c3f90fbe616d0483a3b4e7d266ce850332880b3 --- /dev/null +++ b/distance-oracle/src/tests.rs @@ -0,0 +1,20 @@ +use dubp_wot::data::rusty::RustyWebOfTrust; +use flate2::read::ZlibDecoder; +use std::{fs::File, io::Read}; + +#[tokio::test] +async fn test_distance_against_v1() { + let _wot = wot_from_v1_file(); + + crate::run(&Default::default(), false).await; +} + +fn wot_from_v1_file() -> RustyWebOfTrust { + let file = File::open("wot.deflate").expect("Cannot open wot.deflate"); + let mut decompressor = ZlibDecoder::new(file); + let mut decompressed_bytes = Vec::new(); + decompressor + .read_to_end(&mut decompressed_bytes) + .expect("Cannot decompress wot.deflate"); + bincode::deserialize::<RustyWebOfTrust>(&decompressed_bytes).expect("Cannot decode wot.deflate") +} diff --git a/distance-oracle/wot.deflate b/distance-oracle/wot.deflate new file mode 100644 index 0000000000000000000000000000000000000000..087c09cfc125ee954a63b79d04e4013630b4c4f5 Binary files /dev/null and b/distance-oracle/wot.deflate differ diff --git a/end2end-tests/Cargo.toml b/end2end-tests/Cargo.toml index 07df18c1aa1878428fd2c5c9b1b655df405ea810..95e15c04acbbcdfd5179b8b5e1a7ecab6e6cc9a4 100644 --- a/end2end-tests/Cargo.toml +++ b/end2end-tests/Cargo.toml @@ -14,6 +14,7 @@ async-trait = "0.1" clap = { version = "3.2.23", features = ["derive"] } ctrlc = "3.2.2" cucumber = "0.11" +distance-oracle = { path = "../distance-oracle", default_features = false } env_logger = "0.9.0" hex = "0.4" notify = "4.0" @@ -26,4 +27,4 @@ tokio = { version = "1.15.0", features = ["macros"] } [[test]] name = "cucumber_tests" -harness = false # allows Cucumber to print output instead of libtest +harness = false # allows Cucumber to print output instead of libtest diff --git a/end2end-tests/cucumber-features/distance.feature b/end2end-tests/cucumber-features/distance.feature deleted file mode 100644 index 9cc18d514ce91ef7d296edfbcb3cab0520b4691c..0000000000000000000000000000000000000000 --- a/end2end-tests/cucumber-features/distance.feature +++ /dev/null @@ -1,24 +0,0 @@ -Feature: Distance - - Scenario: Alice certifies Eve - When alice sends 6 ĞD to dave - When 15 blocks later - When alice creates identity for dave - Then dave identity should be created - Then dave should be certified by alice - When dave confirms his identity with pseudo "dave" - Then dave identity should be confirmed - When bob certifies dave - Then dave should be certified by bob - When alice requests distance evaluation for dave - Then dave should have distance result in 2 sessions - When 30 blocks later - Then dave should have distance result in 1 session - When distance oracle runs - Then dave should have distance result in 1 session - When 30 blocks later - Then dave should have distance result in 0 session - Then dave should have distance ok - When alice validates dave identity - When 3 blocks later - Then dave identity should be validated diff --git a/end2end-tests/cucumber-features/identity_creation.feature b/end2end-tests/cucumber-features/identity_creation.feature index 81113d8b1549154e6f3ca5e91691ff137691a18b..56a7404dffbd9390f1560fb547259c540fcfaa1a 100644 --- a/end2end-tests/cucumber-features/identity_creation.feature +++ b/end2end-tests/cucumber-features/identity_creation.feature @@ -5,8 +5,9 @@ Feature: Identity creation # - account creation fees (3 ĞD) # - existential deposit (2 ĞD) # - transaction fees (below 1 ĞD) - When alice sends 6 ĞD to dave - When bob sends 6 ĞD to eve + When alice sends 7 ĞD to dave + When bob sends 750 cĞD to dave + When charlie sends 6 ĞD to eve # alice last certification is counted from block zero # then next cert can be done after cert_period, which is 15 When 15 block later @@ -20,6 +21,12 @@ Feature: Identity creation When charlie certifies dave Then dave should be certified by bob Then dave should be certified by charlie - When 3 block later + When dave requests distance evaluation + Then dave should have distance result in 2 sessions + When 30 blocks later + Then dave should have distance result in 1 session + When alice runs distance oracle + When 30 blocks later + Then dave should have distance ok When eve validates dave identity Then dave identity should be validated diff --git a/end2end-tests/tests/common/distance.rs b/end2end-tests/tests/common/distance.rs index 6c792700d3140c150fbdb218b2aeb93282bc0cac..15f9d61cee4bd9c4f94aa8f7b3750a70e1abb9b5 100644 --- a/end2end-tests/tests/common/distance.rs +++ b/end2end-tests/tests/common/distance.rs @@ -19,18 +19,19 @@ use super::gdev::runtime_types::pallet_identity; use super::*; use crate::DuniterWorld; use sp_keyring::AccountKeyring; -use subxt::tx::PairSigner; +use subxt::ext::sp_runtime::AccountId32; +use subxt::tx::{PairSigner, Signer}; -pub async fn request_evaluation(client: &Client, from: AccountKeyring, to: u32) -> Result<()> { - let from = PairSigner::new(from.pair()); +pub async fn request_evaluation(client: &Client, origin: AccountKeyring) -> Result<()> { + let origin = PairSigner::new(origin.pair()); let _events = create_block_with_extrinsic( client, client .tx() .create_signed( - &gdev::tx().distance().evaluate_distance(to), - &from, + &gdev::tx().distance().evaluate_distance(), + &origin, BaseExtrinsicParamsBuilder::new(), ) .await?, @@ -39,3 +40,52 @@ pub async fn request_evaluation(client: &Client, from: AccountKeyring, to: u32) Ok(()) } + +pub async fn run_oracle(client: &Client, origin: AccountKeyring, rpc_url: String) -> Result<()> { + let origin = PairSigner::new(origin.pair()); + let account_id: &AccountId32 = origin.account_id(); + + if let Some((distances, _current_session, _evaluation_result_path)) = distance_oracle::run( + &distance_oracle::Settings { + evaluation_result_dir: PathBuf::default(), + max_depth: 5, + rpc_url, + }, + false, + ) + .await + { + for _ in 0..30 { + super::create_empty_block(client).await?; + } + + let _events = create_block_with_extrinsic( + client, + client + .tx() + .create_signed( + &gdev::tx().sudo().sudo(/*gdev::runtime_types::gdev_runtime::RuntimeCall::UpgradeOrigin(gdev::runtime_types::pallet_upgrade_origin::pallet::Call::dispatch_as_root { + call: Box::new(*/gdev::runtime_types::gdev_runtime::RuntimeCall::Distance( + gdev::runtime_types::pallet_distance::pallet::Call::force_update_evaluation { + evaluator: account_id.clone(), + computation_result: + gdev::runtime_types::sp_distance::ComputationResult { + distances: unsafe { + std::mem::transmute(distances) + }, + }, + }, + )/*) + })*/), + //&gdev::tx().upgrade_origin().dispatch_as_root( + //), + &origin, + BaseExtrinsicParamsBuilder::new(), + ) + .await?, + ) + .await?; + } + + Ok(()) +} diff --git a/end2end-tests/tests/common/identity.rs b/end2end-tests/tests/common/identity.rs index 58e881264a4daed837fdc4bc0a039e3f2428f27c..735e655fd66fa8d55bb74e709a7d0fc5946e245a 100644 --- a/end2end-tests/tests/common/identity.rs +++ b/end2end-tests/tests/common/identity.rs @@ -84,9 +84,11 @@ pub async fn validate_identity(client: &Client, from: AccountKeyring, to: u32) - &from, BaseExtrinsicParamsBuilder::new(), ) - .await?, + .await + .unwrap(), ) - .await?; + .await + .unwrap(); Ok(()) } diff --git a/end2end-tests/tests/cucumber_tests.rs b/end2end-tests/tests/cucumber_tests.rs index 8ac684502e3ad1bd0114dc2f4cec4582d7a3b60d..1712389a72c462c85d6dfc3e4f8ce6060fee758d 100644 --- a/end2end-tests/tests/cucumber_tests.rs +++ b/end2end-tests/tests/cucumber_tests.rs @@ -322,31 +322,30 @@ async fn confirm_identity(world: &mut DuniterWorld, from: String, pseudo: String async fn validate_identity(world: &mut DuniterWorld, from: String, to: String) -> Result<()> { // input names to keyrings let from = AccountKeyring::from_str(&from).expect("unknown from"); - let to: u32 = common::identity::get_identity_index(world, to).await?; + let to: u32 = common::identity::get_identity_index(world, to) + .await + .unwrap(); common::identity::validate_identity(world.client(), from, to).await } -#[when(regex = r#"([a-zA-Z]+) requests distance evaluation for ([a-zA-Z]+)"#)] -async fn request_distance_evaluation( - world: &mut DuniterWorld, - from: String, - to: String, -) -> Result<()> { - // input names to keyrings - let from = AccountKeyring::from_str(&from).expect("unknown from"); - let to: u32 = common::identity::get_identity_index(world, to).await?; +#[when(regex = r#"([a-zA-Z]+) requests distance evaluation"#)] +async fn request_distance_evaluation(world: &mut DuniterWorld, who: String) -> Result<()> { + let who = AccountKeyring::from_str(&who).expect("unknown origin"); - common::distance::request_evaluation(world.client(), from, to).await + common::distance::request_evaluation(world.client(), who).await } -#[when(regex = r#"distance oracle runs"#)] -async fn run_distance_oracle(world: &mut DuniterWorld) -> Result<()> { - common::spawn_distance_oracle( - common::DISTANCE_ORACLE_LOCAL_PATH, - world.inner.as_ref().unwrap().ws_port, - ); - Ok(()) +#[when(regex = r#"([a-zA-Z]+) runs distance oracle"#)] +async fn run_distance_oracle(world: &mut DuniterWorld, who: String) -> Result<()> { + let who = AccountKeyring::from_str(&who).expect("unknown origin"); + + common::distance::run_oracle( + world.client(), + who, + format!("ws://127.0.0.1:{}", world.inner.as_ref().unwrap().ws_port), + ) + .await } // ===== then ==== @@ -513,14 +512,19 @@ async fn should_have_distance_ok(world: &mut DuniterWorld, who: String) -> Resul .await? .unwrap(); - if world - .read(&gdev::storage().distance().distance_ok_identities(idty_id)) + match world + .read( + &gdev::storage() + .distance() + .identities_distance_status(idty_id), + ) .await? - .unwrap() { - Ok(()) - } else { - Err(anyhow::anyhow!("no evaluation in given pool").into()) + Some(gdev::runtime_types::pallet_distance::types::DistanceStatus::Valid) => Ok(()), + Some(gdev::runtime_types::pallet_distance::types::DistanceStatus::Pending) => { + Err(anyhow::anyhow!("pending distance status").into()) + } + None => Err(anyhow::anyhow!("no distance status").into()), } } diff --git a/pallets/distance/src/lib.rs b/pallets/distance/src/lib.rs index cce777e7d6d3e43a76b6135c2534774ab1497c9c..1779011ef308f9e6db346509122d95e486c8d9d6 100644 --- a/pallets/distance/src/lib.rs +++ b/pallets/distance/src/lib.rs @@ -20,12 +20,12 @@ mod median; mod traits; mod types; -use median::*; pub use pallet::*; pub use traits::*; pub use types::*; use frame_support::traits::StorageVersion; +//use median_accumulator::*; use pallet_authority_members::SessionIndex; use sp_distance::{InherentError, INHERENT_IDENTIFIER}; use sp_inherents::{InherentData, InherentIdentifier}; @@ -36,7 +36,7 @@ type IdtyIndex = u32; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::{pallet_prelude::*, traits::ReservableCurrency, BoundedBTreeSet}; + use frame_support::{pallet_prelude::*, traits::ReservableCurrency}; use frame_system::pallet_prelude::*; use sp_runtime::Perbill; @@ -65,6 +65,7 @@ pub mod pallet { /// Maximum number of identities to be evaluated in a session type MaxEvaluationsPerSession: Get<u32>; /// Maximum number of evaluators in a session + #[pallet::constant] type MaxEvaluatorsPerSession: Get<u32>; /// Minimum ratio of accessible referees type MinAccessibleReferees: Get<Perbill>; @@ -74,46 +75,39 @@ pub mod pallet { // STORAGE // - pub type EvaluationPool< - AccountId, - IdtyIndex, - MaxEvaluationsPerSession, - MaxEvaluatorsPerSession, - > = ( - BoundedVec<(IdtyIndex, MedianAcc<Perbill>), MaxEvaluationsPerSession>, - BoundedBTreeSet<AccountId, MaxEvaluatorsPerSession>, - ); - #[pallet::storage] + #[pallet::getter(fn evaluation_pool_0)] pub type EvaluationPool0<T: Config<I>, I: 'static = ()> = StorageValue< _, EvaluationPool< <T as frame_system::Config>::AccountId, <T as pallet_certification::Config<I>>::IdtyIndex, <T as Config<I>>::MaxEvaluationsPerSession, - <T as Config<I>>::MaxEvaluatorsPerSession, + 100, //<T as Config<I>>::MaxEvaluatorsPerSession, >, ValueQuery, >; #[pallet::storage] + #[pallet::getter(fn evaluation_pool_1)] pub type EvaluationPool1<T: Config<I>, I: 'static = ()> = StorageValue< _, EvaluationPool< <T as frame_system::Config>::AccountId, <T as pallet_certification::Config<I>>::IdtyIndex, <T as Config<I>>::MaxEvaluationsPerSession, - <T as Config<I>>::MaxEvaluatorsPerSession, + 100, //<T as Config<I>>::MaxEvaluatorsPerSession, >, ValueQuery, >; #[pallet::storage] + #[pallet::getter(fn evaluation_pool_2)] pub type EvaluationPool2<T: Config<I>, I: 'static = ()> = StorageValue< _, EvaluationPool< <T as frame_system::Config>::AccountId, <T as pallet_certification::Config<I>>::IdtyIndex, <T as Config<I>>::MaxEvaluationsPerSession, - <T as Config<I>>::MaxEvaluatorsPerSession, + 100, //<T as Config<I>>::MaxEvaluatorsPerSession, >, ValueQuery, >; @@ -209,66 +203,24 @@ pub mod pallet { !DidUpdate::<T, I>::exists(), Error::<T, I>::ManyEvaluationsInBlock, ); + let author = pallet_authorship::Pallet::<T>::author().ok_or(Error::<T, I>::NoAuthor)?; - Pallet::<T, I>::mutate_current_pool( - pallet_session::CurrentIndex::<T>::get().wrapping_add(1), - |result_pool| { - ensure!( - computation_result.distances.len() == result_pool.0.len(), - Error::<T, I>::WrongResultLength - ); - - let author = - pallet_authorship::Pallet::<T>::author().ok_or(Error::<T, I>::NoAuthor)?; - - if result_pool - .1 - .try_insert(author) - .map_err(|_| Error::<T, I>::TooManyEvaluators)? - { - for (distance_value, (_identity, median_acc)) in computation_result - .distances - .into_iter() - .zip(result_pool.0.iter_mut()) - { - median_acc.push(distance_value); - } + Pallet::<T, I>::do_update_evaluation(author, computation_result)?; - Ok(()) - } else { - Err(Error::<T, I>::ManyEvaluationsByAuthor.into()) - } - }, - ) + DidUpdate::<T, I>::set(true); + Ok(()) } - } - - // PUBLIC FUNCTIONS // - - impl<T: Config<I>, I: 'static> Pallet<T, I> { - pub fn do_evaluate_distance( - who: T::AccountId, - idty_index: <T as pallet_certification::Config<I>>::IdtyIndex, - ) -> Result<(), DispatchError> { - Pallet::<T, I>::mutate_current_pool( - pallet_session::CurrentIndex::<T>::get(), - |current_pool| { - ensure!( - current_pool.0.len() - < (<T as Config<I>>::MaxEvaluationsPerSession::get() as usize), - Error::<T, I>::QueueFull - ); - - T::Currency::reserve(&who, <T as Config<I>>::EvaluationPrice::get())?; - current_pool - .0 - .try_push((idty_index, median::MedianAcc::new())) - .map_err(|_| Error::<T, I>::QueueFull)?; + /// Push an evaluation result to the pool + #[pallet::weight(1_000_000_000)] + pub fn force_update_evaluation( + origin: OriginFor<T>, + evaluator: <T as frame_system::Config>::AccountId, + computation_result: ComputationResult, + ) -> DispatchResult { + ensure_root(origin)?; - Ok(()) - }, - ) + Pallet::<T, I>::do_update_evaluation(evaluator, computation_result) } } @@ -283,7 +235,7 @@ pub mod pallet { <T as frame_system::Config>::AccountId, <T as pallet_certification::Config<I>>::IdtyIndex, <T as Config<I>>::MaxEvaluationsPerSession, - <T as Config<I>>::MaxEvaluatorsPerSession, + 100, //<T as Config<I>>::MaxEvaluatorsPerSession, >, ) -> R, >( @@ -294,7 +246,7 @@ pub mod pallet { 0 => EvaluationPool2::<T, I>::mutate(f), 1 => EvaluationPool0::<T, I>::mutate(f), 2 => EvaluationPool1::<T, I>::mutate(f), - _ => panic!("index % 3 < 3"), + _ => unreachable!("index % 3 < 3"), } } @@ -306,15 +258,75 @@ pub mod pallet { <T as frame_system::Config>::AccountId, <T as pallet_certification::Config<I>>::IdtyIndex, <T as Config<I>>::MaxEvaluationsPerSession, - <T as Config<I>>::MaxEvaluatorsPerSession, + 100, //<T as Config<I>>::MaxEvaluatorsPerSession, > { match index % 3 { 0 => EvaluationPool2::<T, I>::take(), 1 => EvaluationPool0::<T, I>::take(), 2 => EvaluationPool1::<T, I>::take(), - _ => panic!("index % 3 < 3"), + _ => unreachable!("index % 3 < 3"), } } + + fn do_evaluate_distance( + who: T::AccountId, + idty_index: <T as pallet_certification::Config<I>>::IdtyIndex, + ) -> Result<(), DispatchError> { + Pallet::<T, I>::mutate_current_pool( + pallet_session::CurrentIndex::<T>::get(), + |current_pool| { + ensure!( + current_pool.0.len() + < (<T as Config<I>>::MaxEvaluationsPerSession::get() as usize), + Error::<T, I>::QueueFull + ); + + T::Currency::reserve(&who, <T as Config<I>>::EvaluationPrice::get())?; + + current_pool + .0 + .try_push((idty_index, median::MedianAcc::new())) + .map_err(|_| Error::<T, I>::QueueFull)?; + + IdentitiesDistanceStatus::<T, I>::insert(idty_index, DistanceStatus::Pending); + + Ok(()) + }, + ) + } + + fn do_update_evaluation( + evaluator: <T as frame_system::Config>::AccountId, + computation_result: ComputationResult, + ) -> DispatchResult { + Pallet::<T, I>::mutate_current_pool( + pallet_session::CurrentIndex::<T>::get().wrapping_add(1), + |result_pool| { + ensure!( + computation_result.distances.len() == result_pool.0.len(), + Error::<T, I>::WrongResultLength + ); + + if result_pool + .1 + .try_insert(evaluator) + .map_err(|_| Error::<T, I>::TooManyEvaluators)? + { + for (distance_value, (_identity, median_acc)) in computation_result + .distances + .into_iter() + .zip(result_pool.0.iter_mut()) + { + median_acc.push(distance_value); + } + + Ok(()) + } else { + Err(Error::<T, I>::ManyEvaluationsByAuthor.into()) + } + }, + ) + } } impl<T: Config<I>, I: 'static> pallet_authority_members::OnNewSession for Pallet<T, I> { @@ -328,7 +340,7 @@ pub mod pallet { <T as frame_system::Config>::AccountId, <T as pallet_certification::Config<I>>::IdtyIndex, <T as Config<I>>::MaxEvaluationsPerSession, - <T as Config<I>>::MaxEvaluatorsPerSession, + 100, //<T as Config<I>>::MaxEvaluatorsPerSession, > = Pallet::<T, I>::take_current_pool(index); for (idty, median_acc) in current_pool.0.into_iter() { let Some(idty_value) = @@ -340,7 +352,9 @@ pub mod pallet { MedianResult::Two(m1, m2) => m1 + (m2 - m1) / 2, // Avoid overflow (since max is 1) }; if median >= T::MinAccessibleReferees::get() { - IdentitiesDistanceStatus::<T, I>::insert(idty, DistanceStatus::Valid); + IdentitiesDistanceStatus::<T, I>::mutate(idty, |status| { + *status = Some(DistanceStatus::Valid) + }); T::Currency::unreserve( &account_id, <T as Config<I>>::EvaluationPrice::get(), diff --git a/pallets/distance/src/median.rs b/pallets/distance/src/median.rs index 065efb3119377cea4906e86c45e176f56f1b1941..feab0d863176a4d24e33b85856717346a1ec29a7 100644 --- a/pallets/distance/src/median.rs +++ b/pallets/distance/src/median.rs @@ -15,25 +15,38 @@ // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. use frame_support::pallet_prelude::*; -use sp_std::{cmp::Ordering, vec::Vec}; +use sp_std::cmp::Ordering; #[derive(Clone, Debug, Decode, Default, Encode, TypeInfo)] -pub struct MedianAcc<T: Clone + Decode + Encode + Ord + TypeInfo> { - samples: Vec<(T, u32)>, +pub struct MedianAcc< + T: Clone + Decode + Encode + Ord + TypeInfo, + const S: u32, /*Get<u32> + TypeInfo*/ +> { + samples: BoundedVec<(T, u32), ConstU32<S>>, median_index: Option<u32>, median_subindex: u32, } +/*impl<T: 'static + Clone + Decode + Encode + Ord + TypeInfo, S: 'static + Get<u32>> TypeInfo + for MedianAcc<T, S> +{ + type Identity = Self; + + fn type_info() -> scale_info::Type<scale_info::form::MetaForm> {} +}*/ + #[derive(Clone, Debug, Eq, PartialEq)] pub enum MedianResult<T: Clone + Ord> { One(T), Two(T, T), } -impl<T: Clone + Decode + Encode + Ord + TypeInfo> MedianAcc<T> { +impl<T: Clone + Decode + Encode + Ord + TypeInfo, const S: u32 /*Get<u32> + TypeInfo*/> + MedianAcc<T, S> +{ pub fn new() -> Self { Self { - samples: Vec::new(), + samples: BoundedVec::default(), median_index: None, median_subindex: 0, } @@ -84,7 +97,7 @@ impl<T: Clone + Decode + Encode + Ord + TypeInfo> MedianAcc<T> { } } Err(sample_index) => { - self.samples.insert(sample_index, (sample, 1)); + self.samples.try_insert(sample_index, (sample, 1)).ok(); if *median_index as usize >= sample_index { if self.median_subindex == 0 { self.median_subindex = self @@ -115,7 +128,7 @@ impl<T: Clone + Decode + Encode + Ord + TypeInfo> MedianAcc<T> { } } } else { - self.samples.push((sample, 1)); + self.samples.try_push((sample, 1)).ok(); self.median_index = Some(0); } } diff --git a/pallets/distance/src/types.rs b/pallets/distance/src/types.rs index 2d3edf946f670bc274f11519046e2a32fb3ea6ea..f5f6589b350b3bdbed34edf9540e54281eac2b2f 100644 --- a/pallets/distance/src/types.rs +++ b/pallets/distance/src/types.rs @@ -14,10 +14,12 @@ // 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/>. +pub use crate::median::*; pub use sp_distance::ComputationResult; use codec::{Decode, Encode}; -use frame_support::pallet_prelude::*; +use frame_support::{pallet_prelude::*, BoundedBTreeSet}; +use sp_runtime::Perbill; #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub enum DistanceStatus { @@ -26,3 +28,16 @@ pub enum DistanceStatus { /// Identity respects the distance Valid, } + +pub type EvaluationPool< + AccountId, + IdtyIndex, + MaxEvaluationsPerSession, + const MAX_EVALUATORS_PER_SESSION: u32, +> = ( + BoundedVec< + (IdtyIndex, MedianAcc<Perbill, MAX_EVALUATORS_PER_SESSION>), + MaxEvaluationsPerSession, + >, + BoundedBTreeSet<AccountId, ConstU32<MAX_EVALUATORS_PER_SESSION>>, +); diff --git a/resources/metadata.scale b/resources/metadata.scale index c5d992edbe02769551ed0a8ba738d79232bccfbe..9582cd2af48d15f0121380317bb240fe5e4527a7 100644 Binary files a/resources/metadata.scale and b/resources/metadata.scale differ