From f5b1111eacc81e6b088f11eae3148f6114795546 Mon Sep 17 00:00:00 2001 From: Oleg Fomenko <35123037+olegfomenko@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:47:30 +0300 Subject: [PATCH] Rewriting the hash_to_range function (#8) * rewriting the hash_to_range function to achieve better uniform distribution. Now it uses seeded random based on ChaCha12Rng instead of DefaultHash as before. Also, it fixes distribution in range by executing selection several times to achieve uniform distribution when range != 2^k * Adding comments * fix: resolved linter issues/failing tests --------- Co-authored-by: Nikita Masych --- .github/workflows/rust.yml | 1 + Cargo.toml | 2 + src/party.rs | 109 ++++++++++++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f8d9382..4e6ca37 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - epic/* push: branches: - main diff --git a/Cargo.toml b/Cargo.toml index 2fc08a5..4d16495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,5 @@ rkyv = { version = "0.7.44", features = ["validation"]} serde = { version = "1.0.207", features = ["derive"] } bincode = "1.3.3" tokio = { version = "1.39.2", features = ["full", "test-util"] } +rand = "0.9.0-alpha.2" +seeded-random = "0.6.0" \ No newline at end of file diff --git a/src/party.rs b/src/party.rs index bfd8a54..c32ddd1 100644 --- a/src/party.rs +++ b/src/party.rs @@ -7,6 +7,7 @@ use crate::message::{ }; use crate::{Value, ValueSelector}; use rkyv::{AlignedVec, Deserialize, Infallible}; +use seeded_random::{Random, Seed}; use std::cmp::{Ordering, PartialEq}; use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::Entry::Vacant; @@ -138,9 +139,29 @@ impl DefaultLeaderElector { /// Hash the seed to a value within a given range. fn hash_to_range(seed: u64, range: u64) -> u64 { - let mut hasher = DefaultHasher::new(); - seed.hash(&mut hasher); - hasher.finish() % range + // Select the `k` suck that value 2^k >= `range` and 2^k is the smallest. + let mut k = 64; + while 1u64 << (k - 1) >= range { + k -= 1; + } + + // The following algorithm selects a random u64 value using `ChaCha12Rng` + // and reduces the result to the k-bits such that 2^k >= `range` the closes power of to the `range`. + // After we check if the result lies in [0..`range`) or [`range`..2^k). + // In the first case result is an acceptable value generated uniformly. + // In the second case we repeat the process again with the incremented iterations counter. + // Ref: Practical Cryptography 1st Edition by Niels Ferguson, Bruce Schneier, paragraph 10.8 + let rng = Random::from_seed(Seed::unsafe_new(seed)); + loop { + let mut raw_res: u64 = rng.gen(); + raw_res >>= 64 - k; + + if raw_res < range { + return raw_res; + } + // Executing this loop does not require a large number of iterations. + // Check tests for more info + } } } @@ -772,7 +793,10 @@ impl> Party { mod tests { use super::*; + use rand::Rng; + use seeded_random::{Random, Seed}; use std::collections::HashMap; + use std::thread; // Mock implementation of Value #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -870,11 +894,11 @@ mod tests { party.ballot = 1; // Must send this message from leader of the ballot. - let leader = party.elector.get_leader(&party).unwrap(); + party.leader = 0; // this party's id let msg = Message1aContent { ballot: 1 }; let routing = MessageRouting { - sender: leader, + sender: 0, receivers: vec![2, 3], is_broadcast: false, msg_type: ProtocolMessage::Msg1a, @@ -966,14 +990,14 @@ mod tests { party.ballot = 1; // Must send this message from leader of the ballot. - let leader = party.elector.get_leader(&party).unwrap(); + party.leader = 0; // this party's id let msg = Message2aContent { ballot: 1, value: bincode::serialize(&MockValue(42)).unwrap(), }; let routing = MessageRouting { - sender: leader, + sender: 0, receivers: vec![0], is_broadcast: false, msg_type: ProtocolMessage::Msg2a, @@ -1248,4 +1272,75 @@ mod tests { time::advance(cfg.finalize_timeout - cfg.launch2b_timeout).await; assert_eq!(event_receiver.recv().await.unwrap(), PartyEvent::Finalize); } + + fn debug_hash_to_range_new(seed: u64, range: u64) -> u64 { + assert!(range > 1); + + let mut k = 64; + while 1u64 << (k - 1) >= range { + k -= 1; + } + + let rng = Random::from_seed(Seed::unsafe_new(seed)); + + let mut iteration = 1u64; + loop { + let mut raw_res: u64 = rng.gen(); + raw_res >>= 64 - k; + + if raw_res < range { + return raw_res; + } + + iteration += 1; + assert!(iteration <= 50) + } + } + + #[test] + #[ignore] // Ignoring since it takes a while to run + fn test_hash_range_random() { + // test the uniform distribution + + const N: usize = 37; + const M: i64 = 10000000; + + let mut cnt1: [i64; N] = [0; N]; + + for _ in 0..M { + let mut rng = rand::thread_rng(); + let seed: u64 = rng.random(); + + let res1 = debug_hash_to_range_new(seed, N as u64); + assert!(res1 < N as u64); + + cnt1[res1 as usize] += 1; + } + + println!("1: {:?}", cnt1); + + let mut avg1: i64 = 0; + + for item in cnt1.iter().take(N) { + avg1 += (M / (N as i64) - item).abs(); + } + + avg1 /= N as i64; + + println!("Avg 1: {}", avg1); + } + + #[test] + fn test_rng() { + let rng1 = Random::from_seed(Seed::unsafe_new(123456)); + let rng2 = Random::from_seed(Seed::unsafe_new(123456)); + + println!("{}", rng1.gen::()); + println!("{}", rng2.gen::()); + + thread::sleep(Duration::from_secs(2)); + + println!("{}", rng1.gen::()); + println!("{}", rng2.gen::()); + } }