diff --git a/Cargo.lock b/Cargo.lock index 4231a086a501edfab6b11677e8bca2bfd4a74a73..ba62ddef50eefaf84463c67904569113bcbc42f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.8", +] + [[package]] name = "autocfg" version = "1.0.0" @@ -37,6 +46,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + [[package]] name = "bs58" version = "0.3.1" @@ -67,6 +82,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "core_affinity" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8a03115cc34fb0d7c321dd154a3914b3ca082ccc5c11d91bf7117dbbe7171f" +dependencies = [ + "kernel32-sys", + "libc", + "num_cpus", + "winapi 0.2.8", +] + [[package]] name = "crc32fast" version = "1.2.0" @@ -129,6 +156,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "697c714f50560202b1f4e2e09cd50a421881c83e9025db75d15f276616f04f40" +[[package]] +name = "ctor" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf6b25ee9ac1995c54d7adb2eff8cfffb7260bc774fb63c601ec65467f43cd9d" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "curve25519-dalek" version = "2.0.0" @@ -142,6 +179,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + [[package]] name = "digest" version = "0.8.1" @@ -151,6 +194,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "dubp-pow" +version = "0.1.0" +dependencies = [ + "core_affinity", + "duniter-common-tools", + "dup-crypto", + "log", + "rand", + "thiserror", +] + [[package]] name = "dubp-wot" version = "0.11.0" @@ -162,6 +217,16 @@ dependencies = [ "serde", ] +[[package]] +name = "duniter-common-tools" +version = "0.1.0" +dependencies = [ + "pretty_assertions", + "serde", + "shrinkwraprs", + "thiserror", +] + [[package]] name = "duniteroxyde" version = "0.2.9" @@ -238,6 +303,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.37" @@ -247,6 +321,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -350,6 +434,33 @@ dependencies = [ "libc", ] +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi 0.3.8", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +dependencies = [ + "ansi_term", + "ctor", + "difference", + "output_vt100", +] + [[package]] name = "proc-macro2" version = "1.0.10" @@ -368,6 +479,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -377,6 +511,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.3.0" @@ -431,7 +574,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.8", ] [[package]] @@ -475,6 +618,19 @@ dependencies = [ "syn", ] +[[package]] +name = "shrinkwraprs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e6744142336dfb606fe2b068afa2e1cca1ee6a5d8377277a92945d81fa331" +dependencies = [ + "bitflags", + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "spin" version = "0.5.2" @@ -633,6 +789,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.8" @@ -643,6 +805,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 551b803b8e95fa86068ce5cce35a3c0d1ba91003..8403939e17451b3dd860b765f41b95d46ddef650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,6 @@ [workspace] -members = ["neon/native"] +members = [ + "neon/native", + "rust-libs/pow", + "rust-libs/common-tools" +] diff --git a/rust-libs/common-tools/Cargo.toml b/rust-libs/common-tools/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..023d9db8b9ba5afcc4c7cb940acc13c0b9ecf8e4 --- /dev/null +++ b/rust-libs/common-tools/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "duniter-common-tools" +version = "0.1.0" +authors = ["elois <elois@duniter.org>"] +description = "Common rust tools for Duniter project." +keywords = ["duniter", "tools"] +license = "AGPL-3.0" +edition = "2018" + +[lib] +path = "src/lib.rs" + +[dependencies] +shrinkwraprs = "0.3.*" +serde = { version = "1.0.*", features = ["derive"] } +thiserror = "1.0.11" + +[dev-dependencies] +pretty_assertions = "0.6.1" diff --git a/rust-libs/common-tools/src/lib.rs b/rust-libs/common-tools/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..772035c354767798ec1951e4045a02e4601b0cee --- /dev/null +++ b/rust-libs/common-tools/src/lib.rs @@ -0,0 +1,82 @@ +// Copyright (C) 2019 Éloïs SANCHEZ +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Common rust tools for Duniter project. + +#![deny( + clippy::option_unwrap_used, + clippy::result_unwrap_used, + missing_docs, + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces +)] + +//pub mod fns; +//pub mod macros; +pub mod traits; + +use thiserror::Error; + +/// Percent +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct Percent(u8); + +/// Percent error +#[derive(Debug, Copy, Clone, PartialEq, Eq, Error, Hash)] +pub enum PercentError { + /// Percent too large (greater than 100) + #[error("percent too large (greater than 100)")] + TooLarge(u8), +} + +impl Percent { + /// Max percent (= 100%) + pub const MAX: Percent = Percent(100u8); + + /// New percent + pub fn new(percent: u8) -> Result<Percent, PercentError> { + if percent <= 100 { + Ok(Percent(percent)) + } else { + Err(PercentError::TooLarge(percent)) + } + } +} + +impl Into<u8> for Percent { + fn into(self) -> u8 { + self.0 + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_percent() { + assert_eq!(Percent::new(101), Err(PercentError::TooLarge(101))); + + let percent = Percent::new(100).expect("wrong percent"); + let percent_value: u8 = percent.into(); + assert_eq!(percent_value, 100u8); + } +} diff --git a/rust-libs/common-tools/src/traits.rs b/rust-libs/common-tools/src/traits.rs new file mode 100644 index 0000000000000000000000000000000000000000..2bee8814ed961c4ef97389a97a213dacdf509455 --- /dev/null +++ b/rust-libs/common-tools/src/traits.rs @@ -0,0 +1,23 @@ +// Copyright (C) 2019 Éloïs SANCHEZ +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Common rust traits for Duniter project. + +pub mod bool_ext; +pub mod merge; + +/// Allows to mark the real structure in order to differentiate it from the mocked structure, +/// is essential in some special cases +pub trait NotMock {} diff --git a/rust-libs/common-tools/src/traits/bool_ext.rs b/rust-libs/common-tools/src/traits/bool_ext.rs new file mode 100644 index 0000000000000000000000000000000000000000..612bab9caf00248df973ba6d56be7360303de60a --- /dev/null +++ b/rust-libs/common-tools/src/traits/bool_ext.rs @@ -0,0 +1,49 @@ +// Copyright (C) 2019 Éloïs SANCHEZ +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Trait BoolExt. + +/// Transform any type to Result +pub trait BoolExt { + /// Transform any type to result<(), E> + fn or_err<E>(self, err: E) -> Result<(), E>; + /// Transform any type to result<T, E> + fn as_result<T, E>(self, ok: T, err: E) -> Result<T, E>; + /// Reverse bool + fn not(&self) -> bool; +} + +impl BoolExt for bool { + #[inline] + fn or_err<E>(self, err: E) -> Result<(), E> { + if self { + Ok(()) + } else { + Err(err) + } + } + #[inline] + fn as_result<T, E>(self, ok: T, err: E) -> Result<T, E> { + if self { + Ok(ok) + } else { + Err(err) + } + } + #[inline] + fn not(&self) -> bool { + !self + } +} diff --git a/rust-libs/common-tools/src/traits/merge.rs b/rust-libs/common-tools/src/traits/merge.rs new file mode 100644 index 0000000000000000000000000000000000000000..31ac75376d6775f46d6c5c415d3642a6d346cf2c --- /dev/null +++ b/rust-libs/common-tools/src/traits/merge.rs @@ -0,0 +1,22 @@ +// Copyright (C) 2019 Éloïs SANCHEZ +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Trait Merge. + +/// Merge two instances of same type +pub trait Merge { + /// Merge two instances of same type + fn merge(self, other: Self) -> Self; +} diff --git a/rust-libs/pow/Cargo.toml b/rust-libs/pow/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8dcd7c51c9409f476875c314a264a7748e058d06 --- /dev/null +++ b/rust-libs/pow/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dubp-pow" +version = "0.1.0" +authors = ["elois <elois@duniter.org>"] +description = "Makes Proof of Work computations for the DUBP protocol." +repository = "https://git.duniter.org/libs/dubp-rs-libs" +readme = "README.md" +keywords = ["dubp", "dunitrust", "pow"] +license = "AGPL-3.0" +edition = "2018" + +[lib] +path = "src/lib.rs" + +[dependencies] +core_affinity = "0.5.10" +dup-crypto = { version = "0.15.0", default-features = false, features = ["rand"] } +duniter-common-tools = { path = "../common-tools" } +log = "0.4.8" +rand = "0.7.3" +thiserror = "1.0.11" + +[dev-dependencies] + +[features] diff --git a/rust-libs/pow/src/calculator.rs b/rust-libs/pow/src/calculator.rs new file mode 100644 index 0000000000000000000000000000000000000000..aab4e35555fa3225486da81771ebb713e5f15ffc --- /dev/null +++ b/rust-libs/pow/src/calculator.rs @@ -0,0 +1,187 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Makes Proof of Work computation for the DUBP protocol. + +use super::{ClusterMsg, PowConf, PowQuery, ValidProof, WorkerMsg, WorkerNonceArea}; +use crate::fast_check_hash_pattern::{PowDiff, PowDiffiV10}; +use duniter_common_tools::Percent; +use dup_crypto::hashs::Hash; +use dup_crypto::keys::{ed25519::Signator as Ed25519Signator, KeyPair, Signator}; +use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender}; +use std::time::{Duration, SystemTime}; + +#[cfg(not(target_arch = "arm"))] +const NUMBER_OF_ITERATIONS_WITHOUT_SLEEP: usize = 500; +#[cfg(target_arch = "arm")] +const NUMBER_OF_ITERATIONS_WITHOUT_SLEEP: usize = 200; + +pub enum PowCalculatorResult { + FoundValidProof(ValidProof), + PowStoppedByCluster, + PowStoppedByClusterAndNeedNotification, + Quit, +} + +pub struct PowCalculator<'a> { + begin_nonce: u64, + block_inner_hash: Hash, + cpu_percent: Percent, + difficulty: PowDiff, + marker: usize, + signator: Ed25519Signator, + sleep_factor: f64, + worker_nonce_area: WorkerNonceArea, + worker_receiver: &'a Receiver<WorkerMsg>, +} + +impl<'a> PowCalculator<'a> { + pub(crate) fn new( + pow_params: &PowQuery, + worker_nonce_area: WorkerNonceArea, + worker_receiver: &'a Receiver<WorkerMsg>, + ) -> Self { + // Init nonce + let marker = std::cmp::min(pow_params.marker, PowQuery::MARKER_MAX); + let begin_nonce = (pow_params.conf.prefix.get() as u64 * 10_000_000_000_000u64) + + (worker_nonce_area.0 as u64 * 100_000_000_000u64) + + (marker as u64 * 100_000_000u64); + + // Precompute sleep factor ((100 - cpu_percent) / cpu_percent) + let cpu_percent_u8: u8 = pow_params.conf.cpu.into(); + let sleep_factor: f64 = f64::from(100u8 - cpu_percent_u8) / f64::from(cpu_percent_u8); + + PowCalculator { + begin_nonce, + block_inner_hash: pow_params.block_inner_hash, + cpu_percent: pow_params.conf.cpu, + difficulty: PowDiff::V10(PowDiffiV10::from(pow_params.diff)), + marker: std::cmp::min(pow_params.marker, PowQuery::MARKER_MAX), + sleep_factor, + signator: pow_params.conf.keypair.generate_signator(), + worker_nonce_area, + worker_receiver, + } + } + + fn update(&mut self, new_pow_conf: &PowConf) { + // Recompute begin nonce + self.begin_nonce = (new_pow_conf.prefix.get() as u64 * 10_000_000_000_000u64) + + (self.worker_nonce_area.0 as u64 * 100_000_000_000u64) + + (self.marker as u64 * 100_000_000u64); + + // Update cup_percent + self.cpu_percent = new_pow_conf.cpu; + + // Update signator + self.signator = new_pow_conf.keypair.generate_signator(); + + // Recompute sleep factor ((100-cpu_percent) / cpu_percent) + let cpu_percent_u8: u8 = new_pow_conf.cpu.into(); + self.sleep_factor = f64::from(100u8 - cpu_percent_u8) / f64::from(cpu_percent_u8); + } + + pub(crate) fn start_computation_main_loop( + mut self, + cluster_sender: &Sender<ClusterMsg>, + ) -> PowCalculatorResult { + // Notify the cluster that the computation has started + if cluster_sender.send(ClusterMsg::PowStarted).is_err() { + return PowCalculatorResult::Quit; + } + + // Start computation main loop + let mut nonce = self.begin_nonce; + let mut iters_count = 0; + let mut new_pow_conf_opt = None; + loop { + let computation_start_time = SystemTime::now(); + for _ in 0..NUMBER_OF_ITERATIONS_WITHOUT_SLEEP { + let block_sig = self.signator.sign( + format!("InnerHash: {}\nNonce: {}\n", self.block_inner_hash, nonce).as_bytes(), + ); + let block_hash = Hash::compute( + format!( + "InnerHash: {}\nNonce: {}\n{}\n", + self.block_inner_hash, nonce, block_sig + ) + .as_bytes(), + ); + + if self.difficulty.check_hash(block_hash) { + return PowCalculatorResult::FoundValidProof(ValidProof { + iters_count: iters_count + 1, + nonce, + sig: block_sig, + hash: block_hash, + worker_nonce_area: self.worker_nonce_area, + }); + } else { + nonce += 1; + iters_count += 1; + } + } + // Compute sleep time needed to respect cpu percent + // This sleep time is achieved by a maximum time to listen incoming messages. + let timeout = if self.cpu_percent == Percent::MAX { + Duration::from_secs(0) + } else { + let computation_duration = SystemTime::now() + .duration_since(computation_start_time) + .expect("Duration error !") + .as_millis(); + let sleep_time = f64::from(computation_duration as u32) * self.sleep_factor; + Duration::from_millis(sleep_time.trunc() as u64) + }; + + // Listen incoming messages + match self.worker_receiver.recv_timeout(timeout) { + Ok(msg) => match msg { + WorkerMsg::ChangeConf(new_pow_conf) => { + new_pow_conf_opt = Some(new_pow_conf); + } + WorkerMsg::StartPoW { .. } => {} + WorkerMsg::StopPow => { + if cluster_sender.send(ClusterMsg::PowStopped).is_err() { + return PowCalculatorResult::Quit; + } else { + return PowCalculatorResult::PowStoppedByCluster; + } + } + WorkerMsg::StopPowAndNotifyRequestor => { + if cluster_sender.send(ClusterMsg::PowStopped).is_err() { + return PowCalculatorResult::Quit; + } else { + return PowCalculatorResult::PowStoppedByClusterAndNeedNotification; + } + } + WorkerMsg::Quit => return PowCalculatorResult::Quit, + }, + Err(RecvTimeoutError::Disconnected) => return PowCalculatorResult::Quit, + Err(RecvTimeoutError::Timeout) => continue, + } + + // If you receive a new conf, update calculator data and reset nonce + if let Some(ref new_pow_conf) = new_pow_conf_opt { + self.update(new_pow_conf); + new_pow_conf_opt = None; + nonce = self.begin_nonce + iters_count; + if cluster_sender.send(ClusterMsg::PowConfUpdated).is_err() { + return PowCalculatorResult::Quit; + } + } + } + } +} diff --git a/rust-libs/pow/src/fast_check_hash_pattern.rs b/rust-libs/pow/src/fast_check_hash_pattern.rs new file mode 100644 index 0000000000000000000000000000000000000000..2fa6add185c040181696d400137755de0c09807a --- /dev/null +++ b/rust-libs/pow/src/fast_check_hash_pattern.rs @@ -0,0 +1,147 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Fast check hash pattern for DUBP Proof of Work. + +use dup_crypto::hashs::Hash; +use log::info; + +pub enum PowDiff { + V10(PowDiffiV10), +} + +impl PowDiff { + #[inline] + pub fn check_hash(&self, hash: Hash) -> bool { + match self { + Self::V10(pow_diff_v10) => pow_diff_v10.check_hash(hash), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PowDiffiV10 { + pub zeros_bytes: usize, + pub remaining_byte_max: u8, + pub shift_remaining_byte: bool, +} + +impl From<usize> for PowDiffiV10 { + fn from(diff: usize) -> Self { + let zeros = diff / 16; + + PowDiffiV10 { + zeros_bytes: zeros / 2, + remaining_byte_max: 15u8 - (diff % 16) as u8, + shift_remaining_byte: zeros % 2 == 0, + } + } +} + +impl PowDiffiV10 { + #[inline] + #[allow(clippy::comparison_chain, clippy::needless_range_loop)] + pub fn check_hash(&self, hash: Hash) -> bool { + let bytes = hash.0; + for i in 0..32 { + if i < self.zeros_bytes { + if bytes[i] != 0u8 { + return false; + } else if i >= 3 { + info!("POW: Matched {} zeros with hash = {}", i, hash); + } else { + continue; + } + } else if i == self.zeros_bytes { + let remaining_byte = if self.shift_remaining_byte { + bytes[i] >> 4 + } else { + bytes[i] + }; + return remaining_byte <= self.remaining_byte_max; + } else { + return false; + } + } + + false + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn fast_check_hash_pattern_v10() { + let hash = + Hash::from_hex("B2ECA8B21738301F647C3DC960421992EC87ED9139F6FDDB07F8D495B4F0A6F1") + .expect("invalid hash"); + + assert_eq!( + PowDiffiV10 { + zeros_bytes: 0, + remaining_byte_max: 11, + shift_remaining_byte: true, + }, + PowDiffiV10::from(4), + ); + + assert!(PowDiffiV10::from(4).check_hash(hash)); + assert!(!PowDiffiV10::from(5).check_hash(hash)); + + let hash2 = + Hash::from_hex("000000F31738301F647C3DC960421992EC87ED9139F6FDDB07F8D495B4F0A6F1") + .expect("invalid hash"); + + assert_eq!( + PowDiffiV10 { + zeros_bytes: 2, + remaining_byte_max: 0, + shift_remaining_byte: false, + }, + PowDiffiV10::from(95), + ); + assert_eq!( + PowDiffiV10 { + zeros_bytes: 3, + remaining_byte_max: 15, + shift_remaining_byte: true, + }, + PowDiffiV10::from(96), + ); + + assert!(PowDiffiV10::from(95).check_hash(hash2)); + assert!(PowDiffiV10::from(96).check_hash(hash2)); + assert!(!PowDiffiV10::from(97).check_hash(hash2)); + + let hash3 = + Hash::from_hex("00000FB21738301F647C3DC960421992EC87ED9139F6FDDB07F8D495B4F0A6F1") + .expect("invalid hash"); + + assert_eq!( + PowDiffiV10 { + zeros_bytes: 2, + remaining_byte_max: 14, + shift_remaining_byte: false, + }, + PowDiffiV10::from(81), + ); + + assert!(PowDiffiV10::from(80).check_hash(hash3)); + assert!(!PowDiffiV10::from(81).check_hash(hash3)); + } +} diff --git a/rust-libs/pow/src/lib.rs b/rust-libs/pow/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..e46dd510a8dce81b7c784772849e0416544592f9 --- /dev/null +++ b/rust-libs/pow/src/lib.rs @@ -0,0 +1,577 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Handle Proof of Work cluster for the DUBP protocol. + +#![deny( + clippy::option_unwrap_used, + clippy::result_unwrap_used, + missing_docs, + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +mod calculator; +mod fast_check_hash_pattern; +mod query; +mod verify; + +pub use query::{PowConf, PowQuery}; +pub use verify::{verify_hash_pattern, InvalidHashPattern}; + +use crate::calculator::{PowCalculator, PowCalculatorResult}; +use crate::query::InvalidPowQuery; +use dup_crypto::hashs::Hash; +use dup_crypto::keys::ed25519::Signature; +use rand::seq::SliceRandom; +use rand::thread_rng; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; +use thiserror::Error; + +#[derive(Clone, Copy, Debug)] +/// Valid proof +pub struct ValidProof { + /// Number of iterations performed by the worker who found the proof + pub iters_count: u64, + /// Block nonce + pub nonce: u64, + /// Block signature + pub sig: Signature, + /// Block hash + pub hash: Hash, + /// Worker that found proof + pub worker_nonce_area: WorkerNonceArea, +} + +#[derive(Clone, Copy, Debug)] +/// Message for pow requestor +pub enum RequestorMsg { + /// Found valid proof + FoundValidProof(ValidProof), + /// Pow stopped + PowStoppedByCluster, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +/// Worker id +pub struct WorkerId(pub usize); + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +/// Nonce area "explored" by the worker +pub struct WorkerNonceArea(pub usize); + +#[derive(Clone, Debug)] +enum ClusterMsg { + Initialized(Sender<WorkerMsg>), + PowStarted, + PowConfUpdated, + PowStopped, +} + +#[derive(Clone, Debug)] +enum WorkerMsg { + StartPoW { + worker_nonce_area: WorkerNonceArea, + pow_query: PowQuery, + }, + ChangeConf(PowConf), + StopPowAndNotifyRequestor, + StopPow, + Quit, +} + +#[derive(Clone, Copy, Debug, Error)] +/// Pow cluster error +pub enum PowClusterError { + /// No available core + #[error("No available core.")] + NoAvailableCore, + /// Fail to init cluster + #[error("Fail to init cluster.")] + FailToInitCluster, + /// Fail to start pow + #[error("Fail to start pow.")] + FailToStartPow, + /// Fail to stop pow + #[error("Fail to stop pow.")] + FailToStopPow, + /// Invalid pow parameters + #[error("Invalid pow parameters: {}", _0)] + InvalidPowQuery(InvalidPowQuery), + /// A worker is unreachable + #[error("A worker is unreachable.")] + WorkerUnreachable, + /// A worker panicked + #[error("A worker panicked.")] + WorkerPanic, +} + +/// Pow cluster +#[derive(Debug)] +pub struct PowCluster { + cluster_receiver: Receiver<ClusterMsg>, + workers_handles: Vec<JoinHandle<()>>, + workers_ids_computing: Vec<WorkerId>, + workers_senders: Vec<Sender<WorkerMsg>>, +} + +impl PowCluster { + /// Get the number of workers + #[inline] + pub fn get_workers_count(&self) -> usize { + self.workers_handles.len() + } + /// Initialize pow cluster + pub fn init(max_cores_opt: Option<usize>, workers_name: &str) -> Result<Self, PowClusterError> { + if let Some(mut core_ids) = core_affinity::get_core_ids() { + core_ids.shuffle(&mut thread_rng()); + if let Some(max_cores) = max_cores_opt { + while core_ids.len() > max_cores { + core_ids.pop(); + } + } + + let count_cores = core_ids.len(); + + let (cluster_sender, cluster_receiver) = channel(); + + let workers_handles = core_ids + .into_iter() + .map(|core_id| { + let cluster_sender_clone = cluster_sender.clone(); + thread::Builder::new() + .name(format!("{}{}", workers_name, core_id.id + 1)) + .spawn(move || { + // Pin this thread to a single CPU core. + core_affinity::set_for_current(core_id); + + // Create worker channel and send worker sender to cluster + let (worker_sender, worker_receiver) = channel(); + let _ = + cluster_sender_clone.send(ClusterMsg::Initialized(worker_sender)); + + // Start worker main loop + worker_main_loop(cluster_sender_clone, &worker_receiver); + }) + }) + .collect::<std::io::Result<Vec<_>>>() + .map_err(|_| PowClusterError::FailToInitCluster)?; + + let mut workers_senders = Vec::with_capacity(count_cores); + for _ in 0..count_cores { + match cluster_receiver.recv_timeout(Duration::from_millis(1000)) { + Ok(msg) => match msg { + ClusterMsg::Initialized(worker_sender) => { + workers_senders.push(worker_sender); + } + _ => return Err(PowClusterError::FailToInitCluster), + }, + Err(_) => return Err(PowClusterError::FailToInitCluster), + } + } + + Ok(PowCluster { + cluster_receiver, + workers_handles, + workers_ids_computing: Vec::with_capacity(count_cores), + workers_senders, + }) + } else { + Err(PowClusterError::NoAvailableCore) + } + } + fn select_workers_ids(&mut self, number_of_workers_solicited_opt: Option<usize>) { + let workers_count = self.get_workers_count(); + let number_of_workers_solicited = + if let Some(number_of_workers_solicited) = number_of_workers_solicited_opt { + number_of_workers_solicited + } else { + workers_count + }; + let mut workers_ids: Vec<WorkerId> = (0..workers_count).map(WorkerId).collect(); + workers_ids.shuffle(&mut thread_rng()); + self.workers_ids_computing = workers_ids + .into_iter() + .take(number_of_workers_solicited) + .collect(); + } + /// Start PoW + pub fn start_pow( + &mut self, + pow_query: &PowQuery, + number_of_workers_solicited_opt: Option<usize>, + ) -> Result<(), PowClusterError> { + // Check pow query validity + pow_query + .verify() + .map_err(PowClusterError::InvalidPowQuery)?; + + // We must stop the pow because the new request won't be sent to all workers, + // so it's possible that some workers currently calculating are not selected to process this new request. + self.stop_pow(false)?; + + // Select workers + self.select_workers_ids(number_of_workers_solicited_opt); + + for (worker_nonce_area, worker_id) in self.workers_ids_computing.iter().enumerate() { + self.workers_senders[worker_id.0] + .send(WorkerMsg::StartPoW { + worker_nonce_area: WorkerNonceArea(worker_nonce_area), + pow_query: pow_query.clone(), + }) + .map_err(|_| PowClusterError::WorkerUnreachable)?; + } + for _ in 0..self.workers_ids_computing.len() { + match self + .cluster_receiver + .recv_timeout(Duration::from_millis(10_000)) + { + Ok(msg) => match msg { + ClusterMsg::PowStarted => continue, + _other => { + //println!("other={:?}", _other); + return Err(PowClusterError::FailToStartPow); + } + }, + Err(_e) => { + //println!("err={:?}", _e); + return Err(PowClusterError::FailToStartPow); + } + } + } + + Ok(()) + } + /// Update pow conf + pub fn change_pow_conf(&self, new_pow_conf: &PowConf) -> Result<(), PowClusterError> { + for worker_sender in &self.workers_senders { + worker_sender + .send(WorkerMsg::ChangeConf(new_pow_conf.clone())) + .map_err(|_| PowClusterError::WorkerUnreachable)?; + } + Ok(()) + } + /// Stop PoW + pub fn stop_pow(&self, notify_requestor: bool) -> Result<(), PowClusterError> { + let mut first_worker = true; + for worker_sender in &self.workers_senders { + if first_worker && notify_requestor { + worker_sender + .send(WorkerMsg::StopPowAndNotifyRequestor) + .map_err(|_| PowClusterError::WorkerUnreachable)?; + } else { + worker_sender + .send(WorkerMsg::StopPow) + .map_err(|_| PowClusterError::WorkerUnreachable)?; + } + first_worker = false; + } + for _ in 0..self.workers_senders.len() { + match self + .cluster_receiver + .recv_timeout(Duration::from_millis(1000)) + { + Ok(msg) => match msg { + ClusterMsg::PowStopped => continue, + _ => return Err(PowClusterError::FailToStopPow), + }, + Err(_) => return Err(PowClusterError::FailToStopPow), + } + } + Ok(()) + } + /// Destroy cluster + pub fn quit(self) { + for worker_sender in &self.workers_senders { + let _ = worker_sender.send(WorkerMsg::Quit); + } + for worker_handle in self.workers_handles { + let _ = worker_handle.join(); + } + } + /// Orders the workers to stop but does not check if they have actually stopped. + /// This method is unfortunately necessary for NodeJs binding because Neon does not allow methods that take the ownership on self. + /// Do not use this method if you can do otherwise. + #[doc(hidden)] + pub fn quit_unchecked(&mut self) { + for worker_sender in &self.workers_senders { + let _ = worker_sender.send(WorkerMsg::Quit); + } + while let Ok(_) = self.cluster_receiver.recv() {} + self.workers_handles = Vec::with_capacity(0); + } +} + +fn worker_main_loop(cluster_sender: Sender<ClusterMsg>, worker_receiver: &Receiver<WorkerMsg>) { + while let Ok(msg) = worker_receiver.recv() { + match msg { + WorkerMsg::StartPoW { + worker_nonce_area, + pow_query, + } => { + let calculator = PowCalculator::new(&pow_query, worker_nonce_area, worker_receiver); + match calculator.start_computation_main_loop(&cluster_sender) { + PowCalculatorResult::FoundValidProof(valid_proof) => { + if pow_query + .send_msg_to_requestor(RequestorMsg::FoundValidProof(valid_proof)) + .is_err() + { + break; + } else { + continue; + } + } + PowCalculatorResult::PowStoppedByCluster => continue, + PowCalculatorResult::PowStoppedByClusterAndNeedNotification => { + if pow_query + .send_msg_to_requestor(RequestorMsg::PowStoppedByCluster) + .is_err() + { + break; + } + } + PowCalculatorResult::Quit => break, + } + } + WorkerMsg::ChangeConf(_) => {} + WorkerMsg::StopPow | WorkerMsg::StopPowAndNotifyRequestor => { + if cluster_sender.send(ClusterMsg::PowStopped).is_err() { + break; + } + } + WorkerMsg::Quit => break, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use duniter_common_tools::Percent; + use dup_crypto::hashs::Hash; + use dup_crypto::keys::ed25519::Ed25519KeyPair; + use std::num::NonZeroUsize; + use std::sync::mpsc::{channel, Receiver}; + + const TESTS_WORKERS_NAME: &str = "pow_worker_"; + + fn gen_pow_query( + cpu_percent: u8, + diff: usize, + marker: usize, + requestor_sender: Sender<RequestorMsg>, + ) -> PowQuery { + PowQuery { + block_inner_hash: Hash::random().expect("fail to gen random hash"), + conf: PowConf { + cpu: Percent::new(cpu_percent).expect("invalid percent"), + keypair: Ed25519KeyPair::generate_random().expect("fail to gen random keypair"), + prefix: unsafe { NonZeroUsize::new_unchecked(2) }, + }, + diff, + marker, + requestor_sender, + } + } + + fn proof_found_before_timeout( + receiver: &Receiver<RequestorMsg>, + timeout: u64, + ) -> Result<bool, PowClusterError> { + if let Ok(RequestorMsg::FoundValidProof(proof)) = + receiver.recv_timeout(Duration::from_millis(timeout)) + { + println!( + "Find valid proof '{}' with nonce '{}'", + proof.hash, proof.nonce + ); + Ok(true) + } else { + Ok(false) + } + } + + #[test] + fn test_diff_zero() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(None, TESTS_WORKERS_NAME)?; + let (sender, receiver) = channel(); + let pow_query = gen_pow_query(100, 0, 0, sender); + pow_cluster.start_pow(&pow_query, None)?; + + if proof_found_before_timeout(&receiver, 10)? { + pow_cluster.quit(); + Ok(()) + } else { + pow_cluster.quit(); + panic!("must be found valid proof under 10 ms") + } + } + + #[test] + fn test_fast_pow() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(None, TESTS_WORKERS_NAME)?; + let (sender, receiver) = channel(); + let pow_query = gen_pow_query(100, 16, 0, sender); + pow_cluster.start_pow(&pow_query, None)?; + + if proof_found_before_timeout(&receiver, 100)? { + pow_cluster.quit(); + Ok(()) + } else { + pow_cluster.quit(); + panic!("must be found valid proof under 100 ms") + } + } + + #[test] + fn test_soliciting_a_single_worker() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(None, TESTS_WORKERS_NAME)?; + let (sender, receiver) = channel(); + let pow_query = gen_pow_query(100, 16, 0, sender); + pow_cluster.start_pow(&pow_query, Some(1))?; + + if let Ok(RequestorMsg::FoundValidProof(proof)) = + receiver.recv_timeout(Duration::from_millis(500)) + { + assert_eq!(proof.worker_nonce_area, WorkerNonceArea(0)); + } else { + pow_cluster.quit(); + panic!("must be found valid proof under 500 ms") + } + + pow_cluster.quit(); + Ok(()) + } + + #[test] + fn test_start_and_quit_unchecked() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(None, TESTS_WORKERS_NAME)?; + let (sender, _receiver) = channel(); + let pow_query = gen_pow_query(1, 150, 0, sender); + pow_cluster.start_pow(&pow_query, None)?; + assert!(pow_cluster.get_workers_count() > 0); + pow_cluster.quit_unchecked(); + assert!(pow_cluster.get_workers_count() == 0); + Ok(()) + } + + #[test] + fn test_stop_and_restart_pow() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(Some(2), TESTS_WORKERS_NAME)?; + let (sender, receiver) = channel(); + let pow_query = gen_pow_query(100, 120, 999, sender.clone()); + pow_cluster.start_pow(&pow_query, None)?; + assert!(pow_cluster.get_workers_count() > 0); + + if proof_found_before_timeout(&receiver, 1_000)? { + pow_cluster.quit(); + panic!("Must not found proof"); + } + + pow_cluster.stop_pow(true)?; + pow_cluster.stop_pow(true)?; // Pow should be stopped several times + + // Must be receive None message from first worker only + if let Ok(RequestorMsg::PowStoppedByCluster) = + receiver.recv_timeout(Duration::from_millis(3_000)) + { + // We receive pow cancellation signal from first worker + } else { + panic!("Must be not receive any message") + } + + let pow_query = gen_pow_query(100, 30, 999, sender); + pow_cluster.start_pow(&pow_query, None)?; + if proof_found_before_timeout(&receiver, 5_000)? { + // Workers found proof OK + } else { + panic!("must be found valid proof under 5 secs") + } + + pow_cluster.stop_pow(false)?; + + // Must be not receive any message from workers + if let Ok(RequestorMsg::PowStoppedByCluster) = + receiver.recv_timeout(Duration::from_millis(500)) + { + pow_cluster.quit(); + panic!("Must be not receive any message from workers"); + } else { + // We not receive any message OK + pow_cluster.quit(); + Ok(()) + } + } + + #[test] + fn test_hot_change_of_conf() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(Some(4), TESTS_WORKERS_NAME)?; + let (sender, receiver) = channel(); + let pow_query = gen_pow_query(1, 48, 999, sender); + pow_cluster.start_pow(&pow_query, None)?; + + if proof_found_before_timeout(&receiver, 1_000)? { + pow_cluster.quit(); + return Ok(()); + } + + println!("No found proof, change prefix and cpu"); + pow_cluster.change_pow_conf(&PowConf { + cpu: Percent::new(100).expect("invalid percent"), + keypair: pow_query.conf.keypair, + prefix: unsafe { NonZeroUsize::new_unchecked(3) }, + })?; + + if !proof_found_before_timeout(&receiver, 5_000)? { + pow_cluster.quit(); + panic!("must be found valid proof under 5 secs"); + } else { + pow_cluster.quit(); + Ok(()) + } + } + #[test] + fn test_hot_change_pow_query() -> Result<(), PowClusterError> { + let mut pow_cluster = PowCluster::init(Some(4), TESTS_WORKERS_NAME)?; + let (sender, receiver) = channel(); + let pow_query = gen_pow_query(1, 120, 999, sender.clone()); + pow_cluster.start_pow(&pow_query, None)?; + + if proof_found_before_timeout(&receiver, 1_000)? { + pow_cluster.quit(); + return Ok(()); + } + + println!("blockchain HEAD change, start new pow query"); + let pow_query_2 = gen_pow_query(50, 32, 0, sender); + pow_cluster.start_pow(&pow_query_2, None)?; + + if !proof_found_before_timeout(&receiver, 3_000)? { + pow_cluster.quit(); + panic!("must be found valid proof under 3 secs"); + } else { + pow_cluster.quit(); + Ok(()) + } + } +} diff --git a/rust-libs/pow/src/query.rs b/rust-libs/pow/src/query.rs new file mode 100644 index 0000000000000000000000000000000000000000..39e865a5a640acd88cf55bec4f664a04340d026e --- /dev/null +++ b/rust-libs/pow/src/query.rs @@ -0,0 +1,88 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Define Proof of Work request. + +use super::RequestorMsg; +use duniter_common_tools::Percent; +use dup_crypto::hashs::Hash; +use dup_crypto::keys::ed25519::Ed25519KeyPair; +use std::num::NonZeroUsize; +use std::sync::mpsc::{SendError, Sender}; +use thiserror::Error; + +/// PoW conf +#[derive(Clone, Debug)] +pub struct PowConf { + /// Cpu usage in % + pub cpu: Percent, + /// Leypair used to sign block + pub keypair: Ed25519KeyPair, + /// Nonce prefix + pub prefix: NonZeroUsize, +} + +impl PowConf { + const PREFIX_MAX: usize = 899; +} + +#[derive(Clone, Debug)] +/// PoW request +pub struct PowQuery { + /// Block inner hash + pub block_inner_hash: Hash, + /// PoW configuration + pub conf: PowConf, + /// PoW Difficulty + pub diff: usize, + /// None marker + pub marker: usize, + /// Proof sender + pub requestor_sender: Sender<RequestorMsg>, +} + +#[derive(Clone, Copy, Debug, Error)] +/// Invalid pow parameters +pub enum InvalidPowQuery { + /// Too large marker + #[error("Too large marker.")] + TooLargeMarker, + /// Too large prefix + #[error("Too large prefix.")] + TooLargePrefix, +} + +impl PowQuery { + /// Maximum value for nonce marker + pub const MARKER_MAX: usize = 999; + + pub(crate) fn verify(&self) -> Result<(), InvalidPowQuery> { + if self.marker > Self::MARKER_MAX { + Err(InvalidPowQuery::TooLargeMarker) + } else if self.conf.prefix.get() > PowConf::PREFIX_MAX { + Err(InvalidPowQuery::TooLargePrefix) + } else { + Ok(()) + } + } + + #[inline] + pub(crate) fn send_msg_to_requestor( + &self, + msg: RequestorMsg, + ) -> Result<(), SendError<RequestorMsg>> { + self.requestor_sender.send(msg) + } +} diff --git a/rust-libs/pow/src/verify.rs b/rust-libs/pow/src/verify.rs new file mode 100644 index 0000000000000000000000000000000000000000..d89eb4da20eeebca50c85009f58bbac44f07cc78 --- /dev/null +++ b/rust-libs/pow/src/verify.rs @@ -0,0 +1,71 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program 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, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 this program. If not, see <https://www.gnu.org/licenses/>. + +//! Makes Proof of Work computations for the DUBP protocol. + +use duniter_common_tools::traits::bool_ext::BoolExt; +use dup_crypto::hashs::Hash; + +static ZERO_STRING: &str = "0"; + +/// Invalid PoW pattern +#[derive(Debug, PartialEq)] +pub struct InvalidHashPattern { + expected_pattern: String, + actual_hash: String, +} + +#[inline] +/// Verify hash pattern +pub fn verify_hash_pattern(hash: Hash, diffi: usize) -> Result<(), InvalidHashPattern> { + let hash_string = hash.to_hex(); + let nb_zeros = diffi / 16; + let expected_pattern_last_hex_digit = 16 - (diffi % 16); + let repeated_zero_string = ZERO_STRING.repeat(nb_zeros); + let expected_pattern = if expected_pattern_last_hex_digit < 15 && nb_zeros < 64 { + let expected_pattern_last_char = + std::char::from_digit(expected_pattern_last_hex_digit as u32, 16) + .expect("expected_pattern_last_hex_digit is necessarily less than 16"); + let expected_pattern = format!( + "{}[0-{}]*", + repeated_zero_string, expected_pattern_last_char + ); + let actual_pattern_last_hex_digit = usize::from_str_radix( + hash_string + .get(nb_zeros..=nb_zeros) + .expect("Hash string is necessary greater than nb_zeros + 1."), + 16, + ) + .expect("Hash type guarantees a valid hexadecimal string."); + // remainder must be less than or equal to expected_end_pattern + (actual_pattern_last_hex_digit <= expected_pattern_last_hex_digit).or_err( + InvalidHashPattern { + expected_pattern: expected_pattern.clone(), + actual_hash: hash_string.clone(), + }, + )?; + expected_pattern + } else { + repeated_zero_string.clone() + }; + hash_string + .starts_with(&repeated_zero_string) + .or_err(InvalidHashPattern { + expected_pattern, + actual_hash: hash_string, + })?; + + Ok(()) +}