From 486865e803dff978254057161fe0cd05ad0fd2ce Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 14 Aug 2018 16:23:38 +1000 Subject: [PATCH] Add new shuffling function --- Cargo.toml | 2 + src/main.rs | 1 + src/shuffling/mod.rs | 52 +++++++++++++++++ src/shuffling/rng.rs | 132 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 src/shuffling/mod.rs create mode 100644 src/shuffling/rng.rs diff --git a/Cargo.toml b/Cargo.toml index 41473f4a2..b416eed6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,9 @@ version = "0.0.1" authors = ["Paul Hauner "] [dependencies] +# TODO: remove "blake2" in favor of "blake2-rfc" blake2 = "^0.7.1" +blake2-rfc = "0.2.18" bls = { git = "https://github.com/sigp/bls" } bytes = "" crypto-mac = "^0.6.2" diff --git a/src/main.rs b/src/main.rs index 51ef4a724..60c89d36d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ extern crate clap; extern crate network_libp2p; pub mod pubkeystore; +pub mod shuffling; pub mod state; pub mod sync; pub mod utils; diff --git a/src/shuffling/mod.rs b/src/shuffling/mod.rs new file mode 100644 index 000000000..d706fad2c --- /dev/null +++ b/src/shuffling/mod.rs @@ -0,0 +1,52 @@ +extern crate blake2_rfc; + +mod rng; + +use self::rng::ShuffleRng; + +#[derive(Debug)] +pub enum ShuffleErr { + ExceedsListLength, +} + +/// Performs a deterministic, in-place shuffle of a vector of bytes. +/// The final order of the shuffle is determined by successive hashes +/// of the supplied `seed`. +pub fn shuffle( + seed: &[u8], + mut list: Vec) + -> Result, ShuffleErr> +{ + let mut rng = ShuffleRng::new(seed); + if list.len() > rng.rand_max as usize { + return Err(ShuffleErr::ExceedsListLength); + } + for i in 0..(list.len() - 1) { + let n = list.len() - i; + let j = rng.rand_range(n as u32) as usize + i; + list.swap(i, j); + } + Ok(list) +} + + +#[cfg(test)] +mod tests { + use super::*; + use super::blake2_rfc::blake2s::{ blake2s, Blake2sResult }; + + fn hash(seed: &[u8]) -> Blake2sResult { + blake2s(32, &[], seed) + } + + #[test] + fn test_shuffling() { + let seed = hash(b"4kn4driuctg8"); + let list: Vec = (0..12).collect(); + let s = shuffle(seed.as_bytes(), list).unwrap(); + assert_eq!( + s, + vec![7, 4, 8, 6, 5, 3, 0, 11, 1, 2, 10, 9], + ) + } +} diff --git a/src/shuffling/rng.rs b/src/shuffling/rng.rs new file mode 100644 index 000000000..7564b2ab1 --- /dev/null +++ b/src/shuffling/rng.rs @@ -0,0 +1,132 @@ +use super::blake2_rfc::blake2s::{ Blake2s, Blake2sResult }; + +const SEED_SIZE_BYTES: usize = 32; +const RAND_BYTES: usize = 3; // 24 / 8 +const RAND_MAX: u32 = 16777216; // 2**24 + +/// A pseudo-random number generator which given a seed +/// uses successive blake2s hashing to generate "entropy". +pub struct ShuffleRng { + seed: Blake2sResult, + idx: usize, + pub rand_max: u32, +} + +impl ShuffleRng { + /// Create a new instance given some "seed" bytes. + pub fn new(initial_seed: &[u8]) -> Self { + Self { + seed: hash(initial_seed), + idx: 0, + rand_max: RAND_MAX, + } + } + + /// "Regenerates" the seed by hashing it. + fn rehash_seed(&mut self) { + self.seed = hash(self.seed.as_bytes()); + self.idx = 0; + } + + /// Extracts 3 bytes from the `seed`. Rehashes seed if required. + fn rand(&mut self) -> u32 { + self.idx += RAND_BYTES; + match self.idx >= SEED_SIZE_BYTES { + true => { + self.rehash_seed(); + self.rand() + } + false => { + int_from_byte_slice( + self.seed.as_bytes(), + self.idx - RAND_BYTES, + ) + } + } + } + + /// Generate a random u32 below the specified maximum `n`. + /// + /// Provides a filtered result from a higher-level rng, by discarding + /// results which may bias the output. Because of this, execution time is + /// not linear and may potentially be infinite. + pub fn rand_range(&mut self, n: u32) -> u32 { + assert!(n < RAND_MAX, "RAND_MAX exceed"); + let mut x = self.rand(); + while x >= self.rand_max - (self.rand_max % n) { + x = self.rand(); + } + x % n + } +} + +/// Reads the next three bytes of `source`, starting from `offset` and +/// interprets those bytes as a 24 bit big-endian integer. +/// Returns that integer. +fn int_from_byte_slice(source: &[u8], offset: usize) -> u32 { + ( + source[offset + 2] as u32) | + ((source[offset + 1] as u32) << 8) | + ((source[offset ] as u32) << 16 + ) +} + +/// Peform a blake2s hash on the given bytes. +fn hash(bytes: &[u8]) -> Blake2sResult { + let mut hasher = Blake2s::new(SEED_SIZE_BYTES); + hasher.update(bytes); + hasher.finalize() +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_shuffling_int_from_slice() { + let mut x = int_from_byte_slice( + &[0, 0, 1], + 0); + assert_eq!((x as u32), 1); + + x = int_from_byte_slice( + &[0, 1, 1], + 0); + assert_eq!(x, 257); + + x = int_from_byte_slice( + &[1, 1, 1], + 0); + assert_eq!(x, 65793); + + x = int_from_byte_slice( + &[255, 1, 1], + 0); + assert_eq!(x, 16711937); + + x = int_from_byte_slice( + &[255, 255, 255], + 0); + assert_eq!(x, 16777215); + + x = int_from_byte_slice( + &[0x8f, 0xbb, 0xc7], + 0); + assert_eq!(x, 9419719); + } + + #[test] + fn test_shuffling_hash_fn() { + let digest = hash(hash(b"4kn4driuctg8").as_bytes()); // double-hash is intentional + let digest_bytes = digest.as_bytes(); + let expected = [ + 0xff, 0xff, 0xff, 0x8f, 0xbb, 0xc7, 0xab, 0x64, 0x43, 0x9a, + 0xe5, 0x12, 0x44, 0xd8, 0x70, 0xcf, 0xe5, 0x79, 0xf6, 0x55, + 0x6b, 0xbd, 0x81, 0x43, 0xc5, 0xcd, 0x70, 0x2b, 0xbe, 0xe3, + 0x87, 0xc7, + ]; + assert_eq!(digest_bytes.len(), expected.len()); + assert_eq!(digest_bytes, expected) + } +}