cuprate_consensus/
block.rs

1//! Block Verification.
2//!
3//! This module contains functions for verifying blocks:
4//! - [`verify_main_chain_block`]
5//! - [`batch_prepare_main_chain_blocks`]
6//! - [`verify_prepped_main_chain_block`]
7//! - [`sanity_check_alt_block`]
8//!
9use std::{collections::HashMap, mem};
10
11use monero_serai::{block::Block, transaction::Input};
12use tower::{Service, ServiceExt};
13
14use cuprate_consensus_context::{
15    BlockChainContextRequest, BlockChainContextResponse, BlockchainContextService,
16};
17use cuprate_helper::asynch::rayon_spawn_async;
18use cuprate_types::{
19    AltBlockInformation, TransactionVerificationData, VerifiedBlockInformation,
20    VerifiedTransactionInformation,
21};
22
23use cuprate_consensus_rules::{
24    blocks::{
25        calculate_pow_hash, check_block, check_block_pow, randomx_seed_height, BlockError, RandomX,
26    },
27    hard_forks::HardForkError,
28    miner_tx::MinerTxError,
29    ConsensusError, HardFork,
30};
31
32use crate::{transactions::start_tx_verification, Database, ExtendedConsensusError};
33
34mod alt_block;
35mod batch_prepare;
36mod free;
37
38pub use alt_block::sanity_check_alt_block;
39pub use batch_prepare::{batch_prepare_main_chain_blocks, BatchPrepareCache};
40use free::pull_ordered_transactions;
41
42/// A pre-prepared block with all data needed to verify it, except the block's proof of work.
43#[derive(Debug)]
44pub struct PreparedBlockExPow {
45    /// The block.
46    pub block: Block,
47    /// The serialised block's bytes.
48    pub block_blob: Vec<u8>,
49
50    /// The block's hard-fork vote.
51    pub hf_vote: HardFork,
52    /// The block's hard-fork version.
53    pub hf_version: HardFork,
54
55    /// The block's hash.
56    pub block_hash: [u8; 32],
57    /// The height of the block.
58    pub height: usize,
59
60    /// The weight of the block's miner transaction.
61    pub miner_tx_weight: usize,
62}
63
64impl PreparedBlockExPow {
65    /// Prepare a new block.
66    ///
67    /// # Errors
68    /// This errors if either the `block`'s:
69    /// - Hard-fork values are invalid
70    /// - Miner transaction is missing a miner input
71    pub fn new(block: Block) -> Result<Self, ConsensusError> {
72        let (hf_version, hf_vote) = HardFork::from_block_header(&block.header)
73            .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?;
74
75        let Some(Input::Gen(height)) = block.miner_transaction.prefix().inputs.first() else {
76            return Err(ConsensusError::Block(BlockError::MinerTxError(
77                MinerTxError::InputNotOfTypeGen,
78            )));
79        };
80
81        Ok(Self {
82            block_blob: block.serialize(),
83            hf_vote,
84            hf_version,
85
86            block_hash: block.hash(),
87            height: *height,
88
89            miner_tx_weight: block.miner_transaction.weight(),
90            block,
91        })
92    }
93}
94
95/// A pre-prepared block with all data needed to verify it.
96#[derive(Debug)]
97pub struct PreparedBlock {
98    /// The block
99    pub block: Block,
100    /// The serialised blocks bytes
101    pub block_blob: Vec<u8>,
102
103    /// The blocks hf vote
104    pub hf_vote: HardFork,
105    /// The blocks hf version
106    pub hf_version: HardFork,
107
108    /// The blocks hash
109    pub block_hash: [u8; 32],
110    /// The blocks POW hash.
111    pub pow_hash: [u8; 32],
112
113    /// The weight of the blocks miner transaction.
114    pub miner_tx_weight: usize,
115}
116
117impl PreparedBlock {
118    /// Creates a new [`PreparedBlock`].
119    ///
120    /// The randomX VM must be Some if RX is needed or this will panic.
121    /// The randomX VM must also be initialised with the correct seed.
122    pub fn new<R: RandomX>(block: Block, randomx_vm: Option<&R>) -> Result<Self, ConsensusError> {
123        let (hf_version, hf_vote) = HardFork::from_block_header(&block.header)
124            .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?;
125
126        let [Input::Gen(height)] = &block.miner_transaction.prefix().inputs[..] else {
127            return Err(ConsensusError::Block(BlockError::MinerTxError(
128                MinerTxError::InputNotOfTypeGen,
129            )));
130        };
131
132        Ok(Self {
133            block_blob: block.serialize(),
134            hf_vote,
135            hf_version,
136
137            block_hash: block.hash(),
138            pow_hash: calculate_pow_hash(
139                randomx_vm,
140                &block.serialize_pow_hash(),
141                *height,
142                &hf_version,
143            )?,
144
145            miner_tx_weight: block.miner_transaction.weight(),
146            block,
147        })
148    }
149
150    /// Creates a new [`PreparedBlock`] from a [`PreparedBlockExPow`].
151    ///
152    /// This function will give an invalid proof-of-work hash if `randomx_vm` is not initialised
153    /// with the correct seed.
154    ///
155    /// # Panics
156    /// This function will panic if `randomx_vm` is
157    /// [`None`] even though RandomX is needed.
158    fn new_prepped<R: RandomX>(
159        block: PreparedBlockExPow,
160        randomx_vm: Option<&R>,
161    ) -> Result<Self, ConsensusError> {
162        Ok(Self {
163            block_blob: block.block_blob,
164            hf_vote: block.hf_vote,
165            hf_version: block.hf_version,
166
167            block_hash: block.block_hash,
168            pow_hash: calculate_pow_hash(
169                randomx_vm,
170                &block.block.serialize_pow_hash(),
171                block.height,
172                &block.hf_version,
173            )?,
174
175            miner_tx_weight: block.block.miner_transaction.weight(),
176            block: block.block,
177        })
178    }
179
180    /// Creates a new [`PreparedBlock`] from an [`AltBlockInformation`].
181    pub fn new_alt_block(block: AltBlockInformation) -> Result<Self, ConsensusError> {
182        Ok(Self {
183            block_blob: block.block_blob,
184            hf_vote: HardFork::from_version(block.block.header.hardfork_version)
185                .map_err(|_| BlockError::HardForkError(HardForkError::HardForkUnknown))?,
186            hf_version: HardFork::from_vote(block.block.header.hardfork_signal),
187            block_hash: block.block_hash,
188            pow_hash: block.pow_hash,
189            miner_tx_weight: block.block.miner_transaction.weight(),
190            block: block.block,
191        })
192    }
193}
194
195/// Fully verify a block and all its transactions.
196pub async fn verify_main_chain_block<D>(
197    block: Block,
198    txs: HashMap<[u8; 32], TransactionVerificationData>,
199    context_svc: &mut BlockchainContextService,
200    database: D,
201) -> Result<VerifiedBlockInformation, ExtendedConsensusError>
202where
203    D: Database + Clone + Send + 'static,
204{
205    let context = context_svc.blockchain_context().clone();
206    tracing::debug!("got blockchain context: {:?}", context);
207
208    tracing::debug!(
209        "Preparing block for verification, expected height: {}",
210        context.chain_height
211    );
212
213    // Set up the block and just pass it to [`verify_prepped_main_chain_block`]
214
215    // We just use the raw `hardfork_version` here, no need to turn it into a `HardFork`.
216    let rx_vms = if block.header.hardfork_version < 12 {
217        HashMap::new()
218    } else {
219        let BlockChainContextResponse::RxVms(rx_vms) = context_svc
220            .ready()
221            .await?
222            .call(BlockChainContextRequest::CurrentRxVms)
223            .await?
224        else {
225            panic!("Blockchain context service returned wrong response!");
226        };
227
228        rx_vms
229    };
230
231    let height = context.chain_height;
232    let prepped_block = rayon_spawn_async(move || {
233        PreparedBlock::new(
234            block,
235            rx_vms.get(&randomx_seed_height(height)).map(AsRef::as_ref),
236        )
237    })
238    .await?;
239
240    check_block_pow(&prepped_block.pow_hash, context.next_difficulty)
241        .map_err(ConsensusError::Block)?;
242
243    // Check that the txs included are what we need and that there are not any extra.
244    let ordered_txs = pull_ordered_transactions(&prepped_block.block, txs)?;
245
246    verify_prepped_main_chain_block(prepped_block, ordered_txs, context_svc, database, None).await
247}
248
249/// Fully verify a block that has already been prepared using [`batch_prepare_main_chain_blocks`].
250pub async fn verify_prepped_main_chain_block<D>(
251    prepped_block: PreparedBlock,
252    mut txs: Vec<TransactionVerificationData>,
253    context_svc: &mut BlockchainContextService,
254    database: D,
255    batch_prep_cache: Option<&mut BatchPrepareCache>,
256) -> Result<VerifiedBlockInformation, ExtendedConsensusError>
257where
258    D: Database + Clone + Send + 'static,
259{
260    let context = context_svc.blockchain_context();
261
262    tracing::debug!("verifying block: {}", hex::encode(prepped_block.block_hash));
263
264    check_block_pow(&prepped_block.pow_hash, context.next_difficulty)
265        .map_err(ConsensusError::Block)?;
266
267    if prepped_block.block.transactions.len() != txs.len() {
268        return Err(ExtendedConsensusError::TxsIncludedWithBlockIncorrect);
269    }
270
271    if !prepped_block.block.transactions.is_empty() {
272        for (expected_tx_hash, tx) in prepped_block.block.transactions.iter().zip(txs.iter()) {
273            if expected_tx_hash != &tx.tx_hash {
274                return Err(ExtendedConsensusError::TxsIncludedWithBlockIncorrect);
275            }
276        }
277
278        let temp = start_tx_verification()
279            .append_prepped_txs(mem::take(&mut txs))
280            .prepare()?
281            .full(
282                context.chain_height,
283                context.top_hash,
284                context.current_adjusted_timestamp_for_time_lock(),
285                context.current_hf,
286                database,
287                batch_prep_cache.as_deref(),
288            )
289            .verify()
290            .await?;
291
292        txs = temp;
293    }
294
295    let block_weight =
296        prepped_block.miner_tx_weight + txs.iter().map(|tx| tx.tx_weight).sum::<usize>();
297    let total_fees = txs.iter().map(|tx| tx.fee).sum::<u64>();
298
299    tracing::debug!("Verifying block header.");
300    let (_, generated_coins) = check_block(
301        &prepped_block.block,
302        total_fees,
303        block_weight,
304        prepped_block.block_blob.len(),
305        &context.context_to_verify_block,
306    )
307    .map_err(ConsensusError::Block)?;
308
309    let block = VerifiedBlockInformation {
310        block_hash: prepped_block.block_hash,
311        block: prepped_block.block,
312        block_blob: prepped_block.block_blob,
313        txs: txs
314            .into_iter()
315            .map(|tx| VerifiedTransactionInformation {
316                tx_blob: tx.tx_blob,
317                tx_weight: tx.tx_weight,
318                fee: tx.fee,
319                tx_hash: tx.tx_hash,
320                tx: tx.tx,
321            })
322            .collect(),
323        pow_hash: prepped_block.pow_hash,
324        generated_coins,
325        weight: block_weight,
326        height: context.chain_height,
327        long_term_weight: context.next_block_long_term_weight(block_weight),
328        cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty,
329    };
330
331    if let Some(batch_prep_cache) = batch_prep_cache {
332        batch_prep_cache.output_cache.add_block_to_cache(&block);
333    }
334
335    Ok(block)
336}