#[cfg(feature = "opencl")]
use ocl::{
builders::DeviceSpecifier::TypeFlags,
flags::{DeviceType, MemFlags},
prm::Uint16,
Buffer, Context, Kernel, Platform, Program, Queue,
};
use std::{
array::TryFromSliceError,
cmp::Ord,
convert::{TryFrom, TryInto},
fmt::Debug,
iter::{once, repeat},
mem::{align_of, size_of},
ops::Range,
slice::from_raw_parts_mut,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread::{spawn, JoinHandle},
};
#[derive(Debug, PartialEq)]
pub struct HashSearchWorker<H: GitHash> {
processed_commit: ProcessedCommit,
desired_prefix: HashPrefix<H>,
search_space: Range<u64>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct HashPrefix<H: GitHash> {
data: H::State,
mask: H::State,
}
#[derive(Debug, PartialEq, Clone)]
struct ProcessedCommit {
data: Box<[u8]>,
commit_range: Range<usize>,
num_static_blocks: usize,
}
#[derive(Debug)]
struct PartiallyHashedCommit<'a, H: GitHash> {
intermediate_state: H::State,
dynamic_blocks: &'a mut [H::Block],
}
#[derive(Debug, PartialEq, Eq)]
pub struct HashedCommit<H: GitHash> {
pub commit: Vec<u8>,
pub hash: H::State,
}
pub trait GitHash: private::Sealed + Debug + Send + Clone + Eq + 'static {
type State: AsRef<[u32]>
+ Clone
+ Copy
+ Debug
+ Eq
+ Send
+ ToString
+ for<'a> TryFrom<&'a [u32], Error = TryFromSliceError>;
const INITIAL_STATE: Self::State;
type Block: AsRef<[u8]> + AsMut<[u8]> + Copy + Debug;
fn compress(state: &mut Self::State, blocks: &[Self::Block]);
#[cfg(feature = "opencl")]
const KERNEL: &'static str;
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Sha1 {}
impl GitHash for Sha1 {
type State = StateVector<5>;
const INITIAL_STATE: Self::State =
StateVector([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0]);
type Block =
sha1::digest::generic_array::GenericArray<u8, sha1::digest::generic_array::typenum::U64>;
fn compress(state: &mut Self::State, blocks: &[Self::Block]) {
sha1::compress(&mut state.0, blocks)
}
#[cfg(feature = "opencl")]
const KERNEL: &'static str = include_str!("sha1_prefix_matcher.cl");
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Sha256 {}
impl GitHash for Sha256 {
type State = StateVector<8>;
const INITIAL_STATE: Self::State = StateVector([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
]);
type Block =
sha2::digest::generic_array::GenericArray<u8, sha2::digest::generic_array::typenum::U64>;
fn compress(state: &mut Self::State, blocks: &[Self::Block]) {
sha2::compress256(&mut state.0, blocks)
}
#[cfg(feature = "opencl")]
const KERNEL: &'static str = include_str!("sha256_prefix_matcher.cl");
}
mod private {
pub trait Sealed {}
impl Sealed for super::Sha1 {}
impl Sealed for super::Sha256 {}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StateVector<const N: usize>([u32; N]);
impl<const N: usize> AsRef<[u32]> for StateVector<N> {
fn as_ref(&self) -> &[u32] {
self.0.as_ref()
}
}
impl<'a, const N: usize> TryFrom<&'a [u32]> for StateVector<N> {
type Error = <[u32; N] as TryFrom<&'a [u32]>>::Error;
fn try_from(value: &'_ [u32]) -> Result<Self, Self::Error> {
<[u32; N]>::try_from(value).map(Self)
}
}
impl<const N: usize> ToString for StateVector<N> {
fn to_string(&self) -> String {
self.0.iter().map(|word| format!("{:08x}", word)).collect()
}
}
impl<H: GitHash> HashSearchWorker<H> {
pub fn new(current_commit: &[u8], desired_prefix: HashPrefix<H>) -> 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
..Ord::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<HashedCommit<H>> {
#[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<HashedCommit<H>> {
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 = Arc::clone(&lame_duck_cancel_signal);
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<HashedCommit<H>> {
let HashSearchWorker {
search_space,
desired_prefix,
mut processed_commit,
..
} = self;
let mut partially_hashed_commit = processed_commit.as_partially_hashed_commit::<H>();
let lame_duck_check_interval = Ord::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(HashedCommit::new(processed_commit.commit()));
}
}
if lame_duck_cancel_signal.load(Ordering::Relaxed) {
break;
}
}
None
}
#[cfg(feature = "opencl")]
fn search_with_gpu(self) -> ocl::Result<Option<HashedCommit<H>>> {
let HashSearchWorker {
search_space,
desired_prefix,
mut processed_commit,
..
} = self;
let mut partially_hashed_commit = processed_commit.as_partially_hashed_commit::<H>();
let num_threads = *[
desired_prefix.estimated_hashes_needed().saturating_mul(4),
search_space.end - search_space.start,
1 << 22,
]
.iter()
.min()
.unwrap() as usize;
assert!(num_threads < u32::MAX as usize);
let devices = TypeFlags(DeviceType::GPU).to_device_list(Some(Platform::default()))?[0];
let context = Context::builder().devices(devices).build()?;
let queue = Queue::new(&context, devices, None)?;
let mut successful_match_receiver_host_handle = [u32::MAX];
let successful_match_receiver = Buffer::builder()
.queue(queue.clone())
.len(1)
.flags(MemFlags::READ_WRITE)
.copy_host_slice(&successful_match_receiver_host_handle)
.build()?;
const BASE_PADDING_SPECIFIER_ARG: &str = "base_padding_specifier";
let kernel = Kernel::builder()
.name("scatter_padding_and_find_match")
.program(
&Program::builder()
.src(H::KERNEL)
.cmplr_opt("-Werror")
.build(&context)?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(desired_prefix.data.as_ref().len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(desired_prefix.data.as_ref())
.build()?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(desired_prefix.mask.as_ref().len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(desired_prefix.mask.as_ref())
.build()?,
)
.arg(
&Buffer::builder()
.queue(queue.clone())
.len(partially_hashed_commit.intermediate_state.as_ref().len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(partially_hashed_commit.intermediate_state.as_ref())
.build()?,
)
.arg_named(BASE_PADDING_SPECIFIER_ARG, 0u64) .arg(
&Buffer::builder()
.queue(queue.clone())
.len(partially_hashed_commit.dynamic_blocks.len())
.flags(MemFlags::READ_ONLY)
.copy_host_slice(
&partially_hashed_commit
.dynamic_blocks
.iter()
.map(|&block| encode_into_opencl_vector::<H>(block))
.collect::<Vec<_>>(),
)
.build()?,
)
.arg(partially_hashed_commit.dynamic_blocks.len() as u64)
.arg(&successful_match_receiver)
.queue(queue)
.global_work_size(num_threads)
.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] != u32::MAX {
let successful_padding_specifier =
base_padding_specifier + (successful_match_receiver_host_handle[0] as u64);
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/SHA256) have diverged \
from each other.\n\npartial commit:\n\t{:?}\ndesired prefix:\n\t{:?}\ncommit hash \
produced during postprocessing:{:?}\n\tpadding specifier: {}",
partially_hashed_commit,
desired_prefix,
partially_hashed_commit.current_hash(),
successful_padding_specifier,
);
return Ok(Some(HashedCommit::new(processed_commit.commit())));
}
}
Ok(None)
}
}
impl ProcessedCommit {
const DYNAMIC_PADDING_LENGTH: usize = 48;
fn new(original_commit: &[u8]) -> Self {
let padding_insertion_point = Self::get_padding_insertion_point(original_commit);
let replaceable_padding_size = original_commit[padding_insertion_point..]
.iter()
.take_while(|&&byte| byte == b' ' || byte == b'\t')
.count();
let static_padding_length = Self::compute_static_padding_length(
padding_insertion_point,
original_commit.len() - replaceable_padding_size + Self::DYNAMIC_PADDING_LENGTH,
);
let commit_length = original_commit.len() - replaceable_padding_size
+ static_padding_length
+ Self::DYNAMIC_PADDING_LENGTH;
let mut data = format!("commit {}\0", commit_length).into_bytes();
let commit_range = data.len()..(data.len() + commit_length);
data.extend(&original_commit[..padding_insertion_point]);
data.resize(data.len() + static_padding_length, b' ');
assert_eq!(data.len() % 64, 0);
let num_static_blocks = data.len() / 64;
data.resize(data.len() + Self::DYNAMIC_PADDING_LENGTH, b'\t');
data.extend(&original_commit[padding_insertion_point + replaceable_padding_size..]);
assert_eq!(data.len(), commit_range.end);
data.extend(sha_finalization_padding(data.len()));
assert_eq!(data.len() % 64, 0);
Self {
data: data.into_boxed_slice(),
commit_range,
num_static_blocks,
}
}
fn get_padding_insertion_point(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 ")
|| commit[index..].starts_with(b"\ngpgsig-sha256 ")
{
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 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 {}\0",
commit_length_excluding_static_padding + padding_len
)
.len()
+ commit_length_before_static_padding
+ padding_len)
% 64
};
let prefix_length_estimate = format!("commit {}\0", 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!(compute_alignment(static_padding_length), 0);
debug_assert!((0..static_padding_length).all(|len| compute_alignment(len) != 0));
static_padding_length
}
fn commit(&self) -> &[u8] {
&self.data[self.commit_range.clone()]
}
fn as_partially_hashed_commit<H: GitHash>(&mut self) -> PartiallyHashedCommit<H> {
let (static_blocks, dynamic_blocks) =
as_chunks_mut::<H>(&mut self.data[..]).split_at_mut(self.num_static_blocks);
let mut intermediate_state = H::INITIAL_STATE;
H::compress(&mut intermediate_state, static_blocks);
PartiallyHashedCommit {
intermediate_state,
dynamic_blocks,
}
}
}
impl<'a, H: GitHash> PartiallyHashedCommit<'a, H> {
#[inline(always)]
fn dynamic_padding_mut(&mut self) -> &mut [u8] {
&mut self.dynamic_blocks[0].as_mut()[..48]
}
#[inline(always)]
fn scatter_padding(&mut self, padding_specifier: u64) {
for (padding_chunk, &padding_specifier_byte) in self
.dynamic_padding_mut()
.chunks_exact_mut(8)
.zip(padding_specifier.to_le_bytes().iter())
{
padding_chunk.copy_from_slice(&PADDING_CHUNKS[padding_specifier_byte as usize]);
}
}
#[inline(always)]
fn current_hash(&self) -> H::State {
let mut hash = self.intermediate_state;
H::compress(&mut hash, self.dynamic_blocks);
hash
}
}
impl<H: GitHash> HashPrefix<H> {
pub fn new(prefix: &str) -> Option<Self> {
let num_words = H::INITIAL_STATE.as_ref().len();
if prefix.len() > num_words * 8 {
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 = Vec::new();
let mut mask = Vec::new();
for chunk in prefix.as_bytes().chunks(8) {
let value =
u32::from_str_radix(&String::from_utf8(chunk.to_vec()).unwrap(), 16).unwrap();
let num_unspecified_bits = 32 - 4 * chunk.len();
data.push(value << num_unspecified_bits);
mask.push(u32::MAX >> num_unspecified_bits << num_unspecified_bits);
}
data.resize(num_words, 0);
mask.resize(num_words, 0);
Some(HashPrefix {
data: data[..].try_into().unwrap(),
mask: mask[..].try_into().unwrap(),
})
}
#[inline(always)]
fn matches(&self, hash: &H::State) -> bool {
hash.as_ref()
.iter()
.zip(self.mask.as_ref().iter())
.map(|(&hash_word, &mask_word)| hash_word & mask_word)
.zip(self.data.as_ref().iter())
.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
.as_ref()
.iter()
.map(|word| word.count_ones())
.sum(),
)
}
}
impl<H: GitHash> Default for HashPrefix<H> {
fn default() -> Self {
HashPrefix::new("0000000").unwrap()
}
}
impl<H: GitHash> HashedCommit<H> {
fn new(commit: &[u8]) -> Self {
Self {
commit: commit.to_vec(),
hash: hash_git_commit::<H>(commit),
}
}
}
#[cfg(feature = "opencl")]
fn encode_into_opencl_vector<H: GitHash>(data: H::Block) -> Uint16 {
let words: [u32; 16] = data
.as_ref()
.chunks(4)
.map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
.collect::<Vec<_>>()
.try_into()
.unwrap();
words.into()
}
pub fn hash_git_commit<H: GitHash>(commit: &[u8]) -> H::State {
let mut state = H::INITIAL_STATE;
let commit_header = format!("commit {}\0", commit.len()).into_bytes();
let commit_data_length = commit_header.len() + commit.len();
H::compress(
&mut state,
as_chunks_mut::<H>(
commit_header
.into_iter()
.chain(commit.to_owned().into_iter())
.chain(sha_finalization_padding(commit_data_length))
.collect::<Vec<_>>()
.as_mut(),
),
);
state
}
fn as_chunks_mut<H: GitHash>(slice: &mut [u8]) -> &mut [H::Block] {
assert_eq!(size_of::<H::Block>(), 64);
assert_eq!(align_of::<H::Block>(), align_of::<u8>());
assert_eq!(slice.len() % size_of::<H::Block>(), 0);
unsafe {
from_raw_parts_mut(
slice.as_mut_ptr().cast(),
slice.len() / size_of::<H::Block>(),
)
}
}
fn sha_finalization_padding(data_length: usize) -> impl IntoIterator<Item = u8> {
once(0x80)
.chain(repeat(0).take((55 - data_length as isize).rem_euclid(64) as usize))
.chain(<[u8; 8]>::into_iter((data_length as u64 * 8).to_be_bytes()))
}
static 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
};
#[cfg(test)]
mod tests;