cuprate_consensus_rules/
blocks.rs

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
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 % RX_SEEDHASH_EPOCH_BLOCKS == 0
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 blocks 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 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
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 blocks 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 blocks 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: 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
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 blocks 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    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}