Initial commit

This commit is contained in:
Lee Bousfield 2021-02-03 21:31:54 -06:00
commit 5c049caac2
11 changed files with 508 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

25
Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "brute-force"
version = "0.1.0"
authors = ["Lee Bousfield <ljbousfield@gmail.com>"]
edition = "2018"
license = "MIT"
description = "A library for brute forcing arbitrary computations"
respository = "https://github.com/PlasmaPower/brute-force"
documentation = "https://docs.rs/brute-force"
[dependencies]
crossbeam-channel = "0.5.0"
crossbeam-utils = "0.8.1"
curve25519-dalek = { version = "3.0.2", optional = true }
log = "0.4.14"
num_cpus = "1.13.0"
rand = { version = "0.7", optional = true }
[features]
nightly = []
curve25519 = ["curve25519-dalek", "rand"]
[dev-dependencies]
blake2 = "0.9.1"
hex = "0.4.2"

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2021 Lee Bousfield <ljbousfield@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# brute-force: A library for brute forcing arbitrary computations in Rust
This is a library meant to take care of the repetitive tasks of spinning up
threads, checking if the computation is finished, returning the result, and
even generating the inputs.
The adaptor system allows you to compose different helpers in a modular way,
as not all brute forcing is as simple as proof of work.
The common assumption of this library is that each thread will be working off a
state to be the first to find a result, and the computation will end once a
result is found.
## Simple example
```rust
use brute_force::{brute_force, adaptors};
use blake2::{Blake2b, Digest};
#[test]
fn test_proof_of_work() {
let config = brute_force::Config::default();
let f = |nonce: &u64| {
let digest = Blake2b::digest(&nonce.to_le_bytes());
digest.as_slice()[..3] == [0; 3]
};
let nonce = brute_force(config, adaptors::output_input(adaptors::auto_advance(f)));
let digest = Blake2b::digest(&nonce.to_le_bytes());
assert!(digest.as_slice()[..3] == [0; 3])
}
```
Here, we use the `auto_advance` adaptor to automatically generate nonces for us,
and we use the `output_input` adaptor to automatically return the input nonce
used if a computation succeeds (instead of manually specifying the output).
For more examples, see
[the src/tests directory](https://github.com/PlasmaPower/brute-force/tree/master/src/tests).
For documentation on the config or adaptors, see
[the docs](https://docs.rs/brute-force).

57
src/adaptors.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::Advance;
/// Automatically advance the state by incrementing it. Note that you'll likely
/// need to use this adaptor before others, as others assume the function takes
/// a mutable input.
pub fn auto_advance<S, R, F>(f: F) -> impl Fn(&mut S) -> R
where
S: Advance,
F: Fn(&S) -> R,
{
move |s| {
s.advance();
f(s)
}
}
/// Instead of returning an optional output, return a success boolean.
/// If an execution is successful, the brute force output is that execution's
/// input.
pub fn output_input<S, F>(f: F) -> impl Fn(&mut S) -> Option<S>
where
F: Fn(&mut S) -> bool,
S: Clone,
{
move |s| {
if f(s) {
Some(s.clone())
} else {
None
}
}
}
#[cfg(feature = "rand")]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct RandomStart<S>(pub S);
#[cfg(feature = "rand")]
impl<S> crate::Start for RandomStart<S>
where
rand::distributions::Standard: rand::distributions::Distribution<S>,
{
fn start_for_thread(_thread: usize, _thread_count: usize) -> Self {
RandomStart(rand::Rng::gen(&mut rand::rngs::OsRng))
}
}
/// Starts each thread with a cryptographically secure random state. Note that
/// this is normally done for curve25519 scalars, but not normal integers.
#[cfg(feature = "rand")]
pub fn random_start<S, R, F>(f: F) -> impl Fn(&mut RandomStart<S>) -> R
where
RandomStart<S>: crate::Start,
F: Fn(&mut S) -> R,
{
move |s| f(&mut s.0)
}

112
src/lib.rs Normal file
View File

@ -0,0 +1,112 @@
use log::warn;
use std::{
sync::atomic::{self, AtomicBool},
time::Duration,
};
pub mod adaptors;
mod traits;
#[cfg(test)]
mod tests;
pub use traits::{Advance, Start};
#[non_exhaustive]
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub struct Config {
/// Number of threads to use.
/// Falls back on `BRUTE_FORCE_THREADS`, or if that doesn't exist, the
/// number of logical CPU cores.
pub threads: Option<usize>,
/// The number of iterations to perform between checking if the computation
/// is done (or timed out). Defaults to 512.
pub iters_per_stop_check: usize,
}
impl Config {
fn get_threads(&self) -> usize {
if let Some(threads) = self.threads {
return threads;
}
if let Ok(s) = std::env::var("BRUTE_FORCE_THREADS") {
match s.parse() {
Ok(t) => return t,
Err(err) => {
warn!(
"Failed to parse BRUTE_FORCE_THREADS environment variable: {}",
err,
);
}
}
}
num_cpus::get()
}
}
impl Default for Config {
fn default() -> Self {
Self {
threads: None,
iters_per_stop_check: 512,
}
}
}
fn brute_force_core<F, S, R, RF>(options: Config, f: F, do_recv: RF) -> Option<R>
where
F: Fn(&mut S) -> Option<R> + Send + Sync,
S: Start,
R: Send + Sync,
RF: FnOnce(&crossbeam_channel::Receiver<R>) -> Option<R>,
{
let (send, recv) = crossbeam_channel::bounded(1);
let stopped = AtomicBool::new(false);
crossbeam_utils::thread::scope(|s| {
let thread_count = options.get_threads();
let f = &f;
let stopped = &stopped;
for thread in 0..thread_count {
let send = send.clone();
s.spawn(move |_| {
let mut state = S::start_for_thread(thread, thread_count);
loop {
for _ in 0..options.iters_per_stop_check {
if let Some(result) = f(&mut state) {
let _ = send.try_send(result);
return;
}
}
if stopped.load(atomic::Ordering::Relaxed) {
return;
}
}
});
}
drop(send); // Ensure panics propagate
let r = do_recv(&recv);
stopped.store(true, atomic::Ordering::Relaxed);
r
})
.expect("Brute force host panicked")
}
/// Start a brute force that will run until finding a solution.
pub fn brute_force<F, S, R>(options: Config, f: F) -> R
where
F: Fn(&mut S) -> Option<R> + Send + Sync,
S: Start,
R: Send + Sync,
{
brute_force_core(options, f, |r| r.recv().ok()).expect("Brute force workers died")
}
/// Start a brute force that will run until finding a solution or timing out.
pub fn brute_force_with_timeout<F, S, R>(options: Config, timeout: Duration, f: F) -> Option<R>
where
F: Fn(&mut S) -> Option<R> + Send + Sync,
S: Start,
R: Send + Sync,
{
brute_force_core(options, f, |r| r.recv_timeout(timeout).ok())
}

3
src/tests/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod pollard_rho_search;
mod proof_of_work;
mod propagate_panics;

View File

@ -0,0 +1,103 @@
use crate::{brute_force, Config, Start};
use blake2::{
digest::{Update, VariableOutput},
VarBlake2b,
};
use std::{collections::HashMap, sync::Mutex};
const LEN: usize = 5;
const CHAIN_SEGMENT_LEN: usize = 2;
pub struct PollardRhoState {
prev_input: [u8; LEN],
current_input: [u8; LEN],
}
impl PollardRhoState {
fn advance(&mut self, current_output: [u8; LEN]) -> Option<[u8; LEN]> {
let mut ret = None;
if current_output[..CHAIN_SEGMENT_LEN] == [0; CHAIN_SEGMENT_LEN] {
ret = Some(self.prev_input);
self.prev_input = current_output;
}
self.current_input = current_output;
ret
}
}
impl Start for PollardRhoState {
fn start_for_thread(thread: usize, thread_count: usize) -> Self {
let start = <[u8; LEN]>::start_for_thread(thread, thread_count);
Self {
prev_input: start,
current_input: start,
}
}
}
fn hash(input: [u8; LEN]) -> [u8; LEN] {
let mut hasher = VarBlake2b::new(LEN).unwrap();
hasher.update(&input);
let mut hash = [0u8; LEN];
hasher.finalize_variable(|h| hash.copy_from_slice(h));
hash
}
#[test]
fn polard_rho_search() {
// Note: not even close to an efficient implementation of a pollard rho
// search! In practice you'd want a fixed size array of atomic values, not
// a hash map behind a mutex.
let config = Config::default();
let map = Mutex::new(HashMap::new());
let f = |state: &mut PollardRhoState| {
let hash = hash(state.current_input);
if let Some(prev_input) = state.advance(hash) {
let mut map = map.lock().unwrap();
if let Some(other_input) = map.insert(hash, prev_input) {
if other_input != prev_input {
return Some((other_input, prev_input));
}
}
}
None
};
let (first_input, second_input) = brute_force(config, f);
println!(
"found colliding hash chains with starts {} and {}",
hex::encode(first_input),
hex::encode(second_input),
);
// Again, an inefficient algorithm to find the collision
let mut out_to_in = HashMap::new();
let mut input = first_input;
let mut output = hash(input);
let mut first_iter = true;
while first_iter || input[..CHAIN_SEGMENT_LEN] != [0; CHAIN_SEGMENT_LEN] {
out_to_in.insert(output, input);
input = output;
output = hash(input);
first_iter = false;
}
input = second_input;
output = hash(input);
first_iter = true;
while first_iter || input[..CHAIN_SEGMENT_LEN] != [0; CHAIN_SEGMENT_LEN] {
if let Some(&other_input) = out_to_in.get(&output) {
assert!(input != other_input);
let other_output = hash(other_input);
assert_eq!(output, other_output);
println!(
"Found collision! {} and {} both output {}",
hex::encode(input),
hex::encode(other_input),
hex::encode(output),
);
return;
}
input = output;
output = hash(input);
first_iter = false;
}
panic!("Didn't find collision in brute force result hash chain sections");
}

View File

@ -0,0 +1,14 @@
use crate::{adaptors, brute_force, Config};
use blake2::{Blake2b, Digest};
#[test]
fn proof_of_work() {
let config = Config::default();
let f = |nonce: &u64| {
let digest = Blake2b::digest(&nonce.to_le_bytes());
digest.as_slice()[..3] == [0; 3]
};
let nonce = brute_force(config, adaptors::output_input(adaptors::auto_advance(f)));
let digest = Blake2b::digest(&nonce.to_le_bytes());
assert!(digest.as_slice()[..3] == [0; 3])
}

View File

@ -0,0 +1,9 @@
use crate::{brute_force, Config};
#[test]
#[should_panic]
fn propagate_panics() {
brute_force(Config::default(), |_: &mut u8| -> Option<()> {
panic!("Test");
});
}

138
src/traits.rs Normal file
View File

@ -0,0 +1,138 @@
use std::convert::TryFrom;
pub trait Start {
fn start_for_thread(thread: usize, thread_count: usize) -> Self;
}
pub trait Advance {
fn advance(&mut self);
}
macro_rules! impl_for_primitive {
($t:ty, $s:ty) => {
impl Start for $t {
fn start_for_thread(thread: usize, thread_count: usize) -> Self {
if let Ok(thread) = Self::try_from(thread) {
if let Ok(thread_count) = Self::try_from(thread_count) {
(Self::MAX / thread_count) * thread
} else {
thread
}
} else {
0
}
}
}
impl Advance for $t {
fn advance(&mut self) {
*self = self.wrapping_add(1);
}
}
impl Start for $s {
fn start_for_thread(thread: usize, thread_count: usize) -> Self {
<$t>::start_for_thread(thread, thread_count) as Self
}
}
impl Advance for $s {
fn advance(&mut self) {
*self = self.wrapping_add(1);
}
}
};
}
impl_for_primitive!(u8, i8);
impl_for_primitive!(u16, i16);
impl_for_primitive!(u32, i32);
impl_for_primitive!(u64, i64);
impl_for_primitive!(u128, i128);
macro_rules! impl_for_bytes {
($n:tt, $($t:tt)*) => {
impl<$($t)*> Start for [u8; $n] {
fn start_for_thread(thread: usize, thread_count: usize) -> Self {
let mut ret = [0u8; $n];
if $n >= 4 {
#[allow(clippy::out_of_bounds_indexing)]
ret[..4].copy_from_slice(&u32::start_for_thread(thread, thread_count).to_be_bytes());
} else {
ret[0] = u8::start_for_thread(thread, thread_count);
}
ret
}
}
impl<$($t)*> Advance for [u8; $n] {
fn advance(&mut self) {
for byte in self.iter_mut().rev() {
if *byte < u8::MAX {
*byte += 1;
return;
} else {
*byte = 0;
}
}
}
}
}
}
#[cfg(feature = "nightly")]
impl_for_bytes!(N, const N: usize);
#[cfg(not(feature = "nightly"))]
mod impl_bytes {
use super::*;
impl_for_bytes!(1,);
impl_for_bytes!(2,);
impl_for_bytes!(3,);
impl_for_bytes!(4,);
impl_for_bytes!(5,);
impl_for_bytes!(6,);
impl_for_bytes!(7,);
impl_for_bytes!(8,);
impl_for_bytes!(9,);
impl_for_bytes!(10,);
impl_for_bytes!(11,);
impl_for_bytes!(12,);
impl_for_bytes!(13,);
impl_for_bytes!(14,);
impl_for_bytes!(15,);
impl_for_bytes!(16,);
impl_for_bytes!(17,);
impl_for_bytes!(18,);
impl_for_bytes!(19,);
impl_for_bytes!(20,);
impl_for_bytes!(21,);
impl_for_bytes!(22,);
impl_for_bytes!(23,);
impl_for_bytes!(24,);
impl_for_bytes!(25,);
impl_for_bytes!(26,);
impl_for_bytes!(27,);
impl_for_bytes!(28,);
impl_for_bytes!(29,);
impl_for_bytes!(30,);
impl_for_bytes!(32,);
impl_for_bytes!(64,);
impl_for_bytes!(128,);
impl_for_bytes!(256,);
}
#[cfg(feature = "curve25519-dalek")]
impl Start for curve25519_dalek::scalar::Scalar {
fn start_for_thread(_thread: usize, _thread_count: usize) -> Self {
Self::random(&mut rand::rngs::OsRng)
}
}
#[cfg(feature = "curve25519-dalek")]
impl Advance for curve25519_dalek::scalar::Scalar {
fn advance(&mut self) {
*self += curve25519_dalek::scalar::Scalar::one();
}
}