1use std::collections::HashSet;
2
3use crypto_bigint::{CheckedMul, U256};
4use monero_serai::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 blocks 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 blocks previous ID is incorrect.")]
36 PreviousIDIncorrect,
37 #[error("The blocks timestamp is invalid.")]
38 TimeStampInvalid,
39 #[error("The block contains a duplicate transaction.")]
40 DuplicateTransaction,
41 #[error("Hard-fork error: {0}")]
42 HardForkError(#[from] HardForkError),
43 #[error("Miner transaction error: {0}")]
44 MinerTxError(#[from] MinerTxError),
45}
46
47pub trait RandomX {
49 type Error;
50
51 fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>;
52}
53
54pub const fn is_randomx_seed_height(height: usize) -> bool {
56 height % RX_SEEDHASH_EPOCH_BLOCKS == 0
57}
58
59pub const fn randomx_seed_height(height: usize) -> usize {
63 if height <= RX_SEEDHASH_EPOCH_BLOCKS + RX_SEEDHASH_EPOCH_LAG {
64 0
65 } else {
66 (height - RX_SEEDHASH_EPOCH_LAG - 1) & !(RX_SEEDHASH_EPOCH_BLOCKS - 1)
67 }
68}
69
70pub fn calculate_pow_hash<R: RandomX>(
76 randomx_vm: Option<&R>,
77 buf: &[u8],
78 height: usize,
79 hf: &HardFork,
80) -> Result<[u8; 32], BlockError> {
81 if height == 202612 {
82 return Ok(BLOCK_202612_POW_HASH);
83 }
84
85 Ok(if hf < &HardFork::V7 {
86 cryptonight_hash_v0(buf)
87 } else if hf == &HardFork::V7 {
88 cryptonight_hash_v1(buf).map_err(|_| BlockError::POWInvalid)?
89 } else if hf < &HardFork::V10 {
90 cryptonight_hash_v2(buf)
91 } else if hf < &HardFork::V12 {
92 cryptonight_hash_r(buf, height as u64)
94 } else {
95 randomx_vm
96 .expect("RandomX VM needed from hf 12")
97 .calculate_hash(buf)
98 .map_err(|_| BlockError::POWInvalid)?
99 })
100}
101
102pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> {
106 let int_hash = U256::from_le_slice(hash);
107
108 let difficulty = U256::from(difficulty);
109
110 if int_hash.checked_mul(&difficulty).is_none().unwrap_u8() == 1 {
111 tracing::debug!(
112 "Invalid POW: {}, difficulty: {}",
113 hex::encode(hash),
114 difficulty
115 );
116 Err(BlockError::POWInvalid)
117 } else {
118 Ok(())
119 }
120}
121
122pub fn penalty_free_zone(hf: HardFork) -> usize {
126 if hf == HardFork::V1 {
127 PENALTY_FREE_ZONE_1
128 } else if hf >= HardFork::V2 && hf < HardFork::V5 {
129 PENALTY_FREE_ZONE_2
130 } else {
131 PENALTY_FREE_ZONE_5
132 }
133}
134
135const fn block_size_sanity_check(
139 block_blob_len: usize,
140 effective_median: usize,
141) -> Result<(), BlockError> {
142 if block_blob_len > effective_median * 2 + BLOCK_SIZE_SANITY_LEEWAY {
143 Err(BlockError::TooLarge)
144 } else {
145 Ok(())
146 }
147}
148
149pub const fn check_block_weight(
153 block_weight: usize,
154 median_for_block_reward: usize,
155) -> Result<(), BlockError> {
156 if block_weight > median_for_block_reward * 2 {
157 Err(BlockError::TooLarge)
158 } else {
159 Ok(())
160 }
161}
162
163const fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> {
167 if number_none_miner_txs + 1 > 0x10000000 {
168 Err(BlockError::TooManyTxs)
169 } else {
170 Ok(())
171 }
172}
173
174fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> {
178 if &block.header.previous == top_hash {
179 Ok(())
180 } else {
181 Err(BlockError::PreviousIDIncorrect)
182 }
183}
184
185pub fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> {
189 if block.header.timestamp < median_timestamp
190 || block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT
191 {
192 Err(BlockError::TimeStampInvalid)
193 } else {
194 Ok(())
195 }
196}
197
198fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> {
202 let set = txs.iter().collect::<HashSet<_>>();
203
204 if set.len() == txs.len() {
205 Ok(())
206 } else {
207 Err(BlockError::DuplicateTransaction)
208 }
209}
210
211#[derive(Debug, Clone, Eq, PartialEq)]
214pub struct ContextToVerifyBlock {
215 pub median_weight_for_block_reward: usize,
217 pub effective_median_weight: usize,
219 pub top_hash: [u8; 32],
221 pub median_block_timestamp: Option<u64>,
223 pub chain_height: usize,
225 pub current_hf: HardFork,
227 pub next_difficulty: u128,
229 pub already_generated_coins: u64,
231}
232
233pub fn check_block(
246 block: &Block,
247 total_fees: u64,
248 block_weight: usize,
249 block_blob_len: usize,
250 block_chain_ctx: &ContextToVerifyBlock,
251) -> Result<(HardFork, u64), BlockError> {
252 let (version, vote) =
253 HardFork::from_block_header(&block.header).map_err(|_| HardForkError::HardForkUnknown)?;
254
255 check_block_version_vote(&block_chain_ctx.current_hf, &version, &vote)?;
256
257 if let Some(median_timestamp) = block_chain_ctx.median_block_timestamp {
258 check_timestamp(block, median_timestamp)?;
259 }
260
261 check_prev_id(block, &block_chain_ctx.top_hash)?;
262
263 check_block_weight(block_weight, block_chain_ctx.median_weight_for_block_reward)?;
264 block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?;
265
266 check_amount_txs(block.transactions.len())?;
267 check_txs_unique(&block.transactions)?;
268
269 let generated_coins = check_miner_tx(
270 &block.miner_transaction,
271 total_fees,
272 block_chain_ctx.chain_height,
273 block_weight,
274 block_chain_ctx.median_weight_for_block_reward,
275 block_chain_ctx.already_generated_coins,
276 block_chain_ctx.current_hf,
277 )?;
278
279 Ok((vote, generated_coins))
280}
281
282#[cfg(test)]
283mod tests {
284 use proptest::{collection::vec, prelude::*};
285
286 use super::*;
287
288 proptest! {
289 #[test]
290 fn test_check_unique_txs(
291 mut txs in vec(any::<[u8; 32]>(), 2..3000),
292 duplicate in any::<[u8; 32]>(),
293 dup_idx_1 in any::<usize>(),
294 dup_idx_2 in any::<usize>(),
295 ) {
296
297 prop_assert!(check_txs_unique(&txs).is_ok());
298
299 txs.insert(dup_idx_1 % txs.len(), duplicate);
300 txs.insert(dup_idx_2 % txs.len(), duplicate);
301
302 prop_assert!(check_txs_unique(&txs).is_err());
303 }
304 }
305}