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),
}
pub trait RandomX {
type Error;
fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>;
}
pub const fn is_randomx_seed_height(height: usize) -> bool {
height % RX_SEEDHASH_EPOCH_BLOCKS == 0
}
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)
}
}
pub fn calculate_pow_hash<R: RandomX>(
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 {
cryptonight_hash_r(buf, height as u64)
} else {
randomx_vm
.expect("RandomX VM needed from hf 12")
.calculate_hash(buf)
.map_err(|_| BlockError::POWInvalid)?
})
}
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(())
}
}
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
}
}
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(())
}
}
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(())
}
}
const fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> {
if number_none_miner_txs + 1 > 0x10000000 {
Err(BlockError::TooManyTxs)
} else {
Ok(())
}
}
fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> {
if &block.header.previous == top_hash {
Ok(())
} else {
Err(BlockError::PreviousIDIncorrect)
}
}
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(())
}
}
fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> {
let set = txs.iter().collect::<HashSet<_>>();
if set.len() == txs.len() {
Ok(())
} else {
Err(BlockError::DuplicateTransaction)
}
}
#[derive(Debug, Clone)]
pub struct ContextToVerifyBlock {
pub median_weight_for_block_reward: usize,
pub effective_median_weight: usize,
pub top_hash: [u8; 32],
pub median_block_timestamp: Option<u64>,
pub chain_height: usize,
pub current_hf: HardFork,
pub next_difficulty: u128,
pub already_generated_coins: u64,
}
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::<usize>(),
dup_idx_2 in any::<usize>(),
) {
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());
}
}
}