Skip to content
Snippets Groups Projects
Commit 4234da98 authored by Pascal Engélibert's avatar Pascal Engélibert :bicyclist: Committed by Hugo Trentesaux
Browse files

Distance Oracle (nodes/rust/duniter-v2s!105)

* Fix distance-oracle EvaluationPool type

* Remove instanciation and dep to certification

* Doc comments, make max size params const

* Fix default distance dir

* Rename pool accessors

* doc add READMEs

* fix(distance): Remember account_id who reserved

* Log, comments, crate author

* feat: distance

* integration tests, expiration

* fixes & working end2end tests

* fix(distance): max_depth, compute min_certs_for_referee

* fix(distance): add distance pallet to gtest, g1

* test(distance): WiP end2end test

* feat: distance
parent 71bc932e
No related branches found
No related tags found
1 merge request!105Distance Oracle
Pipeline #33247 waiting for manual action
Showing
with 1839 additions and 446 deletions
...@@ -14,5 +14,6 @@ ...@@ -14,5 +14,6 @@
"port_p2p": 19931, "port_p2p": 19931,
"port_rpc": 19932, "port_rpc": 19932,
"port_ws": 19933 "port_ws": 19933
} },
"rust-analyzer.showUnlinkedFileNotification": false
} }
\ No newline at end of file
This diff is collapsed.
...@@ -32,10 +32,10 @@ runtime-benchmarks = [ ...@@ -32,10 +32,10 @@ runtime-benchmarks = [
'sc-client-db/runtime-benchmarks', 'sc-client-db/runtime-benchmarks',
] ]
try-runtime = [ try-runtime = [
#"g1-runtime/try-runtime", #"g1-runtime/try-runtime",
"gdev-runtime/try-runtime", "gdev-runtime/try-runtime",
#"gtest-runtime/try-runtime", #"gtest-runtime/try-runtime",
"try-runtime-cli" "try-runtime-cli",
] ]
[build-dependencies] [build-dependencies]
...@@ -48,19 +48,28 @@ rusty-hook = "^0.11.2" ...@@ -48,19 +48,28 @@ rusty-hook = "^0.11.2"
# Dependencies for specific targets # Dependencies for specific targets
[target.'cfg(any(target_arch="x86_64", target_arch="aarch64"))'.dependencies] [target.'cfg(any(target_arch="x86_64", target_arch="aarch64"))'.dependencies]
sc-cli = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32", default-features = false, features = ["wasmtime"] } sc-cli = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32", default-features = false, features = [
sc-service = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32", default-features = false, features = ["wasmtime"] } "wasmtime",
sp-trie = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32", features = ["memory-tracker"] } ] }
sc-service = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32", default-features = false, features = [
"wasmtime",
] }
sp-trie = { git = "https://github.com/duniter/substrate", branch = "duniter-substrate-v0.9.32", features = [
"memory-tracker",
] }
[dependencies] [dependencies]
# local dependencies # local dependencies
common-runtime = { path = 'runtime/common' } common-runtime = { path = 'runtime/common' }
dc-distance = { path = 'client/distance' }
distance-oracle = { path = 'distance-oracle', optional = true }
g1-runtime = { path = 'runtime/g1', optional = true } g1-runtime = { path = 'runtime/g1', optional = true }
gdev-runtime = { path = 'runtime/gdev', optional = true } gdev-runtime = { path = 'runtime/gdev', optional = true }
gtest-runtime = { path = 'runtime/gtest', optional = true } gtest-runtime = { path = 'runtime/gtest', optional = true }
pallet-certification = { path = 'pallets/certification' } pallet-certification = { path = 'pallets/certification' }
pallet-oneshot-account = { path = 'pallets/oneshot-account' } pallet-oneshot-account = { path = 'pallets/oneshot-account' }
sp-distance = { path = 'primitives/distance' }
sp-membership = { path = 'primitives/membership' } sp-membership = { path = 'primitives/membership' }
# crates.io dependencies # crates.io dependencies
...@@ -131,9 +140,12 @@ try-runtime-cli = { git = "https://github.com/duniter/substrate", branch = "duni ...@@ -131,9 +140,12 @@ try-runtime-cli = { git = "https://github.com/duniter/substrate", branch = "duni
resolver = "2" resolver = "2"
members = [ members = [
'client/distance',
'distance-oracle',
'end2end-tests', 'end2end-tests',
'live-tests', 'live-tests',
'pallets/certification', 'pallets/certification',
'pallets/distance',
'pallets/duniter-test-parameters', 'pallets/duniter-test-parameters',
'pallets/duniter-test-parameters/macro', 'pallets/duniter-test-parameters/macro',
'pallets/duniter-wot', 'pallets/duniter-wot',
...@@ -144,6 +156,7 @@ members = [ ...@@ -144,6 +156,7 @@ members = [
'pallets/universal-dividend', 'pallets/universal-dividend',
'pallets/upgrade-origin', 'pallets/upgrade-origin',
'primitives/membership', 'primitives/membership',
'primitives/distance',
'runtime/common', 'runtime/common',
'runtime/gdev', 'runtime/gdev',
'xtask', 'xtask',
......
[package]
authors = ['tuxmain <tuxmain@zettascript.org>']
description = 'Duniter client distance'
edition = '2021'
homepage = 'https://duniter.org'
license = 'AGPL-3.0'
name = 'dc-distance'
readme = 'README.md'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '1.0.0'
[dependencies]
pallet-distance = { path = "../../pallets/distance" }
sp-distance = { path = "../../primitives/distance" }
log = "0.4"
thiserror = "1.0.30"
# substrate
scale-info = { version = "2.1.1", features = ["derive"] }
[dependencies.codec]
features = ['derive']
package = 'parity-scale-codec'
version = '3.1.5'
[dependencies.frame-support]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sc-client-api]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-core]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-keystore]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
[dependencies.sp-runtime]
git = 'https://github.com/duniter/substrate'
branch = 'duniter-substrate-v0.9.32'
### DOC ###
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
// Copyright 2022 Axiom-Team
//
// This file is part of Substrate-Libre-Currency.
//
// Substrate-Libre-Currency 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.
//
// Substrate-Libre-Currency 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 Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
use codec::{Decode, Encode};
use frame_support::pallet_prelude::*;
use sc_client_api::{ProvideUncles, StorageKey, StorageProvider};
use scale_info::TypeInfo;
use sp_runtime::{generic::BlockId, traits::Block as BlockT, AccountId32};
use std::path::PathBuf;
type IdtyIndex = u32;
#[derive(Debug, thiserror::Error)]
pub enum Error<B: BlockT> {
#[error("Could not retrieve the block hash for block id: {0:?}")]
NoHashForBlockId(BlockId<B>),
}
/// Create a new [`sp_distance::InherentDataProvider`] at the given block.
pub fn create_distance_inherent_data_provider<B, C, Backend>(
client: &C,
parent: B::Hash,
distance_dir: PathBuf,
owner_keys: &[sp_core::sr25519::Public],
) -> Result<sp_distance::InherentDataProvider<IdtyIndex>, sc_client_api::blockchain::Error>
where
B: BlockT,
C: ProvideUncles<B> + StorageProvider<B, Backend>,
Backend: sc_client_api::Backend<B>,
IdtyIndex: Decode + Encode + PartialEq + TypeInfo,
{
let &[owner_key] = owner_keys else {
return Ok(sp_distance::InherentDataProvider::<IdtyIndex>::new(
None
))
};
let owner_key = sp_runtime::AccountId32::new(owner_key.0);
let session_index = client
.storage(
&parent,
&StorageKey(
frame_support::storage::storage_prefix(b"Session", b"CurrentIndex").to_vec(),
),
)
.expect("CurrentIndex is Err")
.map_or(0, |raw| {
u32::decode(&mut &raw.0[..]).expect("cannot decode CurrentIndex")
});
let published_results = client
.storage(
&parent,
&StorageKey(
frame_support::storage::storage_prefix(
b"Distance",
match session_index % 3 {
0 => b"StoragePublishedResults1",
1 => b"StoragePublishedResults2",
2 => b"StoragePublishedResults0",
_ => unreachable!("n%3<3"),
},
)
.to_vec(),
),
)?
.map_or_else(Default::default, |raw| {
pallet_distance::EvaluationPool::<AccountId32, IdtyIndex>::decode(&mut &raw.0[..])
.expect("cannot decode EvaluationPool")
});
// Have we already published a result for this session?
if published_results.evaluators.contains(&owner_key) {
return Ok(sp_distance::InherentDataProvider::<IdtyIndex>::new(None));
}
// Read evaluation result from file, if it exists
let evaluation_result = match std::fs::read(distance_dir.join(session_index.to_string())) {
Ok(data) => data,
Err(e) => {
match e.kind() {
std::io::ErrorKind::NotFound => {}
_ => {
log::error!("Cannot read distance evaluation result file: {e:?}");
}
}
return Ok(sp_distance::InherentDataProvider::<IdtyIndex>::new(None));
}
};
Ok(sp_distance::InherentDataProvider::<IdtyIndex>::new(Some(
sp_distance::ComputationResult::decode(&mut evaluation_result.as_slice()).unwrap(),
)))
}
[package]
name = "distance-oracle"
version = "0.1.0"
authors = ["tuxmain <tuxmain@zettascript.org>"]
repository = "https://git.duniter.org/nodes/rust/duniter-v2s"
license = "AGPL-3.0-only"
edition = "2021"
[dependencies]
sp-distance = { path = "../primitives/distance" }
codec = { package = "parity-scale-codec", version = "3.1.5" }
fnv = "1.0.7"
log = "0.4.17"
num-traits = "0.2.15"
rayon = "1.7.0"
simple_logger = "4.2.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" }
subxt = { git = 'https://github.com/duniter/subxt.git', branch = "duniter-substrate-v0.9.32" }
time = "<=0.3.23"# required for MSRV
time-macros = "=0.2.10"
# standalone only
clap = { version = "4.0", features = ["derive"], optional = true }
tokio = { version = "1.15.0", features = [
"rt-multi-thread",
"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"]
[[bin]]
name = "distance-oracle"
required-features = ["standalone"]
# Distance oracle
> for explanation about the Duniter web of trust, see https://duniter.org/wiki/web-of-trust/deep-dive-wot/
Distance computation on the Duniter web of trust is an expensive operation that should not be included in the runtime for multiple reasons:
- it could exceed the time available for a block computation
- it takes a lot of resource from the host machine
- the result is not critical to the operation of Ğ1
It is then separated into an other program that the user (a duniter smith) can choose to run or not. This program publishes its result in a inherent and the network selects the median of the results given by the smith who published some.
## Structure
This feature is organized in multiple parts:
- **/distance-oracle/** (here): binary executing the distance algorithm
- **/primitives/distance/**: primitive types used both by client and runtime
- **/client/distance/**: exposes the `create_distance_inherent_data_provider` which provides data to the runtime
- **/pallets/distance/**: distance pallet exposing type, traits, storage/calls/hooks executing in the runtime
\ No newline at end of file
// Copyright 2023 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 crate::runtime;
use sp_core::H256;
use subxt::storage::StorageKey;
pub type Client = subxt::OnlineClient<crate::RuntimeConfig>;
pub type AccountId = subxt::ext::sp_runtime::AccountId32;
pub type IdtyIndex = u32;
pub async fn client(rpc_url: String) -> Client {
Client::from_url(rpc_url)
.await
.expect("Cannot create RPC client")
}
pub async fn parent_hash(client: &Client) -> H256 {
client
.storage()
.fetch(&runtime::storage().system().parent_hash(), None)
.await
.expect("Cannot fetch parent hash")
.expect("Parent hash is None")
}
pub async fn current_session(client: &Client, parent_hash: H256) -> u32 {
client
.storage()
.fetch(
&runtime::storage().session().current_index(),
Some(parent_hash),
)
.await
.expect("Cannot fetch current session")
.unwrap_or_default()
}
pub async fn current_pool(
client: &Client,
parent_hash: H256,
current_session: u32,
) -> Option<runtime::runtime_types::pallet_distance::types::EvaluationPool<AccountId, IdtyIndex>> {
client
.storage()
.fetch(
&match current_session % 3 {
0 => runtime::storage().distance().evaluation_pool1(),
1 => runtime::storage().distance().evaluation_pool2(),
2 => runtime::storage().distance().evaluation_pool0(),
_ => unreachable!("n%3<3"),
},
Some(parent_hash),
)
.await
.expect("Cannot fetch current pool")
}
pub async fn evaluation_block(client: &Client, parent_hash: H256) -> H256 {
client
.storage()
.fetch(
&runtime::storage().distance().evaluation_block(),
Some(parent_hash),
)
.await
.expect("Cannot fetch evaluation block")
.expect("No evaluation block")
}
pub async fn member_iter(client: &Client, evaluation_block: H256) -> MemberIter {
MemberIter(
client
.storage()
.iter(
runtime::storage().membership().membership(0),
100,
Some(evaluation_block),
)
.await
.expect("Cannot fetch memberships"),
)
}
pub struct MemberIter(
subxt::storage::KeyIter<
crate::RuntimeConfig,
Client,
subxt::metadata::DecodeStaticType<
runtime::runtime_types::sp_membership::MembershipData<u32>,
>,
>,
);
impl MemberIter {
pub async fn next(&mut self) -> Result<Option<IdtyIndex>, subxt::error::Error> {
Ok(self
.0
.next()
.await?
.map(|(storage_key, _membership_data)| idty_id_from_storage_key(&storage_key)))
}
}
pub async fn cert_iter(client: &Client, evaluation_block: H256) -> CertIter {
CertIter(
client
.storage()
.iter(
runtime::storage().cert().certs_by_receiver(0),
100,
Some(evaluation_block),
)
.await
.expect("Cannot fetch certifications"),
)
}
pub struct CertIter(
subxt::storage::KeyIter<
crate::RuntimeConfig,
Client,
subxt::metadata::DecodeStaticType<Vec<(IdtyIndex, u32)>>,
>,
);
impl CertIter {
pub async fn next(
&mut self,
) -> Result<Option<(IdtyIndex, Vec<(IdtyIndex, u32)>)>, subxt::error::Error> {
Ok(self
.0
.next()
.await?
.map(|(storage_key, issuers)| (idty_id_from_storage_key(&storage_key), issuers)))
}
}
fn idty_id_from_storage_key(storage_key: &StorageKey) -> IdtyIndex {
u32::from_le_bytes(storage_key.as_ref()[40..44].try_into().unwrap())
}
// Copyright 2023 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(not(test))]
pub mod api;
#[cfg(test)]
pub mod mock;
#[cfg(test)]
mod tests;
#[cfg(test)]
pub use mock as api;
use api::{AccountId, IdtyIndex};
use codec::Encode;
use fnv::{FnvHashMap, FnvHashSet};
use log::{debug, error, warn};
use rayon::iter::IntoParallelRefIterator;
use rayon::iter::ParallelIterator;
use std::io::Write;
use std::path::PathBuf;
// TODO select metadata file using features
#[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")]
pub mod runtime {}
pub enum RuntimeConfig {}
impl subxt::config::Config for RuntimeConfig {
type Index = u32;
type BlockNumber = u32;
type Hash = sp_core::H256;
type Hashing = subxt::ext::sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>;
type Header = subxt::ext::sp_runtime::generic::Header<
Self::BlockNumber,
subxt::ext::sp_runtime::traits::BlakeTwo256,
>;
type Signature = subxt::ext::sp_runtime::MultiSignature;
type ExtrinsicParams = subxt::tx::BaseExtrinsicParams<Self, Tip>;
}
#[derive(Copy, Clone, Debug, Default, Encode)]
pub struct Tip {
#[codec(compact)]
tip: u64,
}
impl Tip {
pub fn new(amount: u64) -> Self {
Tip { tip: amount }
}
}
impl From<u64> for Tip {
fn from(n: u64) -> Self {
Self::new(n)
}
}
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_and_save(client: &api::Client, settings: Settings) {
let Some((evaluation, current_session, evaluation_result_path)) = run(client, &settings, true).await else {return};
let mut evaluation_result_file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&evaluation_result_path)
.unwrap_or_else(|e| {
panic!(
"Cannot open distance evaluation result file `{evaluation_result_path:?}`: {e:?}"
)
});
evaluation_result_file
.write_all(
&sp_distance::ComputationResult {
distances: evaluation,
}
.encode(),
)
.unwrap_or_else(|e| {
panic!(
"Cannot write distance evaluation result to file `{evaluation_result_path:?}`: {e:?}"
)
});
// Remove old results
let mut files_to_remove = Vec::new();
for entry in settings
.evaluation_result_dir
.read_dir()
.unwrap_or_else(|e| {
panic!(
"Cannot read distance evaluation result directory `{0:?}`: {e:?}",
settings.evaluation_result_dir
)
})
.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| warn!("Cannot remove old result file `{f:?}`: {e:?}"));
});
}
/// Returns `Option<(evaluation, current_session, evaluation_result_path)>`
pub async fn run(
client: &api::Client,
settings: &Settings,
handle_fs: bool,
) -> Option<(Vec<sp_runtime::Perbill>, u32, PathBuf)> {
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) = api::current_pool(client, parent_hash, current_session).await
else {
debug!("Nothing to do: Pool does not exist");
return None
};
// Stop if nothing to evaluate
if evaluation_pool.evaluations.0.is_empty() {
debug!("Nothing to do: Pool is empty");
return None;
}
let evaluation_result_path = settings
.evaluation_result_dir
.join((current_session + 1).to_string());
if handle_fs {
// Stop if already evaluated
if evaluation_result_path.try_exists().unwrap() {
debug!("Nothing to do: File already exists");
return None;
}
std::fs::create_dir_all(&settings.evaluation_result_dir).unwrap_or_else(|e| {
error!(
"Cannot create distance evaluation result directory `{0:?}`: {e:?}",
settings.evaluation_result_dir
);
});
}
let evaluation_block = api::evaluation_block(client, parent_hash).await;
// member idty -> issued certs
let mut members = FnvHashMap::<IdtyIndex, u32>::default();
let mut members_iter = api::member_iter(client, evaluation_block).await;
while let Some(member_idty) = members_iter.next().await.unwrap() {
members.insert(member_idty, 0);
}
let min_certs_for_referee = (members.len() as f32)
.powf(1. / (settings.max_depth as f32))
.ceil() as u32;
// idty -> received certs
let mut received_certs = FnvHashMap::<IdtyIndex, Vec<IdtyIndex>>::default();
let mut certs_iter = api::cert_iter(client, evaluation_block).await;
while let Some((receiver, issuers)) = certs_iter
.next()
.await
.expect("Cannot fetch next certification")
{
if (issuers.len() as u32) < min_certs_for_referee {
// This member is not referee
members.remove(&receiver);
}
for (issuer, _removable_on) in issuers.iter() {
if let Some(issued_certs) = members.get_mut(issuer) {
*issued_certs += 1;
}
}
received_certs.insert(
receiver,
issuers
.into_iter()
.map(|(issuer, _removable_on)| issuer)
.collect(),
);
}
// Only retain referees
members.retain(|_idty, issued_certs| *issued_certs >= min_certs_for_referee);
let referees = members;
let evaluation = evaluation_pool
.evaluations
.0
.as_slice()
.par_iter()
.map(|(idty, _)| distance_rule(&received_certs, &referees, settings.max_depth, *idty))
.collect();
Some((evaluation, current_session, evaluation_result_path))
}
fn distance_rule_recursive(
received_certs: &FnvHashMap<IdtyIndex, Vec<IdtyIndex>>,
referees: &FnvHashMap<IdtyIndex, u32>,
idty: IdtyIndex,
accessible_referees: &mut FnvHashSet<IdtyIndex>,
known_idties: &mut FnvHashMap<IdtyIndex, u32>,
depth: u32,
) {
// Do not re-explore identities that have already been explored at least as deeply
match known_idties.entry(idty) {
std::collections::hash_map::Entry::Occupied(mut entry) => {
if *entry.get() >= depth {
return;
} else {
*entry.get_mut() = depth;
}
}
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(depth);
}
}
// If referee, add it to the list
if referees.contains_key(&idty) {
accessible_referees.insert(idty);
}
// If reached the maximum distance, stop exploring
if depth == 0 {
return;
}
// Explore certifiers
for &certifier in received_certs.get(&idty).unwrap_or(&vec![]).iter() {
distance_rule_recursive(
received_certs,
referees,
certifier,
accessible_referees,
known_idties,
depth - 1,
);
}
}
/// Returns the fraction `nb_accessible_referees / nb_referees`
fn distance_rule(
received_certs: &FnvHashMap<IdtyIndex, Vec<IdtyIndex>>,
referees: &FnvHashMap<IdtyIndex, u32>,
depth: u32,
idty: IdtyIndex,
) -> sp_runtime::Perbill {
let mut accessible_referees =
FnvHashSet::<IdtyIndex>::with_capacity_and_hasher(referees.len(), Default::default());
let mut known_idties =
FnvHashMap::<IdtyIndex, u32>::with_capacity_and_hasher(referees.len(), Default::default());
distance_rule_recursive(
received_certs,
referees,
idty,
&mut accessible_referees,
&mut known_idties,
depth,
);
if referees.contains_key(&idty) {
sp_runtime::Perbill::from_rational(
accessible_referees.len() as u32 - 1,
referees.len() as u32 - 1,
)
} else {
sp_runtime::Perbill::from_rational(accessible_referees.len() as u32, referees.len() as u32)
}
}
// Copyright 2023 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 clap::Parser;
#[derive(Debug, clap::Parser)]
struct Cli {
#[clap(short = 'd', long, default_value = "/tmp/duniter/chains/gdev/distance")]
evaluation_result_dir: String,
/// Maximum depth to explore the WoT graph for referees
#[clap(short = 'D', long, default_value = "5")]
max_depth: u32,
#[clap(short = 'u', long, default_value = "ws://127.0.0.1:9944")]
rpc_url: String,
/// Log level (off, error, warn, info, debug, trace)
#[clap(short = 'l', long, default_value = "info")]
log: log::LevelFilter,
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
simple_logger::SimpleLogger::new()
.with_level(cli.log)
.init()
.unwrap();
distance_oracle::run_and_save(
&distance_oracle::api::client(cli.rpc_url.clone()).await,
distance_oracle::Settings {
evaluation_result_dir: cli.evaluation_result_dir.into(),
max_depth: cli.max_depth,
rpc_url: cli.rpc_url,
},
)
.await;
}
// Copyright 2023 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 crate::runtime::runtime_types::{
pallet_distance::median::MedianAcc, sp_arithmetic::per_things::Perbill,
};
use dubp_wot::{data::rusty::RustyWebOfTrust, WebOfTrust, WotId};
use sp_core::H256;
use std::collections::BTreeSet;
pub struct Client {
wot: RustyWebOfTrust,
pub pool_len: usize,
}
pub type AccountId = subxt::ext::sp_runtime::AccountId32;
pub type IdtyIndex = u32;
pub struct EvaluationPool<AccountId: Ord, IdtyIndex> {
pub evaluations: (Vec<(IdtyIndex, MedianAcc<Perbill>)>,),
pub evaluators: BTreeSet<AccountId>,
}
pub async fn client(_rpc_url: String) -> Client {
unimplemented!()
}
pub fn client_from_wot(wot: RustyWebOfTrust) -> Client {
Client { wot, pool_len: 1 }
}
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(EvaluationPool {
evaluations: (client
.wot
.get_enabled()
.into_iter()
.chain(client.wot.get_disabled().into_iter())
.zip(0..client.pool_len)
.map(|(wot_id, _)| {
(wot_id.0 as IdtyIndex, unsafe {
std::mem::transmute((Vec::<()>::new(), Option::<u32>::None, 0))
})
})
.collect(),),
evaluators: 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) -> MemberIter {
MemberIter(client.wot.get_enabled().into_iter())
}
pub struct MemberIter(std::vec::IntoIter<WotId>);
impl MemberIter {
pub async fn next(&mut self) -> Result<Option<IdtyIndex>, subxt::error::Error> {
Ok(self.0.next().map(|wot_id| wot_id.0 as u32))
}
}
pub async fn cert_iter(client: &Client, _evaluation_block: H256) -> CertIter {
CertIter(
client
.wot
.get_enabled()
.iter()
.chain(client.wot.get_disabled().iter())
.map(|wot_id| {
(
wot_id.0 as IdtyIndex,
client
.wot
.get_links_source(*wot_id)
.unwrap_or_default()
.into_iter()
.map(|wot_id| (wot_id.0 as IdtyIndex, 0))
.collect::<Vec<(IdtyIndex, u32)>>(),
)
})
.collect::<Vec<_>>()
.into_iter(),
)
}
pub struct CertIter(std::vec::IntoIter<(IdtyIndex, Vec<(IdtyIndex, u32)>)>);
impl CertIter {
pub async fn next(&mut self) -> Result<Option<(u32, Vec<(u32, u32)>)>, subxt::error::Error> {
Ok(self.0.next())
}
}
// Copyright 2023 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 dubp_wot::{
data::rusty::RustyWebOfTrust, operations::distance::DistanceCalculator, WebOfTrust,
};
use flate2::read::ZlibDecoder;
use sp_runtime::Perbill;
use std::{fs::File, io::Read};
#[tokio::test]
#[ignore = "long to execute"]
async fn test_distance_against_v1() {
let wot = wot_from_v1_file();
let n = wot.size();
let max_depth = 5;
let min_certs_for_referee = (wot.get_enabled().len() as f32).powf(1. / 5.).ceil() as u32;
// Reference implementation
let ref_calculator = dubp_wot::operations::distance::RustyDistanceCalculator;
let t_a = std::time::Instant::now();
let ref_results: Vec<Perbill> = wot
.get_enabled()
.into_iter()
.chain(wot.get_disabled().into_iter())
.zip(0..n)
.map(|(i, _)| {
let result = ref_calculator
.compute_distance(
&wot,
dubp_wot::operations::distance::WotDistanceParameters {
node: i,
sentry_requirement: min_certs_for_referee,
step_max: max_depth,
x_percent: 0.8,
},
)
.unwrap();
Perbill::from_rational(result.success, result.sentries)
})
.collect();
println!("ref time: {}", t_a.elapsed().as_millis());
// Our implementation
let mut client = crate::api::client_from_wot(wot);
client.pool_len = n;
let t_a = std::time::Instant::now();
let results = crate::run(
&client,
&crate::Settings {
max_depth,
..Default::default()
},
false,
)
.await
.unwrap();
println!("new time: {}", t_a.elapsed().as_millis());
assert_eq!(results.0.len(), n);
let mut errors: Vec<_> = results
.0
.iter()
.zip(ref_results.iter())
.map(|(r, r_ref)| r.deconstruct() as i64 - r_ref.deconstruct() as i64)
.collect();
errors.sort_unstable();
println!(
"Error: {:?} / {:?} / {:?} / {:?} / {:?} (min / 1Q / med / 3Q / max)",
errors[0],
errors[errors.len() / 4],
errors[errors.len() / 2],
errors[errors.len() * 3 / 4],
errors[errors.len() - 1]
);
let correct_results = results
.0
.iter()
.zip(ref_results.iter())
.map(|(r, r_ref)| (r == r_ref) as usize)
.sum::<usize>();
println!("Correct results: {correct_results} / {n}");
assert_eq!(correct_results, n);
}
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")
}
File added
...@@ -20,6 +20,9 @@ Only use `identity` pallet. The `membership` calls are disabled. ...@@ -20,6 +20,9 @@ Only use `identity` pallet. The `membership` calls are disabled.
1. The account that wants to gain membership needs to exists. 1. The account that wants to gain membership needs to exists.
1. Any account that already has membership and respects the identity creation period can create an identity for another account, using `identity.createIdentity`. 1. Any account that already has membership and respects the identity creation period can create an identity for another account, using `identity.createIdentity`.
1. The account has to confirm its identity with a name, using `identity.confirmIdentity`. The name must be ASCII alphanumeric, punctuation or space characters: ``/^[-!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~a-zA-Z0-9 ]{3,64}$/`` (additionally, trailing spaces and double spaces are forbidden, as a phishing countermeasure). If the name is already used, the call will fail. 1. The account has to confirm its identity with a name, using `identity.confirmIdentity`. The name must be ASCII alphanumeric, punctuation or space characters: ``/^[-!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~a-zA-Z0-9 ]{3,64}$/`` (additionally, trailing spaces and double spaces are forbidden, as a phishing countermeasure). If the name is already used, the call will fail.
1. 4 different member accounts must certify the account using `cert.addCert`.
1. The distance evaluation must be requested for the pending identity using `distance.evaluateDistance`.
1. 3 sessions later, if the distance rule is respected, `identity.validateIdentity` can be called.
## Change key ## Change key
......
# Distance rule evaluation
The [distance rule](https://duniter.org/blog/duniter-deep-dive-wot/) is computationally too heavy to be handled by the runtime. Therefore it is computed offchain using the distance oracle.
Distance evaluation is operated on a voluntary basis by individual smiths. Since evaluators can lie or make errors, the result considered for applying the distance rule is the median of results published by the different evaluators.
## Running distance evaluation
Any smith member authoring blocks can run a distance evaluation oracle. It is better to have a machine more powerful than the reference machine.
The simplest way is to run the oracle on the same machine as Duniter.
Build the oracle:
cargo build --release -p distance-oracle
It will be available at `./target/release/distance-oracle`. Move it to somewhere appropriate.
Add this line to your cron with the command `crontab -e`: (add option `-u <user>` to edit another user's cron)
4,24,44 * * * * nice -n 2 /absolute/path/to/distance-oracle
The precise hours don't matter so you can pick random values, but it should run at least one time per hour, and running it more often decreases the risk of problem in case of missing blocks or temporary network failure.
If the evaluation ran successfully in a session, the next runs in the same session won't re-evaluate the same data.
The `nice -n 2` lowers the oracle's priority, so that Duniter has the priority even when the oracle wants to use all the cores.
### Additional Duniter configuration
Duniter should keep states at least one session old, that it 600 blocks (while 256 is the default). Use the option `--state-pruning 600` if your node is not already an archive (`--state-pruning archive`).
...@@ -14,6 +14,7 @@ async-trait = "0.1" ...@@ -14,6 +14,7 @@ async-trait = "0.1"
clap = { version = "3.2.23", features = ["derive"] } clap = { version = "3.2.23", features = ["derive"] }
ctrlc = "3.2.2" ctrlc = "3.2.2"
cucumber = "0.11" cucumber = "0.11"
distance-oracle = { path = "../distance-oracle", default_features = false }
env_logger = "0.9.0" env_logger = "0.9.0"
hex = "0.4" hex = "0.4"
notify = "4.0" notify = "4.0"
...@@ -26,4 +27,4 @@ tokio = { version = "1.15.0", features = ["macros"] } ...@@ -26,4 +27,4 @@ tokio = { version = "1.15.0", features = ["macros"] }
[[test]] [[test]]
name = "cucumber_tests" name = "cucumber_tests"
harness = false # allows Cucumber to print output instead of libtest harness = false # allows Cucumber to print output instead of libtest
...@@ -163,7 +163,7 @@ To work, the integration tests need to have the runtime metadata up to date, her ...@@ -163,7 +163,7 @@ To work, the integration tests need to have the runtime metadata up to date, her
them: them:
```bash ```bash
subxt metadata -f bytes > resources/metadata.scale subxt metadata -f bytes --version 14 > resources/metadata.scale
``` ```
If you don't have subxt, install it: `cargo install subxt-cli` If you don't have subxt, install it: `cargo install subxt-cli`
......
...@@ -5,8 +5,9 @@ Feature: Identity creation ...@@ -5,8 +5,9 @@ Feature: Identity creation
# - account creation fees (3 ĞD) # - account creation fees (3 ĞD)
# - existential deposit (2 ĞD) # - existential deposit (2 ĞD)
# - transaction fees (below 1 ĞD) # - transaction fees (below 1 ĞD)
When alice sends 6 ĞD to dave When alice sends 7 ĞD to dave
When bob sends 6 ĞD to eve When bob sends 750 cĞD to dave
When charlie sends 6 ĞD to eve
# alice last certification is counted from block zero # alice last certification is counted from block zero
# then next cert can be done after cert_period, which is 15 # then next cert can be done after cert_period, which is 15
When 15 block later When 15 block later
...@@ -20,6 +21,12 @@ Feature: Identity creation ...@@ -20,6 +21,12 @@ Feature: Identity creation
When charlie certifies dave When charlie certifies dave
Then dave should be certified by bob Then dave should be certified by bob
Then dave should be certified by charlie 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 When eve validates dave identity
Then dave identity should be validated Then dave identity should be validated
// Copyright 2023 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;
use super::gdev::runtime_types::pallet_identity;
use super::*;
use crate::DuniterWorld;
use sp_keyring::AccountKeyring;
use subxt::ext::sp_runtime::AccountId32;
use subxt::tx::{PairSigner, Signer};
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().request_distance_evaluation(),
&origin,
BaseExtrinsicParamsBuilder::new(),
)
.await?,
)
.await?;
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::api::client(rpc_url.clone()).await,
&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::Distance(
gdev::runtime_types::pallet_distance::pallet::Call::force_update_evaluation {
evaluator: account_id.clone(),
computation_result:
gdev::runtime_types::sp_distance::ComputationResult {
distances: distances.into_iter().map(|res| unsafe{std::mem::transmute(res)}).collect(),
},
},
)
),
&origin,
BaseExtrinsicParamsBuilder::new(),
)
.await?,
)
.await?;
}
Ok(())
}
...@@ -84,9 +84,11 @@ pub async fn validate_identity(client: &Client, from: AccountKeyring, to: u32) - ...@@ -84,9 +84,11 @@ pub async fn validate_identity(client: &Client, from: AccountKeyring, to: u32) -
&from, &from,
BaseExtrinsicParamsBuilder::new(), BaseExtrinsicParamsBuilder::new(),
) )
.await?, .await
.unwrap(),
) )
.await?; .await
.unwrap();
Ok(()) Ok(())
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment