diff --git a/Cargo.lock b/Cargo.lock index 3d88b70..fa18bdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,6 +129,7 @@ dependencies = [ "clap", "config", "crevice", + "debug-ignore", "futures", "futures-util", "gl", @@ -906,6 +907,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "debug-ignore" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" + [[package]] name = "deranged" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index 4ec3b11..2655b87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ chrono = "0.4.10" clap = "2.33.0" config = { version = "0.10.1", features = ["toml"] } crevice = "0.16.0" +debug-ignore = "1.0.5" futures = "0.3" futures-util = "0.3.15" gl = "0.14.0" diff --git a/src/api/tests.rs b/src/api/tests.rs index d83f23b..55b43a2 100644 --- a/src/api/tests.rs +++ b/src/api/tests.rs @@ -251,7 +251,7 @@ async fn get_db_with_block_no_mutex() -> SimpleDb { block.set_txs_merkle_root_and_hash().await; block.header = apply_mining_tx(block.header, nonce, "test".to_string()); - if !validate_pow_block(&block.header) { + if validate_pow_block(&block.header).is_err() { block.header.nonce_and_mining_tx_hash.0 = generate_pow_for_block(&block.header) .expect("error occurred while mining block") .expect("couldn't find a valid nonce"); diff --git a/src/asert.rs b/src/asert.rs index 9738099..ba7a04b 100644 --- a/src/asert.rs +++ b/src/asert.rs @@ -1,3 +1,4 @@ +use std::convert::TryInto; use { crate::constants::{ASERT_HALF_LIFE, ASERT_TARGET_HASHES_PER_BLOCK}, rug::{integer::ParseIntegerError, Integer}, @@ -278,6 +279,28 @@ impl Sub for Timestamp { } } +/// An error which can occur when working with `CompactTarget`. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum CompactTargetError { + /// Indicates that an attempt was made to construct a `CompactTarget` from a byte slice with + /// an invalid length. + SliceLength { + /// The length of the slice + length: usize, + }, +} + +impl std::error::Error for CompactTargetError {} + +impl fmt::Display for CompactTargetError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::SliceLength { length } => + write!(f, "Cannot construct CompactTarget from slice of length {}", length), + } + } +} + /// A 32-bit approximation of a 256-bit number that represents the target /// a block hash must be lower than in order to meet PoW requirements. /// @@ -321,14 +344,11 @@ impl CompactTarget { Self(u32::from_be_bytes(array)) } - pub fn try_from_slice(slice: &[u8]) -> Option { - if slice.len() < 4 { - return None; - } - - let mut array = [0u8; 4]; - array.copy_from_slice(&slice[slice.len() - 4..]); - Some(Self::from_array(array)) + pub fn try_from_slice(slice: &[u8]) -> Result { + // This requires that the slice's length is exactly 4 + slice.try_into() + .map(Self::from_array) + .map_err(|_| CompactTargetError::SliceLength { length: slice.len() }) } } @@ -379,13 +399,13 @@ impl CompactTarget { } } +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct HeaderHash(sha3_256::Output); impl HeaderHash { - pub fn try_calculate(header: &BlockHeader) -> Option { - let serialized = bincode::serialize(header).ok()?; - let hashed = sha3_256::digest(&serialized); - Some(Self(hashed)) + pub fn calculate(header: &BlockHeader) -> Self { + let serialized = bincode::serialize(header).unwrap(); + Self(sha3_256::digest(&serialized)) } pub fn is_below_target(&self, target: &Target) -> bool { @@ -402,6 +422,10 @@ impl HeaderHash { self.is_below_target(&target) } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + pub fn into_vec(self) -> Vec { self.0.to_vec() } @@ -413,6 +437,18 @@ impl From> for HeaderHash { } } +impl From for sha3_256::Output { + fn from(value: HeaderHash) -> Self { + value.0 + } +} + +impl AsRef<[u8]> for HeaderHash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + /// The expanded integer from of a `CompactTarget` that represents the target /// a block hash must be lower than in order to meet PoW requirements. /// diff --git a/src/mempool.rs b/src/mempool.rs index b778df1..6c87f65 100644 --- a/src/mempool.rs +++ b/src/mempool.rs @@ -2079,10 +2079,10 @@ impl MempoolNode { // Perform validation let coinbase_hash = construct_tx_hash(&coinbase); let block_to_check = apply_mining_tx(block_to_check, nonce, coinbase_hash); - if !validate_pow_block(&block_to_check) { + if let Err(err) = validate_pow_block(&block_to_check) { return Some(Response { success: false, - reason: "Invalid PoW for block".to_owned(), + reason: format!("Invalid PoW for block: {}", err), }); } diff --git a/src/miner_pow/cpu.rs b/src/miner_pow/cpu.rs index 2c84c64..44aa165 100644 --- a/src/miner_pow/cpu.rs +++ b/src/miner_pow/cpu.rs @@ -1,6 +1,6 @@ use tw_chain::crypto::sha3_256; use crate::constants::MINING_DIFFICULTY; -use crate::miner_pow::{MinerStatistics, SHA3_256PoWMiner, PoWDifficulty, MineError}; +use crate::miner_pow::{MinerStatistics, Sha3_256PoWMiner, PoWDifficulty, MineError}; /// A miner which runs on the CPU. #[derive(Copy, Clone, Debug)] @@ -13,7 +13,7 @@ impl CpuMiner { } } -impl SHA3_256PoWMiner for CpuMiner { +impl Sha3_256PoWMiner for CpuMiner { fn is_hw_accelerated(&self) -> bool { false } diff --git a/src/miner_pow/mod.rs b/src/miner_pow/mod.rs index 307de67..8cd9784 100644 --- a/src/miner_pow/mod.rs +++ b/src/miner_pow/mod.rs @@ -4,37 +4,93 @@ pub mod vulkan; use std::fmt; use std::fmt::Debug; -use std::ops::Range; -use std::sync::Arc; +use std::ops::RangeInclusive; +use std::sync::{Arc, Mutex, OnceLock}; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use tw_chain::primitives::block::BlockHeader; -use crate::asert::CompactTarget; +use crate::asert::{CompactTarget, CompactTargetError}; use crate::constants::{ADDRESS_POW_NONCE_LEN, MINING_DIFFICULTY, POW_NONCE_MAX_LEN}; use crate::interfaces::ProofOfWork; use crate::miner_pow::cpu::CpuMiner; -use crate::utils::{split_range_into_blocks, UnitsPrefixed}; +use crate::utils::{all_byte_strings, split_range_into_blocks, UnitsPrefixed}; pub const SHA3_256_BYTES: usize = 32; pub const BLOCK_HEADER_MAX_BYTES: usize = 1024; +/// A difficulty requirement for a proof-of-work object. #[derive(Clone, Eq, PartialEq, Debug)] pub enum PoWDifficulty { + /// Indicates that the object's hash should start with a fixed number of leading zero bytes. LeadingZeroBytes { + /// The number of zero bytes which the object's hash must start with. leading_zeroes: usize, }, + /// Indicates that the object's hash is irrelevant; any hash would meet the difficulty + /// requirements. TargetHashAlwaysPass, + /// Indicates that the object's hash must be lexicographically less than or equal to the given + /// target hash threshold. TargetHash { + /// The target hash threshold which the object's hash must be lexicographically less than + /// or equal to. target_hash: [u8; SHA3_256_BYTES], }, } -fn find_nonce_location( +impl PoWDifficulty { + /// Checks if the given hash meets this difficulty requirement. + /// + /// ### Arguments + /// + /// * `hash` - the hash to check + pub fn check_hash(&self, hash: &[u8; SHA3_256_BYTES]) -> bool { + match self { + PoWDifficulty::TargetHashAlwaysPass => true, + PoWDifficulty::LeadingZeroBytes { leading_zeroes } => + *leading_zeroes <= hash.len() && hash[..*leading_zeroes].iter().all(|b| *b == 0), + PoWDifficulty::TargetHash { target_hash } => + hash <= target_hash, + } + } +} + +/// An error relating to an invalid nonce value. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum PoWError { + /// Indicates that an invalid nonce length was provided. + NonceLength { + /// The provided nonce length + nonce_length: usize, + /// The permitted nonce lengths + permitted_lengths: RangeInclusive, + }, +} + +impl std::error::Error for PoWError {} + +impl fmt::Display for PoWError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::NonceLength { nonce_length, permitted_lengths } => + write!(f, "The provided nonce length {} is invalid (should be in range {}..={})", + nonce_length, permitted_lengths.start(), permitted_lengths.end()), + } + } +} + +fn find_nonce_location( object: &T, - nonce_setter: impl Fn(&mut T, Vec) -> (), nonce_length: usize, -) -> (Box<[u8]>, Box<[u8]>) { +) -> Result<(Box<[u8]>, Box<[u8]>), PoWError> { + if !::permitted_nonce_lengths().contains(&nonce_length) { + return Err(PoWError::NonceLength { + nonce_length, + permitted_lengths: ::permitted_nonce_lengths(), + }); + } + assert_ne!(nonce_length, 0, "nonce_length may not be 0!"); let mut object = object.clone(); @@ -42,9 +98,9 @@ fn find_nonce_location( let nonce_00 = vec![0x00u8; nonce_length]; let nonce_ff = vec![0xFFu8; nonce_length]; - nonce_setter(&mut object, nonce_00.clone()); + object.set_nonce(nonce_00.clone()).unwrap(); let serialized_00 = bincode::serialize(&object).unwrap(); - nonce_setter(&mut object, nonce_ff.clone()); + object.set_nonce(nonce_ff.clone()).unwrap(); let serialized_ff = bincode::serialize(&object).unwrap(); assert_ne!(serialized_00, serialized_ff, @@ -64,29 +120,7 @@ fn find_nonce_location( let leading_bytes = serialized_00[..nonce_offset].into(); let trailing_bytes = serialized_00[nonce_offset + nonce_length..].into(); - (leading_bytes, trailing_bytes) -} - -fn extract_target_difficulty( - difficulty: &[u8], -) -> Result { - if difficulty.is_empty() { - // There is no difficulty function enabled - return Ok(PoWDifficulty::LeadingZeroBytes { - leading_zeroes: MINING_DIFFICULTY, - }); - } - - // Decode the difficulty bytes into a CompactTarget and then expand that into a target - // hash threshold. - let compact_target = CompactTarget::try_from_slice(difficulty) - .ok_or("block header contains invalid difficulty")?; - - match expand_compact_target_difficulty(compact_target) { - // The target value is higher than the largest possible SHA3-256 hash. - None => Ok(PoWDifficulty::TargetHashAlwaysPass), - Some(target_hash) => Ok(PoWDifficulty::TargetHash { target_hash }), - } + Ok((leading_bytes, trailing_bytes)) } fn expand_compact_target_difficulty(compact_target: CompactTarget) -> Option<[u8; SHA3_256_BYTES]> { @@ -107,12 +141,33 @@ fn expand_compact_target_difficulty(compact_target: CompactTarget) -> Option<[u8 } /// An object which contains a nonce and can therefore be mined. -pub trait PoWObject { - /// Gets the difficulty requirements for mining this object. - fn pow_difficulty(&self) -> PoWDifficulty; - +pub trait PoWObject : Clone { /// Gets a range containing all nonce lengths permitted by this object. - fn permitted_nonce_lengths(&self) -> Range; + fn permitted_nonce_lengths() -> RangeInclusive; + + fn check_nonce_length(nonce_length: usize) -> Result<(), PoWError> { + if Self::permitted_nonce_lengths().contains(&nonce_length) { + Ok(()) + } else { + Err(PoWError::NonceLength { + nonce_length, + permitted_lengths: Self::permitted_nonce_lengths(), + }) + } + } + + /// Sets this object's nonce to the given value. + /// + /// ### Arguments + /// + /// * `nonce` - the nonce which the cloned object should contain + fn set_nonce(&mut self, nonce: Vec) -> Result<(), PoWError>; + + /// Gets a reference to this object's nonce. + fn get_nonce(&self) -> &[u8]; + + /// Gets the difficulty requirements for mining this object. + fn pow_difficulty(&self) -> Result; /// Gets the leading and trailing bytes for this object. /// @@ -125,60 +180,78 @@ pub trait PoWObject { fn get_leading_and_trailing_bytes_for_mine( &self, nonce_length: usize, - ) -> Result<(Box<[u8]>, Box<[u8]>), usize>; + ) -> Result<(Box<[u8]>, Box<[u8]>), PoWError>; } impl PoWObject for BlockHeader { - fn pow_difficulty(&self) -> PoWDifficulty { - extract_target_difficulty(&self.difficulty) - .expect("Block header contains invalid difficulty!") + fn permitted_nonce_lengths() -> RangeInclusive { + 1..=POW_NONCE_MAX_LEN } - fn permitted_nonce_lengths(&self) -> Range { - 1..POW_NONCE_MAX_LEN + 1 + fn set_nonce(&mut self, nonce: Vec) -> Result<(), PoWError> { + Self::check_nonce_length(nonce.len())?; + self.nonce_and_mining_tx_hash.0 = nonce; + Ok(()) + } + + fn get_nonce(&self) -> &[u8] { + &self.nonce_and_mining_tx_hash.0 + } + + fn pow_difficulty(&self) -> Result { + if self.difficulty.is_empty() { + // There is no difficulty function enabled + return Ok(PoWDifficulty::LeadingZeroBytes { + leading_zeroes: MINING_DIFFICULTY, + }); + } + + // Decode the difficulty bytes into a CompactTarget and then expand that into a target + // hash threshold. + let compact_target = CompactTarget::try_from_slice(&self.difficulty)?; + + match expand_compact_target_difficulty(compact_target) { + // The target value is higher than the largest possible SHA3-256 hash. + None => Ok(PoWDifficulty::TargetHashAlwaysPass), + Some(target_hash) => Ok(PoWDifficulty::TargetHash { target_hash }), + } } fn get_leading_and_trailing_bytes_for_mine( &self, nonce_length: usize, - ) -> Result<(Box<[u8]>, Box<[u8]>), usize> { - if !self.permitted_nonce_lengths().contains(&nonce_length) { - return Err(nonce_length); - } - - Ok(find_nonce_location( - self, - |block_header, nonce| block_header.nonce_and_mining_tx_hash.0 = nonce, - nonce_length, - )) + ) -> Result<(Box<[u8]>, Box<[u8]>), PoWError> { + find_nonce_location(self, nonce_length) } } impl PoWObject for ProofOfWork { - fn pow_difficulty(&self) -> PoWDifficulty { - // see utils::validate_pow_for_address() - PoWDifficulty::LeadingZeroBytes { - leading_zeroes: MINING_DIFFICULTY, - } + fn permitted_nonce_lengths() -> RangeInclusive { + ADDRESS_POW_NONCE_LEN..=ADDRESS_POW_NONCE_LEN + } + + fn set_nonce(&mut self, nonce: Vec) -> Result<(), PoWError> { + Self::check_nonce_length(nonce.len())?; + self.nonce = nonce; + Ok(()) } - fn permitted_nonce_lengths(&self) -> Range { - ADDRESS_POW_NONCE_LEN..ADDRESS_POW_NONCE_LEN + 1 + fn get_nonce(&self) -> &[u8] { + &self.nonce + } + + fn pow_difficulty(&self) -> Result { + // see utils::validate_pow_for_address() + Ok(PoWDifficulty::LeadingZeroBytes { + leading_zeroes: MINING_DIFFICULTY, + }) } fn get_leading_and_trailing_bytes_for_mine( &self, nonce_length: usize, - ) -> Result<(Box<[u8]>, Box<[u8]>), usize> { - if !self.permitted_nonce_lengths().contains(&nonce_length) { - return Err(nonce_length); - } - - Ok(find_nonce_location( - self, - |pow, nonce| pow.nonce = nonce, - nonce_length, - )) + ) -> Result<(Box<[u8]>, Box<[u8]>), PoWError> { + find_nonce_location(self, nonce_length) } } @@ -271,6 +344,8 @@ impl fmt::Display for MinerStatistics { /// An error which is thrown by a miner. #[derive(Debug)] pub enum MineError { + GetDifficulty(CompactTargetError), + GetLeadingTrailingBytes(PoWError), Wrapped(Box), } @@ -285,12 +360,17 @@ impl std::error::Error for MineError {} impl fmt::Display for MineError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::Wrapped(cause) => write!(f, "An error occurred while mining: {cause}"), + Self::GetDifficulty(cause) => + write!(f, "Unable to determine PoW difficulty: {}", cause), + Self::GetLeadingTrailingBytes(cause) => + write!(f, "Unable to get leading and trailing bytes for PoW object: {}", cause), + Self::Wrapped(cause) => + write!(f, "An error occurred while mining: {}", cause), } } } -pub trait SHA3_256PoWMiner { +pub trait Sha3_256PoWMiner: Debug { /// Returns true if this miner is hardware-accelerated. fn is_hw_accelerated(&self) -> bool; @@ -351,58 +431,87 @@ pub trait SHA3_256PoWMiner { /// * `timeout_duration` - If set, this is the maximum duration which the miner will run for. /// The miner will stop after approximately this duration, regardless /// of whether a result was found. -pub fn generate_pow( - miner: &mut dyn SHA3_256PoWMiner, - object: &impl PoWObject, +pub fn generate_pow( + miner: &mut dyn Sha3_256PoWMiner, + object: &O, statistics: &mut MinerStatistics, terminate_flag: Option>, timeout_duration: Option, ) -> Result { - let difficulty = object.pow_difficulty(); - - // TODO: Change the nonce prefix if we run out of hashes to try - let nonce_prefix_length = std::mem::size_of::(); - let (leading_bytes, trailing_bytes) = - object.get_leading_and_trailing_bytes_for_mine(nonce_prefix_length) - .expect("Object doesn't permit a 32-bit nonce!"); - - let peel_amount = miner.nonce_peel_amount(); - let total_start_time = Instant::now(); - for (first_nonce, nonce_count) in split_range_into_blocks(0, u32::MAX, peel_amount) { - let result = miner.generate_pow_internal( - &leading_bytes, - &trailing_bytes, - &difficulty, - first_nonce, - nonce_count, - statistics, - )?; - - println!("Mining statistics: {}", statistics); - - if let Some(nonce) = result { - return Ok(MineResult::FoundNonce { - nonce: nonce.to_le_bytes().to_vec(), - }); - } + let difficulty = object.pow_difficulty().map_err(MineError::GetDifficulty)?; + let peel_amount = miner.nonce_peel_amount(); - if let Some(terminate_flag) = &terminate_flag { - if terminate_flag.load(Ordering::Acquire) { - debug!("Miner terminating after being requested to terminate"); - return Ok(MineResult::TerminateRequested); - } + // Try to generate nonces of every length + for nonce_length in ::permitted_nonce_lengths() { + if nonce_length < std::mem::size_of::() { + // SHA3_256PoWMiner only supports inserting exactly 4 bytes of nonce between objects, + // so we'll skip all nonce lengths lower than 4 + continue; } - if let Some(timeout_duration) = &timeout_duration { - let total_elapsed_time = total_start_time.elapsed(); - if total_elapsed_time >= *timeout_duration { - debug!( - "Miner terminating after reaching timeout (timeout={}s, elapsed time={}s)", - timeout_duration.as_secs_f64(), - total_elapsed_time.as_secs_f64()); - return Ok(MineResult::TimeoutReached); + // Precompute the leading and trailing bytes for the object assuming a `nonce_length`-byte + // nonce. + let (leading_bytes, trailing_bytes) = + object.get_leading_and_trailing_bytes_for_mine(nonce_length) + .map_err(MineError::GetLeadingTrailingBytes)?; + + // Since SHA3_256PoWMiner always inserts exactly 4 bytes of nonce, we'll need to provide + // an additional `nonce_length - 4` bytes so that the resulting nonce does end up being + // exactly `nonce_length` bytes long. + for nonce_supplement in all_byte_strings(nonce_length - std::mem::size_of::()) { + // In theory, it shouldn't really matter whether we put the nonce supplement before + // or after the 4-byte nonce inserted by the miner, but the miner implementation may + // prefer the leading/trailing bytes to be aligned on some boundary? That's a potential + // optimization for The Future(tm). + // For now, we'll always put the nonce supplement after the 4-byte nonce. + let leading_bytes = &leading_bytes; + let trailing_bytes = [nonce_supplement.as_ref(), trailing_bytes.as_ref()].concat(); + + // Have the miner try `peel_amount` nonces at a time. This value should be chosen + // appropriately so that mining doesn't take unnecessarily long, or cause other + // undesirable side effects like locking up the GPU for so long that the desktop + // environment crashes. + for (first_nonce, nonce_count) in split_range_into_blocks(0, u32::MAX, peel_amount) { + let result = miner.generate_pow_internal( + &leading_bytes, + &trailing_bytes, + &difficulty, + first_nonce, + nonce_count, + statistics, + )?; + + info!("Mining statistics: {}", statistics); + + if let Some(nonce_4byte) = result { + let full_nonce = [&nonce_4byte.to_le_bytes(), nonce_supplement.as_ref()].concat(); + info!("Miner found nonce: {:?}", full_nonce); + return Ok(MineResult::FoundNonce { + nonce: full_nonce, + }); + } + + // If a termination flag was provided and is set to true, stop mining now. + if let Some(terminate_flag) = &terminate_flag { + if terminate_flag.load(Ordering::Acquire) { + debug!("Miner terminating after being requested to terminate"); + return Ok(MineResult::TerminateRequested); + } + } + + // If a timeout duration was provided and has been reached, stop mining now. + if let Some(timeout_duration) = &timeout_duration { + let total_elapsed_time = total_start_time.elapsed(); + if total_elapsed_time >= *timeout_duration { + debug!( + "Miner terminating after reaching timeout (timeout={}s, elapsed time={}s)", + timeout_duration.as_secs_f64(), + total_elapsed_time.as_secs_f64()); + return Ok(MineResult::TimeoutReached); + } + } } } } @@ -410,32 +519,48 @@ pub fn generate_pow( Ok(MineResult::Exhausted) } +static OPENGL_ERRORED: OnceLock<()> = OnceLock::new(); + /// Creates a miner. +/// +/// ### Arguments +/// +/// * `difficulty` - An optional hint for the difficulty of the resource that will be mined, to +/// help choose an optimal miner implementation. pub fn create_any_miner( difficulty: Option<&PoWDifficulty>, -) -> Box { +) -> Arc> { if let Some(difficulty) = difficulty { match difficulty { // If the difficulty is sufficiently low that the overhead of a GPU miner would make // things slower, don't bother! PoWDifficulty::TargetHashAlwaysPass | PoWDifficulty::LeadingZeroBytes { leading_zeroes: ..=1 } => - return Box::new(CpuMiner::new()), + return Arc::new(Mutex::new(CpuMiner::new())), _ => (), } } - match vulkan::VulkanMiner::new() { - Ok(miner) => return Box::new(miner), + match vulkan::VulkanMiner::get() { + Ok(miner) => return miner.clone(), Err(cause) => warn!("Failed to create Vulkan miner: {cause}"), }; - match opengl::OpenGlMiner::new() { - Ok(miner) => return Box::new(miner), - Err(cause) => warn!("Failed to create OpenGL miner: {cause}"), - }; + if OPENGL_ERRORED.get().is_none() { + // Previous attempts to create an OpenGL miner have succeeded, or we haven't tried yet + match opengl::OpenGlMiner::new() { + Ok(miner) => return Arc::new(Mutex::new(miner)), + Err(cause) => { + warn!("Failed to create OpenGL miner: {cause}"); + + // Remember that OpenGL miner creation failed, so we don't keep trying over and over + // on subsequent attempts. + OPENGL_ERRORED.get_or_init(|| ()); + }, + }; + } - Box::new(CpuMiner::new()) + Arc::new(Mutex::new(CpuMiner::new())) } #[cfg(test)] @@ -498,7 +623,7 @@ pub(super) mod test { Self::THRESHOLD_VERY_HARD, ]; - pub fn test_miner(&self, miner: &mut impl SHA3_256PoWMiner, is_bench: bool) { + pub fn test_miner(&self, miner: &mut impl Sha3_256PoWMiner, is_bench: bool) { if !miner.is_hw_accelerated() && if is_bench { self.requires_hw_accel.1 } else { self.requires_hw_accel.0 } { println!("Skipping test case {} (too hard)", self.name); @@ -507,10 +632,10 @@ pub(super) mod test { let block_header = test_block_header(self.difficulty); - let difficulty = block_header.pow_difficulty(); + let difficulty = block_header.pow_difficulty().expect(self.name); let (leading_bytes, trailing_bytes) = block_header.get_leading_and_trailing_bytes_for_mine(4) - .unwrap(); + .expect(self.name); let mut statistics = Default::default(); assert_eq!( @@ -519,7 +644,7 @@ pub(super) mod test { &trailing_bytes, &difficulty, 0, self.max_nonce_count, &mut statistics, - ).unwrap(), + ).expect(self.name), Some(self.expected_nonce), "Test case {:?}", self); @@ -615,9 +740,9 @@ pub(super) mod test { #[test] fn verify_vulkan() { - let mut miner = VulkanMiner::new().unwrap(); + let miner = VulkanMiner::get().unwrap(); for case in TestBlockMinerInternal::ALL_TEST { - case.test_miner(&mut miner, false); + case.test_miner(&mut *miner.lock().unwrap(), false); } } } @@ -650,9 +775,9 @@ mod bench { #[test] fn bench_vulkan() { - let mut miner = VulkanMiner::new().unwrap(); + let miner = VulkanMiner::get().unwrap(); for case in TestBlockMinerInternal::ALL_BENCH { - case.test_miner(&mut miner, true); + case.test_miner(&mut *miner.lock().unwrap(), true); } } } diff --git a/src/miner_pow/opengl.rs b/src/miner_pow/opengl.rs index 710276e..e20b5cd 100644 --- a/src/miner_pow/opengl.rs +++ b/src/miner_pow/opengl.rs @@ -5,7 +5,7 @@ use std::sync::{Mutex, MutexGuard, OnceLock}; use gl::types::*; use glfw::{Context, Glfw, GlfwReceiver, OpenGlProfileHint, PWindow, WindowEvent, WindowHint, WindowMode}; use tracing::{debug, info, warn}; -use crate::miner_pow::{MinerStatistics, SHA3_256PoWMiner, PoWDifficulty, MineError}; +use crate::miner_pow::{MinerStatistics, Sha3_256PoWMiner, PoWDifficulty, MineError}; use crate::miner_pow::opengl::gl_error::{AddContext, CompileShaderError, GlError, LinkProgramError}; use crate::miner_pow::opengl::gl_wrapper::{Buffer, GetIntIndexedType, GetProgramIntType, GetStringType, ImmutableBuffer, IndexedBufferTarget, MemoryBarrierBit, Program, Shader, ShaderType, UniformLocation}; use crate::utils::split_range_into_blocks; @@ -42,7 +42,8 @@ impl fmt::Display for OpenGlMinerError { impl error::Error for OpenGlMinerError {} -struct GlfwContext { +#[derive(Debug)] +struct GlfwContext { mutex: Mutex, glfw_window: PWindow, @@ -51,17 +52,18 @@ struct GlfwContext { glfw_mutex_guard: MutexGuard<'static, ()>, } -impl Drop for GlfwContext { +impl Drop for GlfwContext { fn drop(&mut self) { debug!("Thread {} is destroying the GLFW context", std::thread::current().name().unwrap_or("")); } } -pub struct GlfwContextGuard<'glfw, Ctx> { +#[derive(Debug)] +pub struct GlfwContextGuard<'glfw, Ctx: fmt::Debug> { mutex_guard: MutexGuard<'glfw, Ctx>, } -impl<'glfw, Ctx> std::ops::Deref for GlfwContextGuard<'glfw, Ctx> { +impl<'glfw, Ctx: fmt::Debug> std::ops::Deref for GlfwContextGuard<'glfw, Ctx> { type Target = Ctx; fn deref(&self) -> &Self::Target { @@ -69,13 +71,13 @@ impl<'glfw, Ctx> std::ops::Deref for GlfwContextGuard<'glfw, Ctx> { } } -impl<'glfw, Ctx> std::ops::DerefMut for GlfwContextGuard<'glfw, Ctx> { +impl<'glfw, Ctx: fmt::Debug> std::ops::DerefMut for GlfwContextGuard<'glfw, Ctx> { fn deref_mut(&mut self) -> &mut Self::Target { &mut *self.mutex_guard } } -impl<'glfw, Ctx> Drop for GlfwContextGuard<'glfw, Ctx> { +impl<'glfw, Ctx: fmt::Debug> Drop for GlfwContextGuard<'glfw, Ctx> { fn drop(&mut self) { // un-bind the context before releasing the context's mutex debug!("Thread {} is releasing the GLFW context", std::thread::current().name().unwrap_or("")); @@ -85,7 +87,7 @@ impl<'glfw, Ctx> Drop for GlfwContextGuard<'glfw, Ctx> { static GLFW_MUTEX: Mutex<()> = Mutex::new(()); -impl GlfwContext { +impl GlfwContext { fn new(f: impl FnOnce() -> Result) -> Result { debug!("Thread {} is creating the GLFW context", std::thread::current().name().unwrap_or("")); @@ -130,6 +132,7 @@ impl GlfwContext { } } +#[derive(Debug)] pub struct OpenGlMiner { program: Program, program_first_nonce_uniform: UniformLocation, @@ -197,7 +200,7 @@ impl OpenGlMiner { } } -impl SHA3_256PoWMiner for OpenGlMiner { +impl Sha3_256PoWMiner for OpenGlMiner { fn is_hw_accelerated(&self) -> bool { true } @@ -573,6 +576,7 @@ mod gl_wrapper { } /// An OpenGL shader object + #[derive(Debug)] pub struct Shader { pub id: GLuint, } @@ -671,6 +675,7 @@ mod gl_wrapper { } /// An OpenGL program object + #[derive(Debug)] pub struct Program { pub id: GLuint, } @@ -808,7 +813,7 @@ mod gl_wrapper { } /// Shared trait providing functions accessible to both kinds of OpenGL buffers. - pub trait Buffer { + pub trait Buffer: fmt::Debug { fn invalidate(&mut self) -> Result<(), GlError>; fn upload_sub_data(&mut self, offset: usize, data: &[u8]) -> Result<(), GlError>; @@ -818,7 +823,7 @@ mod gl_wrapper { fn bind_base(&self, target: IndexedBufferTarget, index: GLuint) -> Result<(), GlError>; } - impl + AsMut> Buffer for T { + impl + AsMut + fmt::Debug> Buffer for T { fn invalidate(&mut self) -> Result<(), GlError> { self.as_mut().invalidate() } @@ -837,6 +842,7 @@ mod gl_wrapper { } /// An OpenGL buffer + #[derive(Debug)] struct BaseBuffer { id: GLuint, size: usize, @@ -894,6 +900,7 @@ mod gl_wrapper { } /// An OpenGL immutable buffer + #[derive(Debug)] pub struct ImmutableBuffer { internal_buffer: BaseBuffer, flags: GLbitfield, diff --git a/src/miner_pow/vulkan.rs b/src/miner_pow/vulkan.rs index 3b1da20..87561fe 100644 --- a/src/miner_pow/vulkan.rs +++ b/src/miner_pow/vulkan.rs @@ -1,9 +1,10 @@ use std::collections::BTreeMap; use std::convert::TryInto; use std::fmt; -use std::sync::Arc; +use std::sync::{Arc, Mutex, OnceLock}; use crevice::std140::AsStd140; use crevice::std430::AsStd430; +use debug_ignore::DebugIgnore; use tracing::warn; use vulkano::*; use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer}; @@ -20,16 +21,16 @@ use vulkano::pipeline::{ComputePipeline, Pipeline, PipelineBindPoint, PipelineLa use vulkano::pipeline::layout::{PipelineDescriptorSetLayoutCreateInfo, PipelineLayoutCreateInfo}; use vulkano::shader::{ShaderModule, ShaderStages, SpecializationConstant}; use vulkano::sync::GpuFuture; -use crate::miner_pow::{BLOCK_HEADER_MAX_BYTES, MineError, MinerStatistics, PoWDifficulty, SHA3_256_BYTES, SHA3_256PoWMiner}; +use crate::miner_pow::{BLOCK_HEADER_MAX_BYTES, MineError, MinerStatistics, PoWDifficulty, SHA3_256_BYTES, Sha3_256PoWMiner}; use crate::miner_pow::vulkan::sha3_256_shader::SHA3_KECCAK_SPONGE_WORDS; use crate::utils::split_range_into_blocks; // synced with vulkan_miner.glsl const WORK_GROUP_SIZE: u32 = 256; -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum VulkanMinerError { - Load(LoadingError), + Load(String), Instance(Validated), EnumerateDevices(VulkanError), NoDevices, @@ -74,6 +75,7 @@ impl VulkanMinerVariant { } } +#[derive(Debug)] pub struct VulkanMiner { uniform_buffer: Subbuffer<::Output>, response_buffer: Subbuffer<::Output>, @@ -84,7 +86,7 @@ pub struct VulkanMiner { work_group_size: u32, descriptor_set_layout_index: usize, - descriptor_set: Arc, + descriptor_set: DebugIgnore>, queue: Arc, device: Arc, @@ -92,16 +94,29 @@ pub struct VulkanMiner { instance: Arc, } +static VULKAN_MINER: OnceLock>, VulkanMinerError>> = OnceLock::new(); + impl VulkanMiner { - pub fn new() -> Result { - let library = VulkanLibrary::new().map_err(VulkanMinerError::Load)?; + pub fn get() -> Result>, VulkanMinerError> { + VULKAN_MINER.get_or_init(|| Self::new().map(|miner| Arc::new(Mutex::new(miner)))) + .clone() + //Self::new().map(|miner| Arc::new(Mutex::new(miner))) + } + + fn new() -> Result { + let library = VulkanLibrary::new() + .map_err(|err| VulkanMinerError::Load(err.to_string()))?; let instance = Instance::new(library, InstanceCreateInfo::default()) .map_err(VulkanMinerError::Instance)?; let physical_device = instance .enumerate_physical_devices() .map_err(VulkanMinerError::EnumerateDevices)? - //.filter(|d| d.supported_features().shader_int64) + .filter(|d| { + let features = d.supported_features(); + features.shader_int8 && + features.shader_int64 + }) .next() .ok_or_else(|| VulkanMinerError::NoDevices)?; @@ -215,7 +230,7 @@ impl VulkanMiner { work_group_size: WORK_GROUP_SIZE, descriptor_set_layout_index, - descriptor_set, + descriptor_set: descriptor_set.into(), queue, device, @@ -252,7 +267,7 @@ impl VulkanMiner { } } -impl SHA3_256PoWMiner for VulkanMiner { +impl Sha3_256PoWMiner for VulkanMiner { fn is_hw_accelerated(&self) -> bool { true } @@ -374,7 +389,7 @@ impl SHA3_256PoWMiner for VulkanMiner { PipelineBindPoint::Compute, compute_pipeline.layout().clone(), self.descriptor_set_layout_index as u32, - self.descriptor_set.clone(), + (*self.descriptor_set).clone(), ) .unwrap() .dispatch(work_group_counts) diff --git a/src/utils.rs b/src/utils.rs index b4af78e..5c4205e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,6 +16,7 @@ use tracing_subscriber::field::debug; use std::collections::BTreeMap; use std::error::Error; use std::{fmt, vec}; +use std::fmt::Write; use std::fs::File; use std::future::Future; use std::io::Read; @@ -44,6 +45,8 @@ use tw_chain::utils::transaction_utils::{ get_tx_out_with_out_point, get_tx_out_with_out_point_cloned, }; use url::Url; +use crate::asert::{CompactTarget, CompactTargetError, HeaderHash}; +use crate::miner_pow::{MineError, PoWError, PoWObject}; pub type RoutesPoWInfo = Arc>>; pub type ApiKeys = Arc>>>; @@ -176,21 +179,35 @@ impl fmt::Debug for LocalEventChannel { #[derive(PartialEq, Eq)] pub struct StringError(pub String); -impl Error for StringError { - fn description(&self) -> &str { - &self.0 +impl StringError { + pub fn from(value: T) -> Self { + Self(value.to_string()) } } +impl Error for StringError {} + impl fmt::Display for StringError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) + f.write_str(&self.0) } } impl fmt::Debug for StringError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) + f.write_str(&self.0) + } +} + +impl From for StringError { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for StringError { + fn from(value: &str) -> Self { + Self(value.to_owned()) } } @@ -605,7 +622,7 @@ pub fn validate_pow_for_address(pow: &ProofOfWork, rand_num: &Option<&Vec>) pow_body.extend(rand_num.iter().flat_map(|r| r.iter()).copied()); pow_body.extend(&pow.nonce); - validate_pow_leading_zeroes(&pow_body).is_some() + validate_pow_leading_zeroes(&pow_body).is_ok() } /// Will attempt deserialization of a given byte array using bincode @@ -622,23 +639,30 @@ pub fn try_deserialize<'a, T: Deserialize<'a>>(data: &'a [u8]) -> Result Result>, Box> { +pub fn generate_pow_for_block(header: &BlockHeader) -> Result>, MineError> { use crate::miner_pow::*; let mut statistics = Default::default(); let terminate_flag = None; let timeout_duration = None; - let mut miner = create_any_miner(Some(&header.pow_difficulty())); - let result = generate_pow(&mut *miner, header, &mut statistics, terminate_flag, timeout_duration) - .map_err(Box::new)?; + let miner = create_any_miner(Some( + &header.pow_difficulty().map_err(MineError::GetDifficulty)? + )); + let result = generate_pow( + &mut *miner.lock().unwrap(), + header, + &mut statistics, + terminate_flag, + timeout_duration, + )?; match result { MineResult::FoundNonce { nonce } => { // Verify that the found nonce is actually valid let mut header_copy = header.clone(); header_copy.nonce_and_mining_tx_hash.0 = nonce.clone(); - assert!(validate_pow_block(&header_copy), + assert_eq!(validate_pow_block(&header_copy), Ok(()), "generated PoW nonce {} isn't actually valid! block header: {:#?}", hex::encode(&nonce), header); Ok(Some(header_copy.nonce_and_mining_tx_hash.0)) @@ -667,22 +691,63 @@ pub fn construct_valid_block_pow_hash(block: &Block) -> Result fmt::Result { + match self { + Self::InvalidNonce { cause } => + write!(f, "Block header nonce is invalid: {}", cause), + Self::InvalidDifficulty { cause } => + write!(f, "Block header difficulty is invalid: {}", cause), + Self::InvalidLeadingBytes { cause } => + write!(f, "Block header does not meet the difficulty requirements: {}", cause), + Self::DoesntMeetThreshold { header_hash: hash, target } => + write!(f, "Block header does not meet the difficulty requirements: target={}, hash={:?}", + target, hash.as_bytes()), + } + } +} + /// Validate Proof of Work for a block with a mining transaction /// /// ### Arguments /// /// * `header` - The header for PoW -pub fn validate_pow_block(header: &BlockHeader) -> bool { - validate_pow_block_hash(header).is_some() +pub fn validate_pow_block(header: &BlockHeader) -> Result<(), ValidatePoWBlockError> { + validate_pow_block_hash(header).map(|_| ()) } /// Validate Proof of Work for a block with a mining transaction returning the PoW hash @@ -690,7 +755,9 @@ pub fn validate_pow_block(header: &BlockHeader) -> bool { /// ### Arguments /// /// * `header` - The header for PoW -fn validate_pow_block_hash(header: &BlockHeader) -> Option> { +fn validate_pow_block_hash( + header: &BlockHeader, +) -> Result { // [AM] even though we've got explicit activation height in configuration // and a hard-coded fallback elsewhere in the code, here // we're basically sniffing at the difficulty field in the @@ -704,36 +771,64 @@ fn validate_pow_block_hash(header: &BlockHeader) -> Option> { // arguments or be moved to something more stateful that can // access configuration. + // Ensure the nonce length is valid + BlockHeader::check_nonce_length(header.get_nonce().len()) + .map_err(|cause| ValidatePoWBlockError::InvalidNonce { cause })?; + if header.difficulty.is_empty() { let pow = serialize(header).unwrap(); validate_pow_leading_zeroes(&pow) + .map(HeaderHash::from) + .map_err(|cause| ValidatePoWBlockError::InvalidLeadingBytes { cause }) } else { - use crate::asert::{CompactTarget, HeaderHash}; - info!("We have difficuly"); + info!("We have difficulty"); - let target = CompactTarget::try_from_slice(&header.difficulty)?; - let header_hash = HeaderHash::try_calculate(header)?; + let target = CompactTarget::try_from_slice(&header.difficulty) + .map_err(|cause| ValidatePoWBlockError::InvalidDifficulty { cause })?; + let header_hash = HeaderHash::calculate(header); if header_hash.is_below_compact_target(&target) { - Some(header_hash.into_vec()) + Ok(header_hash) } else { - None + Err(ValidatePoWBlockError::DoesntMeetThreshold { header_hash, target }) } } } +/// An error which indicates that a Proof-of-Work action contained an invalid number of leading +/// zero bytes. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct ValidatePoWLeadingZeroesError { + /// The computed hash + pub hash: sha3::digest::Output, + /// The mining difficulty + pub mining_difficulty: usize, +} + +impl Error for ValidatePoWLeadingZeroesError {} + +impl fmt::Display for ValidatePoWLeadingZeroesError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Hash {:?} has fewer than {} leading zero bytes", + self.hash.as_slice(), self.mining_difficulty) + } +} + /// Check the hash of given data reach MINING_DIFFICULTY /// /// ### Arguments /// /// * `mining_difficulty` - usize mining difficulty /// * `pow` - &u8 proof of work -fn validate_pow_leading_zeroes_for_diff(mining_difficulty: usize, pow: &[u8]) -> Option> { - let pow_hash = sha3_256::digest(pow).to_vec(); - if pow_hash[0..mining_difficulty].iter().all(|v| *v == 0) { - Some(pow_hash) +fn validate_pow_leading_zeroes_for_diff( + mining_difficulty: usize, + pow: &[u8], +) -> Result, ValidatePoWLeadingZeroesError> { + let hash = sha3_256::digest(pow); + if hash.as_slice()[0..mining_difficulty].iter().all(|v| *v == 0) { + Ok(hash) } else { - None + Err(ValidatePoWLeadingZeroesError { hash, mining_difficulty }) } } @@ -742,7 +837,9 @@ fn validate_pow_leading_zeroes_for_diff(mining_difficulty: usize, pow: &[u8]) -> /// ### Arguments /// /// * `pow` - &u8 proof of work -fn validate_pow_leading_zeroes(pow: &[u8]) -> Option> { +fn validate_pow_leading_zeroes( + pow: &[u8], +) -> Result, ValidatePoWLeadingZeroesError> { validate_pow_leading_zeroes_for_diff(MINING_DIFFICULTY, pow) } @@ -1459,6 +1556,59 @@ pub fn split_range_into_blocks( } } +/// Returns all possible byte strings with the given length. +/// +/// Note that for a length of `0` this will return a single 0-length byte string. +/// +/// ### Arguments +/// +/// * `len` - the length of the byte strings to generate +pub fn all_byte_strings( + len: usize, +) -> impl Iterator> { + struct Itr { + buf: Box<[u8]>, + done: bool, + } + + impl Iterator for Itr { + type Item = Box<[u8]>; + + fn next(&mut self) -> Option { + if self.done { + // Iterator has already reached the end + return None; + } + + // Save the current value to be returned later + let result = self.buf.clone(); + + // Try to increment the value + for byte in self.buf.as_mut() { + if let Some(next_value) = (*byte).checked_add(1) { + // The byte was incremented successfully + *byte = next_value; + return Some(result); + } else { + // The byte overflowed, set it to 0 and proceed to increment the next one + *byte = 0; + } + } + + // If we got this far, all the bytes were 0xFF, meaning that we've successfully + // iterated through the entire sequence. + self.done = true; + + Some(result) + } + } + + Itr { + buf: vec![0u8; len].into_boxed_slice(), + done: false, + } +} + /*---- TESTS ----*/ #[cfg(test)] @@ -1518,4 +1668,19 @@ mod util_tests { (3 << 30, (1 << 30) - 1), ]); } + + #[test] + fn test_all_byte_strings() { + assert_eq!( + all_byte_strings(0).collect::>(), + vec![Box::from([])]); + + assert_eq!( + all_byte_strings(1).collect::>(), + (u8::MIN..=u8::MAX).map(|b| b.to_le_bytes().into()).collect::>()); + + assert_eq!( + all_byte_strings(2).collect::>(), + (u16::MIN..=u16::MAX).map(|b| b.to_le_bytes().into()).collect::>()); + } }