2024-05-31 00:52:12 +00:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
2024-01-21 15:18:25 +00:00
|
|
|
use crypto_bigint::{CheckedMul, U256};
|
2023-12-16 23:03:02 +00:00
|
|
|
use monero_serai::block::Block;
|
|
|
|
|
2024-06-24 01:30:47 +00:00
|
|
|
use cuprate_cryptonight::*;
|
2023-12-16 23:03:02 +00:00
|
|
|
|
|
|
|
use crate::{
|
2023-12-17 14:50:08 +00:00
|
|
|
current_unix_timestamp,
|
2023-12-16 23:03:02 +00:00
|
|
|
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;
|
2024-01-05 22:36:47 +00:00
|
|
|
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;
|
|
|
|
|
2024-02-13 00:51:11 +00:00
|
|
|
pub const RX_SEEDHASH_EPOCH_BLOCKS: u64 = 2048;
|
|
|
|
pub const RX_SEEDHASH_EPOCH_LAG: u64 = 64;
|
2023-12-16 23:03:02 +00:00
|
|
|
|
|
|
|
#[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,
|
2024-01-07 01:15:33 +00:00
|
|
|
#[error("The block contains a duplicate transaction.")]
|
|
|
|
DuplicateTransaction,
|
2023-12-16 23:03:02 +00:00
|
|
|
#[error("Hard-fork error: {0}")]
|
|
|
|
HardForkError(#[from] HardForkError),
|
|
|
|
#[error("Miner transaction error: {0}")]
|
|
|
|
MinerTxError(#[from] MinerTxError),
|
|
|
|
}
|
|
|
|
|
2024-01-07 01:15:33 +00:00
|
|
|
/// A trait to represent the RandomX VM.
|
2024-01-05 22:36:47 +00:00
|
|
|
pub trait RandomX {
|
|
|
|
type Error;
|
|
|
|
|
|
|
|
fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>;
|
|
|
|
}
|
|
|
|
|
2024-01-07 01:15:33 +00:00
|
|
|
/// Returns if this height is a RandomX seed height.
|
2024-01-05 22:36:47 +00:00
|
|
|
pub fn is_randomx_seed_height(height: u64) -> bool {
|
|
|
|
height % RX_SEEDHASH_EPOCH_BLOCKS == 0
|
|
|
|
}
|
|
|
|
|
2024-01-07 01:15:33 +00:00
|
|
|
/// Returns the RandomX seed height for this block.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#randomx-seed>
|
2024-01-05 22:36:47 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-16 23:03:02 +00:00
|
|
|
/// Calculates the POW hash of this block.
|
2024-01-07 01:15:33 +00:00
|
|
|
///
|
2024-01-09 22:39:29 +00:00
|
|
|
/// `randomx_vm` must be [`Some`] after hf 12.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#pow-function>
|
2024-01-05 22:36:47 +00:00
|
|
|
pub fn calculate_pow_hash<R: RandomX>(
|
2024-01-09 22:39:29 +00:00
|
|
|
randomx_vm: Option<&R>,
|
2024-01-05 22:36:47 +00:00
|
|
|
buf: &[u8],
|
|
|
|
height: u64,
|
|
|
|
hf: &HardFork,
|
|
|
|
) -> Result<[u8; 32], BlockError> {
|
2023-12-16 23:03:02 +00:00
|
|
|
if height == 202612 {
|
2024-01-05 22:36:47 +00:00
|
|
|
return Ok(BLOCK_202612_POW_HASH);
|
2023-12-16 23:03:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2024-01-05 22:36:47 +00:00
|
|
|
randomx_vm
|
2024-01-09 22:39:29 +00:00
|
|
|
.expect("RandomX VM needed from hf 12")
|
2024-01-05 22:36:47 +00:00
|
|
|
.calculate_hash(buf)
|
|
|
|
.map_err(|_| BlockError::POWInvalid)?
|
2023-12-16 23:03:02 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns if the blocks POW hash is valid for the current difficulty.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash>
|
2023-12-16 23:03:02 +00:00
|
|
|
pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> {
|
2024-01-21 15:18:25 +00:00
|
|
|
let int_hash = U256::from_le_slice(hash);
|
2023-12-16 23:03:02 +00:00
|
|
|
|
|
|
|
let difficulty = U256::from(difficulty);
|
|
|
|
|
2024-01-21 15:18:25 +00:00
|
|
|
if int_hash.checked_mul(&difficulty).is_none().unwrap_u8() == 1 {
|
2024-01-05 22:36:47 +00:00
|
|
|
tracing::debug!(
|
|
|
|
"Invalid POW: {}, difficulty: {}",
|
|
|
|
hex::encode(hash),
|
|
|
|
difficulty
|
|
|
|
);
|
2023-12-16 23:03:02 +00:00
|
|
|
Err(BlockError::POWInvalid)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-08 01:26:44 +00:00
|
|
|
/// Returns the penalty free zone
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// <https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#penalty-free-zone>
|
2024-01-08 01:26:44 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-16 23:03:02 +00:00
|
|
|
/// Sanity check on the block blob size.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size>
|
2023-12-16 23:03:02 +00:00
|
|
|
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.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size>
|
2023-12-16 23:03:02 +00:00
|
|
|
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(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-07 01:15:33 +00:00
|
|
|
/// Sanity check on number of txs in the block.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#amount-of-transactions>
|
2024-01-07 01:15:33 +00:00
|
|
|
fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> {
|
|
|
|
if number_none_miner_txs + 1 > 0x10000000 {
|
|
|
|
Err(BlockError::TooManyTxs)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-16 23:03:02 +00:00
|
|
|
/// Verifies the previous id is the last blocks hash
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#previous-id>
|
2023-12-16 23:03:02 +00:00
|
|
|
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.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#timestamp>
|
2023-12-16 23:03:02 +00:00
|
|
|
fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> {
|
|
|
|
if block.header.timestamp < median_timestamp
|
2023-12-17 14:50:08 +00:00
|
|
|
|| block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT
|
2023-12-16 23:03:02 +00:00
|
|
|
{
|
|
|
|
Err(BlockError::TimeStampInvalid)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-07 01:15:33 +00:00
|
|
|
/// Checks that all txs in the block have a unique hash.
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#no-duplicate-transactions>
|
2024-01-07 01:15:33 +00:00
|
|
|
fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> {
|
2024-05-31 00:52:12 +00:00
|
|
|
let set = txs.iter().collect::<HashSet<_>>();
|
|
|
|
|
|
|
|
if set.len() == txs.len() {
|
2024-01-07 01:15:33 +00:00
|
|
|
Ok(())
|
2024-05-31 00:52:12 +00:00
|
|
|
} else {
|
|
|
|
Err(BlockError::DuplicateTransaction)
|
|
|
|
}
|
2024-01-07 01:15:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// This struct contains the data needed to verify a block, implementers MUST make sure
|
|
|
|
/// the data in this struct is calculated correctly.
|
2023-12-16 23:03:02 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct ContextToVerifyBlock {
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#median-weight-for-coinbase-checks>
|
2023-12-16 23:03:02 +00:00
|
|
|
pub median_weight_for_block_reward: usize,
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#effective-median-weight>
|
2023-12-16 23:03:02 +00:00
|
|
|
pub effective_median_weight: usize,
|
2024-01-07 01:15:33 +00:00
|
|
|
/// The top hash of the blockchain, aka the block hash of the previous block to the one we are verifying.
|
2023-12-16 23:03:02 +00:00
|
|
|
pub top_hash: [u8; 32],
|
2024-01-07 01:15:33 +00:00
|
|
|
/// Contains the median timestamp over the last 60 blocks, if there is less than 60 blocks this should be [`None`]
|
2023-12-16 23:03:02 +00:00
|
|
|
pub median_block_timestamp: Option<u64>,
|
2024-01-07 01:15:33 +00:00
|
|
|
/// The current chain height.
|
2023-12-16 23:03:02 +00:00
|
|
|
pub chain_height: u64,
|
2024-01-07 01:15:33 +00:00
|
|
|
/// The current hard-fork.
|
2023-12-16 23:03:02 +00:00
|
|
|
pub current_hf: HardFork,
|
2024-02-10 00:08:39 +00:00
|
|
|
/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#calculating-difficulty>
|
2023-12-16 23:03:02 +00:00
|
|
|
pub next_difficulty: u128,
|
2024-01-07 01:15:33 +00:00
|
|
|
/// The amount of coins already minted.
|
2023-12-16 23:03:02 +00:00
|
|
|
pub already_generated_coins: u64,
|
|
|
|
}
|
|
|
|
|
2024-01-07 01:15:33 +00:00
|
|
|
/// 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:
|
|
|
|
///
|
2024-02-10 00:08:39 +00:00
|
|
|
/// <https://monero-book.cuprate.org/consensus_rules/blocks.html#key-images>
|
|
|
|
/// <https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash>
|
2024-01-07 01:15:33 +00:00
|
|
|
///
|
2023-12-16 23:03:02 +00:00
|
|
|
///
|
|
|
|
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())?;
|
2024-01-07 01:15:33 +00:00
|
|
|
check_txs_unique(&block.txs)?;
|
2023-12-16 23:03:02 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
2024-05-31 00:52:12 +00:00
|
|
|
|
|
|
|
#[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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|