Skip to main content

cuprate_consensus_rules/
blocks.rs

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
53/// A trait to represent the RandomX VM.
54pub trait RandomX {
55    type Error;
56
57    fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>;
58}
59
60/// Returns if this height is a RandomX seed height.
61pub const fn is_randomx_seed_height(height: usize) -> bool {
62    height.is_multiple_of(RX_SEEDHASH_EPOCH_BLOCKS)
63}
64
65/// Returns the RandomX seed height for this block.
66///
67/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#randomx-seed>
68pub 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
76/// Calculates the POW hash of this block.
77///
78/// `randomx_vm` must be [`Some`] after hf 12.
79///
80/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#pow-function>
81pub 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        // FIXME: https://github.com/Cuprate/cuprate/issues/167.
99        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
108/// Returns if the block's POW hash is valid for the current difficulty.
109///
110/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash>
111pub 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
128/// Returns the penalty free zone
129///
130/// <https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#penalty-free-zone>
131pub 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
141/// Sanity check on the block blob size.
142///
143/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size>
144const 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
155/// Sanity check on the block weight.
156///
157/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size>
158pub 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
169/// Sanity check on number of txs in the block.
170///
171/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#amount-of-transactions>
172const 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
180/// Verifies the previous id is the last block's hash
181///
182/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#previous-id>
183fn 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
191/// Checks the block's timestamp is in the valid range.
192///
193/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#timestamp>
194pub 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
204/// Checks that all txs in the block have a unique hash.
205///
206/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#no-duplicate-transactions>
207fn 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/// This struct contains the data needed to verify a block, implementers MUST make sure
218/// the data in this struct is calculated correctly.
219#[derive(Debug, Clone, Eq, PartialEq)]
220pub struct ContextToVerifyBlock {
221    /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#median-weight-for-coinbase-checks>
222    pub median_weight_for_block_reward: usize,
223    /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#effective-median-weight>
224    pub effective_median_weight: usize,
225    /// The top hash of the blockchain, aka the block hash of the previous block to the one we are verifying.
226    pub top_hash: [u8; 32],
227    /// Contains the median timestamp over the last 60 blocks, if there is less than 60 blocks this should be [`None`]
228    pub median_block_timestamp: Option<u64>,
229    /// The current chain height.
230    pub chain_height: usize,
231    /// The current hard-fork.
232    pub current_hf: HardFork,
233    /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#calculating-difficulty>
234    pub next_difficulty: u128,
235    /// The amount of coins already minted.
236    pub already_generated_coins: u64,
237}
238
239/// Checks the block is valid returning the block's hard-fork `VOTE` and the amount of coins generated in this block.
240///
241/// This does not check the POW nor does it calculate the POW hash, this is because checking POW is very expensive and
242/// to allow the computation of the POW hashes to be done separately. This also does not check the transactions in the
243/// block are valid.
244///
245/// Missed block checks in this function:
246///
247/// <https://monero-book.cuprate.org/consensus_rules/blocks.html#key-images>
248/// <https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash>
249///
250///
251pub 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}