use std::collections::HashSet; use crypto_bigint::{CheckedMul, U256}; use monero_serai::block::Block; use cuprate_cryptonight::*; use crate::{ check_block_version_vote, current_unix_timestamp, hard_forks::HardForkError, miner_tx::{check_miner_tx, MinerTxError}, HardFork, }; const BLOCK_SIZE_SANITY_LEEWAY: usize = 100; const BLOCK_FUTURE_TIME_LIMIT: u64 = 60 * 60 * 2; const BLOCK_202612_POW_HASH: [u8; 32] = hex_literal::hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000"); pub const PENALTY_FREE_ZONE_1: usize = 20000; pub const PENALTY_FREE_ZONE_2: usize = 60000; pub const PENALTY_FREE_ZONE_5: usize = 300000; pub const RX_SEEDHASH_EPOCH_BLOCKS: usize = 2048; pub const RX_SEEDHASH_EPOCH_LAG: usize = 64; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum BlockError { #[error("The blocks POW is invalid.")] POWInvalid, #[error("The block is too big.")] TooLarge, #[error("The block has too many transactions.")] TooManyTxs, #[error("The blocks previous ID is incorrect.")] PreviousIDIncorrect, #[error("The blocks timestamp is invalid.")] TimeStampInvalid, #[error("The block contains a duplicate transaction.")] DuplicateTransaction, #[error("Hard-fork error: {0}")] HardForkError(#[from] HardForkError), #[error("Miner transaction error: {0}")] MinerTxError(#[from] MinerTxError), } /// A trait to represent the `RandomX` VM. pub trait RandomX { type Error; fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>; } /// Returns if this height is a `RandomX` seed height. pub const fn is_randomx_seed_height(height: usize) -> bool { height % RX_SEEDHASH_EPOCH_BLOCKS == 0 } /// Returns the `RandomX` seed height for this block. /// /// ref: pub const fn randomx_seed_height(height: usize) -> usize { if height <= RX_SEEDHASH_EPOCH_BLOCKS + RX_SEEDHASH_EPOCH_LAG { 0 } else { (height - RX_SEEDHASH_EPOCH_LAG - 1) & !(RX_SEEDHASH_EPOCH_BLOCKS - 1) } } /// Calculates the POW hash of this block. /// /// `randomx_vm` must be [`Some`] after hf 12. /// /// ref: pub fn calculate_pow_hash( randomx_vm: Option<&R>, buf: &[u8], height: usize, hf: &HardFork, ) -> Result<[u8; 32], BlockError> { if height == 202612 { return Ok(BLOCK_202612_POW_HASH); } Ok(if hf < &HardFork::V7 { cryptonight_hash_v0(buf) } else if hf == &HardFork::V7 { cryptonight_hash_v1(buf).map_err(|_| BlockError::POWInvalid)? } else if hf < &HardFork::V10 { cryptonight_hash_v2(buf) } else if hf < &HardFork::V12 { // FIXME: https://github.com/Cuprate/cuprate/issues/167. cryptonight_hash_r(buf, height as u64) } else { randomx_vm .expect("RandomX VM needed from hf 12") .calculate_hash(buf) .map_err(|_| BlockError::POWInvalid)? }) } /// Returns if the blocks POW hash is valid for the current difficulty. /// /// ref: pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> { let int_hash = U256::from_le_slice(hash); let difficulty = U256::from(difficulty); if int_hash.checked_mul(&difficulty).is_none().unwrap_u8() == 1 { tracing::debug!( "Invalid POW: {}, difficulty: {}", hex::encode(hash), difficulty ); Err(BlockError::POWInvalid) } else { Ok(()) } } /// Returns the penalty free zone /// /// pub fn penalty_free_zone(hf: HardFork) -> usize { if hf == HardFork::V1 { PENALTY_FREE_ZONE_1 } else if hf >= HardFork::V2 && hf < HardFork::V5 { PENALTY_FREE_ZONE_2 } else { PENALTY_FREE_ZONE_5 } } /// Sanity check on the block blob size. /// /// ref: const fn block_size_sanity_check( block_blob_len: usize, effective_median: usize, ) -> Result<(), BlockError> { if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY { Err(BlockError::TooLarge) } else { Ok(()) } } /// Sanity check on the block weight. /// /// ref: pub const fn check_block_weight( block_weight: usize, median_for_block_reward: usize, ) -> Result<(), BlockError> { if block_weight > median_for_block_reward * 2 { Err(BlockError::TooLarge) } else { Ok(()) } } /// Sanity check on number of txs in the block. /// /// ref: const fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> { if number_none_miner_txs + 1 > 0x10000000 { Err(BlockError::TooManyTxs) } else { Ok(()) } } /// Verifies the previous id is the last blocks hash /// /// ref: fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> { if &block.header.previous == top_hash { Ok(()) } else { Err(BlockError::PreviousIDIncorrect) } } /// Checks the blocks timestamp is in the valid range. /// /// ref: pub fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { if block.header.timestamp < median_timestamp || block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT { Err(BlockError::TimeStampInvalid) } else { Ok(()) } } /// Checks that all txs in the block have a unique hash. /// /// ref: fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> { let set = txs.iter().collect::>(); if set.len() == txs.len() { Ok(()) } else { Err(BlockError::DuplicateTransaction) } } /// This struct contains the data needed to verify a block, implementers MUST make sure /// the data in this struct is calculated correctly. #[derive(Debug, Clone)] pub struct ContextToVerifyBlock { /// ref: pub median_weight_for_block_reward: usize, /// ref: pub effective_median_weight: usize, /// The top hash of the blockchain, aka the block hash of the previous block to the one we are verifying. pub top_hash: [u8; 32], /// Contains the median timestamp over the last 60 blocks, if there is less than 60 blocks this should be [`None`] pub median_block_timestamp: Option, /// The current chain height. pub chain_height: usize, /// The current hard-fork. pub current_hf: HardFork, /// ref: pub next_difficulty: u128, /// The amount of coins already minted. pub already_generated_coins: u64, } /// Checks the block is valid returning the blocks hard-fork `VOTE` and the amount of coins generated in this block. /// /// This does not check the POW nor does it calculate the POW hash, this is because checking POW is very expensive and /// to allow the computation of the POW hashes to be done separately. This also does not check the transactions in the /// block are valid. /// /// Missed block checks in this function: /// /// /// /// /// pub fn check_block( block: &Block, total_fees: u64, block_weight: usize, block_blob_len: usize, block_chain_ctx: &ContextToVerifyBlock, ) -> Result<(HardFork, u64), BlockError> { let (version, vote) = HardFork::from_block_header(&block.header).map_err(|_| HardForkError::HardForkUnknown)?; check_block_version_vote(&block_chain_ctx.current_hf, &version, &vote)?; if let Some(median_timestamp) = block_chain_ctx.median_block_timestamp { check_timestamp(block, median_timestamp)?; } check_prev_id(block, &block_chain_ctx.top_hash)?; check_block_weight(block_weight, block_chain_ctx.median_weight_for_block_reward)?; block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?; check_amount_txs(block.transactions.len())?; check_txs_unique(&block.transactions)?; let generated_coins = check_miner_tx( &block.miner_transaction, total_fees, block_chain_ctx.chain_height, block_weight, block_chain_ctx.median_weight_for_block_reward, block_chain_ctx.already_generated_coins, block_chain_ctx.current_hf, )?; Ok((vote, generated_coins)) } #[cfg(test)] mod tests { use proptest::{collection::vec, prelude::*}; use super::*; proptest! { #[test] fn test_check_unique_txs( mut txs in vec(any::<[u8; 32]>(), 2..3000), duplicate in any::<[u8; 32]>(), dup_idx_1 in any::(), dup_idx_2 in any::(), ) { prop_assert!(check_txs_unique(&txs).is_ok()); txs.insert(dup_idx_1 % txs.len(), duplicate); txs.insert(dup_idx_2 % txs.len(), duplicate); prop_assert!(check_txs_unique(&txs).is_err()); } } }