#[cfg(feature = "opencl")]
use ocl::{
builders::DeviceSpecifier::TypeFlags,
flags::{DeviceType, MemFlags},
prm::{Uint16, Uint4},
Platform, ProQue,
};
use sha1::{
compress,
digest::{generic_array::GenericArray, BlockInput},
Sha1,
};
#[cfg(feature = "opencl")]
use std::convert::TryInto;
use std::{
cmp::min,
ops::Range,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread::{spawn, JoinHandle},
};
#[derive(Debug, PartialEq)]
pub struct HashSearchWorker {
processed_commit: ProcessedCommit,
desired_prefix: HashPrefix,
search_space: Range<u64>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct HashPrefix {
data: [u32; 5],
mask: [u32; 5],
}
#[derive(Debug, PartialEq)]
pub struct HashMatch {
pub commit: Vec<u8>,
pub hash: String,
}
#[derive(Debug, PartialEq, Clone)]
struct ProcessedCommit {
header: Vec<u8>,
commit: Vec<u8>,
dynamic_padding_start_index: usize,
}
struct PartiallyHashedCommit {
prehashed_commit_section: Vec<u8>,
intermediate_sha1_state: [u32; 5],
remaining_blocks: Vec<GenericArray<u8, <Sha1 as BlockInput>::BlockSize>>,
total_commit_length: usize,
}
impl HashSearchWorker {
pub fn new(current_commit: &[u8], desired_prefix: HashPrefix) -> Self {
Self {
processed_commit: ProcessedCommit::new(current_commit),
desired_prefix,
search_space: 0..(1 << 48),
}
}
pub fn with_capped_search_space(mut self, workload: u64) -> Self {
self.search_space =
self.search_space.start..min(self.search_space.end, self.search_space.start + workload);
self
}
fn split_search_space(self, divisor: u64) -> impl Iterator<Item = Self> {
let amount_per_worker = (self.search_space.end - self.search_space.start) / divisor;
(0..divisor).map(move |index| {
let range_start = index * amount_per_worker + self.search_space.start;
let range_end = if index < divisor - 1 {
range_start + amount_per_worker
} else {
self.search_space.end
};
Self {
processed_commit: self.processed_commit.clone(),
desired_prefix: self.desired_prefix.clone(),
search_space: range_start..range_end,
}
})
}
pub fn search(self) -> Option<HashMatch> {
#[cfg(feature = "opencl")]
if Self::gpus_available() {
return self.search_with_gpu().unwrap();
}
self.search_with_cpus()
}
#[cfg(feature = "opencl")]
fn gpus_available() -> bool {
Platform::first().is_ok()
&& !TypeFlags(DeviceType::GPU)
.to_device_list(None::<Platform>)
.unwrap()
.is_empty()
}
#[allow(clippy::needless_collect)]
fn search_with_cpus(self) -> Option<HashMatch> {
let thread_count = num_cpus::get_physical();
let lame_duck_cancel_signal = Arc::new(AtomicBool::new(false));
let (shared_sender, receiver) = mpsc::channel();
let _handles = self
.split_search_space(thread_count as u64)
.map(|worker| {
let result_sender = shared_sender.clone();
let worker_cancel_signal = lame_duck_cancel_signal.clone();
spawn(move || {
let _ = result_sender
.send(worker.search_with_cpu_single_threaded(worker_cancel_signal));
})
})
.collect::<Vec<JoinHandle<()>>>();
for _ in 0..thread_count {
if let Some(result) = receiver.recv().unwrap() {
lame_duck_cancel_signal.store(true, Ordering::Relaxed);
#[cfg(debug_assertions)]
_handles
.into_iter()
.map(JoinHandle::join)
.collect::<Result<Vec<_>, _>>()
.unwrap();
return Some(result);
}
}
None
}
#[inline(never)]
fn search_with_cpu_single_threaded(
self,
lame_duck_cancel_signal: Arc<AtomicBool>,
) -> Option<HashMatch> {
let HashSearchWorker {
search_space,
desired_prefix,
processed_commit,
} = self;
let mut partially_hashed_commit = processed_commit.into_partially_hashed_commit();
let lame_duck_check_interval = min(search_space.end - search_space.start, 1 << 20);
for base_padding_specifier in search_space.step_by(lame_duck_check_interval as usize) {
for index_in_interval in 0..lame_duck_check_interval {
partially_hashed_commit.scatter_padding(base_padding_specifier + index_in_interval);
if desired_prefix.matches(&partially_hashed_commit.current_hash()) {
return Some(partially_hashed_commit.into_hash_match());
}
}
if lame_duck_cancel_signal.load(Ordering::Relaxed) {
break;
}
}
None
}
#[cfg(feature = "opencl")]
fn search_with_gpu(self) -> ocl::Result<Option<HashMatch>> {
let HashSearchWorker {
search_space,
desired_prefix,
processed_commit,
} = self;
let mut partially_hashed_commit = processed_commit.into_partially_hashed_commit();
const BASE_PADDING_SPECIFIER_ARG: &str = "base_padding_specifier";
let num_threads = *[
desired_prefix.estimated_hashes_needed().saturating_mul(4),
search_space.end - search_space.start,
1 << 22,
]
.iter()
.min()
.unwrap() as usize;
let pro_que = ProQue::builder()
.src(include_str!("sha1_prefix_matcher.cl"))
.dims(num_threads)
.device(TypeFlags(DeviceType::GPU))
.build()?;
let desired_prefix_data_buffer = pro_que
.buffer_builder::<u32>()
.len(desired_prefix.data.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&desired_prefix.data[..])
.build()?;
let desired_prefix_mask_buffer = pro_que
.buffer_builder::<u32>()
.len(desired_prefix.mask.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&desired_prefix.mask[..])
.build()?;
let initial_state_buffer = pro_que
.buffer_builder::<u32>()
.len(partially_hashed_commit.intermediate_sha1_state.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&partially_hashed_commit.intermediate_sha1_state)
.build()?;
let post_padding_blocks_buffer = if partially_hashed_commit.remaining_blocks.len() > 1 {
pro_que
.buffer_builder::<Uint16>()
.len(partially_hashed_commit.remaining_blocks.len() - 1)
.flags(MemFlags::READ_ONLY)
.copy_host_slice(
partially_hashed_commit.remaining_blocks[1..]
.iter()
.map(|block| Uint16::from(encode_big_endian_words_64(block)))
.collect::<Vec<_>>()
.as_slice(),
)
.build()?
} else {
pro_que
.buffer_builder::<Uint16>()
.len(1)
.flags(MemFlags::READ_ONLY)
.copy_host_slice(&[Uint16::zero()])
.build()?
};
let mut successful_match_receiver_host_handle = [u64::MAX];
let successful_match_receiver = pro_que
.buffer_builder::<u64>()
.len(1)
.flags(MemFlags::WRITE_ONLY)
.copy_host_slice(&successful_match_receiver_host_handle)
.build()?;
let kernel = pro_que
.kernel_builder("scatter_padding_and_find_match")
.arg(&desired_prefix_data_buffer)
.arg(&desired_prefix_mask_buffer)
.arg(&initial_state_buffer)
.arg_named(BASE_PADDING_SPECIFIER_ARG, &0)
.arg(Uint4::from(encode_big_endian_words_16(
&partially_hashed_commit.remaining_blocks[0][48..]
.try_into()
.unwrap(),
)))
.arg(partially_hashed_commit.remaining_blocks.len() - 1)
.arg(&post_padding_blocks_buffer)
.arg(&successful_match_receiver)
.build()?;
for base_padding_specifier in search_space.step_by(num_threads) {
kernel.set_arg(BASE_PADDING_SPECIFIER_ARG, base_padding_specifier)?;
unsafe {
kernel.enq()?;
}
successful_match_receiver
.read(&mut successful_match_receiver_host_handle[..])
.enq()?;
if successful_match_receiver_host_handle[0] != u64::MAX {
let successful_padding_specifier = successful_match_receiver_host_handle[0];
partially_hashed_commit.scatter_padding(successful_padding_specifier);
assert!(
desired_prefix.matches(&partially_hashed_commit.current_hash()),
"\
A GPU search reported a commit with a successful match, but when that \
commit was hashed in postprocessing, it didn't match the desired prefix. \
This is a bug. The most likely explanation is that the two implementations of \
`scatter_padding` in Rust and OpenCL (or the implementations of SHA1) have diverged \
from each other.\n\ndesired prefix:\n\t{:?}\ncommit hash produced during \
postprocessing:{:?}\n\tpadding specifier: {}\n\tcommit: {:?}",
desired_prefix,
partially_hashed_commit.current_hash(),
successful_padding_specifier,
String::from_utf8(partially_hashed_commit.into_hash_match().commit).unwrap_or_else(|_| "(invalid utf8)".to_owned())
);
return Ok(Some(partially_hashed_commit.into_hash_match()));
}
}
Ok(None)
}
}
impl ProcessedCommit {
fn new(original_commit: &[u8]) -> Self {
let commit_split_index = Self::get_commit_split_index(original_commit);
const DYNAMIC_PADDING_LENGTH: usize = 48;
let replaceable_padding_size = original_commit[commit_split_index..]
.iter()
.take_while(|&&byte| byte == b' ' || byte == b'\t')
.count();
let static_padding_length = Self::compute_static_padding_length(
commit_split_index,
original_commit.len() - replaceable_padding_size + DYNAMIC_PADDING_LENGTH,
);
let commit_length = original_commit.len() - replaceable_padding_size
+ static_padding_length
+ DYNAMIC_PADDING_LENGTH;
let header = format!("commit {}\x00", commit_length).into_bytes();
let mut commit = Vec::with_capacity(commit_length);
commit.extend(&original_commit[..commit_split_index]);
commit.resize(commit.len() + static_padding_length, b' ');
let dynamic_padding_start_index = commit.len();
commit.resize(commit.len() + DYNAMIC_PADDING_LENGTH, b'\t');
commit.extend(&original_commit[commit_split_index + replaceable_padding_size..]);
assert!((header.len() + dynamic_padding_start_index) % 64 == 0);
Self {
header,
commit,
dynamic_padding_start_index,
}
}
fn compute_static_padding_length(
commit_length_before_static_padding: usize,
commit_length_excluding_static_padding: usize,
) -> usize {
let compute_alignment = |padding_len: usize| {
(format!(
"commit {}\x00",
commit_length_excluding_static_padding + padding_len
)
.len()
+ commit_length_before_static_padding
+ padding_len)
% 64
};
let prefix_length_estimate =
format!("commit {}\x00", commit_length_excluding_static_padding).len()
+ commit_length_before_static_padding;
let initial_padding_length_guess = (64 - prefix_length_estimate % 64) % 64;
let static_padding_length = if compute_alignment(initial_padding_length_guess) == 0 {
initial_padding_length_guess
} else if compute_alignment(initial_padding_length_guess - 1) == 0 {
initial_padding_length_guess - 1
} else {
initial_padding_length_guess + 63
};
assert_eq!(0, compute_alignment(static_padding_length));
debug_assert!((0..static_padding_length).all(|len| compute_alignment(len) != 0));
static_padding_length
}
fn get_commit_split_index(commit: &[u8]) -> usize {
let mut found_gpgsig_line = false;
const SIGNATURE_MARKER: &[u8] = b"-----END PGP SIGNATURE-----";
for index in 0..commit.len() {
if commit[index..].starts_with(b"\ngpgsig ") {
found_gpgsig_line = true;
} else if !found_gpgsig_line && commit[index..].starts_with(b"\n\n") {
break;
} else if found_gpgsig_line && commit[index..].starts_with(SIGNATURE_MARKER) {
return index + SIGNATURE_MARKER.len();
}
}
commit.len()
- commit
.iter()
.rev()
.take_while(|&&byte| byte == b' ' || byte == b'\t' || byte == b'\n')
.count()
}
fn into_partially_hashed_commit(mut self) -> PartiallyHashedCommit {
const SHA1_INITIAL_STATE: [u32; 5] =
[0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
let prehashed_commit_section = self.commit[..self.dynamic_padding_start_index].to_vec();
let mut intermediate_sha1_state = SHA1_INITIAL_STATE;
compress(
&mut intermediate_sha1_state,
self.header
.iter()
.chain(prehashed_commit_section.iter())
.copied()
.collect::<Vec<_>>()
.chunks_exact(64)
.map(GenericArray::clone_from_slice)
.collect::<Vec<_>>()
.as_slice(),
);
let total_commit_length = self.commit.len();
self.commit.push(0x80);
while self.commit[self.dynamic_padding_start_index..].len() % 64 != 56 {
self.commit.push(0);
}
self.commit.extend_from_slice(
&((self.header.len() + total_commit_length) as u64 * 8).to_be_bytes(),
);
PartiallyHashedCommit {
prehashed_commit_section,
intermediate_sha1_state,
total_commit_length,
remaining_blocks: self.commit[self.dynamic_padding_start_index..]
.chunks_exact(64)
.map(GenericArray::clone_from_slice)
.collect(),
}
}
}
impl PartiallyHashedCommit {
#[inline(always)]
fn scatter_padding(&mut self, padding_specifier: u64) {
for (padding_chunk, padding_specifier_byte) in self.remaining_blocks[0][..48]
.chunks_exact_mut(8)
.zip(padding_specifier.to_le_bytes().iter())
{
padding_chunk.copy_from_slice(&Self::PADDING_CHUNKS[*padding_specifier_byte as usize]);
}
}
const PADDING_CHUNKS: [[u8; 8]; 256] = {
let mut padding_chunks = [[0; 8]; 256];
let mut i = 0;
while i < 256 {
let mut j = 0;
while j < 8 {
padding_chunks[i][j] = if i & (0x80 >> j) == 0 { b' ' } else { b'\t' };
j += 1;
}
i += 1;
}
padding_chunks
};
#[inline(always)]
fn current_hash(&self) -> [u32; 5] {
let mut sha1_hash = self.intermediate_sha1_state;
compress(&mut sha1_hash, &self.remaining_blocks[..]);
sha1_hash
}
fn into_hash_match(self) -> HashMatch {
HashMatch {
commit: self
.prehashed_commit_section
.iter()
.chain(self.remaining_blocks.iter().flat_map(|block| block.iter()))
.take(self.total_commit_length)
.copied()
.collect(),
hash: self
.current_hash()
.iter()
.map(|&byte| format!("{:08x}", byte))
.collect::<String>(),
}
}
}
impl HashPrefix {
pub fn new(prefix: &str) -> Option<Self> {
if prefix.len() > 40 {
return None;
}
let contains_only_valid_characters = prefix.chars().all(|c| {
('0'..='9').contains(&c) || ('a'..='f').contains(&c) || ('A'..='F').contains(&c)
});
if !contains_only_valid_characters {
return None;
}
let mut data = [0u32; 5];
let mut mask = [0u32; 5];
for (i, chunk) in prefix.as_bytes().chunks(8).enumerate() {
let value =
u32::from_str_radix(&String::from_utf8(chunk.to_vec()).unwrap(), 16).unwrap();
let num_unset_bits = 32 - 4 * chunk.len();
data[i] = value << num_unset_bits;
mask[i] = u32::MAX >> num_unset_bits << num_unset_bits;
}
Some(HashPrefix { data, mask })
}
#[inline(always)]
fn matches(&self, hash: &[u32; 5]) -> bool {
hash.iter()
.zip(&self.mask)
.map(|(hash_word, &mask_word)| hash_word & mask_word)
.zip(&self.data)
.all(|(masked_hash_word, &desired_prefix_word)| masked_hash_word == desired_prefix_word)
}
#[cfg(feature = "opencl")]
fn estimated_hashes_needed(&self) -> u64 {
2u64.saturating_pow(self.mask.iter().map(|word| word.count_ones()).sum())
}
}
impl Default for HashPrefix {
fn default() -> Self {
HashPrefix::new("0000000").unwrap()
}
}
#[cfg(feature = "opencl")]
fn encode_big_endian_words_16(data: &[u8; 16]) -> [u32; 4] {
data.chunks(4)
.map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
.collect::<Vec<_>>()
.as_slice()
.try_into()
.unwrap()
}
#[cfg(feature = "opencl")]
fn encode_big_endian_words_64(
data: &GenericArray<u8, <Sha1 as BlockInput>::BlockSize>,
) -> [u32; 16] {
data.chunks(4)
.map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
.collect::<Vec<_>>()
.as_slice()
.try_into()
.unwrap()
}
#[cfg(test)]
mod tests;