1use std::collections::HashSet;
2
3use crypto_bigint::U256;
4use monero_oxide::block::Block;
5
6use cuprate_cryptonight::*;
7
8use crate::{
9 check_block_version_vote, current_unix_timestamp,
10 hard_forks::HardForkError,
11 miner_tx::{check_miner_tx, MinerTxError},
12 HardFork,
13};
14
15const BLOCK_SIZE_SANITY_LEEWAY: usize = 100;
16const BLOCK_FUTURE_TIME_LIMIT: u64 = 60 * 60 * 2;
17const BLOCK_202612_POW_HASH: [u8; 32] =
18 hex_literal::hex!("84f64766475d51837ac9efbef1926486e58563c95a19fef4aec3254f03000000");
19
20pub const PENALTY_FREE_ZONE_1: usize = 20000;
21pub const PENALTY_FREE_ZONE_2: usize = 60000;
22pub const PENALTY_FREE_ZONE_5: usize = 300000;
23
24pub const RX_SEEDHASH_EPOCH_BLOCKS: usize = 2048;
25pub const RX_SEEDHASH_EPOCH_LAG: usize = 64;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
28pub enum BlockError {
29 #[error("The block's POW is invalid.")]
30 POWInvalid,
31 #[error("The block is too big.")]
32 TooLarge,
33 #[error("The block has too many transactions.")]
34 TooManyTxs,
35 #[error("The block's previous ID is incorrect.")]
36 PreviousIDIncorrect,
37 #[error("The block's timestamp is invalid.")]
38 TimeStampInvalid,
39 #[error("The block contains a duplicate transaction.")]
40 DuplicateTransaction,
41 #[error("The transactions passed in with the block are incorrect.")]
42 TxsIncludedWithBlockIncorrect,
43 #[error("Block batch verification failed.")]
44 BatchVerificationFailed,
45 #[error("No blocks to verify.")]
46 NoBlocksToVerify,
47 #[error("Hard-fork error: {0}")]
48 HardForkError(#[from] HardForkError),
49 #[error("Miner transaction error: {0}")]
50 MinerTxError(#[from] MinerTxError),
51}
52
53pub trait RandomX {
55 type Error;
56
57 fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>;
58}
59
60pub const fn is_randomx_seed_height(height: usize) -> bool {
62 height.is_multiple_of(RX_SEEDHASH_EPOCH_BLOCKS)
63}
64
65pub const fn randomx_seed_height(height: usize) -> usize {
69 if height <= RX_SEEDHASH_EPOCH_BLOCKS + RX_SEEDHASH_EPOCH_LAG {
70 0
71 } else {
72 (height - RX_SEEDHASH_EPOCH_LAG - 1) & !(RX_SEEDHASH_EPOCH_BLOCKS - 1)
73 }
74}
75
76pub fn calculate_pow_hash<R: RandomX>(
82 randomx_vm: Option<&R>,
83 buf: &[u8],
84 height: usize,
85 hf: &HardFork,
86) -> Result<[u8; 32], BlockError> {
87 if height == 202612 {
88 return Ok(BLOCK_202612_POW_HASH);
89 }
90
91 Ok(if hf < &HardFork::V7 {
92 cryptonight_hash_v0(buf)
93 } else if hf == &HardFork::V7 {
94 cryptonight_hash_v1(buf).map_err(|_| BlockError::POWInvalid)?
95 } else if hf < &HardFork::V10 {
96 cryptonight_hash_v2(buf)
97 } else if hf < &HardFork::V12 {
98 cryptonight_hash_r(buf, height as u64)
100 } else {
101 randomx_vm
102 .expect("RandomX VM needed from hf 12")
103 .calculate_hash(buf)
104 .map_err(|_| BlockError::POWInvalid)?
105 })
106}
107
108pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> {
112 let int_hash = U256::from_le_slice(hash);
113
114 let difficulty = U256::from(difficulty);
115
116 if bool::from(int_hash.checked_mul(&difficulty).is_none()) {
117 tracing::debug!(
118 "Invalid POW: {}, difficulty: {}",
119 hex::encode(hash),
120 difficulty
121 );
122 Err(BlockError::POWInvalid)
123 } else {
124 Ok(())
125 }
126}
127
128pub fn penalty_free_zone(hf: HardFork) -> usize {
132 if hf == HardFork::V1 {
133 PENALTY_FREE_ZONE_1
134 } else if hf >= HardFork::V2 && hf < HardFork::V5 {
135 PENALTY_FREE_ZONE_2
136 } else {
137 PENALTY_FREE_ZONE_5
138 }
139}
140
141const fn block_size_sanity_check(
145 block_blob_len: usize,
146 effective_median: usize,
147) -> Result<(), BlockError> {
148 if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY {
149 Err(BlockError::TooLarge)
150 } else {
151 Ok(())
152 }
153}
154
155pub const fn check_block_weight(
159 block_weight: usize,
160 median_for_block_reward: usize,
161) -> Result<(), BlockError> {
162 if block_weight > median_for_block_reward * 2 {
163 Err(BlockError::TooLarge)
164 } else {
165 Ok(())
166 }
167}
168
169const fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> {
173 if number_none_miner_txs + 1 > 0x10000000 {
174 Err(BlockError::TooManyTxs)
175 } else {
176 Ok(())
177 }
178}
179
180fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> {
184 if &block.header.previous == top_hash {
185 Ok(())
186 } else {
187 Err(BlockError::PreviousIDIncorrect)
188 }
189}
190
191pub fn check_timestamp(block: &Block, median_timestamp: Option<u64>) -> Result<(), BlockError> {
195 if block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT
196 || median_timestamp.is_some_and(|median| block.header.timestamp < median)
197 {
198 Err(BlockError::TimeStampInvalid)
199 } else {
200 Ok(())
201 }
202}
203
204fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> {
208 let set = txs.iter().collect::<HashSet<_>>();
209
210 if set.len() == txs.len() {
211 Ok(())
212 } else {
213 Err(BlockError::DuplicateTransaction)
214 }
215}
216
217#[derive(Debug, Clone, Eq, PartialEq)]
220pub struct ContextToVerifyBlock {
221 pub median_weight_for_block_reward: usize,
223 pub effective_median_weight: usize,
225 pub top_hash: [u8; 32],
227 pub median_block_timestamp: Option<u64>,
229 pub chain_height: usize,
231 pub current_hf: HardFork,
233 pub next_difficulty: u128,
235 pub already_generated_coins: u64,
237}
238
239pub fn check_block(
252 block: &Block,
253 total_fees: u64,
254 block_weight: usize,
255 block_blob_len: usize,
256 block_chain_ctx: &ContextToVerifyBlock,
257) -> Result<(HardFork, u64), BlockError> {
258 let (version, vote) =
259 HardFork::from_block_header(&block.header).map_err(|_| HardForkError::HardForkUnknown)?;
260
261 check_block_version_vote(&block_chain_ctx.current_hf, &version, &vote)?;
262
263 check_timestamp(block, block_chain_ctx.median_block_timestamp)?;
264
265 check_prev_id(block, &block_chain_ctx.top_hash)?;
266
267 check_block_weight(block_weight, block_chain_ctx.median_weight_for_block_reward)?;
268 block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?;
269
270 check_amount_txs(block.transactions.len())?;
271 check_txs_unique(&block.transactions)?;
272
273 let generated_coins = check_miner_tx(
274 block.miner_transaction(),
275 total_fees,
276 block_chain_ctx.chain_height,
277 block_weight,
278 block_chain_ctx.median_weight_for_block_reward,
279 block_chain_ctx.already_generated_coins,
280 block_chain_ctx.current_hf,
281 )?;
282
283 Ok((vote, generated_coins))
284}
285
286#[cfg(test)]
287mod tests {
288 use proptest::{collection::vec, prelude::*};
289
290 use super::*;
291
292 proptest! {
293 #[test]
294 fn test_check_unique_txs(
295 mut txs in vec(any::<[u8; 32]>(), 2..3000),
296 duplicate in any::<[u8; 32]>(),
297 dup_idx_1 in any::<usize>(),
298 dup_idx_2 in any::<usize>(),
299 ) {
300
301 prop_assert!(check_txs_unique(&txs).is_ok());
302
303 txs.insert(dup_idx_1 % txs.len(), duplicate);
304 txs.insert(dup_idx_2 % txs.len(), duplicate);
305
306 prop_assert!(check_txs_unique(&txs).is_err());
307 }
308 }
309}