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("Hard-fork error: {0}")]
42    HardForkError(#[from] HardForkError),
43    #[error("Miner transaction error: {0}")]
44    MinerTxError(#[from] MinerTxError),
45}
46
47/// A trait to represent the RandomX VM.
48pub trait RandomX {
49    type Error;
50
51    fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>;
52}
53
54/// Returns if this height is a RandomX seed height.
55pub const fn is_randomx_seed_height(height: usize) -> bool {
56    height.is_multiple_of(RX_SEEDHASH_EPOCH_BLOCKS)
57}
58
59/// Returns the RandomX seed height for this block.
60///
61/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#randomx-seed>
62pub 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
70/// Calculates the POW hash of this block.
71///
72/// `randomx_vm` must be [`Some`] after hf 12.
73///
74/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#pow-function>
75pub 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        // FIXME: https://github.com/Cuprate/cuprate/issues/167.
93        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
102/// Returns if the block's POW hash is valid for the current difficulty.
103///
104/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash>
105pub 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 bool::from(int_hash.checked_mul(&difficulty).is_none()) {
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
122/// Returns the penalty free zone
123///
124/// <https://cuprate.github.io/monero-book/consensus_rules/blocks/weight_limit.html#penalty-free-zone>
125pub 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
135/// Sanity check on the block blob size.
136///
137/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size>
138const 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
149/// Sanity check on the block weight.
150///
151/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size>
152pub 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
163/// Sanity check on number of txs in the block.
164///
165/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#amount-of-transactions>
166const 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
174/// Verifies the previous id is the last block's hash
175///
176/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#previous-id>
177fn 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
185/// Checks the block's timestamp is in the valid range.
186///
187/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#timestamp>
188pub fn check_timestamp(block: &Block, median_timestamp: Option<u64>) -> Result<(), BlockError> {
189    if block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT
190        || median_timestamp.is_some_and(|median| block.header.timestamp < median)
191    {
192        Err(BlockError::TimeStampInvalid)
193    } else {
194        Ok(())
195    }
196}
197
198/// Checks that all txs in the block have a unique hash.
199///
200/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks.html#no-duplicate-transactions>
201fn 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/// This struct contains the data needed to verify a block, implementers MUST make sure
212/// the data in this struct is calculated correctly.
213#[derive(Debug, Clone, Eq, PartialEq)]
214pub struct ContextToVerifyBlock {
215    /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#median-weight-for-coinbase-checks>
216    pub median_weight_for_block_reward: usize,
217    /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#effective-median-weight>
218    pub effective_median_weight: usize,
219    /// The top hash of the blockchain, aka the block hash of the previous block to the one we are verifying.
220    pub top_hash: [u8; 32],
221    /// Contains the median timestamp over the last 60 blocks, if there is less than 60 blocks this should be [`None`]
222    pub median_block_timestamp: Option<u64>,
223    /// The current chain height.
224    pub chain_height: usize,
225    /// The current hard-fork.
226    pub current_hf: HardFork,
227    /// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#calculating-difficulty>
228    pub next_difficulty: u128,
229    /// The amount of coins already minted.
230    pub already_generated_coins: u64,
231}
232
233/// Checks the block is valid returning the block's hard-fork `VOTE` and the amount of coins generated in this block.
234///
235/// This does not check the POW nor does it calculate the POW hash, this is because checking POW is very expensive and
236/// to allow the computation of the POW hashes to be done separately. This also does not check the transactions in the
237/// block are valid.
238///
239/// Missed block checks in this function:
240///
241/// <https://monero-book.cuprate.org/consensus_rules/blocks.html#key-images>
242/// <https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash>
243///
244///
245pub 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    check_timestamp(block, block_chain_ctx.median_block_timestamp)?;
258
259    check_prev_id(block, &block_chain_ctx.top_hash)?;
260
261    check_block_weight(block_weight, block_chain_ctx.median_weight_for_block_reward)?;
262    block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?;
263
264    check_amount_txs(block.transactions.len())?;
265    check_txs_unique(&block.transactions)?;
266
267    let generated_coins = check_miner_tx(
268        block.miner_transaction(),
269        total_fees,
270        block_chain_ctx.chain_height,
271        block_weight,
272        block_chain_ctx.median_weight_for_block_reward,
273        block_chain_ctx.already_generated_coins,
274        block_chain_ctx.current_hf,
275    )?;
276
277    Ok((vote, generated_coins))
278}
279
280#[cfg(test)]
281mod tests {
282    use proptest::{collection::vec, prelude::*};
283
284    use super::*;
285
286    proptest! {
287        #[test]
288        fn test_check_unique_txs(
289            mut txs in vec(any::<[u8; 32]>(), 2..3000),
290            duplicate in any::<[u8; 32]>(),
291            dup_idx_1 in any::<usize>(),
292            dup_idx_2 in any::<usize>(),
293        ) {
294
295            prop_assert!(check_txs_unique(&txs).is_ok());
296
297            txs.insert(dup_idx_1 % txs.len(), duplicate);
298            txs.insert(dup_idx_2 % txs.len(), duplicate);
299
300            prop_assert!(check_txs_unique(&txs).is_err());
301        }
302    }
303}