cuprate-hinto-janai/consensus/rules/src/blocks.rs

275 lines
8.8 KiB
Rust
Raw Normal View History

use monero_serai::block::Block;
use primitive_types::U256;
use cryptonight_cuprate::*;
use crate::{
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");
2024-01-08 01:26:44 +00:00
pub const PENALTY_FREE_ZONE_1: usize = 20000;
pub const PENALTY_FREE_ZONE_2: usize = 60000;
pub const PENALTY_FREE_ZONE_5: usize = 300000;
const RX_SEEDHASH_EPOCH_BLOCKS: u64 = 2048;
const RX_SEEDHASH_EPOCH_LAG: u64 = 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 fn is_randomx_seed_height(height: u64) -> bool {
height % RX_SEEDHASH_EPOCH_BLOCKS == 0
}
/// Returns the RandomX seed height for this block.
///
/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#randomx-seed
pub fn randomx_seed_height(height: u64) -> u64 {
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.
///
/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#pow-function
pub fn calculate_pow_hash<R: RandomX>(
randomx_vm: &R,
buf: &[u8],
height: u64,
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)
} else {
randomx_vm
.calculate_hash(buf)
.map_err(|_| BlockError::POWInvalid)?
})
}
/// Returns if the blocks POW hash is valid for the current difficulty.
///
/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash
pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> {
let int_hash = U256::from_little_endian(hash);
let difficulty = U256::from(difficulty);
if int_hash.checked_mul(difficulty).is_none() {
tracing::debug!(
"Invalid POW: {}, difficulty: {}",
hex::encode(hash),
difficulty
);
Err(BlockError::POWInvalid)
} else {
Ok(())
}
}
2024-01-08 01:26:44 +00:00
/// Returns the penalty free zone
///
/// https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#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: https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size
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: https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size
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: https://monero-book.cuprate.org/consensus_rules/blocks.html#amount-of-transactions
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: https://monero-book.cuprate.org/consensus_rules/blocks.html#previous-id
fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> {
if &block.header.previous != top_hash {
Err(BlockError::PreviousIDIncorrect)
} else {
Ok(())
}
}
/// Checks the blocks timestamp is in the valid range.
///
/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#timestamp
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: https://monero-book.cuprate.org/consensus_rules/blocks.html#no-duplicate-transactions
fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> {
txs.windows(2).try_for_each(|window| {
2024-01-08 01:26:44 +00:00
if window[0] == window[1] {
Err(BlockError::DuplicateTransaction)?;
}
Ok(())
})
}
/// 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: https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#median-weight-for-coinbase-checks
pub median_weight_for_block_reward: usize,
/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#effective-median-weight
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<u64>,
/// The current chain height.
pub chain_height: u64,
/// The current hard-fork.
pub current_hf: HardFork,
/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#calculating-difficulty
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:
///
/// https://monero-book.cuprate.org/consensus_rules/blocks.html#key-images
/// https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash
///
///
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)?;
block_chain_ctx
.current_hf
.check_block_version_vote(&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.txs.len())?;
check_txs_unique(&block.txs)?;
let generated_coins = check_miner_tx(
&block.miner_tx,
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))
}